use crate::layout::LayoutEngine;
use crate::modifier::{Brush, Color, Modifier};
use crate::primitives::{Column, ColumnSpec};
use crate::renderer::{HeadlessRenderer, RenderOp};
use crate::widgets::{Popup, PopupHost};
use crate::{layout::LayoutTree, Composition};
use cranpose_core::{location_key, MemoryApplier, NodeId};
use cranpose_ui_graphics::{DrawPrimitive, Point, Rect, Size};
const MARKER: Color = Color(0.9, 0.1, 0.3, 1.0);
fn compute_layout(composition: &mut Composition<MemoryApplier>, root: NodeId) -> LayoutTree {
let handle = composition.runtime_handle();
let mut applier = composition.applier_mut();
applier.set_runtime_handle(handle);
let layout = applier
.compute_layout(
root,
Size {
width: 400.0,
height: 400.0,
},
)
.expect("layout");
applier.clear_runtime_handle();
layout
}
fn settle(
composition: &mut Composition<MemoryApplier>,
key: cranpose_core::Key,
content: &mut dyn FnMut(),
) {
for _ in 0..16 {
if !composition.should_render() {
break;
}
composition
.reconcile(key, &mut *content)
.expect("reconcile");
}
}
fn marker_rects(scene: &crate::renderer::RecordedRenderScene) -> Vec<Rect> {
scene
.operations()
.iter()
.filter_map(|op| match op {
RenderOp::Primitive {
primitive: DrawPrimitive::Rect { rect, brush },
..
}
| RenderOp::Primitive {
primitive: DrawPrimitive::RoundRect { rect, brush, .. },
..
} if *brush == Brush::solid(MARKER) => Some(*rect),
_ => None,
})
.collect()
}
#[test]
fn popup_content_renders_outside_its_anchors_parent_bounds() {
let _app_context = crate::render_state::app_context_test_scope();
let mut composition = Composition::new(MemoryApplier::new());
let key = location_key(file!(), line!(), column!());
let mut content = || {
PopupHost(|| {
Column(
Modifier::empty()
.size(Size {
width: 50.0,
height: 50.0,
})
.clip_to_bounds(),
ColumnSpec::default(),
|| {
Popup(
Rect {
x: 200.0,
y: 300.0,
width: 0.0,
height: 0.0,
},
Point { x: 0.0, y: 0.0 },
|| {
Column(
Modifier::empty()
.size(Size {
width: 20.0,
height: 20.0,
})
.background(MARKER),
ColumnSpec::default(),
|| {},
);
},
);
},
);
});
};
composition
.render(key, &mut content)
.expect("initial render");
settle(&mut composition, key, &mut content);
let root = composition.root().expect("popup host root");
let layout = compute_layout(&mut composition, root);
let scene = HeadlessRenderer::new().render(&layout);
let rects = marker_rects(&scene);
assert_eq!(
rects.len(),
1,
"expected exactly one marker rect from the overlay content, got {rects:?}"
);
let rect = rects[0];
assert!(
rect.x >= 200.0 - 0.5 && rect.x <= 200.0 + 0.5,
"marker x should be at the anchor (200), was {}",
rect.x
);
assert!(
rect.y >= 300.0 - 0.5 && rect.y <= 300.0 + 0.5,
"marker y should be at the anchor (300), was {}",
rect.y
);
assert!(
rect.x >= 50.0,
"marker must render outside the anchor's parent bounds (x>=50), was {}",
rect.x
);
}
#[test]
fn popup_is_removed_from_overlay_when_no_longer_composed() {
use cranpose_core::mutableStateOf;
let _app_context = crate::render_state::app_context_test_scope();
let mut composition = Composition::new(MemoryApplier::new());
let key = location_key(file!(), line!(), column!());
let show = mutableStateOf(true);
let show_for_content = show;
let mut content = move || {
let show = show_for_content;
PopupHost(move || {
if show.value() {
Popup(
Rect {
x: 120.0,
y: 60.0,
width: 0.0,
height: 0.0,
},
Point { x: 0.0, y: 0.0 },
|| {
Column(
Modifier::empty()
.size(Size {
width: 10.0,
height: 10.0,
})
.background(MARKER),
ColumnSpec::default(),
|| {},
);
},
);
}
});
};
composition.render(key, &mut content).expect("render");
settle(&mut composition, key, &mut content);
let root = composition.root().expect("root");
let layout = compute_layout(&mut composition, root);
let scene = HeadlessRenderer::new().render(&layout);
assert_eq!(
marker_rects(&scene).len(),
1,
"popup visible while composed"
);
show.set(false);
settle(&mut composition, key, &mut content);
let root = composition.root().expect("root");
let layout = compute_layout(&mut composition, root);
let scene = HeadlessRenderer::new().render(&layout);
assert_eq!(
marker_rects(&scene).len(),
0,
"popup removed from overlay after it stops being composed"
);
}