1use frus_core::{
5 AlignmentGeometry, Border, BorderRadius, BoxBorder, BoxDecoration, BoxShadow, BoxShape, Color,
6 Curve, DecorationImage, Gradient, Insets, InsetsGeometry, LinearGradient, Rect, Scene, Size,
7};
8use frus_layout::{Align, Dimension, Justify, Style};
9
10use crate::interaction::Status;
11use crate::theme::Theme;
12use crate::widget::Widget;
13
14fn ease(t: f32) -> f32 {
16 let t = t.clamp(0.0, 1.0);
17 t * t * (3.0 - 2.0 * t)
18}
19
20pub struct Container<Msg = crate::callback::Callback> {
22 width: Dimension,
23 height: Dimension,
24 flex_grow: f32,
25 flex_shrink: f32,
26 padding: InsetsGeometry,
27 margin: InsetsGeometry,
29 radius: BorderRadius,
30 clip: bool,
32 foreground: Option<BoxDecoration>,
35 border: Option<BoxBorder>,
37 color: Option<Color>,
38 hover_color: Option<Color>,
39 pressed_color: Option<Color>,
40 towards: Option<(Color, [f32; 2])>,
43 gradient: Option<Gradient>,
45 shadows: Vec<BoxShadow>,
47 shape: BoxShape,
49 image: Option<DecorationImage>,
51 background_blend_mode: Option<frus_core::BlendMode>,
53 on_click: Option<Msg>,
54 on_long_press: Option<Msg>,
55 repaint_boundary: bool,
58 opacity: Option<f32>,
60 opacity_anim: Option<(f32, Curve)>,
63 color_anim: Option<(Color, f32, Curve)>,
66 size_anim: Option<(Size, f32, Curve)>,
69 radius_anim: Option<(BorderRadius, f32, Curve)>,
72 padding_anim: Option<(f32, Curve)>,
75 alignment_anim: Option<(f32, Curve)>,
77 alignment: Option<AlignmentGeometry>,
81 reference_sizing: bool,
85 limits: [Option<f32>; 4],
88 shift: Option<(f32, f32)>,
90 scale: Option<(f32, f32)>,
91 turn: Option<f32>,
92 pivot: frus_core::Alignment,
94 children: Vec<Box<dyn Widget<Msg>>>,
95}
96
97impl<Msg> Container<Msg> {
98 fn reference_fill(&self) -> crate::widget::FillAxes {
101 let fills = self.reference_sizing && (self.children.is_empty() || self.alignment.is_some());
102 crate::widget::FillAxes {
103 horizontal: fills && matches!(self.width, Dimension::Auto),
104 vertical: fills && matches!(self.height, Dimension::Auto),
105 }
106 }
107
108 pub(crate) fn hugging(mut self) -> Self {
112 self.reference_sizing = false;
113 self
114 }
115
116 #[must_use]
120 pub fn min_width(mut self, width: f32) -> Self {
121 self.limits[0] = Some(width);
122 self
123 }
124
125 #[must_use]
127 pub fn max_width(mut self, width: f32) -> Self {
128 self.limits[1] = Some(width);
129 self
130 }
131
132 #[must_use]
134 pub fn min_height(mut self, height: f32) -> Self {
135 self.limits[2] = Some(height);
136 self
137 }
138
139 #[must_use]
141 pub fn max_height(mut self, height: f32) -> Self {
142 self.limits[3] = Some(height);
143 self
144 }
145
146 #[must_use]
149 pub fn translated(mut self, dx: f32, dy: f32) -> Self {
150 self.shift = Some((dx, dy));
151 self
152 }
153
154 #[must_use]
157 pub fn scaled(mut self, sx: f32, sy: f32) -> Self {
158 self.scale = Some((sx, sy));
159 self
160 }
161
162 #[must_use]
165 pub fn rotated(mut self, radians: f32) -> Self {
166 self.turn = Some(radians);
167 self
168 }
169
170 #[must_use]
172 pub fn transform_alignment(mut self, pivot: frus_core::Alignment) -> Self {
173 self.pivot = pivot;
174 self
175 }
176
177 pub fn new() -> Self {
179 Self {
180 width: Dimension::Auto,
181 height: Dimension::Auto,
182 flex_grow: 0.0,
183 flex_shrink: 0.0,
184 padding: InsetsGeometry::Physical(Insets::ZERO),
185 margin: InsetsGeometry::Physical(Insets::ZERO),
186 radius: BorderRadius::ZERO,
187 clip: false,
188 foreground: None,
189 border: None,
190 color: None,
191 hover_color: None,
192 pressed_color: None,
193 towards: None,
194 gradient: None,
195 shadows: Vec::new(),
196 shape: BoxShape::Rectangle,
197 image: None,
198 background_blend_mode: None,
199 on_click: None,
200 on_long_press: None,
201 repaint_boundary: false,
202 opacity: None,
203 opacity_anim: None,
204 color_anim: None,
205 size_anim: None,
206 radius_anim: None,
207 padding_anim: None,
208 alignment_anim: None,
209 alignment: None,
210 reference_sizing: true,
211 limits: [None; 4],
212 shift: None,
213 scale: None,
214 turn: None,
215 pivot: frus_core::Alignment::CENTER,
216 children: Vec::new(),
217 }
218 }
219
220 fn effective_padding(&self) -> Insets {
225 self.effective_padding_in(frus_core::TextDirection::Ltr)
226 }
227
228 fn effective_padding_in(&self, direction: frus_core::TextDirection) -> Insets {
231 let mut padding = self.padding.laid_out(direction);
232 if let Some(border) = self.border {
234 let w = border.resolve(direction).dimensions();
235 padding.top += w.top;
236 padding.right += w.right;
237 padding.bottom += w.bottom;
238 padding.left += w.left;
239 }
240 padding
241 }
242
243 pub fn repaint_boundary(mut self) -> Self {
248 self.repaint_boundary = true;
249 self
250 }
251
252 pub fn width(mut self, width: f32) -> Self {
254 self.width = Dimension::Length(width);
255 self
256 }
257
258 pub fn height(mut self, height: f32) -> Self {
260 self.height = Dimension::Length(height);
261 self
262 }
263
264 pub fn width_fraction(mut self, fraction: f32) -> Self {
266 self.width = Dimension::Percent(fraction);
267 self
268 }
269
270 pub fn flex(mut self, grow: f32) -> Self {
272 self.flex_grow = grow;
273 self
274 }
275
276 pub fn shrink(mut self, shrink: f32) -> Self {
285 self.flex_shrink = shrink;
286 self
287 }
288
289 pub fn no_shrink(self) -> Self {
292 self.shrink(0.0)
293 }
294
295 pub fn padding(mut self, padding: f32) -> Self {
297 self.padding = InsetsGeometry::Physical(Insets::uniform(padding));
298 self
299 }
300
301 pub fn padding_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
303 self.padding = InsetsGeometry::Physical(Insets::new(top, right, bottom, left));
304 self
305 }
306
307 pub fn padding_insets(mut self, padding: impl Into<InsetsGeometry>) -> Self {
311 self.padding = padding.into();
312 self
313 }
314
315 pub fn margin_insets(mut self, margin: impl Into<InsetsGeometry>) -> Self {
318 self.margin = margin.into();
319 self
320 }
321
322 pub fn margin(mut self, margin: f32) -> Self {
326 self.margin = InsetsGeometry::Physical(Insets::uniform(margin));
327 self
328 }
329
330 pub fn margin_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
332 self.margin = InsetsGeometry::Physical(Insets::new(top, right, bottom, left));
333 self
334 }
335
336 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
339 self.radius = radius.into();
340 self
341 }
342
343 pub fn clip(mut self) -> Self {
353 self.clip = true;
354 self
355 }
356
357 pub fn border(mut self, width: f32, color: Color) -> Self {
359 self.border = (width > 0.0).then(|| Border::new(width, color).into());
360 self
361 }
362
363 pub fn box_border(mut self, border: impl Into<BoxBorder>) -> Self {
367 self.border = Some(border.into());
368 self
369 }
370
371 pub fn shape(mut self, shape: BoxShape) -> Self {
374 self.shape = shape;
375 self
376 }
377
378 pub fn shadows(mut self, shadows: impl IntoIterator<Item = BoxShadow>) -> Self {
380 self.shadows = shadows.into_iter().collect();
381 self
382 }
383
384 pub fn color(mut self, color: Color) -> Self {
386 self.color = Some(color);
387 self
388 }
389
390 pub fn hover_color(mut self, color: Color) -> Self {
392 self.hover_color = Some(color);
393 self
394 }
395
396 pub fn pressed_color(mut self, color: Color) -> Self {
398 self.pressed_color = Some(color);
399 self
400 }
401
402 pub fn gradient(mut self, end: Color, dir: [f32; 2]) -> Self {
405 self.towards = Some((end, dir));
406 self
407 }
408
409 pub fn image(mut self, image: DecorationImage) -> Self {
412 self.image = Some(image);
413 self
414 }
415
416 pub fn background_blend_mode(mut self, mode: frus_core::BlendMode) -> Self {
420 self.background_blend_mode = Some(mode);
421 self
422 }
423
424 pub fn shadow(mut self, dx: f32, dy: f32, blur: f32, color: Color) -> Self {
427 self.shadows.push(BoxShadow::new(dx, dy, blur, color));
428 self
429 }
430
431 pub fn on_click(mut self, message: impl Into<Msg>) -> Self {
433 self.on_click = Some(message.into());
434 self
435 }
436
437 pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
440 self.on_long_press = Some(message.into());
441 self
442 }
443
444 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
446 self.children.clear();
447 self.children.push(Box::new(child));
448 self
449 }
450
451 pub fn opacity(mut self, opacity: f32) -> Self {
455 self.opacity = Some(opacity.clamp(0.0, 1.0));
456 self
457 }
458
459 pub fn animated_opacity(mut self, opacity: f32, duration: f32, curve: Curve) -> Self {
463 self.opacity = Some(opacity.clamp(0.0, 1.0));
464 self.opacity_anim = Some((duration, curve));
465 self
466 }
467
468 pub fn animated_color(mut self, color: Color, duration: f32, curve: Curve) -> Self {
473 self.color = Some(color);
474 self.color_anim = Some((color, duration, curve));
475 self
476 }
477
478 pub fn animated_size(mut self, width: f32, height: f32, duration: f32, curve: Curve) -> Self {
483 self.width = Dimension::Length(width);
484 self.height = Dimension::Length(height);
485 self.size_anim = Some((Size::new(width, height), duration, curve));
486 self
487 }
488
489 pub fn animated_radius(
494 mut self,
495 radius: impl Into<BorderRadius>,
496 duration: f32,
497 curve: Curve,
498 ) -> Self {
499 let radius = radius.into();
500 self.radius = radius;
501 self.radius_anim = Some((radius, duration, curve));
502 self
503 }
504
505 pub fn animated_padding(mut self, padding: f32, duration: f32, curve: Curve) -> Self {
510 self.padding = InsetsGeometry::Physical(Insets::uniform(padding));
511 self.padding_anim = Some((duration, curve));
512 self
513 }
514
515 pub fn animated_alignment(
522 mut self,
523 alignment: impl Into<AlignmentGeometry>,
524 duration: f32,
525 curve: Curve,
526 ) -> Self {
527 self.alignment = Some(alignment.into());
528 self.alignment_anim = Some((duration, curve));
529 self
530 }
531
532 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
540 self.alignment = Some(alignment.into());
541 self
542 }
543
544 pub fn foreground(mut self, decoration: BoxDecoration) -> Self {
572 self.foreground = Some(decoration);
573 self
574 }
575
576 pub fn decoration(mut self, decoration: BoxDecoration) -> Self {
580 if let Some(color) = decoration.color {
581 self.color = Some(color);
582 }
583 if let Some(gradient) = decoration.gradient {
584 self.gradient = Some(gradient);
585 }
586 if let Some(image) = decoration.image {
587 self.image = Some(image);
588 }
589 if let Some(mode) = decoration.background_blend_mode {
590 self.background_blend_mode = Some(mode);
591 }
592 if let Some(border) = decoration.border {
593 self.border = Some(border);
594 }
595 self.radius = decoration.radius;
596 if !decoration.shadows.is_empty() {
597 self.shadows = decoration.shadows;
598 }
599 self.shape = decoration.shape;
600 self
601 }
602}
603
604impl<Msg> Default for Container<Msg> {
605 fn default() -> Self {
606 Self::new()
607 }
608}
609
610impl<Msg> Container<Msg> {
611 fn style_in(&self, direction: frus_core::TextDirection) -> Style {
613 let mut style = Style {
614 width: self.width,
615 height: self.height,
616 flex_grow: self.flex_grow,
617 flex_shrink: self.flex_shrink,
618 padding: self.effective_padding_in(direction),
619 margin: self.margin.laid_out(direction),
620 ..Default::default()
621 };
622 let limit = |v: Option<f32>| v.map_or(Dimension::Auto, Dimension::Length);
623 style.min_width = limit(self.limits[0]);
624 style.max_width = limit(self.limits[1]);
625 style.min_height = limit(self.limits[2]);
626 style.max_height = limit(self.limits[3]);
627 if self.alignment.is_some() {
632 style.justify = Justify::Start;
633 style.align = Align::Start;
634 }
635 style
636 }
637}
638
639impl<Msg: Clone> Widget<Msg> for Container<Msg> {
640 fn style(&self) -> Style {
641 self.style_in(frus_core::TextDirection::Ltr)
642 }
643
644 fn style_themed(&self, theme: &Theme) -> Style {
647 self.style_in(theme.direction)
648 }
649
650 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
651 &self.children
652 }
653
654 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
655 let color = if self.color_anim.is_some() {
662 status.anim_color.or(self.color)
663 } else {
664 let settled = match (self.color, self.hover_color) {
665 (Some(base), Some(hover)) => Some(base.lerp(hover, ease(status.hover_progress))),
666 _ => self.color,
667 };
668 let press = ease(status.press_progress.clamp(0.0, 1.0));
669 match (settled, self.pressed_color.or(self.hover_color)) {
670 (Some(from), Some(held)) => Some(from.lerp(held, press)),
671 (None, held) if press > 0.0 => held,
674 (settled, _) => settled,
675 }
676 };
677
678 let radius = if self.radius_anim.is_some() {
683 status.anim_radius.unwrap_or(self.radius)
684 } else {
685 self.radius
686 };
687
688 let gradient = self.gradient.clone().or_else(|| {
691 let (end, dir) = self.towards?;
692 let start = color?;
693 Some(Gradient::from(
694 LinearGradient::new(vec![start, end]).between(
695 frus_core::Alignment::new(-dir[0], -dir[1]),
696 frus_core::Alignment::new(dir[0], dir[1]),
697 ),
698 ))
699 });
700 let decoration = BoxDecoration {
701 color,
702 gradient,
703 border: self.border,
704 radius,
705 shadows: self.shadows.clone(),
706 shape: self.shape,
707 image: self.image.clone(),
708 background_blend_mode: self.background_blend_mode,
709 };
710 decoration.paint_into_in(scene, bounds, status.opacity, theme.direction);
711 }
712
713 fn clip_shape(&self) -> Option<frus_core::ClipShape> {
714 self.clip.then(|| match self.shape {
715 BoxShape::Rectangle => frus_core::ClipShape::RRect(self.radius.clamped()),
716 BoxShape::Circle => frus_core::ClipShape::Oval,
718 })
719 }
720
721 fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
722 self.foreground.clone().map(|decoration| {
723 if decoration.radius == frus_core::BorderRadius::ZERO {
724 decoration.radius(self.radius)
725 } else {
726 decoration
727 }
728 })
729 }
730
731 fn on_click(&self) -> Option<Msg> {
732 self.on_click.clone()
733 }
734
735 fn on_long_press(&self) -> Option<Msg> {
736 self.on_long_press.clone()
737 }
738
739 fn repaint_boundary(&self) -> bool {
740 self.repaint_boundary
741 }
742
743 fn opacity_group(&self) -> Option<f32> {
744 self.opacity
745 }
746
747 fn anim_target(&self) -> Option<f32> {
750 self.opacity_anim.as_ref().and(self.opacity)
751 }
752
753 fn anim_color(&self) -> Option<Color> {
754 self.color_anim.as_ref().and(self.color)
755 }
756
757 fn anim_size(&self) -> Option<Size> {
758 self.size_anim.as_ref().map(|(s, _, _)| *s)
759 }
760
761 fn anim_radius(&self) -> Option<BorderRadius> {
762 self.radius_anim.as_ref().map(|(r, _, _)| *r)
763 }
764
765 fn anim_padding(&self) -> Option<Insets> {
766 self.padding_anim.as_ref().map(|_| self.effective_padding())
768 }
769
770 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
771 self.alignment
772 }
773
774 fn transform_translate(&self) -> Option<(f32, f32)> {
775 self.shift
776 }
777
778 fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
779 self.scale.map(|(sx, sy)| (sx, sy, self.pivot))
780 }
781
782 fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
783 self.turn.map(|r| (r, self.pivot))
784 }
785
786 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
793 self.reference_fill()
794 }
795
796 fn anim_offset(&self) -> Option<(f32, f32)> {
797 self.alignment_anim
800 .as_ref()
801 .and(self.alignment)
802 .map(|a| a.fractions())
803 }
804
805 fn anim_duration(&self) -> f32 {
806 self.opacity_anim
809 .as_ref()
810 .map(|(d, _)| *d)
811 .or(self.color_anim.as_ref().map(|(_, d, _)| *d))
812 .or(self.size_anim.as_ref().map(|(_, d, _)| *d))
813 .or(self.radius_anim.as_ref().map(|(_, d, _)| *d))
814 .or(self.padding_anim.as_ref().map(|(d, _)| *d))
815 .or(self.alignment_anim.as_ref().map(|(d, _)| *d))
816 .unwrap_or(crate::runtime::ANIM_DURATION)
817 }
818
819 fn anim_curve(&self) -> Curve {
820 self.opacity_anim
821 .as_ref()
822 .map(|(_, c)| c.clone())
823 .or(self.color_anim.as_ref().map(|(_, _, c)| c.clone()))
824 .or(self.size_anim.as_ref().map(|(_, _, c)| c.clone()))
825 .or(self.radius_anim.as_ref().map(|(_, _, c)| c.clone()))
826 .or(self.padding_anim.as_ref().map(|(_, c)| c.clone()))
827 .or(self.alignment_anim.as_ref().map(|(_, c)| c.clone()))
828 .unwrap_or(Curve::Linear)
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835
836 #[test]
844 fn a_box_in_an_over_budget_row_keeps_its_width() {
845 use crate::interaction::WidgetId;
846 use crate::runtime::Runtime;
847 use crate::theme::Theme;
848 use crate::Flex;
849
850 let widths = |row: &Flex<()>| -> Vec<f32> {
851 let runtime = Runtime::default();
852 let theme = Theme::light();
853 let mut layout = frus_layout::Layout::new();
854 let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
855 layout.compute_filled(node, 100.0, 50.0);
856 layout
857 .absolute_rects(node)
858 .iter()
859 .skip(1)
860 .take(2)
861 .map(|(r, _)| r.width)
862 .collect()
863 };
864
865 let row = Flex::row()
868 .child(Container::<()>::new().width(80.0).height(20.0))
869 .child(Container::<()>::new().width(40.0).height(20.0));
870 assert_eq!(
871 widths(&row),
872 vec![80.0, 40.0],
873 "an inflexible child is never squeezed"
874 );
875
876 let giving_way = Flex::row()
878 .child(Container::<()>::new().width(80.0).height(20.0).shrink(1.0))
879 .child(Container::<()>::new().width(40.0).height(20.0));
880 assert_eq!(widths(&giving_way), vec![60.0, 40.0]);
881 }
882
883 #[test]
888 fn a_held_box_crosses_over_to_its_pressed_colour() {
889 use frus_core::Primitive;
890
891 let rest = Color::rgb(0.2, 0.2, 0.2);
892 let held = Color::rgb(0.8, 0.8, 0.8);
893 let box_: Container<()> = Container::new()
894 .width(20.0)
895 .height(20.0)
896 .color(rest)
897 .pressed_color(held);
898
899 let fill = |press: f32| {
900 let mut scene = Scene::new();
901 Widget::<()>::paint(
902 &box_,
903 Rect::new(0.0, 0.0, 20.0, 20.0),
904 Status {
905 press_progress: press,
906 ..Default::default()
907 },
908 &crate::Theme::dark(),
909 &mut scene,
910 );
911 scene
912 .primitives()
913 .iter()
914 .find_map(|p| match p {
915 Primitive::Rect { color, .. } => Some(*color),
916 _ => None,
917 })
918 .expect("a box paints its background")
919 };
920
921 assert_eq!(fill(0.0), rest, "untouched");
922 assert_eq!(fill(1.0), held, "held");
923 let half = fill(0.5).r;
924 assert!(
925 half > rest.r && half < held.r,
926 "half way through the press: {half}"
927 );
928 }
929
930 #[test]
933 fn opacity_group_wraps_subtree_in_a_layer() {
934 use frus_core::{Primitive, Size};
935 let root: Container<()> = Container::new()
936 .width(40.0)
937 .height(40.0)
938 .color(Color::rgb(1.0, 0.0, 0.0))
939 .opacity(0.5);
940 let rt = crate::runtime::Runtime::default();
941 let theme = crate::Theme::dark();
942 let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
943 let layer = ui.scene().primitives().iter().find_map(|p| match p {
944 Primitive::Layer {
945 opacity,
946 primitives,
947 ..
948 } => Some((*opacity, primitives.len())),
949 _ => None,
950 });
951 let (op, n) = layer.expect("a group opacity layer");
952 assert!((op - 0.5).abs() < 1e-6, "group opacity = {op}");
953 assert!(
954 n >= 1,
955 "the layer wraps the painted content ({n} primitives)"
956 );
957 }
958
959 #[test]
961 fn full_opacity_emits_no_layer() {
962 use frus_core::{Primitive, Size};
963 let root: Container<()> = Container::new()
964 .width(40.0)
965 .height(40.0)
966 .color(Color::rgb(1.0, 0.0, 0.0))
967 .opacity(1.0);
968 let rt = crate::runtime::Runtime::default();
969 let theme = crate::Theme::dark();
970 let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
971 assert!(
972 !ui.scene()
973 .primitives()
974 .iter()
975 .any(|p| matches!(p, Primitive::Layer { .. })),
976 "no layer at full opacity"
977 );
978 }
979
980 #[test]
983 fn animated_opacity_declares_anim_target() {
984 let animated: Container<()> = Container::new().animated_opacity(0.0, 0.3, Curve::ease_in());
985 assert_eq!(Widget::<()>::anim_target(&animated), Some(0.0));
986 assert_eq!(Widget::<()>::anim_duration(&animated), 0.3);
987 assert_eq!(Widget::<()>::anim_curve(&animated), Curve::ease_in());
988 assert_eq!(Widget::<()>::opacity_group(&animated), Some(0.0));
989
990 let fixed: Container<()> = Container::new().opacity(0.5);
992 assert_eq!(Widget::<()>::anim_target(&fixed), None);
993 assert_eq!(Widget::<()>::opacity_group(&fixed), Some(0.5));
994 }
995
996 #[test]
1000 fn animated_color_paints_the_interpolated_color() {
1001 use frus_core::{Primitive, Size};
1002 let red = Color::rgb(1.0, 0.0, 0.0);
1003 let blue = Color::rgb(0.0, 0.0, 1.0);
1004 let mut rt = crate::runtime::Runtime::default();
1005 let start: Container<()> =
1006 Container::new()
1007 .width(20.0)
1008 .height(20.0)
1009 .animated_color(red, 0.10, Curve::Linear);
1010 rt.advance_colors(&start, 1.0); let to_blue: Container<()> =
1012 Container::new()
1013 .width(20.0)
1014 .height(20.0)
1015 .animated_color(blue, 0.10, Curve::Linear);
1016 rt.advance_colors(&to_blue, 0.05); let theme = crate::Theme::dark();
1019 let ui = crate::ui::build_ui(&to_blue, Size::new(20.0, 20.0), &rt, &theme);
1020 let color = ui
1021 .scene()
1022 .primitives()
1023 .iter()
1024 .find_map(|p| match p {
1025 Primitive::Rect { color, .. } => Some(*color),
1026 _ => None,
1027 })
1028 .expect("un rectangle de fond");
1029 assert!(
1030 (color.r - 0.5).abs() < 0.1 && (color.b - 0.5).abs() < 0.1,
1031 "the interpolated color is painted: {color:?}"
1032 );
1033 }
1034
1035 #[test]
1039 fn animated_size_drives_the_layout() {
1040 use frus_core::{Primitive, Size};
1041 let mut rt = crate::runtime::Runtime::default();
1042 let red = Color::rgb(1.0, 0.0, 0.0);
1043 let start: Container<()> =
1044 Container::new()
1045 .color(red)
1046 .animated_size(20.0, 20.0, 0.10, Curve::Linear);
1047 rt.advance_sizes(&start, 1.0); let to_big: Container<()> =
1049 Container::new()
1050 .color(red)
1051 .animated_size(40.0, 40.0, 0.10, Curve::Linear);
1052 rt.advance_sizes(&to_big, 0.05); let theme = crate::Theme::dark();
1055 let ui = crate::ui::build_ui(&to_big, Size::new(100.0, 100.0), &rt, &theme);
1056 let rect = ui
1057 .scene()
1058 .primitives()
1059 .iter()
1060 .find_map(|p| match p {
1061 Primitive::Rect { rect, .. } => Some(*rect),
1062 _ => None,
1063 })
1064 .expect("un rectangle de fond");
1065 assert!(
1066 (rect.width - 30.0).abs() < 1.0,
1067 "interpolated width: {}",
1068 rect.width
1069 );
1070 assert!(
1071 (rect.height - 30.0).abs() < 1.0,
1072 "interpolated height: {}",
1073 rect.height
1074 );
1075 }
1076
1077 #[test]
1080 fn animated_radius_paints_the_interpolated_radius() {
1081 use frus_core::{Primitive, Size};
1082 let red = Color::rgb(1.0, 0.0, 0.0);
1083 let mut rt = crate::runtime::Runtime::default();
1084 let sharp: Container<()> = Container::new()
1085 .width(40.0)
1086 .height(40.0)
1087 .color(red)
1088 .animated_radius(0.0, 0.10, Curve::Linear);
1089 rt.advance_radii(&sharp, 1.0); let round: Container<()> = Container::new()
1091 .width(40.0)
1092 .height(40.0)
1093 .color(red)
1094 .animated_radius(20.0, 0.10, Curve::Linear);
1095 rt.advance_radii(&round, 0.05); let theme = crate::Theme::dark();
1098 let ui = crate::ui::build_ui(&round, Size::new(60.0, 60.0), &rt, &theme);
1099 let radius = ui
1100 .scene()
1101 .primitives()
1102 .iter()
1103 .find_map(|p| match p {
1104 Primitive::Rect { radius, .. } => Some(*radius),
1105 _ => None,
1106 })
1107 .expect("un rectangle de fond");
1108 assert!(
1109 (radius.top_left - 10.0).abs() < 1.0,
1110 "interpolated radius: {}",
1111 radius.top_left
1112 );
1113 }
1114
1115 #[test]
1119 fn animated_padding_insets_the_child_at_layout() {
1120 use frus_core::{Primitive, Size};
1121 let red = Color::rgb(1.0, 0.0, 0.0);
1122 let build = |pad: f32| {
1123 Container::<()>::new()
1124 .width(60.0)
1125 .height(60.0)
1126 .animated_padding(pad, 0.10, Curve::Linear)
1127 .child(Container::new().width(20.0).height(20.0).color(red))
1128 };
1129 let mut rt = crate::runtime::Runtime::default();
1130 rt.advance_paddings(&build(0.0), 1.0); let bigger = build(20.0);
1132 rt.advance_paddings(&bigger, 0.05); let theme = crate::Theme::dark();
1135 let ui = crate::ui::build_ui(&bigger, Size::new(60.0, 60.0), &rt, &theme);
1136 let rect = ui
1137 .scene()
1138 .primitives()
1139 .iter()
1140 .find_map(|p| match p {
1141 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1142 _ => None,
1143 })
1144 .expect("the child's red background");
1145 assert!(
1146 (rect.x - 10.0).abs() < 1.0 && (rect.y - 10.0).abs() < 1.0,
1147 "the child is inset by ~10 by the interpolated padding: {rect:?}"
1148 );
1149 }
1150
1151 #[test]
1154 fn alignment_centers_the_child() {
1155 use frus_core::{Alignment, Primitive, Size};
1156 let red = Color::rgb(1.0, 0.0, 0.0);
1157 let root: Container<()> = Container::new()
1158 .width(100.0)
1159 .height(100.0)
1160 .alignment(Alignment::CENTER)
1161 .child(Container::new().width(20.0).height(20.0).color(red));
1162 let rt = crate::runtime::Runtime::default();
1163 let theme = crate::Theme::dark();
1164 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1165 let rect = ui
1166 .scene()
1167 .primitives()
1168 .iter()
1169 .find_map(|p| match p {
1170 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1171 _ => None,
1172 })
1173 .expect("the child's red background");
1174 assert!(
1175 (rect.x - 40.0).abs() < 1.0 && (rect.y - 40.0).abs() < 1.0,
1176 "centred: {rect:?}"
1177 );
1178 }
1179
1180 #[test]
1183 fn alignment_anchors_child_to_a_corner() {
1184 use frus_core::{Alignment, Primitive, Size};
1185 let red = Color::rgb(1.0, 0.0, 0.0);
1186 let root: Container<()> = Container::new()
1187 .width(100.0)
1188 .height(100.0)
1189 .alignment(Alignment::BOTTOM_RIGHT)
1190 .child(Container::new().width(20.0).height(20.0).color(red));
1191 let rt = crate::runtime::Runtime::default();
1192 let theme = crate::Theme::dark();
1193 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1194 let rect = ui
1195 .scene()
1196 .primitives()
1197 .iter()
1198 .find_map(|p| match p {
1199 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1200 _ => None,
1201 })
1202 .expect("the child's red background");
1203 assert!(
1204 (rect.x - 80.0).abs() < 1.0 && (rect.y - 80.0).abs() < 1.0,
1205 "bottom-right corner: {rect:?}"
1206 );
1207 }
1208
1209 #[test]
1215 fn fractional_alignment_places_child_proportionally() {
1216 use frus_core::{Alignment, Primitive, Size};
1217 let red = Color::rgb(1.0, 0.0, 0.0);
1218 let root: Container<()> = Container::new()
1219 .width(100.0)
1220 .height(100.0)
1221 .alignment(Alignment::new(0.5, -0.5))
1222 .child(Container::new().width(20.0).height(20.0).color(red));
1223 let rt = crate::runtime::Runtime::default();
1224 let theme = crate::Theme::dark();
1225 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1226 let rect = ui
1227 .scene()
1228 .primitives()
1229 .iter()
1230 .find_map(|p| match p {
1231 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1232 _ => None,
1233 })
1234 .expect("the child's red background");
1235 assert!(
1236 (rect.x - 60.0).abs() < 1.0 && (rect.y - 20.0).abs() < 1.0,
1237 "fractionnel : {rect:?}"
1238 );
1239 }
1240
1241 #[test]
1245 fn directional_alignment_flips_the_child_in_rtl() {
1246 use frus_core::{AlignmentDirectional, Primitive, Size};
1247 let red = Color::rgb(1.0, 0.0, 0.0);
1248 let rt = crate::runtime::Runtime::default();
1249 let child_x = |theme: &crate::Theme| {
1250 let root: Container<()> = Container::new()
1251 .width(100.0)
1252 .height(100.0)
1253 .alignment(AlignmentDirectional::CENTER_START)
1254 .child(Container::new().width(20.0).height(20.0).color(red));
1255 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, theme);
1256 ui.scene()
1257 .primitives()
1258 .iter()
1259 .find_map(|p| match p {
1260 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1261 _ => None,
1262 })
1263 .expect("the child's red background")
1264 };
1265 assert!(
1266 child_x(&crate::Theme::dark()).abs() < 1.0,
1267 "start on the left in LTR"
1268 );
1269 assert!(
1270 (child_x(&crate::Theme::dark().rtl()) - 80.0).abs() < 1.0,
1271 "start on the right in RTL"
1272 );
1273 }
1274
1275 #[test]
1282 fn sides_shape_and_shadows_reach_the_box() {
1283 use frus_core::{BorderDirectional, BorderSide, BoxShape, ClipShape, TextDirection};
1284 let sided: Container<()> = Container::new().box_border(BorderDirectional {
1285 start: BorderSide::new(Color::WHITE, 6.0),
1286 top: BorderSide::new(Color::WHITE, 2.0),
1287 ..Default::default()
1288 });
1289 assert_eq!(
1290 sided.effective_padding_in(TextDirection::Ltr),
1291 Insets::new(2.0, 0.0, 0.0, 6.0)
1292 );
1293 assert_eq!(
1294 sided.effective_padding_in(TextDirection::Rtl),
1295 Insets::new(2.0, 6.0, 0.0, 0.0)
1296 );
1297 let round: Container<()> = Container::new().shape(BoxShape::Circle).clip();
1298 assert_eq!(Widget::<()>::clip_shape(&round), Some(ClipShape::Oval));
1299 let two: Container<()> = Container::new().shadow(0.0, 1.0, 2.0, Color::BLACK).shadow(
1300 0.0,
1301 4.0,
1302 8.0,
1303 Color::BLACK,
1304 );
1305 assert_eq!(two.shadows.len(), 2, "each call adds one");
1306 let none: Container<()> = Container::new().border(0.0, Color::WHITE);
1307 assert_eq!(none.border, None, "no width, no border");
1308 }
1309
1310 #[test]
1311 fn decoration_applies_composite_fields() {
1312 use frus_core::{BorderRadius, BoxDecoration, Primitive, Size};
1313 let green = Color::rgb(0.0, 1.0, 0.0);
1314 let deco = BoxDecoration {
1315 color: Some(green),
1316 radius: BorderRadius::uniform(8.0),
1317 border: Some(Border::new(2.0, Color::WHITE).into()),
1318 ..Default::default()
1319 };
1320 let root: Container<()> = Container::new().width(40.0).height(40.0).decoration(deco);
1321 assert_eq!(Widget::style(&root).padding, Insets::uniform(2.0));
1323
1324 let rt = crate::runtime::Runtime::default();
1325 let theme = crate::Theme::dark();
1326 let ui = crate::ui::build_ui(&root, Size::new(40.0, 40.0), &rt, &theme);
1327 let (color, radius) = ui
1328 .scene()
1329 .primitives()
1330 .iter()
1331 .find_map(|p| match p {
1332 Primitive::Rect { color, radius, .. } if color.g > 0.5 => Some((*color, *radius)),
1333 _ => None,
1334 })
1335 .expect("the decorated green background");
1336 assert!(color.g > 0.9, "a green background: {color:?}");
1337 assert!(
1338 (radius.top_left - 8.0).abs() < 1e-3,
1339 "rayon composite : {}",
1340 radius.top_left
1341 );
1342 }
1343
1344 #[test]
1349 fn margin_pushes_siblings_and_insets() {
1350 use frus_core::{Primitive, Size};
1351 let red = Color::rgb(1.0, 0.0, 0.0);
1352 let green = Color::rgb(0.0, 1.0, 0.0);
1353 let root = crate::Flex::<()>::column()
1354 .width(100.0)
1355 .child(Container::new().height(20.0).color(red))
1356 .child(Container::new().height(20.0).margin(10.0).color(green));
1357 let rt = crate::runtime::Runtime::default();
1358 let theme = crate::Theme::dark();
1359 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
1360 let rect = ui
1361 .scene()
1362 .primitives()
1363 .iter()
1364 .find_map(|p| match p {
1365 Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
1366 Some(*rect)
1367 }
1368 _ => None,
1369 })
1370 .expect("the 2nd child's green background");
1371 assert!(
1372 (rect.y - 30.0).abs() < 0.5 && (rect.x - 10.0).abs() < 0.5,
1373 "margin: pushed to y=30, inset to x=10: {rect:?}"
1374 );
1375 assert!(
1376 (rect.height - 20.0).abs() < 0.5,
1377 "the margin does not grow the box: {rect:?}"
1378 );
1379 }
1380
1381 #[test]
1382 fn visible_border_reserves_layout_padding() {
1383 let bordered: Container<()> = Container::new().padding(4.0).border(2.0, Color::WHITE);
1385 assert_eq!(Widget::style(&bordered).padding, Insets::uniform(6.0));
1386
1387 let plain: Container<()> = Container::new().padding(4.0);
1389 assert_eq!(Widget::style(&plain).padding, Insets::uniform(4.0));
1390 let invisible: Container<()> = Container::new()
1391 .padding(4.0)
1392 .border(2.0, Color::TRANSPARENT);
1393 assert_eq!(Widget::style(&invisible).padding, Insets::uniform(4.0));
1394 }
1395}
1396
1397#[cfg(test)]
1398mod clip_tests {
1399 use super::*;
1400 use crate::{build_ui, Runtime, Size};
1401 use frus_core::Primitive;
1402
1403 fn layers(root: &Container<()>) -> Vec<frus_core::ClipShape> {
1404 let ui = build_ui(
1405 root,
1406 Size::new(100.0, 100.0),
1407 &Runtime::default(),
1408 &Theme::default(),
1409 );
1410 ui.scene()
1411 .primitives()
1412 .iter()
1413 .filter_map(|p| match p {
1414 Primitive::Layer { clip_shape, .. } => Some(clip_shape.clone()),
1415 _ => None,
1416 })
1417 .collect()
1418 }
1419
1420 #[test]
1423 fn a_rounded_box_does_not_clip_unless_asked() {
1424 let plain = Container::<()>::new()
1425 .width(100.0)
1426 .height(100.0)
1427 .radius(12.0)
1428 .color(Color::rgb(1.0, 0.0, 0.0))
1429 .child(Container::<()>::new().width(100.0).height(100.0));
1430 assert!(layers(&plain).is_empty(), "no compositing layer");
1431 }
1432
1433 #[test]
1435 fn clip_confines_the_child_to_the_corners() {
1436 let clipped = Container::<()>::new()
1437 .width(100.0)
1438 .height(100.0)
1439 .radius(12.0)
1440 .clip()
1441 .color(Color::rgb(1.0, 0.0, 0.0))
1442 .child(Container::<()>::new().width(100.0).height(100.0));
1443 let shapes = layers(&clipped);
1444 assert_eq!(shapes.len(), 1);
1445 match &shapes[0] {
1446 frus_core::ClipShape::RRect(r) => assert_eq!(r.top_left, 12.0),
1447 other => panic!("a rounded rectangle, not {other:?}"),
1448 }
1449 }
1450}
1451
1452#[cfg(test)]
1453mod foreground_tests {
1454 use super::*;
1455 use crate::{build_ui, Runtime, Size};
1456 use frus_core::{Primitive, Scene};
1457
1458 const BACK: Color = Color::rgb(1.0, 0.0, 0.0);
1459 const CHILD: Color = Color::rgb(0.0, 1.0, 0.0);
1460 const OVER: Color = Color::rgb(0.0, 0.0, 1.0);
1461
1462 fn scene(root: &Container<()>) -> Scene {
1463 build_ui(
1464 root,
1465 Size::new(100.0, 100.0),
1466 &Runtime::default(),
1467 &Theme::default(),
1468 )
1469 .scene()
1470 .clone()
1471 }
1472
1473 fn order(root: &Container<()>) -> Vec<Color> {
1475 scene(root)
1476 .primitives()
1477 .iter()
1478 .filter_map(|p| match p {
1479 Primitive::Rect { color, .. } if *color == BACK || *color == CHILD => Some(*color),
1480 Primitive::Rect {
1481 border_color,
1482 border_width,
1483 ..
1484 } if *border_width > 0.0 && *border_color == OVER => Some(OVER),
1485 _ => None,
1486 })
1487 .collect()
1488 }
1489
1490 fn card() -> Container<()> {
1491 Container::<()>::new()
1492 .width(100.0)
1493 .height(100.0)
1494 .color(BACK)
1495 .child(
1496 Container::<()>::new()
1497 .width(100.0)
1498 .height(100.0)
1499 .color(CHILD),
1500 )
1501 }
1502
1503 #[test]
1505 fn nothing_paints_over_the_child_by_default() {
1506 assert_eq!(order(&card()), vec![BACK, CHILD]);
1507 }
1508
1509 #[test]
1511 fn a_foreground_paints_after_the_child() {
1512 let card = card().foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1513 assert_eq!(order(&card), vec![BACK, CHILD, OVER]);
1514 }
1515
1516 #[test]
1518 fn it_covers_the_container_s_own_box() {
1519 let card = card()
1520 .padding(10.0)
1521 .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1522 let over = scene(&card)
1523 .primitives()
1524 .iter()
1525 .find_map(|p| match p {
1526 Primitive::Rect {
1527 border_color,
1528 border_width,
1529 rect,
1530 ..
1531 } if *border_width > 0.0 && *border_color == OVER => Some(*rect),
1532 _ => None,
1533 })
1534 .expect("the foreground is painted");
1535 assert_eq!((over.width, over.height), (100.0, 100.0));
1536 }
1537
1538 #[test]
1541 fn a_silent_foreground_wears_the_container_s_corners() {
1542 let card = card()
1543 .radius(12.0)
1544 .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1545 assert_eq!(
1546 Widget::<()>::foreground(&card, &Theme::default())
1547 .unwrap()
1548 .radius,
1549 BorderRadius::uniform(12.0),
1550 "resolved against the container on the way out"
1551 );
1552 }
1553
1554 #[test]
1556 fn an_explicit_radius_is_left_alone() {
1557 let card = card().radius(12.0).foreground(
1558 BoxDecoration::default()
1559 .border(Border::new(2.0, OVER))
1560 .radius(4.0),
1561 );
1562 assert_eq!(
1563 Widget::<()>::foreground(&card, &Theme::default())
1564 .unwrap()
1565 .radius,
1566 BorderRadius::uniform(4.0)
1567 );
1568 }
1569}
1570
1571#[cfg(test)]
1573mod reference_tests {
1574 use super::*;
1575 use crate::{build_ui_inspected, Center, Flex, Runtime, Text};
1576
1577 fn rect_of(root: &dyn Widget<()>, size: Size, nth: usize) -> Rect {
1578 let (_, nodes) = build_ui_inspected(root, size, &Runtime::default(), &Theme::dark());
1579 nodes
1580 .iter()
1581 .filter(|n| n.name == "Container")
1582 .nth(nth)
1583 .map(|n| n.rect)
1584 .expect("a container")
1585 }
1586
1587 #[test]
1591 fn an_empty_container_takes_the_room_allowed() {
1592 let size = Size::new(300.0, 200.0);
1593 let alone = Center::new(Container::<()>::new().color(Color::rgb8(1, 2, 3)));
1594 assert_eq!(rect_of(&alone, size, 0), Rect::new(0.0, 0.0, 300.0, 200.0));
1595 let row = Flex::<()>::row()
1596 .width(300.0)
1597 .height(50.0)
1598 .child(Container::new().color(Color::rgb8(1, 2, 3)))
1599 .child(Container::new().width(10.0).height(10.0));
1600 let empty = rect_of(&row, size, 0);
1601 assert_eq!(
1602 (empty.width, empty.height),
1603 (0.0, 50.0),
1604 "nothing along, the row across"
1605 );
1606 }
1607
1608 #[test]
1611 fn an_aligning_container_fills_its_parent() {
1612 let size = Size::new(300.0, 200.0);
1613 let aligned = Center::new(
1614 Container::<()>::new()
1615 .alignment(frus_core::Alignment::BOTTOM_RIGHT)
1616 .child(Container::new().width(20.0).height(10.0)),
1617 );
1618 assert_eq!(
1619 rect_of(&aligned, size, 0),
1620 Rect::new(0.0, 0.0, 300.0, 200.0)
1621 );
1622 assert_eq!(
1623 rect_of(&aligned, size, 1),
1624 Rect::new(280.0, 190.0, 20.0, 10.0)
1625 );
1626 let hugging = Center::new(Container::<()>::new().child(Text::new("Hi")));
1627 let hug = rect_of(&hugging, size, 0);
1628 assert!(hug.width < 40.0 && hug.height < 30.0, "{hug:?}");
1629 }
1630
1631 #[test]
1633 fn constraints_hold_the_box() {
1634 let size = Size::new(300.0, 200.0);
1635 let floored = Center::new(
1636 Container::<()>::new()
1637 .min_width(80.0)
1638 .min_height(30.0)
1639 .child(Text::new("Hi")),
1640 );
1641 let r = rect_of(&floored, size, 0);
1642 assert_eq!((r.width, r.height), (80.0, 30.0));
1643 let capped = Center::new(
1644 Container::<()>::new()
1645 .width(500.0)
1646 .max_width(120.0)
1647 .height(10.0),
1648 );
1649 assert_eq!(rect_of(&capped, size, 0).width, 120.0);
1650 let ceiling = Center::new(Container::<()>::new().max_width(50.0).max_height(40.0));
1652 let r = rect_of(&ceiling, size, 0);
1653 assert_eq!((r.width, r.height), (50.0, 40.0));
1654 }
1655
1656 #[test]
1658 fn a_transform_is_about_its_alignment() {
1659 let c = Container::<()>::new()
1660 .width(40.0)
1661 .height(20.0)
1662 .translated(5.0, 6.0)
1663 .scaled(2.0, 3.0)
1664 .rotated(0.5)
1665 .transform_alignment(frus_core::Alignment::TOP_LEFT);
1666 assert_eq!(Widget::<()>::transform_translate(&c), Some((5.0, 6.0)));
1667 assert_eq!(
1668 Widget::<()>::transform_scale(&c),
1669 Some((2.0, 3.0, frus_core::Alignment::TOP_LEFT))
1670 );
1671 assert_eq!(
1672 Widget::<()>::transform_rotate(&c),
1673 Some((0.5, frus_core::Alignment::TOP_LEFT))
1674 );
1675 let plain = Container::<()>::new().scaled(2.0, 2.0);
1676 assert_eq!(
1677 Widget::<()>::transform_scale(&plain).map(|t| t.2),
1678 Some(frus_core::Alignment::CENTER),
1679 "about the centre unless told"
1680 );
1681 }
1682}