use super::*;
struct PlatformViewProbeWidget {
slot_id: u64,
should_publish: bool,
}
impl crate::widget::Widget for PlatformViewProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
if self.should_publish {
ctx.publish_platform_view(PlatformViewFrame {
slot_id: self.slot_id,
view_type: "dev.frust.Probe".to_string(),
params_json: String::new(),
params_generation: 0,
rect: kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()),
clip: None,
visible: true,
interactive: false,
shields: Vec::new(),
command_index: 0,
});
ctx.report_input_shield(kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()));
}
}
}
struct PlatformViewRootWidget {
a: crate::widget::ChildPod,
b: crate::widget::ChildPod,
}
impl crate::widget::Widget for PlatformViewRootWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.a.layout_child(ctx, bc);
self.b.layout_child(ctx, bc);
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.a.paint_child(ctx, scene);
self.b.paint_child(ctx, scene);
}
}
struct PlatformViewRootView {
publish_a: bool,
publish_b: bool,
}
impl View<PvState> for PlatformViewRootView {
type Element = PlatformViewRootWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
PlatformViewRootWidget {
a: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
slot_id: 1,
should_publish: self.publish_a,
})),
b: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
slot_id: 2,
should_publish: self.publish_b,
})),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut Self::Element,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
if prev.publish_a != self.publish_a || prev.publish_b != self.publish_b {
element
.a
.widget_mut()
.downcast_mut::<PlatformViewProbeWidget>()
.expect("slot a stays a PlatformViewProbeWidget")
.should_publish = self.publish_a;
element
.b
.widget_mut()
.downcast_mut::<PlatformViewProbeWidget>()
.expect("slot b stays a PlatformViewProbeWidget")
.should_publish = self.publish_b;
ChangeFlags::PAINT
} else {
ChangeFlags::NONE
}
}
}
#[derive(Default)]
struct PvState {
publish_a: bool,
publish_b: bool,
}
fn platform_view_logic(state: &mut PvState) -> PlatformViewRootView {
PlatformViewRootView {
publish_a: state.publish_a,
publish_b: state.publish_b,
}
}
#[test]
fn platform_view_frames_arrive_in_order_and_clear_on_empty_pass() {
let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
let mut state = PvState {
publish_a: true,
publish_b: true,
};
root.rebuild(&mut platform_view_logic, &mut state);
root.layout(Size::new(200.0, 200.0));
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
let frames = root.platform_view_frames();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].slot_id, 1);
assert_eq!(frames[1].slot_id, 2);
state.publish_a = false;
state.publish_b = false;
root.rebuild(&mut platform_view_logic, &mut state);
root.layout(Size::new(200.0, 200.0));
root.paint(&mut scene, FrameTime::ZERO);
assert!(
root.platform_view_frames().is_empty(),
"a paint pass with no publishers must yield an empty slice"
);
}
#[test]
fn input_shields_arrive_in_order_and_clear_on_empty_pass() {
let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
let mut state = PvState {
publish_a: true,
publish_b: true,
};
root.rebuild(&mut platform_view_logic, &mut state);
root.layout(Size::new(200.0, 200.0));
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
assert_eq!(root.input_shields().len(), 2);
assert_eq!(
root.input_shields()[0],
Rect::from_origin_size(Point::ZERO, Size::new(10.0, 10.0)),
"a shield is reported in absolute paint coordinates"
);
state.publish_a = false;
state.publish_b = false;
root.rebuild(&mut platform_view_logic, &mut state);
root.layout(Size::new(200.0, 200.0));
root.paint(&mut scene, FrameTime::ZERO);
assert!(
root.input_shields().is_empty(),
"a paint pass reporting no shields must yield an empty slice"
);
}
#[test]
fn retired_platform_views_drain_exactly_once() {
let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
let _ = root.take_retired_platform_views();
crate::widget::report_retired_slot(7);
crate::widget::report_retired_slot(9);
assert_eq!(root.take_retired_platform_views(), vec![7, 9]);
assert!(
root.take_retired_platform_views().is_empty(),
"draining is destructive — a second drain reports nothing"
);
}
#[test]
fn retired_platform_views_are_capped_dropping_the_oldest() {
let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
let _ = root.take_retired_platform_views();
for slot_id in 0..1_000u64 {
crate::widget::report_retired_slot(slot_id);
}
let drained = root.take_retired_platform_views();
assert!(drained.len() <= 256, "the pending list stays bounded");
assert_eq!(
*drained.last().expect("non-empty"),
999,
"the newest report always survives"
);
assert!(
!drained.contains(&0),
"the oldest reports are the ones dropped"
);
}
static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());