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