use std::rc::Rc;
use cranpose_ui_graphics::Size as GeometrySize;
use cranpose_ui_layout::{Measurable, MeasureResult, MeasureScope};
use super::*;
#[derive(Default)]
struct TestMeasurePolicy;
impl MeasurePolicy for TestMeasurePolicy {
fn measure(
&self,
_scope: &dyn MeasureScope,
_measurables: &[Box<dyn Measurable>],
_constraints: Constraints,
) -> MeasureResult {
MeasureResult::new(
GeometrySize {
width: 0.0,
height: 0.0,
},
Vec::new(),
)
}
fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
0.0
}
fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
0.0
}
fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
0.0
}
fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
0.0
}
}
fn fresh_node() -> LayoutNode {
node_with(Modifier::empty())
}
fn node_with(modifier: Modifier) -> LayoutNode {
LayoutNode::new(modifier, Rc::new(TestMeasurePolicy))
}
#[test]
fn bubbling_preserves_the_layout_nodes_existing_owner() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.set_parent_for_bubbling(41);
node.set_parent_for_bubbling(42);
assert_eq!(node.parent(), Some(41));
node.on_removed_from_parent();
node.set_parent_for_bubbling(42);
assert_eq!(node.parent(), Some(42));
}
#[test]
fn modifier_slices_cache_reuses_unique_snapshot_allocation() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
let snapshot = node.modifier_slices_snapshot();
let snapshot_ptr = Rc::as_ptr(&snapshot);
drop(snapshot);
node.set_modifier(Modifier::empty().padding(4.0));
let updated = node.modifier_slices_snapshot();
assert_eq!(Rc::as_ptr(&updated), snapshot_ptr);
}
#[test]
fn modifier_slices_cache_preserves_live_snapshot_isolation() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
let old_snapshot = node.modifier_slices_snapshot();
let old_snapshot_ptr = Rc::as_ptr(&old_snapshot);
node.set_modifier(Modifier::empty().padding(4.0));
let updated = node.modifier_slices_snapshot();
assert_ne!(Rc::as_ptr(&updated), old_snapshot_ptr);
assert_eq!(old_snapshot.draw_commands().len(), 0);
}
#[test]
fn a_changed_modifier_collects_its_slices_once_when_they_are_read() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.set_node_id(allocate_virtual_node_id());
node.set_modifier(Modifier::empty().background(crate::modifier::Color::WHITE));
assert!(
node.modifier_slices_dirty.get(),
"composition leaves the slices to the first reader"
);
let slices = node.modifier_slices_snapshot();
assert_eq!(slices.draw_commands().len(), 1);
assert!(!node.modifier_slices_dirty.get());
assert!(
Rc::ptr_eq(&slices, &node.modifier_slices_snapshot()),
"a second read reuses the collected slices"
);
}
#[test]
fn an_equal_modifier_leaves_the_chain_and_its_slices_as_they_were() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.set_modifier(Modifier::empty().padding(4.0));
let held = node.modifier_slices_snapshot();
node.set_modifier(Modifier::empty().padding(4.0));
assert!(
Rc::ptr_eq(&held, &node.modifier_slices_snapshot()),
"an equal modifier is not synced or collected again"
);
node.set_modifier(Modifier::empty().padding(5.0));
assert!(
!Rc::ptr_eq(&held, &node.modifier_slices_snapshot()),
"a changed modifier is"
);
}
#[test]
fn layout_node_registry_retains_warm_capacity_after_large_cleanup() {
let _app_context = crate::render_state::app_context_test_scope();
let app_context = crate::render_state::AppContext::new_with_density(1.0);
app_context.enter(|| {
let nodes: Vec<_> = (0..2048)
.map(|_| {
let id = allocate_virtual_node_id();
let node = fresh_node();
let owner_context_id = register_layout_node(id, &node);
(id, owner_context_id, node)
})
.collect();
for (id, owner_context_id, _) in &nodes {
unregister_layout_node(*owner_context_id, *id);
}
let stats = layout_node_registry_stats();
assert_eq!(stats.len, 0);
assert!(
(MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY
..=MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY.saturating_mul(2))
.contains(&stats.capacity),
"registry warm capacity {} fell outside expected retained range {}..={}",
stats.capacity,
MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY,
MIN_RETAINED_LAYOUT_NODE_REGISTRY_CAPACITY.saturating_mul(2),
);
});
}
#[test]
fn layout_node_registry_is_scoped_by_app_context() {
let _app_context = crate::render_state::app_context_test_scope();
let first = crate::render_state::AppContext::new_with_density(1.0);
let second = crate::render_state::AppContext::new_with_density(1.0);
let first_id = first.enter(allocate_virtual_node_id);
let second_id = second.enter(allocate_virtual_node_id);
assert_eq!(first_id, VIRTUAL_NODE_ID_START);
assert_eq!(second_id, VIRTUAL_NODE_ID_START);
let virtual_node = LayoutNode::new_virtual();
let regular_node = fresh_node();
first.enter(|| {
register_layout_node(first_id, &virtual_node);
register_layout_node(101, ®ular_node);
assert!(is_virtual_node(first_id));
assert_eq!(layout_node_registry_stats().len, 2);
});
second.enter(|| {
assert!(!is_virtual_node(first_id));
assert_eq!(layout_node_registry_stats().len, 0);
assert_eq!(allocate_virtual_node_id(), VIRTUAL_NODE_ID_START + 1);
});
first.enter(|| {
let owner_context_id = crate::render_state::current_app_context_id();
unregister_layout_node(owner_context_id, first_id);
unregister_layout_node(owner_context_id, 101);
assert_eq!(layout_node_registry_stats().len, 0);
});
}
fn invalidation(kind: InvalidationKind) -> ModifierInvalidation {
ModifierInvalidation::new(kind, NodeCapabilities::for_invalidation(kind))
}
#[test]
fn layout_invalidation_requires_layout_capability() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().draw_behind(|_| {}));
node.clear_needs_measure();
node.clear_needs_layout();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
assert!(!node.needs_measure());
assert!(!node.needs_layout());
}
#[test]
fn semantics_configuration_reflects_modifier_state() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.set_modifier(Modifier::empty().semantics(|config| {
config.content_description = Some("greeting".into());
config.is_clickable = true;
}));
let config = node
.semantics_configuration()
.expect("expected semantics configuration");
assert_eq!(config.content_description.as_deref(), Some("greeting"));
assert!(config.is_clickable);
}
#[test]
fn layout_invalidation_marks_flags_when_capability_present() {
let _app_context = crate::render_state::app_context_test_scope();
let _guard = crate::render_state::render_state_test_guard();
crate::reset_render_state_for_tests();
let node = node_with(Modifier::empty().padding(1.0));
node.id.set(Some(11));
node.clear_needs_measure();
node.clear_needs_layout();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
assert!(node.needs_measure());
assert!(node.needs_layout());
assert_eq!(crate::take_layout_repass_nodes(), vec![11]);
assert!(
!crate::take_layout_invalidation(),
"a scoped modifier invalidation must not invalidate the whole tree"
);
}
#[test]
fn layout_invalidation_skips_repass_while_composing() {
let _app_context = crate::render_state::app_context_test_scope();
let _guard = crate::render_state::render_state_test_guard();
crate::reset_render_state_for_tests();
let node = Rc::new(RefCell::new(node_with(Modifier::empty().padding(1.0))));
{
let node = node.borrow_mut();
node.id.set(Some(17));
node.clear_needs_measure();
node.clear_needs_layout();
}
let node_for_composition = Rc::clone(&node);
let _composition = crate::run_test_composition(move || {
node_for_composition
.borrow()
.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Layout)]);
});
let node = node.borrow();
assert!(node.needs_measure());
assert!(node.needs_layout());
assert!(crate::take_layout_repass_nodes().is_empty());
assert!(!crate::take_layout_invalidation());
}
#[test]
fn draw_invalidation_marks_redraw_flag_when_capable() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().draw_behind(|_| {}));
node.clear_needs_measure();
node.clear_needs_layout();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Draw)]);
assert!(node.needs_redraw());
assert!(!node.needs_layout());
}
#[test]
fn draw_invalidation_capability_marks_redraw_for_layout_modifier_update() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().padding(1.0));
node.clear_needs_measure();
node.clear_needs_layout();
node.clear_needs_redraw();
node.dispatch_modifier_invalidations(&[ModifierInvalidation::new(
InvalidationKind::Draw,
NodeCapabilities::DRAW,
)]);
assert!(node.needs_redraw());
assert!(!node.needs_measure());
assert!(!node.needs_layout());
}
#[test]
fn semantics_invalidation_sets_semantics_flag_only() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().semantics(|_| {}));
node.clear_needs_measure();
node.clear_needs_layout();
node.clear_needs_semantics();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Semantics)]);
assert!(node.needs_semantics());
assert!(!node.needs_measure());
assert!(!node.needs_layout());
}
#[test]
fn pointer_invalidation_requires_pointer_capability() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().draw_behind(|_| {}));
node.clear_needs_pointer_pass();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::PointerInput)]);
assert!(!node.needs_pointer_pass());
}
#[test]
fn pointer_invalidation_marks_flag_and_requests_queue() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().clickable(|_| {}));
node.clear_needs_pointer_pass();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::PointerInput)]);
assert!(node.needs_pointer_pass());
}
#[test]
fn focus_invalidation_requires_focus_capability() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().draw_behind(|_| {}));
node.clear_needs_focus_sync();
crate::take_focus_invalidation();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Focus)]);
assert!(!node.needs_focus_sync());
assert!(!crate::take_focus_invalidation());
}
#[test]
fn focus_invalidation_marks_flag_and_requests_queue() {
let _app_context = crate::render_state::app_context_test_scope();
let node = node_with(Modifier::empty().focus_target());
node.clear_needs_focus_sync();
crate::take_focus_invalidation();
node.dispatch_modifier_invalidations(&[invalidation(InvalidationKind::Focus)]);
assert!(node.needs_focus_sync());
assert!(crate::take_focus_invalidation());
}
#[test]
fn set_modifier_marks_semantics_dirty() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.clear_needs_semantics();
node.set_modifier(Modifier::empty().semantics(|config| {
config.is_clickable = true;
}));
assert!(node.needs_semantics());
}
#[test]
fn modifier_child_capabilities_reflect_chain_head() {
let _app_context = crate::render_state::app_context_test_scope();
let mut node = fresh_node();
node.set_modifier(Modifier::empty().padding(4.0));
assert!(
node.modifier_child_capabilities()
.contains(NodeCapabilities::LAYOUT),
"padding should introduce layout capability"
);
}
fn drain_semantics_layout_changes() -> Vec<NodeId> {
let mut changed = Vec::new();
crate::render_state::with_current_semantics_layout_log(|log| log.begin_sync(&mut changed));
changed
}
#[test]
fn layout_state_reports_the_changes_a_semantics_tree_reads() {
let _app_context = crate::render_state::app_context_test_scope();
drain_semantics_layout_changes();
let mut state = LayoutState::default();
state.set_node_id(9);
state.set_content_offset(Point::default());
state.set_size(Size::default());
state.clear_placed();
assert!(
drain_semantics_layout_changes().is_empty(),
"writes that change nothing report nothing"
);
state.place(Point::default());
assert_eq!(drain_semantics_layout_changes(), vec![9], "placed");
state.clear_placed();
state.place(Point::default());
assert_eq!(
drain_semantics_layout_changes(),
vec![9],
"placed again in a pass, reported once"
);
state.place(Point { x: 1.0, y: 0.0 });
assert_eq!(drain_semantics_layout_changes(), vec![9], "moved");
state.set_content_offset(Point { x: 2.0, y: 0.0 });
assert_eq!(state.content_offset(), Point { x: 2.0, y: 0.0 });
assert_eq!(drain_semantics_layout_changes(), vec![9], "content moved");
state.set_size(Size {
width: 4.0,
height: 4.0,
});
assert_eq!(drain_semantics_layout_changes(), vec![9], "resized");
}
#[test]
fn a_new_child_list_is_reported_to_the_semantics_tree() {
let _app_context = crate::render_state::app_context_test_scope();
drain_semantics_layout_changes();
let mut node = fresh_node();
node.set_node_id(4);
assert!(node.insert_child(5));
assert_eq!(drain_semantics_layout_changes(), vec![4], "a child joined");
node.move_child(0, 0);
assert!(node.remove_child(5));
assert_eq!(drain_semantics_layout_changes(), vec![4], "a child left");
node.update_children(&[6, 7]);
node.move_child(0, 1);
assert_eq!(
drain_semantics_layout_changes(),
vec![4],
"the children changed"
);
}
#[test]
fn cloning_cache_handles_from_the_same_cache_keeps_the_handle() {
let cache = LayoutNodeCacheHandles::default();
let mut shared = cache.clone();
cache.activate(7);
shared.clone_from(&cache);
assert_eq!(Rc::strong_count(&cache.state), 2);
let mut other = LayoutNodeCacheHandles::default();
other.clone_from(&cache);
assert!(Rc::ptr_eq(&other.state, &cache.state));
assert_eq!(other.epoch(), 7);
assert_eq!(Rc::strong_count(&cache.state), 3);
}
#[test]
fn layout_state_handle_lends_the_state_the_node_reports() {
let _app_context = crate::render_state::app_context_test_scope();
let node = fresh_node();
let handle = node.layout_state_handle();
assert_eq!(
Rc::strong_count(handle),
1,
"lending the state must not share it"
);
handle.borrow_mut().set_size(Size {
width: 12.0,
height: 5.0,
});
assert_eq!(
node.measured_size(),
Size {
width: 12.0,
height: 5.0,
}
);
assert!(Rc::ptr_eq(handle, node.layout_state_handle()));
}
#[test]
fn a_new_layout_runtime_state_has_no_children_or_coordinators() {
let stats = LayoutRuntimeState::new(Rc::new(TestMeasurePolicy)).debug_stats();
assert!(stats.child_ids.is_empty());
assert_eq!(stats.child_measurable_count, 0);
assert_eq!(stats.coordinator_node_count, 0);
}
fn state_of(id: NodeId) -> LayoutState {
let mut state = LayoutState::default();
state.set_node_id(id);
state
}
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
#[test]
fn a_first_placement_reports_the_node_even_at_the_origin() {
let _app_context = crate::render_state::app_context_test_scope();
begin_placement_pass();
let mut state = state_of(7);
state.place(origin());
assert!(state.is_placed());
assert_eq!(crate::take_geometry_scene_nodes(), vec![7]);
}
#[test]
fn a_node_its_pass_clears_and_places_where_it_was_reports_nothing() {
let _app_context = crate::render_state::app_context_test_scope();
let mut state = state_of(7);
state.place(origin());
let _ = crate::take_geometry_scene_nodes();
begin_placement_pass();
state.clear_placed();
state.clear_placed();
assert!(placement_pass_with_unplaced_nodes().is_some());
state.place(origin());
assert_eq!(placement_pass_with_unplaced_nodes(), None);
assert!(crate::take_geometry_scene_nodes().is_empty());
state.place(Point { x: 0.0, y: 18.0 });
assert_eq!(
crate::take_geometry_scene_nodes(),
vec![7],
"a move is reported"
);
}
#[test]
fn a_node_its_pass_clears_and_never_places_is_left_unplaced() {
let _app_context = crate::render_state::app_context_test_scope();
let mut state = state_of(7);
state.place(origin());
begin_placement_pass();
state.clear_placed();
let pass = placement_pass_with_unplaced_nodes().expect("the pass left a node unplaced");
assert!(state.unplaced_in(pass));
assert!(!state.unplaced_in(pass + 1));
begin_placement_pass();
assert_eq!(
placement_pass_with_unplaced_nodes(),
None,
"a new pass starts with nothing unplaced"
);
let _ = crate::take_geometry_scene_nodes();
state.place(origin());
assert_eq!(
crate::take_geometry_scene_nodes(),
vec![7],
"placing a node an earlier pass left unplaced reports it, where it was or not"
);
}