use std::{cell::Cell, rc::Rc};
use cranpose_core::NodeId;
use cranpose_ui_graphics::Size as ViewportSize;
use super::*;
fn modal_box(
size: f32,
hidden: bool,
id: &Rc<Cell<Option<NodeId>>>,
content: impl FnMut() + 'static,
) {
id.set(Some(Box(
Modifier::empty()
.size_points(size, size)
.semantics(move |config| {
config.is_modal = true;
config.hidden = hidden;
}),
BoxSpec::default(),
content,
)));
}
fn walk_and_tree(build: impl FnMut() + 'static) -> (Option<NodeId>, NodeId, bool) {
let mut composition = run_test_composition(build);
let root = composition.root().expect("root");
let handle = composition.runtime_handle();
let mut applier = composition.applier_mut();
applier.set_runtime_handle(handle);
measure_layout(
&mut applier,
root,
ViewportSize {
width: 320.0,
height: 480.0,
},
)
.expect("layout");
cranpose_core::Applier::get_mut(&mut *applier, root)
.expect("root node")
.mark_needs_semantics();
let walked = crate::top_modal_from_applier(&mut applier, root).expect("walk");
let still_dirty = crate::tree_needs_semantics(&mut *applier, root).expect("dirty flag");
let tree_root = crate::build_semantics_tree_from_applier(&mut applier, root)
.expect("semantics tree")
.expect("semantics present")
.root()
.node_id;
applier.clear_runtime_handle();
(walked, tree_root, still_dirty)
}
#[test]
fn the_walk_finds_the_modal_the_semantics_tree_is_rooted_at() {
let first = Rc::new(Cell::new(None));
let outer = Rc::new(Cell::new(None));
let inner = Rc::new(Cell::new(None));
let hidden = Rc::new(Cell::new(None));
let empty = Rc::new(Cell::new(None));
let ids = (
Rc::clone(&first),
Rc::clone(&outer),
Rc::clone(&inner),
Rc::clone(&hidden),
Rc::clone(&empty),
);
let (walked, tree_root, still_dirty) = walk_and_tree(move || {
let (first, outer, inner, hidden, empty) = ids.clone();
Column(Modifier::empty(), ColumnSpec::default(), move || {
modal_box(40.0, false, &first, || {});
let inner = Rc::clone(&inner);
modal_box(80.0, false, &outer, move || {
modal_box(20.0, false, &inner, || {
Text("Inside", Modifier::empty(), TextStyle::default());
});
});
modal_box(40.0, true, &hidden, || {});
modal_box(0.0, false, &empty, || {});
});
});
assert_eq!(
walked,
inner.get(),
"the last visible modal, innermost first"
);
assert_eq!(walked, Some(tree_root));
assert!(
still_dirty,
"the walk leaves the semantics tree to be built"
);
}
fn walk(composition: &mut crate::TestComposition) -> Option<NodeId> {
let root = composition.root().expect("root");
let handle = composition.runtime_handle();
let mut applier = composition.applier_mut();
applier.set_runtime_handle(handle);
measure_layout(
&mut applier,
root,
ViewportSize {
width: 320.0,
height: 480.0,
},
)
.expect("layout");
let walked = crate::top_modal_from_applier(&mut applier, root).expect("walk");
applier.clear_runtime_handle();
walked
}
#[composable]
fn TogglingModalBox(modal: cranpose_core::MutableState<bool>, dialog: Rc<Cell<Option<NodeId>>>) {
let sized = Modifier::empty().size_points(40.0, 40.0);
let modifier = if modal.get() {
sized.semantics(|config| config.is_modal = true)
} else {
sized
};
dialog.set(Some(Box(modifier, BoxSpec::default(), || {})));
}
type ModalWidget = fn(cranpose_core::MutableState<bool>, Rc<Cell<Option<NodeId>>>);
fn assert_toggled_modal_reaches_the_walk(widget: ModalWidget) {
let modal_state = Rc::new(std::cell::RefCell::new(None));
let dialog = Rc::new(Cell::new(None));
let (state_slot, dialog_slot) = (Rc::clone(&modal_state), Rc::clone(&dialog));
let mut composition = run_test_composition(move || {
let modal = cranpose_core::rememberMutableStateOf(|| true);
*state_slot.borrow_mut() = Some(modal);
widget(modal, Rc::clone(&dialog_slot));
});
assert_eq!(walk(&mut composition), dialog.get());
let modal = modal_state.borrow().expect("state remembered");
modal.set_value(false);
composition
.process_invalid_scopes()
.expect("recompose without the modal");
assert_eq!(
walk(&mut composition),
None,
"the recomposed modifier's semantics must replace the old"
);
modal.set_value(true);
composition
.process_invalid_scopes()
.expect("recompose with the modal");
assert_eq!(walk(&mut composition), dialog.get());
}
#[test]
fn a_modal_modifier_a_recomposition_adds_or_drops_reaches_the_walk() {
assert_toggled_modal_reaches_the_walk(TogglingModalBox);
}
#[test]
fn without_a_modal_the_walk_finds_nothing() {
let (walked, tree_root, _) = walk_and_tree(|| {
Column(Modifier::empty(), ColumnSpec::default(), || {
Text("Plain", Modifier::empty(), TextStyle::default());
Box(
Modifier::empty().size_points(10.0, 10.0),
BoxSpec::default(),
|| {},
);
});
});
assert_eq!(walked, None);
assert_ne!(tree_root, 0);
}
fn chain_reach(modifier: &Modifier) -> cranpose_foundation::SemanticsReach {
let mut handle = crate::modifier::ModifierChainHandle::new();
let _ = handle.update(modifier);
crate::modifier::semantics_reach_of_chain(handle.chain())
}
#[test]
fn a_chains_reach_joins_every_semantics_modifier_in_it() {
let modifier = Modifier::empty()
.semantics(|config| config.is_modal = true)
.padding(2.0)
.semantics(|config| config.hidden = true);
assert_eq!(
chain_reach(&modifier),
cranpose_foundation::SemanticsReach {
is_modal: true,
hidden: true,
}
);
assert_eq!(
chain_reach(&Modifier::empty().padding(2.0)),
cranpose_foundation::SemanticsReach::default()
);
}
fn captured_modal(modal: cranpose_core::MutableState<bool>) -> Modifier {
let is_modal = modal.get();
Modifier::empty()
.size_points(40.0, 40.0)
.semantics(move |config| config.is_modal = is_modal)
}
#[composable]
fn CapturedModalBox(modal: cranpose_core::MutableState<bool>, dialog: Rc<Cell<Option<NodeId>>>) {
dialog.set(Some(Box(captured_modal(modal), BoxSpec::default(), || {})));
}
#[composable]
fn CapturedModalLazyColumn(
modal: cranpose_core::MutableState<bool>,
dialog: Rc<Cell<Option<NodeId>>>,
) {
let state = cranpose_foundation::lazy::rememberLazyListState();
dialog.set(Some(LazyColumn(
captured_modal(modal),
state,
LazyColumnSpec::default(),
|_| {},
)));
}
#[composable]
fn CapturedModalScalingList(
modal: cranpose_core::MutableState<bool>,
dialog: Rc<Cell<Option<NodeId>>>,
) {
let state = crate::widgets::wear::rememberWearScalingListState(
crate::round_scaling_list::CentreAnchor::default(),
);
dialog.set(Some(crate::widgets::wear::WearScalingLazyColumn(
captured_modal(modal),
state,
crate::widgets::wear::WearScalingLazyColumnSpec::default(),
|_| {},
)));
}
#[test]
fn a_semantics_closure_a_recomposition_replaces_reaches_the_walk_through_a_box() {
assert_toggled_modal_reaches_the_walk(CapturedModalBox);
}
#[test]
fn a_lazy_columns_semantics_closure_a_recomposition_replaces_reaches_the_walk() {
assert_toggled_modal_reaches_the_walk(CapturedModalLazyColumn);
}
#[test]
fn a_scaling_lists_semantics_closure_a_recomposition_replaces_reaches_the_walk() {
assert_toggled_modal_reaches_the_walk(CapturedModalScalingList);
}
#[composable]
fn ScrollModalLazyColumn(
list: Rc<std::cell::RefCell<Option<cranpose_foundation::lazy::LazyListState>>>,
dialog: Rc<Cell<Option<NodeId>>>,
) {
let state = cranpose_foundation::lazy::rememberLazyListState();
*list.borrow_mut() = Some(state);
Column(Modifier::empty(), ColumnSpec::default(), move || {
let at_top = state.first_visible_item_index() == 0;
dialog.set(Some(LazyColumn(
Modifier::empty()
.size_points(40.0, 240.0)
.semantics(move |config| config.is_modal = at_top),
state,
LazyColumnSpec::default(),
|scope| {
scope.items(cranpose_foundation::lazy::LazyItems::new(50), |_| {
Spacer(Size {
width: 40.0,
height: 48.0,
});
});
},
)));
});
}
#[test]
fn a_lazy_columns_semantics_closure_its_own_scroll_replaces_reaches_the_walk() {
let list = Rc::new(std::cell::RefCell::new(None));
let dialog = Rc::new(Cell::new(None));
let (list_slot, dialog_slot) = (Rc::clone(&list), Rc::clone(&dialog));
let mut composition = run_test_composition(move || {
ScrollModalLazyColumn(Rc::clone(&list_slot), Rc::clone(&dialog_slot));
});
assert_eq!(walk(&mut composition), dialog.get());
let state = list.borrow().expect("state remembered");
state.dispatch_scroll_delta(-160.0);
let _ = walk(&mut composition);
assert!(state.first_visible_item_index() > 0, "the list scrolled");
composition
.process_invalid_scopes()
.expect("recompose the list's caller after the scroll");
assert_eq!(
walk(&mut composition),
None,
"a closure rebuilt by a list-state recomposition must replace the old"
);
}