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::{Column, FlexView};
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, any(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(vec![
767 any(PersistentField {
768 size: Size::new(100.0, 40.0),
769 }),
770 any(pattern_switcher(
771 key.get(),
772 FadeThrough,
773 leaf_any(100.0, 40.0),
774 )),
775 ])
776 }
777 };
778 let mut root: RenderRoot<(), FlexView<()>> = RenderRoot::new();
779 let mut state = ();
780 root.rebuild(&mut app, &mut state);
781 root.layout(Size::new(100.0, 100.0));
782 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
783
784 root.event(&mut state, &down(5.0, 5.0));
787 assert!(
788 root.is_focus_active(),
789 "the sibling field owns the focus session"
790 );
791 assert_eq!(
792 root.ime_state().map(|s| s.editing.text),
793 Some("query".to_string()),
794 "…and the shell-facing surface is the field's"
795 );
796 let generation = root.focus_ime_generation();
797
798 key.set(2);
801 root.rebuild(&mut app, &mut state);
802 root.layout(Size::new(100.0, 100.0));
803 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
804
805 assert!(
806 root.is_focus_active(),
807 "an identity change in an unfocused switcher must not release the \
808 sibling field's session"
809 );
810 let ime = root
811 .ime_state()
812 .expect("the identity change dropped the sibling field's IME surface entirely");
813 assert!(ime.active, "the field's surface was left inactive");
814 assert_eq!(
815 ime.editing.text, "query",
816 "the field's surface was replaced with the switcher's cleared one"
817 );
818 assert_eq!(
819 root.focus_ime_generation(),
820 generation,
821 "an identity change in an unfocused switcher is not a focus/IME edge"
822 );
823 }
824
825 #[test]
828 fn exiting_child_is_retained_until_settle_then_torn_down() {
829 let cancelled = Rc::new(Cell::new(false));
830 let torn = Rc::new(Cell::new(0u32));
831 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
832 let v1: PatternSwitcherView<(), _> =
833 pattern_switcher(1u32, FadeThrough, any(recorder(&cancelled, &torn))).timing(short);
834 let mut w = build(&v1);
835 let size = w.layout(&mut LayoutCtx::new(), &loose());
836
837 let v2: PatternSwitcherView<(), _> =
839 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
840 let mut counter = 0u64;
841 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
842 assert!(w.exiting.is_some());
843 assert_eq!(
844 torn.get(),
845 0,
846 "exiting not torn down while the transition runs"
847 );
848
849 w.paint(
851 &mut PaintCtx::new(Point::ZERO, size),
852 &mut RecordingScene::default(),
853 );
854 assert!(w.exiting.is_some(), "retained during the transition");
855 assert_eq!(torn.get(), 0);
856 assert!(!w.settled);
857
858 w.driver
861 .as_mut()
862 .expect("driver created on first paint")
863 .advance(ft_ms(10_000.0));
864
865 w.paint(
867 &mut PaintCtx::new(Point::ZERO, size),
868 &mut RecordingScene::default(),
869 );
870 assert!(w.settled, "paint flags settle once the driver reaches rest");
871 assert!(
872 w.exiting.is_some(),
873 "still retained until the finalize rebuild"
874 );
875 assert_eq!(torn.get(), 0);
876
877 let v3: PatternSwitcherView<(), _> =
879 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
880 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
881 assert!(w.exiting.is_none(), "exiting dropped after settle");
882 assert_eq!(
883 torn.get(),
884 1,
885 "exiting torn down exactly once, after progress 1.0"
886 );
887 assert!(!w.settled);
888 assert!(w.driver.is_none());
889 }
890
891 #[test]
894 fn reduce_motion_collapses_to_fast_crossfade() {
895 let theme = reduced_motion_theme();
896 let v1: PatternSwitcherView<(), _> =
900 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
901 let mut w = build(&v1);
902 let size = w.layout(&mut LayoutCtx::new(), &loose());
903
904 let v2: PatternSwitcherView<(), _> =
905 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
906 let mut counter = 0u64;
907 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
908
909 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
910 w.paint(&mut pctx, &mut RecordingScene::default());
911
912 assert!(
913 w.reduce_motion,
914 "reduce_motion must be resolved from the threaded theme"
915 );
916 assert_eq!(
919 w.child.origin(),
920 Point::ZERO,
921 "reduce_motion collapses to a non-directional crossfade (no slide)"
922 );
923 }
924
925 #[test]
926 fn without_reduce_motion_the_configured_directional_pattern_slides() {
927 let v1: PatternSwitcherView<(), _> =
930 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
931 let mut w = build(&v1);
932 let size = w.layout(&mut LayoutCtx::new(), &loose());
933
934 let v2: PatternSwitcherView<(), _> =
935 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
936 let mut counter = 0u64;
937 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
938
939 w.paint(
940 &mut PaintCtx::new(Point::ZERO, size),
941 &mut RecordingScene::default(),
942 );
943 assert!(!w.reduce_motion);
944 assert_eq!(w.child.origin().x, 30.0);
947 }
948
949 #[test]
952 fn steady_state_paints_the_single_child_with_no_layer_or_transform() {
953 let v: PatternSwitcherView<(), _> =
954 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
955 let mut w = build(&v);
956 let size = w.layout(&mut LayoutCtx::new(), &loose());
957
958 let mut scene = RecordingScene::default();
959 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
960 assert_eq!(scene.rects.len(), 1, "the single child paints its fill");
961 assert!(
962 scene.layers.is_empty(),
963 "no crossfade layer at steady state"
964 );
965 assert!(
966 scene.transforms.is_empty(),
967 "no scale transform at steady state"
968 );
969 }
970
971 #[test]
976 fn alpha_zero_staged_child_records_no_ops_and_no_zero_alpha_layer() {
977 let leaf = leaf_any(10.0, 10.0);
978 let mut counter = 0u64;
979 let mut pod = crate::authoring::build_child(&leaf, &mut BuildCtx::new(&mut counter));
980 let size = pod.layout_child(&mut LayoutCtx::new(), &loose());
981 pod.set_origin(Point::ZERO);
982
983 let mut scene = RecordingScene::default();
984 let layer = PatternLayer {
985 dx: 0.0,
986 dy: 0.0,
987 alpha: 0.0,
988 scale: 1.0,
989 };
990 paint_staged_child(
991 &mut pod,
992 &mut PaintCtx::new(Point::ZERO, size),
993 &mut scene,
994 layer,
995 1,
996 );
997
998 assert!(scene.rects.is_empty(), "an alpha-0 child records no fill");
999 assert!(
1000 scene.layers.is_empty(),
1001 "no zero-alpha push_layer is recorded either"
1002 );
1003 }
1004
1005 #[test]
1011 fn staged_children_paint_through_a_snapshot_bracket_keyed_by_snapshot_base() {
1012 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1017 let v1: PatternSwitcherView<(), _> =
1018 pattern_switcher(1u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
1019 let mut w = build(&v1);
1020 let size = w.layout(&mut LayoutCtx::new(), &loose());
1021
1022 let v2: PatternSwitcherView<(), _> =
1023 pattern_switcher(2u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
1024 let mut counter = 0u64;
1025 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1026
1027 w.paint(
1029 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1030 &mut SnapshotRecordingScene::default(),
1031 );
1032
1033 let base = w.snapshot_base;
1036 let mut scene = SnapshotRecordingScene::default();
1037 w.paint(
1038 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
1039 &mut scene,
1040 );
1041
1042 assert_eq!(
1043 scene.snapshots.len(),
1044 2,
1045 "both the incoming and frozen exiting child paint through a \
1046 snapshot bracket: {:?}",
1047 scene.snapshots
1048 );
1049 assert_eq!(scene.snapshot_pops, 2, "each bracket pops exactly once");
1050 assert!(
1051 scene.layers.is_empty(),
1052 "no push_layer bracket of its own: {:?}",
1053 scene.layers
1054 );
1055 assert_eq!(
1056 scene.transform_pushes, 0,
1057 "no push_transform bracket of its own"
1058 );
1059
1060 let keys: Vec<u64> = scene.snapshots.iter().map(|(k, ..)| *k).collect();
1061 assert!(
1062 keys.contains(&base),
1063 "the incoming child is keyed by snapshot_base: {keys:?}"
1064 );
1065 assert!(
1066 keys.contains(&(base + 1)),
1067 "the frozen exiting child is keyed by snapshot_base + 1: {keys:?}"
1068 );
1069 for (_, _, _, alpha, _) in &scene.snapshots {
1070 assert!(*alpha > 0.0, "an alpha-0 child never reaches the bracket");
1071 }
1072 }
1073
1074 #[test]
1075 fn bracket_origin_follows_the_pod_dx_in_slide_patterns() {
1076 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1082 let v1: PatternSwitcherView<(), _> =
1083 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
1084 let mut w = build(&v1);
1085 let size = w.layout(&mut LayoutCtx::new(), &loose());
1086
1087 let v2: PatternSwitcherView<(), _> =
1088 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
1089 let mut counter = 0u64;
1090 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1091
1092 w.paint(
1094 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1095 &mut SnapshotRecordingScene::default(),
1096 );
1097
1098 let mut scene = SnapshotRecordingScene::default();
1103 w.paint(
1104 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1105 &mut scene,
1106 );
1107
1108 assert_eq!(
1109 scene.snapshots.len(),
1110 1,
1111 "only the exiting child is visible at p=0"
1112 );
1113
1114 let (_, origin, _, _, _) = scene.snapshots[0];
1117 assert_eq!(
1118 origin,
1119 Point::new(0.0, 0.0),
1120 "exiting child bracket origin at x=0.0 when dx=0 (was {:?})",
1121 origin
1122 );
1123
1124 let mut scene_mid = SnapshotRecordingScene::default();
1133 w.paint(
1134 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)),
1135 &mut scene_mid,
1136 );
1137
1138 assert_eq!(
1141 scene_mid.snapshots.len(),
1142 1,
1143 "at mid-frame (p=0.5), only one child is visible through a snapshot"
1144 );
1145 let (_, origin_mid, _, _, _) = scene_mid.snapshots[0];
1146 assert_eq!(
1147 origin_mid,
1148 Point::new(15.0, 0.0),
1149 "incoming child bracket origin at x=15.0 when dx=15.0 (was {:?})",
1150 origin_mid
1151 );
1152 }
1153
1154 #[test]
1158 fn snapshot_base_is_stable_across_multiple_switches_on_the_same_widget() {
1159 let v1: PatternSwitcherView<(), _> =
1160 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1161 let mut w = build(&v1);
1162 let base = w.snapshot_base;
1163 let mut counter = 0u64;
1164
1165 let v2: PatternSwitcherView<(), _> =
1166 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
1167 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1168 assert_eq!(
1169 w.snapshot_base, base,
1170 "a switch does not reassign the key pair"
1171 );
1172
1173 let v3: PatternSwitcherView<(), _> =
1174 pattern_switcher(3u32, FadeThrough, leaf_any(10.0, 10.0));
1175 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1176 assert_eq!(
1177 w.snapshot_base, base,
1178 "a second switch on the same widget still keeps the same key pair"
1179 );
1180 }
1181
1182 #[test]
1185 fn two_switcher_instances_get_disjoint_snapshot_key_pairs() {
1186 let v: PatternSwitcherView<(), _> =
1187 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1188 let w1 = build(&v);
1189 let w2 = build(&v);
1190 assert_ne!(
1191 w1.snapshot_base, w2.snapshot_base,
1192 "two switcher instances never share a snapshot key pair"
1193 );
1194 }
1195}