1use std::time::Duration;
89
90use frust_core::{
91 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, Curve, DiscardScene, EditingState,
92 EventCtx, EventResult, ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, RetainedKey,
93 SemanticsCtx, View, Widget, any,
94};
95use frust_theme::Theme;
96use kurbo::{Affine, Point, Size, Vec2};
97
98use crate::ChildKey;
99use crate::Timing;
100use crate::motion::patterns::{FadeThrough, PatternLayer, TransitionPattern};
101use crate::nav::transition::{TransitionDriver, make_driver};
102use crate::repaint_boundary::{RetainedBoundary, within};
103
104pub const FALLBACK_DURATION: Duration = Duration::from_millis(220);
108
109pub const REDUCE_MOTION_DURATION: Duration = Duration::from_millis(120);
114
115fn resolve_switch_timing(theme: Option<&Theme>, explicit: Option<Timing>) -> Timing {
120 explicit.unwrap_or_else(|| match theme {
121 Some(theme) => Timing::Duration(
122 Duration::from_secs_f64(theme.motion.durations.base / 1000.0),
123 theme.motion.easing.effects,
124 ),
125 None => Timing::Duration(FALLBACK_DURATION, Curve::Emphasized),
126 })
127}
128
129fn paint_staged_child(
147 pod: &mut ChildPod,
148 boundary: &mut RetainedBoundary,
149 ctx: &mut PaintCtx,
150 scene: &mut dyn PaintScene,
151 layer: PatternLayer,
152) {
153 pod.set_origin(Point::new(layer.dx, layer.dy));
154 let alpha = layer.alpha.clamp(0.0, 1.0);
155
156 if alpha <= 0.0 {
157 boundary.paint_plain(pod, ctx, &mut DiscardScene);
158 return;
159 }
160
161 let origin = ctx.origin();
162 let size = ctx.size();
163 let child_origin = origin + Vec2::new(layer.dx, layer.dy);
164
165 if (layer.scale - 1.0).abs() <= f64::EPSILON {
166 let may_record = within(child_origin, size, origin, size);
167 boundary.paint(pod, ctx, scene, child_origin, size, alpha, may_record);
168 return;
169 }
170
171 let center = Point::new(
172 child_origin.x + size.width / 2.0,
173 child_origin.y + size.height / 2.0,
174 );
175 let layered = alpha < 1.0;
176 scene.push_transform(Affine::scale_about(layer.scale, center));
177 if layered {
178 scene.push_layer(child_origin, size, alpha);
179 }
180 boundary.paint_plain(pod, ctx, scene);
181 if layered {
182 scene.pop_layer();
183 }
184 scene.pop_transform();
185}
186
187fn cleared_ime_state() -> ImeState {
191 ImeState {
192 active: false,
193 editing: EditingState {
194 text: String::new(),
195 selection_base: -1,
196 selection_extent: -1,
197 composing_base: -1,
198 composing_extent: -1,
199 },
200 caret: None,
201 content_type: Default::default(),
202 suppress_soft_keyboard: false,
203 }
204}
205
206pub struct PatternSwitcherView<State: 'static, P: TransitionPattern + Clone + 'static> {
208 key: ChildKey,
209 pattern: P,
210 timing: Option<Timing>,
211 reverse: bool,
212 child: AnyView<State>,
213}
214
215pub fn pattern_switcher<State, P, V>(
219 key: impl Into<ChildKey>,
220 pattern: P,
221 child: V,
222) -> PatternSwitcherView<State, P>
223where
224 State: 'static,
225 P: TransitionPattern + Clone + 'static,
226 V: View<State>,
227{
228 PatternSwitcherView {
229 key: key.into(),
230 pattern,
231 timing: None,
232 reverse: false,
233 child: any(child),
234 }
235}
236
237#[allow(non_snake_case)]
240pub fn PatternSwitcher<State, P, V>(
241 key: impl Into<ChildKey>,
242 pattern: P,
243 child: V,
244) -> PatternSwitcherView<State, P>
245where
246 State: 'static,
247 P: TransitionPattern + Clone + 'static,
248 V: View<State>,
249{
250 pattern_switcher(key, pattern, child)
251}
252
253impl<State: 'static, P: TransitionPattern + Clone + 'static> PatternSwitcherView<State, P> {
254 pub fn timing(mut self, timing: Timing) -> Self {
256 self.timing = Some(timing);
257 self
258 }
259
260 pub fn reverse(mut self, reverse: bool) -> Self {
263 self.reverse = reverse;
264 self
265 }
266}
267
268pub struct PatternSwitcherWidget<P: TransitionPattern + Clone + 'static> {
277 key: ChildKey,
278 child: ChildPod,
279 exiting: Option<ChildPod>,
283 pattern: P,
284 explicit_timing: Option<Timing>,
285 reverse: bool,
286 driver: Option<TransitionDriver>,
290 pending_start: bool,
293 settled: bool,
296 reduce_motion: bool,
299 needs_ime_clear: bool,
302 child_boundary: RetainedBoundary,
311 exiting_boundary: RetainedBoundary,
314}
315
316impl<State: 'static, P: TransitionPattern + Clone + 'static> View<State>
317 for PatternSwitcherView<State, P>
318{
319 type Element = PatternSwitcherWidget<P>;
320
321 fn build(&self, ctx: &mut BuildCtx<'_>) -> PatternSwitcherWidget<P> {
322 let keys = RetainedKey::mint_range(2);
323 PatternSwitcherWidget {
324 key: self.key,
325 child: crate::authoring::build_child(&self.child, ctx),
326 exiting: None,
327 pattern: self.pattern.clone(),
328 explicit_timing: self.timing,
329 reverse: self.reverse,
330 driver: None,
331 pending_start: false,
332 settled: false,
333 reduce_motion: false,
334 needs_ime_clear: false,
335 child_boundary: RetainedBoundary::with_key(keys),
336 exiting_boundary: RetainedBoundary::with_key(RetainedKey(keys.get() + 1)),
337 }
338 }
339
340 fn rebuild(
341 &self,
342 prev: &Self,
343 element: &mut PatternSwitcherWidget<P>,
344 ctx: &mut BuildCtx<'_>,
345 ) -> ChangeFlags {
346 let mut flags = ChangeFlags::NONE;
347
348 element.pattern = self.pattern.clone();
352 if element.explicit_timing != self.timing {
353 element.explicit_timing = self.timing;
354 flags |= ChangeFlags::PAINT;
355 }
356 if element.reverse != self.reverse {
357 element.reverse = self.reverse;
358 flags |= ChangeFlags::PAINT;
359 }
360
361 if element.settled {
366 element.exiting = None;
367 element.driver = None;
368 element.settled = false;
369 }
370
371 if self.key != element.key {
372 element.exiting = None;
376 element.driver = None;
377 element.reduce_motion = false;
378
379 let mut old = std::mem::replace(
382 &mut element.child,
383 crate::authoring::build_child(&self.child, ctx),
384 );
385
386 if old.is_active() {
389 crate::authoring::cancel_pod(&mut old);
390 old.set_active(false);
391 }
392 if old.is_focused() {
393 old.set_focused(false);
394 if ctx.has_focus() {
411 element.needs_ime_clear = true;
412 }
413 }
414
415 crate::authoring::teardown_child(&prev.child, &mut old, ctx);
426
427 element.exiting = Some(old);
428 std::mem::swap(&mut element.child_boundary, &mut element.exiting_boundary);
432 element.child_boundary = RetainedBoundary::with_key(element.child_boundary.key());
433 element.key = self.key;
434 element.pending_start = true;
435 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
436 } else {
437 flags |=
439 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
440 }
441
442 flags
443 }
444
445 fn teardown(&self, element: &mut PatternSwitcherWidget<P>, ctx: &mut BuildCtx<'_>) {
446 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
447 element.exiting = None;
451 }
452}
453
454impl<P: TransitionPattern + Clone + 'static> Widget for PatternSwitcherWidget<P> {
455 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
456 let size = self.child.layout_child(ctx, bc);
457 self.child.set_origin(Point::ZERO);
458 if let Some(exiting) = &mut self.exiting {
462 exiting.layout_child(ctx, bc);
463 exiting.set_origin(Point::ZERO);
464 }
465 size
466 }
467
468 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
469 if self.pending_start {
472 let theme = Theme::from_paint_ctx(ctx);
473 let reduce = theme.map(|t| t.motion.reduce_motion).unwrap_or(false);
474 let timing = if reduce {
475 Timing::Duration(REDUCE_MOTION_DURATION, Curve::Linear)
476 } else {
477 resolve_switch_timing(theme, self.explicit_timing)
478 };
479 self.driver = Some(make_driver(timing).0);
480 self.reduce_motion = reduce;
481 self.pending_start = false;
482 }
483
484 if let (Some(exiting), Some(driver)) = (self.exiting.as_mut(), self.driver.as_mut()) {
485 let adv = driver.advance(ctx.frame_time());
486 let size = ctx.size();
487 let (incoming_layer, exiting_layer) = if self.reduce_motion {
489 FadeThrough.resolve(adv.value, self.reverse, size)
490 } else {
491 self.pattern.resolve(adv.value, self.reverse, size)
492 };
493 paint_staged_child(
496 exiting,
497 &mut self.exiting_boundary,
498 ctx,
499 scene,
500 exiting_layer,
501 );
502 paint_staged_child(
503 &mut self.child,
504 &mut self.child_boundary,
505 ctx,
506 scene,
507 incoming_layer,
508 );
509
510 if adv.animating {
511 ctx.request_frame();
512 }
513 if adv.done {
514 self.settled = true;
517 ctx.request_frame();
518 }
519 } else {
520 self.child_boundary.paint_plain(&mut self.child, ctx, scene);
521 }
522
523 if self.needs_ime_clear {
526 ctx.publish_ime_state(cleared_ime_state());
527 self.needs_ime_clear = false;
528 }
529 }
530
531 fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
534 visitor(&self.child);
535 }
536
537 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
538 crate::authoring::route_event_single(&mut self.child, ctx, event)
541 }
542
543 fn semantics(&self, ctx: &mut SemanticsCtx) {
544 self.child.semantics_child(ctx);
547 }
548
549 crate::authoring::visit_children!(exiting, child);
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::motion::patterns::{FadeScale, SharedAxis};
556 use crate::test_support::{RecordingScene, leaf_any};
557 use crate::{FlexView, column};
558 use frust_core::{
559 FrameTime, PointerButton, PointerEvent, PointerPhase, RenderRoot, RetainedLedger,
560 };
561 use kurbo::Rect;
562 use std::cell::Cell;
563 use std::rc::Rc;
564
565 fn ft_ms(ms: f64) -> FrameTime {
566 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
567 }
568
569 fn build<S: 'static, V: View<S>>(view: &V) -> V::Element {
570 let mut counter = 0u64;
571 view.build(&mut BuildCtx::new(&mut counter))
572 }
573
574 fn down(x: f64, y: f64) -> InputEvent {
575 InputEvent::Pointer(PointerEvent {
576 phase: PointerPhase::Down,
577 position: Point::new(x, y),
578 button: PointerButton::Primary,
579 })
580 }
581
582 fn loose() -> BoxConstraints {
583 BoxConstraints::loose(Size::new(100.0, 100.0))
584 }
585
586 #[derive(Default)]
594 struct RetainedRecordingScene {
595 rects: Vec<(Point, Size)>,
596 layers: Vec<(Point, Size, f32)>,
597 transform_pushes: u32,
598 retained_pushes: Vec<(u64, Point, Size, f32, f64)>,
599 retained_pops: u32,
600 draws: Vec<(u64, Point, Size, f32, f64)>,
601 }
602 impl PaintScene for RetainedRecordingScene {
603 fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
604 self.rects.push((origin, size));
605 }
606 fn draw_text(&mut self, _origin: Point, _text: &str) {}
607 fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
608 self.layers.push((origin, size, alpha));
609 }
610 fn push_transform(&mut self, _transform: kurbo::Affine) {
611 self.transform_pushes += 1;
612 }
613 fn push_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
614 self.retained_pushes.push((key, origin, size, alpha, scale));
615 }
616 fn pop_retained(&mut self) {
617 self.retained_pops += 1;
618 }
619 fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
620 self.draws.push((key, origin, size, alpha, scale));
621 }
622 }
623
624 struct Recorder {
632 cancelled: Rc<Cell<bool>>,
633 torn: Rc<Cell<u32>>,
634 teardowns: Rc<Cell<u32>>,
635 }
636 struct RecorderWidget {
637 cancelled: Rc<Cell<bool>>,
638 torn: Rc<Cell<u32>>,
639 }
640 impl View<()> for Recorder {
641 type Element = RecorderWidget;
642 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
643 RecorderWidget {
644 cancelled: self.cancelled.clone(),
645 torn: self.torn.clone(),
646 }
647 }
648 fn rebuild(
649 &self,
650 _prev: &Self,
651 _element: &mut RecorderWidget,
652 _ctx: &mut BuildCtx<'_>,
653 ) -> ChangeFlags {
654 ChangeFlags::NONE
655 }
656 fn teardown(&self, _element: &mut RecorderWidget, _ctx: &mut BuildCtx<'_>) {
657 self.teardowns.set(self.teardowns.get() + 1);
658 }
659 }
660 impl Widget for RecorderWidget {
661 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
662 bc.max()
663 }
664 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
665 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
666 if let InputEvent::Pointer(p) = event {
667 match p.phase {
668 PointerPhase::Down => {
669 ctx.capture_pointer();
670 ctx.request_focus();
671 return EventResult::Handled;
672 }
673 PointerPhase::Cancel => {
676 self.cancelled.set(true);
677 return EventResult::Handled;
678 }
679 _ => {}
680 }
681 }
682 EventResult::Ignored
683 }
684 }
685 impl Drop for RecorderWidget {
686 fn drop(&mut self) {
687 self.torn.set(self.torn.get() + 1);
688 }
689 }
690
691 fn recorder(cancelled: &Rc<Cell<bool>>, torn: &Rc<Cell<u32>>) -> Recorder {
692 Recorder {
693 cancelled: cancelled.clone(),
694 torn: torn.clone(),
695 teardowns: Rc::new(Cell::new(0)),
696 }
697 }
698
699 fn counting_recorder(torn: &Rc<Cell<u32>>, teardowns: &Rc<Cell<u32>>) -> Recorder {
701 Recorder {
702 cancelled: Rc::new(Cell::new(false)),
703 torn: torn.clone(),
704 teardowns: teardowns.clone(),
705 }
706 }
707
708 fn reduced_motion_theme() -> Theme {
709 let mut theme = Theme::neutral();
710 theme.motion.reduce_motion = true;
711 theme
712 }
713
714 #[test]
717 fn same_identity_rebuild_does_not_transition_but_changed_identity_does() {
718 let v1: PatternSwitcherView<(), _> =
719 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
720 let mut w = build(&v1);
721 assert!(w.exiting.is_none());
722
723 let v2: PatternSwitcherView<(), _> =
725 pattern_switcher(1u32, FadeThrough, leaf_any(20.0, 20.0));
726 let mut counter = 0u64;
727 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
728 assert!(
729 w.exiting.is_none(),
730 "same identity must not stage a transition"
731 );
732 assert!(!w.pending_start);
733
734 let v3: PatternSwitcherView<(), _> =
736 pattern_switcher(2u32, FadeThrough, leaf_any(30.0, 30.0));
737 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
738 assert!(
739 w.exiting.is_some(),
740 "an identity change must retain the exiting child"
741 );
742 assert!(w.pending_start, "the next paint starts the driver");
743 assert_eq!(w.key, 2u32.into());
744 }
745
746 #[test]
749 fn identity_change_cancels_focus_and_clears_ime_on_the_exiting_child() {
750 let cancelled = Rc::new(Cell::new(false));
751 let torn = Rc::new(Cell::new(0u32));
752 let v1: PatternSwitcherView<(), _> =
753 pattern_switcher(1u32, FadeThrough, recorder(&cancelled, &torn));
754 let mut w = build(&v1);
755 let size = w.layout(&mut LayoutCtx::new(), &loose());
756
757 let mut st = ();
759 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
760 w.event(&mut ectx, &down(5.0, 5.0));
761 assert!(w.child.is_active(), "the child captured the pointer");
762 assert!(w.child.is_focused(), "the child took focus");
763
764 let v2: PatternSwitcherView<(), _> =
767 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
768 let mut counter = 0u64;
769 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
770
771 assert!(
772 cancelled.get(),
773 "the exiting child received a synthetic Cancel"
774 );
775 let exiting = w.exiting.as_ref().expect("exiting child retained");
776 assert!(
777 !exiting.is_focused(),
778 "the exiting child's focus was cleared"
779 );
780 assert!(!exiting.is_active(), "its capture path was released");
781 assert!(
782 w.needs_ime_clear,
783 "an IME clear is queued for the next paint"
784 );
785
786 let mut pctx = PaintCtx::new(Point::ZERO, size);
788 let mut scene = RecordingScene::default();
789 w.paint(&mut pctx, &mut scene);
790 let ime = pctx
791 .take_ime_state()
792 .expect("a cleared IME surface was published on the post-switch paint");
793 assert!(!ime.active);
794 assert!(ime.editing.text.is_empty());
795 assert!(!w.needs_ime_clear, "the queued clear was consumed");
796 }
797
798 #[test]
803 fn freeze_teardown_of_a_focused_child_raises_no_second_focus_release() {
804 let cancelled = Rc::new(Cell::new(false));
805 let torn = Rc::new(Cell::new(0u32));
806 let teardowns = Rc::new(Cell::new(0u32));
807 let v1: PatternSwitcherView<(), _> = pattern_switcher(
808 1u32,
809 FadeThrough,
810 Recorder {
811 cancelled: cancelled.clone(),
812 torn: torn.clone(),
813 teardowns: teardowns.clone(),
814 },
815 );
816 let mut w = build(&v1);
817 let size = w.layout(&mut LayoutCtx::new(), &loose());
818 let mut st = ();
819 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
820 w.event(&mut ectx, &down(5.0, 5.0));
821 assert!(w.child.is_active() && w.child.is_focused());
822
823 let _ = frust_core::take_focus_orphaned();
824 let v2: PatternSwitcherView<(), _> =
825 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
826 let mut counter = 0u64;
827 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
829
830 assert_eq!(
831 teardowns.get(),
832 1,
833 "the focused child was torn down at the switch"
834 );
835 assert!(cancelled.get(), "cancelled exactly as before");
836 assert!(w.needs_ime_clear, "the IME clear is the session release");
837 assert!(
838 !frust_core::take_focus_orphaned(),
839 "the teardown must not raise a second (orphan) release"
840 );
841 }
842
843 fn field_surface() -> ImeState {
848 ImeState {
849 active: true,
850 editing: EditingState {
851 text: "query".to_string(),
852 selection_base: 5,
853 selection_extent: 5,
854 composing_base: -1,
855 composing_extent: -1,
856 },
857 caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
858 content_type: Default::default(),
859 suppress_soft_keyboard: false,
860 }
861 }
862
863 struct PersistentField {
869 size: Size,
870 }
871 struct PersistentFieldWidget {
872 size: Size,
873 }
874 impl<S: 'static> View<S> for PersistentField {
875 type Element = PersistentFieldWidget;
876 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PersistentFieldWidget {
877 PersistentFieldWidget { size: self.size }
878 }
879 fn rebuild(
880 &self,
881 _prev: &Self,
882 _element: &mut PersistentFieldWidget,
883 _ctx: &mut BuildCtx<'_>,
884 ) -> ChangeFlags {
885 ChangeFlags::NONE
886 }
887 }
888 impl Widget for PersistentFieldWidget {
889 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
890 bc.constrain(self.size)
891 }
892 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
893 if ctx.has_focus() {
894 ctx.publish_ime_state(field_surface());
895 }
896 }
897 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
898 if let InputEvent::Pointer(p) = event
899 && p.phase == PointerPhase::Down
900 {
901 ctx.request_focus();
902 ctx.publish_ime_state(field_surface());
903 return EventResult::Handled;
904 }
905 EventResult::Ignored
906 }
907 }
908
909 #[test]
910 fn identity_change_in_an_unfocused_switcher_leaves_a_sibling_fields_session_alive() {
911 let key = Rc::new(Cell::new(1u32));
916 let mut app = {
917 let key = key.clone();
918 move |_: &mut ()| {
919 column()
920 .child(PersistentField {
921 size: Size::new(100.0, 40.0),
922 })
923 .child(pattern_switcher(
924 key.get(),
925 FadeThrough,
926 leaf_any(100.0, 40.0),
927 ))
928 }
929 };
930 let mut root: RenderRoot<(), FlexView<()>> = RenderRoot::new();
931 let mut state = ();
932 root.rebuild(&mut app, &mut state);
933 root.layout(Size::new(100.0, 100.0));
934 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
935
936 root.event(&mut state, &down(5.0, 5.0));
939 assert!(
940 root.is_focus_active(),
941 "the sibling field owns the focus session"
942 );
943 assert_eq!(
944 root.ime_state().map(|s| s.editing.text),
945 Some("query".to_string()),
946 "…and the shell-facing surface is the field's"
947 );
948 let generation = root.focus_ime_generation();
949
950 key.set(2);
953 root.rebuild(&mut app, &mut state);
954 root.layout(Size::new(100.0, 100.0));
955 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
956
957 assert!(
958 root.is_focus_active(),
959 "an identity change in an unfocused switcher must not release the \
960 sibling field's session"
961 );
962 let ime = root
963 .ime_state()
964 .expect("the identity change dropped the sibling field's IME surface entirely");
965 assert!(ime.active, "the field's surface was left inactive");
966 assert_eq!(
967 ime.editing.text, "query",
968 "the field's surface was replaced with the switcher's cleared one"
969 );
970 assert_eq!(
971 root.focus_ime_generation(),
972 generation,
973 "an identity change in an unfocused switcher is not a focus/IME edge"
974 );
975 }
976
977 #[test]
981 fn exiting_child_is_torn_down_at_the_switch_and_dropped_after_settle() {
982 let torn = Rc::new(Cell::new(0u32));
983 let teardowns = Rc::new(Cell::new(0u32));
984 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
985 let v1: PatternSwitcherView<(), _> =
986 pattern_switcher(1u32, FadeThrough, counting_recorder(&torn, &teardowns)).timing(short);
987 let mut w = build(&v1);
988 let size = w.layout(&mut LayoutCtx::new(), &loose());
989
990 let v2: PatternSwitcherView<(), _> =
993 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
994 let mut counter = 0u64;
995 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
996 assert!(w.exiting.is_some());
997 assert_eq!(teardowns.get(), 1, "torn down at the switch, exactly once");
998 assert_eq!(
999 torn.get(),
1000 0,
1001 "its pod is not dropped while the transition runs"
1002 );
1003
1004 w.paint(
1006 &mut PaintCtx::new(Point::ZERO, size),
1007 &mut RecordingScene::default(),
1008 );
1009 assert!(w.exiting.is_some(), "retained during the transition");
1010 assert_eq!(torn.get(), 0);
1011 assert!(!w.settled);
1012
1013 w.driver
1016 .as_mut()
1017 .expect("driver created on first paint")
1018 .advance(ft_ms(10_000.0));
1019
1020 w.paint(
1022 &mut PaintCtx::new(Point::ZERO, size),
1023 &mut RecordingScene::default(),
1024 );
1025 assert!(w.settled, "paint flags settle once the driver reaches rest");
1026 assert!(
1027 w.exiting.is_some(),
1028 "still retained until the finalize rebuild"
1029 );
1030 assert_eq!(torn.get(), 0);
1031
1032 let v3: PatternSwitcherView<(), _> =
1035 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1036 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1037 assert!(w.exiting.is_none(), "exiting dropped after settle");
1038 assert_eq!(
1039 torn.get(),
1040 1,
1041 "exiting dropped exactly once, after progress 1.0"
1042 );
1043 assert_eq!(
1044 teardowns.get(),
1045 1,
1046 "the settle drop is not a second teardown"
1047 );
1048 assert!(!w.settled);
1049 assert!(w.driver.is_none());
1050
1051 v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1053 assert_eq!((teardowns.get(), torn.get()), (1, 1));
1054 }
1055
1056 #[test]
1057 fn a_shortcut_reaches_the_incoming_child_not_the_frozen_exiting_one() {
1058 use crate::test_support::probe;
1059 let short = Timing::Duration(std::time::Duration::from_millis(100), Curve::Linear);
1060 let v1: PatternSwitcherView<Vec<u32>, _> =
1061 pattern_switcher(1u32, FadeThrough, probe(1)).timing(short);
1062 let mut w = build(&v1);
1063 let mut counter = 0u64;
1064 let v2: PatternSwitcherView<Vec<u32>, _> =
1065 pattern_switcher(2u32, FadeThrough, probe(2)).timing(short);
1066 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1067 assert!(
1068 w.exiting.is_some(),
1069 "the old child is frozen, still painted"
1070 );
1071 let size = w.layout(&mut LayoutCtx::new(), &loose());
1072 let mut log: Vec<u32> = Vec::new();
1073 let mut ectx = EventCtx::new(&mut log, Point::ZERO, size);
1074 w.event(
1075 &mut ectx,
1076 &InputEvent::Shortcut(frust_core::ShortcutId::next()),
1077 );
1078 assert_eq!(log, vec![2], "only the incoming child answers");
1079 }
1080
1081 #[test]
1086 fn a_navigator_in_the_outgoing_child_unmounts_on_the_switch_but_still_paints() {
1087 use crate::test_support::leaf;
1088 use crate::{NavigatorController, navigator};
1089
1090 let ctrl: NavigatorController<()> = NavigatorController::new();
1091 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1092 let section = |key: u32| -> PatternSwitcherView<(), FadeThrough> {
1093 let child = if key == 1 {
1094 any(navigator(&ctrl, || leaf(10.0, 10.0)))
1095 } else {
1096 leaf_any(10.0, 10.0)
1097 };
1098 pattern_switcher(key, FadeThrough, child).timing(short)
1099 };
1100 let mut counter = 0u64;
1101 let v1 = section(1);
1102 let mut w = build(&v1);
1103 assert!(ctrl.is_mounted());
1104
1105 ctrl.push(|| leaf(40.0, 40.0));
1107 let v2 = section(1);
1108 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1109 assert_eq!(ctrl.depth(), 2);
1110 assert!(
1111 ctrl.back_interest(),
1112 "a mounted depth-2 navigator claims back"
1113 );
1114
1115 let v3 = section(2);
1117 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1118 assert!(!ctrl.is_mounted(), "torn down at the switch, not at settle");
1119 assert!(
1120 !ctrl.back_interest(),
1121 "a torn-down navigator claims nothing, with no re-wire in between"
1122 );
1123
1124 let size = w.layout(&mut LayoutCtx::new(), &loose());
1127 let mut scene = RecordingScene::default();
1128 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1129 assert!(
1130 scene.rects.iter().any(|(_, s)| *s == Size::new(40.0, 40.0)),
1131 "the torn-down navigator's top page still paints: {:?}",
1132 scene.rects
1133 );
1134
1135 w.driver
1137 .as_mut()
1138 .expect("driver created on first paint")
1139 .advance(ft_ms(10_000.0));
1140 w.paint(
1141 &mut PaintCtx::new(Point::ZERO, size),
1142 &mut RecordingScene::default(),
1143 );
1144 assert!(w.settled);
1145 let v4 = section(2);
1146 v4.rebuild(&v3, &mut w, &mut BuildCtx::new(&mut counter));
1147 assert!(w.exiting.is_none());
1148 assert!(!ctrl.is_mounted());
1149 assert!(!ctrl.back_interest());
1150 }
1151
1152 struct ProbeComp {
1159 teardowns: Rc<Cell<u32>>,
1160 state_drops: Rc<Cell<u32>>,
1161 }
1162 struct ProbeState {
1163 drops: Rc<Cell<u32>>,
1164 }
1165 impl Drop for ProbeState {
1166 fn drop(&mut self) {
1167 self.drops.set(self.drops.get() + 1);
1168 }
1169 }
1170 impl frust_core::Component for ProbeComp {
1171 type State = ProbeState;
1172 fn init(&self) -> ProbeState {
1173 ProbeState {
1174 drops: self.state_drops.clone(),
1175 }
1176 }
1177 fn build(&self, _state: &mut ProbeState) -> impl View<ProbeState> {
1178 TeardownLeaf {
1179 teardowns: self.teardowns.clone(),
1180 }
1181 }
1182 }
1183 struct TeardownLeaf {
1185 teardowns: Rc<Cell<u32>>,
1186 }
1187 impl<S: 'static> View<S> for TeardownLeaf {
1188 type Element = crate::test_support::LeafWidget;
1189 fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
1190 View::<()>::build(&crate::test_support::leaf(30.0, 30.0), ctx)
1191 }
1192 fn rebuild(
1193 &self,
1194 _prev: &Self,
1195 _element: &mut Self::Element,
1196 _ctx: &mut BuildCtx<'_>,
1197 ) -> ChangeFlags {
1198 ChangeFlags::NONE
1199 }
1200 fn teardown(&self, _element: &mut Self::Element, _ctx: &mut BuildCtx<'_>) {
1201 self.teardowns.set(self.teardowns.get() + 1);
1202 }
1203 }
1204
1205 #[test]
1206 fn a_component_in_the_outgoing_child_is_torn_down_exactly_once() {
1207 let teardowns = Rc::new(Cell::new(0u32));
1208 let state_drops = Rc::new(Cell::new(0u32));
1209 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1210 let comp = || {
1211 any(frust_core::component(ProbeComp {
1212 teardowns: teardowns.clone(),
1213 state_drops: state_drops.clone(),
1214 }))
1215 };
1216 let mut counter = 0u64;
1217
1218 let v1: PatternSwitcherView<(), _> =
1220 pattern_switcher(1u32, FadeThrough, comp()).timing(short);
1221 let mut w = build(&v1);
1222 let size = w.layout(&mut LayoutCtx::new(), &loose());
1223 let v2: PatternSwitcherView<(), _> =
1224 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1225 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1226 assert_eq!(
1227 teardowns.get(),
1228 1,
1229 "the component is torn down at the freeze"
1230 );
1231 assert_eq!(
1232 state_drops.get(),
1233 0,
1234 "its widget (and state) is still painting"
1235 );
1236
1237 let mut scene = RecordingScene::default();
1239 w.layout(&mut LayoutCtx::new(), &loose());
1240 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1241 assert!(
1242 scene.rects.iter().any(|(_, s)| *s == Size::new(30.0, 30.0)),
1243 "the disposed component's subtree still paints: {:?}",
1244 scene.rects
1245 );
1246
1247 w.driver
1248 .as_mut()
1249 .expect("driver created on first paint")
1250 .advance(ft_ms(10_000.0));
1251 w.paint(
1252 &mut PaintCtx::new(Point::ZERO, size),
1253 &mut RecordingScene::default(),
1254 );
1255 let v3: PatternSwitcherView<(), _> =
1256 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1257 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1258 assert!(w.exiting.is_none());
1259 assert_eq!(state_drops.get(), 1, "dropped once at settle");
1260 assert_eq!(
1261 teardowns.get(),
1262 1,
1263 "the settle drop is not a second teardown"
1264 );
1265 v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1266 assert_eq!((teardowns.get(), state_drops.get()), (1, 1));
1267
1268 teardowns.set(0);
1271 state_drops.set(0);
1272 let v1: PatternSwitcherView<(), _> =
1273 pattern_switcher(1u32, FadeThrough, comp()).timing(short);
1274 let mut w = build(&v1);
1275 let v2: PatternSwitcherView<(), _> =
1276 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1277 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1278 assert_eq!((teardowns.get(), state_drops.get()), (1, 0));
1279 v2.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1280 assert!(
1281 w.exiting.is_none(),
1282 "the switcher's teardown drops the exiting pod"
1283 );
1284 assert_eq!(
1285 (teardowns.get(), state_drops.get()),
1286 (1, 1),
1287 "torn down once at the freeze, dropped once by the switcher's teardown"
1288 );
1289 }
1290
1291 #[test]
1295 fn superseding_switches_tear_each_outgoing_child_down_exactly_once() {
1296 let counters = || (Rc::new(Cell::new(0u32)), Rc::new(Cell::new(0u32)));
1297 let (a_drop, a_td) = counters();
1298 let (b_drop, b_td) = counters();
1299 let (c_drop, c_td) = counters();
1300 let mut counter = 0u64;
1301
1302 let va: PatternSwitcherView<(), _> =
1303 pattern_switcher(1u32, FadeThrough, counting_recorder(&a_drop, &a_td));
1304 let mut w = build(&va);
1305 let vb: PatternSwitcherView<(), _> =
1306 pattern_switcher(2u32, FadeThrough, counting_recorder(&b_drop, &b_td));
1307 vb.rebuild(&va, &mut w, &mut BuildCtx::new(&mut counter));
1308 assert_eq!(
1309 (a_td.get(), a_drop.get()),
1310 (1, 0),
1311 "A frozen: torn down, kept"
1312 );
1313 assert_eq!((b_td.get(), b_drop.get()), (0, 0), "B is live");
1314
1315 let vc: PatternSwitcherView<(), _> =
1318 pattern_switcher(3u32, FadeThrough, counting_recorder(&c_drop, &c_td));
1319 vc.rebuild(&vb, &mut w, &mut BuildCtx::new(&mut counter));
1320 assert_eq!(
1321 (a_td.get(), a_drop.get()),
1322 (1, 1),
1323 "A superseded: dropped only"
1324 );
1325 assert_eq!(
1326 (b_td.get(), b_drop.get()),
1327 (1, 0),
1328 "B frozen: torn down, kept"
1329 );
1330 assert_eq!((c_td.get(), c_drop.get()), (0, 0), "C is live");
1331
1332 vc.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1335 assert_eq!((a_td.get(), a_drop.get()), (1, 1));
1336 assert_eq!((b_td.get(), b_drop.get()), (1, 1), "B dropped, not re-torn");
1337 assert_eq!(
1338 c_td.get(),
1339 1,
1340 "the live child is torn down by the switcher's teardown"
1341 );
1342 drop(w);
1343 assert_eq!(
1344 (a_td.get(), b_td.get(), c_td.get()),
1345 (1, 1, 1),
1346 "each child torn down exactly once"
1347 );
1348 assert_eq!((a_drop.get(), b_drop.get(), c_drop.get()), (1, 1, 1));
1349 }
1350
1351 #[test]
1354 fn same_key_rebuilds_reconcile_in_place_with_no_teardown() {
1355 let torn = Rc::new(Cell::new(0u32));
1356 let teardowns = Rc::new(Cell::new(0u32));
1357 let mut counter = 0u64;
1358 let make = || -> PatternSwitcherView<(), FadeThrough> {
1359 pattern_switcher(7u32, FadeThrough, counting_recorder(&torn, &teardowns))
1360 };
1361 let mut prev = make();
1362 let mut w = build(&prev);
1363 for _ in 0..3 {
1364 let next = make();
1365 next.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
1366 prev = next;
1367 }
1368 assert!(w.exiting.is_none(), "no transition staged");
1369 assert_eq!(
1370 (teardowns.get(), torn.get()),
1371 (0, 0),
1372 "the live child was reconciled in place: never torn down, never dropped"
1373 );
1374 }
1375
1376 #[test]
1379 fn reduce_motion_collapses_to_fast_crossfade() {
1380 let theme = reduced_motion_theme();
1381 let v1: PatternSwitcherView<(), _> =
1385 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
1386 let mut w = build(&v1);
1387 let size = w.layout(&mut LayoutCtx::new(), &loose());
1388
1389 let v2: PatternSwitcherView<(), _> =
1390 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
1391 let mut counter = 0u64;
1392 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1393
1394 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
1395 w.paint(&mut pctx, &mut RecordingScene::default());
1396
1397 assert!(
1398 w.reduce_motion,
1399 "reduce_motion must be resolved from the threaded theme"
1400 );
1401 assert_eq!(
1404 w.child.origin(),
1405 Point::ZERO,
1406 "reduce_motion collapses to a non-directional crossfade (no slide)"
1407 );
1408 }
1409
1410 #[test]
1411 fn without_reduce_motion_the_configured_directional_pattern_slides() {
1412 let v1: PatternSwitcherView<(), _> =
1415 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
1416 let mut w = build(&v1);
1417 let size = w.layout(&mut LayoutCtx::new(), &loose());
1418
1419 let v2: PatternSwitcherView<(), _> =
1420 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
1421 let mut counter = 0u64;
1422 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1423
1424 w.paint(
1425 &mut PaintCtx::new(Point::ZERO, size),
1426 &mut RecordingScene::default(),
1427 );
1428 assert!(!w.reduce_motion);
1429 assert_eq!(w.child.origin().x, 30.0);
1432 }
1433
1434 #[test]
1437 fn steady_state_paints_the_single_child_with_no_layer_or_transform() {
1438 let v: PatternSwitcherView<(), _> =
1439 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1440 let mut w = build(&v);
1441 let size = w.layout(&mut LayoutCtx::new(), &loose());
1442
1443 let mut scene = RecordingScene::default();
1444 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1445 assert_eq!(scene.rects.len(), 1, "the single child paints its fill");
1446 assert!(
1447 scene.layers.is_empty(),
1448 "no crossfade layer at steady state"
1449 );
1450 assert!(
1451 scene.transforms.is_empty(),
1452 "no scale transform at steady state"
1453 );
1454 }
1455
1456 #[test]
1461 fn alpha_zero_staged_child_records_no_ops_and_no_zero_alpha_layer() {
1462 let leaf = leaf_any(10.0, 10.0);
1463 let mut counter = 0u64;
1464 let mut pod = crate::authoring::build_child(&leaf, &mut BuildCtx::new(&mut counter));
1465 let size = pod.layout_child(&mut LayoutCtx::new(), &loose());
1466 pod.set_origin(Point::ZERO);
1467
1468 let mut scene = RecordingScene::default();
1469 let layer = PatternLayer {
1470 dx: 0.0,
1471 dy: 0.0,
1472 alpha: 0.0,
1473 scale: 1.0,
1474 };
1475 paint_staged_child(
1476 &mut pod,
1477 &mut RetainedBoundary::new(),
1478 &mut PaintCtx::new(Point::ZERO, size),
1479 &mut scene,
1480 layer,
1481 );
1482
1483 assert!(scene.rects.is_empty(), "an alpha-0 child records no fill");
1484 assert!(
1485 scene.layers.is_empty(),
1486 "no zero-alpha push_layer is recorded either"
1487 );
1488 }
1489
1490 fn resident(keys: &[RetainedKey]) -> RetainedLedger {
1500 RetainedLedger::from_entries(1, keys.iter().map(|k| (k.get(), (10, 10), 1.0)))
1501 }
1502
1503 fn switched<P: TransitionPattern + Clone + 'static>(
1506 pattern: P,
1507 ) -> (PatternSwitcherWidget<P>, Size, RetainedKey) {
1508 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1509 let v1: PatternSwitcherView<(), _> =
1510 pattern_switcher(1u32, pattern.clone(), leaf_any(10.0, 10.0)).timing(timing);
1511 let mut w = build(&v1);
1512 let size = w.layout(&mut LayoutCtx::new(), &loose());
1513 let live_key = w.child_boundary.key();
1514 let v2: PatternSwitcherView<(), _> =
1515 pattern_switcher(2u32, pattern, leaf_any(10.0, 10.0)).timing(timing);
1516 let mut counter = 0u64;
1517 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1518 w.layout(&mut LayoutCtx::new(), &loose());
1519 (w, size, live_key)
1520 }
1521
1522 #[test]
1523 fn the_seed_frame_records_the_exiting_child_under_the_key_it_held_while_live() {
1524 let (mut w, size, live_key) = switched(SharedAxis::X);
1525 assert_eq!(
1526 w.exiting_boundary.key(),
1527 live_key,
1528 "the frozen pod keeps the boundary it painted through while live"
1529 );
1530 assert_ne!(
1531 w.child_boundary.key(),
1532 live_key,
1533 "the fresh child takes the other key"
1534 );
1535
1536 let ledger = RetainedLedger::empty();
1537 let mut scene = RetainedRecordingScene::default();
1538 w.paint(
1539 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
1540 &mut scene,
1541 );
1542 assert_eq!(
1545 scene.retained_pushes,
1546 vec![(live_key.get(), Point::ZERO, size, 1.0, 1.0)],
1547 "one recording bracket, keyed by the exiting child's own key, at scale 1"
1548 );
1549 assert_eq!(scene.retained_pops, 1, "the bracket is balanced");
1550 assert!(scene.draws.is_empty(), "nothing is resident yet");
1551 }
1552
1553 #[test]
1554 fn a_clean_resident_exiting_child_composites_its_page_as_it_fades() {
1555 let (mut w, size, live_key) = switched(FadeScale);
1556 let empty = RetainedLedger::empty();
1557 w.paint(
1558 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&empty),
1559 &mut RetainedRecordingScene::default(),
1560 );
1561
1562 let ledger = resident(&[live_key]);
1565 let mut scene = RetainedRecordingScene::default();
1566 w.paint(
1567 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)).with_retained_ledger(&ledger),
1568 &mut scene,
1569 );
1570 assert_eq!(
1571 scene.draws.len(),
1572 1,
1573 "the exiting page is drawn: {:?}",
1574 scene.draws
1575 );
1576 let (key, origin, draw_size, alpha, scale) = scene.draws[0];
1577 assert_eq!(
1578 (key, origin, draw_size, scale),
1579 (live_key.get(), Point::ZERO, size, 1.0)
1580 );
1581 assert!(
1582 alpha > 0.0 && alpha < 1.0,
1583 "at the fade's own alpha (was {alpha})"
1584 );
1585 assert_eq!(
1586 scene.rects.len(),
1587 1,
1588 "only the incoming child paints a body; the exiting one is composited"
1589 );
1590 assert!(
1591 scene.retained_pushes.is_empty(),
1592 "the incoming child's scale step is never bracketed: {:?}",
1593 scene.retained_pushes
1594 );
1595 assert_eq!(
1596 scene.transform_pushes, 1,
1597 "the incoming child's scale step paints inline under a push_transform"
1598 );
1599 }
1600
1601 #[test]
1602 fn without_a_ledger_staged_children_paint_with_no_bracket() {
1603 let (mut w, size, _) = switched(FadeScale);
1604 w.paint(
1605 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1606 &mut RetainedRecordingScene::default(),
1607 );
1608 let mut scene = RetainedRecordingScene::default();
1609 w.paint(
1610 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
1611 &mut scene,
1612 );
1613 assert!(scene.retained_pushes.is_empty() && scene.draws.is_empty());
1614 assert_eq!(scene.rects.len(), 2, "both children paint their bodies");
1615 assert_eq!(scene.layers.len(), 2, "each under its own fade layer");
1616 }
1617
1618 #[test]
1619 fn a_slid_child_is_never_recorded_while_it_overhangs_the_switcher() {
1620 let (mut w, size, _) = switched(SharedAxis::X);
1624 let ledger = RetainedLedger::empty();
1625 w.paint(
1626 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
1627 &mut RetainedRecordingScene::default(),
1628 );
1629 let mut scene = RetainedRecordingScene::default();
1630 w.paint(
1631 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)).with_retained_ledger(&ledger),
1632 &mut scene,
1633 );
1634 assert!(
1635 scene.retained_pushes.is_empty(),
1636 "no bracket: {:?}",
1637 scene.retained_pushes
1638 );
1639 assert_eq!(
1640 scene.rects,
1641 vec![(Point::new(15.0, 0.0), size)],
1642 "the incoming child paints at its slid origin"
1643 );
1644 }
1645
1646 fn key_pair<P: TransitionPattern + Clone + 'static>(w: &PatternSwitcherWidget<P>) -> [u64; 2] {
1650 let mut keys = [w.child_boundary.key().get(), w.exiting_boundary.key().get()];
1651 keys.sort_unstable();
1652 keys
1653 }
1654
1655 #[test]
1656 fn the_key_pair_is_stable_across_multiple_switches_on_the_same_widget() {
1657 let v1: PatternSwitcherView<(), _> =
1658 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1659 let mut w = build(&v1);
1660 let pair = key_pair(&w);
1661 assert_eq!(pair[1], pair[0] + 1, "a reserved consecutive pair");
1662 let mut counter = 0u64;
1663
1664 let v2: PatternSwitcherView<(), _> =
1665 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
1666 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1667 assert_eq!(
1668 key_pair(&w),
1669 pair,
1670 "a switch does not reassign the key pair"
1671 );
1672
1673 let v3: PatternSwitcherView<(), _> =
1674 pattern_switcher(3u32, FadeThrough, leaf_any(10.0, 10.0));
1675 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1676 assert_eq!(
1677 key_pair(&w),
1678 pair,
1679 "a second switch on the same widget still keeps the same key pair"
1680 );
1681 }
1682
1683 #[test]
1686 fn two_switcher_instances_get_disjoint_key_pairs() {
1687 let v: PatternSwitcherView<(), _> =
1688 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1689 let w1 = build(&v);
1690 let w2 = build(&v);
1691 let (a, b) = (key_pair(&w1), key_pair(&w2));
1692 assert!(
1693 a.iter().all(|k| !b.contains(k)),
1694 "two switcher instances never share a key: {a:?} vs {b:?}"
1695 );
1696 }
1697}