use std::cell::Cell;
use std::rc::Rc;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EditingState, EventCtx, EventResult,
ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, ViewSeq, Widget,
any,
};
use kurbo::{Point, Size};
use crate::nav::{ambient_page_reach, with_page_reach};
pub struct IndexedStackView<State: 'static> {
index: Option<usize>,
children: Vec<AnyView<State>>,
}
#[allow(non_snake_case)]
pub fn IndexedStack<State: 'static, M>(
index: Option<usize>,
children: impl ViewSeq<State, M>,
) -> IndexedStackView<State> {
indexed_stack(index).children(children)
}
pub fn indexed_stack<State: 'static>(index: Option<usize>) -> IndexedStackView<State> {
IndexedStackView {
index,
children: Vec::new(),
}
}
impl<State: 'static> IndexedStackView<State> {
pub fn child<V: View<State>>(mut self, view: V) -> Self {
self.children.push(any(view));
self
}
pub fn children<M>(mut self, children: impl ViewSeq<State, M>) -> Self {
children.extend_views(&mut self.children);
self
}
pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
if cond { f(self) } else { self }
}
pub fn when_some<T>(self, opt: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
match opt {
Some(value) => f(self, value),
None => self,
}
}
}
pub struct IndexedStackWidget {
children: Vec<ChildPod>,
reach: Vec<Rc<Cell<bool>>>,
index: Option<usize>,
needs_ime_clear: bool,
}
fn parent_reachable() -> bool {
ambient_page_reach().is_none_or(|cell| cell.get())
}
impl IndexedStackWidget {
fn selected_mut(&mut self) -> Option<&mut ChildPod> {
self.index.and_then(|i| self.children.get_mut(i))
}
fn selected(&self) -> Option<&ChildPod> {
self.index.and_then(|i| self.children.get(i))
}
fn switch_away(&mut self, ctx: &BuildCtx<'_>) {
let Some(pod) = self.selected_mut() else {
return;
};
let outgoing_focused = pod.is_focused();
if pod.is_active() {
crate::authoring::cancel_pod(pod);
pod.set_active(false);
}
if outgoing_focused {
pod.set_focused(false);
}
if outgoing_focused && ctx.has_focus() {
self.needs_ime_clear = true;
}
}
fn publish_reach(&self, parent_reachable: bool) {
for (i, cell) in self.reach.iter().enumerate() {
cell.set(parent_reachable && Some(i) == self.index);
}
}
}
impl<State: 'static> View<State> for IndexedStackView<State> {
type Element = IndexedStackWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> IndexedStackWidget {
let parent = parent_reachable();
let reach: Vec<Rc<Cell<bool>>> = (0..self.children.len())
.map(|i| Rc::new(Cell::new(parent && Some(i) == self.index)))
.collect();
let children = self
.children
.iter()
.zip(&reach)
.map(|(view, cell)| with_page_reach(cell, || crate::authoring::build_child(view, ctx)))
.collect();
IndexedStackWidget {
children,
reach,
index: self.index,
needs_ime_clear: false,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut IndexedStackWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
let parent = parent_reachable();
if element.index != self.index {
element.switch_away(ctx);
element.index = self.index;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
let next_len = self.children.len();
let removed_reach = if element.reach.len() > next_len {
element.reach.split_off(next_len)
} else {
Vec::new()
};
for cell in &removed_reach {
cell.set(false);
}
element
.reach
.resize_with(next_len, || Rc::new(Cell::new(false)));
element.publish_reach(parent);
let IndexedStackWidget {
children: pods,
reach,
..
} = element;
let common = prev.children.len().min(next_len);
for i in 0..common {
let (prev_view, next_view, pod) = (&prev.children[i], &self.children[i], &mut pods[i]);
flags |= with_page_reach(&reach[i], || {
crate::authoring::rebuild_child(prev_view, next_view, pod, ctx)
});
}
if next_len > common {
for (view, cell) in self.children[common..].iter().zip(&reach[common..]) {
pods.push(with_page_reach(cell, || {
crate::authoring::build_child(view, ctx)
}));
}
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
} else if prev.children.len() > next_len {
for ((view, pod), cell) in prev.children[next_len..]
.iter()
.zip(pods[next_len..].iter_mut())
.zip(&removed_reach)
{
with_page_reach(cell, || crate::authoring::teardown_child(view, pod, ctx));
}
pods.truncate(next_len);
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
fn teardown(&self, element: &mut IndexedStackWidget, ctx: &mut BuildCtx<'_>) {
for cell in &element.reach {
cell.set(false);
}
for ((view, pod), cell) in self
.children
.iter()
.zip(element.children.iter_mut())
.zip(&element.reach)
{
with_page_reach(cell, || crate::authoring::teardown_child(view, pod, ctx));
}
}
}
impl Widget for IndexedStackWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let loose = bc.loosen();
let mut max = Size::ZERO;
for pod in &mut self.children {
let size = pod.layout_child(ctx, &loose);
pod.set_origin(Point::ZERO);
max = Size::new(max.width.max(size.width), max.height.max(size.height));
}
bc.constrain(max)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if let Some(pod) = self.selected_mut() {
pod.paint_child(ctx, scene);
}
if self.needs_ime_clear {
ctx.publish_ime_state(cleared_ime_state());
self.needs_ime_clear = false;
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if event.is_broadcast() {
for pod in &mut self.children {
crate::authoring::route_event_single(pod, ctx, event);
}
return EventResult::Ignored;
}
match self.selected_mut() {
Some(pod) => crate::authoring::route_event_single(pod, ctx, event),
None => EventResult::Ignored,
}
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
if let Some(pod) = self.selected() {
pod.semantics_child(ctx);
}
}
crate::authoring::visit_children!(children);
}
fn cleared_ime_state() -> ImeState {
ImeState {
active: false,
editing: EditingState {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
},
caret: None,
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nav::navigator::{NavigatorController, NavigatorView, navigator};
use crate::test_support::{RecordingScene, leaf, probe, swap_leaf};
use frust_core::{PointerButton, PointerEvent, PointerPhase, RenderRoot};
fn build<S: 'static>(view: &IndexedStackView<S>) -> IndexedStackWidget {
let mut counter = 0u64;
view.build(&mut BuildCtx::new(&mut counter))
}
fn rebuild<S: 'static>(
next: &IndexedStackView<S>,
prev: &IndexedStackView<S>,
w: &mut IndexedStackWidget,
) -> ChangeFlags {
let mut counter = 100u64;
next.rebuild(prev, w, &mut BuildCtx::new(&mut counter))
}
fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
fn down(x: f64, y: f64) -> InputEvent {
pointer(PointerPhase::Down, x, y)
}
fn reach_of(w: &IndexedStackWidget) -> Vec<bool> {
w.reach.iter().map(|c| c.get()).collect()
}
#[test]
fn sizes_to_the_largest_child_and_paints_only_the_selected_one() {
let view: IndexedStackView<()> = indexed_stack(Some(1))
.child(leaf(30.0, 60.0))
.child(leaf(80.0, 20.0));
let mut w = build(&view);
let size = w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::loose(Size::new(500.0, 500.0)),
);
assert_eq!(size, Size::new(80.0, 60.0));
assert_eq!(w.children[0].origin(), Point::ZERO);
assert_eq!(w.children[1].origin(), Point::ZERO);
let mut scene = RecordingScene::default();
w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
assert_eq!(
scene.rects,
vec![(Point::ZERO, Size::new(80.0, 20.0))],
"only the selected child paints"
);
}
#[test]
fn a_filling_child_fills_the_stack_under_tight_constraints() {
let view: IndexedStackView<Vec<u32>> =
indexed_stack(Some(0)).child(probe(0)).child(probe(1));
let mut w = build(&view);
let size = w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(100.0, 70.0)),
);
assert_eq!(size, Size::new(100.0, 70.0));
assert_eq!(w.children[0].size(), Size::new(100.0, 70.0));
assert_eq!(w.children[1].size(), Size::new(100.0, 70.0));
}
#[test]
fn a_pointer_down_reaches_only_the_selected_child() {
let view: IndexedStackView<Vec<u32>> =
IndexedStack(Some(0), (probe(0), probe(1), probe(2)));
let mut w = build(&view);
w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(100.0, 100.0)),
);
let mut log: Vec<u32> = Vec::new();
let mut ectx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
let result = w.event(&mut ectx, &down(50.0, 50.0));
assert_eq!(result, EventResult::Handled);
assert_eq!(log, vec![0], "only the selected child saw the Down");
}
#[test]
fn a_broadcast_reaches_every_child_and_is_not_consumed() {
let view: IndexedStackView<Vec<u32>> = IndexedStack(Some(1), (probe(0), probe(1)));
let mut w = build(&view);
w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(100.0, 100.0)),
);
let mut log: Vec<u32> = Vec::new();
let mut ectx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
let result = w.event(&mut ectx, &InputEvent::Housekeeping);
assert_eq!(result, EventResult::Ignored);
assert_eq!(
log,
vec![0, 1],
"housekeeping is not input: every tab gets it"
);
}
struct Labelled {
label: &'static str,
}
struct LabelledWidget {
label: &'static str,
}
impl View<()> for Labelled {
type Element = LabelledWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> LabelledWidget {
LabelledWidget { label: self.label }
}
fn rebuild(
&self,
_prev: &Self,
element: &mut LabelledWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.label = self.label;
ChangeFlags::NONE
}
}
impl Widget for LabelledWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn semantics(&self, ctx: &mut SemanticsCtx) {
ctx.push_node(frust_core::accesskit::Role::Button, |node| {
node.set_label(self.label)
});
}
}
fn labels(root: &RenderRoot<(), IndexedStackView<()>>) -> Vec<String> {
root.semantics()
.nodes
.iter()
.filter_map(|(_, node)| node.label().map(str::to_string))
.collect()
}
#[test]
fn semantics_expose_only_the_selected_child() {
let selected = Rc::new(Cell::new(Some(1usize)));
let mut app = {
let selected = selected.clone();
move |_: &mut ()| {
IndexedStack(
selected.get(),
(
Labelled { label: "tab-a" },
Labelled { label: "tab-b" },
Labelled { label: "tab-c" },
),
)
}
};
let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
root.layout(Size::new(100.0, 100.0));
assert_eq!(labels(&root), vec!["tab-b".to_string()]);
selected.set(Some(2));
root.rebuild(&mut app, &mut ());
root.layout(Size::new(100.0, 100.0));
assert_eq!(labels(&root), vec!["tab-c".to_string()]);
selected.set(None);
root.rebuild(&mut app, &mut ());
root.layout(Size::new(100.0, 100.0));
assert!(labels(&root).is_empty(), "no selection, no nodes");
}
#[test]
fn only_the_selected_tabs_navigator_claims_back() {
let a: NavigatorController<()> = NavigatorController::new();
let b: NavigatorController<()> = NavigatorController::new();
let selected = Rc::new(Cell::new(0usize));
let mut app = {
let (a, b, selected) = (a.clone(), b.clone(), selected.clone());
move |_: &mut ()| {
indexed_stack(Some(selected.get()))
.child(navigator(&a, || leaf(10.0, 10.0)))
.child(navigator(&b, || leaf(10.0, 10.0)))
}
};
let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
a.push(|| leaf(20.0, 20.0));
b.push(|| leaf(20.0, 20.0));
root.rebuild(&mut app, &mut ());
assert_eq!((a.depth(), b.depth()), (2, 2), "both stacks were pushed");
assert!(a.back_interest(), "the selected tab claims back");
assert!(!b.back_interest(), "the unselected tab claims nothing");
selected.set(1);
root.rebuild(&mut app, &mut ());
assert!(!a.back_interest(), "switched away: tab A claims nothing");
assert!(b.back_interest(), "switched to: tab B claims back");
assert!(a.is_mounted() && b.is_mounted(), "both stay mounted");
assert_eq!((a.depth(), b.depth()), (2, 2), "both keep their stacks");
}
#[test]
fn a_navigator_two_levels_down_claims_back_only_while_its_tab_is_selected() {
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let selected = Rc::new(Cell::new(0usize));
let mut app = {
let (outer, inner, selected) = (outer.clone(), inner.clone(), selected.clone());
move |_: &mut ()| {
let inner = inner.clone();
indexed_stack(Some(selected.get()))
.child(navigator(&outer, move || {
navigator(&inner, || leaf(10.0, 10.0))
}))
.child(leaf(10.0, 10.0))
}
};
let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
inner.push(|| leaf(20.0, 20.0));
root.rebuild(&mut app, &mut ());
assert_eq!(inner.depth(), 2);
assert!(
inner.back_interest(),
"selected tab: the inner stack claims"
);
selected.set(1);
root.rebuild(&mut app, &mut ());
assert!(!inner.back_interest(), "switched away: nothing claims");
assert!(!outer.back_interest());
root.rebuild(&mut app, &mut ());
assert!(!inner.back_interest(), "still switched away");
selected.set(0);
root.rebuild(&mut app, &mut ());
assert!(
inner.back_interest(),
"switched back: the inner stack claims"
);
assert_eq!(inner.depth(), 2, "and kept its stack throughout");
}
#[test]
fn a_covered_host_page_silences_every_tab_navigator() {
for cull in [false, true] {
let host: NavigatorController<()> = NavigatorController::new();
let t0: NavigatorController<()> = NavigatorController::new();
let t1: NavigatorController<()> = NavigatorController::new();
let mut app = {
let (host, t0, t1) = (host.clone(), t0.clone(), t1.clone());
move |_: &mut ()| {
let (t0, t1) = (t0.clone(), t1.clone());
navigator(&host, move || {
indexed_stack(Some(0))
.child(navigator(&t0, || leaf(10.0, 10.0)))
.child(navigator(&t1, || leaf(10.0, 10.0)))
})
.cull_covered_builds(cull)
}
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
t0.push(|| leaf(20.0, 20.0));
t1.push(|| leaf(20.0, 20.0));
root.rebuild(&mut app, &mut ());
assert!(t0.back_interest(), "selected tab claims (cull={cull})");
assert!(!t1.back_interest(), "unselected tab silent (cull={cull})");
host.push(|| leaf(30.0, 30.0));
root.rebuild(&mut app, &mut ());
assert!(!t0.back_interest(), "covered: tab 0 silent (cull={cull})");
assert!(!t1.back_interest(), "covered: tab 1 silent (cull={cull})");
assert!(
host.back_interest(),
"the host claims instead (cull={cull})"
);
for _ in 0..3 {
root.rebuild(&mut app, &mut ());
}
assert!(
!t0.back_interest(),
"stays silent while covered (cull={cull})"
);
host.pop();
root.rebuild(&mut app, &mut ());
assert!(t0.back_interest(), "revealed: tab 0 claims (cull={cull})");
assert!(!t1.back_interest(), "tab 1 still silent (cull={cull})");
}
}
#[test]
fn no_or_out_of_range_index_selects_nothing() {
for index in [None, Some(2), Some(99)] {
let view: IndexedStackView<Vec<u32>> = IndexedStack(index, (probe(0), probe(1)));
let mut w = build(&view);
assert_eq!(reach_of(&w), vec![false, false], "{index:?}: no reach");
let size = w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(50.0, 50.0)),
);
assert_eq!(size, Size::new(50.0, 50.0), "children are still laid out");
let mut log: Vec<u32> = Vec::new();
let mut ectx = EventCtx::new(&mut log, Point::ZERO, size);
assert_eq!(w.event(&mut ectx, &down(5.0, 5.0)), EventResult::Ignored);
assert!(log.is_empty(), "{index:?}: nothing routed");
}
let view: IndexedStackView<()> = indexed_stack(None)
.child(leaf(10.0, 10.0))
.child(leaf(20.0, 20.0));
let mut w = build(&view);
let size = w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::loose(Size::new(100.0, 100.0)),
);
let mut scene = RecordingScene::default();
w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
assert!(scene.rects.is_empty(), "nothing painted");
let nav: NavigatorController<()> = NavigatorController::new();
let mut app = {
let nav = nav.clone();
move |_: &mut ()| indexed_stack(None).child(navigator(&nav, || leaf(10.0, 10.0)))
};
let mut root: RenderRoot<(), IndexedStackView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
nav.push(|| leaf(20.0, 20.0));
root.rebuild(&mut app, &mut ());
assert_eq!(nav.depth(), 2);
assert!(!nav.back_interest(), "no selection: no back reach");
}
#[test]
fn reach_cells_follow_child_count_changes() {
let v1: IndexedStackView<()> = indexed_stack(Some(1)).child(leaf(1.0, 1.0));
let mut w = build(&v1);
assert_eq!(
reach_of(&w),
vec![false],
"index past the end selects nothing"
);
let v2: IndexedStackView<()> = indexed_stack(Some(1))
.child(leaf(1.0, 1.0))
.child(leaf(2.0, 2.0))
.child(leaf(3.0, 3.0));
let flags = rebuild(&v2, &v1, &mut w);
assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
assert_eq!(w.children.len(), 3);
assert_eq!(reach_of(&w), vec![false, true, false]);
let dropped = Rc::clone(&w.reach[2]);
let v3: IndexedStackView<()> = indexed_stack(Some(1))
.child(leaf(1.0, 1.0))
.child(leaf(2.0, 2.0));
let flags = rebuild(&v3, &v2, &mut w);
assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
assert_eq!(w.children.len(), 2);
assert_eq!(reach_of(&w), vec![false, true]);
assert!(!dropped.get());
let v4: IndexedStackView<()> = indexed_stack(Some(1))
.child(leaf(1.0, 1.0))
.child(leaf(2.0, 2.0));
assert_eq!(rebuild(&v4, &v3, &mut w), ChangeFlags::NONE);
let v5: IndexedStackView<()> = indexed_stack(Some(0))
.child(leaf(1.0, 1.0))
.child(leaf(2.0, 2.0));
let flags = rebuild(&v5, &v4, &mut w);
assert!(flags.contains(ChangeFlags::LAYOUT | ChangeFlags::PAINT));
assert_eq!(reach_of(&w), vec![true, false]);
}
struct Recorder {
cancelled: Rc<Cell<bool>>,
ups: Rc<Cell<u32>>,
}
struct RecorderWidget {
cancelled: Rc<Cell<bool>>,
ups: Rc<Cell<u32>>,
}
impl View<()> for Recorder {
type Element = RecorderWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
RecorderWidget {
cancelled: self.cancelled.clone(),
ups: self.ups.clone(),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut RecorderWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
impl Widget for RecorderWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event {
match p.phase {
PointerPhase::Down => {
ctx.capture_pointer();
ctx.request_focus();
return EventResult::Handled;
}
PointerPhase::Cancel => {
self.cancelled.set(true);
return EventResult::Handled;
}
PointerPhase::Up => {
self.ups.set(self.ups.get() + 1);
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
}
struct Armed {
w: IndexedStackWidget,
v1: IndexedStackView<()>,
cancelled: Rc<Cell<bool>>,
ups: Rc<Cell<u32>>,
size: Size,
}
fn armed() -> Armed {
let cancelled = Rc::new(Cell::new(false));
let ups = Rc::new(Cell::new(0u32));
let v1: IndexedStackView<()> = indexed_stack(Some(0))
.child(Recorder {
cancelled: cancelled.clone(),
ups: ups.clone(),
})
.child(leaf(10.0, 10.0));
let mut w = build(&v1);
let size = w.layout(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(100.0, 100.0)),
);
let mut st = ();
let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
w.event(&mut ectx, &down(5.0, 5.0));
assert!(w.children[0].is_active(), "tab 0 captured the pointer");
assert!(w.children[0].is_focused(), "tab 0 took focus");
Armed {
w,
v1,
cancelled,
ups,
size,
}
}
fn switch_view() -> IndexedStackView<()> {
indexed_stack(Some(1))
.child(Recorder {
cancelled: Rc::new(Cell::new(false)),
ups: Rc::new(Cell::new(0)),
})
.child(leaf(10.0, 10.0))
}
#[test]
fn a_selection_change_cancels_capture_clears_focus_and_clears_the_ime() {
let Armed {
mut w,
v1,
cancelled,
ups,
size,
} = armed();
let v2 = switch_view();
rebuild(&v2, &v1, &mut w);
assert!(
cancelled.get(),
"the outgoing tab received a synthetic Cancel"
);
assert!(!w.children[0].is_active(), "its capture path was released");
assert!(!w.children[0].is_focused(), "its focus flag was cleared");
assert!(w.needs_ime_clear, "a cleared IME publish is queued");
let mut st = ();
let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
w.event(&mut ectx, &pointer(PointerPhase::Up, 5.0, 5.0));
assert_eq!(ups.get(), 0, "the cancelled tab does not fire on Up");
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut RecordingScene::default());
let ime = pctx
.take_ime_state()
.expect("the post-switch paint publishes a cleared IME surface");
assert!(!ime.active);
assert!(!w.needs_ime_clear, "the queued clear was consumed");
assert!(!w.children[1].is_focused());
}
#[test]
fn a_selection_change_off_the_live_focus_chain_publishes_no_ime_clear() {
let Armed {
mut w,
v1,
cancelled,
size,
..
} = armed();
let v2 = switch_view();
let mut counter = 100u64;
let mut ctx = BuildCtx::new(&mut counter);
ctx.with_focus_link(false, |ctx| v2.rebuild(&v1, &mut w, ctx));
assert!(cancelled.get(), "capture is still cancelled");
assert!(
!w.children[0].is_focused(),
"the stale flag is still cleared"
);
assert!(!w.needs_ime_clear, "but no session release is queued");
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut RecordingScene::default());
assert!(pctx.take_ime_state().is_none(), "no IME publish");
}
#[test]
fn switching_away_from_an_unfocused_tab_publishes_no_ime_clear() {
let v1: IndexedStackView<()> = indexed_stack(Some(0))
.child(leaf(10.0, 10.0))
.child(leaf(10.0, 10.0));
let mut w = build(&v1);
let v2: IndexedStackView<()> = indexed_stack(Some(1))
.child(leaf(10.0, 10.0))
.child(leaf(10.0, 10.0));
rebuild(&v2, &v1, &mut w);
assert!(!w.needs_ime_clear, "the outgoing tab held no session");
}
#[test]
fn fluent_and_tuple_builders_build() {
let fluent: IndexedStackView<()> = indexed_stack(Some(0))
.child(leaf(1.0, 1.0))
.children((swap_leaf(), leaf(2.0, 2.0)))
.when(true, |s| s.child(leaf(3.0, 3.0)))
.when(false, |s| s.child(leaf(4.0, 4.0)))
.when_some(Some(5.0), |s, side| s.child(leaf(side, side)))
.when_some(None::<f64>, |s, side| s.child(leaf(side, side)));
assert_eq!(build(&fluent).children.len(), 5);
let tuple: IndexedStackView<()> =
IndexedStack(Some(1), (leaf(1.0, 1.0), swap_leaf(), leaf(2.0, 2.0)));
let w = build(&tuple);
assert_eq!(w.children.len(), 3);
assert_eq!(reach_of(&w), vec![false, true, false]);
}
}