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> {
274 key: ChildKey,
275 child: ChildPod,
276 exiting: Option<ChildPod>,
280 pattern: P,
281 explicit_timing: Option<Timing>,
282 reverse: bool,
283 driver: Option<TransitionDriver>,
287 pending_start: bool,
290 settled: bool,
293 reduce_motion: bool,
296 needs_ime_clear: bool,
299 child_boundary: RetainedBoundary,
308 exiting_boundary: RetainedBoundary,
311}
312
313impl<State: 'static, P: TransitionPattern + Clone + 'static> View<State>
314 for PatternSwitcherView<State, P>
315{
316 type Element = PatternSwitcherWidget<P>;
317
318 fn build(&self, ctx: &mut BuildCtx<'_>) -> PatternSwitcherWidget<P> {
319 let keys = RetainedKey::mint_range(2);
320 PatternSwitcherWidget {
321 key: self.key,
322 child: crate::authoring::build_child(&self.child, ctx),
323 exiting: None,
324 pattern: self.pattern.clone(),
325 explicit_timing: self.timing,
326 reverse: self.reverse,
327 driver: None,
328 pending_start: false,
329 settled: false,
330 reduce_motion: false,
331 needs_ime_clear: false,
332 child_boundary: RetainedBoundary::with_key(keys),
333 exiting_boundary: RetainedBoundary::with_key(RetainedKey(keys.get() + 1)),
334 }
335 }
336
337 fn rebuild(
338 &self,
339 prev: &Self,
340 element: &mut PatternSwitcherWidget<P>,
341 ctx: &mut BuildCtx<'_>,
342 ) -> ChangeFlags {
343 let mut flags = ChangeFlags::NONE;
344
345 element.pattern = self.pattern.clone();
349 if element.explicit_timing != self.timing {
350 element.explicit_timing = self.timing;
351 flags |= ChangeFlags::PAINT;
352 }
353 if element.reverse != self.reverse {
354 element.reverse = self.reverse;
355 flags |= ChangeFlags::PAINT;
356 }
357
358 if element.settled {
363 element.exiting = None;
364 element.driver = None;
365 element.settled = false;
366 }
367
368 if self.key != element.key {
369 element.exiting = None;
373 element.driver = None;
374 element.reduce_motion = false;
375
376 let mut old = std::mem::replace(
379 &mut element.child,
380 crate::authoring::build_child(&self.child, ctx),
381 );
382
383 if old.is_active() {
386 crate::authoring::cancel_pod(&mut old);
387 old.set_active(false);
388 }
389 if old.is_focused() {
390 old.set_focused(false);
391 if ctx.has_focus() {
408 element.needs_ime_clear = true;
409 }
410 }
411
412 crate::authoring::teardown_child(&prev.child, &mut old, ctx);
423
424 element.exiting = Some(old);
425 std::mem::swap(&mut element.child_boundary, &mut element.exiting_boundary);
429 element.child_boundary = RetainedBoundary::with_key(element.child_boundary.key());
430 element.key = self.key;
431 element.pending_start = true;
432 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
433 } else {
434 flags |=
436 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
437 }
438
439 flags
440 }
441
442 fn teardown(&self, element: &mut PatternSwitcherWidget<P>, ctx: &mut BuildCtx<'_>) {
443 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
444 element.exiting = None;
448 }
449}
450
451impl<P: TransitionPattern + Clone + 'static> Widget for PatternSwitcherWidget<P> {
452 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
453 let size = self.child.layout_child(ctx, bc);
454 self.child.set_origin(Point::ZERO);
455 if let Some(exiting) = &mut self.exiting {
459 exiting.layout_child(ctx, bc);
460 exiting.set_origin(Point::ZERO);
461 }
462 size
463 }
464
465 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
466 if self.pending_start {
469 let theme = Theme::from_paint_ctx(ctx);
470 let reduce = theme.map(|t| t.motion.reduce_motion).unwrap_or(false);
471 let timing = if reduce {
472 Timing::Duration(REDUCE_MOTION_DURATION, Curve::Linear)
473 } else {
474 resolve_switch_timing(theme, self.explicit_timing)
475 };
476 self.driver = Some(make_driver(timing).0);
477 self.reduce_motion = reduce;
478 self.pending_start = false;
479 }
480
481 if let (Some(exiting), Some(driver)) = (self.exiting.as_mut(), self.driver.as_mut()) {
482 let adv = driver.advance(ctx.frame_time());
483 let size = ctx.size();
484 let (incoming_layer, exiting_layer) = if self.reduce_motion {
486 FadeThrough.resolve(adv.value, self.reverse, size)
487 } else {
488 self.pattern.resolve(adv.value, self.reverse, size)
489 };
490 paint_staged_child(
493 exiting,
494 &mut self.exiting_boundary,
495 ctx,
496 scene,
497 exiting_layer,
498 );
499 paint_staged_child(
500 &mut self.child,
501 &mut self.child_boundary,
502 ctx,
503 scene,
504 incoming_layer,
505 );
506
507 if adv.animating {
508 ctx.request_frame();
509 }
510 if adv.done {
511 self.settled = true;
514 ctx.request_frame();
515 }
516 } else {
517 self.child_boundary.paint_plain(&mut self.child, ctx, scene);
518 }
519
520 if self.needs_ime_clear {
523 ctx.publish_ime_state(cleared_ime_state());
524 self.needs_ime_clear = false;
525 }
526 }
527
528 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
529 crate::authoring::route_event_single(&mut self.child, ctx, event)
532 }
533
534 fn semantics(&self, ctx: &mut SemanticsCtx) {
535 self.child.semantics_child(ctx);
538 }
539
540 crate::authoring::visit_children!(exiting, child);
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::motion::patterns::{FadeScale, SharedAxis};
547 use crate::test_support::{RecordingScene, leaf_any};
548 use crate::{FlexView, column};
549 use frust_core::{
550 FrameTime, PointerButton, PointerEvent, PointerPhase, RenderRoot, RetainedLedger,
551 };
552 use kurbo::Rect;
553 use std::cell::Cell;
554 use std::rc::Rc;
555
556 fn ft_ms(ms: f64) -> FrameTime {
557 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
558 }
559
560 fn build<S: 'static, V: View<S>>(view: &V) -> V::Element {
561 let mut counter = 0u64;
562 view.build(&mut BuildCtx::new(&mut counter))
563 }
564
565 fn down(x: f64, y: f64) -> InputEvent {
566 InputEvent::Pointer(PointerEvent {
567 phase: PointerPhase::Down,
568 position: Point::new(x, y),
569 button: PointerButton::Primary,
570 })
571 }
572
573 fn loose() -> BoxConstraints {
574 BoxConstraints::loose(Size::new(100.0, 100.0))
575 }
576
577 #[derive(Default)]
585 struct RetainedRecordingScene {
586 rects: Vec<(Point, Size)>,
587 layers: Vec<(Point, Size, f32)>,
588 transform_pushes: u32,
589 retained_pushes: Vec<(u64, Point, Size, f32, f64)>,
590 retained_pops: u32,
591 draws: Vec<(u64, Point, Size, f32, f64)>,
592 }
593 impl PaintScene for RetainedRecordingScene {
594 fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
595 self.rects.push((origin, size));
596 }
597 fn draw_text(&mut self, _origin: Point, _text: &str) {}
598 fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
599 self.layers.push((origin, size, alpha));
600 }
601 fn push_transform(&mut self, _transform: kurbo::Affine) {
602 self.transform_pushes += 1;
603 }
604 fn push_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
605 self.retained_pushes.push((key, origin, size, alpha, scale));
606 }
607 fn pop_retained(&mut self) {
608 self.retained_pops += 1;
609 }
610 fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
611 self.draws.push((key, origin, size, alpha, scale));
612 }
613 }
614
615 struct Recorder {
623 cancelled: Rc<Cell<bool>>,
624 torn: Rc<Cell<u32>>,
625 teardowns: Rc<Cell<u32>>,
626 }
627 struct RecorderWidget {
628 cancelled: Rc<Cell<bool>>,
629 torn: Rc<Cell<u32>>,
630 }
631 impl View<()> for Recorder {
632 type Element = RecorderWidget;
633 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
634 RecorderWidget {
635 cancelled: self.cancelled.clone(),
636 torn: self.torn.clone(),
637 }
638 }
639 fn rebuild(
640 &self,
641 _prev: &Self,
642 _element: &mut RecorderWidget,
643 _ctx: &mut BuildCtx<'_>,
644 ) -> ChangeFlags {
645 ChangeFlags::NONE
646 }
647 fn teardown(&self, _element: &mut RecorderWidget, _ctx: &mut BuildCtx<'_>) {
648 self.teardowns.set(self.teardowns.get() + 1);
649 }
650 }
651 impl Widget for RecorderWidget {
652 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
653 bc.max()
654 }
655 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
656 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
657 if let InputEvent::Pointer(p) = event {
658 match p.phase {
659 PointerPhase::Down => {
660 ctx.capture_pointer();
661 ctx.request_focus();
662 return EventResult::Handled;
663 }
664 PointerPhase::Cancel => {
667 self.cancelled.set(true);
668 return EventResult::Handled;
669 }
670 _ => {}
671 }
672 }
673 EventResult::Ignored
674 }
675 }
676 impl Drop for RecorderWidget {
677 fn drop(&mut self) {
678 self.torn.set(self.torn.get() + 1);
679 }
680 }
681
682 fn recorder(cancelled: &Rc<Cell<bool>>, torn: &Rc<Cell<u32>>) -> Recorder {
683 Recorder {
684 cancelled: cancelled.clone(),
685 torn: torn.clone(),
686 teardowns: Rc::new(Cell::new(0)),
687 }
688 }
689
690 fn counting_recorder(torn: &Rc<Cell<u32>>, teardowns: &Rc<Cell<u32>>) -> Recorder {
692 Recorder {
693 cancelled: Rc::new(Cell::new(false)),
694 torn: torn.clone(),
695 teardowns: teardowns.clone(),
696 }
697 }
698
699 fn reduced_motion_theme() -> Theme {
700 let mut theme = Theme::neutral();
701 theme.motion.reduce_motion = true;
702 theme
703 }
704
705 #[test]
708 fn same_identity_rebuild_does_not_transition_but_changed_identity_does() {
709 let v1: PatternSwitcherView<(), _> =
710 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
711 let mut w = build(&v1);
712 assert!(w.exiting.is_none());
713
714 let v2: PatternSwitcherView<(), _> =
716 pattern_switcher(1u32, FadeThrough, leaf_any(20.0, 20.0));
717 let mut counter = 0u64;
718 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
719 assert!(
720 w.exiting.is_none(),
721 "same identity must not stage a transition"
722 );
723 assert!(!w.pending_start);
724
725 let v3: PatternSwitcherView<(), _> =
727 pattern_switcher(2u32, FadeThrough, leaf_any(30.0, 30.0));
728 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
729 assert!(
730 w.exiting.is_some(),
731 "an identity change must retain the exiting child"
732 );
733 assert!(w.pending_start, "the next paint starts the driver");
734 assert_eq!(w.key, 2u32.into());
735 }
736
737 #[test]
740 fn identity_change_cancels_focus_and_clears_ime_on_the_exiting_child() {
741 let cancelled = Rc::new(Cell::new(false));
742 let torn = Rc::new(Cell::new(0u32));
743 let v1: PatternSwitcherView<(), _> =
744 pattern_switcher(1u32, FadeThrough, recorder(&cancelled, &torn));
745 let mut w = build(&v1);
746 let size = w.layout(&mut LayoutCtx::new(), &loose());
747
748 let mut st = ();
750 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
751 w.event(&mut ectx, &down(5.0, 5.0));
752 assert!(w.child.is_active(), "the child captured the pointer");
753 assert!(w.child.is_focused(), "the child took focus");
754
755 let v2: PatternSwitcherView<(), _> =
758 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
759 let mut counter = 0u64;
760 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
761
762 assert!(
763 cancelled.get(),
764 "the exiting child received a synthetic Cancel"
765 );
766 let exiting = w.exiting.as_ref().expect("exiting child retained");
767 assert!(
768 !exiting.is_focused(),
769 "the exiting child's focus was cleared"
770 );
771 assert!(!exiting.is_active(), "its capture path was released");
772 assert!(
773 w.needs_ime_clear,
774 "an IME clear is queued for the next paint"
775 );
776
777 let mut pctx = PaintCtx::new(Point::ZERO, size);
779 let mut scene = RecordingScene::default();
780 w.paint(&mut pctx, &mut scene);
781 let ime = pctx
782 .take_ime_state()
783 .expect("a cleared IME surface was published on the post-switch paint");
784 assert!(!ime.active);
785 assert!(ime.editing.text.is_empty());
786 assert!(!w.needs_ime_clear, "the queued clear was consumed");
787 }
788
789 #[test]
794 fn freeze_teardown_of_a_focused_child_raises_no_second_focus_release() {
795 let cancelled = Rc::new(Cell::new(false));
796 let torn = Rc::new(Cell::new(0u32));
797 let teardowns = Rc::new(Cell::new(0u32));
798 let v1: PatternSwitcherView<(), _> = pattern_switcher(
799 1u32,
800 FadeThrough,
801 Recorder {
802 cancelled: cancelled.clone(),
803 torn: torn.clone(),
804 teardowns: teardowns.clone(),
805 },
806 );
807 let mut w = build(&v1);
808 let size = w.layout(&mut LayoutCtx::new(), &loose());
809 let mut st = ();
810 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
811 w.event(&mut ectx, &down(5.0, 5.0));
812 assert!(w.child.is_active() && w.child.is_focused());
813
814 let _ = frust_core::take_focus_orphaned();
815 let v2: PatternSwitcherView<(), _> =
816 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
817 let mut counter = 0u64;
818 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
820
821 assert_eq!(
822 teardowns.get(),
823 1,
824 "the focused child was torn down at the switch"
825 );
826 assert!(cancelled.get(), "cancelled exactly as before");
827 assert!(w.needs_ime_clear, "the IME clear is the session release");
828 assert!(
829 !frust_core::take_focus_orphaned(),
830 "the teardown must not raise a second (orphan) release"
831 );
832 }
833
834 fn field_surface() -> ImeState {
839 ImeState {
840 active: true,
841 editing: EditingState {
842 text: "query".to_string(),
843 selection_base: 5,
844 selection_extent: 5,
845 composing_base: -1,
846 composing_extent: -1,
847 },
848 caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
849 content_type: Default::default(),
850 suppress_soft_keyboard: false,
851 }
852 }
853
854 struct PersistentField {
860 size: Size,
861 }
862 struct PersistentFieldWidget {
863 size: Size,
864 }
865 impl<S: 'static> View<S> for PersistentField {
866 type Element = PersistentFieldWidget;
867 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PersistentFieldWidget {
868 PersistentFieldWidget { size: self.size }
869 }
870 fn rebuild(
871 &self,
872 _prev: &Self,
873 _element: &mut PersistentFieldWidget,
874 _ctx: &mut BuildCtx<'_>,
875 ) -> ChangeFlags {
876 ChangeFlags::NONE
877 }
878 }
879 impl Widget for PersistentFieldWidget {
880 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
881 bc.constrain(self.size)
882 }
883 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
884 if ctx.has_focus() {
885 ctx.publish_ime_state(field_surface());
886 }
887 }
888 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
889 if let InputEvent::Pointer(p) = event
890 && p.phase == PointerPhase::Down
891 {
892 ctx.request_focus();
893 ctx.publish_ime_state(field_surface());
894 return EventResult::Handled;
895 }
896 EventResult::Ignored
897 }
898 }
899
900 #[test]
901 fn identity_change_in_an_unfocused_switcher_leaves_a_sibling_fields_session_alive() {
902 let key = Rc::new(Cell::new(1u32));
907 let mut app = {
908 let key = key.clone();
909 move |_: &mut ()| {
910 column()
911 .child(PersistentField {
912 size: Size::new(100.0, 40.0),
913 })
914 .child(pattern_switcher(
915 key.get(),
916 FadeThrough,
917 leaf_any(100.0, 40.0),
918 ))
919 }
920 };
921 let mut root: RenderRoot<(), FlexView<()>> = RenderRoot::new();
922 let mut state = ();
923 root.rebuild(&mut app, &mut state);
924 root.layout(Size::new(100.0, 100.0));
925 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
926
927 root.event(&mut state, &down(5.0, 5.0));
930 assert!(
931 root.is_focus_active(),
932 "the sibling field owns the focus session"
933 );
934 assert_eq!(
935 root.ime_state().map(|s| s.editing.text),
936 Some("query".to_string()),
937 "…and the shell-facing surface is the field's"
938 );
939 let generation = root.focus_ime_generation();
940
941 key.set(2);
944 root.rebuild(&mut app, &mut state);
945 root.layout(Size::new(100.0, 100.0));
946 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
947
948 assert!(
949 root.is_focus_active(),
950 "an identity change in an unfocused switcher must not release the \
951 sibling field's session"
952 );
953 let ime = root
954 .ime_state()
955 .expect("the identity change dropped the sibling field's IME surface entirely");
956 assert!(ime.active, "the field's surface was left inactive");
957 assert_eq!(
958 ime.editing.text, "query",
959 "the field's surface was replaced with the switcher's cleared one"
960 );
961 assert_eq!(
962 root.focus_ime_generation(),
963 generation,
964 "an identity change in an unfocused switcher is not a focus/IME edge"
965 );
966 }
967
968 #[test]
972 fn exiting_child_is_torn_down_at_the_switch_and_dropped_after_settle() {
973 let torn = Rc::new(Cell::new(0u32));
974 let teardowns = Rc::new(Cell::new(0u32));
975 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
976 let v1: PatternSwitcherView<(), _> =
977 pattern_switcher(1u32, FadeThrough, counting_recorder(&torn, &teardowns)).timing(short);
978 let mut w = build(&v1);
979 let size = w.layout(&mut LayoutCtx::new(), &loose());
980
981 let v2: PatternSwitcherView<(), _> =
984 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
985 let mut counter = 0u64;
986 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
987 assert!(w.exiting.is_some());
988 assert_eq!(teardowns.get(), 1, "torn down at the switch, exactly once");
989 assert_eq!(
990 torn.get(),
991 0,
992 "its pod is not dropped while the transition runs"
993 );
994
995 w.paint(
997 &mut PaintCtx::new(Point::ZERO, size),
998 &mut RecordingScene::default(),
999 );
1000 assert!(w.exiting.is_some(), "retained during the transition");
1001 assert_eq!(torn.get(), 0);
1002 assert!(!w.settled);
1003
1004 w.driver
1007 .as_mut()
1008 .expect("driver created on first paint")
1009 .advance(ft_ms(10_000.0));
1010
1011 w.paint(
1013 &mut PaintCtx::new(Point::ZERO, size),
1014 &mut RecordingScene::default(),
1015 );
1016 assert!(w.settled, "paint flags settle once the driver reaches rest");
1017 assert!(
1018 w.exiting.is_some(),
1019 "still retained until the finalize rebuild"
1020 );
1021 assert_eq!(torn.get(), 0);
1022
1023 let v3: PatternSwitcherView<(), _> =
1026 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1027 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1028 assert!(w.exiting.is_none(), "exiting dropped after settle");
1029 assert_eq!(
1030 torn.get(),
1031 1,
1032 "exiting dropped exactly once, after progress 1.0"
1033 );
1034 assert_eq!(
1035 teardowns.get(),
1036 1,
1037 "the settle drop is not a second teardown"
1038 );
1039 assert!(!w.settled);
1040 assert!(w.driver.is_none());
1041
1042 v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1044 assert_eq!((teardowns.get(), torn.get()), (1, 1));
1045 }
1046
1047 #[test]
1052 fn a_navigator_in_the_outgoing_child_unmounts_on_the_switch_but_still_paints() {
1053 use crate::test_support::leaf;
1054 use crate::{NavigatorController, navigator};
1055
1056 let ctrl: NavigatorController<()> = NavigatorController::new();
1057 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1058 let section = |key: u32| -> PatternSwitcherView<(), FadeThrough> {
1059 let child = if key == 1 {
1060 any(navigator(&ctrl, || leaf(10.0, 10.0)))
1061 } else {
1062 leaf_any(10.0, 10.0)
1063 };
1064 pattern_switcher(key, FadeThrough, child).timing(short)
1065 };
1066 let mut counter = 0u64;
1067 let v1 = section(1);
1068 let mut w = build(&v1);
1069 assert!(ctrl.is_mounted());
1070
1071 ctrl.push(|| leaf(40.0, 40.0));
1073 let v2 = section(1);
1074 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1075 assert_eq!(ctrl.depth(), 2);
1076 assert!(
1077 ctrl.back_interest(),
1078 "a mounted depth-2 navigator claims back"
1079 );
1080
1081 let v3 = section(2);
1083 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1084 assert!(!ctrl.is_mounted(), "torn down at the switch, not at settle");
1085 assert!(
1086 !ctrl.back_interest(),
1087 "a torn-down navigator claims nothing, with no re-wire in between"
1088 );
1089
1090 let size = w.layout(&mut LayoutCtx::new(), &loose());
1093 let mut scene = RecordingScene::default();
1094 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1095 assert!(
1096 scene.rects.iter().any(|(_, s)| *s == Size::new(40.0, 40.0)),
1097 "the torn-down navigator's top page still paints: {:?}",
1098 scene.rects
1099 );
1100
1101 w.driver
1103 .as_mut()
1104 .expect("driver created on first paint")
1105 .advance(ft_ms(10_000.0));
1106 w.paint(
1107 &mut PaintCtx::new(Point::ZERO, size),
1108 &mut RecordingScene::default(),
1109 );
1110 assert!(w.settled);
1111 let v4 = section(2);
1112 v4.rebuild(&v3, &mut w, &mut BuildCtx::new(&mut counter));
1113 assert!(w.exiting.is_none());
1114 assert!(!ctrl.is_mounted());
1115 assert!(!ctrl.back_interest());
1116 }
1117
1118 struct ProbeComp {
1125 teardowns: Rc<Cell<u32>>,
1126 state_drops: Rc<Cell<u32>>,
1127 }
1128 struct ProbeState {
1129 drops: Rc<Cell<u32>>,
1130 }
1131 impl Drop for ProbeState {
1132 fn drop(&mut self) {
1133 self.drops.set(self.drops.get() + 1);
1134 }
1135 }
1136 impl frust_core::Component for ProbeComp {
1137 type State = ProbeState;
1138 fn init(&self) -> ProbeState {
1139 ProbeState {
1140 drops: self.state_drops.clone(),
1141 }
1142 }
1143 fn build(&self, _state: &mut ProbeState) -> impl View<ProbeState> {
1144 TeardownLeaf {
1145 teardowns: self.teardowns.clone(),
1146 }
1147 }
1148 }
1149 struct TeardownLeaf {
1151 teardowns: Rc<Cell<u32>>,
1152 }
1153 impl<S: 'static> View<S> for TeardownLeaf {
1154 type Element = crate::test_support::LeafWidget;
1155 fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
1156 View::<()>::build(&crate::test_support::leaf(30.0, 30.0), ctx)
1157 }
1158 fn rebuild(
1159 &self,
1160 _prev: &Self,
1161 _element: &mut Self::Element,
1162 _ctx: &mut BuildCtx<'_>,
1163 ) -> ChangeFlags {
1164 ChangeFlags::NONE
1165 }
1166 fn teardown(&self, _element: &mut Self::Element, _ctx: &mut BuildCtx<'_>) {
1167 self.teardowns.set(self.teardowns.get() + 1);
1168 }
1169 }
1170
1171 #[test]
1172 fn a_component_in_the_outgoing_child_is_torn_down_exactly_once() {
1173 let teardowns = Rc::new(Cell::new(0u32));
1174 let state_drops = Rc::new(Cell::new(0u32));
1175 let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1176 let comp = || {
1177 any(frust_core::component(ProbeComp {
1178 teardowns: teardowns.clone(),
1179 state_drops: state_drops.clone(),
1180 }))
1181 };
1182 let mut counter = 0u64;
1183
1184 let v1: PatternSwitcherView<(), _> =
1186 pattern_switcher(1u32, FadeThrough, comp()).timing(short);
1187 let mut w = build(&v1);
1188 let size = w.layout(&mut LayoutCtx::new(), &loose());
1189 let v2: PatternSwitcherView<(), _> =
1190 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1191 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1192 assert_eq!(
1193 teardowns.get(),
1194 1,
1195 "the component is torn down at the freeze"
1196 );
1197 assert_eq!(
1198 state_drops.get(),
1199 0,
1200 "its widget (and state) is still painting"
1201 );
1202
1203 let mut scene = RecordingScene::default();
1205 w.layout(&mut LayoutCtx::new(), &loose());
1206 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1207 assert!(
1208 scene.rects.iter().any(|(_, s)| *s == Size::new(30.0, 30.0)),
1209 "the disposed component's subtree still paints: {:?}",
1210 scene.rects
1211 );
1212
1213 w.driver
1214 .as_mut()
1215 .expect("driver created on first paint")
1216 .advance(ft_ms(10_000.0));
1217 w.paint(
1218 &mut PaintCtx::new(Point::ZERO, size),
1219 &mut RecordingScene::default(),
1220 );
1221 let v3: PatternSwitcherView<(), _> =
1222 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1223 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1224 assert!(w.exiting.is_none());
1225 assert_eq!(state_drops.get(), 1, "dropped once at settle");
1226 assert_eq!(
1227 teardowns.get(),
1228 1,
1229 "the settle drop is not a second teardown"
1230 );
1231 v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1232 assert_eq!((teardowns.get(), state_drops.get()), (1, 1));
1233
1234 teardowns.set(0);
1237 state_drops.set(0);
1238 let v1: PatternSwitcherView<(), _> =
1239 pattern_switcher(1u32, FadeThrough, comp()).timing(short);
1240 let mut w = build(&v1);
1241 let v2: PatternSwitcherView<(), _> =
1242 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
1243 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1244 assert_eq!((teardowns.get(), state_drops.get()), (1, 0));
1245 v2.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1246 assert!(
1247 w.exiting.is_none(),
1248 "the switcher's teardown drops the exiting pod"
1249 );
1250 assert_eq!(
1251 (teardowns.get(), state_drops.get()),
1252 (1, 1),
1253 "torn down once at the freeze, dropped once by the switcher's teardown"
1254 );
1255 }
1256
1257 #[test]
1261 fn superseding_switches_tear_each_outgoing_child_down_exactly_once() {
1262 let counters = || (Rc::new(Cell::new(0u32)), Rc::new(Cell::new(0u32)));
1263 let (a_drop, a_td) = counters();
1264 let (b_drop, b_td) = counters();
1265 let (c_drop, c_td) = counters();
1266 let mut counter = 0u64;
1267
1268 let va: PatternSwitcherView<(), _> =
1269 pattern_switcher(1u32, FadeThrough, counting_recorder(&a_drop, &a_td));
1270 let mut w = build(&va);
1271 let vb: PatternSwitcherView<(), _> =
1272 pattern_switcher(2u32, FadeThrough, counting_recorder(&b_drop, &b_td));
1273 vb.rebuild(&va, &mut w, &mut BuildCtx::new(&mut counter));
1274 assert_eq!(
1275 (a_td.get(), a_drop.get()),
1276 (1, 0),
1277 "A frozen: torn down, kept"
1278 );
1279 assert_eq!((b_td.get(), b_drop.get()), (0, 0), "B is live");
1280
1281 let vc: PatternSwitcherView<(), _> =
1284 pattern_switcher(3u32, FadeThrough, counting_recorder(&c_drop, &c_td));
1285 vc.rebuild(&vb, &mut w, &mut BuildCtx::new(&mut counter));
1286 assert_eq!(
1287 (a_td.get(), a_drop.get()),
1288 (1, 1),
1289 "A superseded: dropped only"
1290 );
1291 assert_eq!(
1292 (b_td.get(), b_drop.get()),
1293 (1, 0),
1294 "B frozen: torn down, kept"
1295 );
1296 assert_eq!((c_td.get(), c_drop.get()), (0, 0), "C is live");
1297
1298 vc.teardown(&mut w, &mut BuildCtx::new(&mut counter));
1301 assert_eq!((a_td.get(), a_drop.get()), (1, 1));
1302 assert_eq!((b_td.get(), b_drop.get()), (1, 1), "B dropped, not re-torn");
1303 assert_eq!(
1304 c_td.get(),
1305 1,
1306 "the live child is torn down by the switcher's teardown"
1307 );
1308 drop(w);
1309 assert_eq!(
1310 (a_td.get(), b_td.get(), c_td.get()),
1311 (1, 1, 1),
1312 "each child torn down exactly once"
1313 );
1314 assert_eq!((a_drop.get(), b_drop.get(), c_drop.get()), (1, 1, 1));
1315 }
1316
1317 #[test]
1320 fn same_key_rebuilds_reconcile_in_place_with_no_teardown() {
1321 let torn = Rc::new(Cell::new(0u32));
1322 let teardowns = Rc::new(Cell::new(0u32));
1323 let mut counter = 0u64;
1324 let make = || -> PatternSwitcherView<(), FadeThrough> {
1325 pattern_switcher(7u32, FadeThrough, counting_recorder(&torn, &teardowns))
1326 };
1327 let mut prev = make();
1328 let mut w = build(&prev);
1329 for _ in 0..3 {
1330 let next = make();
1331 next.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
1332 prev = next;
1333 }
1334 assert!(w.exiting.is_none(), "no transition staged");
1335 assert_eq!(
1336 (teardowns.get(), torn.get()),
1337 (0, 0),
1338 "the live child was reconciled in place: never torn down, never dropped"
1339 );
1340 }
1341
1342 #[test]
1345 fn reduce_motion_collapses_to_fast_crossfade() {
1346 let theme = reduced_motion_theme();
1347 let v1: PatternSwitcherView<(), _> =
1351 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
1352 let mut w = build(&v1);
1353 let size = w.layout(&mut LayoutCtx::new(), &loose());
1354
1355 let v2: PatternSwitcherView<(), _> =
1356 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
1357 let mut counter = 0u64;
1358 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1359
1360 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
1361 w.paint(&mut pctx, &mut RecordingScene::default());
1362
1363 assert!(
1364 w.reduce_motion,
1365 "reduce_motion must be resolved from the threaded theme"
1366 );
1367 assert_eq!(
1370 w.child.origin(),
1371 Point::ZERO,
1372 "reduce_motion collapses to a non-directional crossfade (no slide)"
1373 );
1374 }
1375
1376 #[test]
1377 fn without_reduce_motion_the_configured_directional_pattern_slides() {
1378 let v1: PatternSwitcherView<(), _> =
1381 pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
1382 let mut w = build(&v1);
1383 let size = w.layout(&mut LayoutCtx::new(), &loose());
1384
1385 let v2: PatternSwitcherView<(), _> =
1386 pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
1387 let mut counter = 0u64;
1388 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1389
1390 w.paint(
1391 &mut PaintCtx::new(Point::ZERO, size),
1392 &mut RecordingScene::default(),
1393 );
1394 assert!(!w.reduce_motion);
1395 assert_eq!(w.child.origin().x, 30.0);
1398 }
1399
1400 #[test]
1403 fn steady_state_paints_the_single_child_with_no_layer_or_transform() {
1404 let v: PatternSwitcherView<(), _> =
1405 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1406 let mut w = build(&v);
1407 let size = w.layout(&mut LayoutCtx::new(), &loose());
1408
1409 let mut scene = RecordingScene::default();
1410 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
1411 assert_eq!(scene.rects.len(), 1, "the single child paints its fill");
1412 assert!(
1413 scene.layers.is_empty(),
1414 "no crossfade layer at steady state"
1415 );
1416 assert!(
1417 scene.transforms.is_empty(),
1418 "no scale transform at steady state"
1419 );
1420 }
1421
1422 #[test]
1427 fn alpha_zero_staged_child_records_no_ops_and_no_zero_alpha_layer() {
1428 let leaf = leaf_any(10.0, 10.0);
1429 let mut counter = 0u64;
1430 let mut pod = crate::authoring::build_child(&leaf, &mut BuildCtx::new(&mut counter));
1431 let size = pod.layout_child(&mut LayoutCtx::new(), &loose());
1432 pod.set_origin(Point::ZERO);
1433
1434 let mut scene = RecordingScene::default();
1435 let layer = PatternLayer {
1436 dx: 0.0,
1437 dy: 0.0,
1438 alpha: 0.0,
1439 scale: 1.0,
1440 };
1441 paint_staged_child(
1442 &mut pod,
1443 &mut RetainedBoundary::new(),
1444 &mut PaintCtx::new(Point::ZERO, size),
1445 &mut scene,
1446 layer,
1447 );
1448
1449 assert!(scene.rects.is_empty(), "an alpha-0 child records no fill");
1450 assert!(
1451 scene.layers.is_empty(),
1452 "no zero-alpha push_layer is recorded either"
1453 );
1454 }
1455
1456 fn resident(keys: &[RetainedKey]) -> RetainedLedger {
1466 RetainedLedger::from_entries(1, keys.iter().map(|k| (k.get(), (10, 10), 1.0)))
1467 }
1468
1469 fn switched<P: TransitionPattern + Clone + 'static>(
1472 pattern: P,
1473 ) -> (PatternSwitcherWidget<P>, Size, RetainedKey) {
1474 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
1475 let v1: PatternSwitcherView<(), _> =
1476 pattern_switcher(1u32, pattern.clone(), leaf_any(10.0, 10.0)).timing(timing);
1477 let mut w = build(&v1);
1478 let size = w.layout(&mut LayoutCtx::new(), &loose());
1479 let live_key = w.child_boundary.key();
1480 let v2: PatternSwitcherView<(), _> =
1481 pattern_switcher(2u32, pattern, leaf_any(10.0, 10.0)).timing(timing);
1482 let mut counter = 0u64;
1483 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1484 w.layout(&mut LayoutCtx::new(), &loose());
1485 (w, size, live_key)
1486 }
1487
1488 #[test]
1489 fn the_seed_frame_records_the_exiting_child_under_the_key_it_held_while_live() {
1490 let (mut w, size, live_key) = switched(SharedAxis::X);
1491 assert_eq!(
1492 w.exiting_boundary.key(),
1493 live_key,
1494 "the frozen pod keeps the boundary it painted through while live"
1495 );
1496 assert_ne!(
1497 w.child_boundary.key(),
1498 live_key,
1499 "the fresh child takes the other key"
1500 );
1501
1502 let ledger = RetainedLedger::empty();
1503 let mut scene = RetainedRecordingScene::default();
1504 w.paint(
1505 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
1506 &mut scene,
1507 );
1508 assert_eq!(
1511 scene.retained_pushes,
1512 vec![(live_key.get(), Point::ZERO, size, 1.0, 1.0)],
1513 "one recording bracket, keyed by the exiting child's own key, at scale 1"
1514 );
1515 assert_eq!(scene.retained_pops, 1, "the bracket is balanced");
1516 assert!(scene.draws.is_empty(), "nothing is resident yet");
1517 }
1518
1519 #[test]
1520 fn a_clean_resident_exiting_child_composites_its_page_as_it_fades() {
1521 let (mut w, size, live_key) = switched(FadeScale);
1522 let empty = RetainedLedger::empty();
1523 w.paint(
1524 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&empty),
1525 &mut RetainedRecordingScene::default(),
1526 );
1527
1528 let ledger = resident(&[live_key]);
1531 let mut scene = RetainedRecordingScene::default();
1532 w.paint(
1533 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)).with_retained_ledger(&ledger),
1534 &mut scene,
1535 );
1536 assert_eq!(
1537 scene.draws.len(),
1538 1,
1539 "the exiting page is drawn: {:?}",
1540 scene.draws
1541 );
1542 let (key, origin, draw_size, alpha, scale) = scene.draws[0];
1543 assert_eq!(
1544 (key, origin, draw_size, scale),
1545 (live_key.get(), Point::ZERO, size, 1.0)
1546 );
1547 assert!(
1548 alpha > 0.0 && alpha < 1.0,
1549 "at the fade's own alpha (was {alpha})"
1550 );
1551 assert_eq!(
1552 scene.rects.len(),
1553 1,
1554 "only the incoming child paints a body; the exiting one is composited"
1555 );
1556 assert!(
1557 scene.retained_pushes.is_empty(),
1558 "the incoming child's scale step is never bracketed: {:?}",
1559 scene.retained_pushes
1560 );
1561 assert_eq!(
1562 scene.transform_pushes, 1,
1563 "the incoming child's scale step paints inline under a push_transform"
1564 );
1565 }
1566
1567 #[test]
1568 fn without_a_ledger_staged_children_paint_with_no_bracket() {
1569 let (mut w, size, _) = switched(FadeScale);
1570 w.paint(
1571 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
1572 &mut RetainedRecordingScene::default(),
1573 );
1574 let mut scene = RetainedRecordingScene::default();
1575 w.paint(
1576 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
1577 &mut scene,
1578 );
1579 assert!(scene.retained_pushes.is_empty() && scene.draws.is_empty());
1580 assert_eq!(scene.rects.len(), 2, "both children paint their bodies");
1581 assert_eq!(scene.layers.len(), 2, "each under its own fade layer");
1582 }
1583
1584 #[test]
1585 fn a_slid_child_is_never_recorded_while_it_overhangs_the_switcher() {
1586 let (mut w, size, _) = switched(SharedAxis::X);
1590 let ledger = RetainedLedger::empty();
1591 w.paint(
1592 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
1593 &mut RetainedRecordingScene::default(),
1594 );
1595 let mut scene = RetainedRecordingScene::default();
1596 w.paint(
1597 &mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)).with_retained_ledger(&ledger),
1598 &mut scene,
1599 );
1600 assert!(
1601 scene.retained_pushes.is_empty(),
1602 "no bracket: {:?}",
1603 scene.retained_pushes
1604 );
1605 assert_eq!(
1606 scene.rects,
1607 vec![(Point::new(15.0, 0.0), size)],
1608 "the incoming child paints at its slid origin"
1609 );
1610 }
1611
1612 fn key_pair<P: TransitionPattern + Clone + 'static>(w: &PatternSwitcherWidget<P>) -> [u64; 2] {
1616 let mut keys = [w.child_boundary.key().get(), w.exiting_boundary.key().get()];
1617 keys.sort_unstable();
1618 keys
1619 }
1620
1621 #[test]
1622 fn the_key_pair_is_stable_across_multiple_switches_on_the_same_widget() {
1623 let v1: PatternSwitcherView<(), _> =
1624 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1625 let mut w = build(&v1);
1626 let pair = key_pair(&w);
1627 assert_eq!(pair[1], pair[0] + 1, "a reserved consecutive pair");
1628 let mut counter = 0u64;
1629
1630 let v2: PatternSwitcherView<(), _> =
1631 pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
1632 v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
1633 assert_eq!(
1634 key_pair(&w),
1635 pair,
1636 "a switch does not reassign the key pair"
1637 );
1638
1639 let v3: PatternSwitcherView<(), _> =
1640 pattern_switcher(3u32, FadeThrough, leaf_any(10.0, 10.0));
1641 v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
1642 assert_eq!(
1643 key_pair(&w),
1644 pair,
1645 "a second switch on the same widget still keeps the same key pair"
1646 );
1647 }
1648
1649 #[test]
1652 fn two_switcher_instances_get_disjoint_key_pairs() {
1653 let v: PatternSwitcherView<(), _> =
1654 pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
1655 let w1 = build(&v);
1656 let w2 = build(&v);
1657 let (a, b) = (key_pair(&w1), key_pair(&w2));
1658 assert!(
1659 a.iter().all(|k| !b.contains(k)),
1660 "two switcher instances never share a key: {a:?} vs {b:?}"
1661 );
1662 }
1663}