Skip to main content

frus_widgets/
animated.rs

1//! **Named** implicit-animation widgets: ergonomic sugar over [`Container`].
2//!
3//! Each one **wraps a configured [`Container`]** and delegates *everything* to it
4//! (a transparent wrapper, like [`crate::Keyed`]): the inner `Container` is the
5//! animated node, and its child stays a **separate** node — so the per-node
6//! animated values never collide. The identities (`child_id`), and therefore the
7//! animations, line up exactly with the paint walk.
8
9use 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
19/// Implements `Widget` for a `{ inner: Container<Msg> }` wrapper by delegating
20/// exactly the methods `Container` overrides (the rest are trait defaults,
21/// identical to `Container`'s). `debug_name` is **not** delegated: that way the
22/// inspector shows the named widget's own name.
23macro_rules! forward_to_container {
24    ($ty:ident) => {
25        // `Container` has **inherent** methods (the `on_click`, `repaint_boundary`…
26        // builders) sharing their names with the trait's, so the trait is called in
27        // fully qualified syntax (`Widget::…(&self.inner)`) to remove the
28        // ambiguity.
29        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            // Absent until milestone 477, and silently so: no wrapper set a padding, so
73            // nothing missed it. `AnimatedPadding` is the first, and the runtime looked
74            // straight through the wrapper and found nothing to drive.
75            fn anim_padding(&self) -> Option<frus_core::Insets> {
76                Widget::anim_padding(&self.inner)
77            }
78            // And this one is why that comment is here: `AnimatedAlign` is the first
79            // wrapper to set an offset, and a hook the macro does not forward is a value
80            // the runtime never finds.
81            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
129/// Applies a fixed **group opacity** `[0,1]` to its child, as one block. See
130/// [`Container::opacity`].
131pub struct Opacity<Msg = crate::callback::Callback> {
132    inner: Container<Msg>,
133}
134
135impl<Msg: Clone + 'static> Opacity<Msg> {
136    /// Wraps `child` in a group opacity of `opacity`.
137    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
146/// **Fades** its child toward `opacity` on every change. See
147/// [`Container::animated_opacity`].
148pub struct AnimatedOpacity<Msg = crate::callback::Callback> {
149    inner: Container<Msg>,
150}
151
152impl<Msg: Clone + 'static> AnimatedOpacity<Msg> {
153    /// Wraps `child` in an animated group opacity (`duration`, `curve`).
154    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
170/// A box whose properties **animate** on every change: color, size, radius and
171/// opacity — all with the **same** `(duration, curve)`. Builds a [`Container`]
172/// under the hood.
173///
174/// ```ignore
175/// AnimatedContainer::new(0.3, Curve::ease_in_out())
176///     .color(theme.primary)
177///     .size(200.0, 100.0)
178///     .radius(12.0)
179///     .child(Text::new("hi"))
180/// ```
181pub 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    /// A new animated box: `duration` (seconds) and `curve` shared by all of its
189    /// animated properties.
190    pub fn new(duration: f32, curve: Curve) -> Self {
191        Self {
192            inner: Container::new(),
193            duration,
194            curve,
195        }
196    }
197
198    /// A background whose color animates toward `color`.
199    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    /// Animated `width×height` size (interpolated at layout time).
207    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    /// Animated corner radius (uniform via `f32`, or per corner via [`BorderRadius`]).
215    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    /// Animated group opacity `[0,1]`.
223    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    /// Inner padding (static).
231    pub fn padding(mut self, padding: f32) -> Self {
232        self.inner = self.inner.padding(padding);
233        self
234    }
235
236    /// The box's child.
237    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
245/// **Slides its child** to each new anchor instead of letting it jump across the box. The
246/// reference's `AnimatedAlign`.
247///
248/// The anchor is where the child sits in the free space around it, so this is the widget
249/// for a thing that moves *within* something — a thumb crossing a track, a badge changing
250/// corner, a label that settles against the other edge when a panel opens.
251///
252/// ```
253/// use frus_core::{Alignment, Curve};
254/// use frus_widgets::{AnimatedAlign, Container};
255///
256/// let on = true;
257/// let _thumb: AnimatedAlign<()> = AnimatedAlign::new(
258///     if on { Alignment::CENTER_RIGHT } else { Alignment::CENTER_LEFT },
259///     0.15,
260///     Curve::ease_out(),
261///     Container::new().width(20.0).height(20.0),
262/// );
263/// ```
264///
265/// The **kind** of anchor is kept the whole way: a directional one stays directional, so
266/// in a right-to-left script the whole movement mirrors rather than the two ends
267/// mirroring separately and the child crossing the box the wrong way.
268///
269/// Unlike a scale's pivot, an anchor **is** the quantity here — it says where the child
270/// is, not where the maths starts from — which is why this one animates and that one does
271/// not.
272pub struct AnimatedAlign<Msg = crate::callback::Callback> {
273    inner: crate::Aligned<Msg>,
274}
275
276impl<Msg: Clone + 'static> AnimatedAlign<Msg> {
277    /// Anchors `child` at `alignment`, moving there over `duration`.
278    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    /// The box is `factor` times as wide as its child, rather than as wide as the room. See
290    /// [`Aligned::width_factor`](crate::Aligned::width_factor). The factor itself does not
291    /// animate, as the reference's does not: the anchor does.
292    pub fn width_factor(mut self, factor: f32) -> Self {
293        self.inner = self.inner.width_factor(factor);
294        self
295    }
296
297    /// The box is `factor` times as tall as its child. See
298    /// [`Aligned::height_factor`](crate::Aligned::height_factor).
299    pub fn height_factor(mut self, factor: f32) -> Self {
300        self.inner = self.inner.height_factor(factor);
301        self
302    }
303}
304
305/// Everything is the [`Aligned`](crate::Aligned) it is, which moves its anchor.
306impl<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
358/// **Slides its child by a fraction of its own size**, at paint time, moving there rather
359/// than jumping. The reference's `AnimatedSlide`.
360///
361/// `(1.0, 0.0)` is one whole width to the right — exactly clear of where the child was,
362/// whatever that width turns out to be. That is the difference between this and an
363/// animated `Transform::translate`: the number the caller writes is a multiple of a size
364/// the **layout** decides, so a panel can be parked off its own edge without anybody
365/// having to know how wide it ended up.
366///
367/// ```
368/// use frus_core::Curve;
369/// use frus_widgets::{AnimatedSlide, Text};
370///
371/// let open = true;
372/// let _panel: AnimatedSlide<()> = AnimatedSlide::new(
373///     if open { 0.0 } else { -1.0 },
374///     0.0,
375///     0.25,
376///     Curve::ease_out(),
377///     Text::new("Filters"),
378/// );
379/// ```
380///
381/// Layout is untouched: the box stays where it was put and the neighbours do not move, so
382/// a child sliding out leaves its space behind rather than dragging the page after it.
383pub struct AnimatedSlide<Msg = crate::callback::Callback> {
384    inner: Box<dyn Widget<Msg>>,
385}
386
387impl<Msg: Clone + 'static> AnimatedSlide<Msg> {
388    /// Slides `child` towards `fx` widths across and `fy` heights down.
389    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    /// A transparent wrapper: the box is the child's. In an impl with no bounds, because
408    /// the forwarding macro's own impl has none — a `restyle` declared beside the
409    /// constructors could not be reached from it.
410    fn restyle(&self, base: Style) -> Style {
411        base
412    }
413}
414
415crate::transparent::forward_transparent!(AnimatedSlide {
416    /// Every one of these is **forwarded**: a paint-time slide is not an identity, not a
417    /// place, not a theme and not a surface. It is its child, moving.
418    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    /// Forwarded: a wrapper around a form is still that form (milestone 512).
440    fn autofill_group(&self) -> bool {
441        self.inner.autofill_group()
442    }
443    /// Forwarded: the area's bar is the area's, whatever wraps it.
444    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    /// `AnimatedContainer` does declare the animated targets of its properties, with
457    /// the shared duration and curve.
458    #[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)); // animated opacity
473        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    /// `Opacity` is a fixed opacity group (no animated value) wrapping its child
479    /// (a separate node).
480    #[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    /// `AnimatedOpacity` declares an animated opacity (the runtime tweens it).
493    #[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        // Its own name for the inspector (not delegated to the Container).
501        assert_eq!(Widget::<()>::debug_name(&w), "AnimatedOpacity");
502    }
503}
504
505/// **Grows and shrinks its child** rather than letting it jump between sizes — a
506/// paint-time scale, so nothing around it moves.
507///
508/// The reference's `AnimatedScale`. It wraps a [`Transform`](crate::Transform) that was
509/// told to animate: the widget declares the scale it is heading for and the runtime
510/// drives it there, on the same clock as every other implicit animation here.
511///
512/// ```
513/// use frus_core::Curve;
514/// use frus_widgets::{AnimatedScale, Text};
515///
516/// let pressed = true;
517/// let _sunken: AnimatedScale<()> = AnimatedScale::new(
518///     if pressed { 0.95 } else { 1.0 },
519///     0.12,
520///     Curve::ease_out(),
521///     Text::new("Tap"),
522/// );
523/// ```
524pub struct AnimatedScale<Msg = crate::callback::Callback> {
525    inner: Box<dyn Widget<Msg>>,
526}
527
528impl<Msg: Clone + 'static> AnimatedScale<Msg> {
529    /// Scales `child` towards `scale`, about its **centre**.
530    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    /// The same, about a `pivot` of the caller's choosing — the corner or edge that
535    /// stays put while the rest grows away from it.
536    ///
537    /// The pivot itself does not animate, whatever it is set to: it is a choice of
538    /// origin, not a quantity, and moving it would slide the child across the screen
539    /// with nothing describing the child having changed.
540    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    /// A transparent wrapper: the box is the child's. In an impl with no bounds,
559    /// because the forwarding macro's own impl has none — a `restyle` declared beside
560    /// the constructors could not be reached from it.
561    fn restyle(&self, base: Style) -> Style {
562        base
563    }
564}
565
566crate::transparent::forward_transparent!(AnimatedScale {
567    /// Every one of these is **forwarded**: an animated transform is not an identity,
568    /// not a place, not a theme and not a surface. It is its child, moving.
569    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    /// Forwarded: a wrapper around a form is still that form (milestone 512).
591    fn autofill_group(&self) -> bool {
592        self.inner.autofill_group()
593    }
594    /// Forwarded: the area's bar is the area's, whatever wraps it.
595    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
603/// **Turns its child** rather than letting it snap between angles — a paint-time
604/// rotation, so nothing around it moves. The reference's `AnimatedRotation`.
605///
606/// Its unit is **radians**, clockwise, like every other angle in this framework, and not
607/// the reference's turns: a turn is a nice number to type and a bad one to mix with the
608/// `Path` and `Transform` calls beside it, which are all radians.
609///
610/// ```
611/// use frus_core::Curve;
612/// use frus_widgets::{AnimatedRotation, Icon, Icons};
613///
614/// let open = true;
615/// let _chevron: AnimatedRotation<()> = AnimatedRotation::new(
616///     if open { std::f32::consts::PI } else { 0.0 },
617///     0.2,
618///     Curve::ease_in_out(),
619///     Icon::new(Icons::EXPAND_MORE),
620/// );
621/// ```
622pub struct AnimatedRotation<Msg = crate::callback::Callback> {
623    inner: Box<dyn Widget<Msg>>,
624}
625
626impl<Msg: Clone + 'static> AnimatedRotation<Msg> {
627    /// Turns `child` towards `radians`, about its **centre**.
628    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    /// The same, about a `pivot` — the point the child turns around.
644    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    /// A transparent wrapper: the box is the child's. In an impl with no bounds,
663    /// because the forwarding macro's own impl has none — a `restyle` declared beside
664    /// the constructors could not be reached from it.
665    fn restyle(&self, base: Style) -> Style {
666        base
667    }
668}
669
670crate::transparent::forward_transparent!(AnimatedRotation {
671    /// Every one of these is **forwarded**: an animated transform is not an identity,
672    /// not a place, not a theme and not a surface. It is its child, moving.
673    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    /// Forwarded: a wrapper around a form is still that form (milestone 512).
695    fn autofill_group(&self) -> bool {
696        self.inner.autofill_group()
697    }
698    /// Forwarded: the area's bar is the area's, whatever wraps it.
699    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
707/// **Moves its child's inset** rather than letting the space around it jump. The
708/// reference's `AnimatedPadding`.
709///
710/// Unlike the two above, this one is **layout**: the interpolated padding is injected
711/// while the tree is being measured, so everything beside and below the child follows it
712/// as it moves. That is the point of padding animating at all — a paint-time offset
713/// would slide the child over its neighbours instead of making room.
714///
715/// ```
716/// use frus_core::Curve;
717/// use frus_widgets::{AnimatedPadding, Text};
718///
719/// let selected = true;
720/// let _row: AnimatedPadding<()> = AnimatedPadding::new(
721///     if selected { 24.0 } else { 8.0 },
722///     0.2,
723///     Curve::ease_in_out(),
724///     Text::new("Inbox"),
725/// );
726/// ```
727pub struct AnimatedPadding<Msg = crate::callback::Callback> {
728    inner: Container<Msg>,
729}
730
731impl<Msg: Clone + 'static> AnimatedPadding<Msg> {
732    /// Insets `child` by `padding` on all four sides, moving there over `duration`.
733    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
749/// **Moves a stack layer between two sets of pins** instead of letting it appear at the
750/// new ones. The reference's `AnimatedPositioned`.
751///
752/// A panel that slides in from an edge, a sheet that grows to fill the page, a card that
753/// travels to a corner: each is a layer of a [`crate::Stack`] whose distances from the
754/// stack's own edges change, and each of them jumped before this.
755///
756/// ```
757/// use frus_core::Curve;
758/// use frus_widgets::{AnimatedPositioned, Container, Stack};
759///
760/// let open = true;
761/// let _sheet: Stack<()> = Stack::new().layer(
762///     AnimatedPositioned::new(0.25, Curve::ease_out(), Container::new())
763///         .left(0.0)
764///         .right(0.0)
765///         .bottom(0.0)
766///         .height(if open { 320.0 } else { 0.0 }),
767/// );
768/// ```
769///
770/// **A pin that is unset does not animate.** `None` on an edge is not nought there: it is
771/// a layer not pinned on that side, sized by itself and placed by the stack's alignment,
772/// which is a different arrangement rather than a different number. So an edge arriving
773/// takes hold at once, an edge leaving lets go at once, and only an edge set at both ends
774/// travels. Where a movement is wanted, both ends must say where they are.
775///
776/// The six pins share **one timeline**, as a scale and a turn do: a layer changing two of
777/// its edges arrives on both at the same moment rather than on two clocks that happen to
778/// agree.
779///
780/// Unlike [`crate::Positioned`], which is a transparent wrapper, this is a **node of its
781/// own** with the child beneath it. An animated value belongs to a node, and a wrapper
782/// that fused with its child would put the layer's timeline and the child's on the same
783/// one — the rule the whole of this module is built on.
784pub 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    /// A layer holding `child`, pinned nowhere yet, moving to each new set of pins over
793    /// `duration` seconds on `curve`.
794    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    /// Distance from the stack's left edge.
804    pub fn left(mut self, px: f32) -> Self {
805        self.spec.left = Some(px);
806        self
807    }
808
809    /// Distance from the stack's top edge.
810    pub fn top(mut self, px: f32) -> Self {
811        self.spec.top = Some(px);
812        self
813    }
814
815    /// Distance from the stack's right edge.
816    pub fn right(mut self, px: f32) -> Self {
817        self.spec.right = Some(px);
818        self
819    }
820
821    /// Distance from the stack's bottom edge.
822    pub fn bottom(mut self, px: f32) -> Self {
823        self.spec.bottom = Some(px);
824        self
825    }
826
827    /// Distance from the stack's **start** edge: the left in a left-to-right script, the
828    /// right in a right-to-left one (milestone 589). A `left` or a `right` set as well wins
829    /// on its own side.
830    pub fn start(mut self, px: f32) -> Self {
831        self.spec.start = Some(px);
832        self
833    }
834
835    /// Distance from the stack's **end** edge: the right in a left-to-right script, the left
836    /// in a right-to-left one.
837    pub fn end(mut self, px: f32) -> Self {
838        self.spec.end = Some(px);
839        self
840    }
841
842    /// An explicit width, used when only one horizontal edge is pinned.
843    pub fn width(mut self, px: f32) -> Self {
844        self.spec.width = Some(px);
845        self
846    }
847
848    /// An explicit height, used when only one vertical edge is pinned.
849    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        // Nothing of its own: the stack forces a pinned layer into what its edges say,
858        // and an axis nobody pinned is left to the child underneath.
859        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        // A pure layout widget: no decoration of its own.
868    }
869
870    fn on_click(&self) -> Option<Msg> {
871        None
872    }
873
874    /// Where it is **going**. The stack reads the runtime's interpolated pins instead
875    /// wherever there are any, which is what makes the difference between the two.
876    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
897/// **Hands a moving text style down to everything below it** rather than restyling the
898/// subtree at once. The reference's `AnimatedDefaultTextStyle`.
899///
900/// A heading that shrinks as a page scrolls, a label that goes from muted to emphatic when
901/// its row is selected, a caption thickening as it becomes the thing being edited: each is
902/// a run of words whose *type* changes, and each of them jumped before this.
903///
904/// ```
905/// use frus_core::{Curve, TextStyle};
906/// use frus_widgets::{AnimatedDefaultTextStyle, Text};
907///
908/// let selected = true;
909/// let _row: AnimatedDefaultTextStyle<()> = AnimatedDefaultTextStyle::new(
910///     0.2,
911///     Curve::ease_out(),
912///     TextStyle::NONE.size(if selected { 22.0 } else { 16.0 }),
913///     Text::new("Reminders"),
914/// );
915/// ```
916///
917/// It reaches the subtree through the **theme**, the way
918/// [`DefaultTextStyle::around`](crate::DefaultTextStyle::around) does: a
919/// [`Text`](crate::Text) resolves *what the caller said ?? what the subtree hands down ??
920/// what the framework ships*, so this styles the words a caller passed in already
921/// assembled and never has to be threaded through them. A field the caller set on a
922/// particular text still wins — an inherited style is the answer to a question, not an
923/// instruction.
924///
925/// **It is the only animated value in this framework that layout reads through the
926/// theme**, which is why it is the only one whose consumer is the walk's theme swap
927/// rather than the node. That swap is made four times over a frame, and all four go
928/// through one function so they cannot answer differently.
929///
930/// **Only the type moves.** Alignment, wrapping, overflow and the line count take effect
931/// the moment they change, exactly as the reference says of its own: they are
932/// arrangements rather than quantities, and there is nothing between wrapping and not.
933/// What travels, what holds and what swaps at the halfway point within the type itself is
934/// [`TextStyle::lerp`]'s business — in short, sizes travel, a field only one end states
935/// holds still, and faces swap in the middle.
936///
937/// Like [`AnimatedPositioned`], and unlike the transparent
938/// [`Themed`](crate::Themed) it is built on the same idea as, this is a **node of its
939/// own**. Two of these nested — an outer one moving the colour, an inner one the size,
940/// which is a thing the cascade positively invites — would otherwise fuse into one node
941/// and put two timelines on it.
942pub 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    /// A subtree wearing `style`, moving to each new one over `duration` seconds on
955    /// `curve`.
956    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    /// Where the lines sit inside their box. **Not animated** — it takes effect at once.
975    pub fn align(mut self, align: TextAlign) -> Self {
976        self.align = Some(align);
977        self
978    }
979
980    /// Whether the text wraps at the width it is given. **Not animated.**
981    pub fn soft_wrap(mut self, wrap: bool) -> Self {
982        self.soft_wrap = Some(wrap);
983        self
984    }
985
986    /// What becomes of text that does not fit. **Not animated.**
987    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
988        self.overflow = Some(overflow);
989        self
990    }
991
992    /// At most this many lines. **Not animated.**
993    pub fn max_lines(mut self, lines: usize) -> Self {
994        self.max_lines = Some(lines);
995        self
996    }
997
998    /// The style this subtree hands down, as a whole.
999    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        // Nothing of its own: it is a style handed down, not a box. The child's box is
1014        // this node's box.
1015        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        // Nothing of its own: what it does, it does to the theme.
1024    }
1025
1026    fn on_click(&self) -> Option<Msg> {
1027        None
1028    }
1029
1030    /// Where it is **going**, plus the four answers that are not going anywhere.
1031    ///
1032    /// The walk lays the runtime's interpolated style over this wherever there is one
1033    /// (`crate::ui::scoped_theme`), which is what makes the difference between this and a
1034    /// plain [`DefaultTextStyle::around`](crate::DefaultTextStyle::around). On the frame
1035    /// this node mounts there is no interpolated style yet, and the target is the right
1036    /// answer: an implicit animation adopts its target on mount rather than playing from
1037    /// nowhere.
1038    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
1061/// **Grows and shrinks its share of the parent** rather than taking the new share at
1062/// once. The reference's `AnimatedFractionallySizedBox`, and a name over
1063/// [`crate::FractionallySizedBox::animated`].
1064///
1065/// The quantity is a fraction and not a length, which is the reason to reach for this
1066/// rather than an animated size: a bar filling half its row, a panel taking two thirds of
1067/// a page, a sheet at a third of the height — none of them knows what those come to, and
1068/// a resize of the window during the movement is answered by the layout rather than by
1069/// a number that was right when it was written.
1070///
1071/// ```
1072/// use frus_core::Curve;
1073/// use frus_widgets::{AnimatedFractionallySizedBox, Container};
1074///
1075/// let full = true;
1076/// let _bar: AnimatedFractionallySizedBox<()> =
1077///     AnimatedFractionallySizedBox::new(0.3, Curve::ease_out(), Container::new())
1078///         .width_factor(if full { 1.0 } else { 0.25 });
1079/// ```
1080///
1081/// An axis whose factor is **unset** follows its content, and does not travel to or from
1082/// a factor: that is a change of arrangement, not of value.
1083pub struct AnimatedFractionallySizedBox<Msg = crate::callback::Callback> {
1084    inner: crate::fractional::FractionallySizedBox<Msg>,
1085}
1086
1087impl<Msg: Clone + 'static> AnimatedFractionallySizedBox<Msg> {
1088    /// A fractional box holding `child`, with no factor yet, moving to each new one over
1089    /// `duration` seconds on `curve`.
1090    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    /// Fraction `0.0..=1.0` of the parent's **width** (clamped to `>= 0`).
1099    pub fn width_factor(mut self, factor: f32) -> Self {
1100        self.inner = self.inner.width_factor(factor);
1101        self
1102    }
1103
1104    /// Fraction `0.0..=1.0` of the parent's **height** (clamped to `>= 0`).
1105    pub fn height_factor(mut self, factor: f32) -> Self {
1106        self.inner = self.inner.height_factor(factor);
1107        self
1108    }
1109}
1110
1111// Written out rather than macro-forwarded: the inner widget is a `FractionallySizedBox`
1112// and not a `Container`, and it overrides seven hooks — the four a layout widget has and
1113// the three this one is for.
1114impl<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    /// A tree with one animated scale on it, so the runtime has a target to drive.
1149    fn scaled(to: f32) -> AnimatedScale<()> {
1150        AnimatedScale::new(to, 0.10, Curve::Linear, Text::new("x"))
1151    }
1152
1153    /// **Mounted, mid-flight, at rest** — the three the issue asks each of these to pin.
1154    ///
1155    /// The first is the one worth stating: a widget that appears already scaled **adopts**
1156    /// its target rather than growing into it from nothing. Without that rule every
1157    /// implicit animation in the framework would play once on the frame it was born, and
1158    /// a page would breathe when it opened.
1159    #[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        // Halfway along a linear curve of 0.10 s: halfway between 2 and 1.
1174        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    /// The same three for a rotation, and that the two do not leak into one another: a
1191    /// widget that only turns keeps its scale at **one**, not at nought — which is the
1192    /// difference between a turning child and a child that vanished.
1193    #[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    /// And the padding, which is the one that is **layout** rather than paint: the
1218    /// interpolated value is injected while the tree is measured.
1219    #[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    /// **And the paint reads it**, which is a separate question from whether the runtime
1238    /// computes it. A tween the walk never asks for is a number that moves correctly and
1239    /// changes nothing on the screen, and this framework has shipped that bug before.
1240    #[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        // Now heading back to 1, half way along.
1258        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        // A runtime that has never heard of it draws the target, which is what an
1266        // isolated frame — a test, a golden — has to show.
1267        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    /// A tree with one animated anchor on it, so the runtime has a pair to drive.
1273    fn anchored(to: frus_core::Alignment) -> AnimatedAlign<()> {
1274        AnimatedAlign::new(to, 0.10, Curve::Linear, Text::new("x"))
1275    }
1276
1277    /// **Mounted, mid-flight, at rest**, for the anchor.
1278    #[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        // Halfway along a linear curve of 0.10 s: halfway from the left edge to the right.
1293        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    /// And for the slide, whose pair is a multiple of the child's own box rather than a
1307    /// share of the free space around it.
1308    #[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    /// **And the paint reads it** — the question the value tests above cannot ask. A
1327    /// tween the walk never consults moves correctly and changes nothing on the screen,
1328    /// and this framework has shipped that bug before.
1329    ///
1330    /// Both rules are checked, because they are two separate lines in the walk: an
1331    /// anchor's box slides through the free space, and a slide's box moves by its own
1332    /// width.
1333    #[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        // An anchor crossing a 100 px box: at the halfway point the 20 px mark sits at 40,
1355        // the middle of the 80 px of free space — not at 0 and not at 80.
1356        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        // A slide of one whole width, half done: the 20 px mark has moved 10.
1373        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    /// The anchor is interpolated in **its own** coordinates, so a directional one stays
1385    /// directional the whole way: in a right-to-left script the mirrored movement is the
1386    /// mirror of the movement, rather than a slide between two already-mirrored ends.
1387    #[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        // A quarter of the way from the start edge towards the end one.
1406        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    /// A stack with one animated layer on it, pinned at `left` and 20 px wide.
1436    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    /// **Mounted, mid-flight, at rest** for a layer's pins.
1453    ///
1454    /// A layer that appears already pinned somewhere **adopts** its pins rather than
1455    /// travelling to them from the last place a stack would have put it — the same mount
1456    /// rule as every other implicit animation here, and the reason a page does not
1457    /// rearrange itself when it opens.
1458    #[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        // Child 0 of the stack, which is the layer.
1466        let id = WidgetId::ROOT.child(0);
1467        assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(0.0));
1468
1469        // Halfway along a linear curve of 0.10 s: halfway between 0 and 80.
1470        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    /// **A pin that is unset does not travel**, in either direction.
1491    ///
1492    /// `None` on an edge is not nought there — it is a layer not pinned on that side at
1493    /// all, sized by itself and placed by the stack's alignment. Sliding between the two
1494    /// would be interpolating between two arrangements, and the number it passed through
1495    /// on the way would mean neither of them.
1496    #[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        // Swap which edge is pinned, and step a fifth of the way in.
1514        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    /// **And the stack lays the layer out where the tween says.** The value tests above
1528    /// cannot ask this: pins that move correctly and are read from the widget instead of
1529    /// the runtime give a layer that jumps, and every one of them still passes.
1530    #[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    /// A box taking half its parent, then all of it.
1557    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    /// **Mounted, mid-flight, at rest** for a share of the parent.
1572    #[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    /// **And the layout reads it.** A fraction is consumed while the tree is measured, so
1602    /// the check is the width the child actually came out — half of a hundred, halfway
1603    /// between a quarter of it and three quarters.
1604    #[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    // --- The inherited text style (milestone 498) ---
1626
1627    /// A subtree wearing a style that moves, over a linear tenth of a second.
1628    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    /// **Mounted, mid-flight, at rest** — the three the issue asks each of these to pin.
1638    #[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    /// **And the moving style reaches the words**, which is a different question from
1665    /// whether the number moves — milestone 477's fourth test, and the one that has
1666    /// caught something every time it has been asked.
1667    ///
1668    /// This family is the one where it matters most: the value is consumed by the walk's
1669    /// **theme swap** rather than by the node, so a correct tween and a correct target can
1670    /// sit either side of a subtree that never reads either. Take the merge out of
1671    /// `crate::ui::scoped_theme` and the test above still passes; this one paints the
1672    /// target from the first frame.
1673    #[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    /// **The four unanimated answers take effect at once**, as the reference says of its
1710    /// own: there is nothing between wrapping and not, so they are not quantities and do
1711    /// not travel. They still have to *arrive*, which is what this checks — they reach
1712    /// the subtree through the same theme the style does.
1713    #[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    /// **Two nested ones are two nodes**, which is the reason this is not a transparent
1729    /// wrapper. An outer one moving the colour and an inner one the size is a thing the
1730    /// cascade positively invites; fused into one node they would be two timelines on one
1731    /// identity, and the second would overwrite the first every frame.
1732    #[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    /// Where the red 20-px square landed, with the runtime `rt` driving the anchors.
1757    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    /// **An anchor with a factor**: the box is four squares wide, and the square slides
1778    /// across it from the left to the right edge, half way at half the time (milestone 600).
1779    #[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    /// **And across the other axis**, the one without a factor, the box is the room and the
1806    /// square slides across all of it.
1807    #[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    /// **Without a factor, it takes the room**, as `Aligned` does: in a 100×60 box, a square
1830    /// anchored bottom right is in the box's corner.
1831    #[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}