1use std::cell::Cell;
15use std::rc::Rc;
16
17use frust_core::{
18 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EditingState, EventCtx, EventResult,
19 ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, ViewSeq, Widget,
20 any,
21};
22use kurbo::{Point, Size};
23
24use crate::nav::{ambient_page_reach, with_page_reach};
25
26pub struct IndexedStackView<State: 'static> {
29 index: Option<usize>,
30 children: Vec<AnyView<State>>,
31}
32
33#[allow(non_snake_case)]
94pub fn IndexedStack<State: 'static, M>(
95 index: Option<usize>,
96 children: impl ViewSeq<State, M>,
97) -> IndexedStackView<State> {
98 indexed_stack(index).children(children)
99}
100
101pub fn indexed_stack<State: 'static>(index: Option<usize>) -> IndexedStackView<State> {
104 IndexedStackView {
105 index,
106 children: Vec::new(),
107 }
108}
109
110impl<State: 'static> IndexedStackView<State> {
111 pub fn child<V: View<State>>(mut self, view: V) -> Self {
113 self.children.push(any(view));
114 self
115 }
116
117 pub fn children<M>(mut self, children: impl ViewSeq<State, M>) -> Self {
122 children.extend_views(&mut self.children);
123 self
124 }
125
126 pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
128 if cond { f(self) } else { self }
129 }
130
131 pub fn when_some<T>(self, opt: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
133 match opt {
134 Some(value) => f(self, value),
135 None => self,
136 }
137 }
138}
139
140pub struct IndexedStackWidget {
142 children: Vec<ChildPod>,
143 reach: Vec<Rc<Cell<bool>>>,
147 index: Option<usize>,
148 needs_ime_clear: bool,
151}
152
153fn parent_reachable() -> bool {
156 ambient_page_reach().is_none_or(|cell| cell.get())
157}
158
159impl IndexedStackWidget {
160 fn selected_mut(&mut self) -> Option<&mut ChildPod> {
162 self.index.and_then(|i| self.children.get_mut(i))
163 }
164
165 fn selected(&self) -> Option<&ChildPod> {
166 self.index.and_then(|i| self.children.get(i))
167 }
168
169 fn switch_away(&mut self, ctx: &BuildCtx<'_>) {
176 let Some(pod) = self.selected_mut() else {
177 return;
178 };
179 let outgoing_focused = pod.is_focused();
180 if pod.is_active() {
181 crate::authoring::cancel_pod(pod);
182 pod.set_active(false);
183 }
184 if outgoing_focused {
185 pod.set_focused(false);
186 }
187 if outgoing_focused && ctx.has_focus() {
188 self.needs_ime_clear = true;
189 }
190 }
191
192 fn publish_reach(&self, parent_reachable: bool) {
194 for (i, cell) in self.reach.iter().enumerate() {
195 cell.set(parent_reachable && Some(i) == self.index);
196 }
197 }
198}
199
200impl<State: 'static> View<State> for IndexedStackView<State> {
201 type Element = IndexedStackWidget;
202
203 fn build(&self, ctx: &mut BuildCtx<'_>) -> IndexedStackWidget {
204 let parent = parent_reachable();
205 let reach: Vec<Rc<Cell<bool>>> = (0..self.children.len())
206 .map(|i| Rc::new(Cell::new(parent && Some(i) == self.index)))
207 .collect();
208 let children = self
209 .children
210 .iter()
211 .zip(&reach)
212 .map(|(view, cell)| with_page_reach(cell, || crate::authoring::build_child(view, ctx)))
213 .collect();
214 IndexedStackWidget {
215 children,
216 reach,
217 index: self.index,
218 needs_ime_clear: false,
219 }
220 }
221
222 fn rebuild(
223 &self,
224 prev: &Self,
225 element: &mut IndexedStackWidget,
226 ctx: &mut BuildCtx<'_>,
227 ) -> ChangeFlags {
228 let mut flags = ChangeFlags::NONE;
229 let parent = parent_reachable();
232
233 if element.index != self.index {
235 element.switch_away(ctx);
236 element.index = self.index;
237 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
238 }
239
240 let next_len = self.children.len();
245 let removed_reach = if element.reach.len() > next_len {
246 element.reach.split_off(next_len)
247 } else {
248 Vec::new()
249 };
250 for cell in &removed_reach {
251 cell.set(false);
252 }
253 element
254 .reach
255 .resize_with(next_len, || Rc::new(Cell::new(false)));
256 element.publish_reach(parent);
257
258 let IndexedStackWidget {
267 children: pods,
268 reach,
269 ..
270 } = element;
271 let common = prev.children.len().min(next_len);
272 for i in 0..common {
273 let (prev_view, next_view, pod) = (&prev.children[i], &self.children[i], &mut pods[i]);
274 flags |= with_page_reach(&reach[i], || {
275 crate::authoring::rebuild_child(prev_view, next_view, pod, ctx)
276 });
277 }
278 if next_len > common {
279 for (view, cell) in self.children[common..].iter().zip(&reach[common..]) {
280 pods.push(with_page_reach(cell, || {
281 crate::authoring::build_child(view, ctx)
282 }));
283 }
284 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
285 } else if prev.children.len() > next_len {
286 for ((view, pod), cell) in prev.children[next_len..]
287 .iter()
288 .zip(pods[next_len..].iter_mut())
289 .zip(&removed_reach)
290 {
291 with_page_reach(cell, || crate::authoring::teardown_child(view, pod, ctx));
292 }
293 pods.truncate(next_len);
294 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
295 }
296 flags
297 }
298
299 fn teardown(&self, element: &mut IndexedStackWidget, ctx: &mut BuildCtx<'_>) {
300 for cell in &element.reach {
302 cell.set(false);
303 }
304 for ((view, pod), cell) in self
305 .children
306 .iter()
307 .zip(element.children.iter_mut())
308 .zip(&element.reach)
309 {
310 with_page_reach(cell, || crate::authoring::teardown_child(view, pod, ctx));
311 }
312 }
313}
314
315impl Widget for IndexedStackWidget {
316 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
317 let loose = bc.loosen();
320 let mut max = Size::ZERO;
321 for pod in &mut self.children {
322 let size = pod.layout_child(ctx, &loose);
323 pod.set_origin(Point::ZERO);
324 max = Size::new(max.width.max(size.width), max.height.max(size.height));
325 }
326 bc.constrain(max)
327 }
328
329 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
330 if let Some(pod) = self.selected_mut() {
331 pod.paint_child(ctx, scene);
332 }
333 if self.needs_ime_clear {
336 ctx.publish_ime_state(cleared_ime_state());
337 self.needs_ime_clear = false;
338 }
339 }
340
341 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
342 if matches!(event, InputEvent::Shortcut(_)) {
344 if let Some(pod) = self.selected_mut() {
345 crate::authoring::route_event_single(pod, ctx, event);
346 }
347 return EventResult::Ignored;
348 }
349 if event.is_broadcast() {
350 for pod in &mut self.children {
353 crate::authoring::route_event_single(pod, ctx, event);
354 }
355 return EventResult::Ignored;
356 }
357 match self.selected_mut() {
358 Some(pod) => crate::authoring::route_event_single(pod, ctx, event),
359 None => EventResult::Ignored,
360 }
361 }
362
363 fn semantics(&self, ctx: &mut SemanticsCtx) {
367 if let Some(pod) = self.selected() {
368 pod.semantics_child(ctx);
369 }
370 }
371
372 crate::authoring::visit_children!(children);
375
376 fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
378 if let Some(pod) = self.selected() {
379 visitor(pod);
380 }
381 }
382}
383
384fn cleared_ime_state() -> ImeState {
389 ImeState {
390 active: false,
391 editing: EditingState {
392 text: String::new(),
393 selection_base: -1,
394 selection_extent: -1,
395 composing_base: -1,
396 composing_extent: -1,
397 },
398 caret: None,
399 content_type: Default::default(),
400 suppress_soft_keyboard: false,
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::nav::navigator::{NavigatorController, NavigatorView, navigator};
408 use crate::test_support::{RecordingScene, leaf, probe, swap_leaf};
409 use frust_core::{PointerButton, PointerEvent, PointerPhase, RenderRoot};
410
411 fn build<S: 'static>(view: &IndexedStackView<S>) -> IndexedStackWidget {
412 let mut counter = 0u64;
413 view.build(&mut BuildCtx::new(&mut counter))
414 }
415
416 fn rebuild<S: 'static>(
417 next: &IndexedStackView<S>,
418 prev: &IndexedStackView<S>,
419 w: &mut IndexedStackWidget,
420 ) -> ChangeFlags {
421 let mut counter = 100u64;
422 next.rebuild(prev, w, &mut BuildCtx::new(&mut counter))
423 }
424
425 fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
426 InputEvent::Pointer(PointerEvent {
427 phase,
428 position: Point::new(x, y),
429 button: PointerButton::Primary,
430 })
431 }
432
433 fn down(x: f64, y: f64) -> InputEvent {
434 pointer(PointerPhase::Down, x, y)
435 }
436
437 fn reach_of(w: &IndexedStackWidget) -> Vec<bool> {
438 w.reach.iter().map(|c| c.get()).collect()
439 }
440
441 #[test]
444 fn sizes_to_the_largest_child_and_paints_only_the_selected_one() {
445 let view: IndexedStackView<()> = indexed_stack(Some(1))
446 .child(leaf(30.0, 60.0))
447 .child(leaf(80.0, 20.0));
448 let mut w = build(&view);
449 let size = w.layout(
450 &mut LayoutCtx::new(),
451 &BoxConstraints::loose(Size::new(500.0, 500.0)),
452 );
453 assert_eq!(size, Size::new(80.0, 60.0));
456 assert_eq!(w.children[0].origin(), Point::ZERO);
457 assert_eq!(w.children[1].origin(), Point::ZERO);
458
459 let mut scene = RecordingScene::default();
460 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
461 assert_eq!(
462 scene.rects,
463 vec![(Point::ZERO, Size::new(80.0, 20.0))],
464 "only the selected child paints"
465 );
466 }
467
468 #[test]
469 fn a_filling_child_fills_the_stack_under_tight_constraints() {
470 let view: IndexedStackView<Vec<u32>> =
471 indexed_stack(Some(0)).child(probe(0)).child(probe(1));
472 let mut w = build(&view);
473 let size = w.layout(
474 &mut LayoutCtx::new(),
475 &BoxConstraints::tight(Size::new(100.0, 70.0)),
476 );
477 assert_eq!(size, Size::new(100.0, 70.0));
478 assert_eq!(w.children[0].size(), Size::new(100.0, 70.0));
479 assert_eq!(w.children[1].size(), Size::new(100.0, 70.0));
480 }
481
482 #[test]
485 fn a_pointer_down_reaches_only_the_selected_child() {
486 let view: IndexedStackView<Vec<u32>> =
488 IndexedStack(Some(0), (probe(0), probe(1), probe(2)));
489 let mut w = build(&view);
490 w.layout(
491 &mut LayoutCtx::new(),
492 &BoxConstraints::tight(Size::new(100.0, 100.0)),
493 );
494 let mut log: Vec<u32> = Vec::new();
495 let mut ectx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
496 let result = w.event(&mut ectx, &down(50.0, 50.0));
497 assert_eq!(result, EventResult::Handled);
498 assert_eq!(log, vec![0], "only the selected child saw the Down");
499 }
500
501 #[test]
502 fn a_broadcast_reaches_every_child_and_is_not_consumed() {
503 let view: IndexedStackView<Vec<u32>> = IndexedStack(Some(1), (probe(0), probe(1)));
504 let mut w = build(&view);
505 w.layout(
506 &mut LayoutCtx::new(),
507 &BoxConstraints::tight(Size::new(100.0, 100.0)),
508 );
509 let mut log: Vec<u32> = Vec::new();
510 let mut ectx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
511 let result = w.event(&mut ectx, &InputEvent::Housekeeping);
512 assert_eq!(result, EventResult::Ignored);
513 assert_eq!(
514 log,
515 vec![0, 1],
516 "housekeeping is not input: every tab gets it"
517 );
518 }
519
520 #[test]
521 fn a_shortcut_reaches_only_the_selected_child() {
522 let view: IndexedStackView<Vec<u32>> = IndexedStack(Some(1), (probe(0), probe(1)));
523 let mut w = build(&view);
524 w.layout(
525 &mut LayoutCtx::new(),
526 &BoxConstraints::tight(Size::new(100.0, 100.0)),
527 );
528 let mut log: Vec<u32> = Vec::new();
529 let mut ectx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
530 let shortcut = InputEvent::Shortcut(frust_core::ShortcutId::next());
531 assert_eq!(w.event(&mut ectx, &shortcut), EventResult::Ignored);
532 assert_eq!(log, vec![1], "an unselected tab never answers a shortcut");
533 }
534
535 struct Labelled {
538 label: &'static str,
539 }
540 struct LabelledWidget {
541 label: &'static str,
542 }
543 impl View<()> for Labelled {
544 type Element = LabelledWidget;
545 fn build(&self, _ctx: &mut BuildCtx<'_>) -> LabelledWidget {
546 LabelledWidget { label: self.label }
547 }
548 fn rebuild(
549 &self,
550 _prev: &Self,
551 element: &mut LabelledWidget,
552 _ctx: &mut BuildCtx<'_>,
553 ) -> ChangeFlags {
554 element.label = self.label;
555 ChangeFlags::NONE
556 }
557 }
558 impl Widget for LabelledWidget {
559 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
560 bc.max()
561 }
562 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
563 fn semantics(&self, ctx: &mut SemanticsCtx) {
564 ctx.push_node(frust_core::accesskit::Role::Button, |node| {
565 node.set_label(self.label)
566 });
567 }
568 }
569
570 fn labels(root: &RenderRoot<(), IndexedStackView<()>>) -> Vec<String> {
571 root.semantics()
572 .nodes
573 .iter()
574 .filter_map(|(_, node)| node.label().map(str::to_string))
575 .collect()
576 }
577
578 #[test]
579 fn semantics_expose_only_the_selected_child() {
580 let selected = Rc::new(Cell::new(Some(1usize)));
581 let mut app = {
582 let selected = selected.clone();
583 move |_: &mut ()| {
584 IndexedStack(
585 selected.get(),
586 (
587 Labelled { label: "tab-a" },
588 Labelled { label: "tab-b" },
589 Labelled { label: "tab-c" },
590 ),
591 )
592 }
593 };
594 let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
595 root.rebuild(&mut app, &mut ());
596 root.layout(Size::new(100.0, 100.0));
597 assert_eq!(labels(&root), vec!["tab-b".to_string()]);
598
599 selected.set(Some(2));
600 root.rebuild(&mut app, &mut ());
601 root.layout(Size::new(100.0, 100.0));
602 assert_eq!(labels(&root), vec!["tab-c".to_string()]);
603
604 selected.set(None);
605 root.rebuild(&mut app, &mut ());
606 root.layout(Size::new(100.0, 100.0));
607 assert!(labels(&root).is_empty(), "no selection, no nodes");
608 }
609
610 #[test]
613 fn only_the_selected_tabs_navigator_claims_back() {
614 let a: NavigatorController<()> = NavigatorController::new();
615 let b: NavigatorController<()> = NavigatorController::new();
616 let selected = Rc::new(Cell::new(0usize));
617 let mut app = {
618 let (a, b, selected) = (a.clone(), b.clone(), selected.clone());
619 move |_: &mut ()| {
620 indexed_stack(Some(selected.get()))
621 .child(navigator(&a, || leaf(10.0, 10.0)))
622 .child(navigator(&b, || leaf(10.0, 10.0)))
623 }
624 };
625 let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
626 root.rebuild(&mut app, &mut ());
627 a.push(|| leaf(20.0, 20.0));
628 b.push(|| leaf(20.0, 20.0));
629 root.rebuild(&mut app, &mut ());
630 assert_eq!((a.depth(), b.depth()), (2, 2), "both stacks were pushed");
631 assert!(a.back_interest(), "the selected tab claims back");
632 assert!(!b.back_interest(), "the unselected tab claims nothing");
633
634 selected.set(1);
635 root.rebuild(&mut app, &mut ());
636 assert!(!a.back_interest(), "switched away: tab A claims nothing");
637 assert!(b.back_interest(), "switched to: tab B claims back");
638 assert!(a.is_mounted() && b.is_mounted(), "both stay mounted");
639 assert_eq!((a.depth(), b.depth()), (2, 2), "both keep their stacks");
640 }
641
642 #[test]
645 fn a_navigator_two_levels_down_claims_back_only_while_its_tab_is_selected() {
646 let outer: NavigatorController<()> = NavigatorController::new();
647 let inner: NavigatorController<()> = NavigatorController::new();
648 let selected = Rc::new(Cell::new(0usize));
649 let mut app = {
650 let (outer, inner, selected) = (outer.clone(), inner.clone(), selected.clone());
651 move |_: &mut ()| {
652 let inner = inner.clone();
653 indexed_stack(Some(selected.get()))
654 .child(navigator(&outer, move || {
655 navigator(&inner, || leaf(10.0, 10.0))
656 }))
657 .child(leaf(10.0, 10.0))
658 }
659 };
660 let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
661 root.rebuild(&mut app, &mut ());
662 inner.push(|| leaf(20.0, 20.0));
663 root.rebuild(&mut app, &mut ());
664 assert_eq!(inner.depth(), 2);
665 assert!(
666 inner.back_interest(),
667 "selected tab: the inner stack claims"
668 );
669
670 selected.set(1);
671 root.rebuild(&mut app, &mut ());
672 assert!(!inner.back_interest(), "switched away: nothing claims");
673 assert!(!outer.back_interest());
674 root.rebuild(&mut app, &mut ());
676 assert!(!inner.back_interest(), "still switched away");
677
678 selected.set(0);
679 root.rebuild(&mut app, &mut ());
680 assert!(
681 inner.back_interest(),
682 "switched back: the inner stack claims"
683 );
684 assert_eq!(inner.depth(), 2, "and kept its stack throughout");
685 }
686
687 #[test]
690 fn a_covered_host_page_silences_every_tab_navigator() {
691 for cull in [false, true] {
692 let host: NavigatorController<()> = NavigatorController::new();
693 let t0: NavigatorController<()> = NavigatorController::new();
694 let t1: NavigatorController<()> = NavigatorController::new();
695 let mut app = {
696 let (host, t0, t1) = (host.clone(), t0.clone(), t1.clone());
697 move |_: &mut ()| {
698 let (t0, t1) = (t0.clone(), t1.clone());
699 navigator(&host, move || {
700 indexed_stack(Some(0))
701 .child(navigator(&t0, || leaf(10.0, 10.0)))
702 .child(navigator(&t1, || leaf(10.0, 10.0)))
703 })
704 .cull_covered_builds(cull)
705 }
706 };
707 let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
708 root.rebuild(&mut app, &mut ());
709 t0.push(|| leaf(20.0, 20.0));
710 t1.push(|| leaf(20.0, 20.0));
711 root.rebuild(&mut app, &mut ());
712 assert!(t0.back_interest(), "selected tab claims (cull={cull})");
713 assert!(!t1.back_interest(), "unselected tab silent (cull={cull})");
714
715 host.push(|| leaf(30.0, 30.0));
716 root.rebuild(&mut app, &mut ());
717 assert!(!t0.back_interest(), "covered: tab 0 silent (cull={cull})");
718 assert!(!t1.back_interest(), "covered: tab 1 silent (cull={cull})");
719 assert!(
720 host.back_interest(),
721 "the host claims instead (cull={cull})"
722 );
723 for _ in 0..3 {
724 root.rebuild(&mut app, &mut ());
725 }
726 assert!(
727 !t0.back_interest(),
728 "stays silent while covered (cull={cull})"
729 );
730
731 host.pop();
732 root.rebuild(&mut app, &mut ());
733 assert!(t0.back_interest(), "revealed: tab 0 claims (cull={cull})");
734 assert!(!t1.back_interest(), "tab 1 still silent (cull={cull})");
735 }
736 }
737
738 #[test]
741 fn no_or_out_of_range_index_selects_nothing() {
742 for index in [None, Some(2), Some(99)] {
743 let view: IndexedStackView<Vec<u32>> = IndexedStack(index, (probe(0), probe(1)));
744 let mut w = build(&view);
745 assert_eq!(reach_of(&w), vec![false, false], "{index:?}: no reach");
746 let size = w.layout(
747 &mut LayoutCtx::new(),
748 &BoxConstraints::tight(Size::new(50.0, 50.0)),
749 );
750 assert_eq!(size, Size::new(50.0, 50.0), "children are still laid out");
751
752 let mut log: Vec<u32> = Vec::new();
753 let mut ectx = EventCtx::new(&mut log, Point::ZERO, size);
754 assert_eq!(w.event(&mut ectx, &down(5.0, 5.0)), EventResult::Ignored);
755 assert!(log.is_empty(), "{index:?}: nothing routed");
756 }
757
758 let view: IndexedStackView<()> = indexed_stack(None)
759 .child(leaf(10.0, 10.0))
760 .child(leaf(20.0, 20.0));
761 let mut w = build(&view);
762 let size = w.layout(
763 &mut LayoutCtx::new(),
764 &BoxConstraints::loose(Size::new(100.0, 100.0)),
765 );
766 let mut scene = RecordingScene::default();
767 w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
768 assert!(scene.rects.is_empty(), "nothing painted");
769
770 let nav: NavigatorController<()> = NavigatorController::new();
772 let mut app = {
773 let nav = nav.clone();
774 move |_: &mut ()| indexed_stack(None).child(navigator(&nav, || leaf(10.0, 10.0)))
775 };
776 let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
777 root.rebuild(&mut app, &mut ());
778 nav.push(|| leaf(20.0, 20.0));
779 root.rebuild(&mut app, &mut ());
780 assert_eq!(nav.depth(), 2);
781 assert!(!nav.back_interest(), "no selection: no back reach");
782 }
783
784 #[test]
785 fn reach_cells_follow_child_count_changes() {
786 let v1: IndexedStackView<()> = indexed_stack(Some(1)).child(leaf(1.0, 1.0));
787 let mut w = build(&v1);
788 assert_eq!(
789 reach_of(&w),
790 vec![false],
791 "index past the end selects nothing"
792 );
793
794 let v2: IndexedStackView<()> = indexed_stack(Some(1))
796 .child(leaf(1.0, 1.0))
797 .child(leaf(2.0, 2.0))
798 .child(leaf(3.0, 3.0));
799 let flags = rebuild(&v2, &v1, &mut w);
800 assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
801 assert_eq!(w.children.len(), 3);
802 assert_eq!(reach_of(&w), vec![false, true, false]);
803
804 let dropped = Rc::clone(&w.reach[2]);
806 let v3: IndexedStackView<()> = indexed_stack(Some(1))
807 .child(leaf(1.0, 1.0))
808 .child(leaf(2.0, 2.0));
809 let flags = rebuild(&v3, &v2, &mut w);
810 assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
811 assert_eq!(w.children.len(), 2);
812 assert_eq!(reach_of(&w), vec![false, true]);
813 assert!(!dropped.get());
814
815 let v4: IndexedStackView<()> = indexed_stack(Some(1))
817 .child(leaf(1.0, 1.0))
818 .child(leaf(2.0, 2.0));
819 assert_eq!(rebuild(&v4, &v3, &mut w), ChangeFlags::NONE);
820
821 let v5: IndexedStackView<()> = indexed_stack(Some(0))
823 .child(leaf(1.0, 1.0))
824 .child(leaf(2.0, 2.0));
825 let flags = rebuild(&v5, &v4, &mut w);
826 assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
827 assert_eq!(reach_of(&w), vec![true, false]);
828 }
829
830 struct Recorder {
835 cancelled: Rc<Cell<bool>>,
836 ups: Rc<Cell<u32>>,
837 }
838 struct RecorderWidget {
839 cancelled: Rc<Cell<bool>>,
840 ups: Rc<Cell<u32>>,
841 }
842 impl View<()> for Recorder {
843 type Element = RecorderWidget;
844 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
845 RecorderWidget {
846 cancelled: self.cancelled.clone(),
847 ups: self.ups.clone(),
848 }
849 }
850 fn rebuild(
851 &self,
852 _prev: &Self,
853 _element: &mut RecorderWidget,
854 _ctx: &mut BuildCtx<'_>,
855 ) -> ChangeFlags {
856 ChangeFlags::NONE
857 }
858 }
859 impl Widget for RecorderWidget {
860 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
861 bc.max()
862 }
863 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
864 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
865 if let InputEvent::Pointer(p) = event {
866 match p.phase {
867 PointerPhase::Down => {
868 ctx.capture_pointer();
869 ctx.request_focus();
870 return EventResult::Handled;
871 }
872 PointerPhase::Cancel => {
873 self.cancelled.set(true);
874 return EventResult::Handled;
875 }
876 PointerPhase::Up => {
877 self.ups.set(self.ups.get() + 1);
878 return EventResult::Handled;
879 }
880 _ => {}
881 }
882 }
883 EventResult::Ignored
884 }
885 }
886
887 struct Armed {
888 w: IndexedStackWidget,
889 v1: IndexedStackView<()>,
890 cancelled: Rc<Cell<bool>>,
891 ups: Rc<Cell<u32>>,
892 size: Size,
893 }
894
895 fn armed() -> Armed {
897 let cancelled = Rc::new(Cell::new(false));
898 let ups = Rc::new(Cell::new(0u32));
899 let v1: IndexedStackView<()> = indexed_stack(Some(0))
900 .child(Recorder {
901 cancelled: cancelled.clone(),
902 ups: ups.clone(),
903 })
904 .child(leaf(10.0, 10.0));
905 let mut w = build(&v1);
906 let size = w.layout(
907 &mut LayoutCtx::new(),
908 &BoxConstraints::tight(Size::new(100.0, 100.0)),
909 );
910 let mut st = ();
911 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
912 w.event(&mut ectx, &down(5.0, 5.0));
913 assert!(w.children[0].is_active(), "tab 0 captured the pointer");
914 assert!(w.children[0].is_focused(), "tab 0 took focus");
915 Armed {
916 w,
917 v1,
918 cancelled,
919 ups,
920 size,
921 }
922 }
923
924 fn switch_view() -> IndexedStackView<()> {
925 indexed_stack(Some(1))
927 .child(Recorder {
928 cancelled: Rc::new(Cell::new(false)),
929 ups: Rc::new(Cell::new(0)),
930 })
931 .child(leaf(10.0, 10.0))
932 }
933
934 #[test]
935 fn a_selection_change_cancels_capture_clears_focus_and_clears_the_ime() {
936 let Armed {
937 mut w,
938 v1,
939 cancelled,
940 ups,
941 size,
942 } = armed();
943 let v2 = switch_view();
944 rebuild(&v2, &v1, &mut w);
945
946 assert!(
947 cancelled.get(),
948 "the outgoing tab received a synthetic Cancel"
949 );
950 assert!(!w.children[0].is_active(), "its capture path was released");
951 assert!(!w.children[0].is_focused(), "its focus flag was cleared");
952 assert!(w.needs_ime_clear, "a cleared IME publish is queued");
953
954 let mut st = ();
956 let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
957 w.event(&mut ectx, &pointer(PointerPhase::Up, 5.0, 5.0));
958 assert_eq!(ups.get(), 0, "the cancelled tab does not fire on Up");
959
960 let mut pctx = PaintCtx::new(Point::ZERO, size);
961 w.paint(&mut pctx, &mut RecordingScene::default());
962 let ime = pctx
963 .take_ime_state()
964 .expect("the post-switch paint publishes a cleared IME surface");
965 assert!(!ime.active);
966 assert!(!w.needs_ime_clear, "the queued clear was consumed");
967
968 assert!(!w.children[1].is_focused());
970 }
971
972 #[test]
973 fn a_selection_change_off_the_live_focus_chain_publishes_no_ime_clear() {
974 let Armed {
975 mut w,
976 v1,
977 cancelled,
978 size,
979 ..
980 } = armed();
981 let v2 = switch_view();
982 let mut counter = 100u64;
983 let mut ctx = BuildCtx::new(&mut counter);
984 ctx.with_focus_link(false, |ctx| v2.rebuild(&v1, &mut w, ctx));
986
987 assert!(cancelled.get(), "capture is still cancelled");
988 assert!(
989 !w.children[0].is_focused(),
990 "the stale flag is still cleared"
991 );
992 assert!(!w.needs_ime_clear, "but no session release is queued");
993 let mut pctx = PaintCtx::new(Point::ZERO, size);
994 w.paint(&mut pctx, &mut RecordingScene::default());
995 assert!(pctx.take_ime_state().is_none(), "no IME publish");
996 }
997
998 #[test]
999 fn switching_away_from_an_unfocused_tab_publishes_no_ime_clear() {
1000 let v1: IndexedStackView<()> = indexed_stack(Some(0))
1001 .child(leaf(10.0, 10.0))
1002 .child(leaf(10.0, 10.0));
1003 let mut w = build(&v1);
1004 let v2: IndexedStackView<()> = indexed_stack(Some(1))
1005 .child(leaf(10.0, 10.0))
1006 .child(leaf(10.0, 10.0));
1007 rebuild(&v2, &v1, &mut w);
1008 assert!(!w.needs_ime_clear, "the outgoing tab held no session");
1009 }
1010
1011 #[test]
1014 fn fluent_and_tuple_builders_build() {
1015 let fluent: IndexedStackView<()> = indexed_stack(Some(0))
1016 .child(leaf(1.0, 1.0))
1017 .children((swap_leaf(), leaf(2.0, 2.0)))
1018 .when(true, |s| s.child(leaf(3.0, 3.0)))
1019 .when(false, |s| s.child(leaf(4.0, 4.0)))
1020 .when_some(Some(5.0), |s, side| s.child(leaf(side, side)))
1021 .when_some(None::<f64>, |s, side| s.child(leaf(side, side)));
1022 assert_eq!(build(&fluent).children.len(), 5);
1023
1024 let tuple: IndexedStackView<()> =
1025 IndexedStack(Some(1), (leaf(1.0, 1.0), swap_leaf(), leaf(2.0, 2.0)));
1026 let w = build(&tuple);
1027 assert_eq!(w.children.len(), 3);
1028 assert_eq!(reach_of(&w), vec![false, true, false]);
1029 }
1030
1031 #[test]
1032 fn traversal_enters_only_the_selected_child() {
1033 let view: IndexedStackView<Vec<u32>> =
1034 IndexedStack(Some(1), (probe(0), probe(1), probe(2)));
1035 let w = build(&view);
1036 let mut all = 0;
1037 w.visit_children(&mut |_| all += 1);
1038 let mut entered: Vec<*const ChildPod> = Vec::new();
1039 w.visit_focus_children(&mut |pod| entered.push(pod));
1040 assert_eq!(all, 3, "inspection still sees every retained child");
1041 assert_eq!(entered, vec![&w.children[1] as *const ChildPod]);
1042
1043 let none: IndexedStackView<Vec<u32>> = IndexedStack(None, (probe(0),));
1044 let mut entered = 0;
1045 build(&none).visit_focus_children(&mut |_| entered += 1);
1046 assert_eq!(entered, 0, "nothing selected, nothing to traverse");
1047 }
1048}