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 fn foreground(&self, theme: &Theme) -> Option<frus_core::BoxDecoration> {
121 Widget::foreground(&self.inner, theme)
122 }
123 }
124 };
125}
126
127pub(crate) use forward_to_container;
128
129pub struct Opacity<Msg = crate::callback::Callback> {
132 inner: Container<Msg>,
133}
134
135impl<Msg: Clone + 'static> Opacity<Msg> {
136 pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
138 Self {
139 inner: Container::new().opacity(opacity).child(child),
140 }
141 }
142}
143
144forward_to_container!(Opacity);
145
146pub struct AnimatedOpacity<Msg = crate::callback::Callback> {
149 inner: Container<Msg>,
150}
151
152impl<Msg: Clone + 'static> AnimatedOpacity<Msg> {
153 pub fn new(
155 opacity: f32,
156 duration: f32,
157 curve: Curve,
158 child: impl Widget<Msg> + 'static,
159 ) -> Self {
160 Self {
161 inner: Container::new()
162 .animated_opacity(opacity, duration, curve)
163 .child(child),
164 }
165 }
166}
167
168forward_to_container!(AnimatedOpacity);
169
170pub struct AnimatedContainer<Msg = crate::callback::Callback> {
182 inner: Container<Msg>,
183 duration: f32,
184 curve: Curve,
185}
186
187impl<Msg: Clone + 'static> AnimatedContainer<Msg> {
188 pub fn new(duration: f32, curve: Curve) -> Self {
191 Self {
192 inner: Container::new(),
193 duration,
194 curve,
195 }
196 }
197
198 pub fn color(mut self, color: Color) -> Self {
200 self.inner = self
201 .inner
202 .animated_color(color, self.duration, self.curve.clone());
203 self
204 }
205
206 pub fn size(mut self, width: f32, height: f32) -> Self {
208 self.inner = self
209 .inner
210 .animated_size(width, height, self.duration, self.curve.clone());
211 self
212 }
213
214 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
216 self.inner = self
217 .inner
218 .animated_radius(radius, self.duration, self.curve.clone());
219 self
220 }
221
222 pub fn opacity(mut self, opacity: f32) -> Self {
224 self.inner = self
225 .inner
226 .animated_opacity(opacity, self.duration, self.curve.clone());
227 self
228 }
229
230 pub fn padding(mut self, padding: f32) -> Self {
232 self.inner = self.inner.padding(padding);
233 self
234 }
235
236 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
238 self.inner = self.inner.child(child);
239 self
240 }
241}
242
243forward_to_container!(AnimatedContainer);
244
245pub struct AnimatedAlign<Msg = crate::callback::Callback> {
273 inner: crate::Aligned<Msg>,
274}
275
276impl<Msg: Clone + 'static> AnimatedAlign<Msg> {
277 pub fn new(
279 alignment: impl Into<frus_core::AlignmentGeometry>,
280 duration: f32,
281 curve: Curve,
282 child: impl Widget<Msg> + 'static,
283 ) -> Self {
284 Self {
285 inner: crate::Aligned::animated(alignment, duration, curve, child),
286 }
287 }
288
289 pub fn width_factor(mut self, factor: f32) -> Self {
293 self.inner = self.inner.width_factor(factor);
294 self
295 }
296
297 pub fn height_factor(mut self, factor: f32) -> Self {
300 self.inner = self.inner.height_factor(factor);
301 self
302 }
303}
304
305impl<Msg: Clone> Widget<Msg> for AnimatedAlign<Msg> {
307 fn style(&self) -> Style {
308 self.inner.style()
309 }
310
311 fn style_themed(&self, theme: &Theme) -> Style {
312 self.inner.style_themed(theme)
313 }
314
315 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
316 self.inner.children()
317 }
318
319 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
320
321 fn on_click(&self) -> Option<Msg> {
322 None
323 }
324
325 fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
326 self.inner.alignment_geometry()
327 }
328
329 fn anim_offset(&self) -> Option<(f32, f32)> {
330 self.inner.anim_offset()
331 }
332
333 fn anim_duration(&self) -> f32 {
334 self.inner.anim_duration()
335 }
336
337 fn anim_curve(&self) -> Curve {
338 self.inner.anim_curve()
339 }
340
341 fn constraints_transform(&self) -> Option<crate::constraints::ConstraintsTransform> {
342 self.inner.constraints_transform()
343 }
344
345 fn size_factor(&self) -> Option<(f32, f32)> {
346 self.inner.size_factor()
347 }
348
349 fn fill_axes(&self, theme: &Theme) -> crate::widget::FillAxes {
350 self.inner.fill_axes(theme)
351 }
352
353 fn debug_name(&self) -> &'static str {
354 "AnimatedAlign"
355 }
356}
357
358pub struct AnimatedSlide<Msg = crate::callback::Callback> {
384 inner: Box<dyn Widget<Msg>>,
385}
386
387impl<Msg: Clone + 'static> AnimatedSlide<Msg> {
388 pub fn new(
390 fx: f32,
391 fy: f32,
392 duration: f32,
393 curve: Curve,
394 child: impl Widget<Msg> + 'static,
395 ) -> Self {
396 Self {
397 inner: Box::new(
398 crate::FractionalTranslation::new(fx, fy)
399 .animated(duration, curve)
400 .child(child),
401 ),
402 }
403 }
404}
405
406impl<Msg> AnimatedSlide<Msg> {
407 fn restyle(&self, base: Style) -> Style {
411 base
412 }
413}
414
415crate::transparent::forward_transparent!(AnimatedSlide {
416 fn key(&self) -> Option<u64> {
419 self.inner.key()
420 }
421 fn positioned(&self) -> Option<crate::positioned::Positioning> {
422 self.inner.positioned()
423 }
424 fn theme_override(
425 &self,
426 inherited: &crate::theme::Theme,
427 ) -> Option<Box<crate::theme::Theme>> {
428 self.inner.theme_override(inherited)
429 }
430 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
431 self.inner.media_override(inherited)
432 }
433 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
434 self.inner.scaffold_override()
435 }
436 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
437 self.inner.scroll_behavior_override()
438 }
439 fn autofill_group(&self) -> bool {
441 self.inner.autofill_group()
442 }
443 fn area_toolbar(
445 &self,
446 context: crate::ToolbarContext,
447 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
448 self.inner.area_toolbar(context)
449 }
450});
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
459 fn animated_container_declares_all_targets() {
460 let blue = Color::rgb(0.0, 0.0, 1.0);
461 let w: AnimatedContainer<()> = AnimatedContainer::new(0.25, Curve::ease_out())
462 .color(blue)
463 .size(200.0, 100.0)
464 .radius(12.0)
465 .opacity(0.5);
466 assert_eq!(Widget::<()>::anim_color(&w), Some(blue));
467 assert_eq!(Widget::<()>::anim_size(&w), Some(Size::new(200.0, 100.0)));
468 assert_eq!(
469 Widget::<()>::anim_radius(&w),
470 Some(BorderRadius::from(12.0))
471 );
472 assert_eq!(Widget::<()>::anim_target(&w), Some(0.5)); assert_eq!(Widget::<()>::opacity_group(&w), Some(0.5));
474 assert_eq!(Widget::<()>::anim_duration(&w), 0.25);
475 assert_eq!(Widget::<()>::anim_curve(&w), Curve::ease_out());
476 }
477
478 #[test]
481 fn opacity_wraps_child_as_a_group() {
482 let w: Opacity<()> = Opacity::new(0.4, crate::Container::new().width(10.0).height(10.0));
483 assert_eq!(Widget::<()>::opacity_group(&w), Some(0.4));
484 assert_eq!(Widget::<()>::anim_target(&w), None);
485 assert_eq!(
486 Widget::<()>::children(&w).len(),
487 1,
488 "the child is a separate node"
489 );
490 }
491
492 #[test]
494 fn animated_opacity_declares_a_group_target() {
495 let w: AnimatedOpacity<()> =
496 AnimatedOpacity::new(0.0, 0.2, Curve::ease_in(), crate::Container::new());
497 assert_eq!(Widget::<()>::opacity_group(&w), Some(0.0));
498 assert_eq!(Widget::<()>::anim_target(&w), Some(0.0));
499 assert_eq!(Widget::<()>::anim_duration(&w), 0.2);
500 assert_eq!(Widget::<()>::debug_name(&w), "AnimatedOpacity");
502 }
503}
504
505pub struct AnimatedScale<Msg = crate::callback::Callback> {
525 inner: Box<dyn Widget<Msg>>,
526}
527
528impl<Msg: Clone + 'static> AnimatedScale<Msg> {
529 pub fn new(scale: f32, duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
531 Self::about(scale, frus_core::Alignment::CENTER, duration, curve, child)
532 }
533
534 pub fn about(
541 scale: f32,
542 pivot: frus_core::Alignment,
543 duration: f32,
544 curve: Curve,
545 child: impl Widget<Msg> + 'static,
546 ) -> Self {
547 Self {
548 inner: Box::new(
549 crate::Transform::scale_xy_from(scale, scale, pivot)
550 .animated(duration, curve)
551 .child(child),
552 ),
553 }
554 }
555}
556
557impl<Msg> AnimatedScale<Msg> {
558 fn restyle(&self, base: Style) -> Style {
562 base
563 }
564}
565
566crate::transparent::forward_transparent!(AnimatedScale {
567 fn key(&self) -> Option<u64> {
570 self.inner.key()
571 }
572 fn positioned(&self) -> Option<crate::positioned::Positioning> {
573 self.inner.positioned()
574 }
575 fn theme_override(
576 &self,
577 inherited: &crate::theme::Theme,
578 ) -> Option<Box<crate::theme::Theme>> {
579 self.inner.theme_override(inherited)
580 }
581 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
582 self.inner.media_override(inherited)
583 }
584 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
585 self.inner.scaffold_override()
586 }
587 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
588 self.inner.scroll_behavior_override()
589 }
590 fn autofill_group(&self) -> bool {
592 self.inner.autofill_group()
593 }
594 fn area_toolbar(
596 &self,
597 context: crate::ToolbarContext,
598 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
599 self.inner.area_toolbar(context)
600 }
601});
602
603pub struct AnimatedRotation<Msg = crate::callback::Callback> {
623 inner: Box<dyn Widget<Msg>>,
624}
625
626impl<Msg: Clone + 'static> AnimatedRotation<Msg> {
627 pub fn new(
629 radians: f32,
630 duration: f32,
631 curve: Curve,
632 child: impl Widget<Msg> + 'static,
633 ) -> Self {
634 Self::about(
635 radians,
636 frus_core::Alignment::CENTER,
637 duration,
638 curve,
639 child,
640 )
641 }
642
643 pub fn about(
645 radians: f32,
646 pivot: frus_core::Alignment,
647 duration: f32,
648 curve: Curve,
649 child: impl Widget<Msg> + 'static,
650 ) -> Self {
651 Self {
652 inner: Box::new(
653 crate::Transform::rotate_from(radians, pivot)
654 .animated(duration, curve)
655 .child(child),
656 ),
657 }
658 }
659}
660
661impl<Msg> AnimatedRotation<Msg> {
662 fn restyle(&self, base: Style) -> Style {
666 base
667 }
668}
669
670crate::transparent::forward_transparent!(AnimatedRotation {
671 fn key(&self) -> Option<u64> {
674 self.inner.key()
675 }
676 fn positioned(&self) -> Option<crate::positioned::Positioning> {
677 self.inner.positioned()
678 }
679 fn theme_override(
680 &self,
681 inherited: &crate::theme::Theme,
682 ) -> Option<Box<crate::theme::Theme>> {
683 self.inner.theme_override(inherited)
684 }
685 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
686 self.inner.media_override(inherited)
687 }
688 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
689 self.inner.scaffold_override()
690 }
691 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
692 self.inner.scroll_behavior_override()
693 }
694 fn autofill_group(&self) -> bool {
696 self.inner.autofill_group()
697 }
698 fn area_toolbar(
700 &self,
701 context: crate::ToolbarContext,
702 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
703 self.inner.area_toolbar(context)
704 }
705});
706
707pub struct AnimatedPadding<Msg = crate::callback::Callback> {
728 inner: Container<Msg>,
729}
730
731impl<Msg: Clone + 'static> AnimatedPadding<Msg> {
732 pub fn new(
734 padding: f32,
735 duration: f32,
736 curve: Curve,
737 child: impl Widget<Msg> + 'static,
738 ) -> Self {
739 Self {
740 inner: Container::new()
741 .animated_padding(padding, duration, curve)
742 .child(child),
743 }
744 }
745}
746
747forward_to_container!(AnimatedPadding);
748
749pub struct AnimatedPositioned<Msg = crate::callback::Callback> {
785 children: Vec<Box<dyn Widget<Msg>>>,
786 spec: crate::positioned::Positioning,
787 duration: f32,
788 curve: Curve,
789}
790
791impl<Msg> AnimatedPositioned<Msg> {
792 pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
795 Self {
796 children: vec![Box::new(child)],
797 spec: crate::positioned::Positioning::default(),
798 duration,
799 curve,
800 }
801 }
802
803 pub fn left(mut self, px: f32) -> Self {
805 self.spec.left = Some(px);
806 self
807 }
808
809 pub fn top(mut self, px: f32) -> Self {
811 self.spec.top = Some(px);
812 self
813 }
814
815 pub fn right(mut self, px: f32) -> Self {
817 self.spec.right = Some(px);
818 self
819 }
820
821 pub fn bottom(mut self, px: f32) -> Self {
823 self.spec.bottom = Some(px);
824 self
825 }
826
827 pub fn start(mut self, px: f32) -> Self {
831 self.spec.start = Some(px);
832 self
833 }
834
835 pub fn end(mut self, px: f32) -> Self {
838 self.spec.end = Some(px);
839 self
840 }
841
842 pub fn width(mut self, px: f32) -> Self {
844 self.spec.width = Some(px);
845 self
846 }
847
848 pub fn height(mut self, px: f32) -> Self {
850 self.spec.height = Some(px);
851 self
852 }
853}
854
855impl<Msg: Clone> Widget<Msg> for AnimatedPositioned<Msg> {
856 fn style(&self) -> Style {
857 Style::default()
860 }
861
862 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
863 &self.children
864 }
865
866 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
867 }
869
870 fn on_click(&self) -> Option<Msg> {
871 None
872 }
873
874 fn positioned(&self) -> Option<crate::positioned::Positioning> {
877 Some(self.spec)
878 }
879
880 fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
881 Some(self.spec)
882 }
883
884 fn anim_duration(&self) -> f32 {
885 self.duration
886 }
887
888 fn anim_curve(&self) -> Curve {
889 self.curve.clone()
890 }
891
892 fn debug_name(&self) -> &'static str {
893 "AnimatedPositioned"
894 }
895}
896
897pub struct AnimatedDefaultTextStyle<Msg = crate::callback::Callback> {
943 children: Vec<Box<dyn Widget<Msg>>>,
944 style: TextStyle,
945 align: Option<TextAlign>,
946 soft_wrap: Option<bool>,
947 overflow: Option<TextOverflow>,
948 max_lines: Option<usize>,
949 duration: f32,
950 curve: Curve,
951}
952
953impl<Msg> AnimatedDefaultTextStyle<Msg> {
954 pub fn new(
957 duration: f32,
958 curve: Curve,
959 style: TextStyle,
960 child: impl Widget<Msg> + 'static,
961 ) -> Self {
962 Self {
963 children: vec![Box::new(child)],
964 style,
965 align: None,
966 soft_wrap: None,
967 overflow: None,
968 max_lines: None,
969 duration,
970 curve,
971 }
972 }
973
974 pub fn align(mut self, align: TextAlign) -> Self {
976 self.align = Some(align);
977 self
978 }
979
980 pub fn soft_wrap(mut self, wrap: bool) -> Self {
982 self.soft_wrap = Some(wrap);
983 self
984 }
985
986 pub fn overflow(mut self, overflow: TextOverflow) -> Self {
988 self.overflow = Some(overflow);
989 self
990 }
991
992 pub fn max_lines(mut self, lines: usize) -> Self {
994 self.max_lines = Some(lines);
995 self
996 }
997
998 fn handed_down(&self) -> crate::widgettheme::DefaultTextStyle {
1000 crate::widgettheme::DefaultTextStyle {
1001 style: self.style,
1002 align: self.align,
1003 soft_wrap: self.soft_wrap,
1004 overflow: self.overflow,
1005 max_lines: self.max_lines,
1006 ..crate::widgettheme::DefaultTextStyle::NONE
1007 }
1008 }
1009}
1010
1011impl<Msg: Clone> Widget<Msg> for AnimatedDefaultTextStyle<Msg> {
1012 fn style(&self) -> Style {
1013 Style::default()
1016 }
1017
1018 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
1019 &self.children
1020 }
1021
1022 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
1023 }
1025
1026 fn on_click(&self) -> Option<Msg> {
1027 None
1028 }
1029
1030 fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
1039 let mut theme = inherited.clone();
1040 theme.widgets.text = theme.widgets.text.merge(self.handed_down());
1041 Some(Box::new(theme))
1042 }
1043
1044 fn anim_text_style(&self) -> Option<TextStyle> {
1045 Some(self.style)
1046 }
1047
1048 fn anim_duration(&self) -> f32 {
1049 self.duration
1050 }
1051
1052 fn anim_curve(&self) -> Curve {
1053 self.curve.clone()
1054 }
1055
1056 fn debug_name(&self) -> &'static str {
1057 "AnimatedDefaultTextStyle"
1058 }
1059}
1060
1061pub struct AnimatedFractionallySizedBox<Msg = crate::callback::Callback> {
1084 inner: crate::fractional::FractionallySizedBox<Msg>,
1085}
1086
1087impl<Msg: Clone + 'static> AnimatedFractionallySizedBox<Msg> {
1088 pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
1091 Self {
1092 inner: crate::fractional::FractionallySizedBox::new()
1093 .animated(duration, curve)
1094 .child(child),
1095 }
1096 }
1097
1098 pub fn width_factor(mut self, factor: f32) -> Self {
1100 self.inner = self.inner.width_factor(factor);
1101 self
1102 }
1103
1104 pub fn height_factor(mut self, factor: f32) -> Self {
1106 self.inner = self.inner.height_factor(factor);
1107 self
1108 }
1109}
1110
1111impl<Msg: Clone + 'static> Widget<Msg> for AnimatedFractionallySizedBox<Msg> {
1115 fn style(&self) -> Style {
1116 Widget::style(&self.inner)
1117 }
1118 fn style_themed(&self, theme: &Theme) -> Style {
1119 Widget::style_themed(&self.inner, theme)
1120 }
1121 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
1122 Widget::children(&self.inner)
1123 }
1124 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
1125 Widget::paint(&self.inner, bounds, status, theme, scene)
1126 }
1127 fn on_click(&self) -> Option<Msg> {
1128 Widget::on_click(&self.inner)
1129 }
1130 fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
1131 Widget::anim_fractions(&self.inner)
1132 }
1133 fn anim_duration(&self) -> f32 {
1134 Widget::anim_duration(&self.inner)
1135 }
1136 fn anim_curve(&self) -> Curve {
1137 Widget::anim_curve(&self.inner)
1138 }
1139}
1140
1141#[cfg(test)]
1142mod implicit_tests {
1143 use super::*;
1144 use crate::interaction::WidgetId;
1145 use crate::{build_ui, Runtime, Text};
1146 use frus_core::{Insets, Primitive};
1147
1148 fn scaled(to: f32) -> AnimatedScale<()> {
1150 AnimatedScale::new(to, 0.10, Curve::Linear, Text::new("x"))
1151 }
1152
1153 #[test]
1160 fn a_scale_mounts_settled_moves_and_arrives() {
1161 let mut rt = Runtime::default();
1162 assert!(
1163 !rt.advance_transforms(&scaled(2.0), 1.0),
1164 "a mount is not a transition"
1165 );
1166 let id = WidgetId::ROOT;
1167 assert_eq!(
1168 rt.anim_transform(id).map(|t| t.scale_x),
1169 Some(2.0),
1170 "and it adopts the target whole"
1171 );
1172
1173 assert!(rt.advance_transforms(&scaled(1.0), 0.05));
1175 let mid = rt.anim_transform(id).expect("in flight");
1176 assert!(
1177 (mid.scale_x - 1.5).abs() < 1e-3 && (mid.scale_y - 1.5).abs() < 1e-3,
1178 "halfway between the two: {mid:?}"
1179 );
1180 assert_eq!(mid.rotation, 0.0, "and it turned nothing on the way");
1181
1182 rt.advance_transforms(&scaled(1.0), 1.0);
1183 assert_eq!(rt.anim_transform(id).map(|t| t.scale_x), Some(1.0));
1184 assert!(
1185 !rt.advance_transforms(&scaled(1.0), 0.05),
1186 "and stops asking for frames once it is there"
1187 );
1188 }
1189
1190 #[test]
1194 fn a_rotation_mounts_settled_moves_and_arrives() {
1195 let turned = |to: f32| -> AnimatedRotation<()> {
1196 AnimatedRotation::new(to, 0.10, Curve::Linear, Text::new("x"))
1197 };
1198 let mut rt = Runtime::default();
1199 assert!(!rt.advance_transforms(&turned(0.0), 1.0));
1200 let id = WidgetId::ROOT;
1201 let mounted = rt.anim_transform(id).expect("mounted");
1202 assert_eq!((mounted.scale_x, mounted.scale_y), (1.0, 1.0), "identity");
1203
1204 assert!(rt.advance_transforms(&turned(1.0), 0.05));
1205 let mid = rt.anim_transform(id).expect("in flight");
1206 assert!((mid.rotation - 0.5).abs() < 1e-3, "halfway: {mid:?}");
1207 assert_eq!(
1208 (mid.scale_x, mid.scale_y),
1209 (1.0, 1.0),
1210 "and the scale stayed the identity, not nought"
1211 );
1212
1213 rt.advance_transforms(&turned(1.0), 1.0);
1214 assert_eq!(rt.anim_transform(id).map(|t| t.rotation), Some(1.0));
1215 }
1216
1217 #[test]
1220 fn a_padding_mounts_settled_moves_and_arrives() {
1221 let padded = |to: f32| -> AnimatedPadding<()> {
1222 AnimatedPadding::new(to, 0.10, Curve::Linear, Text::new("x"))
1223 };
1224 let mut rt = Runtime::default();
1225 assert!(!rt.advance_paddings(&padded(0.0), 1.0));
1226 let id = WidgetId::ROOT;
1227 assert_eq!(rt.anim_padding(id), Some(Insets::uniform(0.0)));
1228
1229 assert!(rt.advance_paddings(&padded(20.0), 0.05));
1230 let mid = rt.anim_padding(id).expect("in flight");
1231 assert!((mid.left - 10.0).abs() < 1e-3, "halfway: {mid:?}");
1232
1233 rt.advance_paddings(&padded(20.0), 1.0);
1234 assert_eq!(rt.anim_padding(id), Some(Insets::uniform(20.0)));
1235 }
1236
1237 #[test]
1241 fn the_paint_uses_the_tweened_transform_and_not_the_target() {
1242 let matrix_of = |rt: &Runtime, widget: &AnimatedScale<()>| {
1243 let ui = build_ui(widget, Size::new(100.0, 100.0), rt, &Theme::default());
1244 ui.scene()
1245 .primitives()
1246 .iter()
1247 .find_map(|p| match p {
1248 Primitive::Layer {
1249 transform: Some(t), ..
1250 } => Some(t.affine),
1251 _ => None,
1252 })
1253 .map(|m| m.m[0])
1254 };
1255 let mut rt = Runtime::default();
1256 rt.advance_transforms(&scaled(3.0), 1.0);
1257 rt.advance_transforms(&scaled(1.0), 0.05);
1259 let painted = matrix_of(&rt, &scaled(1.0)).expect("a transformed layer");
1260 assert!(
1261 (painted - 2.0).abs() < 1e-2,
1262 "the paint took the tweened scale (2), not the target (1): {painted}"
1263 );
1264
1265 let fresh = Runtime::default();
1268 let painted = matrix_of(&fresh, &scaled(3.0)).expect("a transformed layer");
1269 assert!((painted - 3.0).abs() < 1e-2, "the target: {painted}");
1270 }
1271
1272 fn anchored(to: frus_core::Alignment) -> AnimatedAlign<()> {
1274 AnimatedAlign::new(to, 0.10, Curve::Linear, Text::new("x"))
1275 }
1276
1277 #[test]
1279 fn an_anchor_mounts_settled_moves_and_arrives() {
1280 let mut rt = Runtime::default();
1281 assert!(
1282 !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_LEFT), 1.0),
1283 "a mount is not a transition"
1284 );
1285 let id = WidgetId::ROOT;
1286 assert_eq!(
1287 rt.anim_offset(id),
1288 Some((-1.0, 0.0)),
1289 "and it adopts the anchor whole"
1290 );
1291
1292 assert!(rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05));
1294 let mid = rt.anim_offset(id).expect("in flight");
1295 assert!(mid.0.abs() < 1e-3, "at the centre on the way past: {mid:?}");
1296 assert_eq!(mid.1, 0.0, "and it did not drift vertically");
1297
1298 rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 1.0);
1299 assert_eq!(rt.anim_offset(id), Some((1.0, 0.0)));
1300 assert!(
1301 !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05),
1302 "and stops asking for frames once it is there"
1303 );
1304 }
1305
1306 #[test]
1309 fn a_slide_mounts_settled_moves_and_arrives() {
1310 let slid = |fx: f32| -> AnimatedSlide<()> {
1311 AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, Text::new("x"))
1312 };
1313 let mut rt = Runtime::default();
1314 assert!(!rt.advance_offsets(&slid(-1.0), 1.0));
1315 let id = WidgetId::ROOT;
1316 assert_eq!(rt.anim_offset(id), Some((-1.0, 0.0)));
1317
1318 assert!(rt.advance_offsets(&slid(0.0), 0.05));
1319 let mid = rt.anim_offset(id).expect("in flight");
1320 assert!((mid.0 + 0.5).abs() < 1e-3, "halfway back: {mid:?}");
1321
1322 rt.advance_offsets(&slid(0.0), 1.0);
1323 assert_eq!(rt.anim_offset(id), Some((0.0, 0.0)));
1324 }
1325
1326 #[test]
1334 fn the_paint_uses_the_tweened_offset_and_not_the_target() {
1335 let mark = |root: &dyn Widget<()>, rt: &Runtime| {
1336 let ui = build_ui(root, Size::new(100.0, 40.0), rt, &Theme::default());
1337 ui.scene()
1338 .primitives()
1339 .iter()
1340 .find_map(|p| match p {
1341 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1342 _ => None,
1343 })
1344 .expect("the mark")
1345 };
1346 let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
1347 let mark_widget = || {
1348 crate::Container::<()>::new()
1349 .width(20.0)
1350 .height(20.0)
1351 .color(red)
1352 };
1353
1354 let aligned = |to: frus_core::Alignment| {
1357 crate::Container::<()>::new()
1358 .width(100.0)
1359 .height(40.0)
1360 .animated_alignment(to, 0.10, Curve::Linear)
1361 .child(mark_widget())
1362 };
1363 let mut rt = Runtime::default();
1364 rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_LEFT), 1.0);
1365 rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_RIGHT), 0.05);
1366 let x = mark(&aligned(frus_core::Alignment::CENTER_RIGHT), &rt);
1367 assert!(
1368 (x - 40.0).abs() < 0.5,
1369 "the anchor's tween reached the paint: {x}"
1370 );
1371
1372 let slid = |fx: f32| AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, mark_widget());
1374 let mut rt = Runtime::default();
1375 rt.advance_offsets(&slid(0.0), 1.0);
1376 rt.advance_offsets(&slid(1.0), 0.05);
1377 let x = mark(&slid(1.0), &rt);
1378 assert!(
1379 (x - 10.0).abs() < 0.5,
1380 "the slide's tween reached the paint: {x}"
1381 );
1382 }
1383
1384 #[test]
1388 fn a_directional_anchor_mirrors_as_a_whole() {
1389 let start = frus_core::AlignmentDirectional::CENTER_START;
1390 let end = frus_core::AlignmentDirectional::CENTER_END;
1391 let aligned = |to: frus_core::AlignmentDirectional| {
1392 crate::Container::<()>::new()
1393 .width(100.0)
1394 .height(40.0)
1395 .animated_alignment(to, 0.10, Curve::Linear)
1396 .child(
1397 crate::Container::<()>::new()
1398 .width(20.0)
1399 .height(20.0)
1400 .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1401 )
1402 };
1403 let mut rt = Runtime::default();
1404 rt.advance_offsets(&aligned(start), 1.0);
1405 rt.advance_offsets(&aligned(end), 0.025);
1407
1408 let x_in = |rtl: bool| {
1409 let theme = match rtl {
1410 true => Theme::default().rtl(),
1411 false => Theme::default(),
1412 };
1413 let ui = build_ui(&aligned(end), Size::new(100.0, 40.0), &rt, &theme);
1414 ui.scene()
1415 .primitives()
1416 .iter()
1417 .find_map(|p| match p {
1418 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1419 _ => None,
1420 })
1421 .expect("the mark")
1422 };
1423 let ltr = x_in(false);
1424 let rtl = x_in(true);
1425 assert!(
1426 (ltr - 20.0).abs() < 0.5,
1427 "a quarter across, from the left: {ltr}"
1428 );
1429 assert!(
1430 (rtl - 60.0).abs() < 0.5,
1431 "and the same quarter from the right: {rtl}"
1432 );
1433 }
1434
1435 fn pinned_at(left: f32) -> crate::Stack<()> {
1437 crate::Stack::new().layer(
1438 AnimatedPositioned::new(
1439 0.10,
1440 Curve::Linear,
1441 crate::Container::<()>::new()
1442 .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
1443 .child(Text::new("x")),
1444 )
1445 .left(left)
1446 .top(0.0)
1447 .width(20.0)
1448 .height(20.0),
1449 )
1450 }
1451
1452 #[test]
1459 fn a_layer_mounts_settled_moves_and_arrives() {
1460 let mut rt = Runtime::default();
1461 assert!(
1462 !rt.advance_pins(&pinned_at(0.0), 1.0),
1463 "a mount is not a transition"
1464 );
1465 let id = WidgetId::ROOT.child(0);
1467 assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(0.0));
1468
1469 assert!(rt.advance_pins(&pinned_at(80.0), 0.05));
1471 let mid = rt.anim_pins(id).expect("in flight");
1472 assert!(
1473 mid.left.is_some_and(|l| (l - 40.0).abs() < 1e-3),
1474 "halfway between the two: {mid:?}"
1475 );
1476 assert_eq!(
1477 mid.width,
1478 Some(20.0),
1479 "and the extents it never asked to move stayed"
1480 );
1481
1482 rt.advance_pins(&pinned_at(80.0), 1.0);
1483 assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(80.0));
1484 assert!(
1485 !rt.advance_pins(&pinned_at(80.0), 0.05),
1486 "and stops asking for frames once it is there"
1487 );
1488 }
1489
1490 #[test]
1497 fn a_pin_that_is_unset_takes_hold_and_lets_go_at_once() {
1498 let layer = |left: Option<f32>, right: Option<f32>| {
1499 let mut l: AnimatedPositioned<()> =
1500 AnimatedPositioned::new(0.10, Curve::Linear, Text::new("x")).top(0.0);
1501 if let Some(px) = left {
1502 l = l.left(px);
1503 }
1504 if let Some(px) = right {
1505 l = l.right(px);
1506 }
1507 crate::Stack::<()>::new().layer(l)
1508 };
1509 let id = WidgetId::ROOT.child(0);
1510 let mut rt = Runtime::default();
1511 rt.advance_pins(&layer(Some(0.0), None), 1.0);
1512
1513 rt.advance_pins(&layer(None, Some(10.0)), 0.02);
1515 let mid = rt.anim_pins(id).expect("present");
1516 assert_eq!(
1517 mid.left, None,
1518 "the edge that let go let go at once: {mid:?}"
1519 );
1520 assert_eq!(
1521 mid.right,
1522 Some(10.0),
1523 "and the edge that took hold took hold at once: {mid:?}"
1524 );
1525 }
1526
1527 #[test]
1531 fn the_stack_places_the_layer_by_the_tween_and_not_the_target() {
1532 let mut rt = Runtime::default();
1533 rt.advance_pins(&pinned_at(0.0), 1.0);
1534 rt.advance_pins(&pinned_at(80.0), 0.05);
1535 let ui = build_ui(
1536 &pinned_at(80.0),
1537 Size::new(100.0, 40.0),
1538 &rt,
1539 &Theme::default(),
1540 );
1541 let x = ui
1542 .scene()
1543 .primitives()
1544 .iter()
1545 .find_map(|p| match p {
1546 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1547 _ => None,
1548 })
1549 .expect("the layer");
1550 assert!(
1551 (x - 40.0).abs() < 0.5,
1552 "halfway across, not at either end: {x}"
1553 );
1554 }
1555
1556 fn share(width_factor: f32) -> crate::Flex<()> {
1558 crate::Flex::column().width(100.0).height(40.0).child(
1559 AnimatedFractionallySizedBox::new(
1560 0.10,
1561 Curve::Linear,
1562 crate::Container::<()>::new()
1563 .flex(1.0)
1564 .height(20.0)
1565 .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1566 )
1567 .width_factor(width_factor),
1568 )
1569 }
1570
1571 #[test]
1573 fn a_share_of_the_parent_mounts_settled_moves_and_arrives() {
1574 let mut rt = Runtime::default();
1575 assert!(
1576 !rt.advance_fractions(&share(0.25), 1.0),
1577 "a mount is not a transition"
1578 );
1579 let id = WidgetId::ROOT.child(0);
1580 assert_eq!(rt.anim_fractions(id), Some((Some(0.25), None)));
1581
1582 assert!(rt.advance_fractions(&share(0.75), 0.05));
1583 let (width, height) = rt.anim_fractions(id).expect("in flight");
1584 assert!(
1585 width.is_some_and(|w| (w - 0.5).abs() < 1e-3),
1586 "halfway between a quarter and three quarters: {width:?}"
1587 );
1588 assert_eq!(
1589 height, None,
1590 "and the axis nobody set still follows its content"
1591 );
1592
1593 rt.advance_fractions(&share(0.75), 1.0);
1594 assert_eq!(rt.anim_fractions(id), Some((Some(0.75), None)));
1595 assert!(
1596 !rt.advance_fractions(&share(0.75), 0.05),
1597 "and stops asking for frames once it is there"
1598 );
1599 }
1600
1601 #[test]
1605 fn the_layout_uses_the_tweened_fraction_and_not_the_target() {
1606 let mut rt = Runtime::default();
1607 rt.advance_fractions(&share(0.25), 1.0);
1608 rt.advance_fractions(&share(0.75), 0.05);
1609 let ui = build_ui(&share(0.75), Size::new(100.0, 40.0), &rt, &Theme::default());
1610 let width = ui
1611 .scene()
1612 .primitives()
1613 .iter()
1614 .find_map(|p| match p {
1615 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.width),
1616 _ => None,
1617 })
1618 .expect("the child's background");
1619 assert!(
1620 (width - 50.0).abs() < 0.5,
1621 "half the parent, on the way from a quarter to three quarters: {width}"
1622 );
1623 }
1624
1625 fn styled(size: f32) -> AnimatedDefaultTextStyle<()> {
1629 AnimatedDefaultTextStyle::new(
1630 0.10,
1631 Curve::Linear,
1632 TextStyle::NONE.size(size),
1633 Text::new("Ag"),
1634 )
1635 }
1636
1637 #[test]
1639 fn a_default_text_style_mounts_settled_moves_and_arrives() {
1640 let mut rt = Runtime::default();
1641 assert!(
1642 !rt.advance_text_styles(&styled(24.0), 1.0),
1643 "a mount is not a transition"
1644 );
1645 let id = WidgetId::ROOT;
1646 assert_eq!(
1647 rt.anim_text_style(id).and_then(|s| s.size),
1648 Some(24.0),
1649 "and it adopts the target whole"
1650 );
1651
1652 assert!(rt.advance_text_styles(&styled(12.0), 0.05));
1653 let mid = rt.anim_text_style(id).expect("in flight").size.unwrap();
1654 assert!((mid - 18.0).abs() < 1e-3, "halfway between the two: {mid}");
1655
1656 rt.advance_text_styles(&styled(12.0), 1.0);
1657 assert_eq!(rt.anim_text_style(id).and_then(|s| s.size), Some(12.0));
1658 assert!(
1659 !rt.advance_text_styles(&styled(12.0), 0.05),
1660 "and stops asking for frames once it is there"
1661 );
1662 }
1663
1664 #[test]
1674 fn the_moving_style_reaches_the_words() {
1675 fn painted_size(rt: &Runtime, tree: &AnimatedDefaultTextStyle<()>) -> f32 {
1676 fn walk(primitives: &[Primitive], out: &mut Vec<f32>) {
1677 for p in primitives {
1678 match p {
1679 Primitive::Text { size, .. } => out.push(*size),
1680 Primitive::Layer { primitives, .. } => walk(primitives, out),
1681 _ => {}
1682 }
1683 }
1684 }
1685 let ui = build_ui(tree, Size::new(400.0, 200.0), rt, &Theme::default());
1686 let mut sizes = Vec::new();
1687 walk(ui.scene().primitives(), &mut sizes);
1688 assert_eq!(sizes.len(), 1, "one run of words: {sizes:?}");
1689 sizes[0]
1690 }
1691
1692 let mut rt = Runtime::default();
1693 rt.advance_text_styles(&styled(24.0), 1.0);
1694 assert_eq!(
1695 painted_size(&rt, &styled(24.0)),
1696 24.0,
1697 "mounted at its target"
1698 );
1699
1700 rt.advance_text_styles(&styled(12.0), 0.05);
1701 let mid = painted_size(&rt, &styled(12.0));
1702 assert!(
1703 (mid - 18.0).abs() < 1e-3,
1704 "the words are drawn at the style in flight, not at the one it is heading \
1705 for: {mid}"
1706 );
1707 }
1708
1709 #[test]
1714 fn the_answers_that_are_not_quantities_take_effect_at_once() {
1715 let tree = AnimatedDefaultTextStyle::<()>::new(
1716 0.10,
1717 Curve::Linear,
1718 TextStyle::NONE.size(16.0),
1719 Text::new("Ag"),
1720 )
1721 .align(TextAlign::Center)
1722 .max_lines(2);
1723 let handed = Widget::<()>::theme_override(&tree, &Theme::default()).expect("a theme");
1724 assert_eq!(handed.widgets.text.align, Some(TextAlign::Center));
1725 assert_eq!(handed.widgets.text.max_lines, Some(2));
1726 }
1727
1728 #[test]
1733 fn two_nested_styles_are_two_timelines() {
1734 let nested = AnimatedDefaultTextStyle::<()>::new(
1735 0.10,
1736 Curve::Linear,
1737 TextStyle::NONE.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1738 AnimatedDefaultTextStyle::new(
1739 0.10,
1740 Curve::Linear,
1741 TextStyle::NONE.size(20.0),
1742 Text::new("Ag"),
1743 ),
1744 );
1745 let mut rt = Runtime::default();
1746 rt.advance_text_styles(&nested, 1.0);
1747 let outer = rt.anim_text_style(WidgetId::ROOT).expect("the outer one");
1748 let inner_id = crate::ui::child_id(WidgetId::ROOT, 0, nested.children()[0].as_ref());
1749 let inner = rt.anim_text_style(inner_id).expect("the inner one");
1750 assert_ne!(inner_id, WidgetId::ROOT, "two nodes, not one");
1751 assert_eq!(outer.size, None, "the outer one says nothing about size");
1752 assert_eq!(inner.size, Some(20.0), "and the inner one does");
1753 assert!(outer.color.is_some() && inner.color.is_none());
1754 }
1755
1756 fn square_at(rt: &Runtime, root: &dyn Widget<()>, size: frus_core::Size) -> frus_core::Rect {
1758 let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
1759 build_ui(root, size, rt, &Theme::dark())
1760 .scene()
1761 .primitives()
1762 .iter()
1763 .find_map(|p| match p {
1764 Primitive::Rect { rect, color, .. } if *color == red => Some(*rect),
1765 _ => None,
1766 })
1767 .expect("the square")
1768 }
1769
1770 fn square() -> Container<()> {
1771 Container::new()
1772 .width(20.0)
1773 .height(20.0)
1774 .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
1775 }
1776
1777 #[test]
1780 fn an_anchor_slides_across_a_box_with_a_factor() {
1781 let size = frus_core::Size::new(300.0, 100.0);
1782 let tree = |to| {
1783 crate::Flex::<()>::row()
1784 .height(100.0)
1785 .child(AnimatedAlign::new(to, 0.10, Curve::Linear, square()).width_factor(4.0))
1786 };
1787 let (left, right) = (
1788 frus_core::Alignment::CENTER_LEFT,
1789 frus_core::Alignment::CENTER_RIGHT,
1790 );
1791 let mut rt = Runtime::default();
1792 rt.advance_offsets(&tree(left), 1.0);
1793 assert!(square_at(&rt, &tree(left), size).x.abs() < 0.5);
1794 rt.advance_offsets(&tree(right), 0.05);
1795 let mid = square_at(&rt, &tree(right), size);
1796 assert!(
1797 (mid.x - 30.0).abs() < 0.5,
1798 "half way across the 80-px box: {mid:?}"
1799 );
1800 rt.advance_offsets(&tree(right), 1.0);
1801 let end = square_at(&rt, &tree(right), size);
1802 assert!((end.x - 60.0).abs() < 0.5, "{end:?}");
1803 }
1804
1805 #[test]
1808 fn an_anchor_slides_across_the_room_beside_a_factor() {
1809 let size = frus_core::Size::new(200.0, 300.0);
1810 let tree = |to| {
1811 crate::Flex::<()>::column()
1812 .child(AnimatedAlign::new(to, 0.10, Curve::Linear, square()).height_factor(2.0))
1813 };
1814 let (left, right) = (
1815 frus_core::Alignment::TOP_LEFT,
1816 frus_core::Alignment::TOP_RIGHT,
1817 );
1818 let mut rt = Runtime::default();
1819 rt.advance_offsets(&tree(left), 1.0);
1820 rt.advance_offsets(&tree(right), 0.05);
1821 let mid = square_at(&rt, &tree(right), size);
1822 assert!(
1823 (mid.x - 90.0).abs() < 0.5,
1824 "half way across the page: {mid:?}"
1825 );
1826 assert!(mid.y.abs() < 0.5, "at the top of its 40-px box: {mid:?}");
1827 }
1828
1829 #[test]
1832 fn an_animated_anchor_takes_the_room_as_aligned_does() {
1833 let size = frus_core::Size::new(300.0, 300.0);
1834 let root = Container::<()>::new()
1835 .width(100.0)
1836 .height(60.0)
1837 .child(AnimatedAlign::new(
1838 frus_core::Alignment::BOTTOM_RIGHT,
1839 0.10,
1840 Curve::Linear,
1841 square(),
1842 ));
1843 let at = square_at(&Runtime::default(), &root, size);
1844 assert!(
1845 (at.x - 80.0).abs() < 0.5 && (at.y - 40.0).abs() < 0.5,
1846 "{at:?}"
1847 );
1848 }
1849}