1use frus_core::{Alignment, BoxDecoration, Curve, Rect, Scene, TextAlign, TextOverflow, TextStyle};
45use frus_layout::Style;
46
47use crate::interaction::Status;
48use crate::theme::Theme;
49use crate::widget::Widget;
50
51pub struct FadeTransition<Msg = crate::callback::Callback> {
66 inner: Box<dyn Widget<Msg>>,
67}
68
69impl<Msg: Clone + 'static> FadeTransition<Msg> {
70 pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
72 Self {
73 inner: Box::new(crate::Opacity::new(opacity.clamp(0.0, 1.0), child)),
74 }
75 }
76}
77
78impl<Msg> FadeTransition<Msg> {
79 fn restyle(&self, base: Style) -> Style {
81 base
82 }
83}
84
85crate::transparent::forward_transparent!(FadeTransition {
86 fn key(&self) -> Option<u64> {
89 self.inner.key()
90 }
91 fn positioned(&self) -> Option<crate::positioned::Positioning> {
92 self.inner.positioned()
93 }
94 fn theme_override(
95 &self,
96 inherited: &crate::theme::Theme,
97 ) -> Option<Box<crate::theme::Theme>> {
98 self.inner.theme_override(inherited)
99 }
100 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
101 self.inner.media_override(inherited)
102 }
103 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
104 self.inner.scaffold_override()
105 }
106 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
107 self.inner.scroll_behavior_override()
108 }
109 fn autofill_group(&self) -> bool {
111 self.inner.autofill_group()
112 }
113 fn area_toolbar(
115 &self,
116 context: crate::ToolbarContext,
117 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
118 self.inner.area_toolbar(context)
119 }
120});
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum SlideFrom {
125 Left,
127 Right,
129 Top,
131 Bottom,
133}
134
135pub struct SlideTransition<Msg = crate::callback::Callback> {
157 inner: Box<dyn Widget<Msg>>,
158}
159
160impl<Msg: Clone + 'static> SlideTransition<Msg> {
161 pub fn new(fx: f32, fy: f32, child: impl Widget<Msg> + 'static) -> Self {
163 Self {
164 inner: Box::new(crate::FractionalTranslation::new(fx, fy).child(child)),
165 }
166 }
167
168 pub fn from_edge(edge: SlideFrom, progress: f32, child: impl Widget<Msg> + 'static) -> Self {
173 let away = 1.0 - progress;
174 let (fx, fy) = match edge {
175 SlideFrom::Left => (-away, 0.0),
176 SlideFrom::Right => (away, 0.0),
177 SlideFrom::Top => (0.0, -away),
178 SlideFrom::Bottom => (0.0, away),
179 };
180 Self::new(fx, fy, child)
181 }
182}
183
184impl<Msg> SlideTransition<Msg> {
185 fn restyle(&self, base: Style) -> Style {
187 base
188 }
189}
190
191crate::transparent::forward_transparent!(SlideTransition {
192 fn key(&self) -> Option<u64> {
195 self.inner.key()
196 }
197 fn positioned(&self) -> Option<crate::positioned::Positioning> {
198 self.inner.positioned()
199 }
200 fn theme_override(
201 &self,
202 inherited: &crate::theme::Theme,
203 ) -> Option<Box<crate::theme::Theme>> {
204 self.inner.theme_override(inherited)
205 }
206 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
207 self.inner.media_override(inherited)
208 }
209 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
210 self.inner.scaffold_override()
211 }
212 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
213 self.inner.scroll_behavior_override()
214 }
215 fn autofill_group(&self) -> bool {
217 self.inner.autofill_group()
218 }
219 fn area_toolbar(
221 &self,
222 context: crate::ToolbarContext,
223 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
224 self.inner.area_toolbar(context)
225 }
226});
227
228pub struct ScaleTransition<Msg = crate::callback::Callback> {
245 inner: Box<dyn Widget<Msg>>,
246}
247
248impl<Msg: Clone + 'static> ScaleTransition<Msg> {
249 pub fn new(scale: f32, child: impl Widget<Msg> + 'static) -> Self {
251 Self::about(scale, Alignment::CENTER, child)
252 }
253
254 pub fn about(scale: f32, pivot: Alignment, child: impl Widget<Msg> + 'static) -> Self {
256 Self {
257 inner: Box::new(
258 crate::Transform::scale_xy_from(scale, scale, pivot)
259 .child(child),
263 ),
264 }
265 }
266}
267
268impl<Msg> ScaleTransition<Msg> {
269 fn restyle(&self, base: Style) -> Style {
271 base
272 }
273}
274
275crate::transparent::forward_transparent!(ScaleTransition {
276 fn key(&self) -> Option<u64> {
279 self.inner.key()
280 }
281 fn positioned(&self) -> Option<crate::positioned::Positioning> {
282 self.inner.positioned()
283 }
284 fn theme_override(
285 &self,
286 inherited: &crate::theme::Theme,
287 ) -> Option<Box<crate::theme::Theme>> {
288 self.inner.theme_override(inherited)
289 }
290 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
291 self.inner.media_override(inherited)
292 }
293 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
294 self.inner.scaffold_override()
295 }
296 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
297 self.inner.scroll_behavior_override()
298 }
299 fn autofill_group(&self) -> bool {
301 self.inner.autofill_group()
302 }
303 fn area_toolbar(
305 &self,
306 context: crate::ToolbarContext,
307 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
308 self.inner.area_toolbar(context)
309 }
310});
311
312pub fn eased(progress: f32, curve: &Curve) -> f32 {
320 curve.transform(progress.clamp(0.0, 1.0))
321}
322
323#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
325pub enum DecorationPosition {
326 #[default]
328 Background,
329 Foreground,
332}
333
334pub struct DecoratedBoxTransition<Msg = crate::callback::Callback> {
363 children: Vec<Box<dyn Widget<Msg>>>,
364 decoration: BoxDecoration,
365 position: DecorationPosition,
366}
367
368impl<Msg> DecoratedBoxTransition<Msg> {
369 pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
371 Self {
372 children: vec![Box::new(child)],
373 decoration,
374 position: DecorationPosition::Background,
375 }
376 }
377
378 pub fn between(
381 from: BoxDecoration,
382 to: BoxDecoration,
383 progress: f32,
384 child: impl Widget<Msg> + 'static,
385 ) -> Self {
386 Self::new(from.lerp(to, progress), child)
387 }
388
389 pub fn position(mut self, position: DecorationPosition) -> Self {
391 self.position = position;
392 self
393 }
394}
395
396impl<Msg: Clone> Widget<Msg> for DecoratedBoxTransition<Msg> {
397 fn style(&self) -> Style {
398 Style::default()
400 }
401
402 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
403 &self.children
404 }
405
406 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
407 if self.position == DecorationPosition::Background {
408 self.decoration
409 .paint_into_in(scene, bounds, status.opacity, theme.direction);
410 }
411 }
412
413 fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
415 (self.position == DecorationPosition::Foreground).then_some(self.decoration.clone())
416 }
417
418 fn on_click(&self) -> Option<Msg> {
419 None
420 }
421
422 fn debug_name(&self) -> &'static str {
423 "DecoratedBoxTransition"
424 }
425}
426
427pub struct DefaultTextStyleTransition<Msg = crate::callback::Callback> {
456 handed: crate::widgettheme::DefaultTextStyle,
457 inner: Box<dyn Widget<Msg>>,
458}
459
460impl<Msg: Clone + 'static> DefaultTextStyleTransition<Msg> {
461 pub fn new(style: TextStyle, child: impl Widget<Msg> + 'static) -> Self {
463 Self {
464 handed: crate::widgettheme::DefaultTextStyle::from_text_style(style),
465 inner: Box::new(child),
466 }
467 }
468
469 pub fn between(
471 from: TextStyle,
472 to: TextStyle,
473 progress: f32,
474 child: impl Widget<Msg> + 'static,
475 ) -> Self {
476 Self::new(from.lerp(to, progress), child)
477 }
478
479 pub fn align(mut self, align: TextAlign) -> Self {
481 self.handed.align = Some(align);
482 self
483 }
484
485 pub fn soft_wrap(mut self, wrap: bool) -> Self {
487 self.handed.soft_wrap = Some(wrap);
488 self
489 }
490
491 pub fn overflow(mut self, overflow: TextOverflow) -> Self {
493 self.handed.overflow = Some(overflow);
494 self
495 }
496
497 pub fn max_lines(mut self, lines: usize) -> Self {
499 self.handed.max_lines = Some(lines);
500 self
501 }
502}
503
504impl<Msg> DefaultTextStyleTransition<Msg> {
505 fn restyle(&self, base: Style) -> Style {
508 base
509 }
510}
511
512crate::transparent::forward_transparent!(DefaultTextStyleTransition {
513 fn key(&self) -> Option<u64> {
515 self.inner.key()
516 }
517 fn positioned(&self) -> Option<crate::positioned::Positioning> {
518 self.inner.positioned()
519 }
520
521 fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
525 let mut mine = inherited.clone();
526 mine.widgets.text = mine.widgets.text.merge(self.handed);
527 Some(
528 self.inner
529 .theme_override(&mine)
530 .unwrap_or_else(|| Box::new(mine)),
531 )
532 }
533
534 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
535 self.inner.media_override(inherited)
536 }
537 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
538 self.inner.scaffold_override()
539 }
540 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
541 self.inner.scroll_behavior_override()
542 }
543 fn autofill_group(&self) -> bool {
545 self.inner.autofill_group()
546 }
547 fn area_toolbar(
549 &self,
550 context: crate::ToolbarContext,
551 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
552 self.inner.area_toolbar(context)
553 }
554});
555
556pub struct SizeTransition<Msg = crate::callback::Callback> {
570 factor: f32,
571 axis: crate::scroll::Axis,
572 axis_alignment: f32,
573 children: Vec<Box<dyn Widget<Msg>>>,
574}
575
576impl<Msg> SizeTransition<Msg> {
577 pub fn new(factor: f32, child: impl Widget<Msg> + 'static) -> Self {
579 Self {
580 factor,
581 axis: crate::scroll::Axis::Vertical,
582 axis_alignment: 0.0,
583 children: vec![Box::new(child)],
584 }
585 }
586
587 pub fn axis(mut self, axis: crate::scroll::Axis) -> Self {
589 self.axis = axis;
590 self
591 }
592
593 pub fn axis_alignment(mut self, alignment: f32) -> Self {
596 self.axis_alignment = alignment;
597 self
598 }
599
600 fn vertical(&self) -> bool {
601 !matches!(self.axis, crate::scroll::Axis::Horizontal)
602 }
603}
604
605impl<Msg: Clone> Widget<Msg> for SizeTransition<Msg> {
606 fn style(&self) -> Style {
607 Style::default()
608 }
609
610 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
611 &self.children
612 }
613
614 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
615
616 fn on_click(&self) -> Option<Msg> {
617 None
618 }
619
620 fn constraints_transform(&self) -> Option<crate::constraints::ConstraintsTransform> {
622 use crate::constraints::AxisConstraint::{AsGiven, Unbounded};
623 let (width, height, alignment) = if self.vertical() {
624 (
625 AsGiven,
626 Unbounded,
627 Alignment {
628 x: -1.0,
629 y: self.axis_alignment,
630 },
631 )
632 } else {
633 (
634 Unbounded,
635 AsGiven,
636 Alignment {
637 x: self.axis_alignment,
638 y: -1.0,
639 },
640 )
641 };
642 Some(crate::constraints::ConstraintsTransform {
643 width,
644 height,
645 alignment,
646 report: false,
647 })
648 }
649
650 fn size_factor(&self) -> Option<(f32, f32)> {
651 let factor = self.factor.max(0.0);
652 Some(if self.vertical() {
653 (1.0, factor)
654 } else {
655 (factor, 1.0)
656 })
657 }
658
659 fn clips_to_box(&self) -> bool {
660 true
661 }
662
663 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
664 crate::widget::FillAxes {
665 horizontal: self.vertical(),
666 vertical: !self.vertical(),
667 }
668 }
669
670 fn debug_name(&self) -> &'static str {
671 "SizeTransition"
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::{build_ui, Container, Runtime, Theme};
679 use frus_core::{Color, Primitive, Rect, Size};
680
681 const RED: Color = Color {
682 r: 1.0,
683 g: 0.0,
684 b: 0.0,
685 a: 1.0,
686 };
687
688 const BLUE: Color = Color {
689 r: 0.0,
690 g: 0.0,
691 b: 1.0,
692 a: 1.0,
693 };
694
695 fn mark() -> Container<()> {
696 Container::new().width(40.0).height(20.0).color(RED)
697 }
698
699 fn red(primitives: &[Primitive]) -> Option<(Rect, Rect)> {
701 primitives.iter().find_map(|p| match p {
702 Primitive::Rect {
703 rect, clip, color, ..
704 } if color.r > 0.5 && color.g < 0.5 => Some((*rect, *clip)),
705 Primitive::Layer { primitives, .. } => red(primitives),
706 _ => None,
707 })
708 }
709
710 fn revealed(factor: f32, alignment: f32) -> crate::Ui<()> {
711 let root = crate::Flex::<()>::column()
712 .child(SizeTransition::new(factor, mark()).axis_alignment(alignment))
713 .child(Container::new().width(40.0).height(10.0).color(BLUE));
714 build_ui(
715 &root,
716 Size::new(100.0, 100.0),
717 &Runtime::default(),
718 &Theme::dark(),
719 )
720 }
721
722 #[test]
725 fn a_size_transition_is_a_fraction_of_its_child() {
726 let ui = revealed(0.5, -1.0);
727 assert_eq!(blue_y(ui.scene().primitives()), Some(10.0));
728 let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
729 assert_eq!(rect.height, 20.0, "uncovered, not squeezed");
730 assert_eq!((rect.y, clip.y + clip.height), (0.0, 10.0), "its top half");
731 }
732
733 #[test]
735 fn the_axis_alignment_chooses_what_shows() {
736 let ui = revealed(0.5, 1.0);
737 let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
738 assert_eq!(
739 rect.y, -10.0,
740 "the mark moved up by the half that is hidden"
741 );
742 assert_eq!(clip.y + clip.height, 10.0);
743 }
744
745 #[test]
748 fn zero_is_nothing_and_one_is_all() {
749 assert_eq!(blue_y(revealed(0.0, -1.0).scene().primitives()), Some(0.0));
750 assert_eq!(blue_y(revealed(1.0, -1.0).scene().primitives()), Some(20.0));
751 }
752
753 #[test]
756 fn a_new_factor_is_a_new_box() {
757 let rt = Runtime::default();
758 let frame = |factor: f32| {
759 let root = crate::Flex::<()>::column()
760 .child(SizeTransition::new(factor, mark()))
761 .child(Container::new().width(40.0).height(10.0).color(BLUE));
762 let ui = build_ui(&root, Size::new(100.0, 100.0), &rt, &Theme::dark());
763 blue_y(ui.scene().primitives())
764 };
765 assert_eq!(frame(0.5), Some(10.0));
766 assert_eq!(frame(1.0), Some(20.0));
767 }
768
769 #[test]
772 fn across_the_axis_the_box_is_the_room() {
773 let root = Container::<()>::new()
774 .width(100.0)
775 .child(SizeTransition::new(
776 1.0,
777 Container::new().height(20.0).color(RED),
778 ));
779 let ui = build_ui(
780 &root,
781 Size::new(300.0, 100.0),
782 &Runtime::default(),
783 &Theme::dark(),
784 );
785 let (rect, _) = red(ui.scene().primitives()).expect("the mark");
786 assert_eq!(rect.width, 100.0);
787 }
788
789 #[test]
792 fn a_horizontal_size_transition_reveals_across() {
793 let root = crate::Flex::<()>::row()
794 .child(
795 SizeTransition::new(0.25, mark())
796 .axis(crate::scroll::Axis::Horizontal)
797 .axis_alignment(-1.0),
798 )
799 .child(Container::new().width(40.0).height(10.0).color(BLUE));
800 let ui = build_ui(
801 &root,
802 Size::new(100.0, 100.0),
803 &Runtime::default(),
804 &Theme::dark(),
805 );
806 fn blue_x(primitives: &[Primitive]) -> Option<f32> {
807 primitives.iter().find_map(|p| match p {
808 Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
809 Some(rect.x)
810 }
811 Primitive::Layer { primitives, .. } => blue_x(primitives),
812 _ => None,
813 })
814 }
815 assert_eq!(blue_x(ui.scene().primitives()), Some(10.0));
816 let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
817 assert_eq!(rect.width, 40.0);
818 assert_eq!(clip.x + clip.width, 10.0);
819 }
820
821 fn blue_y(primitives: &[Primitive]) -> Option<f32> {
823 for primitive in primitives {
824 match primitive {
825 Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
826 return Some(rect.y)
827 }
828 Primitive::Layer { primitives, .. } => {
829 if let Some(found) = blue_y(primitives) {
830 return Some(found);
831 }
832 }
833 _ => {}
834 }
835 }
836 None
837 }
838
839 fn painted(root: &dyn Widget<()>) -> (Rect, f32) {
842 painted_if_any(root).expect("the mark")
843 }
844
845 fn painted_if_any(root: &dyn Widget<()>) -> Option<(Rect, f32)> {
847 let ui = build_ui(
848 root,
849 Size::new(100.0, 100.0),
850 &Runtime::default(),
851 &Theme::dark(),
852 );
853 fn walk(primitives: &[Primitive], opacity: f32) -> Option<(Rect, f32)> {
854 for primitive in primitives {
855 match primitive {
856 Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
857 return Some((*rect, opacity))
858 }
859 Primitive::Layer {
860 primitives,
861 opacity: layer,
862 ..
863 } => {
864 if let Some(found) = walk(primitives, opacity * layer) {
865 return Some(found);
866 }
867 }
868 _ => {}
869 }
870 }
871 None
872 }
873 walk(ui.scene().primitives(), 1.0)
874 }
875
876 #[test]
879 fn a_fade_is_the_value_it_was_given() {
880 assert_eq!(painted_if_any(&FadeTransition::new(0.0, mark())), None);
881 for (given, expected) in [(0.5, 0.5), (1.0, 1.0)] {
882 let root = FadeTransition::new(given, mark());
883 let (_, opacity) = painted(&root);
884 assert!(
885 (opacity - expected).abs() < 1e-3,
886 "at {given}: painted at {opacity}"
887 );
888 }
889 }
890
891 #[test]
894 fn a_slide_is_a_multiple_of_the_child_s_own_box() {
895 for (given, expected) in [(0.0, 0.0), (0.5, 20.0), (1.0, 40.0)] {
896 let root = crate::Flex::<()>::column().child(SlideTransition::new(given, 0.0, mark()));
897 let (rect, _) = painted(&root);
898 assert!(
899 (rect.x - expected).abs() < 0.5,
900 "at {given}: painted at {}",
901 rect.x
902 );
903 }
904 }
905
906 #[test]
909 fn coming_in_from_an_edge_arrives_at_one() {
910 let at = |progress: f32| {
911 let root = crate::Flex::<()>::column().child(SlideTransition::from_edge(
912 SlideFrom::Left,
913 progress,
914 mark(),
915 ));
916 painted(&root).0.x
917 };
918 assert!((at(0.0) + 40.0).abs() < 0.5, "a whole width outside");
919 assert!((at(0.5) + 20.0).abs() < 0.5, "half of one, still outside");
920 assert!(at(1.0).abs() < 0.5, "and home");
921
922 let mark_at = |edge: SlideFrom| {
924 let root =
925 crate::Flex::<()>::column().child(SlideTransition::from_edge(edge, 0.0, mark()));
926 let rect = painted(&root).0;
927 (rect.x, rect.y)
928 };
929 assert!(mark_at(SlideFrom::Left).0 < -1.0);
930 assert!(mark_at(SlideFrom::Right).0 > 1.0);
931 assert!(mark_at(SlideFrom::Top).1 < -1.0);
932 assert!(mark_at(SlideFrom::Bottom).1 > 1.0);
933 }
934
935 #[test]
939 fn a_scale_is_paint_and_not_layout() {
940 let built = |scale: f32| {
941 let root = crate::Flex::<()>::column()
942 .width(100.0)
943 .child(ScaleTransition::new(scale, mark()))
944 .child(Container::<()>::new().width(10.0).height(10.0).color(BLUE));
945 let ui = build_ui(
946 &root,
947 Size::new(100.0, 100.0),
948 &Runtime::default(),
949 &Theme::dark(),
950 );
951 let matrix = ui.scene().primitives().iter().find_map(|p| match p {
952 Primitive::Layer {
953 transform: Some(t), ..
954 } => Some(t.affine.m[0]),
955 _ => None,
956 });
957 let neighbour = blue_y(ui.scene().primitives()).expect("the neighbour");
958 (matrix, neighbour)
959 };
960 for (given, expected) in [(0.5, Some(0.5)), (1.0, None), (2.0, Some(2.0))] {
961 let (matrix, neighbour) = built(given);
962 match (matrix, expected) {
963 (Some(m), Some(e)) => {
964 assert!((m - e).abs() < 1e-3, "at {given}: painted at {m}")
965 }
966 (None, None) => {}
967 _ => panic!("at {given}: {matrix:?} where {expected:?} was expected"),
968 }
969 assert!(
970 (neighbour - 20.0).abs() < 0.5,
971 "the neighbour stayed where the layout put it: {neighbour}"
972 );
973 }
974 }
975
976 #[test]
979 fn easing_a_progress_is_the_frameworks_own_curve() {
980 let curve = Curve::ease_out();
981 assert!(eased(0.0, &curve).abs() < 1e-3);
982 assert!((eased(1.0, &curve) - 1.0).abs() < 1e-3);
983 assert_eq!(eased(0.5, &curve), curve.transform(0.5));
984 assert!(
985 eased(0.5, &curve) > 0.5,
986 "an ease-out is ahead at the middle"
987 );
988 assert_eq!(
989 eased(2.0, &curve),
990 eased(1.0, &curve),
991 "and a progress past its end is its end"
992 );
993 }
994
995 use frus_core::{Border, TextStyle};
998
999 fn rects_of(root: &dyn Widget<()>) -> Vec<(Rect, Color)> {
1001 let ui = build_ui(
1002 root,
1003 Size::new(200.0, 200.0),
1004 &Runtime::default(),
1005 &Theme::dark(),
1006 );
1007 fn walk(primitives: &[Primitive], out: &mut Vec<(Rect, Color)>) {
1008 for primitive in primitives {
1009 match primitive {
1010 Primitive::Rect { rect, color, .. } => out.push((*rect, *color)),
1011 Primitive::Layer { primitives, .. } => walk(primitives, out),
1012 _ => {}
1013 }
1014 }
1015 }
1016 let mut out = Vec::new();
1017 walk(ui.scene().primitives(), &mut out);
1018 out
1019 }
1020
1021 fn texts_of(root: &dyn Widget<()>) -> Vec<(f32, Color)> {
1023 let ui = build_ui(
1024 root,
1025 Size::new(200.0, 200.0),
1026 &Runtime::default(),
1027 &Theme::dark(),
1028 );
1029 fn walk(primitives: &[Primitive], out: &mut Vec<(f32, Color)>) {
1030 for primitive in primitives {
1031 match primitive {
1032 Primitive::Text { size, color, .. } => out.push((*size, *color)),
1033 Primitive::Layer { primitives, .. } => walk(primitives, out),
1034 _ => {}
1035 }
1036 }
1037 }
1038 let mut out = Vec::new();
1039 walk(ui.scene().primitives(), &mut out);
1040 out
1041 }
1042
1043 fn close(a: Color, b: Color) -> bool {
1044 (a.r - b.r).abs() < 1e-4
1045 && (a.g - b.g).abs() < 1e-4
1046 && (a.b - b.b).abs() < 1e-4
1047 && (a.a - b.a).abs() < 1e-4
1048 }
1049
1050 #[test]
1053 fn a_decoration_is_painted_as_it_stands() {
1054 for (t, want) in [(0.0, RED), (0.5, RED.lerp(BLUE, 0.5)), (1.0, BLUE)] {
1055 let tree = DecoratedBoxTransition::<()>::between(
1056 BoxDecoration::filled(RED),
1057 BoxDecoration::filled(BLUE),
1058 t,
1059 Container::new().width(40.0).height(20.0),
1060 );
1061 let rects = rects_of(&tree);
1062 assert_eq!(rects.len(), 1, "at {t}: {rects:?}");
1063 assert!(close(rects[0].1, want), "at {t}: {:?}", rects[0].1);
1064 }
1065 }
1066
1067 #[test]
1069 fn a_fading_fill_is_painted_in_its_own_colour() {
1070 let tree = DecoratedBoxTransition::<()>::between(
1071 BoxDecoration::filled(RED),
1072 BoxDecoration::default(),
1073 0.5,
1074 Container::new().width(40.0).height(20.0),
1075 );
1076 let rects = rects_of(&tree);
1077 assert_eq!(rects.len(), 1, "{rects:?}");
1078 assert!(close(rects[0].1, RED.fade(0.5)), "{:?}", rects[0].1);
1079 }
1080
1081 #[test]
1085 fn a_thickening_border_does_not_move_the_child() {
1086 let child_at = |t: f32| {
1087 let tree = DecoratedBoxTransition::<()>::between(
1088 BoxDecoration::default(),
1089 BoxDecoration::default().border(Border::new(8.0, BLUE)),
1090 t,
1091 mark(),
1092 );
1093 rects_of(&tree)
1094 .into_iter()
1095 .find(|(_, c)| close(*c, RED))
1096 .expect("the child")
1097 .0
1098 };
1099 let (start, end) = (child_at(0.0), child_at(1.0));
1100 assert_eq!(
1101 (start.x, start.y),
1102 (end.x, end.y),
1103 "the child stayed put: {start:?} then {end:?}"
1104 );
1105 }
1106
1107 #[test]
1109 fn in_front_the_decoration_covers_the_child() {
1110 let wash = || BoxDecoration::filled(BLUE.fade(0.3));
1111 let order = |tree: &DecoratedBoxTransition<()>| {
1112 let rects = rects_of(tree);
1113 let child = rects.iter().position(|(_, c)| close(*c, RED));
1114 let over = rects.iter().position(|(_, c)| c.b > 0.5);
1115 (child.expect("the child"), over.expect("the wash"))
1116 };
1117 let (child, over) = order(
1118 &DecoratedBoxTransition::new(wash(), mark()).position(DecorationPosition::Foreground),
1119 );
1120 assert!(child < over, "in front: the wash comes after the child");
1121 let (child, under) = order(&DecoratedBoxTransition::new(wash(), mark()));
1122 assert!(under < child, "behind, by default");
1123 }
1124
1125 #[test]
1127 fn a_text_style_is_handed_down_as_it_stands() {
1128 for (t, want) in [(0.0, 12.0), (0.5, 18.0), (1.0, 24.0)] {
1129 let tree = DefaultTextStyleTransition::<()>::between(
1130 TextStyle::NONE.size(12.0),
1131 TextStyle::NONE.size(24.0),
1132 t,
1133 crate::Text::new("Ag"),
1134 );
1135 let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
1136 assert_eq!(sizes, vec![want], "at {t}");
1137 }
1138 }
1139
1140 #[test]
1145 fn two_nested_hand_down_both_halves() {
1146 let tree = DefaultTextStyleTransition::<()>::new(
1147 TextStyle::NONE.color(BLUE),
1148 DefaultTextStyleTransition::new(TextStyle::NONE.size(20.0), crate::Text::new("Ag")),
1149 );
1150 let texts = texts_of(&tree);
1151 assert_eq!(texts.len(), 1, "{texts:?}");
1152 assert_eq!(texts[0].0, 20.0, "the inner one's size");
1153 assert!(close(texts[0].1, BLUE), "and the outer one's colour");
1154 }
1155
1156 #[test]
1159 fn a_text_that_names_its_size_keeps_it() {
1160 let tree = DefaultTextStyleTransition::<()>::new(
1161 TextStyle::NONE.size(30.0),
1162 crate::Text::new("Ag").size(11.0),
1163 );
1164 let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
1165 assert_eq!(sizes, vec![11.0]);
1166 }
1167}