use frust_core::accesskit::Role;
use frust_core::widget::{PlatformViewFrame, next_slot_id, report_retired_slot};
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
};
use kurbo::{Point, Rect, Size};
#[cfg(debug_assertions)]
use peniko::Color;
#[derive(Clone, Copy, Debug, PartialEq)]
enum SlotSize {
Expand,
Explicit(f64, f64),
}
#[cfg(debug_assertions)]
const DEBUG_FILL_COLOR: Color = Color::from_rgba8(0xE0, 0x00, 0xE0, 0x40);
pub struct PlatformViewView {
view_type: String,
params_json: String,
slot_size: SlotSize,
#[cfg(debug_assertions)]
debug_fill: bool,
semantics_label: Option<String>,
interactive: bool,
shields_local: Vec<Rect>,
}
pub fn platform_view(view_type: impl Into<String>) -> PlatformViewView {
PlatformViewView {
view_type: view_type.into(),
params_json: String::new(),
slot_size: SlotSize::Expand,
#[cfg(debug_assertions)]
debug_fill: false,
semantics_label: None,
interactive: false,
shields_local: Vec::new(),
}
}
impl PlatformViewView {
pub fn params_json(mut self, params_json: impl Into<String>) -> Self {
self.params_json = params_json.into();
self
}
pub fn size(mut self, width: f64, height: f64) -> Self {
self.slot_size = SlotSize::Explicit(width, height);
self
}
pub fn expand(mut self) -> Self {
self.slot_size = SlotSize::Expand;
self
}
#[cfg(debug_assertions)]
pub fn debug_fill(mut self) -> Self {
self.debug_fill = true;
self
}
pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
self.semantics_label = Some(label.into());
self
}
pub fn interactive(mut self) -> Self {
self.interactive = true;
self
}
pub fn shield_local(mut self, rect: Rect) -> Self {
self.shields_local.push(rect);
self
}
}
pub struct PlatformViewWidget {
slot_id: u64,
view_type: String,
params_json: String,
params_generation: u64,
slot_size: SlotSize,
#[cfg(debug_assertions)]
debug_fill: bool,
semantics_label: Option<String>,
interactive: bool,
shields_local: Vec<Rect>,
}
impl<State: 'static> View<State> for PlatformViewView {
type Element = PlatformViewWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> PlatformViewWidget {
PlatformViewWidget {
slot_id: next_slot_id(),
view_type: self.view_type.clone(),
params_json: self.params_json.clone(),
params_generation: 0,
slot_size: self.slot_size,
#[cfg(debug_assertions)]
debug_fill: self.debug_fill,
semantics_label: self.semantics_label.clone(),
interactive: self.interactive,
shields_local: self.shields_local.clone(),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut PlatformViewWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.view_type != self.view_type {
element.slot_id = next_slot_id();
element.view_type = self.view_type.clone();
element.params_json = self.params_json.clone();
element.params_generation = 0;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
} else if prev.params_json != self.params_json {
element.params_json = self.params_json.clone();
element.params_generation += 1;
flags |= ChangeFlags::PAINT;
}
if prev.slot_size != self.slot_size {
element.slot_size = self.slot_size;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
#[cfg(debug_assertions)]
if prev.debug_fill != self.debug_fill {
element.debug_fill = self.debug_fill;
flags |= ChangeFlags::PAINT;
}
if prev.semantics_label != self.semantics_label {
element.semantics_label = self.semantics_label.clone();
}
if prev.interactive != self.interactive || prev.shields_local != self.shields_local {
element.interactive = self.interactive;
element.shields_local = self.shields_local.clone();
flags |= ChangeFlags::PAINT;
}
flags
}
fn teardown(&self, element: &mut PlatformViewWidget, _ctx: &mut BuildCtx<'_>) {
report_retired_slot(element.slot_id);
}
}
impl Widget for PlatformViewWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
match self.slot_size {
SlotSize::Explicit(width, height) => bc.constrain(Size::new(width, height)),
SlotSize::Expand => bc.max(),
}
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let rect = Rect::from_origin_size(ctx.origin(), ctx.size());
let (clip, visible) = match ctx.visible_rect() {
Some(visible_rect) => (
Some(visible_rect.intersect(rect)),
visible_rect.overlaps(rect),
),
None => (None, true),
};
ctx.publish_platform_view(PlatformViewFrame {
slot_id: self.slot_id,
view_type: self.view_type.clone(),
params_json: self.params_json.clone(),
params_generation: self.params_generation,
rect,
clip,
visible,
interactive: self.interactive,
shields: self
.shields_local
.iter()
.map(|r| *r + ctx.origin().to_vec2())
.collect(),
});
if ctx.is_translucent() {
scene.clear_rect(ctx.origin(), ctx.size());
}
#[cfg(debug_assertions)]
if self.debug_fill {
scene.fill_rect(ctx.origin(), ctx.size(), DEBUG_FILL_COLOR);
}
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
if let Some(label) = &self.semantics_label {
ctx.push_node(Role::GenericContainer, |node| {
node.set_label(label.as_str());
});
}
}
}
pub struct ShieldView<State: 'static> {
child: AnyView<State>,
}
pub fn shield<State: 'static, V: View<State>>(child: V) -> ShieldView<State> {
ShieldView { child: any(child) }
}
pub struct ShieldWidget {
child: ChildPod,
}
impl<State: 'static> View<State> for ShieldView<State> {
type Element = ShieldWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> ShieldWidget {
ShieldWidget {
child: crate::authoring::build_child(&self.child, ctx),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut ShieldWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
}
fn teardown(&self, element: &mut ShieldWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
}
}
impl Widget for ShieldWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.child.set_origin(Point::ZERO);
self.child.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
ctx.report_input_shield(Rect::from_origin_size(ctx.origin(), ctx.size()));
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
crate::authoring::route_event_single(&mut self.child, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.child.semantics_child(ctx);
}
crate::authoring::visit_children!(child);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Axis, EdgeInsets, FlexView, Padding, PaddingView, Row, SizedBox, keyed};
use frust_core::{BuildCtx, ChildPod, any};
use kurbo::Point;
fn build(view: &PlatformViewView) -> PlatformViewWidget {
let mut counter = 0u64;
<PlatformViewView as View<()>>::build(view, &mut BuildCtx::new(&mut counter))
}
fn rebuild(
prev: &PlatformViewView,
next: &PlatformViewView,
element: &mut PlatformViewWidget,
) -> ChangeFlags {
let mut counter = 0u64;
<PlatformViewView as View<()>>::rebuild(
next,
prev,
element,
&mut BuildCtx::new(&mut counter),
)
}
#[derive(Default)]
struct NullScene {
ops: Vec<SceneOp>,
}
#[derive(Debug, PartialEq)]
enum SceneOp {
Clear { origin: Point, size: Size },
Fill { origin: Point, size: Size },
}
impl PaintScene for NullScene {
fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
self.ops.push(SceneOp::Fill { origin, size });
}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn clear_rect(&mut self, origin: Point, size: Size) {
self.ops.push(SceneOp::Clear { origin, size });
}
}
#[test]
fn expand_fills_the_incoming_constraints_max() {
let view = platform_view("dev.frust.MapFactory");
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
assert_eq!(size, Size::new(300.0, 200.0));
}
#[test]
fn explicit_size_is_clamped_into_constraints() {
let view = platform_view("dev.frust.MapFactory").size(1000.0, 1000.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
assert_eq!(size, Size::new(300.0, 200.0));
}
#[test]
fn publishes_absolute_rect_under_padding_flex_nesting_and_scroll_offset() {
let mut counter = 0u64;
let insets = EdgeInsets::all(10.0);
let row: FlexView<()> = Row(vec![
any(SizedBox::<()>(Some(50.0), Some(30.0))),
any(platform_view("dev.frust.MapFactory").size(80.0, 60.0)),
]);
let padded: PaddingView<()> = Padding(insets, row);
let widget = padded.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
let mut pod = ChildPod::new(Box::new(widget));
pod.set_origin(Point::new(-15.0, -25.0));
pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(2000.0, 2000.0));
pod.paint_child(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(frames.len(), 1);
let expected_origin = Point::new(-15.0 + 10.0 + 50.0, -25.0 + 10.0);
assert_eq!(
frames[0].rect,
Rect::from_origin_size(expected_origin, Size::new(80.0, 60.0))
);
}
#[test]
fn clip_is_intersected_with_visible_rect_when_straddling_edge() {
let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::new(50.0, 0.0), Size::new(100.0, 100.0));
pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
w.paint(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(frames.len(), 1);
let frame: &PlatformViewFrame = &frames[0];
assert_eq!(
frame.rect,
Rect::from_origin_size(Point::new(50.0, 0.0), Size::new(100.0, 100.0))
);
assert_eq!(frame.clip, Some(Rect::new(50.0, 0.0, 100.0, 100.0)));
assert!(frame.visible, "an edge-straddling slot is still visible");
}
#[test]
fn fully_outside_visible_rect_publishes_not_visible() {
let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::new(200.0, 0.0), Size::new(100.0, 100.0));
pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
w.paint(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(frames.len(), 1);
assert!(!frames[0].visible);
}
#[test]
fn no_visible_rect_constraint_means_fully_visible_with_no_clip() {
let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
w.paint(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(frames[0].clip, None);
assert!(frames[0].visible);
}
#[test]
fn parent_culling_of_the_whole_subtree_yields_no_frame_at_all() {
let mut counter = 0u64;
let row: FlexView<()> = Row(vec![any(
platform_view("dev.frust.MapFactory").size(50.0, 50.0)
)]);
let mut widget = row.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
pctx.constrain_visible_rect(Rect::from_origin_size(
Point::new(5000.0, 5000.0),
Size::new(10.0, 10.0),
));
widget.paint(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert!(
frames.is_empty(),
"a culled subtree must publish no frame at all, not a hidden one"
);
}
#[test]
fn translucent_surface_punches_the_slot_rect() {
let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
let mut scene = NullScene::default();
let mut pctx =
PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0)).with_translucent(true);
w.paint(&mut pctx, &mut scene);
assert_eq!(
scene.ops,
vec![SceneOp::Clear {
origin: Point::new(20.0, 30.0),
size: Size::new(100.0, 80.0),
}],
"a translucent slot must clear its own absolute rect"
);
}
#[test]
fn opaque_surface_does_not_punch_mode_a_unchanged() {
let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
w.paint(&mut pctx, &mut scene);
assert!(
scene.ops.is_empty(),
"an opaque-surface slot must paint nothing (no punch), Mode A unchanged"
);
assert_eq!(pctx.take_platform_views().len(), 1);
}
fn clear_rects(scene: &frust_scene::Scene) -> usize {
scene
.commands()
.iter()
.filter(|c| matches!(c, frust_scene::Command::ClearRect { .. }))
.count()
}
#[test]
fn a_resolved_translucency_downgrade_stops_the_punch_in_the_real_display_list() {
fn logic(_: &mut ()) -> PlatformViewView {
platform_view("dev.frust.MapFactory").size(100.0, 80.0)
}
let mut root: frust_core::RenderRoot<(), PlatformViewView> = frust_core::RenderRoot::new();
let mut state = ();
let mut scene = frust_scene::Scene::new();
root.set_surface_translucent(true);
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(200.0, 200.0));
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
root.paint(&mut builder, frust_core::FrameTime::ZERO);
}
assert_eq!(
clear_rects(&scene),
1,
"a translucent-resolved surface must punch exactly one slot rect"
);
root.set_surface_translucent(false);
assert!(
root.has_pending_change_flags(),
"a translucency downgrade must mark the tree dirty so the next \
frame actually repaints without the punch"
);
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(200.0, 200.0));
scene.reset();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
root.paint(&mut builder, frust_core::FrameTime::ZERO);
}
assert_eq!(
clear_rects(&scene),
0,
"an opaque-resolved surface must encode no punch at all (Mode A)"
);
}
#[cfg(debug_assertions)]
#[test]
fn debug_fill_paints_over_the_punch_on_a_translucent_surface() {
let view = platform_view("dev.frust.MapFactory")
.size(100.0, 80.0)
.debug_fill();
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 80.0)).with_translucent(true);
w.paint(&mut pctx, &mut scene);
assert_eq!(
scene.ops,
vec![
SceneOp::Clear {
origin: Point::ZERO,
size: Size::new(100.0, 80.0),
},
SceneOp::Fill {
origin: Point::ZERO,
size: Size::new(100.0, 80.0),
},
],
"debug_fill must paint OVER the punch, not under it"
);
}
#[test]
fn params_json_change_bumps_generation_exactly_once_unchanged_does_not() {
let prev = platform_view("dev.frust.MapFactory").params_json("a");
let mut w = build(&prev);
assert_eq!(w.params_generation, 0);
let same = platform_view("dev.frust.MapFactory").params_json("a");
rebuild(&prev, &same, &mut w);
assert_eq!(w.params_generation, 0, "unchanged params must not bump");
let changed = platform_view("dev.frust.MapFactory").params_json("b");
rebuild(&same, &changed, &mut w);
assert_eq!(w.params_generation, 1);
let changed_again = platform_view("dev.frust.MapFactory").params_json("c");
rebuild(&changed, &changed_again, &mut w);
assert_eq!(w.params_generation, 2);
}
#[test]
fn view_type_change_allocates_a_fresh_slot_id_and_resets_generation() {
let prev = platform_view("dev.frust.MapFactory").params_json("a");
let mut w = build(&prev);
w.params_generation = 3; let old_slot_id = w.slot_id;
let swapped = platform_view("dev.frust.OtherFactory").params_json("a");
rebuild(&prev, &swapped, &mut w);
assert_ne!(
w.slot_id, old_slot_id,
"a view_type swap must allocate a new slot id"
);
assert_eq!(
w.params_generation, 0,
"a swap resets generation like a fresh build"
);
assert_eq!(w.view_type, "dev.frust.OtherFactory");
}
#[test]
fn slot_id_survives_a_keyed_reorder() {
let mut counter = 0u64;
let a: FlexView<()> = FlexView::new(
Axis::Horizontal,
vec![
keyed(1u64, platform_view("dev.frust.A")),
keyed(2u64, platform_view("dev.frust.B")),
],
);
let mut widget = a.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
widget.paint(&mut pctx, &mut scene);
let before = pctx.take_platform_views();
assert_eq!(before.len(), 2);
let (id_a, id_b) = (before[0].slot_id, before[1].slot_id);
assert_ne!(id_a, id_b);
let b: FlexView<()> = FlexView::new(
Axis::Horizontal,
vec![
keyed(2u64, platform_view("dev.frust.B")),
keyed(1u64, platform_view("dev.frust.A")),
],
);
b.rebuild(&a, &mut widget, &mut BuildCtx::new(&mut counter));
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
widget.paint(&mut pctx2, &mut scene);
let after = pctx2.take_platform_views();
assert_eq!(after.len(), 2);
assert_eq!(after[0].slot_id, id_b, "key 2's slot survives the reorder");
assert_eq!(after[1].slot_id, id_a, "key 1's slot survives the reorder");
}
#[test]
fn shield_reports_its_absolute_painted_rect_and_paints_the_child_unchanged() {
let mut counter = 0u64;
let row: FlexView<()> = Row(vec![
any(SizedBox::<()>(Some(50.0), Some(30.0))),
any(shield(crate::test_support::leaf(80.0, 60.0))),
]);
let padded: PaddingView<()> = Padding(EdgeInsets::all(10.0), row);
let mut widget = padded.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
widget.paint(&mut pctx, &mut scene);
let expected = Rect::from_origin_size(Point::new(60.0, 10.0), Size::new(80.0, 60.0));
assert_eq!(pctx.take_input_shields(), vec![expected]);
assert_eq!(
scene.ops,
vec![SceneOp::Fill {
origin: Point::new(60.0, 10.0),
size: Size::new(80.0, 60.0),
}]
);
}
#[test]
fn two_shields_in_one_pass_both_survive() {
let mut counter = 0u64;
let row: FlexView<()> = Row(vec![
any(shield(crate::test_support::leaf(20.0, 20.0))),
any(shield(crate::test_support::leaf(30.0, 30.0))),
]);
let mut widget = row.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
widget.paint(&mut pctx, &mut scene);
let shields = pctx.take_input_shields();
assert_eq!(shields.len(), 2, "both shields must reach the render root");
assert_eq!(shields[0].size(), Size::new(20.0, 20.0));
assert_eq!(shields[1].size(), Size::new(30.0, 30.0));
}
#[test]
fn shield_local_still_publishes_its_absolute_rect_escape_hatch() {
let view = platform_view("dev.frust.MapFactory")
.size(100.0, 80.0)
.interactive()
.shield_local(Rect::new(0.0, 0.0, 20.0, 10.0));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
w.paint(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert!(frames[0].interactive);
assert_eq!(frames[0].shields, vec![Rect::new(20.0, 30.0, 40.0, 40.0)]);
assert!(
pctx.take_input_shields().is_empty(),
"a manual rect is NOT reported through the auto-collection channel"
);
}
static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn drain_retired() -> Vec<u64> {
frust_core::widget::take_retired_slots()
}
#[test]
fn removing_a_slot_from_a_container_reports_its_id_for_prompt_retire() {
let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _ = drain_retired();
let mut counter = 0u64;
let prev: FlexView<()> = Row(vec![any(platform_view("dev.frust.MapFactory"))]);
let mut widget = prev.build(&mut BuildCtx::new(&mut counter));
let mut scene = NullScene::default();
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
widget.paint(&mut pctx, &mut scene);
let slot_id = pctx.take_platform_views()[0].slot_id;
assert!(
drain_retired().is_empty(),
"a live slot reports no retire while it keeps painting"
);
let next: FlexView<()> = Row(vec![any(SizedBox::<()>(Some(10.0), Some(10.0)))]);
next.rebuild(&prev, &mut widget, &mut BuildCtx::new(&mut counter));
assert_eq!(
drain_retired(),
vec![slot_id],
"a torn-down slot reports its id exactly once"
);
}
#[test]
fn a_culled_slot_reports_no_retire() {
let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _ = drain_retired();
let mut counter = 0u64;
let row: FlexView<()> = Row(vec![any(
platform_view("dev.frust.MapFactory").size(50.0, 50.0)
)]);
let mut widget = row.build(&mut BuildCtx::new(&mut counter));
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
let mut scene = NullScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
pctx.constrain_visible_rect(Rect::from_origin_size(
Point::new(5000.0, 5000.0),
Size::new(10.0, 10.0),
));
widget.paint(&mut pctx, &mut scene);
assert!(pctx.take_platform_views().is_empty(), "the slot was culled");
row.rebuild(&row, &mut widget, &mut BuildCtx::new(&mut counter));
assert!(
drain_retired().is_empty(),
"a culled-but-live slot must never be retired"
);
}
}