1use std::sync::atomic::Ordering;
62use std::time::Duration;
63
64use frust_core::{
65 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, Curve, DiscardScene, EditingState,
66 EventCtx, EventResult, ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx,
67 View, Widget, any,
68};
69use frust_theme::Theme;
70use kurbo::{Point, Size, Vec2};
71
72use crate::ChildKey;
73use crate::Timing;
74use crate::motion::patterns::{FadeThrough, PatternLayer, TransitionPattern};
75use crate::nav::navigator::NEXT_SNAPSHOT_KEY;
76use crate::nav::transition::{TransitionDriver, make_driver};
77
78pub const FALLBACK_DURATION: Duration = Duration::from_millis(220);
82
83pub const REDUCE_MOTION_DURATION: Duration = Duration::from_millis(120);
88
89fn resolve_switch_timing(theme: Option<&Theme>, explicit: Option<Timing>) -> Timing {
94 explicit.unwrap_or_else(|| match theme {
95 Some(theme) => Timing::Duration(
96 Duration::from_secs_f64(theme.motion.durations.base / 1000.0),
97 theme.motion.easing.effects,
98 ),
99 None => Timing::Duration(FALLBACK_DURATION, Curve::Emphasized),
100 })
101}
102
103fn paint_staged_child(
120 pod: &mut ChildPod,
121 ctx: &mut PaintCtx,
122 scene: &mut dyn PaintScene,
123 layer: PatternLayer,
124 snapshot: u64,
125) {
126 pod.set_origin(Point::new(layer.dx, layer.dy));
127 let alpha = layer.alpha.clamp(0.0, 1.0);
128
129 if alpha <= 0.0 {
130 let mut sink = DiscardScene;
131 pod.paint_child(ctx, &mut sink);
132 return;
133 }
134
135 let bracket_origin = ctx.origin() + Vec2::new(layer.dx, layer.dy);
136 scene.push_snapshot(snapshot, bracket_origin, ctx.size(), alpha, layer.scale);
137 pod.paint_child(ctx, scene);
138 scene.pop_snapshot();
139}
140
141fn cleared_ime_state() -> ImeState {
145 ImeState {
146 active: false,
147 editing: EditingState {
148 text: String::new(),
149 selection_base: -1,
150 selection_extent: -1,
151 composing_base: -1,
152 composing_extent: -1,
153 },
154 caret: None,
155 content_type: Default::default(),
156 suppress_soft_keyboard: false,
157 }
158}
159
160pub struct PatternSwitcherView<State: 'static, P: TransitionPattern + Clone + 'static> {
162 key: ChildKey,
163 pattern: P,
164 timing: Option<Timing>,
165 reverse: bool,
166 child: AnyView<State>,
167}
168
169pub fn pattern_switcher<State, P, V>(
173 key: impl Into<ChildKey>,
174 pattern: P,
175 child: V,
176) -> PatternSwitcherView<State, P>
177where
178 State: 'static,
179 P: TransitionPattern + Clone + 'static,
180 V: View<State>,
181{
182 PatternSwitcherView {
183 key: key.into(),
184 pattern,
185 timing: None,
186 reverse: false,
187 child: any(child),
188 }
189}
190
191#[allow(non_snake_case)]
194pub fn PatternSwitcher<State, P, V>(
195 key: impl Into<ChildKey>,
196 pattern: P,
197 child: V,
198) -> PatternSwitcherView<State, P>
199where
200 State: 'static,
201 P: TransitionPattern + Clone + 'static,
202 V: View<State>,
203{
204 pattern_switcher(key, pattern, child)
205}
206
207impl<State: 'static, P: TransitionPattern + Clone + 'static> PatternSwitcherView<State, P> {
208 pub fn timing(mut self, timing: Timing) -> Self {
210 self.timing = Some(timing);
211 self
212 }
213
214 pub fn reverse(mut self, reverse: bool) -> Self {
217 self.reverse = reverse;
218 self
219 }
220}
221
222pub struct PatternSwitcherWidget<P: TransitionPattern + Clone + 'static> {
227 key: ChildKey,
228 child: ChildPod,
229 exiting: Option<ChildPod>,
232 pattern: P,
233 explicit_timing: Option<Timing>,
234 reverse: bool,
235 driver: Option<TransitionDriver>,
239 pending_start: bool,
242 settled: bool,
245 reduce_motion: bool,
248 needs_ime_clear: bool,
251 snapshot_base: u64,
261}
262
263impl<State: 'static, P: TransitionPattern + Clone + 'static> View<State>
264 for PatternSwitcherView<State, P>
265{
266 type Element = PatternSwitcherWidget<P>;
267
268 fn build(&self, ctx: &mut BuildCtx<'_>) -> PatternSwitcherWidget<P> {
269 PatternSwitcherWidget {
270 key: self.key,
271 child: crate::authoring::build_child(&self.child, ctx),
272 exiting: None,
273 pattern: self.pattern.clone(),
274 explicit_timing: self.timing,
275 reverse: self.reverse,
276 driver: None,
277 pending_start: false,
278 settled: false,
279 reduce_motion: false,
280 needs_ime_clear: false,
281 snapshot_base: NEXT_SNAPSHOT_KEY.fetch_add(2, Ordering::Relaxed),
282 }
283 }
284
285 fn rebuild(
286 &self,
287 prev: &Self,
288 element: &mut PatternSwitcherWidget<P>,
289 ctx: &mut BuildCtx<'_>,
290 ) -> ChangeFlags {
291 let mut flags = ChangeFlags::NONE;
292
293 element.pattern = self.pattern.clone();
297 if element.explicit_timing != self.timing {
298 element.explicit_timing = self.timing;
299 flags |= ChangeFlags::PAINT;
300 }
301 if element.reverse != self.reverse {
302 element.reverse = self.reverse;
303 flags |= ChangeFlags::PAINT;
304 }
305
306 if element.settled {
310 element.exiting = None;
311 element.driver = None;
312 element.settled = false;
313 }
314
315 if self.key != element.key {
316 element.exiting = None;
319 element.driver = None;
320 element.reduce_motion = false;
321
322 let mut old = std::mem::replace(
325 &mut element.child,
326 crate::authoring::build_child(&self.child, ctx),
327 );
328
329 if old.is_active() {
332 crate::authoring::cancel_pod(&mut old);
333 old.set_active(false);
334 }
335 if old.is_focused() {
336 old.set_focused(false);
337 if ctx.has_focus() {
354 element.needs_ime_clear = true;
355 }
356 }
357
358 element.exiting = Some(old);
359 element.key = self.key;
360 element.pending_start = true;
361 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
362 } else {
363 flags |=
365 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
366 }
367
368 flags
369 }
370
371 fn teardown(&self, element: &mut PatternSwitcherWidget<P>, ctx: &mut BuildCtx<'_>) {
372 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
373 element.exiting = None;
377 }
378}
379
380impl<P: TransitionPattern + Clone + 'static> Widget for PatternSwitcherWidget<P> {
381 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
382 let size = self.child.layout_child(ctx, bc);
383 self.child.set_origin(Point::ZERO);
384 if let Some(exiting) = &mut self.exiting {
388 exiting.layout_child(ctx, bc);
389 exiting.set_origin(Point::ZERO);
390 }
391 size
392 }
393
394 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
395 if self.pending_start {
398 let theme = Theme::from_paint_ctx(ctx);
399 let reduce = theme.map(|t| t.motion.reduce_motion).unwrap_or(false);
400 let timing = if reduce {
401 Timing::Duration(REDUCE_MOTION_DURATION, Curve::Linear)
402 } else {
403 resolve_switch_timing(theme, self.explicit_timing)
404 };
405 self.driver = Some(make_driver(timing).0);
406 self.reduce_motion = reduce;
407 self.pending_start = false;
408 }
409
410 if let (Some(exiting), Some(driver)) = (self.exiting.as_mut(), self.driver.as_mut()) {
411 let adv = driver.advance(ctx.frame_time());
412 let size = ctx.size();
413 let (incoming_layer, exiting_layer) = if self.reduce_motion {
415 FadeThrough.resolve(adv.value, self.reverse, size)
416 } else {
417 self.pattern.resolve(adv.value, self.reverse, size)
418 };
419 paint_staged_child(exiting, ctx, scene, exiting_layer, self.snapshot_base + 1);
423 paint_staged_child(
424 &mut self.child,
425 ctx,
426 scene,
427 incoming_layer,
428 self.snapshot_base,
429 );
430
431 if adv.animating {
432 ctx.request_frame();
433 }
434 if adv.done {
435 self.settled = true;
438 ctx.request_frame();
439 }
440 } else {
441 self.child.paint_child(ctx, scene);
442 }
443
444 if self.needs_ime_clear {
447 ctx.publish_ime_state(cleared_ime_state());
448 self.needs_ime_clear = false;
449 }
450 }
451
452 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
453 crate::authoring::route_event_single(&mut self.child, ctx, event)
456 }
457
458 fn semantics(&self, ctx: &mut SemanticsCtx) {
459 self.child.semantics_child(ctx);
462 }
463
464 crate::authoring::visit_children!(exiting, child);
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::motion::patterns::{FadeScale, SharedAxis};
471 use crate::test_support::{RecordingScene, leaf_any};
472 use crate::{FlexView, column};
473 use frust_core::{FrameTime, PointerButton, PointerEvent, PointerPhase, RenderRoot};
474 use kurbo::Rect;
475 use std::cell::Cell;
476 use std::rc::Rc;
477
478 fn ft_ms(ms: f64) -> FrameTime {
479 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
480 }
481
482 fn build<S: 'static, V: View<S>>(view: &V) -> V::Element {
483 let mut counter = 0u64;
484 view.build(&mut BuildCtx::new(&mut counter))
485 }
486
487 fn down(x: f64, y: f64) -> InputEvent {
488 InputEvent::Pointer(PointerEvent {
489 phase: PointerPhase::Down,
490 position: Point::new(x, y),
491 button: PointerButton::Primary,
492 })
493 }
494
495 fn loose() -> BoxConstraints {
496 BoxConstraints::loose(Size::new(100.0, 100.0))
497 }
498
499 #[derive(Default)]
507 struct SnapshotRecordingScene {
508 rects: Vec<(Point, Size)>,
509 layers: Vec<(Point, Size, f32)>,
510 transform_pushes: u32,
511 snapshots: Vec<(u64, Point, Size, f32, f64)>,
512 snapshot_pops: u32,
513 }
514 impl PaintScene for SnapshotRecordingScene {
515 fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
516 self.rects.push((origin, size));
517 }
518 fn draw_text(&mut self, _origin: Point, _text: &str) {}
519 fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
520 self.layers.push((origin, size, alpha));
521 }
522 fn push_transform(&mut self, _transform: kurbo::Affine) {
523 self.transform_pushes += 1;
524 }
525 fn push_snapshot(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
526 self.snapshots.push((key, origin, size, alpha, scale));
527 }
528 fn pop_snapshot(&mut self) {
529 self.snapshot_pops += 1;
530 }
531 }
532
533 struct Recorder {
538 cancelled: Rc<Cell<bool>>,
539 torn: Rc<Cell<u32>>,
540 }
541 struct RecorderWidget {
542 cancelled: Rc<Cell<bool>>,
543 torn: Rc<Cell<u32>>,
544 }
545 impl View<()> for Recorder {
546 type Element = RecorderWidget;
547 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
548 RecorderWidget {
549 cancelled: self.cancelled.clone(),
550 torn: self.torn.clone(),
551 }
552 }
553 fn rebuild(
554 &self,
555 _prev: &Self,
556 _element: &mut RecorderWidget,
557 _ctx: &mut BuildCtx<'_>,
558 ) -> ChangeFlags {
559 ChangeFlags::NONE
560 }
561 }
562 impl Widget for RecorderWidget {
563 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
564 bc.max()
565 }
566 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
567 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
568 if let InputEvent::Pointer(p) = event {
569 match p.phase {
570 PointerPhase::Down => {
571 ctx.capture_pointer();
572 ctx.request_focus();
573 return EventResult::Handled;
574 }
575 PointerPhase::Cancel => {
578 self.cancelled.set(true);
579 return EventResult::Handled;
580 }
581 _ => {}
582 }
583 }
584 EventResult::Ignored
585 }
586 }
587 impl Drop for RecorderWidget {
588 fn drop(&mut self) {
589 self.torn.set(self.torn.get() + 1);
590 }
591 }
592
593 fn recorder(cancelled: &Rc<Cell<bool>>, torn: &Rc<Cell<u32>>) -> Recorder {
594 Recorder {
595 cancelled: cancelled.clone(),
596 torn: torn.clone(),
597 }
598 }
599
600 fn reduced_motion_theme() -> Theme {
601 let mut theme = Theme::neutral();
602 theme.motion.reduce_motion = true;
603 theme
604 }
605
606 #[test]
609 fn same_identity_rebuild_does_not_transition_but_changed_identity_does() {
610 let v1: PatternSwitcherView<(), _> =
611 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
612 let mut w = build(&v1);
613 assert!(w.exiting.is_none());
614
615 let v2: PatternSwitcherView<(), _> =
617 pattern_switcher(1u32, FadeThrough, leaf_any(20.0, 20.0));
618 let mut counter = 0u64;
619 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
620 assert!(
621 w.exiting.is_none(),
622 "same identity must not stage a transition"
623 );
624 assert!(!w.pending_start);
625
626 let v3: PatternSwitcherView<(), _> =
628 pattern_switcher(2u32, FadeThrough, leaf_any(30.0, 30.0));
629 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
630 assert!(
631 w.exiting.is_some(),
632 "an identity change must retain the exiting child"
633 );
634 assert!(w.pending_start, "the next paint starts the driver");
635 assert_eq!(w.key, 2u32.into());
636 }
637
638 #[test]
641 fn identity_change_cancels_focus_and_clears_ime_on_the_exiting_child() {
642 let cancelled = Rc::new(Cell::new(false));
643 let torn = Rc::new(Cell::new(0u32));
644 let v1: PatternSwitcherView<(), _> =
645 pattern_switcher(1u32, FadeThrough, recorder(&cancelled, &torn));
646 let mut w = build(&v1);
647 let size = w.layout(&mut LayoutCtx::new(), &loose());
648
649 let mut st = ();
651 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
652 w.event(&mut ectx, &down(5.0, 5.0));
653 assert!(w.child.is_active(), "the child captured the pointer");
654 assert!(w.child.is_focused(), "the child took focus");
655
656 let v2: PatternSwitcherView<(), _> =
659 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
660 let mut counter = 0u64;
661 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
662
663 assert!(
664 cancelled.get(),
665 "the exiting child received a synthetic Cancel"
666 );
667 let exiting = w.exiting.as_ref().expect("exiting child retained");
668 assert!(
669 !exiting.is_focused(),
670 "the exiting child's focus was cleared"
671 );
672 assert!(!exiting.is_active(), "its capture path was released");
673 assert!(
674 w.needs_ime_clear,
675 "an IME clear is queued for the next paint"
676 );
677
678 let mut pctx = PaintCtx::new(Point::ZERO, size);
680 let mut scene = RecordingScene::default();
681 w.paint(&mut pctx, &mut scene);
682 let ime = pctx
683 .take_ime_state()
684 .expect("a cleared IME surface was published on the post-switch paint");
685 assert!(!ime.active);
686 assert!(ime.editing.text.is_empty());
687 assert!(!w.needs_ime_clear, "the queued clear was consumed");
688 }
689
690 fn field_surface() -> ImeState {
695 ImeState {
696 active: true,
697 editing: EditingState {
698 text: "query".to_string(),
699 selection_base: 5,
700 selection_extent: 5,
701 composing_base: -1,
702 composing_extent: -1,
703 },
704 caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
705 content_type: Default::default(),
706 suppress_soft_keyboard: false,
707 }
708 }
709
710 struct PersistentField {
716 size: Size,
717 }
718 struct PersistentFieldWidget {
719 size: Size,
720 }
721 impl<S: 'static> View<S> for PersistentField {
722 type Element = PersistentFieldWidget;
723 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PersistentFieldWidget {
724 PersistentFieldWidget { size: self.size }
725 }
726 fn rebuild(
727 &self,
728 _prev: &Self,
729 _element: &mut PersistentFieldWidget,
730 _ctx: &mut BuildCtx<'_>,
731 ) -> ChangeFlags {
732 ChangeFlags::NONE
733 }
734 }
735 impl Widget for PersistentFieldWidget {
736 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
737 bc.constrain(self.size)
738 }
739 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
740 if ctx.has_focus() {
741 ctx.publish_ime_state(field_surface());
742 }
743 }
744 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
745 if let InputEvent::Pointer(p) = event
746 && p.phase == PointerPhase::Down
747 {
748 ctx.request_focus();
749 ctx.publish_ime_state(field_surface());
750 return EventResult::Handled;
751 }
752 EventResult::Ignored
753 }
754 }
755
756 #[test]
757 fn identity_change_in_an_unfocused_switcher_leaves_a_sibling_fields_session_alive() {
758 let key = Rc::new(Cell::new(1u32));
763 let mut app = {
764 let key = key.clone();
765 move |_: &mut ()| {
766 column()
767 .child(PersistentField {
768 size: Size::new(100.0, 40.0),
769 })
770 .child(pattern_switcher(
771 key.get(),
772 FadeThrough,
773 leaf_any(100.0, 40.0),
774 ))
775 }
776 };
777 let mut root: RenderRoot<(), FlexView<()>> = RenderRoot::new();
778 let mut state = ();
779 root.rebuild(&mut app, &mut state);
780 root.layout(Size::new(100.0, 100.0));
781 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
782
783 root.event(&mut state, &down(5.0, 5.0));
786 assert!(
787 root.is_focus_active(),
788 "the sibling field owns the focus session"
789 );
790 assert_eq!(
791 root.ime_state().map(|s| s.editing.text),
792 Some("query".to_string()),
793 "…and the shell-facing surface is the field's"
794 );
795 let generation = root.focus_ime_generation();
796
797 key.set(2);
800 root.rebuild(&mut app, &mut state);
801 root.layout(Size::new(100.0, 100.0));
802 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
803
804 assert!(
805 root.is_focus_active(),
806 "an identity change in an unfocused switcher must not release the \
807 sibling field's session"
808 );
809 let ime = root
810 .ime_state()
811 .expect("the identity change dropped the sibling field's IME surface entirely");
812 assert!(ime.active, "the field's surface was left inactive");
813 assert_eq!(
814 ime.editing.text, "query",
815 "the field's surface was replaced with the switcher's cleared one"
816 );
817 assert_eq!(
818 root.focus_ime_generation(),
819 generation,
820 "an identity change in an unfocused switcher is not a focus/IME edge"
821 );
822 }
823
824 #[test]
827 fn exiting_child_is_retained_until_settle_then_torn_down() {
828 let cancelled = Rc::new(Cell::new(false));
829 let torn = Rc::new(Cell::new(0u32));
830 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
831 let v1: PatternSwitcherView<(), _> =
832 pattern_switcher(1u32, FadeThrough, recorder(&cancelled, &torn)).timing(short);
833 let mut w = build(&v1);
834 let size = w.layout(&mut LayoutCtx::new(), &loose());
835
836 let v2: PatternSwitcherView<(), _> =
838 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
839 let mut counter = 0u64;
840 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
841 assert!(w.exiting.is_some());
842 assert_eq!(
843 torn.get(),
844 0,
845 "exiting not torn down while the transition runs"
846 );
847
848 w.paint(
850 &mut PaintCtx::new(Point::ZERO, size),
851 &mut RecordingScene::default(),
852 );
853 assert!(w.exiting.is_some(), "retained during the transition");
854 assert_eq!(torn.get(), 0);
855 assert!(!w.settled);
856
857 w.driver
860 .as_mut()
861 .expect("driver created on first paint")
862 .advance(ft_ms(10_000.0));
863
864 w.paint(
866 &mut PaintCtx::new(Point::ZERO, size),
867 &mut RecordingScene::default(),
868 );
869 assert!(w.settled, "paint flags settle once the driver reaches rest");
870 assert!(
871 w.exiting.is_some(),
872 "still retained until the finalize rebuild"
873 );
874 assert_eq!(torn.get(), 0);
875
876 let v3: PatternSwitcherView<(), _> =
878 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
879 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
880 assert!(w.exiting.is_none(), "exiting dropped after settle");
881 assert_eq!(
882 torn.get(),
883 1,
884 "exiting torn down exactly once, after progress 1.0"
885 );
886 assert!(!w.settled);
887 assert!(w.driver.is_none());
888 }
889
890 #[test]
893 fn reduce_motion_collapses_to_fast_crossfade() {
894 let theme = reduced_motion_theme();
895 let v1: PatternSwitcherView<(), _> =
899 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
900 let mut w = build(&v1);
901 let size = w.layout(&mut LayoutCtx::new(), &loose());
902
903 let v2: PatternSwitcherView<(), _> =
904 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
905 let mut counter = 0u64;
906 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
907
908 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
909 w.paint(&mut pctx, &mut RecordingScene::default());
910
911 assert!(
912 w.reduce_motion,
913 "reduce_motion must be resolved from the threaded theme"
914 );
915 assert_eq!(
918 w.child.origin(),
919 Point::ZERO,
920 "reduce_motion collapses to a non-directional crossfade (no slide)"
921 );
922 }
923
924 #[test]
925 fn without_reduce_motion_the_configured_directional_pattern_slides() {
926 let v1: PatternSwitcherView<(), _> =
929 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
930 let mut w = build(&v1);
931 let size = w.layout(&mut LayoutCtx::new(), &loose());
932
933 let v2: PatternSwitcherView<(), _> =
934 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
935 let mut counter = 0u64;
936 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
937
938 w.paint(
939 &mut PaintCtx::new(Point::ZERO, size),
940 &mut RecordingScene::default(),
941 );
942 assert!(!w.reduce_motion);
943 assert_eq!(w.child.origin().x, 30.0);
946 }
947
948 #[test]
951 fn steady_state_paints_the_single_child_with_no_layer_or_transform() {
952 let v: PatternSwitcherView<(), _> =
953 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
954 let mut w = build(&v);
955 let size = w.layout(&mut LayoutCtx::new(), &loose());
956
957 let mut scene = RecordingScene::default();
958 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
959 assert_eq!(scene.rects.len(), 1, "the single child paints its fill");
960 assert!(
961 scene.layers.is_empty(),
962 "no crossfade layer at steady state"
963 );
964 assert!(
965 scene.transforms.is_empty(),
966 "no scale transform at steady state"
967 );
968 }
969
970 #[test]
975 fn alpha_zero_staged_child_records_no_ops_and_no_zero_alpha_layer() {
976 let leaf = leaf_any(10.0, 10.0);
977 let mut counter = 0u64;
978 let mut pod = crate::authoring::build_child(&leaf, &mut BuildCtx::new(&mut counter));
979 let size = pod.layout_child(&mut LayoutCtx::new(), &loose());
980 pod.set_origin(Point::ZERO);
981
982 let mut scene = RecordingScene::default();
983 let layer = PatternLayer {
984 dx: 0.0,
985 dy: 0.0,
986 alpha: 0.0,
987 scale: 1.0,
988 };
989 paint_staged_child(
990 &mut pod,
991 &mut PaintCtx::new(Point::ZERO, size),
992 &mut scene,
993 layer,
994 1,
995 );
996
997 assert!(scene.rects.is_empty(), "an alpha-0 child records no fill");
998 assert!(
999 scene.layers.is_empty(),
1000 "no zero-alpha push_layer is recorded either"
1001 );
1002 }
1003
1004 #[test]
1010 fn staged_children_paint_through_a_snapshot_bracket_keyed_by_snapshot_base() {
1011 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1016 let v1: PatternSwitcherView<(), _> =
1017 pattern_switcher(1u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
1018 let mut w = build(&v1);
1019 let size = w.layout(&mut LayoutCtx::new(), &loose());
1020
1021 let v2: PatternSwitcherView<(), _> =
1022 pattern_switcher(2u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
1023 let mut counter = 0u64;
1024 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1025
1026 w.paint(
1028 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1029 &mut SnapshotRecordingScene::default(),
1030 );
1031
1032 let base = w.snapshot_base;
1035 let mut scene = SnapshotRecordingScene::default();
1036 w.paint(
1037 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
1038 &mut scene,
1039 );
1040
1041 assert_eq!(
1042 scene.snapshots.len(),
1043 2,
1044 "both the incoming and frozen exiting child paint through a \
1045 snapshot bracket: {:?}",
1046 scene.snapshots
1047 );
1048 assert_eq!(scene.snapshot_pops, 2, "each bracket pops exactly once");
1049 assert!(
1050 scene.layers.is_empty(),
1051 "no push_layer bracket of its own: {:?}",
1052 scene.layers
1053 );
1054 assert_eq!(
1055 scene.transform_pushes, 0,
1056 "no push_transform bracket of its own"
1057 );
1058
1059 let keys: Vec<u64> = scene.snapshots.iter().map(|(k, ..)| *k).collect();
1060 assert!(
1061 keys.contains(&base),
1062 "the incoming child is keyed by snapshot_base: {keys:?}"
1063 );
1064 assert!(
1065 keys.contains(&(base + 1)),
1066 "the frozen exiting child is keyed by snapshot_base + 1: {keys:?}"
1067 );
1068 for (_, _, _, alpha, _) in &scene.snapshots {
1069 assert!(*alpha > 0.0, "an alpha-0 child never reaches the bracket");
1070 }
1071 }
1072
1073 #[test]
1074 fn bracket_origin_follows_the_pod_dx_in_slide_patterns() {
1075 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1081 let v1: PatternSwitcherView<(), _> =
1082 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
1083 let mut w = build(&v1);
1084 let size = w.layout(&mut LayoutCtx::new(), &loose());
1085
1086 let v2: PatternSwitcherView<(), _> =
1087 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
1088 let mut counter = 0u64;
1089 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1090
1091 w.paint(
1093 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1094 &mut SnapshotRecordingScene::default(),
1095 );
1096
1097 let mut scene = SnapshotRecordingScene::default();
1102 w.paint(
1103 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1104 &mut scene,
1105 );
1106
1107 assert_eq!(
1108 scene.snapshots.len(),
1109 1,
1110 "only the exiting child is visible at p=0"
1111 );
1112
1113 let (_, origin, _, _, _) = scene.snapshots[0];
1116 assert_eq!(
1117 origin,
1118 Point::new(0.0, 0.0),
1119 "exiting child bracket origin at x=0.0 when dx=0 (was {:?})",
1120 origin
1121 );
1122
1123 let mut scene_mid = SnapshotRecordingScene::default();
1132 w.paint(
1133 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)),
1134 &mut scene_mid,
1135 );
1136
1137 assert_eq!(
1140 scene_mid.snapshots.len(),
1141 1,
1142 "at mid-frame (p=0.5), only one child is visible through a snapshot"
1143 );
1144 let (_, origin_mid, _, _, _) = scene_mid.snapshots[0];
1145 assert_eq!(
1146 origin_mid,
1147 Point::new(15.0, 0.0),
1148 "incoming child bracket origin at x=15.0 when dx=15.0 (was {:?})",
1149 origin_mid
1150 );
1151 }
1152
1153 #[test]
1157 fn snapshot_base_is_stable_across_multiple_switches_on_the_same_widget() {
1158 let v1: PatternSwitcherView<(), _> =
1159 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1160 let mut w = build(&v1);
1161 let base = w.snapshot_base;
1162 let mut counter = 0u64;
1163
1164 let v2: PatternSwitcherView<(), _> =
1165 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
1166 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1167 assert_eq!(
1168 w.snapshot_base, base,
1169 "a switch does not reassign the key pair"
1170 );
1171
1172 let v3: PatternSwitcherView<(), _> =
1173 pattern_switcher(3u32, FadeThrough, leaf_any(10.0, 10.0));
1174 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1175 assert_eq!(
1176 w.snapshot_base, base,
1177 "a second switch on the same widget still keeps the same key pair"
1178 );
1179 }
1180
1181 #[test]
1184 fn two_switcher_instances_get_disjoint_snapshot_key_pairs() {
1185 let v: PatternSwitcherView<(), _> =
1186 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1187 let w1 = build(&v);
1188 let w2 = build(&v);
1189 assert_ne!(
1190 w1.snapshot_base, w2.snapshot_base,
1191 "two switcher instances never share a snapshot key pair"
1192 );
1193 }
1194}