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` animates a size it has to **measure**, and a widget cannot measure
38//! its children here — see the entry in `ROADMAP.md` and issue #52. Not a wrapper, and
39//! not this milestone.
40//! - [`DecoratedBoxTransition`] and [`DefaultTextStyleTransition`] are here since
41//! milestone 501. They are not wrappers over one value but interpolations of a whole
42//! decoration and a whole text style, each part by its own rule — see
43//! `BoxDecoration::lerp` and `TextStyle::lerp`, which is where the real work is.
44
45use frus_core::{Alignment, BoxDecoration, Curve, Rect, Scene, TextAlign, TextOverflow, TextStyle};
46use frus_layout::Style;
47
48use crate::interaction::Status;
49use crate::theme::Theme;
50use crate::widget::Widget;
51
52/// **Fades** its child by a value the caller owns: `0.0` invisible, `1.0` solid.
53///
54/// The subtree is composited as one group, so overlapping children do not darken where
55/// they overlap — the difference between fading a picture and fading each of its parts.
56///
57/// ```
58/// use frus_widgets::{FadeTransition, Text};
59///
60/// let entering = 0.4_f32;
61/// let _fading: FadeTransition<()> = FadeTransition::new(entering, Text::new("Details"));
62/// ```
63///
64/// It is [`crate::Opacity`] with the name the reference uses, which is worth having: a
65/// reader looking for the explicit half of the animation library looks for this word.
66pub struct FadeTransition<Msg = crate::callback::Callback> {
67 inner: Box<dyn Widget<Msg>>,
68}
69
70impl<Msg: Clone + 'static> FadeTransition<Msg> {
71 /// Fades `child` to `opacity`, clamped to `0..=1`.
72 pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
73 Self {
74 inner: Box::new(crate::Opacity::new(opacity.clamp(0.0, 1.0), child)),
75 }
76 }
77}
78
79impl<Msg> FadeTransition<Msg> {
80 /// A transparent wrapper: the box is the child's.
81 fn restyle(&self, base: Style) -> Style {
82 base
83 }
84}
85
86crate::transparent::forward_transparent!(FadeTransition {
87 /// Forwarded, all of them: a fade is not an identity, not a place, not a theme and
88 /// not a surface. It is its child, disappearing.
89 fn key(&self) -> Option<u64> {
90 self.inner.key()
91 }
92 fn positioned(&self) -> Option<crate::positioned::Positioning> {
93 self.inner.positioned()
94 }
95 fn theme_override(
96 &self,
97 inherited: &crate::theme::Theme,
98 ) -> Option<Box<crate::theme::Theme>> {
99 self.inner.theme_override(inherited)
100 }
101 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
102 self.inner.media_override(inherited)
103 }
104 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
105 self.inner.scaffold_override()
106 }
107 /// Forwarded: a wrapper around a form is still that form (milestone 512).
108 fn autofill_group(&self) -> bool {
109 self.inner.autofill_group()
110 }
111});
112
113/// The edge a [`SlideTransition::from_edge`] comes in from.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum SlideFrom {
116 /// From beyond the left edge, moving right.
117 Left,
118 /// From beyond the right edge, moving left.
119 Right,
120 /// From above, moving down.
121 Top,
122 /// From below, moving up.
123 Bottom,
124}
125
126/// **Slides** its child by a value the caller owns, stated as a **fraction of the child's
127/// own size**: `(1.0, 0.0)` is one whole width to the right.
128///
129/// A fraction rather than pixels because the width is the layout's to decide, and the
130/// number that puts a panel exactly off its own edge is `1` whatever that width turns out
131/// to be — which is what makes the same call work for a sheet, a drawer and a row.
132///
133/// Layout is untouched: the box stays where it was put and the neighbours do not move.
134///
135/// ```
136/// use frus_widgets::{SlideFrom, SlideTransition, Text};
137///
138/// let entering = 0.25_f32;
139/// // A quarter of the way in from the left: three quarters of a width still outside.
140/// let _panel: SlideTransition<()> =
141/// SlideTransition::from_edge(SlideFrom::Left, entering, Text::new("Filters"));
142/// ```
143///
144/// [`SlideTransition::from_edge`] is the reason this has a name at all. *Coming in from
145/// the left* is a negative offset that shrinks to nought as the progress grows, and every
146/// application that writes it by hand gets the sign wrong once.
147pub struct SlideTransition<Msg = crate::callback::Callback> {
148 inner: Box<dyn Widget<Msg>>,
149}
150
151impl<Msg: Clone + 'static> SlideTransition<Msg> {
152 /// Slides `child` by `fx` widths across and `fy` heights down.
153 pub fn new(fx: f32, fy: f32, child: impl Widget<Msg> + 'static) -> Self {
154 Self {
155 inner: Box::new(crate::FractionalTranslation::new(fx, fy).child(child)),
156 }
157 }
158
159 /// Slides `child` in from an edge: `progress` of `0` leaves it a whole size outside,
160 /// `1` puts it exactly where the layout laid it out. Values between the two are the
161 /// journey, and values outside them overshoot — which is what a spring does at the end
162 /// of its travel, and is deliberately not clamped away.
163 pub fn from_edge(edge: SlideFrom, progress: f32, child: impl Widget<Msg> + 'static) -> Self {
164 let away = 1.0 - progress;
165 let (fx, fy) = match edge {
166 SlideFrom::Left => (-away, 0.0),
167 SlideFrom::Right => (away, 0.0),
168 SlideFrom::Top => (0.0, -away),
169 SlideFrom::Bottom => (0.0, away),
170 };
171 Self::new(fx, fy, child)
172 }
173}
174
175impl<Msg> SlideTransition<Msg> {
176 /// A transparent wrapper: the box is the child's.
177 fn restyle(&self, base: Style) -> Style {
178 base
179 }
180}
181
182crate::transparent::forward_transparent!(SlideTransition {
183 /// Forwarded, all of them: a slide is not an identity, not a place, not a theme and
184 /// not a surface. It is its child, moving.
185 fn key(&self) -> Option<u64> {
186 self.inner.key()
187 }
188 fn positioned(&self) -> Option<crate::positioned::Positioning> {
189 self.inner.positioned()
190 }
191 fn theme_override(
192 &self,
193 inherited: &crate::theme::Theme,
194 ) -> Option<Box<crate::theme::Theme>> {
195 self.inner.theme_override(inherited)
196 }
197 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
198 self.inner.media_override(inherited)
199 }
200 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
201 self.inner.scaffold_override()
202 }
203 /// Forwarded: a wrapper around a form is still that form (milestone 512).
204 fn autofill_group(&self) -> bool {
205 self.inner.autofill_group()
206 }
207});
208
209/// **Scales** its child by a value the caller owns, about its centre or a pivot of the
210/// caller's choosing. `1.0` is neutral.
211///
212/// Paint only: the box does not change, so nothing around it moves and the child may spill
213/// past its own edges. That is what a control popping under a press wants, and what a
214/// dialog growing into place wants.
215///
216/// ```
217/// use frus_widgets::{ScaleTransition, Text};
218///
219/// let opening = 0.9_f32;
220/// let _dialog: ScaleTransition<()> = ScaleTransition::new(opening, Text::new("Delete?"));
221/// ```
222///
223/// The pivot is a choice of origin rather than a quantity, so it is an argument here and
224/// never a thing to interpolate — the same rule [`crate::AnimatedScale`] states.
225pub struct ScaleTransition<Msg = crate::callback::Callback> {
226 inner: Box<dyn Widget<Msg>>,
227}
228
229impl<Msg: Clone + 'static> ScaleTransition<Msg> {
230 /// Scales `child` by `scale`, about its centre.
231 pub fn new(scale: f32, child: impl Widget<Msg> + 'static) -> Self {
232 Self::about(scale, Alignment::CENTER, child)
233 }
234
235 /// The same, about a `pivot` — the corner or edge that stays put.
236 pub fn about(scale: f32, pivot: Alignment, child: impl Widget<Msg> + 'static) -> Self {
237 Self {
238 inner: Box::new(
239 crate::Transform::scale_xy_from(scale, scale, pivot)
240 // No duration: an explicit transition is **already** where the caller
241 // says it is. A tween here would chase the value the application is
242 // itself stepping, and the two clocks would fight.
243 .child(child),
244 ),
245 }
246 }
247}
248
249impl<Msg> ScaleTransition<Msg> {
250 /// A transparent wrapper: the box is the child's.
251 fn restyle(&self, base: Style) -> Style {
252 base
253 }
254}
255
256crate::transparent::forward_transparent!(ScaleTransition {
257 /// Forwarded, all of them: a scale is not an identity, not a place, not a theme and
258 /// not a surface. It is its child, growing.
259 fn key(&self) -> Option<u64> {
260 self.inner.key()
261 }
262 fn positioned(&self) -> Option<crate::positioned::Positioning> {
263 self.inner.positioned()
264 }
265 fn theme_override(
266 &self,
267 inherited: &crate::theme::Theme,
268 ) -> Option<Box<crate::theme::Theme>> {
269 self.inner.theme_override(inherited)
270 }
271 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
272 self.inner.media_override(inherited)
273 }
274 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
275 self.inner.scaffold_override()
276 }
277 /// Forwarded: a wrapper around a form is still that form (milestone 512).
278 fn autofill_group(&self) -> bool {
279 self.inner.autofill_group()
280 }
281});
282
283/// A curve applied to a progress, for a caller stepping one by hand: `curve.transform(t)`
284/// is the same call the framework's own animations make, so an explicit transition and an
285/// implicit one given the same curve are on the same shape.
286///
287/// It is here rather than left to the caller because reaching for
288/// [`frus_core::Curve::transform`] means knowing it exists, and the name of the thing you
289/// are looking for is *ease this progress*.
290pub fn eased(progress: f32, curve: &Curve) -> f32 {
291 curve.transform(progress.clamp(0.0, 1.0))
292}
293
294/// Where a [`DecoratedBoxTransition`] paints its decoration.
295#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
296pub enum DecorationPosition {
297 /// Behind the child: a fill, a card, a highlight under a row. The default.
298 #[default]
299 Background,
300 /// Over the child: a border around a photograph, a wash across a tile that is
301 /// becoming disabled.
302 Foreground,
303}
304
305/// **Paints a decoration that moves** by a value the caller owns — its fill, gradient,
306/// border, corners and shadow, each travelling by its own rule (see
307/// [`BoxDecoration::lerp`]).
308///
309/// ```
310/// use frus_core::{Border, BoxDecoration, Color};
311/// use frus_widgets::{DecoratedBoxTransition, Text};
312///
313/// let selected = 0.6_f32; // the application's own progress
314/// let _row: DecoratedBoxTransition<()> = DecoratedBoxTransition::between(
315/// BoxDecoration::default().radius(4.0),
316/// BoxDecoration::filled(Color::rgb(0.2, 0.4, 0.9))
317/// .radius(12.0)
318/// .border(Border::new(2.0, Color::rgb(0.1, 0.2, 0.6))),
319/// selected,
320/// Text::new("Reminders"),
321/// );
322/// ```
323///
324/// **Unlike the other three here, this is a node of its own**, with the child beneath it:
325/// a decoration is something painted, and something painted has a box. That box is the
326/// child's, and **the child is not inset by the border** — the same as the reference's
327/// `DecoratedBox`, and for a reason that matters more here than there: a line that
328/// thickens over a transition would otherwise push the content about on every frame of
329/// it. A border that must not overlap the content wants padding the caller chooses.
330///
331/// For a decoration that should catch up with a target on the framework's clock instead,
332/// [`crate::AnimatedContainer`] is the implicit half.
333pub struct DecoratedBoxTransition<Msg = crate::callback::Callback> {
334 children: Vec<Box<dyn Widget<Msg>>>,
335 decoration: BoxDecoration,
336 position: DecorationPosition,
337}
338
339impl<Msg> DecoratedBoxTransition<Msg> {
340 /// `child` over `decoration`, as it stands.
341 pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
342 Self {
343 children: vec![Box::new(child)],
344 decoration,
345 position: DecorationPosition::Background,
346 }
347 }
348
349 /// `child` over the decoration `progress` of the way from `from` to `to` — which is
350 /// what this is nearly always called with.
351 pub fn between(
352 from: BoxDecoration,
353 to: BoxDecoration,
354 progress: f32,
355 child: impl Widget<Msg> + 'static,
356 ) -> Self {
357 Self::new(from.lerp(to, progress), child)
358 }
359
360 /// Paints the decoration over the child instead of behind it.
361 pub fn position(mut self, position: DecorationPosition) -> Self {
362 self.position = position;
363 self
364 }
365}
366
367impl<Msg: Clone> Widget<Msg> for DecoratedBoxTransition<Msg> {
368 fn style(&self) -> Style {
369 // Nothing of its own: the box is the child's, and the border does not inset it.
370 Style::default()
371 }
372
373 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
374 &self.children
375 }
376
377 fn paint(&self, bounds: Rect, status: Status, _theme: &Theme, scene: &mut Scene) {
378 if self.position == DecorationPosition::Background {
379 self.decoration.paint_into(scene, bounds, status.opacity);
380 }
381 }
382
383 /// In front of the child, through the walk's one place for painting after a subtree.
384 fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
385 (self.position == DecorationPosition::Foreground).then_some(self.decoration)
386 }
387
388 fn on_click(&self) -> Option<Msg> {
389 None
390 }
391
392 fn debug_name(&self) -> &'static str {
393 "DecoratedBoxTransition"
394 }
395}
396
397/// **Hands a text style that moves down to everything below it**, by a value the caller
398/// owns. The explicit half of [`crate::AnimatedDefaultTextStyle`].
399///
400/// ```
401/// use frus_core::TextStyle;
402/// use frus_widgets::{DefaultTextStyleTransition, Text};
403///
404/// let arriving = 0.3_f32; // the application's own progress
405/// let _title: DefaultTextStyleTransition<()> = DefaultTextStyleTransition::between(
406/// TextStyle::NONE.size(20.0),
407/// TextStyle::NONE.size(24.0),
408/// arriving,
409/// Text::new("Groceries"),
410/// );
411/// ```
412///
413/// What travels, what holds and what swaps half-way is [`TextStyle::lerp`]'s business.
414/// Alignment, wrapping, overflow and the line count take effect at once, as they do on the
415/// implicit one: they are arrangements, not quantities.
416///
417/// **It is a transparent wrapper, which its implicit twin cannot be.** An implicit
418/// animation keeps a timeline in the runtime, and a timeline belongs to a node, so two of
419/// those nested have to be two nodes or they would share one. This keeps nothing — the
420/// caller's number is the whole of its state — so two nested fuse into one node and simply
421/// hand down both of what they say, the inner one asked by the outer as a nested theme is.
422///
423/// A text that names a field for itself keeps it: an inherited style answers a question,
424/// it does not overrule an answer.
425pub struct DefaultTextStyleTransition<Msg = crate::callback::Callback> {
426 handed: crate::widgettheme::DefaultTextStyle,
427 inner: Box<dyn Widget<Msg>>,
428}
429
430impl<Msg: Clone + 'static> DefaultTextStyleTransition<Msg> {
431 /// `child`'s subtree wearing `style`, as it stands.
432 pub fn new(style: TextStyle, child: impl Widget<Msg> + 'static) -> Self {
433 Self {
434 handed: crate::widgettheme::DefaultTextStyle::from_text_style(style),
435 inner: Box::new(child),
436 }
437 }
438
439 /// `child`'s subtree wearing the style `progress` of the way from `from` to `to`.
440 pub fn between(
441 from: TextStyle,
442 to: TextStyle,
443 progress: f32,
444 child: impl Widget<Msg> + 'static,
445 ) -> Self {
446 Self::new(from.lerp(to, progress), child)
447 }
448
449 /// Where the lines sit inside their box. **Not animated.**
450 pub fn align(mut self, align: TextAlign) -> Self {
451 self.handed.align = Some(align);
452 self
453 }
454
455 /// Whether the text wraps at the width it is given. **Not animated.**
456 pub fn soft_wrap(mut self, wrap: bool) -> Self {
457 self.handed.soft_wrap = Some(wrap);
458 self
459 }
460
461 /// What becomes of text that does not fit. **Not animated.**
462 pub fn overflow(mut self, overflow: TextOverflow) -> Self {
463 self.handed.overflow = Some(overflow);
464 self
465 }
466
467 /// At most this many lines. **Not animated.**
468 pub fn max_lines(mut self, lines: usize) -> Self {
469 self.handed.max_lines = Some(lines);
470 self
471 }
472}
473
474impl<Msg> DefaultTextStyleTransition<Msg> {
475 /// A transparent wrapper: the box is the child's. A style reaches sizes through the
476 /// child's own `style_themed`, not through this.
477 fn restyle(&self, base: Style) -> Style {
478 base
479 }
480}
481
482crate::transparent::forward_transparent!(DefaultTextStyleTransition {
483 /// A style is not an identity, not a place and not a surface: forwarded.
484 fn key(&self) -> Option<u64> {
485 self.inner.key()
486 }
487 fn positioned(&self) -> Option<crate::positioned::Positioning> {
488 self.inner.positioned()
489 }
490
491 /// The one thing this does not delegate: the style it hands down, merged onto what it
492 /// inherits — and then the child asked in turn, because a transparent wrapper *is* its
493 /// child and a nested one would otherwise never be asked at all.
494 fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
495 let mut mine = inherited.clone();
496 mine.widgets.text = mine.widgets.text.merge(self.handed);
497 Some(
498 self.inner
499 .theme_override(&mine)
500 .unwrap_or_else(|| Box::new(mine)),
501 )
502 }
503
504 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
505 self.inner.media_override(inherited)
506 }
507 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
508 self.inner.scaffold_override()
509 }
510 /// Forwarded: a wrapper around a form is still that form (milestone 512).
511 fn autofill_group(&self) -> bool {
512 self.inner.autofill_group()
513 }
514});
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::{build_ui, Container, Runtime, Theme};
520 use frus_core::{Color, Primitive, Rect, Size};
521
522 const RED: Color = Color {
523 r: 1.0,
524 g: 0.0,
525 b: 0.0,
526 a: 1.0,
527 };
528
529 const BLUE: Color = Color {
530 r: 0.0,
531 g: 0.0,
532 b: 1.0,
533 a: 1.0,
534 };
535
536 fn mark() -> Container<()> {
537 Container::new().width(40.0).height(20.0).color(RED)
538 }
539
540 /// Where the blue neighbour was painted, through any layer above it.
541 fn blue_y(primitives: &[Primitive]) -> Option<f32> {
542 for primitive in primitives {
543 match primitive {
544 Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
545 return Some(rect.y)
546 }
547 Primitive::Layer { primitives, .. } => {
548 if let Some(found) = blue_y(primitives) {
549 return Some(found);
550 }
551 }
552 _ => {}
553 }
554 }
555 None
556 }
557
558 /// The first red rectangle, and the opacity of the layer it is inside (`1.0` when
559 /// there is no layer).
560 fn painted(root: &dyn Widget<()>) -> (Rect, f32) {
561 let ui = build_ui(
562 root,
563 Size::new(100.0, 100.0),
564 &Runtime::default(),
565 &Theme::dark(),
566 );
567 fn walk(primitives: &[Primitive], opacity: f32) -> Option<(Rect, f32)> {
568 for primitive in primitives {
569 match primitive {
570 Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
571 return Some((*rect, opacity))
572 }
573 Primitive::Layer {
574 primitives,
575 opacity: layer,
576 ..
577 } => {
578 if let Some(found) = walk(primitives, opacity * layer) {
579 return Some(found);
580 }
581 }
582 _ => {}
583 }
584 }
585 None
586 }
587 walk(ui.scene().primitives(), 1.0).expect("the mark")
588 }
589
590 /// **Nothing, half way, all of it** — the three each of these has to pin.
591 #[test]
592 fn a_fade_is_the_value_it_was_given() {
593 for (given, expected) in [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)] {
594 let root = FadeTransition::new(given, mark());
595 let (_, opacity) = painted(&root);
596 assert!(
597 (opacity - expected).abs() < 1e-3,
598 "at {given}: painted at {opacity}"
599 );
600 }
601 }
602
603 /// A slide is a multiple of the child's **own** box: a 40 px mark slid by half a width
604 /// has moved 20.
605 #[test]
606 fn a_slide_is_a_multiple_of_the_child_s_own_box() {
607 for (given, expected) in [(0.0, 0.0), (0.5, 20.0), (1.0, 40.0)] {
608 let root = crate::Flex::<()>::column().child(SlideTransition::new(given, 0.0, mark()));
609 let (rect, _) = painted(&root);
610 assert!(
611 (rect.x - expected).abs() < 0.5,
612 "at {given}: painted at {}",
613 rect.x
614 );
615 }
616 }
617
618 /// And the sign the name exists for: coming in from the left, nought is a whole width
619 /// outside and one is home.
620 #[test]
621 fn coming_in_from_an_edge_arrives_at_one() {
622 let at = |progress: f32| {
623 let root = crate::Flex::<()>::column().child(SlideTransition::from_edge(
624 SlideFrom::Left,
625 progress,
626 mark(),
627 ));
628 painted(&root).0.x
629 };
630 assert!((at(0.0) + 40.0).abs() < 0.5, "a whole width outside");
631 assert!((at(0.5) + 20.0).abs() < 0.5, "half of one, still outside");
632 assert!(at(1.0).abs() < 0.5, "and home");
633
634 // Each edge sends it the way its name says.
635 let mark_at = |edge: SlideFrom| {
636 let root =
637 crate::Flex::<()>::column().child(SlideTransition::from_edge(edge, 0.0, mark()));
638 let rect = painted(&root).0;
639 (rect.x, rect.y)
640 };
641 assert!(mark_at(SlideFrom::Left).0 < -1.0);
642 assert!(mark_at(SlideFrom::Right).0 > 1.0);
643 assert!(mark_at(SlideFrom::Top).1 < -1.0);
644 assert!(mark_at(SlideFrom::Bottom).1 > 1.0);
645 }
646
647 /// A scale is a **paint** transform: it reaches the screen as a matrix on a composited
648 /// layer, and the box underneath is untouched — so the neighbour below does not move
649 /// however big the child is drawn. A neutral scale costs no layer at all.
650 #[test]
651 fn a_scale_is_paint_and_not_layout() {
652 let built = |scale: f32| {
653 let root = crate::Flex::<()>::column()
654 .width(100.0)
655 .child(ScaleTransition::new(scale, mark()))
656 .child(Container::<()>::new().width(10.0).height(10.0).color(BLUE));
657 let ui = build_ui(
658 &root,
659 Size::new(100.0, 100.0),
660 &Runtime::default(),
661 &Theme::dark(),
662 );
663 let matrix = ui.scene().primitives().iter().find_map(|p| match p {
664 Primitive::Layer {
665 transform: Some(t), ..
666 } => Some(t.affine.m[0]),
667 _ => None,
668 });
669 let neighbour = blue_y(ui.scene().primitives()).expect("the neighbour");
670 (matrix, neighbour)
671 };
672 for (given, expected) in [(0.5, Some(0.5)), (1.0, None), (2.0, Some(2.0))] {
673 let (matrix, neighbour) = built(given);
674 match (matrix, expected) {
675 (Some(m), Some(e)) => {
676 assert!((m - e).abs() < 1e-3, "at {given}: painted at {m}")
677 }
678 (None, None) => {}
679 _ => panic!("at {given}: {matrix:?} where {expected:?} was expected"),
680 }
681 assert!(
682 (neighbour - 20.0).abs() < 0.5,
683 "the neighbour stayed where the layout put it: {neighbour}"
684 );
685 }
686 }
687
688 /// The curve is the framework's own, so the two families agree on what *ease-out, half
689 /// way* means.
690 #[test]
691 fn easing_a_progress_is_the_frameworks_own_curve() {
692 let curve = Curve::ease_out();
693 assert!(eased(0.0, &curve).abs() < 1e-3);
694 assert!((eased(1.0, &curve) - 1.0).abs() < 1e-3);
695 assert_eq!(eased(0.5, &curve), curve.transform(0.5));
696 assert!(
697 eased(0.5, &curve) > 0.5,
698 "an ease-out is ahead at the middle"
699 );
700 assert_eq!(
701 eased(2.0, &curve),
702 eased(1.0, &curve),
703 "and a progress past its end is its end"
704 );
705 }
706
707 // --- A decoration and a text style (milestone 501) ---
708
709 use frus_core::{Border, TextStyle};
710
711 /// Every rectangle painted, in paint order, with its colour — through any layer.
712 fn rects_of(root: &dyn Widget<()>) -> Vec<(Rect, Color)> {
713 let ui = build_ui(
714 root,
715 Size::new(200.0, 200.0),
716 &Runtime::default(),
717 &Theme::dark(),
718 );
719 fn walk(primitives: &[Primitive], out: &mut Vec<(Rect, Color)>) {
720 for primitive in primitives {
721 match primitive {
722 Primitive::Rect { rect, color, .. } => out.push((*rect, *color)),
723 Primitive::Layer { primitives, .. } => walk(primitives, out),
724 _ => {}
725 }
726 }
727 }
728 let mut out = Vec::new();
729 walk(ui.scene().primitives(), &mut out);
730 out
731 }
732
733 /// Every run of words painted: its size and its colour.
734 fn texts_of(root: &dyn Widget<()>) -> Vec<(f32, Color)> {
735 let ui = build_ui(
736 root,
737 Size::new(200.0, 200.0),
738 &Runtime::default(),
739 &Theme::dark(),
740 );
741 fn walk(primitives: &[Primitive], out: &mut Vec<(f32, Color)>) {
742 for primitive in primitives {
743 match primitive {
744 Primitive::Text { size, color, .. } => out.push((*size, *color)),
745 Primitive::Layer { primitives, .. } => walk(primitives, out),
746 _ => {}
747 }
748 }
749 }
750 let mut out = Vec::new();
751 walk(ui.scene().primitives(), &mut out);
752 out
753 }
754
755 fn close(a: Color, b: Color) -> bool {
756 (a.r - b.r).abs() < 1e-4
757 && (a.g - b.g).abs() < 1e-4
758 && (a.b - b.b).abs() < 1e-4
759 && (a.a - b.a).abs() < 1e-4
760 }
761
762 /// **At the start, half-way and at the end — read off the scene**, which is where a
763 /// decoration that moved correctly and never reached the screen would be caught.
764 #[test]
765 fn a_decoration_is_painted_as_it_stands() {
766 for (t, want) in [(0.0, RED), (0.5, RED.lerp(BLUE, 0.5)), (1.0, BLUE)] {
767 let tree = DecoratedBoxTransition::<()>::between(
768 BoxDecoration::filled(RED),
769 BoxDecoration::filled(BLUE),
770 t,
771 Container::new().width(40.0).height(20.0),
772 );
773 let rects = rects_of(&tree);
774 assert_eq!(rects.len(), 1, "at {t}: {rects:?}");
775 assert!(close(rects[0].1, want), "at {t}: {:?}", rects[0].1);
776 }
777 }
778
779 /// **A fill fading out keeps its hue on the screen too**, not only in the arithmetic.
780 #[test]
781 fn a_fading_fill_is_painted_in_its_own_colour() {
782 let tree = DecoratedBoxTransition::<()>::between(
783 BoxDecoration::filled(RED),
784 BoxDecoration::default(),
785 0.5,
786 Container::new().width(40.0).height(20.0),
787 );
788 let rects = rects_of(&tree);
789 assert_eq!(rects.len(), 1, "{rects:?}");
790 assert!(close(rects[0].1, RED.fade(0.5)), "{:?}", rects[0].1);
791 }
792
793 /// **A line that thickens does not move the content.** The child is not inset by the
794 /// border, so a border growing from nothing to eight pixels leaves it where it was on
795 /// every frame — which an inset would not.
796 #[test]
797 fn a_thickening_border_does_not_move_the_child() {
798 let child_at = |t: f32| {
799 let tree = DecoratedBoxTransition::<()>::between(
800 BoxDecoration::default(),
801 BoxDecoration::default().border(Border::new(8.0, BLUE)),
802 t,
803 mark(),
804 );
805 rects_of(&tree)
806 .into_iter()
807 .find(|(_, c)| close(*c, RED))
808 .expect("the child")
809 .0
810 };
811 let (start, end) = (child_at(0.0), child_at(1.0));
812 assert_eq!(
813 (start.x, start.y),
814 (end.x, end.y),
815 "the child stayed put: {start:?} then {end:?}"
816 );
817 }
818
819 /// **In front, it is painted over the child**; behind is the default.
820 #[test]
821 fn in_front_the_decoration_covers_the_child() {
822 let wash = || BoxDecoration::filled(BLUE.fade(0.3));
823 let order = |tree: &DecoratedBoxTransition<()>| {
824 let rects = rects_of(tree);
825 let child = rects.iter().position(|(_, c)| close(*c, RED));
826 let over = rects.iter().position(|(_, c)| c.b > 0.5);
827 (child.expect("the child"), over.expect("the wash"))
828 };
829 let (child, over) = order(
830 &DecoratedBoxTransition::new(wash(), mark()).position(DecorationPosition::Foreground),
831 );
832 assert!(child < over, "in front: the wash comes after the child");
833 let (child, under) = order(&DecoratedBoxTransition::new(wash(), mark()));
834 assert!(under < child, "behind, by default");
835 }
836
837 /// **The words are drawn at the style in flight** — start, half-way and end.
838 #[test]
839 fn a_text_style_is_handed_down_as_it_stands() {
840 for (t, want) in [(0.0, 12.0), (0.5, 18.0), (1.0, 24.0)] {
841 let tree = DefaultTextStyleTransition::<()>::between(
842 TextStyle::NONE.size(12.0),
843 TextStyle::NONE.size(24.0),
844 t,
845 crate::Text::new("Ag"),
846 );
847 let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
848 assert_eq!(sizes, vec![want], "at {t}");
849 }
850 }
851
852 /// **Two nested fuse into one node and hand down both.** An outer one moving the
853 /// colour and an inner one the size: the words wear both, which means the outer asked
854 /// the inner — a transparent wrapper *is* its child, and the inner is never visited as a
855 /// node of its own.
856 #[test]
857 fn two_nested_hand_down_both_halves() {
858 let tree = DefaultTextStyleTransition::<()>::new(
859 TextStyle::NONE.color(BLUE),
860 DefaultTextStyleTransition::new(TextStyle::NONE.size(20.0), crate::Text::new("Ag")),
861 );
862 let texts = texts_of(&tree);
863 assert_eq!(texts.len(), 1, "{texts:?}");
864 assert_eq!(texts[0].0, 20.0, "the inner one's size");
865 assert!(close(texts[0].1, BLUE), "and the outer one's colour");
866 }
867
868 /// A text that names its own size keeps it: an inherited style answers a question, it
869 /// does not overrule an answer.
870 #[test]
871 fn a_text_that_names_its_size_keeps_it() {
872 let tree = DefaultTextStyleTransition::<()>::new(
873 TextStyle::NONE.size(30.0),
874 crate::Text::new("Ag").size(11.0),
875 );
876 let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
877 assert_eq!(sizes, vec![11.0]);
878 }
879}