1use frus_core::{
10 BorderRadius, Color, Curve, Rect, Scene, Size, TextAlign, TextOverflow, TextStyle,
11};
12use frus_layout::Style;
13
14use crate::container::Container;
15use crate::interaction::Status;
16use crate::theme::Theme;
17use crate::widget::Widget;
18
19macro_rules! forward_to_container {
24 ($ty:ident) => {
25 impl<Msg: Clone + 'static> Widget<Msg> for $ty<Msg> {
30 fn style(&self) -> Style {
31 Widget::style(&self.inner)
32 }
33 fn style_themed(&self, theme: &Theme) -> Style {
34 Widget::style_themed(&self.inner, theme)
35 }
36 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
37 Widget::children(&self.inner)
38 }
39 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
40 Widget::paint(&self.inner, bounds, status, theme, scene)
41 }
42 fn on_click(&self) -> Option<Msg> {
43 Widget::on_click(&self.inner)
44 }
45 fn opaque(&self) -> bool {
46 Widget::opaque(&self.inner)
47 }
48 fn on_long_press(&self) -> Option<Msg> {
49 Widget::on_long_press(&self.inner)
50 }
51 fn repaint_boundary(&self) -> bool {
52 Widget::repaint_boundary(&self.inner)
53 }
54 fn opacity_group(&self) -> Option<f32> {
55 Widget::opacity_group(&self.inner)
56 }
57 fn anim_target(&self) -> Option<f32> {
58 Widget::anim_target(&self.inner)
59 }
60 fn anim_transform(&self) -> Option<$crate::runtime::TransformValues> {
61 Widget::anim_transform(&self.inner)
62 }
63 fn anim_color(&self) -> Option<Color> {
64 Widget::anim_color(&self.inner)
65 }
66 fn anim_size(&self) -> Option<Size> {
67 Widget::anim_size(&self.inner)
68 }
69 fn anim_radius(&self) -> Option<BorderRadius> {
70 Widget::anim_radius(&self.inner)
71 }
72 fn anim_padding(&self) -> Option<frus_core::Insets> {
76 Widget::anim_padding(&self.inner)
77 }
78 fn anim_offset(&self) -> Option<(f32, f32)> {
82 Widget::anim_offset(&self.inner)
83 }
84 fn anim_duration(&self) -> f32 {
85 Widget::anim_duration(&self.inner)
86 }
87 fn anim_curve(&self) -> Curve {
88 Widget::anim_curve(&self.inner)
89 }
90 fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
91 Widget::alignment_geometry(&self.inner)
92 }
93 fn transform_translate(&self) -> Option<(f32, f32)> {
94 Widget::transform_translate(&self.inner)
95 }
96 fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
97 Widget::transform_scale(&self.inner)
98 }
99 fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
100 Widget::transform_rotate(&self.inner)
101 }
102 fn clip_shape(&self) -> Option<frus_core::ClipShape> {
103 Widget::clip_shape(&self.inner)
104 }
105 fn clip_path(&self) -> Option<&frus_core::Path> {
106 Widget::clip_path(&self.inner)
107 }
108 fn barrier(&self) -> Option<$crate::barrier::ModalBarrier> {
109 Widget::barrier(&self.inner)
110 }
111 fn interactive(&self) -> Option<(f32, f32)> {
112 Widget::interactive(&self.inner)
113 }
114 fn fitted(&self) -> Option<frus_core::BoxFit> {
115 Widget::fitted(&self.inner)
116 }
117 fn rotated_quarter_turns(&self) -> Option<i32> {
118 Widget::rotated_quarter_turns(&self.inner)
119 }
120 }
121 };
122}
123
124pub struct Opacity<Msg = crate::callback::Callback> {
127 inner: Container<Msg>,
128}
129
130impl<Msg: Clone + 'static> Opacity<Msg> {
131 pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
133 Self {
134 inner: Container::new().opacity(opacity).child(child),
135 }
136 }
137}
138
139forward_to_container!(Opacity);
140
141pub struct AnimatedOpacity<Msg = crate::callback::Callback> {
144 inner: Container<Msg>,
145}
146
147impl<Msg: Clone + 'static> AnimatedOpacity<Msg> {
148 pub fn new(
150 opacity: f32,
151 duration: f32,
152 curve: Curve,
153 child: impl Widget<Msg> + 'static,
154 ) -> Self {
155 Self {
156 inner: Container::new()
157 .animated_opacity(opacity, duration, curve)
158 .child(child),
159 }
160 }
161}
162
163forward_to_container!(AnimatedOpacity);
164
165pub struct AnimatedContainer<Msg = crate::callback::Callback> {
177 inner: Container<Msg>,
178 duration: f32,
179 curve: Curve,
180}
181
182impl<Msg: Clone + 'static> AnimatedContainer<Msg> {
183 pub fn new(duration: f32, curve: Curve) -> Self {
186 Self {
187 inner: Container::new(),
188 duration,
189 curve,
190 }
191 }
192
193 pub fn color(mut self, color: Color) -> Self {
195 self.inner = self
196 .inner
197 .animated_color(color, self.duration, self.curve.clone());
198 self
199 }
200
201 pub fn size(mut self, width: f32, height: f32) -> Self {
203 self.inner = self
204 .inner
205 .animated_size(width, height, self.duration, self.curve.clone());
206 self
207 }
208
209 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
211 self.inner = self
212 .inner
213 .animated_radius(radius, self.duration, self.curve.clone());
214 self
215 }
216
217 pub fn opacity(mut self, opacity: f32) -> Self {
219 self.inner = self
220 .inner
221 .animated_opacity(opacity, self.duration, self.curve.clone());
222 self
223 }
224
225 pub fn padding(mut self, padding: f32) -> Self {
227 self.inner = self.inner.padding(padding);
228 self
229 }
230
231 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
233 self.inner = self.inner.child(child);
234 self
235 }
236}
237
238forward_to_container!(AnimatedContainer);
239
240pub struct AnimatedAlign<Msg = crate::callback::Callback> {
268 inner: Container<Msg>,
269}
270
271impl<Msg: Clone + 'static> AnimatedAlign<Msg> {
272 pub fn new(
274 alignment: impl Into<frus_core::AlignmentGeometry>,
275 duration: f32,
276 curve: Curve,
277 child: impl Widget<Msg> + 'static,
278 ) -> Self {
279 Self {
280 inner: Container::new()
281 .animated_alignment(alignment, duration, curve)
282 .child(child),
283 }
284 }
285}
286
287forward_to_container!(AnimatedAlign);
288
289pub struct AnimatedSlide<Msg = crate::callback::Callback> {
315 inner: Box<dyn Widget<Msg>>,
316}
317
318impl<Msg: Clone + 'static> AnimatedSlide<Msg> {
319 pub fn new(
321 fx: f32,
322 fy: f32,
323 duration: f32,
324 curve: Curve,
325 child: impl Widget<Msg> + 'static,
326 ) -> Self {
327 Self {
328 inner: Box::new(
329 crate::FractionalTranslation::new(fx, fy)
330 .animated(duration, curve)
331 .child(child),
332 ),
333 }
334 }
335}
336
337impl<Msg> AnimatedSlide<Msg> {
338 fn restyle(&self, base: Style) -> Style {
342 base
343 }
344}
345
346crate::transparent::forward_transparent!(AnimatedSlide {
347 fn key(&self) -> Option<u64> {
350 self.inner.key()
351 }
352 fn positioned(&self) -> Option<crate::positioned::Positioning> {
353 self.inner.positioned()
354 }
355 fn theme_override(
356 &self,
357 inherited: &crate::theme::Theme,
358 ) -> Option<Box<crate::theme::Theme>> {
359 self.inner.theme_override(inherited)
360 }
361 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
362 self.inner.media_override(inherited)
363 }
364 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
365 self.inner.scaffold_override()
366 }
367 fn autofill_group(&self) -> bool {
369 self.inner.autofill_group()
370 }
371});
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
380 fn animated_container_declares_all_targets() {
381 let blue = Color::rgb(0.0, 0.0, 1.0);
382 let w: AnimatedContainer<()> = AnimatedContainer::new(0.25, Curve::ease_out())
383 .color(blue)
384 .size(200.0, 100.0)
385 .radius(12.0)
386 .opacity(0.5);
387 assert_eq!(Widget::<()>::anim_color(&w), Some(blue));
388 assert_eq!(Widget::<()>::anim_size(&w), Some(Size::new(200.0, 100.0)));
389 assert_eq!(
390 Widget::<()>::anim_radius(&w),
391 Some(BorderRadius::from(12.0))
392 );
393 assert_eq!(Widget::<()>::anim_target(&w), Some(0.5)); assert_eq!(Widget::<()>::opacity_group(&w), Some(0.5));
395 assert_eq!(Widget::<()>::anim_duration(&w), 0.25);
396 assert_eq!(Widget::<()>::anim_curve(&w), Curve::ease_out());
397 }
398
399 #[test]
402 fn opacity_wraps_child_as_a_group() {
403 let w: Opacity<()> = Opacity::new(0.4, crate::Container::new().width(10.0).height(10.0));
404 assert_eq!(Widget::<()>::opacity_group(&w), Some(0.4));
405 assert_eq!(Widget::<()>::anim_target(&w), None);
406 assert_eq!(
407 Widget::<()>::children(&w).len(),
408 1,
409 "the child is a separate node"
410 );
411 }
412
413 #[test]
415 fn animated_opacity_declares_a_group_target() {
416 let w: AnimatedOpacity<()> =
417 AnimatedOpacity::new(0.0, 0.2, Curve::ease_in(), crate::Container::new());
418 assert_eq!(Widget::<()>::opacity_group(&w), Some(0.0));
419 assert_eq!(Widget::<()>::anim_target(&w), Some(0.0));
420 assert_eq!(Widget::<()>::anim_duration(&w), 0.2);
421 assert_eq!(Widget::<()>::debug_name(&w), "AnimatedOpacity");
423 }
424}
425
426pub struct AnimatedScale<Msg = crate::callback::Callback> {
446 inner: Box<dyn Widget<Msg>>,
447}
448
449impl<Msg: Clone + 'static> AnimatedScale<Msg> {
450 pub fn new(scale: f32, duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
452 Self::about(scale, frus_core::Alignment::CENTER, duration, curve, child)
453 }
454
455 pub fn about(
462 scale: f32,
463 pivot: frus_core::Alignment,
464 duration: f32,
465 curve: Curve,
466 child: impl Widget<Msg> + 'static,
467 ) -> Self {
468 Self {
469 inner: Box::new(
470 crate::Transform::scale_xy_from(scale, scale, pivot)
471 .animated(duration, curve)
472 .child(child),
473 ),
474 }
475 }
476}
477
478impl<Msg> AnimatedScale<Msg> {
479 fn restyle(&self, base: Style) -> Style {
483 base
484 }
485}
486
487crate::transparent::forward_transparent!(AnimatedScale {
488 fn key(&self) -> Option<u64> {
491 self.inner.key()
492 }
493 fn positioned(&self) -> Option<crate::positioned::Positioning> {
494 self.inner.positioned()
495 }
496 fn theme_override(
497 &self,
498 inherited: &crate::theme::Theme,
499 ) -> Option<Box<crate::theme::Theme>> {
500 self.inner.theme_override(inherited)
501 }
502 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
503 self.inner.media_override(inherited)
504 }
505 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
506 self.inner.scaffold_override()
507 }
508 fn autofill_group(&self) -> bool {
510 self.inner.autofill_group()
511 }
512});
513
514pub struct AnimatedRotation<Msg = crate::callback::Callback> {
534 inner: Box<dyn Widget<Msg>>,
535}
536
537impl<Msg: Clone + 'static> AnimatedRotation<Msg> {
538 pub fn new(
540 radians: f32,
541 duration: f32,
542 curve: Curve,
543 child: impl Widget<Msg> + 'static,
544 ) -> Self {
545 Self::about(
546 radians,
547 frus_core::Alignment::CENTER,
548 duration,
549 curve,
550 child,
551 )
552 }
553
554 pub fn about(
556 radians: f32,
557 pivot: frus_core::Alignment,
558 duration: f32,
559 curve: Curve,
560 child: impl Widget<Msg> + 'static,
561 ) -> Self {
562 Self {
563 inner: Box::new(
564 crate::Transform::rotate_from(radians, pivot)
565 .animated(duration, curve)
566 .child(child),
567 ),
568 }
569 }
570}
571
572impl<Msg> AnimatedRotation<Msg> {
573 fn restyle(&self, base: Style) -> Style {
577 base
578 }
579}
580
581crate::transparent::forward_transparent!(AnimatedRotation {
582 fn key(&self) -> Option<u64> {
585 self.inner.key()
586 }
587 fn positioned(&self) -> Option<crate::positioned::Positioning> {
588 self.inner.positioned()
589 }
590 fn theme_override(
591 &self,
592 inherited: &crate::theme::Theme,
593 ) -> Option<Box<crate::theme::Theme>> {
594 self.inner.theme_override(inherited)
595 }
596 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
597 self.inner.media_override(inherited)
598 }
599 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
600 self.inner.scaffold_override()
601 }
602 fn autofill_group(&self) -> bool {
604 self.inner.autofill_group()
605 }
606});
607
608pub struct AnimatedPadding<Msg = crate::callback::Callback> {
629 inner: Container<Msg>,
630}
631
632impl<Msg: Clone + 'static> AnimatedPadding<Msg> {
633 pub fn new(
635 padding: f32,
636 duration: f32,
637 curve: Curve,
638 child: impl Widget<Msg> + 'static,
639 ) -> Self {
640 Self {
641 inner: Container::new()
642 .animated_padding(padding, duration, curve)
643 .child(child),
644 }
645 }
646}
647
648forward_to_container!(AnimatedPadding);
649
650pub struct AnimatedPositioned<Msg = crate::callback::Callback> {
686 children: Vec<Box<dyn Widget<Msg>>>,
687 spec: crate::positioned::Positioning,
688 duration: f32,
689 curve: Curve,
690}
691
692impl<Msg> AnimatedPositioned<Msg> {
693 pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
696 Self {
697 children: vec![Box::new(child)],
698 spec: crate::positioned::Positioning::default(),
699 duration,
700 curve,
701 }
702 }
703
704 pub fn left(mut self, px: f32) -> Self {
706 self.spec.left = Some(px);
707 self
708 }
709
710 pub fn top(mut self, px: f32) -> Self {
712 self.spec.top = Some(px);
713 self
714 }
715
716 pub fn right(mut self, px: f32) -> Self {
718 self.spec.right = Some(px);
719 self
720 }
721
722 pub fn bottom(mut self, px: f32) -> Self {
724 self.spec.bottom = Some(px);
725 self
726 }
727
728 pub fn width(mut self, px: f32) -> Self {
730 self.spec.width = Some(px);
731 self
732 }
733
734 pub fn height(mut self, px: f32) -> Self {
736 self.spec.height = Some(px);
737 self
738 }
739}
740
741impl<Msg: Clone> Widget<Msg> for AnimatedPositioned<Msg> {
742 fn style(&self) -> Style {
743 Style::default()
746 }
747
748 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
749 &self.children
750 }
751
752 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
753 }
755
756 fn on_click(&self) -> Option<Msg> {
757 None
758 }
759
760 fn positioned(&self) -> Option<crate::positioned::Positioning> {
763 Some(self.spec)
764 }
765
766 fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
767 Some(self.spec)
768 }
769
770 fn anim_duration(&self) -> f32 {
771 self.duration
772 }
773
774 fn anim_curve(&self) -> Curve {
775 self.curve.clone()
776 }
777
778 fn debug_name(&self) -> &'static str {
779 "AnimatedPositioned"
780 }
781}
782
783pub struct AnimatedDefaultTextStyle<Msg = crate::callback::Callback> {
829 children: Vec<Box<dyn Widget<Msg>>>,
830 style: TextStyle,
831 align: Option<TextAlign>,
832 soft_wrap: Option<bool>,
833 overflow: Option<TextOverflow>,
834 max_lines: Option<usize>,
835 duration: f32,
836 curve: Curve,
837}
838
839impl<Msg> AnimatedDefaultTextStyle<Msg> {
840 pub fn new(
843 duration: f32,
844 curve: Curve,
845 style: TextStyle,
846 child: impl Widget<Msg> + 'static,
847 ) -> Self {
848 Self {
849 children: vec![Box::new(child)],
850 style,
851 align: None,
852 soft_wrap: None,
853 overflow: None,
854 max_lines: None,
855 duration,
856 curve,
857 }
858 }
859
860 pub fn align(mut self, align: TextAlign) -> Self {
862 self.align = Some(align);
863 self
864 }
865
866 pub fn soft_wrap(mut self, wrap: bool) -> Self {
868 self.soft_wrap = Some(wrap);
869 self
870 }
871
872 pub fn overflow(mut self, overflow: TextOverflow) -> Self {
874 self.overflow = Some(overflow);
875 self
876 }
877
878 pub fn max_lines(mut self, lines: usize) -> Self {
880 self.max_lines = Some(lines);
881 self
882 }
883
884 fn handed_down(&self) -> crate::widgettheme::DefaultTextStyle {
886 crate::widgettheme::DefaultTextStyle {
887 style: self.style,
888 align: self.align,
889 soft_wrap: self.soft_wrap,
890 overflow: self.overflow,
891 max_lines: self.max_lines,
892 }
893 }
894}
895
896impl<Msg: Clone> Widget<Msg> for AnimatedDefaultTextStyle<Msg> {
897 fn style(&self) -> Style {
898 Style::default()
901 }
902
903 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
904 &self.children
905 }
906
907 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
908 }
910
911 fn on_click(&self) -> Option<Msg> {
912 None
913 }
914
915 fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
924 let mut theme = inherited.clone();
925 theme.widgets.text = theme.widgets.text.merge(self.handed_down());
926 Some(Box::new(theme))
927 }
928
929 fn anim_text_style(&self) -> Option<TextStyle> {
930 Some(self.style)
931 }
932
933 fn anim_duration(&self) -> f32 {
934 self.duration
935 }
936
937 fn anim_curve(&self) -> Curve {
938 self.curve.clone()
939 }
940
941 fn debug_name(&self) -> &'static str {
942 "AnimatedDefaultTextStyle"
943 }
944}
945
946pub struct AnimatedFractionallySizedBox<Msg = crate::callback::Callback> {
969 inner: crate::fractional::FractionallySizedBox<Msg>,
970}
971
972impl<Msg: Clone + 'static> AnimatedFractionallySizedBox<Msg> {
973 pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
976 Self {
977 inner: crate::fractional::FractionallySizedBox::new()
978 .animated(duration, curve)
979 .child(child),
980 }
981 }
982
983 pub fn width_factor(mut self, factor: f32) -> Self {
985 self.inner = self.inner.width_factor(factor);
986 self
987 }
988
989 pub fn height_factor(mut self, factor: f32) -> Self {
991 self.inner = self.inner.height_factor(factor);
992 self
993 }
994}
995
996impl<Msg: Clone + 'static> Widget<Msg> for AnimatedFractionallySizedBox<Msg> {
1000 fn style(&self) -> Style {
1001 Widget::style(&self.inner)
1002 }
1003 fn style_themed(&self, theme: &Theme) -> Style {
1004 Widget::style_themed(&self.inner, theme)
1005 }
1006 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
1007 Widget::children(&self.inner)
1008 }
1009 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
1010 Widget::paint(&self.inner, bounds, status, theme, scene)
1011 }
1012 fn on_click(&self) -> Option<Msg> {
1013 Widget::on_click(&self.inner)
1014 }
1015 fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
1016 Widget::anim_fractions(&self.inner)
1017 }
1018 fn anim_duration(&self) -> f32 {
1019 Widget::anim_duration(&self.inner)
1020 }
1021 fn anim_curve(&self) -> Curve {
1022 Widget::anim_curve(&self.inner)
1023 }
1024}
1025
1026#[cfg(test)]
1027mod implicit_tests {
1028 use super::*;
1029 use crate::interaction::WidgetId;
1030 use crate::{build_ui, Runtime, Text};
1031 use frus_core::{Insets, Primitive};
1032
1033 fn scaled(to: f32) -> AnimatedScale<()> {
1035 AnimatedScale::new(to, 0.10, Curve::Linear, Text::new("x"))
1036 }
1037
1038 #[test]
1045 fn a_scale_mounts_settled_moves_and_arrives() {
1046 let mut rt = Runtime::default();
1047 assert!(
1048 !rt.advance_transforms(&scaled(2.0), 1.0),
1049 "a mount is not a transition"
1050 );
1051 let id = WidgetId::ROOT;
1052 assert_eq!(
1053 rt.anim_transform(id).map(|t| t.scale_x),
1054 Some(2.0),
1055 "and it adopts the target whole"
1056 );
1057
1058 assert!(rt.advance_transforms(&scaled(1.0), 0.05));
1060 let mid = rt.anim_transform(id).expect("in flight");
1061 assert!(
1062 (mid.scale_x - 1.5).abs() < 1e-3 && (mid.scale_y - 1.5).abs() < 1e-3,
1063 "halfway between the two: {mid:?}"
1064 );
1065 assert_eq!(mid.rotation, 0.0, "and it turned nothing on the way");
1066
1067 rt.advance_transforms(&scaled(1.0), 1.0);
1068 assert_eq!(rt.anim_transform(id).map(|t| t.scale_x), Some(1.0));
1069 assert!(
1070 !rt.advance_transforms(&scaled(1.0), 0.05),
1071 "and stops asking for frames once it is there"
1072 );
1073 }
1074
1075 #[test]
1079 fn a_rotation_mounts_settled_moves_and_arrives() {
1080 let turned = |to: f32| -> AnimatedRotation<()> {
1081 AnimatedRotation::new(to, 0.10, Curve::Linear, Text::new("x"))
1082 };
1083 let mut rt = Runtime::default();
1084 assert!(!rt.advance_transforms(&turned(0.0), 1.0));
1085 let id = WidgetId::ROOT;
1086 let mounted = rt.anim_transform(id).expect("mounted");
1087 assert_eq!((mounted.scale_x, mounted.scale_y), (1.0, 1.0), "identity");
1088
1089 assert!(rt.advance_transforms(&turned(1.0), 0.05));
1090 let mid = rt.anim_transform(id).expect("in flight");
1091 assert!((mid.rotation - 0.5).abs() < 1e-3, "halfway: {mid:?}");
1092 assert_eq!(
1093 (mid.scale_x, mid.scale_y),
1094 (1.0, 1.0),
1095 "and the scale stayed the identity, not nought"
1096 );
1097
1098 rt.advance_transforms(&turned(1.0), 1.0);
1099 assert_eq!(rt.anim_transform(id).map(|t| t.rotation), Some(1.0));
1100 }
1101
1102 #[test]
1105 fn a_padding_mounts_settled_moves_and_arrives() {
1106 let padded = |to: f32| -> AnimatedPadding<()> {
1107 AnimatedPadding::new(to, 0.10, Curve::Linear, Text::new("x"))
1108 };
1109 let mut rt = Runtime::default();
1110 assert!(!rt.advance_paddings(&padded(0.0), 1.0));
1111 let id = WidgetId::ROOT;
1112 assert_eq!(rt.anim_padding(id), Some(Insets::uniform(0.0)));
1113
1114 assert!(rt.advance_paddings(&padded(20.0), 0.05));
1115 let mid = rt.anim_padding(id).expect("in flight");
1116 assert!((mid.left - 10.0).abs() < 1e-3, "halfway: {mid:?}");
1117
1118 rt.advance_paddings(&padded(20.0), 1.0);
1119 assert_eq!(rt.anim_padding(id), Some(Insets::uniform(20.0)));
1120 }
1121
1122 #[test]
1126 fn the_paint_uses_the_tweened_transform_and_not_the_target() {
1127 let matrix_of = |rt: &Runtime, widget: &AnimatedScale<()>| {
1128 let ui = build_ui(widget, Size::new(100.0, 100.0), rt, &Theme::default());
1129 ui.scene()
1130 .primitives()
1131 .iter()
1132 .find_map(|p| match p {
1133 Primitive::Layer {
1134 transform: Some(t), ..
1135 } => Some(t.affine),
1136 _ => None,
1137 })
1138 .map(|m| m.m[0])
1139 };
1140 let mut rt = Runtime::default();
1141 rt.advance_transforms(&scaled(3.0), 1.0);
1142 rt.advance_transforms(&scaled(1.0), 0.05);
1144 let painted = matrix_of(&rt, &scaled(1.0)).expect("a transformed layer");
1145 assert!(
1146 (painted - 2.0).abs() < 1e-2,
1147 "the paint took the tweened scale (2), not the target (1): {painted}"
1148 );
1149
1150 let fresh = Runtime::default();
1153 let painted = matrix_of(&fresh, &scaled(3.0)).expect("a transformed layer");
1154 assert!((painted - 3.0).abs() < 1e-2, "the target: {painted}");
1155 }
1156
1157 fn anchored(to: frus_core::Alignment) -> AnimatedAlign<()> {
1159 AnimatedAlign::new(to, 0.10, Curve::Linear, Text::new("x"))
1160 }
1161
1162 #[test]
1164 fn an_anchor_mounts_settled_moves_and_arrives() {
1165 let mut rt = Runtime::default();
1166 assert!(
1167 !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_LEFT), 1.0),
1168 "a mount is not a transition"
1169 );
1170 let id = WidgetId::ROOT;
1171 assert_eq!(
1172 rt.anim_offset(id),
1173 Some((-1.0, 0.0)),
1174 "and it adopts the anchor whole"
1175 );
1176
1177 assert!(rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05));
1179 let mid = rt.anim_offset(id).expect("in flight");
1180 assert!(mid.0.abs() < 1e-3, "at the centre on the way past: {mid:?}");
1181 assert_eq!(mid.1, 0.0, "and it did not drift vertically");
1182
1183 rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 1.0);
1184 assert_eq!(rt.anim_offset(id), Some((1.0, 0.0)));
1185 assert!(
1186 !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05),
1187 "and stops asking for frames once it is there"
1188 );
1189 }
1190
1191 #[test]
1194 fn a_slide_mounts_settled_moves_and_arrives() {
1195 let slid = |fx: f32| -> AnimatedSlide<()> {
1196 AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, Text::new("x"))
1197 };
1198 let mut rt = Runtime::default();
1199 assert!(!rt.advance_offsets(&slid(-1.0), 1.0));
1200 let id = WidgetId::ROOT;
1201 assert_eq!(rt.anim_offset(id), Some((-1.0, 0.0)));
1202
1203 assert!(rt.advance_offsets(&slid(0.0), 0.05));
1204 let mid = rt.anim_offset(id).expect("in flight");
1205 assert!((mid.0 + 0.5).abs() < 1e-3, "halfway back: {mid:?}");
1206
1207 rt.advance_offsets(&slid(0.0), 1.0);
1208 assert_eq!(rt.anim_offset(id), Some((0.0, 0.0)));
1209 }
1210
1211 #[test]
1219 fn the_paint_uses_the_tweened_offset_and_not_the_target() {
1220 let mark = |root: &dyn Widget<()>, rt: &Runtime| {
1221 let ui = build_ui(root, Size::new(100.0, 40.0), rt, &Theme::default());
1222 ui.scene()
1223 .primitives()
1224 .iter()
1225 .find_map(|p| match p {
1226 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1227 _ => None,
1228 })
1229 .expect("the mark")
1230 };
1231 let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
1232 let mark_widget = || {
1233 crate::Container::<()>::new()
1234 .width(20.0)
1235 .height(20.0)
1236 .color(red)
1237 };
1238
1239 let aligned = |to: frus_core::Alignment| {
1242 crate::Container::<()>::new()
1243 .width(100.0)
1244 .height(40.0)
1245 .animated_alignment(to, 0.10, Curve::Linear)
1246 .child(mark_widget())
1247 };
1248 let mut rt = Runtime::default();
1249 rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_LEFT), 1.0);
1250 rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_RIGHT), 0.05);
1251 let x = mark(&aligned(frus_core::Alignment::CENTER_RIGHT), &rt);
1252 assert!(
1253 (x - 40.0).abs() < 0.5,
1254 "the anchor's tween reached the paint: {x}"
1255 );
1256
1257 let slid = |fx: f32| AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, mark_widget());
1259 let mut rt = Runtime::default();
1260 rt.advance_offsets(&slid(0.0), 1.0);
1261 rt.advance_offsets(&slid(1.0), 0.05);
1262 let x = mark(&slid(1.0), &rt);
1263 assert!(
1264 (x - 10.0).abs() < 0.5,
1265 "the slide's tween reached the paint: {x}"
1266 );
1267 }
1268
1269 #[test]
1273 fn a_directional_anchor_mirrors_as_a_whole() {
1274 let start = frus_core::AlignmentDirectional::CENTER_START;
1275 let end = frus_core::AlignmentDirectional::CENTER_END;
1276 let aligned = |to: frus_core::AlignmentDirectional| {
1277 crate::Container::<()>::new()
1278 .width(100.0)
1279 .height(40.0)
1280 .animated_alignment(to, 0.10, Curve::Linear)
1281 .child(
1282 crate::Container::<()>::new()
1283 .width(20.0)
1284 .height(20.0)
1285 .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1286 )
1287 };
1288 let mut rt = Runtime::default();
1289 rt.advance_offsets(&aligned(start), 1.0);
1290 rt.advance_offsets(&aligned(end), 0.025);
1292
1293 let x_in = |rtl: bool| {
1294 let theme = match rtl {
1295 true => Theme::default().rtl(),
1296 false => Theme::default(),
1297 };
1298 let ui = build_ui(&aligned(end), Size::new(100.0, 40.0), &rt, &theme);
1299 ui.scene()
1300 .primitives()
1301 .iter()
1302 .find_map(|p| match p {
1303 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1304 _ => None,
1305 })
1306 .expect("the mark")
1307 };
1308 let ltr = x_in(false);
1309 let rtl = x_in(true);
1310 assert!(
1311 (ltr - 20.0).abs() < 0.5,
1312 "a quarter across, from the left: {ltr}"
1313 );
1314 assert!(
1315 (rtl - 60.0).abs() < 0.5,
1316 "and the same quarter from the right: {rtl}"
1317 );
1318 }
1319
1320 fn pinned_at(left: f32) -> crate::Stack<()> {
1322 crate::Stack::new().layer(
1323 AnimatedPositioned::new(
1324 0.10,
1325 Curve::Linear,
1326 crate::Container::<()>::new()
1327 .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
1328 .child(Text::new("x")),
1329 )
1330 .left(left)
1331 .top(0.0)
1332 .width(20.0)
1333 .height(20.0),
1334 )
1335 }
1336
1337 #[test]
1344 fn a_layer_mounts_settled_moves_and_arrives() {
1345 let mut rt = Runtime::default();
1346 assert!(
1347 !rt.advance_pins(&pinned_at(0.0), 1.0),
1348 "a mount is not a transition"
1349 );
1350 let id = WidgetId::ROOT.child(0);
1352 assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(0.0));
1353
1354 assert!(rt.advance_pins(&pinned_at(80.0), 0.05));
1356 let mid = rt.anim_pins(id).expect("in flight");
1357 assert!(
1358 mid.left.is_some_and(|l| (l - 40.0).abs() < 1e-3),
1359 "halfway between the two: {mid:?}"
1360 );
1361 assert_eq!(
1362 mid.width,
1363 Some(20.0),
1364 "and the extents it never asked to move stayed"
1365 );
1366
1367 rt.advance_pins(&pinned_at(80.0), 1.0);
1368 assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(80.0));
1369 assert!(
1370 !rt.advance_pins(&pinned_at(80.0), 0.05),
1371 "and stops asking for frames once it is there"
1372 );
1373 }
1374
1375 #[test]
1382 fn a_pin_that_is_unset_takes_hold_and_lets_go_at_once() {
1383 let layer = |left: Option<f32>, right: Option<f32>| {
1384 let mut l: AnimatedPositioned<()> =
1385 AnimatedPositioned::new(0.10, Curve::Linear, Text::new("x")).top(0.0);
1386 if let Some(px) = left {
1387 l = l.left(px);
1388 }
1389 if let Some(px) = right {
1390 l = l.right(px);
1391 }
1392 crate::Stack::<()>::new().layer(l)
1393 };
1394 let id = WidgetId::ROOT.child(0);
1395 let mut rt = Runtime::default();
1396 rt.advance_pins(&layer(Some(0.0), None), 1.0);
1397
1398 rt.advance_pins(&layer(None, Some(10.0)), 0.02);
1400 let mid = rt.anim_pins(id).expect("present");
1401 assert_eq!(
1402 mid.left, None,
1403 "the edge that let go let go at once: {mid:?}"
1404 );
1405 assert_eq!(
1406 mid.right,
1407 Some(10.0),
1408 "and the edge that took hold took hold at once: {mid:?}"
1409 );
1410 }
1411
1412 #[test]
1416 fn the_stack_places_the_layer_by_the_tween_and_not_the_target() {
1417 let mut rt = Runtime::default();
1418 rt.advance_pins(&pinned_at(0.0), 1.0);
1419 rt.advance_pins(&pinned_at(80.0), 0.05);
1420 let ui = build_ui(
1421 &pinned_at(80.0),
1422 Size::new(100.0, 40.0),
1423 &rt,
1424 &Theme::default(),
1425 );
1426 let x = ui
1427 .scene()
1428 .primitives()
1429 .iter()
1430 .find_map(|p| match p {
1431 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1432 _ => None,
1433 })
1434 .expect("the layer");
1435 assert!(
1436 (x - 40.0).abs() < 0.5,
1437 "halfway across, not at either end: {x}"
1438 );
1439 }
1440
1441 fn share(width_factor: f32) -> crate::Flex<()> {
1443 crate::Flex::column().width(100.0).height(40.0).child(
1444 AnimatedFractionallySizedBox::new(
1445 0.10,
1446 Curve::Linear,
1447 crate::Container::<()>::new()
1448 .flex(1.0)
1449 .height(20.0)
1450 .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1451 )
1452 .width_factor(width_factor),
1453 )
1454 }
1455
1456 #[test]
1458 fn a_share_of_the_parent_mounts_settled_moves_and_arrives() {
1459 let mut rt = Runtime::default();
1460 assert!(
1461 !rt.advance_fractions(&share(0.25), 1.0),
1462 "a mount is not a transition"
1463 );
1464 let id = WidgetId::ROOT.child(0);
1465 assert_eq!(rt.anim_fractions(id), Some((Some(0.25), None)));
1466
1467 assert!(rt.advance_fractions(&share(0.75), 0.05));
1468 let (width, height) = rt.anim_fractions(id).expect("in flight");
1469 assert!(
1470 width.is_some_and(|w| (w - 0.5).abs() < 1e-3),
1471 "halfway between a quarter and three quarters: {width:?}"
1472 );
1473 assert_eq!(
1474 height, None,
1475 "and the axis nobody set still follows its content"
1476 );
1477
1478 rt.advance_fractions(&share(0.75), 1.0);
1479 assert_eq!(rt.anim_fractions(id), Some((Some(0.75), None)));
1480 assert!(
1481 !rt.advance_fractions(&share(0.75), 0.05),
1482 "and stops asking for frames once it is there"
1483 );
1484 }
1485
1486 #[test]
1490 fn the_layout_uses_the_tweened_fraction_and_not_the_target() {
1491 let mut rt = Runtime::default();
1492 rt.advance_fractions(&share(0.25), 1.0);
1493 rt.advance_fractions(&share(0.75), 0.05);
1494 let ui = build_ui(&share(0.75), Size::new(100.0, 40.0), &rt, &Theme::default());
1495 let width = ui
1496 .scene()
1497 .primitives()
1498 .iter()
1499 .find_map(|p| match p {
1500 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.width),
1501 _ => None,
1502 })
1503 .expect("the child's background");
1504 assert!(
1505 (width - 50.0).abs() < 0.5,
1506 "half the parent, on the way from a quarter to three quarters: {width}"
1507 );
1508 }
1509
1510 fn styled(size: f32) -> AnimatedDefaultTextStyle<()> {
1514 AnimatedDefaultTextStyle::new(
1515 0.10,
1516 Curve::Linear,
1517 TextStyle::NONE.size(size),
1518 Text::new("Ag"),
1519 )
1520 }
1521
1522 #[test]
1524 fn a_default_text_style_mounts_settled_moves_and_arrives() {
1525 let mut rt = Runtime::default();
1526 assert!(
1527 !rt.advance_text_styles(&styled(24.0), 1.0),
1528 "a mount is not a transition"
1529 );
1530 let id = WidgetId::ROOT;
1531 assert_eq!(
1532 rt.anim_text_style(id).and_then(|s| s.size),
1533 Some(24.0),
1534 "and it adopts the target whole"
1535 );
1536
1537 assert!(rt.advance_text_styles(&styled(12.0), 0.05));
1538 let mid = rt.anim_text_style(id).expect("in flight").size.unwrap();
1539 assert!((mid - 18.0).abs() < 1e-3, "halfway between the two: {mid}");
1540
1541 rt.advance_text_styles(&styled(12.0), 1.0);
1542 assert_eq!(rt.anim_text_style(id).and_then(|s| s.size), Some(12.0));
1543 assert!(
1544 !rt.advance_text_styles(&styled(12.0), 0.05),
1545 "and stops asking for frames once it is there"
1546 );
1547 }
1548
1549 #[test]
1559 fn the_moving_style_reaches_the_words() {
1560 fn painted_size(rt: &Runtime, tree: &AnimatedDefaultTextStyle<()>) -> f32 {
1561 fn walk(primitives: &[Primitive], out: &mut Vec<f32>) {
1562 for p in primitives {
1563 match p {
1564 Primitive::Text { size, .. } => out.push(*size),
1565 Primitive::Layer { primitives, .. } => walk(primitives, out),
1566 _ => {}
1567 }
1568 }
1569 }
1570 let ui = build_ui(tree, Size::new(400.0, 200.0), rt, &Theme::default());
1571 let mut sizes = Vec::new();
1572 walk(ui.scene().primitives(), &mut sizes);
1573 assert_eq!(sizes.len(), 1, "one run of words: {sizes:?}");
1574 sizes[0]
1575 }
1576
1577 let mut rt = Runtime::default();
1578 rt.advance_text_styles(&styled(24.0), 1.0);
1579 assert_eq!(
1580 painted_size(&rt, &styled(24.0)),
1581 24.0,
1582 "mounted at its target"
1583 );
1584
1585 rt.advance_text_styles(&styled(12.0), 0.05);
1586 let mid = painted_size(&rt, &styled(12.0));
1587 assert!(
1588 (mid - 18.0).abs() < 1e-3,
1589 "the words are drawn at the style in flight, not at the one it is heading for: {mid}"
1590 );
1591 }
1592
1593 #[test]
1598 fn the_answers_that_are_not_quantities_take_effect_at_once() {
1599 let tree = AnimatedDefaultTextStyle::<()>::new(
1600 0.10,
1601 Curve::Linear,
1602 TextStyle::NONE.size(16.0),
1603 Text::new("Ag"),
1604 )
1605 .align(TextAlign::Center)
1606 .max_lines(2);
1607 let handed = Widget::<()>::theme_override(&tree, &Theme::default()).expect("a theme");
1608 assert_eq!(handed.widgets.text.align, Some(TextAlign::Center));
1609 assert_eq!(handed.widgets.text.max_lines, Some(2));
1610 }
1611
1612 #[test]
1617 fn two_nested_styles_are_two_timelines() {
1618 let nested = AnimatedDefaultTextStyle::<()>::new(
1619 0.10,
1620 Curve::Linear,
1621 TextStyle::NONE.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1622 AnimatedDefaultTextStyle::new(
1623 0.10,
1624 Curve::Linear,
1625 TextStyle::NONE.size(20.0),
1626 Text::new("Ag"),
1627 ),
1628 );
1629 let mut rt = Runtime::default();
1630 rt.advance_text_styles(&nested, 1.0);
1631 let outer = rt.anim_text_style(WidgetId::ROOT).expect("the outer one");
1632 let inner_id = crate::ui::child_id(WidgetId::ROOT, 0, nested.children()[0].as_ref());
1633 let inner = rt.anim_text_style(inner_id).expect("the inner one");
1634 assert_ne!(inner_id, WidgetId::ROOT, "two nodes, not one");
1635 assert_eq!(outer.size, None, "the outer one says nothing about size");
1636 assert_eq!(inner.size, Some(20.0), "and the inner one does");
1637 assert!(outer.color.is_some() && inner.color.is_none());
1638 }
1639}