1use frus_core::{
5 AlignmentGeometry, Border, BorderRadius, BoxDecoration, BoxShadow, Color, Curve, Insets,
6 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: Insets,
27 margin: Insets,
29 radius: BorderRadius,
30 clip: bool,
32 foreground: Option<BoxDecoration>,
35 border_width: f32,
36 border_color: Color,
37 color: Option<Color>,
38 hover_color: Option<Color>,
39 pressed_color: Option<Color>,
40 gradient: Option<(Color, [f32; 2])>,
42 shadow: Option<(f32, f32, f32, Color)>,
44 on_click: Option<Msg>,
45 on_long_press: Option<Msg>,
46 repaint_boundary: bool,
49 opacity: Option<f32>,
51 opacity_anim: Option<(f32, Curve)>,
54 color_anim: Option<(Color, f32, Curve)>,
57 size_anim: Option<(Size, f32, Curve)>,
60 radius_anim: Option<(BorderRadius, f32, Curve)>,
63 padding_anim: Option<(f32, Curve)>,
66 alignment_anim: Option<(f32, Curve)>,
68 alignment: Option<AlignmentGeometry>,
72 children: Vec<Box<dyn Widget<Msg>>>,
73}
74
75impl<Msg> Container<Msg> {
76 pub fn new() -> Self {
78 Self {
79 width: Dimension::Auto,
80 height: Dimension::Auto,
81 flex_grow: 0.0,
82 flex_shrink: 0.0,
83 padding: Insets::ZERO,
84 margin: Insets::ZERO,
85 radius: BorderRadius::ZERO,
86 clip: false,
87 foreground: None,
88 border_width: 0.0,
89 border_color: Color::TRANSPARENT,
90 color: None,
91 hover_color: None,
92 pressed_color: None,
93 gradient: None,
94 shadow: None,
95 on_click: None,
96 on_long_press: None,
97 repaint_boundary: false,
98 opacity: None,
99 opacity_anim: None,
100 color_anim: None,
101 size_anim: None,
102 radius_anim: None,
103 padding_anim: None,
104 alignment_anim: None,
105 alignment: None,
106 children: Vec::new(),
107 }
108 }
109
110 fn effective_padding(&self) -> Insets {
115 let mut padding = self.padding;
116 if Border::new(self.border_width, self.border_color).is_visible() {
117 padding.top += self.border_width;
118 padding.right += self.border_width;
119 padding.bottom += self.border_width;
120 padding.left += self.border_width;
121 }
122 padding
123 }
124
125 pub fn repaint_boundary(mut self) -> Self {
130 self.repaint_boundary = true;
131 self
132 }
133
134 pub fn width(mut self, width: f32) -> Self {
136 self.width = Dimension::Length(width);
137 self
138 }
139
140 pub fn height(mut self, height: f32) -> Self {
142 self.height = Dimension::Length(height);
143 self
144 }
145
146 pub fn width_fraction(mut self, fraction: f32) -> Self {
148 self.width = Dimension::Percent(fraction);
149 self
150 }
151
152 pub fn flex(mut self, grow: f32) -> Self {
154 self.flex_grow = grow;
155 self
156 }
157
158 pub fn shrink(mut self, shrink: f32) -> Self {
167 self.flex_shrink = shrink;
168 self
169 }
170
171 pub fn no_shrink(self) -> Self {
174 self.shrink(0.0)
175 }
176
177 pub fn padding(mut self, padding: f32) -> Self {
179 self.padding = Insets::uniform(padding);
180 self
181 }
182
183 pub fn padding_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
185 self.padding = Insets::new(top, right, bottom, left);
186 self
187 }
188
189 pub fn margin(mut self, margin: f32) -> Self {
193 self.margin = Insets::uniform(margin);
194 self
195 }
196
197 pub fn margin_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
199 self.margin = Insets::new(top, right, bottom, left);
200 self
201 }
202
203 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
206 self.radius = radius.into();
207 self
208 }
209
210 pub fn clip(mut self) -> Self {
220 self.clip = true;
221 self
222 }
223
224 pub fn border(mut self, width: f32, color: Color) -> Self {
226 self.border_width = width;
227 self.border_color = color;
228 self
229 }
230
231 pub fn color(mut self, color: Color) -> Self {
233 self.color = Some(color);
234 self
235 }
236
237 pub fn hover_color(mut self, color: Color) -> Self {
239 self.hover_color = Some(color);
240 self
241 }
242
243 pub fn pressed_color(mut self, color: Color) -> Self {
245 self.pressed_color = Some(color);
246 self
247 }
248
249 pub fn gradient(mut self, end: Color, dir: [f32; 2]) -> Self {
252 self.gradient = Some((end, dir));
253 self
254 }
255
256 pub fn shadow(mut self, dx: f32, dy: f32, blur: f32, color: Color) -> Self {
258 self.shadow = Some((dx, dy, blur, color));
259 self
260 }
261
262 pub fn on_click(mut self, message: impl Into<Msg>) -> Self {
264 self.on_click = Some(message.into());
265 self
266 }
267
268 pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
271 self.on_long_press = Some(message.into());
272 self
273 }
274
275 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
277 self.children.clear();
278 self.children.push(Box::new(child));
279 self
280 }
281
282 pub fn opacity(mut self, opacity: f32) -> Self {
286 self.opacity = Some(opacity.clamp(0.0, 1.0));
287 self
288 }
289
290 pub fn animated_opacity(mut self, opacity: f32, duration: f32, curve: Curve) -> Self {
294 self.opacity = Some(opacity.clamp(0.0, 1.0));
295 self.opacity_anim = Some((duration, curve));
296 self
297 }
298
299 pub fn animated_color(mut self, color: Color, duration: f32, curve: Curve) -> Self {
304 self.color = Some(color);
305 self.color_anim = Some((color, duration, curve));
306 self
307 }
308
309 pub fn animated_size(mut self, width: f32, height: f32, duration: f32, curve: Curve) -> Self {
314 self.width = Dimension::Length(width);
315 self.height = Dimension::Length(height);
316 self.size_anim = Some((Size::new(width, height), duration, curve));
317 self
318 }
319
320 pub fn animated_radius(
325 mut self,
326 radius: impl Into<BorderRadius>,
327 duration: f32,
328 curve: Curve,
329 ) -> Self {
330 let radius = radius.into();
331 self.radius = radius;
332 self.radius_anim = Some((radius, duration, curve));
333 self
334 }
335
336 pub fn animated_padding(mut self, padding: f32, duration: f32, curve: Curve) -> Self {
341 self.padding = Insets::uniform(padding);
342 self.padding_anim = Some((duration, curve));
343 self
344 }
345
346 pub fn animated_alignment(
353 mut self,
354 alignment: impl Into<AlignmentGeometry>,
355 duration: f32,
356 curve: Curve,
357 ) -> Self {
358 self.alignment = Some(alignment.into());
359 self.alignment_anim = Some((duration, curve));
360 self
361 }
362
363 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
371 self.alignment = Some(alignment.into());
372 self
373 }
374
375 pub fn foreground(mut self, decoration: BoxDecoration) -> Self {
408 self.foreground = Some(decoration);
409 self
410 }
411
412 pub fn decoration(mut self, decoration: BoxDecoration) -> Self {
416 if let Some(color) = decoration.color {
417 self.color = Some(color);
418 }
419 if let Some(gradient) = decoration.gradient {
420 self.gradient = Some((gradient.end, gradient.direction));
421 }
422 if let Some(border) = decoration.border {
423 self.border_width = border.width;
424 self.border_color = border.color;
425 }
426 self.radius = decoration.radius;
427 if let Some(shadow) = decoration.shadow {
428 self.shadow = Some((shadow.offset.0, shadow.offset.1, shadow.blur, shadow.color));
429 }
430 self
431 }
432}
433
434impl<Msg> Default for Container<Msg> {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440impl<Msg: Clone> Widget<Msg> for Container<Msg> {
441 fn style(&self) -> Style {
442 let mut style = Style {
443 width: self.width,
444 height: self.height,
445 flex_grow: self.flex_grow,
446 flex_shrink: self.flex_shrink,
447 padding: self.effective_padding(),
448 margin: self.margin,
449 ..Default::default()
450 };
451 if self.alignment.is_some() {
456 style.justify = Justify::Start;
457 style.align = Align::Start;
458 }
459 style
460 }
461
462 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
463 &self.children
464 }
465
466 fn paint(&self, bounds: Rect, status: Status, _theme: &Theme, scene: &mut Scene) {
467 let color = if self.color_anim.is_some() {
474 status.anim_color.or(self.color)
475 } else {
476 let settled = match (self.color, self.hover_color) {
477 (Some(base), Some(hover)) => Some(base.lerp(hover, ease(status.hover_progress))),
478 _ => self.color,
479 };
480 let press = ease(status.press_progress.clamp(0.0, 1.0));
481 match (settled, self.pressed_color.or(self.hover_color)) {
482 (Some(from), Some(held)) => Some(from.lerp(held, press)),
483 (None, held) if press > 0.0 => held,
486 (settled, _) => settled,
487 }
488 };
489
490 let radius = if self.radius_anim.is_some() {
495 status.anim_radius.unwrap_or(self.radius)
496 } else {
497 self.radius
498 };
499
500 let decoration = BoxDecoration {
501 color,
502 gradient: self
503 .gradient
504 .map(|(end, dir)| LinearGradient::new(end, dir)),
505 border: (self.border_width > 0.0)
506 .then(|| Border::new(self.border_width, self.border_color)),
507 radius,
508 shadow: self
509 .shadow
510 .map(|(dx, dy, blur, c)| BoxShadow::new(dx, dy, blur, c)),
511 };
512 decoration.paint_into(scene, bounds, status.opacity);
513 }
514
515 fn clip_shape(&self) -> Option<frus_core::ClipShape> {
516 self.clip
517 .then(|| frus_core::ClipShape::RRect(self.radius.clamped()))
518 }
519
520 fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
521 self.foreground.map(|decoration| {
522 if decoration.radius == frus_core::BorderRadius::ZERO {
523 decoration.radius(self.radius)
524 } else {
525 decoration
526 }
527 })
528 }
529
530 fn on_click(&self) -> Option<Msg> {
531 self.on_click.clone()
532 }
533
534 fn on_long_press(&self) -> Option<Msg> {
535 self.on_long_press.clone()
536 }
537
538 fn repaint_boundary(&self) -> bool {
539 self.repaint_boundary
540 }
541
542 fn opacity_group(&self) -> Option<f32> {
543 self.opacity
544 }
545
546 fn anim_target(&self) -> Option<f32> {
549 self.opacity_anim.as_ref().and(self.opacity)
550 }
551
552 fn anim_color(&self) -> Option<Color> {
553 self.color_anim.as_ref().and(self.color)
554 }
555
556 fn anim_size(&self) -> Option<Size> {
557 self.size_anim.as_ref().map(|(s, _, _)| *s)
558 }
559
560 fn anim_radius(&self) -> Option<BorderRadius> {
561 self.radius_anim.as_ref().map(|(r, _, _)| *r)
562 }
563
564 fn anim_padding(&self) -> Option<Insets> {
565 self.padding_anim.as_ref().map(|_| self.effective_padding())
567 }
568
569 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
570 self.alignment
571 }
572
573 fn anim_offset(&self) -> Option<(f32, f32)> {
574 self.alignment_anim
577 .as_ref()
578 .and(self.alignment)
579 .map(|a| a.fractions())
580 }
581
582 fn anim_duration(&self) -> f32 {
583 self.opacity_anim
586 .as_ref()
587 .map(|(d, _)| *d)
588 .or(self.color_anim.as_ref().map(|(_, d, _)| *d))
589 .or(self.size_anim.as_ref().map(|(_, d, _)| *d))
590 .or(self.radius_anim.as_ref().map(|(_, d, _)| *d))
591 .or(self.padding_anim.as_ref().map(|(d, _)| *d))
592 .or(self.alignment_anim.as_ref().map(|(d, _)| *d))
593 .unwrap_or(crate::runtime::ANIM_DURATION)
594 }
595
596 fn anim_curve(&self) -> Curve {
597 self.opacity_anim
598 .as_ref()
599 .map(|(_, c)| c.clone())
600 .or(self.color_anim.as_ref().map(|(_, _, c)| c.clone()))
601 .or(self.size_anim.as_ref().map(|(_, _, c)| c.clone()))
602 .or(self.radius_anim.as_ref().map(|(_, _, c)| c.clone()))
603 .or(self.padding_anim.as_ref().map(|(_, c)| c.clone()))
604 .or(self.alignment_anim.as_ref().map(|(_, c)| c.clone()))
605 .unwrap_or(Curve::Linear)
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
621 fn a_box_in_an_over_budget_row_keeps_its_width() {
622 use crate::interaction::WidgetId;
623 use crate::runtime::Runtime;
624 use crate::theme::Theme;
625 use crate::Flex;
626
627 let widths = |row: &Flex<()>| -> Vec<f32> {
628 let runtime = Runtime::default();
629 let theme = Theme::light();
630 let mut layout = frus_layout::Layout::new();
631 let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
632 layout.compute_filled(node, 100.0, 50.0);
633 layout
634 .absolute_rects(node)
635 .iter()
636 .skip(1)
637 .take(2)
638 .map(|(r, _)| r.width)
639 .collect()
640 };
641
642 let row = Flex::row()
645 .child(Container::<()>::new().width(80.0).height(20.0))
646 .child(Container::<()>::new().width(40.0).height(20.0));
647 assert_eq!(
648 widths(&row),
649 vec![80.0, 40.0],
650 "an inflexible child is never squeezed"
651 );
652
653 let giving_way = Flex::row()
655 .child(Container::<()>::new().width(80.0).height(20.0).shrink(1.0))
656 .child(Container::<()>::new().width(40.0).height(20.0));
657 assert_eq!(widths(&giving_way), vec![60.0, 40.0]);
658 }
659
660 #[test]
665 fn a_held_box_crosses_over_to_its_pressed_colour() {
666 use frus_core::Primitive;
667
668 let rest = Color::rgb(0.2, 0.2, 0.2);
669 let held = Color::rgb(0.8, 0.8, 0.8);
670 let box_: Container<()> = Container::new()
671 .width(20.0)
672 .height(20.0)
673 .color(rest)
674 .pressed_color(held);
675
676 let fill = |press: f32| {
677 let mut scene = Scene::new();
678 Widget::<()>::paint(
679 &box_,
680 Rect::new(0.0, 0.0, 20.0, 20.0),
681 Status {
682 press_progress: press,
683 ..Default::default()
684 },
685 &crate::Theme::dark(),
686 &mut scene,
687 );
688 scene
689 .primitives()
690 .iter()
691 .find_map(|p| match p {
692 Primitive::Rect { color, .. } => Some(*color),
693 _ => None,
694 })
695 .expect("a box paints its background")
696 };
697
698 assert_eq!(fill(0.0), rest, "untouched");
699 assert_eq!(fill(1.0), held, "held");
700 let half = fill(0.5).r;
701 assert!(
702 half > rest.r && half < held.r,
703 "half way through the press: {half}"
704 );
705 }
706
707 #[test]
710 fn opacity_group_wraps_subtree_in_a_layer() {
711 use frus_core::{Primitive, Size};
712 let root: Container<()> = Container::new()
713 .width(40.0)
714 .height(40.0)
715 .color(Color::rgb(1.0, 0.0, 0.0))
716 .opacity(0.5);
717 let rt = crate::runtime::Runtime::default();
718 let theme = crate::Theme::dark();
719 let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
720 let layer = ui.scene().primitives().iter().find_map(|p| match p {
721 Primitive::Layer {
722 opacity,
723 primitives,
724 ..
725 } => Some((*opacity, primitives.len())),
726 _ => None,
727 });
728 let (op, n) = layer.expect("a group opacity layer");
729 assert!((op - 0.5).abs() < 1e-6, "group opacity = {op}");
730 assert!(
731 n >= 1,
732 "the layer wraps the painted content ({n} primitives)"
733 );
734 }
735
736 #[test]
738 fn full_opacity_emits_no_layer() {
739 use frus_core::{Primitive, Size};
740 let root: Container<()> = Container::new()
741 .width(40.0)
742 .height(40.0)
743 .color(Color::rgb(1.0, 0.0, 0.0))
744 .opacity(1.0);
745 let rt = crate::runtime::Runtime::default();
746 let theme = crate::Theme::dark();
747 let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
748 assert!(
749 !ui.scene()
750 .primitives()
751 .iter()
752 .any(|p| matches!(p, Primitive::Layer { .. })),
753 "no layer at full opacity"
754 );
755 }
756
757 #[test]
760 fn animated_opacity_declares_anim_target() {
761 let animated: Container<()> = Container::new().animated_opacity(0.0, 0.3, Curve::ease_in());
762 assert_eq!(Widget::<()>::anim_target(&animated), Some(0.0));
763 assert_eq!(Widget::<()>::anim_duration(&animated), 0.3);
764 assert_eq!(Widget::<()>::anim_curve(&animated), Curve::ease_in());
765 assert_eq!(Widget::<()>::opacity_group(&animated), Some(0.0));
766
767 let fixed: Container<()> = Container::new().opacity(0.5);
769 assert_eq!(Widget::<()>::anim_target(&fixed), None);
770 assert_eq!(Widget::<()>::opacity_group(&fixed), Some(0.5));
771 }
772
773 #[test]
777 fn animated_color_paints_the_interpolated_color() {
778 use frus_core::{Primitive, Size};
779 let red = Color::rgb(1.0, 0.0, 0.0);
780 let blue = Color::rgb(0.0, 0.0, 1.0);
781 let mut rt = crate::runtime::Runtime::default();
782 let start: Container<()> =
783 Container::new()
784 .width(20.0)
785 .height(20.0)
786 .animated_color(red, 0.10, Curve::Linear);
787 rt.advance_colors(&start, 1.0); let to_blue: Container<()> =
789 Container::new()
790 .width(20.0)
791 .height(20.0)
792 .animated_color(blue, 0.10, Curve::Linear);
793 rt.advance_colors(&to_blue, 0.05); let theme = crate::Theme::dark();
796 let ui = crate::ui::build_ui(&to_blue, Size::new(20.0, 20.0), &rt, &theme);
797 let color = ui
798 .scene()
799 .primitives()
800 .iter()
801 .find_map(|p| match p {
802 Primitive::Rect { color, .. } => Some(*color),
803 _ => None,
804 })
805 .expect("un rectangle de fond");
806 assert!(
807 (color.r - 0.5).abs() < 0.1 && (color.b - 0.5).abs() < 0.1,
808 "the interpolated color is painted: {color:?}"
809 );
810 }
811
812 #[test]
816 fn animated_size_drives_the_layout() {
817 use frus_core::{Primitive, Size};
818 let mut rt = crate::runtime::Runtime::default();
819 let red = Color::rgb(1.0, 0.0, 0.0);
820 let start: Container<()> =
821 Container::new()
822 .color(red)
823 .animated_size(20.0, 20.0, 0.10, Curve::Linear);
824 rt.advance_sizes(&start, 1.0); let to_big: Container<()> =
826 Container::new()
827 .color(red)
828 .animated_size(40.0, 40.0, 0.10, Curve::Linear);
829 rt.advance_sizes(&to_big, 0.05); let theme = crate::Theme::dark();
832 let ui = crate::ui::build_ui(&to_big, Size::new(100.0, 100.0), &rt, &theme);
833 let rect = ui
834 .scene()
835 .primitives()
836 .iter()
837 .find_map(|p| match p {
838 Primitive::Rect { rect, .. } => Some(*rect),
839 _ => None,
840 })
841 .expect("un rectangle de fond");
842 assert!(
843 (rect.width - 30.0).abs() < 1.0,
844 "interpolated width: {}",
845 rect.width
846 );
847 assert!(
848 (rect.height - 30.0).abs() < 1.0,
849 "interpolated height: {}",
850 rect.height
851 );
852 }
853
854 #[test]
857 fn animated_radius_paints_the_interpolated_radius() {
858 use frus_core::{Primitive, Size};
859 let red = Color::rgb(1.0, 0.0, 0.0);
860 let mut rt = crate::runtime::Runtime::default();
861 let sharp: Container<()> = Container::new()
862 .width(40.0)
863 .height(40.0)
864 .color(red)
865 .animated_radius(0.0, 0.10, Curve::Linear);
866 rt.advance_radii(&sharp, 1.0); let round: Container<()> = Container::new()
868 .width(40.0)
869 .height(40.0)
870 .color(red)
871 .animated_radius(20.0, 0.10, Curve::Linear);
872 rt.advance_radii(&round, 0.05); let theme = crate::Theme::dark();
875 let ui = crate::ui::build_ui(&round, Size::new(60.0, 60.0), &rt, &theme);
876 let radius = ui
877 .scene()
878 .primitives()
879 .iter()
880 .find_map(|p| match p {
881 Primitive::Rect { radius, .. } => Some(*radius),
882 _ => None,
883 })
884 .expect("un rectangle de fond");
885 assert!(
886 (radius.top_left - 10.0).abs() < 1.0,
887 "interpolated radius: {}",
888 radius.top_left
889 );
890 }
891
892 #[test]
896 fn animated_padding_insets_the_child_at_layout() {
897 use frus_core::{Primitive, Size};
898 let red = Color::rgb(1.0, 0.0, 0.0);
899 let build = |pad: f32| {
900 Container::<()>::new()
901 .width(60.0)
902 .height(60.0)
903 .animated_padding(pad, 0.10, Curve::Linear)
904 .child(Container::new().width(20.0).height(20.0).color(red))
905 };
906 let mut rt = crate::runtime::Runtime::default();
907 rt.advance_paddings(&build(0.0), 1.0); let bigger = build(20.0);
909 rt.advance_paddings(&bigger, 0.05); let theme = crate::Theme::dark();
912 let ui = crate::ui::build_ui(&bigger, Size::new(60.0, 60.0), &rt, &theme);
913 let rect = ui
914 .scene()
915 .primitives()
916 .iter()
917 .find_map(|p| match p {
918 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
919 _ => None,
920 })
921 .expect("the child's red background");
922 assert!(
923 (rect.x - 10.0).abs() < 1.0 && (rect.y - 10.0).abs() < 1.0,
924 "the child is inset by ~10 by the interpolated padding: {rect:?}"
925 );
926 }
927
928 #[test]
931 fn alignment_centers_the_child() {
932 use frus_core::{Alignment, Primitive, Size};
933 let red = Color::rgb(1.0, 0.0, 0.0);
934 let root: Container<()> = Container::new()
935 .width(100.0)
936 .height(100.0)
937 .alignment(Alignment::CENTER)
938 .child(Container::new().width(20.0).height(20.0).color(red));
939 let rt = crate::runtime::Runtime::default();
940 let theme = crate::Theme::dark();
941 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
942 let rect = ui
943 .scene()
944 .primitives()
945 .iter()
946 .find_map(|p| match p {
947 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
948 _ => None,
949 })
950 .expect("the child's red background");
951 assert!(
952 (rect.x - 40.0).abs() < 1.0 && (rect.y - 40.0).abs() < 1.0,
953 "centred: {rect:?}"
954 );
955 }
956
957 #[test]
960 fn alignment_anchors_child_to_a_corner() {
961 use frus_core::{Alignment, Primitive, Size};
962 let red = Color::rgb(1.0, 0.0, 0.0);
963 let root: Container<()> = Container::new()
964 .width(100.0)
965 .height(100.0)
966 .alignment(Alignment::BOTTOM_RIGHT)
967 .child(Container::new().width(20.0).height(20.0).color(red));
968 let rt = crate::runtime::Runtime::default();
969 let theme = crate::Theme::dark();
970 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
971 let rect = ui
972 .scene()
973 .primitives()
974 .iter()
975 .find_map(|p| match p {
976 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
977 _ => None,
978 })
979 .expect("the child's red background");
980 assert!(
981 (rect.x - 80.0).abs() < 1.0 && (rect.y - 80.0).abs() < 1.0,
982 "bottom-right corner: {rect:?}"
983 );
984 }
985
986 #[test]
992 fn fractional_alignment_places_child_proportionally() {
993 use frus_core::{Alignment, Primitive, Size};
994 let red = Color::rgb(1.0, 0.0, 0.0);
995 let root: Container<()> = Container::new()
996 .width(100.0)
997 .height(100.0)
998 .alignment(Alignment::new(0.5, -0.5))
999 .child(Container::new().width(20.0).height(20.0).color(red));
1000 let rt = crate::runtime::Runtime::default();
1001 let theme = crate::Theme::dark();
1002 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1003 let rect = ui
1004 .scene()
1005 .primitives()
1006 .iter()
1007 .find_map(|p| match p {
1008 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1009 _ => None,
1010 })
1011 .expect("the child's red background");
1012 assert!(
1013 (rect.x - 60.0).abs() < 1.0 && (rect.y - 20.0).abs() < 1.0,
1014 "fractionnel : {rect:?}"
1015 );
1016 }
1017
1018 #[test]
1022 fn directional_alignment_flips_the_child_in_rtl() {
1023 use frus_core::{AlignmentDirectional, Primitive, Size};
1024 let red = Color::rgb(1.0, 0.0, 0.0);
1025 let rt = crate::runtime::Runtime::default();
1026 let child_x = |theme: &crate::Theme| {
1027 let root: Container<()> = Container::new()
1028 .width(100.0)
1029 .height(100.0)
1030 .alignment(AlignmentDirectional::CENTER_START)
1031 .child(Container::new().width(20.0).height(20.0).color(red));
1032 let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, theme);
1033 ui.scene()
1034 .primitives()
1035 .iter()
1036 .find_map(|p| match p {
1037 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1038 _ => None,
1039 })
1040 .expect("the child's red background")
1041 };
1042 assert!(
1043 child_x(&crate::Theme::dark()).abs() < 1.0,
1044 "start on the left in LTR"
1045 );
1046 assert!(
1047 (child_x(&crate::Theme::dark().rtl()) - 80.0).abs() < 1.0,
1048 "start on the right in RTL"
1049 );
1050 }
1051
1052 #[test]
1056 fn decoration_applies_composite_fields() {
1057 use frus_core::{BorderRadius, BoxDecoration, Primitive, Size};
1058 let green = Color::rgb(0.0, 1.0, 0.0);
1059 let deco = BoxDecoration {
1060 color: Some(green),
1061 radius: BorderRadius::uniform(8.0),
1062 border: Some(Border::new(2.0, Color::WHITE)),
1063 ..Default::default()
1064 };
1065 let root: Container<()> = Container::new().width(40.0).height(40.0).decoration(deco);
1066 assert_eq!(Widget::style(&root).padding, Insets::uniform(2.0));
1068
1069 let rt = crate::runtime::Runtime::default();
1070 let theme = crate::Theme::dark();
1071 let ui = crate::ui::build_ui(&root, Size::new(40.0, 40.0), &rt, &theme);
1072 let (color, radius) = ui
1073 .scene()
1074 .primitives()
1075 .iter()
1076 .find_map(|p| match p {
1077 Primitive::Rect { color, radius, .. } if color.g > 0.5 => Some((*color, *radius)),
1078 _ => None,
1079 })
1080 .expect("the decorated green background");
1081 assert!(color.g > 0.9, "a green background: {color:?}");
1082 assert!(
1083 (radius.top_left - 8.0).abs() < 1e-3,
1084 "rayon composite : {}",
1085 radius.top_left
1086 );
1087 }
1088
1089 #[test]
1094 fn margin_pushes_siblings_and_insets() {
1095 use frus_core::{Primitive, Size};
1096 let red = Color::rgb(1.0, 0.0, 0.0);
1097 let green = Color::rgb(0.0, 1.0, 0.0);
1098 let root = crate::Flex::<()>::column()
1099 .width(100.0)
1100 .child(Container::new().height(20.0).color(red))
1101 .child(Container::new().height(20.0).margin(10.0).color(green));
1102 let rt = crate::runtime::Runtime::default();
1103 let theme = crate::Theme::dark();
1104 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
1105 let rect = ui
1106 .scene()
1107 .primitives()
1108 .iter()
1109 .find_map(|p| match p {
1110 Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
1111 Some(*rect)
1112 }
1113 _ => None,
1114 })
1115 .expect("the 2nd child's green background");
1116 assert!(
1117 (rect.y - 30.0).abs() < 0.5 && (rect.x - 10.0).abs() < 0.5,
1118 "margin: pushed to y=30, inset to x=10: {rect:?}"
1119 );
1120 assert!(
1121 (rect.height - 20.0).abs() < 0.5,
1122 "the margin does not grow the box: {rect:?}"
1123 );
1124 }
1125
1126 #[test]
1127 fn visible_border_reserves_layout_padding() {
1128 let bordered: Container<()> = Container::new().padding(4.0).border(2.0, Color::WHITE);
1130 assert_eq!(Widget::style(&bordered).padding, Insets::uniform(6.0));
1131
1132 let plain: Container<()> = Container::new().padding(4.0);
1134 assert_eq!(Widget::style(&plain).padding, Insets::uniform(4.0));
1135 let invisible: Container<()> = Container::new()
1136 .padding(4.0)
1137 .border(2.0, Color::TRANSPARENT);
1138 assert_eq!(Widget::style(&invisible).padding, Insets::uniform(4.0));
1139 }
1140}
1141
1142#[cfg(test)]
1143mod clip_tests {
1144 use super::*;
1145 use crate::{build_ui, Runtime, Size};
1146 use frus_core::Primitive;
1147
1148 fn layers(root: &Container<()>) -> Vec<frus_core::ClipShape> {
1149 let ui = build_ui(
1150 root,
1151 Size::new(100.0, 100.0),
1152 &Runtime::default(),
1153 &Theme::default(),
1154 );
1155 ui.scene()
1156 .primitives()
1157 .iter()
1158 .filter_map(|p| match p {
1159 Primitive::Layer { clip_shape, .. } => Some(clip_shape.clone()),
1160 _ => None,
1161 })
1162 .collect()
1163 }
1164
1165 #[test]
1168 fn a_rounded_box_does_not_clip_unless_asked() {
1169 let plain = Container::<()>::new()
1170 .width(100.0)
1171 .height(100.0)
1172 .radius(12.0)
1173 .color(Color::rgb(1.0, 0.0, 0.0))
1174 .child(Container::<()>::new().width(100.0).height(100.0));
1175 assert!(layers(&plain).is_empty(), "no compositing layer");
1176 }
1177
1178 #[test]
1180 fn clip_confines_the_child_to_the_corners() {
1181 let clipped = Container::<()>::new()
1182 .width(100.0)
1183 .height(100.0)
1184 .radius(12.0)
1185 .clip()
1186 .color(Color::rgb(1.0, 0.0, 0.0))
1187 .child(Container::<()>::new().width(100.0).height(100.0));
1188 let shapes = layers(&clipped);
1189 assert_eq!(shapes.len(), 1);
1190 match &shapes[0] {
1191 frus_core::ClipShape::RRect(r) => assert_eq!(r.top_left, 12.0),
1192 other => panic!("a rounded rectangle, not {other:?}"),
1193 }
1194 }
1195}
1196
1197#[cfg(test)]
1198mod foreground_tests {
1199 use super::*;
1200 use crate::{build_ui, Runtime, Size};
1201 use frus_core::{Primitive, Scene};
1202
1203 const BACK: Color = Color::rgb(1.0, 0.0, 0.0);
1204 const CHILD: Color = Color::rgb(0.0, 1.0, 0.0);
1205 const OVER: Color = Color::rgb(0.0, 0.0, 1.0);
1206
1207 fn scene(root: &Container<()>) -> Scene {
1208 build_ui(
1209 root,
1210 Size::new(100.0, 100.0),
1211 &Runtime::default(),
1212 &Theme::default(),
1213 )
1214 .scene()
1215 .clone()
1216 }
1217
1218 fn order(root: &Container<()>) -> Vec<Color> {
1220 scene(root)
1221 .primitives()
1222 .iter()
1223 .filter_map(|p| match p {
1224 Primitive::Rect { color, .. } if *color == BACK || *color == CHILD => Some(*color),
1225 Primitive::Rect {
1226 border_color,
1227 border_width,
1228 ..
1229 } if *border_width > 0.0 && *border_color == OVER => Some(OVER),
1230 _ => None,
1231 })
1232 .collect()
1233 }
1234
1235 fn card() -> Container<()> {
1236 Container::<()>::new()
1237 .width(100.0)
1238 .height(100.0)
1239 .color(BACK)
1240 .child(
1241 Container::<()>::new()
1242 .width(100.0)
1243 .height(100.0)
1244 .color(CHILD),
1245 )
1246 }
1247
1248 #[test]
1250 fn nothing_paints_over_the_child_by_default() {
1251 assert_eq!(order(&card()), vec![BACK, CHILD]);
1252 }
1253
1254 #[test]
1256 fn a_foreground_paints_after_the_child() {
1257 let card = card().foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1258 assert_eq!(order(&card), vec![BACK, CHILD, OVER]);
1259 }
1260
1261 #[test]
1263 fn it_covers_the_container_s_own_box() {
1264 let card = card()
1265 .padding(10.0)
1266 .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1267 let over = scene(&card)
1268 .primitives()
1269 .iter()
1270 .find_map(|p| match p {
1271 Primitive::Rect {
1272 border_color,
1273 border_width,
1274 rect,
1275 ..
1276 } if *border_width > 0.0 && *border_color == OVER => Some(*rect),
1277 _ => None,
1278 })
1279 .expect("the foreground is painted");
1280 assert_eq!((over.width, over.height), (100.0, 100.0));
1281 }
1282
1283 #[test]
1286 fn a_silent_foreground_wears_the_container_s_corners() {
1287 let card = card()
1288 .radius(12.0)
1289 .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1290 assert_eq!(
1291 Widget::<()>::foreground(&card, &Theme::default())
1292 .unwrap()
1293 .radius,
1294 BorderRadius::uniform(12.0),
1295 "resolved against the container on the way out"
1296 );
1297 }
1298
1299 #[test]
1301 fn an_explicit_radius_is_left_alone() {
1302 let card = card().radius(12.0).foreground(
1303 BoxDecoration::default()
1304 .border(Border::new(2.0, OVER))
1305 .radius(4.0),
1306 );
1307 assert_eq!(
1308 Widget::<()>::foreground(&card, &Theme::default())
1309 .unwrap()
1310 .radius,
1311 BorderRadius::uniform(4.0)
1312 );
1313 }
1314}