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        }
121    };
122}
123
124/// Applies a fixed **group opacity** `[0,1]` to its child, as one block. See
125/// [`Container::opacity`].
126pub struct Opacity<Msg = crate::callback::Callback> {
127    inner: Container<Msg>,
128}
129
130impl<Msg: Clone + 'static> Opacity<Msg> {
131    /// Wraps `child` in a group opacity of `opacity`.
132    pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
133        Self {
134            inner: Container::new().opacity(opacity).child(child),
135        }
136    }
137}
138
139forward_to_container!(Opacity);
140
141/// **Fades** its child toward `opacity` on every change. See
142/// [`Container::animated_opacity`].
143pub struct AnimatedOpacity<Msg = crate::callback::Callback> {
144    inner: Container<Msg>,
145}
146
147impl<Msg: Clone + 'static> AnimatedOpacity<Msg> {
148    /// Wraps `child` in an animated group opacity (`duration`, `curve`).
149    pub fn new(
150        opacity: f32,
151        duration: f32,
152        curve: Curve,
153        child: impl Widget<Msg> + 'static,
154    ) -> Self {
155        Self {
156            inner: Container::new()
157                .animated_opacity(opacity, duration, curve)
158                .child(child),
159        }
160    }
161}
162
163forward_to_container!(AnimatedOpacity);
164
165/// A box whose properties **animate** on every change: color, size, radius and
166/// opacity — all with the **same** `(duration, curve)`. Builds a [`Container`]
167/// under the hood.
168///
169/// ```ignore
170/// AnimatedContainer::new(0.3, Curve::ease_in_out())
171///     .color(theme.primary)
172///     .size(200.0, 100.0)
173///     .radius(12.0)
174///     .child(Text::new("hi"))
175/// ```
176pub struct AnimatedContainer<Msg = crate::callback::Callback> {
177    inner: Container<Msg>,
178    duration: f32,
179    curve: Curve,
180}
181
182impl<Msg: Clone + 'static> AnimatedContainer<Msg> {
183    /// A new animated box: `duration` (seconds) and `curve` shared by all of its
184    /// animated properties.
185    pub fn new(duration: f32, curve: Curve) -> Self {
186        Self {
187            inner: Container::new(),
188            duration,
189            curve,
190        }
191    }
192
193    /// A background whose color animates toward `color`.
194    pub fn color(mut self, color: Color) -> Self {
195        self.inner = self
196            .inner
197            .animated_color(color, self.duration, self.curve.clone());
198        self
199    }
200
201    /// Animated `width×height` size (interpolated at layout time).
202    pub fn size(mut self, width: f32, height: f32) -> Self {
203        self.inner = self
204            .inner
205            .animated_size(width, height, self.duration, self.curve.clone());
206        self
207    }
208
209    /// Animated corner radius (uniform via `f32`, or per corner via [`BorderRadius`]).
210    pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
211        self.inner = self
212            .inner
213            .animated_radius(radius, self.duration, self.curve.clone());
214        self
215    }
216
217    /// Animated group opacity `[0,1]`.
218    pub fn opacity(mut self, opacity: f32) -> Self {
219        self.inner = self
220            .inner
221            .animated_opacity(opacity, self.duration, self.curve.clone());
222        self
223    }
224
225    /// Inner padding (static).
226    pub fn padding(mut self, padding: f32) -> Self {
227        self.inner = self.inner.padding(padding);
228        self
229    }
230
231    /// The box's child.
232    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
233        self.inner = self.inner.child(child);
234        self
235    }
236}
237
238forward_to_container!(AnimatedContainer);
239
240/// **Slides its child** to each new anchor instead of letting it jump across the box. The
241/// reference's `AnimatedAlign`.
242///
243/// The anchor is where the child sits in the free space around it, so this is the widget
244/// for a thing that moves *within* something — a thumb crossing a track, a badge changing
245/// corner, a label that settles against the other edge when a panel opens.
246///
247/// ```
248/// use frus_core::{Alignment, Curve};
249/// use frus_widgets::{AnimatedAlign, Container};
250///
251/// let on = true;
252/// let _thumb: AnimatedAlign<()> = AnimatedAlign::new(
253///     if on { Alignment::CENTER_RIGHT } else { Alignment::CENTER_LEFT },
254///     0.15,
255///     Curve::ease_out(),
256///     Container::new().width(20.0).height(20.0),
257/// );
258/// ```
259///
260/// The **kind** of anchor is kept the whole way: a directional one stays directional, so
261/// in a right-to-left script the whole movement mirrors rather than the two ends
262/// mirroring separately and the child crossing the box the wrong way.
263///
264/// Unlike a scale's pivot, an anchor **is** the quantity here — it says where the child
265/// is, not where the maths starts from — which is why this one animates and that one does
266/// not.
267pub struct AnimatedAlign<Msg = crate::callback::Callback> {
268    inner: Container<Msg>,
269}
270
271impl<Msg: Clone + 'static> AnimatedAlign<Msg> {
272    /// Anchors `child` at `alignment`, moving there over `duration`.
273    pub fn new(
274        alignment: impl Into<frus_core::AlignmentGeometry>,
275        duration: f32,
276        curve: Curve,
277        child: impl Widget<Msg> + 'static,
278    ) -> Self {
279        Self {
280            inner: Container::new()
281                .animated_alignment(alignment, duration, curve)
282                .child(child),
283        }
284    }
285}
286
287forward_to_container!(AnimatedAlign);
288
289/// **Slides its child by a fraction of its own size**, at paint time, moving there rather
290/// than jumping. The reference's `AnimatedSlide`.
291///
292/// `(1.0, 0.0)` is one whole width to the right — exactly clear of where the child was,
293/// whatever that width turns out to be. That is the difference between this and an
294/// animated `Transform::translate`: the number the caller writes is a multiple of a size
295/// the **layout** decides, so a panel can be parked off its own edge without anybody
296/// having to know how wide it ended up.
297///
298/// ```
299/// use frus_core::Curve;
300/// use frus_widgets::{AnimatedSlide, Text};
301///
302/// let open = true;
303/// let _panel: AnimatedSlide<()> = AnimatedSlide::new(
304///     if open { 0.0 } else { -1.0 },
305///     0.0,
306///     0.25,
307///     Curve::ease_out(),
308///     Text::new("Filters"),
309/// );
310/// ```
311///
312/// Layout is untouched: the box stays where it was put and the neighbours do not move, so
313/// a child sliding out leaves its space behind rather than dragging the page after it.
314pub struct AnimatedSlide<Msg = crate::callback::Callback> {
315    inner: Box<dyn Widget<Msg>>,
316}
317
318impl<Msg: Clone + 'static> AnimatedSlide<Msg> {
319    /// Slides `child` towards `fx` widths across and `fy` heights down.
320    pub fn new(
321        fx: f32,
322        fy: f32,
323        duration: f32,
324        curve: Curve,
325        child: impl Widget<Msg> + 'static,
326    ) -> Self {
327        Self {
328            inner: Box::new(
329                crate::FractionalTranslation::new(fx, fy)
330                    .animated(duration, curve)
331                    .child(child),
332            ),
333        }
334    }
335}
336
337impl<Msg> AnimatedSlide<Msg> {
338    /// A transparent wrapper: the box is the child's. In an impl with no bounds, because
339    /// the forwarding macro's own impl has none — a `restyle` declared beside the
340    /// constructors could not be reached from it.
341    fn restyle(&self, base: Style) -> Style {
342        base
343    }
344}
345
346crate::transparent::forward_transparent!(AnimatedSlide {
347    /// Every one of these is **forwarded**: a paint-time slide is not an identity, not a
348    /// place, not a theme and not a surface. It is its child, moving.
349    fn key(&self) -> Option<u64> {
350        self.inner.key()
351    }
352    fn positioned(&self) -> Option<crate::positioned::Positioning> {
353        self.inner.positioned()
354    }
355    fn theme_override(
356        &self,
357        inherited: &crate::theme::Theme,
358    ) -> Option<Box<crate::theme::Theme>> {
359        self.inner.theme_override(inherited)
360    }
361    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
362        self.inner.media_override(inherited)
363    }
364    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
365        self.inner.scaffold_override()
366    }
367    /// Forwarded: a wrapper around a form is still that form (milestone 512).
368    fn autofill_group(&self) -> bool {
369        self.inner.autofill_group()
370    }
371});
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    /// `AnimatedContainer` does declare the animated targets of its properties, with
378    /// the shared duration and curve.
379    #[test]
380    fn animated_container_declares_all_targets() {
381        let blue = Color::rgb(0.0, 0.0, 1.0);
382        let w: AnimatedContainer<()> = AnimatedContainer::new(0.25, Curve::ease_out())
383            .color(blue)
384            .size(200.0, 100.0)
385            .radius(12.0)
386            .opacity(0.5);
387        assert_eq!(Widget::<()>::anim_color(&w), Some(blue));
388        assert_eq!(Widget::<()>::anim_size(&w), Some(Size::new(200.0, 100.0)));
389        assert_eq!(
390            Widget::<()>::anim_radius(&w),
391            Some(BorderRadius::from(12.0))
392        );
393        assert_eq!(Widget::<()>::anim_target(&w), Some(0.5)); // animated opacity
394        assert_eq!(Widget::<()>::opacity_group(&w), Some(0.5));
395        assert_eq!(Widget::<()>::anim_duration(&w), 0.25);
396        assert_eq!(Widget::<()>::anim_curve(&w), Curve::ease_out());
397    }
398
399    /// `Opacity` is a fixed opacity group (no animated value) wrapping its child
400    /// (a separate node).
401    #[test]
402    fn opacity_wraps_child_as_a_group() {
403        let w: Opacity<()> = Opacity::new(0.4, crate::Container::new().width(10.0).height(10.0));
404        assert_eq!(Widget::<()>::opacity_group(&w), Some(0.4));
405        assert_eq!(Widget::<()>::anim_target(&w), None);
406        assert_eq!(
407            Widget::<()>::children(&w).len(),
408            1,
409            "the child is a separate node"
410        );
411    }
412
413    /// `AnimatedOpacity` declares an animated opacity (the runtime tweens it).
414    #[test]
415    fn animated_opacity_declares_a_group_target() {
416        let w: AnimatedOpacity<()> =
417            AnimatedOpacity::new(0.0, 0.2, Curve::ease_in(), crate::Container::new());
418        assert_eq!(Widget::<()>::opacity_group(&w), Some(0.0));
419        assert_eq!(Widget::<()>::anim_target(&w), Some(0.0));
420        assert_eq!(Widget::<()>::anim_duration(&w), 0.2);
421        // Its own name for the inspector (not delegated to the Container).
422        assert_eq!(Widget::<()>::debug_name(&w), "AnimatedOpacity");
423    }
424}
425
426/// **Grows and shrinks its child** rather than letting it jump between sizes — a
427/// paint-time scale, so nothing around it moves.
428///
429/// The reference's `AnimatedScale`. It wraps a [`Transform`](crate::Transform) that was
430/// told to animate: the widget declares the scale it is heading for and the runtime
431/// drives it there, on the same clock as every other implicit animation here.
432///
433/// ```
434/// use frus_core::Curve;
435/// use frus_widgets::{AnimatedScale, Text};
436///
437/// let pressed = true;
438/// let _sunken: AnimatedScale<()> = AnimatedScale::new(
439///     if pressed { 0.95 } else { 1.0 },
440///     0.12,
441///     Curve::ease_out(),
442///     Text::new("Tap"),
443/// );
444/// ```
445pub struct AnimatedScale<Msg = crate::callback::Callback> {
446    inner: Box<dyn Widget<Msg>>,
447}
448
449impl<Msg: Clone + 'static> AnimatedScale<Msg> {
450    /// Scales `child` towards `scale`, about its **centre**.
451    pub fn new(scale: f32, duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
452        Self::about(scale, frus_core::Alignment::CENTER, duration, curve, child)
453    }
454
455    /// The same, about a `pivot` of the caller's choosing — the corner or edge that
456    /// stays put while the rest grows away from it.
457    ///
458    /// The pivot itself does not animate, whatever it is set to: it is a choice of
459    /// origin, not a quantity, and moving it would slide the child across the screen
460    /// with nothing describing the child having changed.
461    pub fn about(
462        scale: f32,
463        pivot: frus_core::Alignment,
464        duration: f32,
465        curve: Curve,
466        child: impl Widget<Msg> + 'static,
467    ) -> Self {
468        Self {
469            inner: Box::new(
470                crate::Transform::scale_xy_from(scale, scale, pivot)
471                    .animated(duration, curve)
472                    .child(child),
473            ),
474        }
475    }
476}
477
478impl<Msg> AnimatedScale<Msg> {
479    /// A transparent wrapper: the box is the child's. In an impl with no bounds,
480    /// because the forwarding macro's own impl has none — a `restyle` declared beside
481    /// the constructors could not be reached from it.
482    fn restyle(&self, base: Style) -> Style {
483        base
484    }
485}
486
487crate::transparent::forward_transparent!(AnimatedScale {
488    /// Every one of these is **forwarded**: an animated transform is not an identity,
489    /// not a place, not a theme and not a surface. It is its child, moving.
490    fn key(&self) -> Option<u64> {
491        self.inner.key()
492    }
493    fn positioned(&self) -> Option<crate::positioned::Positioning> {
494        self.inner.positioned()
495    }
496    fn theme_override(
497        &self,
498        inherited: &crate::theme::Theme,
499    ) -> Option<Box<crate::theme::Theme>> {
500        self.inner.theme_override(inherited)
501    }
502    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
503        self.inner.media_override(inherited)
504    }
505    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
506        self.inner.scaffold_override()
507    }
508    /// Forwarded: a wrapper around a form is still that form (milestone 512).
509    fn autofill_group(&self) -> bool {
510        self.inner.autofill_group()
511    }
512});
513
514/// **Turns its child** rather than letting it snap between angles — a paint-time
515/// rotation, so nothing around it moves. The reference's `AnimatedRotation`.
516///
517/// Its unit is **radians**, clockwise, like every other angle in this framework, and not
518/// the reference's turns: a turn is a nice number to type and a bad one to mix with the
519/// `Path` and `Transform` calls beside it, which are all radians.
520///
521/// ```
522/// use frus_core::Curve;
523/// use frus_widgets::{AnimatedRotation, Icon, Icons};
524///
525/// let open = true;
526/// let _chevron: AnimatedRotation<()> = AnimatedRotation::new(
527///     if open { std::f32::consts::PI } else { 0.0 },
528///     0.2,
529///     Curve::ease_in_out(),
530///     Icon::new(Icons::EXPAND_MORE),
531/// );
532/// ```
533pub struct AnimatedRotation<Msg = crate::callback::Callback> {
534    inner: Box<dyn Widget<Msg>>,
535}
536
537impl<Msg: Clone + 'static> AnimatedRotation<Msg> {
538    /// Turns `child` towards `radians`, about its **centre**.
539    pub fn new(
540        radians: f32,
541        duration: f32,
542        curve: Curve,
543        child: impl Widget<Msg> + 'static,
544    ) -> Self {
545        Self::about(
546            radians,
547            frus_core::Alignment::CENTER,
548            duration,
549            curve,
550            child,
551        )
552    }
553
554    /// The same, about a `pivot` — the point the child turns around.
555    pub fn about(
556        radians: f32,
557        pivot: frus_core::Alignment,
558        duration: f32,
559        curve: Curve,
560        child: impl Widget<Msg> + 'static,
561    ) -> Self {
562        Self {
563            inner: Box::new(
564                crate::Transform::rotate_from(radians, pivot)
565                    .animated(duration, curve)
566                    .child(child),
567            ),
568        }
569    }
570}
571
572impl<Msg> AnimatedRotation<Msg> {
573    /// A transparent wrapper: the box is the child's. In an impl with no bounds,
574    /// because the forwarding macro's own impl has none — a `restyle` declared beside
575    /// the constructors could not be reached from it.
576    fn restyle(&self, base: Style) -> Style {
577        base
578    }
579}
580
581crate::transparent::forward_transparent!(AnimatedRotation {
582    /// Every one of these is **forwarded**: an animated transform is not an identity,
583    /// not a place, not a theme and not a surface. It is its child, moving.
584    fn key(&self) -> Option<u64> {
585        self.inner.key()
586    }
587    fn positioned(&self) -> Option<crate::positioned::Positioning> {
588        self.inner.positioned()
589    }
590    fn theme_override(
591        &self,
592        inherited: &crate::theme::Theme,
593    ) -> Option<Box<crate::theme::Theme>> {
594        self.inner.theme_override(inherited)
595    }
596    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
597        self.inner.media_override(inherited)
598    }
599    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
600        self.inner.scaffold_override()
601    }
602    /// Forwarded: a wrapper around a form is still that form (milestone 512).
603    fn autofill_group(&self) -> bool {
604        self.inner.autofill_group()
605    }
606});
607
608/// **Moves its child's inset** rather than letting the space around it jump. The
609/// reference's `AnimatedPadding`.
610///
611/// Unlike the two above, this one is **layout**: the interpolated padding is injected
612/// while the tree is being measured, so everything beside and below the child follows it
613/// as it moves. That is the point of padding animating at all — a paint-time offset
614/// would slide the child over its neighbours instead of making room.
615///
616/// ```
617/// use frus_core::Curve;
618/// use frus_widgets::{AnimatedPadding, Text};
619///
620/// let selected = true;
621/// let _row: AnimatedPadding<()> = AnimatedPadding::new(
622///     if selected { 24.0 } else { 8.0 },
623///     0.2,
624///     Curve::ease_in_out(),
625///     Text::new("Inbox"),
626/// );
627/// ```
628pub struct AnimatedPadding<Msg = crate::callback::Callback> {
629    inner: Container<Msg>,
630}
631
632impl<Msg: Clone + 'static> AnimatedPadding<Msg> {
633    /// Insets `child` by `padding` on all four sides, moving there over `duration`.
634    pub fn new(
635        padding: f32,
636        duration: f32,
637        curve: Curve,
638        child: impl Widget<Msg> + 'static,
639    ) -> Self {
640        Self {
641            inner: Container::new()
642                .animated_padding(padding, duration, curve)
643                .child(child),
644        }
645    }
646}
647
648forward_to_container!(AnimatedPadding);
649
650/// **Moves a stack layer between two sets of pins** instead of letting it appear at the
651/// new ones. The reference's `AnimatedPositioned`.
652///
653/// A panel that slides in from an edge, a sheet that grows to fill the page, a card that
654/// travels to a corner: each is a layer of a [`crate::Stack`] whose distances from the
655/// stack's own edges change, and each of them jumped before this.
656///
657/// ```
658/// use frus_core::Curve;
659/// use frus_widgets::{AnimatedPositioned, Container, Stack};
660///
661/// let open = true;
662/// let _sheet: Stack<()> = Stack::new().layer(
663///     AnimatedPositioned::new(0.25, Curve::ease_out(), Container::new())
664///         .left(0.0)
665///         .right(0.0)
666///         .bottom(0.0)
667///         .height(if open { 320.0 } else { 0.0 }),
668/// );
669/// ```
670///
671/// **A pin that is unset does not animate.** `None` on an edge is not nought there: it is
672/// a layer not pinned on that side, sized by itself and placed by the stack's alignment,
673/// which is a different arrangement rather than a different number. So an edge arriving
674/// takes hold at once, an edge leaving lets go at once, and only an edge set at both ends
675/// travels. Where a movement is wanted, both ends must say where they are.
676///
677/// The six pins share **one timeline**, as a scale and a turn do: a layer changing two of
678/// its edges arrives on both at the same moment rather than on two clocks that happen to
679/// agree.
680///
681/// Unlike [`crate::Positioned`], which is a transparent wrapper, this is a **node of its
682/// own** with the child beneath it. An animated value belongs to a node, and a wrapper
683/// that fused with its child would put the layer's timeline and the child's on the same
684/// one — the rule the whole of this module is built on.
685pub struct AnimatedPositioned<Msg = crate::callback::Callback> {
686    children: Vec<Box<dyn Widget<Msg>>>,
687    spec: crate::positioned::Positioning,
688    duration: f32,
689    curve: Curve,
690}
691
692impl<Msg> AnimatedPositioned<Msg> {
693    /// A layer holding `child`, pinned nowhere yet, moving to each new set of pins over
694    /// `duration` seconds on `curve`.
695    pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
696        Self {
697            children: vec![Box::new(child)],
698            spec: crate::positioned::Positioning::default(),
699            duration,
700            curve,
701        }
702    }
703
704    /// Distance from the stack's left edge.
705    pub fn left(mut self, px: f32) -> Self {
706        self.spec.left = Some(px);
707        self
708    }
709
710    /// Distance from the stack's top edge.
711    pub fn top(mut self, px: f32) -> Self {
712        self.spec.top = Some(px);
713        self
714    }
715
716    /// Distance from the stack's right edge.
717    pub fn right(mut self, px: f32) -> Self {
718        self.spec.right = Some(px);
719        self
720    }
721
722    /// Distance from the stack's bottom edge.
723    pub fn bottom(mut self, px: f32) -> Self {
724        self.spec.bottom = Some(px);
725        self
726    }
727
728    /// An explicit width, used when only one horizontal edge is pinned.
729    pub fn width(mut self, px: f32) -> Self {
730        self.spec.width = Some(px);
731        self
732    }
733
734    /// An explicit height, used when only one vertical edge is pinned.
735    pub fn height(mut self, px: f32) -> Self {
736        self.spec.height = Some(px);
737        self
738    }
739}
740
741impl<Msg: Clone> Widget<Msg> for AnimatedPositioned<Msg> {
742    fn style(&self) -> Style {
743        // Nothing of its own: the stack forces a pinned layer into what its edges say,
744        // and an axis nobody pinned is left to the child underneath.
745        Style::default()
746    }
747
748    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
749        &self.children
750    }
751
752    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
753        // A pure layout widget: no decoration of its own.
754    }
755
756    fn on_click(&self) -> Option<Msg> {
757        None
758    }
759
760    /// Where it is **going**. The stack reads the runtime's interpolated pins instead
761    /// wherever there are any, which is what makes the difference between the two.
762    fn positioned(&self) -> Option<crate::positioned::Positioning> {
763        Some(self.spec)
764    }
765
766    fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
767        Some(self.spec)
768    }
769
770    fn anim_duration(&self) -> f32 {
771        self.duration
772    }
773
774    fn anim_curve(&self) -> Curve {
775        self.curve.clone()
776    }
777
778    fn debug_name(&self) -> &'static str {
779        "AnimatedPositioned"
780    }
781}
782
783/// **Hands a moving text style down to everything below it** rather than restyling the
784/// subtree at once. The reference's `AnimatedDefaultTextStyle`.
785///
786/// A heading that shrinks as a page scrolls, a label that goes from muted to emphatic when
787/// its row is selected, a caption thickening as it becomes the thing being edited: each is
788/// a run of words whose *type* changes, and each of them jumped before this.
789///
790/// ```
791/// use frus_core::{Curve, TextStyle};
792/// use frus_widgets::{AnimatedDefaultTextStyle, Text};
793///
794/// let selected = true;
795/// let _row: AnimatedDefaultTextStyle<()> = AnimatedDefaultTextStyle::new(
796///     0.2,
797///     Curve::ease_out(),
798///     TextStyle::NONE.size(if selected { 22.0 } else { 16.0 }),
799///     Text::new("Reminders"),
800/// );
801/// ```
802///
803/// It reaches the subtree through the **theme**, the way
804/// [`DefaultTextStyle::around`](crate::DefaultTextStyle::around) does: a
805/// [`Text`](crate::Text) resolves *what the caller said ?? what the subtree hands down ??
806/// what the framework ships*, so this styles the words a caller passed in already
807/// assembled and never has to be threaded through them. A field the caller set on a
808/// particular text still wins — an inherited style is the answer to a question, not an
809/// instruction.
810///
811/// **It is the only animated value in this framework that layout reads through the
812/// theme**, which is why it is the only one whose consumer is the walk's theme swap
813/// rather than the node. That swap is made four times over a frame, and all four go
814/// through one function so they cannot answer differently.
815///
816/// **Only the type moves.** Alignment, wrapping, overflow and the line count take effect
817/// the moment they change, exactly as the reference says of its own: they are
818/// arrangements rather than quantities, and there is nothing between wrapping and not.
819/// What travels, what holds and what swaps at the halfway point within the type itself is
820/// [`TextStyle::lerp`]'s business — in short, sizes travel, a field only one end states
821/// holds still, and faces swap in the middle.
822///
823/// Like [`AnimatedPositioned`], and unlike the transparent
824/// [`Themed`](crate::Themed) it is built on the same idea as, this is a **node of its
825/// own**. Two of these nested — an outer one moving the colour, an inner one the size,
826/// which is a thing the cascade positively invites — would otherwise fuse into one node
827/// and put two timelines on it.
828pub struct AnimatedDefaultTextStyle<Msg = crate::callback::Callback> {
829    children: Vec<Box<dyn Widget<Msg>>>,
830    style: TextStyle,
831    align: Option<TextAlign>,
832    soft_wrap: Option<bool>,
833    overflow: Option<TextOverflow>,
834    max_lines: Option<usize>,
835    duration: f32,
836    curve: Curve,
837}
838
839impl<Msg> AnimatedDefaultTextStyle<Msg> {
840    /// A subtree wearing `style`, moving to each new one over `duration` seconds on
841    /// `curve`.
842    pub fn new(
843        duration: f32,
844        curve: Curve,
845        style: TextStyle,
846        child: impl Widget<Msg> + 'static,
847    ) -> Self {
848        Self {
849            children: vec![Box::new(child)],
850            style,
851            align: None,
852            soft_wrap: None,
853            overflow: None,
854            max_lines: None,
855            duration,
856            curve,
857        }
858    }
859
860    /// Where the lines sit inside their box. **Not animated** — it takes effect at once.
861    pub fn align(mut self, align: TextAlign) -> Self {
862        self.align = Some(align);
863        self
864    }
865
866    /// Whether the text wraps at the width it is given. **Not animated.**
867    pub fn soft_wrap(mut self, wrap: bool) -> Self {
868        self.soft_wrap = Some(wrap);
869        self
870    }
871
872    /// What becomes of text that does not fit. **Not animated.**
873    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
874        self.overflow = Some(overflow);
875        self
876    }
877
878    /// At most this many lines. **Not animated.**
879    pub fn max_lines(mut self, lines: usize) -> Self {
880        self.max_lines = Some(lines);
881        self
882    }
883
884    /// The style this subtree hands down, as a whole.
885    fn handed_down(&self) -> crate::widgettheme::DefaultTextStyle {
886        crate::widgettheme::DefaultTextStyle {
887            style: self.style,
888            align: self.align,
889            soft_wrap: self.soft_wrap,
890            overflow: self.overflow,
891            max_lines: self.max_lines,
892        }
893    }
894}
895
896impl<Msg: Clone> Widget<Msg> for AnimatedDefaultTextStyle<Msg> {
897    fn style(&self) -> Style {
898        // Nothing of its own: it is a style handed down, not a box. The child's box is
899        // this node's box.
900        Style::default()
901    }
902
903    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
904        &self.children
905    }
906
907    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
908        // Nothing of its own: what it does, it does to the theme.
909    }
910
911    fn on_click(&self) -> Option<Msg> {
912        None
913    }
914
915    /// Where it is **going**, plus the four answers that are not going anywhere.
916    ///
917    /// The walk lays the runtime's interpolated style over this wherever there is one
918    /// (`crate::ui::scoped_theme`), which is what makes the difference between this and a
919    /// plain [`DefaultTextStyle::around`](crate::DefaultTextStyle::around). On the frame
920    /// this node mounts there is no interpolated style yet, and the target is the right
921    /// answer: an implicit animation adopts its target on mount rather than playing from
922    /// nowhere.
923    fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
924        let mut theme = inherited.clone();
925        theme.widgets.text = theme.widgets.text.merge(self.handed_down());
926        Some(Box::new(theme))
927    }
928
929    fn anim_text_style(&self) -> Option<TextStyle> {
930        Some(self.style)
931    }
932
933    fn anim_duration(&self) -> f32 {
934        self.duration
935    }
936
937    fn anim_curve(&self) -> Curve {
938        self.curve.clone()
939    }
940
941    fn debug_name(&self) -> &'static str {
942        "AnimatedDefaultTextStyle"
943    }
944}
945
946/// **Grows and shrinks its share of the parent** rather than taking the new share at
947/// once. The reference's `AnimatedFractionallySizedBox`, and a name over
948/// [`crate::FractionallySizedBox::animated`].
949///
950/// The quantity is a fraction and not a length, which is the reason to reach for this
951/// rather than an animated size: a bar filling half its row, a panel taking two thirds of
952/// a page, a sheet at a third of the height — none of them knows what those come to, and
953/// a resize of the window during the movement is answered by the layout rather than by
954/// a number that was right when it was written.
955///
956/// ```
957/// use frus_core::Curve;
958/// use frus_widgets::{AnimatedFractionallySizedBox, Container};
959///
960/// let full = true;
961/// let _bar: AnimatedFractionallySizedBox<()> =
962///     AnimatedFractionallySizedBox::new(0.3, Curve::ease_out(), Container::new())
963///         .width_factor(if full { 1.0 } else { 0.25 });
964/// ```
965///
966/// An axis whose factor is **unset** follows its content, and does not travel to or from
967/// a factor: that is a change of arrangement, not of value.
968pub struct AnimatedFractionallySizedBox<Msg = crate::callback::Callback> {
969    inner: crate::fractional::FractionallySizedBox<Msg>,
970}
971
972impl<Msg: Clone + 'static> AnimatedFractionallySizedBox<Msg> {
973    /// A fractional box holding `child`, with no factor yet, moving to each new one over
974    /// `duration` seconds on `curve`.
975    pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
976        Self {
977            inner: crate::fractional::FractionallySizedBox::new()
978                .animated(duration, curve)
979                .child(child),
980        }
981    }
982
983    /// Fraction `0.0..=1.0` of the parent's **width** (clamped to `>= 0`).
984    pub fn width_factor(mut self, factor: f32) -> Self {
985        self.inner = self.inner.width_factor(factor);
986        self
987    }
988
989    /// Fraction `0.0..=1.0` of the parent's **height** (clamped to `>= 0`).
990    pub fn height_factor(mut self, factor: f32) -> Self {
991        self.inner = self.inner.height_factor(factor);
992        self
993    }
994}
995
996// Written out rather than macro-forwarded: the inner widget is a `FractionallySizedBox`
997// and not a `Container`, and it overrides seven hooks — the four a layout widget has and
998// the three this one is for.
999impl<Msg: Clone + 'static> Widget<Msg> for AnimatedFractionallySizedBox<Msg> {
1000    fn style(&self) -> Style {
1001        Widget::style(&self.inner)
1002    }
1003    fn style_themed(&self, theme: &Theme) -> Style {
1004        Widget::style_themed(&self.inner, theme)
1005    }
1006    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
1007        Widget::children(&self.inner)
1008    }
1009    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
1010        Widget::paint(&self.inner, bounds, status, theme, scene)
1011    }
1012    fn on_click(&self) -> Option<Msg> {
1013        Widget::on_click(&self.inner)
1014    }
1015    fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
1016        Widget::anim_fractions(&self.inner)
1017    }
1018    fn anim_duration(&self) -> f32 {
1019        Widget::anim_duration(&self.inner)
1020    }
1021    fn anim_curve(&self) -> Curve {
1022        Widget::anim_curve(&self.inner)
1023    }
1024}
1025
1026#[cfg(test)]
1027mod implicit_tests {
1028    use super::*;
1029    use crate::interaction::WidgetId;
1030    use crate::{build_ui, Runtime, Text};
1031    use frus_core::{Insets, Primitive};
1032
1033    /// A tree with one animated scale on it, so the runtime has a target to drive.
1034    fn scaled(to: f32) -> AnimatedScale<()> {
1035        AnimatedScale::new(to, 0.10, Curve::Linear, Text::new("x"))
1036    }
1037
1038    /// **Mounted, mid-flight, at rest** — the three the issue asks each of these to pin.
1039    ///
1040    /// The first is the one worth stating: a widget that appears already scaled **adopts**
1041    /// its target rather than growing into it from nothing. Without that rule every
1042    /// implicit animation in the framework would play once on the frame it was born, and
1043    /// a page would breathe when it opened.
1044    #[test]
1045    fn a_scale_mounts_settled_moves_and_arrives() {
1046        let mut rt = Runtime::default();
1047        assert!(
1048            !rt.advance_transforms(&scaled(2.0), 1.0),
1049            "a mount is not a transition"
1050        );
1051        let id = WidgetId::ROOT;
1052        assert_eq!(
1053            rt.anim_transform(id).map(|t| t.scale_x),
1054            Some(2.0),
1055            "and it adopts the target whole"
1056        );
1057
1058        // Halfway along a linear curve of 0.10 s: halfway between 2 and 1.
1059        assert!(rt.advance_transforms(&scaled(1.0), 0.05));
1060        let mid = rt.anim_transform(id).expect("in flight");
1061        assert!(
1062            (mid.scale_x - 1.5).abs() < 1e-3 && (mid.scale_y - 1.5).abs() < 1e-3,
1063            "halfway between the two: {mid:?}"
1064        );
1065        assert_eq!(mid.rotation, 0.0, "and it turned nothing on the way");
1066
1067        rt.advance_transforms(&scaled(1.0), 1.0);
1068        assert_eq!(rt.anim_transform(id).map(|t| t.scale_x), Some(1.0));
1069        assert!(
1070            !rt.advance_transforms(&scaled(1.0), 0.05),
1071            "and stops asking for frames once it is there"
1072        );
1073    }
1074
1075    /// The same three for a rotation, and that the two do not leak into one another: a
1076    /// widget that only turns keeps its scale at **one**, not at nought — which is the
1077    /// difference between a turning child and a child that vanished.
1078    #[test]
1079    fn a_rotation_mounts_settled_moves_and_arrives() {
1080        let turned = |to: f32| -> AnimatedRotation<()> {
1081            AnimatedRotation::new(to, 0.10, Curve::Linear, Text::new("x"))
1082        };
1083        let mut rt = Runtime::default();
1084        assert!(!rt.advance_transforms(&turned(0.0), 1.0));
1085        let id = WidgetId::ROOT;
1086        let mounted = rt.anim_transform(id).expect("mounted");
1087        assert_eq!((mounted.scale_x, mounted.scale_y), (1.0, 1.0), "identity");
1088
1089        assert!(rt.advance_transforms(&turned(1.0), 0.05));
1090        let mid = rt.anim_transform(id).expect("in flight");
1091        assert!((mid.rotation - 0.5).abs() < 1e-3, "halfway: {mid:?}");
1092        assert_eq!(
1093            (mid.scale_x, mid.scale_y),
1094            (1.0, 1.0),
1095            "and the scale stayed the identity, not nought"
1096        );
1097
1098        rt.advance_transforms(&turned(1.0), 1.0);
1099        assert_eq!(rt.anim_transform(id).map(|t| t.rotation), Some(1.0));
1100    }
1101
1102    /// And the padding, which is the one that is **layout** rather than paint: the
1103    /// interpolated value is injected while the tree is measured.
1104    #[test]
1105    fn a_padding_mounts_settled_moves_and_arrives() {
1106        let padded = |to: f32| -> AnimatedPadding<()> {
1107            AnimatedPadding::new(to, 0.10, Curve::Linear, Text::new("x"))
1108        };
1109        let mut rt = Runtime::default();
1110        assert!(!rt.advance_paddings(&padded(0.0), 1.0));
1111        let id = WidgetId::ROOT;
1112        assert_eq!(rt.anim_padding(id), Some(Insets::uniform(0.0)));
1113
1114        assert!(rt.advance_paddings(&padded(20.0), 0.05));
1115        let mid = rt.anim_padding(id).expect("in flight");
1116        assert!((mid.left - 10.0).abs() < 1e-3, "halfway: {mid:?}");
1117
1118        rt.advance_paddings(&padded(20.0), 1.0);
1119        assert_eq!(rt.anim_padding(id), Some(Insets::uniform(20.0)));
1120    }
1121
1122    /// **And the paint reads it**, which is a separate question from whether the runtime
1123    /// computes it. A tween the walk never asks for is a number that moves correctly and
1124    /// changes nothing on the screen, and this framework has shipped that bug before.
1125    #[test]
1126    fn the_paint_uses_the_tweened_transform_and_not_the_target() {
1127        let matrix_of = |rt: &Runtime, widget: &AnimatedScale<()>| {
1128            let ui = build_ui(widget, Size::new(100.0, 100.0), rt, &Theme::default());
1129            ui.scene()
1130                .primitives()
1131                .iter()
1132                .find_map(|p| match p {
1133                    Primitive::Layer {
1134                        transform: Some(t), ..
1135                    } => Some(t.affine),
1136                    _ => None,
1137                })
1138                .map(|m| m.m[0])
1139        };
1140        let mut rt = Runtime::default();
1141        rt.advance_transforms(&scaled(3.0), 1.0);
1142        // Now heading back to 1, half way along.
1143        rt.advance_transforms(&scaled(1.0), 0.05);
1144        let painted = matrix_of(&rt, &scaled(1.0)).expect("a transformed layer");
1145        assert!(
1146            (painted - 2.0).abs() < 1e-2,
1147            "the paint took the tweened scale (2), not the target (1): {painted}"
1148        );
1149
1150        // A runtime that has never heard of it draws the target, which is what an
1151        // isolated frame — a test, a golden — has to show.
1152        let fresh = Runtime::default();
1153        let painted = matrix_of(&fresh, &scaled(3.0)).expect("a transformed layer");
1154        assert!((painted - 3.0).abs() < 1e-2, "the target: {painted}");
1155    }
1156
1157    /// A tree with one animated anchor on it, so the runtime has a pair to drive.
1158    fn anchored(to: frus_core::Alignment) -> AnimatedAlign<()> {
1159        AnimatedAlign::new(to, 0.10, Curve::Linear, Text::new("x"))
1160    }
1161
1162    /// **Mounted, mid-flight, at rest**, for the anchor.
1163    #[test]
1164    fn an_anchor_mounts_settled_moves_and_arrives() {
1165        let mut rt = Runtime::default();
1166        assert!(
1167            !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_LEFT), 1.0),
1168            "a mount is not a transition"
1169        );
1170        let id = WidgetId::ROOT;
1171        assert_eq!(
1172            rt.anim_offset(id),
1173            Some((-1.0, 0.0)),
1174            "and it adopts the anchor whole"
1175        );
1176
1177        // Halfway along a linear curve of 0.10 s: halfway from the left edge to the right.
1178        assert!(rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05));
1179        let mid = rt.anim_offset(id).expect("in flight");
1180        assert!(mid.0.abs() < 1e-3, "at the centre on the way past: {mid:?}");
1181        assert_eq!(mid.1, 0.0, "and it did not drift vertically");
1182
1183        rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 1.0);
1184        assert_eq!(rt.anim_offset(id), Some((1.0, 0.0)));
1185        assert!(
1186            !rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05),
1187            "and stops asking for frames once it is there"
1188        );
1189    }
1190
1191    /// And for the slide, whose pair is a multiple of the child's own box rather than a
1192    /// share of the free space around it.
1193    #[test]
1194    fn a_slide_mounts_settled_moves_and_arrives() {
1195        let slid = |fx: f32| -> AnimatedSlide<()> {
1196            AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, Text::new("x"))
1197        };
1198        let mut rt = Runtime::default();
1199        assert!(!rt.advance_offsets(&slid(-1.0), 1.0));
1200        let id = WidgetId::ROOT;
1201        assert_eq!(rt.anim_offset(id), Some((-1.0, 0.0)));
1202
1203        assert!(rt.advance_offsets(&slid(0.0), 0.05));
1204        let mid = rt.anim_offset(id).expect("in flight");
1205        assert!((mid.0 + 0.5).abs() < 1e-3, "halfway back: {mid:?}");
1206
1207        rt.advance_offsets(&slid(0.0), 1.0);
1208        assert_eq!(rt.anim_offset(id), Some((0.0, 0.0)));
1209    }
1210
1211    /// **And the paint reads it** — the question the value tests above cannot ask. A
1212    /// tween the walk never consults moves correctly and changes nothing on the screen,
1213    /// and this framework has shipped that bug before.
1214    ///
1215    /// Both rules are checked, because they are two separate lines in the walk: an
1216    /// anchor's box slides through the free space, and a slide's box moves by its own
1217    /// width.
1218    #[test]
1219    fn the_paint_uses_the_tweened_offset_and_not_the_target() {
1220        let mark = |root: &dyn Widget<()>, rt: &Runtime| {
1221            let ui = build_ui(root, Size::new(100.0, 40.0), rt, &Theme::default());
1222            ui.scene()
1223                .primitives()
1224                .iter()
1225                .find_map(|p| match p {
1226                    Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1227                    _ => None,
1228                })
1229                .expect("the mark")
1230        };
1231        let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
1232        let mark_widget = || {
1233            crate::Container::<()>::new()
1234                .width(20.0)
1235                .height(20.0)
1236                .color(red)
1237        };
1238
1239        // An anchor crossing a 100 px box: at the halfway point the 20 px mark sits at 40,
1240        // the middle of the 80 px of free space — not at 0 and not at 80.
1241        let aligned = |to: frus_core::Alignment| {
1242            crate::Container::<()>::new()
1243                .width(100.0)
1244                .height(40.0)
1245                .animated_alignment(to, 0.10, Curve::Linear)
1246                .child(mark_widget())
1247        };
1248        let mut rt = Runtime::default();
1249        rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_LEFT), 1.0);
1250        rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_RIGHT), 0.05);
1251        let x = mark(&aligned(frus_core::Alignment::CENTER_RIGHT), &rt);
1252        assert!(
1253            (x - 40.0).abs() < 0.5,
1254            "the anchor's tween reached the paint: {x}"
1255        );
1256
1257        // A slide of one whole width, half done: the 20 px mark has moved 10.
1258        let slid = |fx: f32| AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, mark_widget());
1259        let mut rt = Runtime::default();
1260        rt.advance_offsets(&slid(0.0), 1.0);
1261        rt.advance_offsets(&slid(1.0), 0.05);
1262        let x = mark(&slid(1.0), &rt);
1263        assert!(
1264            (x - 10.0).abs() < 0.5,
1265            "the slide's tween reached the paint: {x}"
1266        );
1267    }
1268
1269    /// The anchor is interpolated in **its own** coordinates, so a directional one stays
1270    /// directional the whole way: in a right-to-left script the mirrored movement is the
1271    /// mirror of the movement, rather than a slide between two already-mirrored ends.
1272    #[test]
1273    fn a_directional_anchor_mirrors_as_a_whole() {
1274        let start = frus_core::AlignmentDirectional::CENTER_START;
1275        let end = frus_core::AlignmentDirectional::CENTER_END;
1276        let aligned = |to: frus_core::AlignmentDirectional| {
1277            crate::Container::<()>::new()
1278                .width(100.0)
1279                .height(40.0)
1280                .animated_alignment(to, 0.10, Curve::Linear)
1281                .child(
1282                    crate::Container::<()>::new()
1283                        .width(20.0)
1284                        .height(20.0)
1285                        .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1286                )
1287        };
1288        let mut rt = Runtime::default();
1289        rt.advance_offsets(&aligned(start), 1.0);
1290        // A quarter of the way from the start edge towards the end one.
1291        rt.advance_offsets(&aligned(end), 0.025);
1292
1293        let x_in = |rtl: bool| {
1294            let theme = match rtl {
1295                true => Theme::default().rtl(),
1296                false => Theme::default(),
1297            };
1298            let ui = build_ui(&aligned(end), Size::new(100.0, 40.0), &rt, &theme);
1299            ui.scene()
1300                .primitives()
1301                .iter()
1302                .find_map(|p| match p {
1303                    Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1304                    _ => None,
1305                })
1306                .expect("the mark")
1307        };
1308        let ltr = x_in(false);
1309        let rtl = x_in(true);
1310        assert!(
1311            (ltr - 20.0).abs() < 0.5,
1312            "a quarter across, from the left: {ltr}"
1313        );
1314        assert!(
1315            (rtl - 60.0).abs() < 0.5,
1316            "and the same quarter from the right: {rtl}"
1317        );
1318    }
1319
1320    /// A stack with one animated layer on it, pinned at `left` and 20 px wide.
1321    fn pinned_at(left: f32) -> crate::Stack<()> {
1322        crate::Stack::new().layer(
1323            AnimatedPositioned::new(
1324                0.10,
1325                Curve::Linear,
1326                crate::Container::<()>::new()
1327                    .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
1328                    .child(Text::new("x")),
1329            )
1330            .left(left)
1331            .top(0.0)
1332            .width(20.0)
1333            .height(20.0),
1334        )
1335    }
1336
1337    /// **Mounted, mid-flight, at rest** for a layer's pins.
1338    ///
1339    /// A layer that appears already pinned somewhere **adopts** its pins rather than
1340    /// travelling to them from the last place a stack would have put it — the same mount
1341    /// rule as every other implicit animation here, and the reason a page does not
1342    /// rearrange itself when it opens.
1343    #[test]
1344    fn a_layer_mounts_settled_moves_and_arrives() {
1345        let mut rt = Runtime::default();
1346        assert!(
1347            !rt.advance_pins(&pinned_at(0.0), 1.0),
1348            "a mount is not a transition"
1349        );
1350        // Child 0 of the stack, which is the layer.
1351        let id = WidgetId::ROOT.child(0);
1352        assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(0.0));
1353
1354        // Halfway along a linear curve of 0.10 s: halfway between 0 and 80.
1355        assert!(rt.advance_pins(&pinned_at(80.0), 0.05));
1356        let mid = rt.anim_pins(id).expect("in flight");
1357        assert!(
1358            mid.left.is_some_and(|l| (l - 40.0).abs() < 1e-3),
1359            "halfway between the two: {mid:?}"
1360        );
1361        assert_eq!(
1362            mid.width,
1363            Some(20.0),
1364            "and the extents it never asked to move stayed"
1365        );
1366
1367        rt.advance_pins(&pinned_at(80.0), 1.0);
1368        assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(80.0));
1369        assert!(
1370            !rt.advance_pins(&pinned_at(80.0), 0.05),
1371            "and stops asking for frames once it is there"
1372        );
1373    }
1374
1375    /// **A pin that is unset does not travel**, in either direction.
1376    ///
1377    /// `None` on an edge is not nought there — it is a layer not pinned on that side at
1378    /// all, sized by itself and placed by the stack's alignment. Sliding between the two
1379    /// would be interpolating between two arrangements, and the number it passed through
1380    /// on the way would mean neither of them.
1381    #[test]
1382    fn a_pin_that_is_unset_takes_hold_and_lets_go_at_once() {
1383        let layer = |left: Option<f32>, right: Option<f32>| {
1384            let mut l: AnimatedPositioned<()> =
1385                AnimatedPositioned::new(0.10, Curve::Linear, Text::new("x")).top(0.0);
1386            if let Some(px) = left {
1387                l = l.left(px);
1388            }
1389            if let Some(px) = right {
1390                l = l.right(px);
1391            }
1392            crate::Stack::<()>::new().layer(l)
1393        };
1394        let id = WidgetId::ROOT.child(0);
1395        let mut rt = Runtime::default();
1396        rt.advance_pins(&layer(Some(0.0), None), 1.0);
1397
1398        // Swap which edge is pinned, and step a fifth of the way in.
1399        rt.advance_pins(&layer(None, Some(10.0)), 0.02);
1400        let mid = rt.anim_pins(id).expect("present");
1401        assert_eq!(
1402            mid.left, None,
1403            "the edge that let go let go at once: {mid:?}"
1404        );
1405        assert_eq!(
1406            mid.right,
1407            Some(10.0),
1408            "and the edge that took hold took hold at once: {mid:?}"
1409        );
1410    }
1411
1412    /// **And the stack lays the layer out where the tween says.** The value tests above
1413    /// cannot ask this: pins that move correctly and are read from the widget instead of
1414    /// the runtime give a layer that jumps, and every one of them still passes.
1415    #[test]
1416    fn the_stack_places_the_layer_by_the_tween_and_not_the_target() {
1417        let mut rt = Runtime::default();
1418        rt.advance_pins(&pinned_at(0.0), 1.0);
1419        rt.advance_pins(&pinned_at(80.0), 0.05);
1420        let ui = build_ui(
1421            &pinned_at(80.0),
1422            Size::new(100.0, 40.0),
1423            &rt,
1424            &Theme::default(),
1425        );
1426        let x = ui
1427            .scene()
1428            .primitives()
1429            .iter()
1430            .find_map(|p| match p {
1431                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1432                _ => None,
1433            })
1434            .expect("the layer");
1435        assert!(
1436            (x - 40.0).abs() < 0.5,
1437            "halfway across, not at either end: {x}"
1438        );
1439    }
1440
1441    /// A box taking half its parent, then all of it.
1442    fn share(width_factor: f32) -> crate::Flex<()> {
1443        crate::Flex::column().width(100.0).height(40.0).child(
1444            AnimatedFractionallySizedBox::new(
1445                0.10,
1446                Curve::Linear,
1447                crate::Container::<()>::new()
1448                    .flex(1.0)
1449                    .height(20.0)
1450                    .color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1451            )
1452            .width_factor(width_factor),
1453        )
1454    }
1455
1456    /// **Mounted, mid-flight, at rest** for a share of the parent.
1457    #[test]
1458    fn a_share_of_the_parent_mounts_settled_moves_and_arrives() {
1459        let mut rt = Runtime::default();
1460        assert!(
1461            !rt.advance_fractions(&share(0.25), 1.0),
1462            "a mount is not a transition"
1463        );
1464        let id = WidgetId::ROOT.child(0);
1465        assert_eq!(rt.anim_fractions(id), Some((Some(0.25), None)));
1466
1467        assert!(rt.advance_fractions(&share(0.75), 0.05));
1468        let (width, height) = rt.anim_fractions(id).expect("in flight");
1469        assert!(
1470            width.is_some_and(|w| (w - 0.5).abs() < 1e-3),
1471            "halfway between a quarter and three quarters: {width:?}"
1472        );
1473        assert_eq!(
1474            height, None,
1475            "and the axis nobody set still follows its content"
1476        );
1477
1478        rt.advance_fractions(&share(0.75), 1.0);
1479        assert_eq!(rt.anim_fractions(id), Some((Some(0.75), None)));
1480        assert!(
1481            !rt.advance_fractions(&share(0.75), 0.05),
1482            "and stops asking for frames once it is there"
1483        );
1484    }
1485
1486    /// **And the layout reads it.** A fraction is consumed while the tree is measured, so
1487    /// the check is the width the child actually came out — half of a hundred, halfway
1488    /// between a quarter of it and three quarters.
1489    #[test]
1490    fn the_layout_uses_the_tweened_fraction_and_not_the_target() {
1491        let mut rt = Runtime::default();
1492        rt.advance_fractions(&share(0.25), 1.0);
1493        rt.advance_fractions(&share(0.75), 0.05);
1494        let ui = build_ui(&share(0.75), Size::new(100.0, 40.0), &rt, &Theme::default());
1495        let width = ui
1496            .scene()
1497            .primitives()
1498            .iter()
1499            .find_map(|p| match p {
1500                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.width),
1501                _ => None,
1502            })
1503            .expect("the child's background");
1504        assert!(
1505            (width - 50.0).abs() < 0.5,
1506            "half the parent, on the way from a quarter to three quarters: {width}"
1507        );
1508    }
1509
1510    // --- The inherited text style (milestone 498) ---
1511
1512    /// A subtree wearing a style that moves, over a linear tenth of a second.
1513    fn styled(size: f32) -> AnimatedDefaultTextStyle<()> {
1514        AnimatedDefaultTextStyle::new(
1515            0.10,
1516            Curve::Linear,
1517            TextStyle::NONE.size(size),
1518            Text::new("Ag"),
1519        )
1520    }
1521
1522    /// **Mounted, mid-flight, at rest** — the three the issue asks each of these to pin.
1523    #[test]
1524    fn a_default_text_style_mounts_settled_moves_and_arrives() {
1525        let mut rt = Runtime::default();
1526        assert!(
1527            !rt.advance_text_styles(&styled(24.0), 1.0),
1528            "a mount is not a transition"
1529        );
1530        let id = WidgetId::ROOT;
1531        assert_eq!(
1532            rt.anim_text_style(id).and_then(|s| s.size),
1533            Some(24.0),
1534            "and it adopts the target whole"
1535        );
1536
1537        assert!(rt.advance_text_styles(&styled(12.0), 0.05));
1538        let mid = rt.anim_text_style(id).expect("in flight").size.unwrap();
1539        assert!((mid - 18.0).abs() < 1e-3, "halfway between the two: {mid}");
1540
1541        rt.advance_text_styles(&styled(12.0), 1.0);
1542        assert_eq!(rt.anim_text_style(id).and_then(|s| s.size), Some(12.0));
1543        assert!(
1544            !rt.advance_text_styles(&styled(12.0), 0.05),
1545            "and stops asking for frames once it is there"
1546        );
1547    }
1548
1549    /// **And the moving style reaches the words**, which is a different question from
1550    /// whether the number moves — milestone 477's fourth test, and the one that has
1551    /// caught something every time it has been asked.
1552    ///
1553    /// This family is the one where it matters most: the value is consumed by the walk's
1554    /// **theme swap** rather than by the node, so a correct tween and a correct target can
1555    /// sit either side of a subtree that never reads either. Take the merge out of
1556    /// `crate::ui::scoped_theme` and the test above still passes; this one paints the
1557    /// target from the first frame.
1558    #[test]
1559    fn the_moving_style_reaches_the_words() {
1560        fn painted_size(rt: &Runtime, tree: &AnimatedDefaultTextStyle<()>) -> f32 {
1561            fn walk(primitives: &[Primitive], out: &mut Vec<f32>) {
1562                for p in primitives {
1563                    match p {
1564                        Primitive::Text { size, .. } => out.push(*size),
1565                        Primitive::Layer { primitives, .. } => walk(primitives, out),
1566                        _ => {}
1567                    }
1568                }
1569            }
1570            let ui = build_ui(tree, Size::new(400.0, 200.0), rt, &Theme::default());
1571            let mut sizes = Vec::new();
1572            walk(ui.scene().primitives(), &mut sizes);
1573            assert_eq!(sizes.len(), 1, "one run of words: {sizes:?}");
1574            sizes[0]
1575        }
1576
1577        let mut rt = Runtime::default();
1578        rt.advance_text_styles(&styled(24.0), 1.0);
1579        assert_eq!(
1580            painted_size(&rt, &styled(24.0)),
1581            24.0,
1582            "mounted at its target"
1583        );
1584
1585        rt.advance_text_styles(&styled(12.0), 0.05);
1586        let mid = painted_size(&rt, &styled(12.0));
1587        assert!(
1588            (mid - 18.0).abs() < 1e-3,
1589            "the words are drawn at the style in flight, not at the one it is heading              for: {mid}"
1590        );
1591    }
1592
1593    /// **The four unanimated answers take effect at once**, as the reference says of its
1594    /// own: there is nothing between wrapping and not, so they are not quantities and do
1595    /// not travel. They still have to *arrive*, which is what this checks — they reach
1596    /// the subtree through the same theme the style does.
1597    #[test]
1598    fn the_answers_that_are_not_quantities_take_effect_at_once() {
1599        let tree = AnimatedDefaultTextStyle::<()>::new(
1600            0.10,
1601            Curve::Linear,
1602            TextStyle::NONE.size(16.0),
1603            Text::new("Ag"),
1604        )
1605        .align(TextAlign::Center)
1606        .max_lines(2);
1607        let handed = Widget::<()>::theme_override(&tree, &Theme::default()).expect("a theme");
1608        assert_eq!(handed.widgets.text.align, Some(TextAlign::Center));
1609        assert_eq!(handed.widgets.text.max_lines, Some(2));
1610    }
1611
1612    /// **Two nested ones are two nodes**, which is the reason this is not a transparent
1613    /// wrapper. An outer one moving the colour and an inner one the size is a thing the
1614    /// cascade positively invites; fused into one node they would be two timelines on one
1615    /// identity, and the second would overwrite the first every frame.
1616    #[test]
1617    fn two_nested_styles_are_two_timelines() {
1618        let nested = AnimatedDefaultTextStyle::<()>::new(
1619            0.10,
1620            Curve::Linear,
1621            TextStyle::NONE.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
1622            AnimatedDefaultTextStyle::new(
1623                0.10,
1624                Curve::Linear,
1625                TextStyle::NONE.size(20.0),
1626                Text::new("Ag"),
1627            ),
1628        );
1629        let mut rt = Runtime::default();
1630        rt.advance_text_styles(&nested, 1.0);
1631        let outer = rt.anim_text_style(WidgetId::ROOT).expect("the outer one");
1632        let inner_id = crate::ui::child_id(WidgetId::ROOT, 0, nested.children()[0].as_ref());
1633        let inner = rt.anim_text_style(inner_id).expect("the inner one");
1634        assert_ne!(inner_id, WidgetId::ROOT, "two nodes, not one");
1635        assert_eq!(outer.size, None, "the outer one says nothing about size");
1636        assert_eq!(inner.size, Some(20.0), "and the inner one does");
1637        assert!(outer.color.is_some() && inner.color.is_none());
1638    }
1639}