Skip to main content

frus_widgets/
transitions.rs

1//! The **explicit** transitions: a widget driven by a value the *application* owns.
2//!
3//! The other family, in [`crate::animated`], is implicit: you change a target and the
4//! widget catches up on the framework's clock. These take the number itself — a progress
5//! the caller stepped, a spring's current value, a fraction of a gesture — so their timing
6//! is the application's, and two of them handed the same number move together.
7//!
8//! ## What they are, here
9//!
10//! In the reference this is a family of fifteen classes, because there a widget that
11//! animates has to *listen*: an `Animation<double>` is an object, and each transition
12//! subscribes to one and rebuilds when it ticks. There is no listening here and no retained
13//! tree — the view is rebuilt every frame and an animation is a number — so each of these
14//! is a **thin wrapper over a widget that already takes a value**, and that is not a
15//! shortcut but the shape of the thing.
16//!
17//! What the names buy is that the rule is written once. `SlideTransition::from_edge` is a
18//! sign and a subtraction that every application would otherwise get wrong once; the
19//! progress conventions (`0` = not started, `1` = arrived) are stated in one place; and a
20//! reader coming from the reference's vocabulary finds them under the names they know.
21//!
22//! ## The five here, and the ten that are not
23//!
24//! These three are what a **transition between two states of a screen** is made of, in the
25//! reference too: a fade, a slide, and a scale. The rest of that file maps onto this
26//! framework rather than being missing from it:
27//!
28//! - `RotationTransition` is [`crate::Transform::rotate`] on a value — or
29//!   [`crate::AnimatedRotation`] when the timing is not the caller's.
30//! - `PositionedTransition` and `RelativePositionedTransition` are [`crate::Positioned`]
31//!   with interpolated pins; `AlignTransition` is `Container::alignment`;
32//!   `MatrixTransition` is [`crate::Transform`]'s composed matrix.
33//! - `AnimatedBuilder`, `ListenableBuilder` and `AnimatedWidget` are **nothing** here, and
34//!   that is the whole difference between the two frameworks: they exist to rebuild a
35//!   subtree when a value changes, and here the subtree is rebuilt every frame anyway.
36//! - `SliverFadeTransition` waits on slivers, which this framework does not have.
37//! - [`SizeTransition`] is here since milestone 596. It is not a wrapper: it is a box a
38//!   fraction of its child's size, which needs the child measured, and that waited on #52.
39//! - [`DecoratedBoxTransition`] and [`DefaultTextStyleTransition`] are here since
40//!   milestone 501. They are not wrappers over one value but interpolations of a whole
41//!   decoration and a whole text style, each part by its own rule — see
42//!   `BoxDecoration::lerp` and `TextStyle::lerp`, which is where the real work is.
43
44use frus_core::{Alignment, BoxDecoration, Curve, Rect, Scene, TextAlign, TextOverflow, TextStyle};
45use frus_layout::Style;
46
47use crate::interaction::Status;
48use crate::theme::Theme;
49use crate::widget::Widget;
50
51/// **Fades** its child by a value the caller owns: `0.0` invisible, `1.0` solid.
52///
53/// The subtree is composited as one group, so overlapping children do not darken where
54/// they overlap — the difference between fading a picture and fading each of its parts.
55///
56/// ```
57/// use frus_widgets::{FadeTransition, Text};
58///
59/// let entering = 0.4_f32;
60/// let _fading: FadeTransition<()> = FadeTransition::new(entering, Text::new("Details"));
61/// ```
62///
63/// It is [`crate::Opacity`] with the name the reference uses, which is worth having: a
64/// reader looking for the explicit half of the animation library looks for this word.
65pub struct FadeTransition<Msg = crate::callback::Callback> {
66    inner: Box<dyn Widget<Msg>>,
67}
68
69impl<Msg: Clone + 'static> FadeTransition<Msg> {
70    /// Fades `child` to `opacity`, clamped to `0..=1`.
71    pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
72        Self {
73            inner: Box::new(crate::Opacity::new(opacity.clamp(0.0, 1.0), child)),
74        }
75    }
76}
77
78impl<Msg> FadeTransition<Msg> {
79    /// A transparent wrapper: the box is the child's.
80    fn restyle(&self, base: Style) -> Style {
81        base
82    }
83}
84
85crate::transparent::forward_transparent!(FadeTransition {
86    /// Forwarded, all of them: a fade is not an identity, not a place, not a theme and
87    /// not a surface. It is its child, disappearing.
88    fn key(&self) -> Option<u64> {
89        self.inner.key()
90    }
91    fn positioned(&self) -> Option<crate::positioned::Positioning> {
92        self.inner.positioned()
93    }
94    fn theme_override(
95        &self,
96        inherited: &crate::theme::Theme,
97    ) -> Option<Box<crate::theme::Theme>> {
98        self.inner.theme_override(inherited)
99    }
100    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
101        self.inner.media_override(inherited)
102    }
103    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
104        self.inner.scaffold_override()
105    }
106    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
107        self.inner.scroll_behavior_override()
108    }
109    /// Forwarded: a wrapper around a form is still that form (milestone 512).
110    fn autofill_group(&self) -> bool {
111        self.inner.autofill_group()
112    }
113    /// Forwarded: the area's bar is the area's, whatever wraps it.
114    fn area_toolbar(
115        &self,
116        context: crate::ToolbarContext,
117    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
118        self.inner.area_toolbar(context)
119    }
120});
121
122/// The edge a [`SlideTransition::from_edge`] comes in from.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum SlideFrom {
125    /// From beyond the left edge, moving right.
126    Left,
127    /// From beyond the right edge, moving left.
128    Right,
129    /// From above, moving down.
130    Top,
131    /// From below, moving up.
132    Bottom,
133}
134
135/// **Slides** its child by a value the caller owns, stated as a **fraction of the child's
136/// own size**: `(1.0, 0.0)` is one whole width to the right.
137///
138/// A fraction rather than pixels because the width is the layout's to decide, and the
139/// number that puts a panel exactly off its own edge is `1` whatever that width turns out
140/// to be — which is what makes the same call work for a sheet, a drawer and a row.
141///
142/// Layout is untouched: the box stays where it was put and the neighbours do not move.
143///
144/// ```
145/// use frus_widgets::{SlideFrom, SlideTransition, Text};
146///
147/// let entering = 0.25_f32;
148/// // A quarter of the way in from the left: three quarters of a width still outside.
149/// let _panel: SlideTransition<()> =
150///     SlideTransition::from_edge(SlideFrom::Left, entering, Text::new("Filters"));
151/// ```
152///
153/// [`SlideTransition::from_edge`] is the reason this has a name at all. *Coming in from
154/// the left* is a negative offset that shrinks to nought as the progress grows, and every
155/// application that writes it by hand gets the sign wrong once.
156pub struct SlideTransition<Msg = crate::callback::Callback> {
157    inner: Box<dyn Widget<Msg>>,
158}
159
160impl<Msg: Clone + 'static> SlideTransition<Msg> {
161    /// Slides `child` by `fx` widths across and `fy` heights down.
162    pub fn new(fx: f32, fy: f32, child: impl Widget<Msg> + 'static) -> Self {
163        Self {
164            inner: Box::new(crate::FractionalTranslation::new(fx, fy).child(child)),
165        }
166    }
167
168    /// Slides `child` in from an edge: `progress` of `0` leaves it a whole size outside,
169    /// `1` puts it exactly where the layout laid it out. Values between the two are the
170    /// journey, and values outside them overshoot — which is what a spring does at the end
171    /// of its travel, and is deliberately not clamped away.
172    pub fn from_edge(edge: SlideFrom, progress: f32, child: impl Widget<Msg> + 'static) -> Self {
173        let away = 1.0 - progress;
174        let (fx, fy) = match edge {
175            SlideFrom::Left => (-away, 0.0),
176            SlideFrom::Right => (away, 0.0),
177            SlideFrom::Top => (0.0, -away),
178            SlideFrom::Bottom => (0.0, away),
179        };
180        Self::new(fx, fy, child)
181    }
182}
183
184impl<Msg> SlideTransition<Msg> {
185    /// A transparent wrapper: the box is the child's.
186    fn restyle(&self, base: Style) -> Style {
187        base
188    }
189}
190
191crate::transparent::forward_transparent!(SlideTransition {
192    /// Forwarded, all of them: a slide is not an identity, not a place, not a theme and
193    /// not a surface. It is its child, moving.
194    fn key(&self) -> Option<u64> {
195        self.inner.key()
196    }
197    fn positioned(&self) -> Option<crate::positioned::Positioning> {
198        self.inner.positioned()
199    }
200    fn theme_override(
201        &self,
202        inherited: &crate::theme::Theme,
203    ) -> Option<Box<crate::theme::Theme>> {
204        self.inner.theme_override(inherited)
205    }
206    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
207        self.inner.media_override(inherited)
208    }
209    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
210        self.inner.scaffold_override()
211    }
212    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
213        self.inner.scroll_behavior_override()
214    }
215    /// Forwarded: a wrapper around a form is still that form (milestone 512).
216    fn autofill_group(&self) -> bool {
217        self.inner.autofill_group()
218    }
219    /// Forwarded: the area's bar is the area's, whatever wraps it.
220    fn area_toolbar(
221        &self,
222        context: crate::ToolbarContext,
223    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
224        self.inner.area_toolbar(context)
225    }
226});
227
228/// **Scales** its child by a value the caller owns, about its centre or a pivot of the
229/// caller's choosing. `1.0` is neutral.
230///
231/// Paint only: the box does not change, so nothing around it moves and the child may spill
232/// past its own edges. That is what a control popping under a press wants, and what a
233/// dialog growing into place wants.
234///
235/// ```
236/// use frus_widgets::{ScaleTransition, Text};
237///
238/// let opening = 0.9_f32;
239/// let _dialog: ScaleTransition<()> = ScaleTransition::new(opening, Text::new("Delete?"));
240/// ```
241///
242/// The pivot is a choice of origin rather than a quantity, so it is an argument here and
243/// never a thing to interpolate — the same rule [`crate::AnimatedScale`] states.
244pub struct ScaleTransition<Msg = crate::callback::Callback> {
245    inner: Box<dyn Widget<Msg>>,
246}
247
248impl<Msg: Clone + 'static> ScaleTransition<Msg> {
249    /// Scales `child` by `scale`, about its centre.
250    pub fn new(scale: f32, child: impl Widget<Msg> + 'static) -> Self {
251        Self::about(scale, Alignment::CENTER, child)
252    }
253
254    /// The same, about a `pivot` — the corner or edge that stays put.
255    pub fn about(scale: f32, pivot: Alignment, child: impl Widget<Msg> + 'static) -> Self {
256        Self {
257            inner: Box::new(
258                crate::Transform::scale_xy_from(scale, scale, pivot)
259                    // No duration: an explicit transition is **already** where the caller
260                    // says it is. A tween here would chase the value the application is
261                    // itself stepping, and the two clocks would fight.
262                    .child(child),
263            ),
264        }
265    }
266}
267
268impl<Msg> ScaleTransition<Msg> {
269    /// A transparent wrapper: the box is the child's.
270    fn restyle(&self, base: Style) -> Style {
271        base
272    }
273}
274
275crate::transparent::forward_transparent!(ScaleTransition {
276    /// Forwarded, all of them: a scale is not an identity, not a place, not a theme and
277    /// not a surface. It is its child, growing.
278    fn key(&self) -> Option<u64> {
279        self.inner.key()
280    }
281    fn positioned(&self) -> Option<crate::positioned::Positioning> {
282        self.inner.positioned()
283    }
284    fn theme_override(
285        &self,
286        inherited: &crate::theme::Theme,
287    ) -> Option<Box<crate::theme::Theme>> {
288        self.inner.theme_override(inherited)
289    }
290    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
291        self.inner.media_override(inherited)
292    }
293    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
294        self.inner.scaffold_override()
295    }
296    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
297        self.inner.scroll_behavior_override()
298    }
299    /// Forwarded: a wrapper around a form is still that form (milestone 512).
300    fn autofill_group(&self) -> bool {
301        self.inner.autofill_group()
302    }
303    /// Forwarded: the area's bar is the area's, whatever wraps it.
304    fn area_toolbar(
305        &self,
306        context: crate::ToolbarContext,
307    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
308        self.inner.area_toolbar(context)
309    }
310});
311
312/// A curve applied to a progress, for a caller stepping one by hand: `curve.transform(t)`
313/// is the same call the framework's own animations make, so an explicit transition and an
314/// implicit one given the same curve are on the same shape.
315///
316/// It is here rather than left to the caller because reaching for
317/// [`frus_core::Curve::transform`] means knowing it exists, and the name of the thing you
318/// are looking for is *ease this progress*.
319pub fn eased(progress: f32, curve: &Curve) -> f32 {
320    curve.transform(progress.clamp(0.0, 1.0))
321}
322
323/// Where a [`DecoratedBoxTransition`] paints its decoration.
324#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
325pub enum DecorationPosition {
326    /// Behind the child: a fill, a card, a highlight under a row. The default.
327    #[default]
328    Background,
329    /// Over the child: a border around a photograph, a wash across a tile that is
330    /// becoming disabled.
331    Foreground,
332}
333
334/// **Paints a decoration that moves** by a value the caller owns — its fill, gradient,
335/// border, corners and shadow, each travelling by its own rule (see
336/// [`BoxDecoration::lerp`]).
337///
338/// ```
339/// use frus_core::{Border, BoxDecoration, Color};
340/// use frus_widgets::{DecoratedBoxTransition, Text};
341///
342/// let selected = 0.6_f32; // the application's own progress
343/// let _row: DecoratedBoxTransition<()> = DecoratedBoxTransition::between(
344///     BoxDecoration::default().radius(4.0),
345///     BoxDecoration::filled(Color::rgb(0.2, 0.4, 0.9))
346///         .radius(12.0)
347///         .border(Border::new(2.0, Color::rgb(0.1, 0.2, 0.6))),
348///     selected,
349///     Text::new("Reminders"),
350/// );
351/// ```
352///
353/// **Unlike the other three here, this is a node of its own**, with the child beneath it:
354/// a decoration is something painted, and something painted has a box. That box is the
355/// child's, and **the child is not inset by the border** — the same as the reference's
356/// `DecoratedBox`, and for a reason that matters more here than there: a line that
357/// thickens over a transition would otherwise push the content about on every frame of
358/// it. A border that must not overlap the content wants padding the caller chooses.
359///
360/// For a decoration that should catch up with a target on the framework's clock instead,
361/// [`crate::AnimatedContainer`] is the implicit half.
362pub struct DecoratedBoxTransition<Msg = crate::callback::Callback> {
363    children: Vec<Box<dyn Widget<Msg>>>,
364    decoration: BoxDecoration,
365    position: DecorationPosition,
366}
367
368impl<Msg> DecoratedBoxTransition<Msg> {
369    /// `child` over `decoration`, as it stands.
370    pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
371        Self {
372            children: vec![Box::new(child)],
373            decoration,
374            position: DecorationPosition::Background,
375        }
376    }
377
378    /// `child` over the decoration `progress` of the way from `from` to `to` — which is
379    /// what this is nearly always called with.
380    pub fn between(
381        from: BoxDecoration,
382        to: BoxDecoration,
383        progress: f32,
384        child: impl Widget<Msg> + 'static,
385    ) -> Self {
386        Self::new(from.lerp(to, progress), child)
387    }
388
389    /// Paints the decoration over the child instead of behind it.
390    pub fn position(mut self, position: DecorationPosition) -> Self {
391        self.position = position;
392        self
393    }
394}
395
396impl<Msg: Clone> Widget<Msg> for DecoratedBoxTransition<Msg> {
397    fn style(&self) -> Style {
398        // Nothing of its own: the box is the child's, and the border does not inset it.
399        Style::default()
400    }
401
402    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
403        &self.children
404    }
405
406    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
407        if self.position == DecorationPosition::Background {
408            self.decoration
409                .paint_into_in(scene, bounds, status.opacity, theme.direction);
410        }
411    }
412
413    /// In front of the child, through the walk's one place for painting after a subtree.
414    fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
415        (self.position == DecorationPosition::Foreground).then_some(self.decoration.clone())
416    }
417
418    fn on_click(&self) -> Option<Msg> {
419        None
420    }
421
422    fn debug_name(&self) -> &'static str {
423        "DecoratedBoxTransition"
424    }
425}
426
427/// **Hands a text style that moves down to everything below it**, by a value the caller
428/// owns. The explicit half of [`crate::AnimatedDefaultTextStyle`].
429///
430/// ```
431/// use frus_core::TextStyle;
432/// use frus_widgets::{DefaultTextStyleTransition, Text};
433///
434/// let arriving = 0.3_f32; // the application's own progress
435/// let _title: DefaultTextStyleTransition<()> = DefaultTextStyleTransition::between(
436///     TextStyle::NONE.size(20.0),
437///     TextStyle::NONE.size(24.0),
438///     arriving,
439///     Text::new("Groceries"),
440/// );
441/// ```
442///
443/// What travels, what holds and what swaps half-way is [`TextStyle::lerp`]'s business.
444/// Alignment, wrapping, overflow and the line count take effect at once, as they do on the
445/// implicit one: they are arrangements, not quantities.
446///
447/// **It is a transparent wrapper, which its implicit twin cannot be.** An implicit
448/// animation keeps a timeline in the runtime, and a timeline belongs to a node, so two of
449/// those nested have to be two nodes or they would share one. This keeps nothing — the
450/// caller's number is the whole of its state — so two nested fuse into one node and simply
451/// hand down both of what they say, the inner one asked by the outer as a nested theme is.
452///
453/// A text that names a field for itself keeps it: an inherited style answers a question,
454/// it does not overrule an answer.
455pub struct DefaultTextStyleTransition<Msg = crate::callback::Callback> {
456    handed: crate::widgettheme::DefaultTextStyle,
457    inner: Box<dyn Widget<Msg>>,
458}
459
460impl<Msg: Clone + 'static> DefaultTextStyleTransition<Msg> {
461    /// `child`'s subtree wearing `style`, as it stands.
462    pub fn new(style: TextStyle, child: impl Widget<Msg> + 'static) -> Self {
463        Self {
464            handed: crate::widgettheme::DefaultTextStyle::from_text_style(style),
465            inner: Box::new(child),
466        }
467    }
468
469    /// `child`'s subtree wearing the style `progress` of the way from `from` to `to`.
470    pub fn between(
471        from: TextStyle,
472        to: TextStyle,
473        progress: f32,
474        child: impl Widget<Msg> + 'static,
475    ) -> Self {
476        Self::new(from.lerp(to, progress), child)
477    }
478
479    /// Where the lines sit inside their box. **Not animated.**
480    pub fn align(mut self, align: TextAlign) -> Self {
481        self.handed.align = Some(align);
482        self
483    }
484
485    /// Whether the text wraps at the width it is given. **Not animated.**
486    pub fn soft_wrap(mut self, wrap: bool) -> Self {
487        self.handed.soft_wrap = Some(wrap);
488        self
489    }
490
491    /// What becomes of text that does not fit. **Not animated.**
492    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
493        self.handed.overflow = Some(overflow);
494        self
495    }
496
497    /// At most this many lines. **Not animated.**
498    pub fn max_lines(mut self, lines: usize) -> Self {
499        self.handed.max_lines = Some(lines);
500        self
501    }
502}
503
504impl<Msg> DefaultTextStyleTransition<Msg> {
505    /// A transparent wrapper: the box is the child's. A style reaches sizes through the
506    /// child's own `style_themed`, not through this.
507    fn restyle(&self, base: Style) -> Style {
508        base
509    }
510}
511
512crate::transparent::forward_transparent!(DefaultTextStyleTransition {
513    /// A style is not an identity, not a place and not a surface: forwarded.
514    fn key(&self) -> Option<u64> {
515        self.inner.key()
516    }
517    fn positioned(&self) -> Option<crate::positioned::Positioning> {
518        self.inner.positioned()
519    }
520
521    /// The one thing this does not delegate: the style it hands down, merged onto what it
522    /// inherits — and then the child asked in turn, because a transparent wrapper *is* its
523    /// child and a nested one would otherwise never be asked at all.
524    fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
525        let mut mine = inherited.clone();
526        mine.widgets.text = mine.widgets.text.merge(self.handed);
527        Some(
528            self.inner
529                .theme_override(&mine)
530                .unwrap_or_else(|| Box::new(mine)),
531        )
532    }
533
534    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
535        self.inner.media_override(inherited)
536    }
537    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
538        self.inner.scaffold_override()
539    }
540    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
541        self.inner.scroll_behavior_override()
542    }
543    /// Forwarded: a wrapper around a form is still that form (milestone 512).
544    fn autofill_group(&self) -> bool {
545        self.inner.autofill_group()
546    }
547    /// Forwarded: the area's bar is the area's, whatever wraps it.
548    fn area_toolbar(
549        &self,
550        context: crate::ToolbarContext,
551    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
552        self.inner.area_toolbar(context)
553    }
554});
555
556/// **Reveals** its child along one axis by a value the caller owns: the box is that fraction
557/// of the child's size, `0.0` nothing and `1.0` all of it, and the child is cut to it.
558///
559/// The child is laid out at its own size throughout, so it is uncovered, not squeezed.
560/// Across the axis, the box is as big as the room. What is below or beside it moves with
561/// it.
562///
563/// ```
564/// use frus_widgets::{text, SizeTransition};
565///
566/// let opening = 0.3_f32;
567/// let details = SizeTransition::<()>::new(opening, text("Shown as it opens"));
568/// ```
569pub struct SizeTransition<Msg = crate::callback::Callback> {
570    factor: f32,
571    axis: crate::scroll::Axis,
572    axis_alignment: f32,
573    children: Vec<Box<dyn Widget<Msg>>>,
574}
575
576impl<Msg> SizeTransition<Msg> {
577    /// Reveals `child` down to `factor` of its height.
578    pub fn new(factor: f32, child: impl Widget<Msg> + 'static) -> Self {
579        Self {
580            factor,
581            axis: crate::scroll::Axis::Vertical,
582            axis_alignment: 0.0,
583            children: vec![Box::new(child)],
584        }
585    }
586
587    /// The axis it reveals along: down by default, or across.
588    pub fn axis(mut self, axis: crate::scroll::Axis) -> Self {
589        self.axis = axis;
590        self
591    }
592
593    /// Which part of the child shows while it is not all there, along the axis: `-1.0` the
594    /// start (the top, or the left), `0.0` the middle — the default — and `1.0` the end.
595    pub fn axis_alignment(mut self, alignment: f32) -> Self {
596        self.axis_alignment = alignment;
597        self
598    }
599
600    fn vertical(&self) -> bool {
601        !matches!(self.axis, crate::scroll::Axis::Horizontal)
602    }
603}
604
605impl<Msg: Clone> Widget<Msg> for SizeTransition<Msg> {
606    fn style(&self) -> Style {
607        Style::default()
608    }
609
610    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
611        &self.children
612    }
613
614    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
615
616    fn on_click(&self) -> Option<Msg> {
617        None
618    }
619
620    /// The axis it reveals along is the child's to decide; the other is the room's.
621    fn constraints_transform(&self) -> Option<crate::constraints::ConstraintsTransform> {
622        use crate::constraints::AxisConstraint::{AsGiven, Unbounded};
623        let (width, height, alignment) = if self.vertical() {
624            (
625                AsGiven,
626                Unbounded,
627                Alignment {
628                    x: -1.0,
629                    y: self.axis_alignment,
630                },
631            )
632        } else {
633            (
634                Unbounded,
635                AsGiven,
636                Alignment {
637                    x: self.axis_alignment,
638                    y: -1.0,
639                },
640            )
641        };
642        Some(crate::constraints::ConstraintsTransform {
643            width,
644            height,
645            alignment,
646            report: false,
647        })
648    }
649
650    fn size_factor(&self) -> Option<(f32, f32)> {
651        let factor = self.factor.max(0.0);
652        Some(if self.vertical() {
653            (1.0, factor)
654        } else {
655            (factor, 1.0)
656        })
657    }
658
659    fn clips_to_box(&self) -> bool {
660        true
661    }
662
663    fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
664        crate::widget::FillAxes {
665            horizontal: self.vertical(),
666            vertical: !self.vertical(),
667        }
668    }
669
670    fn debug_name(&self) -> &'static str {
671        "SizeTransition"
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use crate::{build_ui, Container, Runtime, Theme};
679    use frus_core::{Color, Primitive, Rect, Size};
680
681    const RED: Color = Color {
682        r: 1.0,
683        g: 0.0,
684        b: 0.0,
685        a: 1.0,
686    };
687
688    const BLUE: Color = Color {
689        r: 0.0,
690        g: 0.0,
691        b: 1.0,
692        a: 1.0,
693    };
694
695    fn mark() -> Container<()> {
696        Container::new().width(40.0).height(20.0).color(RED)
697    }
698
699    /// The red mark's box and the clip it is painted under, through any layer.
700    fn red(primitives: &[Primitive]) -> Option<(Rect, Rect)> {
701        primitives.iter().find_map(|p| match p {
702            Primitive::Rect {
703                rect, clip, color, ..
704            } if color.r > 0.5 && color.g < 0.5 => Some((*rect, *clip)),
705            Primitive::Layer { primitives, .. } => red(primitives),
706            _ => None,
707        })
708    }
709
710    fn revealed(factor: f32, alignment: f32) -> crate::Ui<()> {
711        let root = crate::Flex::<()>::column()
712            .child(SizeTransition::new(factor, mark()).axis_alignment(alignment))
713            .child(Container::new().width(40.0).height(10.0).color(BLUE));
714        build_ui(
715            &root,
716            Size::new(100.0, 100.0),
717            &Runtime::default(),
718            &Theme::dark(),
719        )
720    }
721
722    /// **Half of a 20-px mark is a 10-px box**: what is below starts at 10, the mark is still
723    /// 20 px tall, and it is cut at the box.
724    #[test]
725    fn a_size_transition_is_a_fraction_of_its_child() {
726        let ui = revealed(0.5, -1.0);
727        assert_eq!(blue_y(ui.scene().primitives()), Some(10.0));
728        let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
729        assert_eq!(rect.height, 20.0, "uncovered, not squeezed");
730        assert_eq!((rect.y, clip.y + clip.height), (0.0, 10.0), "its top half");
731    }
732
733    /// The axis alignment chooses which half: the end shows the bottom of the mark.
734    #[test]
735    fn the_axis_alignment_chooses_what_shows() {
736        let ui = revealed(0.5, 1.0);
737        let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
738        assert_eq!(
739            rect.y, -10.0,
740            "the mark moved up by the half that is hidden"
741        );
742        assert_eq!(clip.y + clip.height, 10.0);
743    }
744
745    /// Nothing and all of it are the two ends: 0 is no box and nothing painted where it would
746    /// show, 1 is the whole mark.
747    #[test]
748    fn zero_is_nothing_and_one_is_all() {
749        assert_eq!(blue_y(revealed(0.0, -1.0).scene().primitives()), Some(0.0));
750        assert_eq!(blue_y(revealed(1.0, -1.0).scene().primitives()), Some(20.0));
751    }
752
753    /// **A factor that moves from frame to frame moves the box**, on one runtime whose layout
754    /// cache would otherwise hand back the last frame's box.
755    #[test]
756    fn a_new_factor_is_a_new_box() {
757        let rt = Runtime::default();
758        let frame = |factor: f32| {
759            let root = crate::Flex::<()>::column()
760                .child(SizeTransition::new(factor, mark()))
761                .child(Container::new().width(40.0).height(10.0).color(BLUE));
762            let ui = build_ui(&root, Size::new(100.0, 100.0), &rt, &Theme::dark());
763            blue_y(ui.scene().primitives())
764        };
765        assert_eq!(frame(0.5), Some(10.0));
766        assert_eq!(frame(1.0), Some(20.0));
767    }
768
769    /// **Across the axis, the box is the room's**: in a 100-px box that stretches nothing, a
770    /// mark with no width of its own is 100 px wide.
771    #[test]
772    fn across_the_axis_the_box_is_the_room() {
773        let root = Container::<()>::new()
774            .width(100.0)
775            .child(SizeTransition::new(
776                1.0,
777                Container::new().height(20.0).color(RED),
778            ));
779        let ui = build_ui(
780            &root,
781            Size::new(300.0, 100.0),
782            &Runtime::default(),
783            &Theme::dark(),
784        );
785        let (rect, _) = red(ui.scene().primitives()).expect("the mark");
786        assert_eq!(rect.width, 100.0);
787    }
788
789    /// Across: a quarter of a 40-px-wide mark is a 10-px-wide box, and the next thing in the
790    /// row starts there.
791    #[test]
792    fn a_horizontal_size_transition_reveals_across() {
793        let root = crate::Flex::<()>::row()
794            .child(
795                SizeTransition::new(0.25, mark())
796                    .axis(crate::scroll::Axis::Horizontal)
797                    .axis_alignment(-1.0),
798            )
799            .child(Container::new().width(40.0).height(10.0).color(BLUE));
800        let ui = build_ui(
801            &root,
802            Size::new(100.0, 100.0),
803            &Runtime::default(),
804            &Theme::dark(),
805        );
806        fn blue_x(primitives: &[Primitive]) -> Option<f32> {
807            primitives.iter().find_map(|p| match p {
808                Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
809                    Some(rect.x)
810                }
811                Primitive::Layer { primitives, .. } => blue_x(primitives),
812                _ => None,
813            })
814        }
815        assert_eq!(blue_x(ui.scene().primitives()), Some(10.0));
816        let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
817        assert_eq!(rect.width, 40.0);
818        assert_eq!(clip.x + clip.width, 10.0);
819    }
820
821    /// Where the blue neighbour was painted, through any layer above it.
822    fn blue_y(primitives: &[Primitive]) -> Option<f32> {
823        for primitive in primitives {
824            match primitive {
825                Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
826                    return Some(rect.y)
827                }
828                Primitive::Layer { primitives, .. } => {
829                    if let Some(found) = blue_y(primitives) {
830                        return Some(found);
831                    }
832                }
833                _ => {}
834            }
835        }
836        None
837    }
838
839    /// The first red rectangle, and the opacity of the layer it is inside (`1.0` when
840    /// there is no layer).
841    fn painted(root: &dyn Widget<()>) -> (Rect, f32) {
842        painted_if_any(root).expect("the mark")
843    }
844
845    /// The same, or `None` when nothing red is painted at all.
846    fn painted_if_any(root: &dyn Widget<()>) -> Option<(Rect, f32)> {
847        let ui = build_ui(
848            root,
849            Size::new(100.0, 100.0),
850            &Runtime::default(),
851            &Theme::dark(),
852        );
853        fn walk(primitives: &[Primitive], opacity: f32) -> Option<(Rect, f32)> {
854            for primitive in primitives {
855                match primitive {
856                    Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
857                        return Some((*rect, opacity))
858                    }
859                    Primitive::Layer {
860                        primitives,
861                        opacity: layer,
862                        ..
863                    } => {
864                        if let Some(found) = walk(primitives, opacity * layer) {
865                            return Some(found);
866                        }
867                    }
868                    _ => {}
869                }
870            }
871            None
872        }
873        walk(ui.scene().primitives(), 1.0)
874    }
875
876    /// **Nothing, half way, all of it** — the three each of these has to pin. Nothing is
877    /// not painted at all, as the reference's faded-out child is not (milestone 595).
878    #[test]
879    fn a_fade_is_the_value_it_was_given() {
880        assert_eq!(painted_if_any(&FadeTransition::new(0.0, mark())), None);
881        for (given, expected) in [(0.5, 0.5), (1.0, 1.0)] {
882            let root = FadeTransition::new(given, mark());
883            let (_, opacity) = painted(&root);
884            assert!(
885                (opacity - expected).abs() < 1e-3,
886                "at {given}: painted at {opacity}"
887            );
888        }
889    }
890
891    /// A slide is a multiple of the child's **own** box: a 40 px mark slid by half a width
892    /// has moved 20.
893    #[test]
894    fn a_slide_is_a_multiple_of_the_child_s_own_box() {
895        for (given, expected) in [(0.0, 0.0), (0.5, 20.0), (1.0, 40.0)] {
896            let root = crate::Flex::<()>::column().child(SlideTransition::new(given, 0.0, mark()));
897            let (rect, _) = painted(&root);
898            assert!(
899                (rect.x - expected).abs() < 0.5,
900                "at {given}: painted at {}",
901                rect.x
902            );
903        }
904    }
905
906    /// And the sign the name exists for: coming in from the left, nought is a whole width
907    /// outside and one is home.
908    #[test]
909    fn coming_in_from_an_edge_arrives_at_one() {
910        let at = |progress: f32| {
911            let root = crate::Flex::<()>::column().child(SlideTransition::from_edge(
912                SlideFrom::Left,
913                progress,
914                mark(),
915            ));
916            painted(&root).0.x
917        };
918        assert!((at(0.0) + 40.0).abs() < 0.5, "a whole width outside");
919        assert!((at(0.5) + 20.0).abs() < 0.5, "half of one, still outside");
920        assert!(at(1.0).abs() < 0.5, "and home");
921
922        // Each edge sends it the way its name says.
923        let mark_at = |edge: SlideFrom| {
924            let root =
925                crate::Flex::<()>::column().child(SlideTransition::from_edge(edge, 0.0, mark()));
926            let rect = painted(&root).0;
927            (rect.x, rect.y)
928        };
929        assert!(mark_at(SlideFrom::Left).0 < -1.0);
930        assert!(mark_at(SlideFrom::Right).0 > 1.0);
931        assert!(mark_at(SlideFrom::Top).1 < -1.0);
932        assert!(mark_at(SlideFrom::Bottom).1 > 1.0);
933    }
934
935    /// A scale is a **paint** transform: it reaches the screen as a matrix on a composited
936    /// layer, and the box underneath is untouched — so the neighbour below does not move
937    /// however big the child is drawn. A neutral scale costs no layer at all.
938    #[test]
939    fn a_scale_is_paint_and_not_layout() {
940        let built = |scale: f32| {
941            let root = crate::Flex::<()>::column()
942                .width(100.0)
943                .child(ScaleTransition::new(scale, mark()))
944                .child(Container::<()>::new().width(10.0).height(10.0).color(BLUE));
945            let ui = build_ui(
946                &root,
947                Size::new(100.0, 100.0),
948                &Runtime::default(),
949                &Theme::dark(),
950            );
951            let matrix = ui.scene().primitives().iter().find_map(|p| match p {
952                Primitive::Layer {
953                    transform: Some(t), ..
954                } => Some(t.affine.m[0]),
955                _ => None,
956            });
957            let neighbour = blue_y(ui.scene().primitives()).expect("the neighbour");
958            (matrix, neighbour)
959        };
960        for (given, expected) in [(0.5, Some(0.5)), (1.0, None), (2.0, Some(2.0))] {
961            let (matrix, neighbour) = built(given);
962            match (matrix, expected) {
963                (Some(m), Some(e)) => {
964                    assert!((m - e).abs() < 1e-3, "at {given}: painted at {m}")
965                }
966                (None, None) => {}
967                _ => panic!("at {given}: {matrix:?} where {expected:?} was expected"),
968            }
969            assert!(
970                (neighbour - 20.0).abs() < 0.5,
971                "the neighbour stayed where the layout put it: {neighbour}"
972            );
973        }
974    }
975
976    /// The curve is the framework's own, so the two families agree on what *ease-out, half
977    /// way* means.
978    #[test]
979    fn easing_a_progress_is_the_frameworks_own_curve() {
980        let curve = Curve::ease_out();
981        assert!(eased(0.0, &curve).abs() < 1e-3);
982        assert!((eased(1.0, &curve) - 1.0).abs() < 1e-3);
983        assert_eq!(eased(0.5, &curve), curve.transform(0.5));
984        assert!(
985            eased(0.5, &curve) > 0.5,
986            "an ease-out is ahead at the middle"
987        );
988        assert_eq!(
989            eased(2.0, &curve),
990            eased(1.0, &curve),
991            "and a progress past its end is its end"
992        );
993    }
994
995    // --- A decoration and a text style (milestone 501) ---
996
997    use frus_core::{Border, TextStyle};
998
999    /// Every rectangle painted, in paint order, with its colour — through any layer.
1000    fn rects_of(root: &dyn Widget<()>) -> Vec<(Rect, Color)> {
1001        let ui = build_ui(
1002            root,
1003            Size::new(200.0, 200.0),
1004            &Runtime::default(),
1005            &Theme::dark(),
1006        );
1007        fn walk(primitives: &[Primitive], out: &mut Vec<(Rect, Color)>) {
1008            for primitive in primitives {
1009                match primitive {
1010                    Primitive::Rect { rect, color, .. } => out.push((*rect, *color)),
1011                    Primitive::Layer { primitives, .. } => walk(primitives, out),
1012                    _ => {}
1013                }
1014            }
1015        }
1016        let mut out = Vec::new();
1017        walk(ui.scene().primitives(), &mut out);
1018        out
1019    }
1020
1021    /// Every run of words painted: its size and its colour.
1022    fn texts_of(root: &dyn Widget<()>) -> Vec<(f32, Color)> {
1023        let ui = build_ui(
1024            root,
1025            Size::new(200.0, 200.0),
1026            &Runtime::default(),
1027            &Theme::dark(),
1028        );
1029        fn walk(primitives: &[Primitive], out: &mut Vec<(f32, Color)>) {
1030            for primitive in primitives {
1031                match primitive {
1032                    Primitive::Text { size, color, .. } => out.push((*size, *color)),
1033                    Primitive::Layer { primitives, .. } => walk(primitives, out),
1034                    _ => {}
1035                }
1036            }
1037        }
1038        let mut out = Vec::new();
1039        walk(ui.scene().primitives(), &mut out);
1040        out
1041    }
1042
1043    fn close(a: Color, b: Color) -> bool {
1044        (a.r - b.r).abs() < 1e-4
1045            && (a.g - b.g).abs() < 1e-4
1046            && (a.b - b.b).abs() < 1e-4
1047            && (a.a - b.a).abs() < 1e-4
1048    }
1049
1050    /// **At the start, half-way and at the end — read off the scene**, which is where a
1051    /// decoration that moved correctly and never reached the screen would be caught.
1052    #[test]
1053    fn a_decoration_is_painted_as_it_stands() {
1054        for (t, want) in [(0.0, RED), (0.5, RED.lerp(BLUE, 0.5)), (1.0, BLUE)] {
1055            let tree = DecoratedBoxTransition::<()>::between(
1056                BoxDecoration::filled(RED),
1057                BoxDecoration::filled(BLUE),
1058                t,
1059                Container::new().width(40.0).height(20.0),
1060            );
1061            let rects = rects_of(&tree);
1062            assert_eq!(rects.len(), 1, "at {t}: {rects:?}");
1063            assert!(close(rects[0].1, want), "at {t}: {:?}", rects[0].1);
1064        }
1065    }
1066
1067    /// **A fill fading out keeps its hue on the screen too**, not only in the arithmetic.
1068    #[test]
1069    fn a_fading_fill_is_painted_in_its_own_colour() {
1070        let tree = DecoratedBoxTransition::<()>::between(
1071            BoxDecoration::filled(RED),
1072            BoxDecoration::default(),
1073            0.5,
1074            Container::new().width(40.0).height(20.0),
1075        );
1076        let rects = rects_of(&tree);
1077        assert_eq!(rects.len(), 1, "{rects:?}");
1078        assert!(close(rects[0].1, RED.fade(0.5)), "{:?}", rects[0].1);
1079    }
1080
1081    /// **A line that thickens does not move the content.** The child is not inset by the
1082    /// border, so a border growing from nothing to eight pixels leaves it where it was on
1083    /// every frame — which an inset would not.
1084    #[test]
1085    fn a_thickening_border_does_not_move_the_child() {
1086        let child_at = |t: f32| {
1087            let tree = DecoratedBoxTransition::<()>::between(
1088                BoxDecoration::default(),
1089                BoxDecoration::default().border(Border::new(8.0, BLUE)),
1090                t,
1091                mark(),
1092            );
1093            rects_of(&tree)
1094                .into_iter()
1095                .find(|(_, c)| close(*c, RED))
1096                .expect("the child")
1097                .0
1098        };
1099        let (start, end) = (child_at(0.0), child_at(1.0));
1100        assert_eq!(
1101            (start.x, start.y),
1102            (end.x, end.y),
1103            "the child stayed put: {start:?} then {end:?}"
1104        );
1105    }
1106
1107    /// **In front, it is painted over the child**; behind is the default.
1108    #[test]
1109    fn in_front_the_decoration_covers_the_child() {
1110        let wash = || BoxDecoration::filled(BLUE.fade(0.3));
1111        let order = |tree: &DecoratedBoxTransition<()>| {
1112            let rects = rects_of(tree);
1113            let child = rects.iter().position(|(_, c)| close(*c, RED));
1114            let over = rects.iter().position(|(_, c)| c.b > 0.5);
1115            (child.expect("the child"), over.expect("the wash"))
1116        };
1117        let (child, over) = order(
1118            &DecoratedBoxTransition::new(wash(), mark()).position(DecorationPosition::Foreground),
1119        );
1120        assert!(child < over, "in front: the wash comes after the child");
1121        let (child, under) = order(&DecoratedBoxTransition::new(wash(), mark()));
1122        assert!(under < child, "behind, by default");
1123    }
1124
1125    /// **The words are drawn at the style in flight** — start, half-way and end.
1126    #[test]
1127    fn a_text_style_is_handed_down_as_it_stands() {
1128        for (t, want) in [(0.0, 12.0), (0.5, 18.0), (1.0, 24.0)] {
1129            let tree = DefaultTextStyleTransition::<()>::between(
1130                TextStyle::NONE.size(12.0),
1131                TextStyle::NONE.size(24.0),
1132                t,
1133                crate::Text::new("Ag"),
1134            );
1135            let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
1136            assert_eq!(sizes, vec![want], "at {t}");
1137        }
1138    }
1139
1140    /// **Two nested fuse into one node and hand down both.** An outer one moving the
1141    /// colour and an inner one the size: the words wear both, which means the outer asked
1142    /// the inner — a transparent wrapper *is* its child, and the inner is never visited as a
1143    /// node of its own.
1144    #[test]
1145    fn two_nested_hand_down_both_halves() {
1146        let tree = DefaultTextStyleTransition::<()>::new(
1147            TextStyle::NONE.color(BLUE),
1148            DefaultTextStyleTransition::new(TextStyle::NONE.size(20.0), crate::Text::new("Ag")),
1149        );
1150        let texts = texts_of(&tree);
1151        assert_eq!(texts.len(), 1, "{texts:?}");
1152        assert_eq!(texts[0].0, 20.0, "the inner one's size");
1153        assert!(close(texts[0].1, BLUE), "and the outer one's colour");
1154    }
1155
1156    /// A text that names its own size keeps it: an inherited style answers a question, it
1157    /// does not overrule an answer.
1158    #[test]
1159    fn a_text_that_names_its_size_keeps_it() {
1160        let tree = DefaultTextStyleTransition::<()>::new(
1161            TextStyle::NONE.size(30.0),
1162            crate::Text::new("Ag").size(11.0),
1163        );
1164        let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
1165        assert_eq!(sizes, vec![11.0]);
1166    }
1167}