1use frust_core::accesskit::Role;
92use frust_core::widget::{PlatformViewFrame, next_slot_id, report_retired_slot};
93use frust_core::{
94 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
95 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
96};
97use kurbo::{Point, Rect, Size};
98#[cfg(debug_assertions)]
99use peniko::Color;
100
101#[derive(Clone, Copy, Debug, PartialEq)]
109enum SlotSize {
110 Expand,
112 Explicit(f64, f64),
114}
115
116#[cfg(debug_assertions)]
121const DEBUG_FILL_COLOR: Color = Color::from_rgba8(0xE0, 0x00, 0xE0, 0x40);
122
123pub struct PlatformViewView {
125 view_type: String,
126 params_json: String,
127 slot_size: SlotSize,
128 #[cfg(debug_assertions)]
129 debug_fill: bool,
130 semantics_label: Option<String>,
131 interactive: bool,
132 shields_local: Vec<Rect>,
133}
134
135pub fn platform_view(view_type: impl Into<String>) -> PlatformViewView {
140 PlatformViewView {
141 view_type: view_type.into(),
142 params_json: String::new(),
143 slot_size: SlotSize::Expand,
144 #[cfg(debug_assertions)]
145 debug_fill: false,
146 semantics_label: None,
147 interactive: false,
148 shields_local: Vec::new(),
149 }
150}
151
152impl PlatformViewView {
153 pub fn params_json(mut self, params_json: impl Into<String>) -> Self {
159 self.params_json = params_json.into();
160 self
161 }
162
163 pub fn size(mut self, width: f64, height: f64) -> Self {
168 self.slot_size = SlotSize::Explicit(width, height);
169 self
170 }
171
172 pub fn expand(mut self) -> Self {
176 self.slot_size = SlotSize::Expand;
177 self
178 }
179
180 #[cfg(debug_assertions)]
186 pub fn debug_fill(mut self) -> Self {
187 self.debug_fill = true;
188 self
189 }
190
191 pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
197 self.semantics_label = Some(label.into());
198 self
199 }
200
201 pub fn interactive(mut self) -> Self {
207 self.interactive = true;
208 self
209 }
210
211 pub fn shield_local(mut self, rect: Rect) -> Self {
225 self.shields_local.push(rect);
226 self
227 }
228}
229
230pub struct PlatformViewWidget {
232 slot_id: u64,
235 view_type: String,
236 params_json: String,
237 params_generation: u64,
241 slot_size: SlotSize,
242 #[cfg(debug_assertions)]
243 debug_fill: bool,
244 semantics_label: Option<String>,
245 interactive: bool,
246 shields_local: Vec<Rect>,
247}
248
249impl<State: 'static> View<State> for PlatformViewView {
250 type Element = PlatformViewWidget;
251
252 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PlatformViewWidget {
253 PlatformViewWidget {
254 slot_id: next_slot_id(),
255 view_type: self.view_type.clone(),
256 params_json: self.params_json.clone(),
257 params_generation: 0,
258 slot_size: self.slot_size,
259 #[cfg(debug_assertions)]
260 debug_fill: self.debug_fill,
261 semantics_label: self.semantics_label.clone(),
262 interactive: self.interactive,
263 shields_local: self.shields_local.clone(),
264 }
265 }
266
267 fn rebuild(
268 &self,
269 prev: &Self,
270 element: &mut PlatformViewWidget,
271 _ctx: &mut BuildCtx<'_>,
272 ) -> ChangeFlags {
273 let mut flags = ChangeFlags::NONE;
274
275 if prev.view_type != self.view_type {
276 element.slot_id = next_slot_id();
280 element.view_type = self.view_type.clone();
281 element.params_json = self.params_json.clone();
282 element.params_generation = 0;
283 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
284 } else if prev.params_json != self.params_json {
285 element.params_json = self.params_json.clone();
286 element.params_generation += 1;
287 flags |= ChangeFlags::PAINT;
288 }
289
290 if prev.slot_size != self.slot_size {
291 element.slot_size = self.slot_size;
292 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
293 }
294
295 #[cfg(debug_assertions)]
296 if prev.debug_fill != self.debug_fill {
297 element.debug_fill = self.debug_fill;
298 flags |= ChangeFlags::PAINT;
299 }
300
301 if prev.semantics_label != self.semantics_label {
302 element.semantics_label = self.semantics_label.clone();
306 }
307
308 if prev.interactive != self.interactive || prev.shields_local != self.shields_local {
309 element.interactive = self.interactive;
310 element.shields_local = self.shields_local.clone();
311 flags |= ChangeFlags::PAINT;
314 }
315
316 flags
317 }
318
319 fn teardown(&self, element: &mut PlatformViewWidget, _ctx: &mut BuildCtx<'_>) {
320 report_retired_slot(element.slot_id);
325 }
326}
327
328impl Widget for PlatformViewWidget {
329 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
330 match self.slot_size {
333 SlotSize::Explicit(width, height) => bc.constrain(Size::new(width, height)),
334 SlotSize::Expand => bc.max(),
335 }
336 }
337
338 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
339 let rect = Rect::from_origin_size(ctx.origin(), ctx.size());
340
341 let (clip, visible) = match ctx.visible_rect() {
348 Some(visible_rect) => (
349 Some(visible_rect.intersect(rect)),
350 visible_rect.overlaps(rect),
351 ),
352 None => (None, true),
353 };
354
355 ctx.publish_platform_view(PlatformViewFrame {
357 slot_id: self.slot_id,
358 view_type: self.view_type.clone(),
359 params_json: self.params_json.clone(),
360 params_generation: self.params_generation,
361 rect,
362 clip,
363 visible,
364 interactive: self.interactive,
365 shields: self
366 .shields_local
367 .iter()
368 .map(|r| *r + ctx.origin().to_vec2())
369 .collect(),
370 });
371
372 if ctx.is_translucent() {
384 scene.clear_rect(ctx.origin(), ctx.size());
385 }
386
387 #[cfg(debug_assertions)]
392 if self.debug_fill {
393 scene.fill_rect(ctx.origin(), ctx.size(), DEBUG_FILL_COLOR);
394 }
395 }
396
397 fn semantics(&self, ctx: &mut SemanticsCtx) {
398 if let Some(label) = &self.semantics_label {
399 ctx.push_node(Role::GenericContainer, |node| {
400 node.set_label(label.as_str());
401 });
402 }
403 }
404}
405
406pub struct ShieldView<State: 'static> {
408 child: AnyView<State>,
409}
410
411pub fn shield<State: 'static, V: View<State>>(child: V) -> ShieldView<State> {
432 ShieldView { child: any(child) }
433}
434
435pub struct ShieldWidget {
437 child: ChildPod,
438}
439
440impl<State: 'static> View<State> for ShieldView<State> {
441 type Element = ShieldWidget;
442
443 fn build(&self, ctx: &mut BuildCtx<'_>) -> ShieldWidget {
444 ShieldWidget {
445 child: crate::authoring::build_child(&self.child, ctx),
446 }
447 }
448
449 fn rebuild(
450 &self,
451 prev: &Self,
452 element: &mut ShieldWidget,
453 ctx: &mut BuildCtx<'_>,
454 ) -> ChangeFlags {
455 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
456 }
457
458 fn teardown(&self, element: &mut ShieldWidget, ctx: &mut BuildCtx<'_>) {
459 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
460 }
461}
462
463impl Widget for ShieldWidget {
464 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
465 self.child.set_origin(Point::ZERO);
468 self.child.layout_child(ctx, bc)
469 }
470
471 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
472 self.child.paint_child(ctx, scene);
476 ctx.report_input_shield(Rect::from_origin_size(ctx.origin(), ctx.size()));
477 }
478
479 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
480 crate::authoring::route_event_single(&mut self.child, ctx, event)
481 }
482
483 fn semantics(&self, ctx: &mut SemanticsCtx) {
484 self.child.semantics_child(ctx);
487 }
488
489 crate::authoring::visit_children!(child);
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::{EdgeInsets, FlexView, Padding, PaddingView, SizedBox, row};
496 use frust_core::{BuildCtx, ChildPod};
497 use kurbo::Point;
498
499 fn build(view: &PlatformViewView) -> PlatformViewWidget {
500 let mut counter = 0u64;
501 <PlatformViewView as View<()>>::build(view, &mut BuildCtx::new(&mut counter))
502 }
503
504 fn rebuild(
505 prev: &PlatformViewView,
506 next: &PlatformViewView,
507 element: &mut PlatformViewWidget,
508 ) -> ChangeFlags {
509 let mut counter = 0u64;
510 <PlatformViewView as View<()>>::rebuild(
511 next,
512 prev,
513 element,
514 &mut BuildCtx::new(&mut counter),
515 )
516 }
517
518 #[derive(Default)]
522 struct NullScene {
523 ops: Vec<SceneOp>,
524 }
525
526 #[derive(Debug, PartialEq)]
527 enum SceneOp {
528 Clear { origin: Point, size: Size },
529 Fill { origin: Point, size: Size },
530 }
531
532 impl PaintScene for NullScene {
533 fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
534 self.ops.push(SceneOp::Fill { origin, size });
535 }
536 fn draw_text(&mut self, _origin: Point, _text: &str) {}
537 fn clear_rect(&mut self, origin: Point, size: Size) {
538 self.ops.push(SceneOp::Clear { origin, size });
539 }
540 }
541
542 #[test]
545 fn expand_fills_the_incoming_constraints_max() {
546 let view = platform_view("dev.frust.MapFactory");
547 let mut w = build(&view);
548 let mut lctx = LayoutCtx::new();
549 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
550 assert_eq!(size, Size::new(300.0, 200.0));
551 }
552
553 #[test]
554 fn explicit_size_is_clamped_into_constraints() {
555 let view = platform_view("dev.frust.MapFactory").size(1000.0, 1000.0);
556 let mut w = build(&view);
557 let mut lctx = LayoutCtx::new();
558 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
559 assert_eq!(size, Size::new(300.0, 200.0));
560 }
561
562 #[test]
565 fn publishes_absolute_rect_under_padding_flex_nesting_and_scroll_offset() {
566 let mut counter = 0u64;
567 let insets = EdgeInsets::all(10.0);
568 let row: FlexView<()> = row()
569 .child(SizedBox::<()>(Some(50.0), Some(30.0)))
570 .child(platform_view("dev.frust.MapFactory").size(80.0, 60.0));
571 let padded: PaddingView<()> = Padding(insets, row);
572
573 let widget = padded.build(&mut BuildCtx::new(&mut counter));
574 let mut lctx = LayoutCtx::new();
575
576 let mut pod = ChildPod::new(Box::new(widget));
581 pod.set_origin(Point::new(-15.0, -25.0));
582 pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
583
584 let mut scene = NullScene::default();
585 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(2000.0, 2000.0));
586 pod.paint_child(&mut pctx, &mut scene);
587
588 let frames = pctx.take_platform_views();
589 assert_eq!(frames.len(), 1);
590 let expected_origin = Point::new(-15.0 + 10.0 + 50.0, -25.0 + 10.0);
594 assert_eq!(
595 frames[0].rect,
596 Rect::from_origin_size(expected_origin, Size::new(80.0, 60.0))
597 );
598 }
599
600 #[test]
603 fn clip_is_intersected_with_visible_rect_when_straddling_edge() {
604 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
605 let mut w = build(&view);
606 let mut lctx = LayoutCtx::new();
607 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
608
609 let mut scene = NullScene::default();
610 let mut pctx = PaintCtx::new(Point::new(50.0, 0.0), Size::new(100.0, 100.0));
611 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
612 w.paint(&mut pctx, &mut scene);
613
614 let frames = pctx.take_platform_views();
615 assert_eq!(frames.len(), 1);
616 let frame: &PlatformViewFrame = &frames[0];
617 assert_eq!(
618 frame.rect,
619 Rect::from_origin_size(Point::new(50.0, 0.0), Size::new(100.0, 100.0))
620 );
621 assert_eq!(frame.clip, Some(Rect::new(50.0, 0.0, 100.0, 100.0)));
622 assert!(frame.visible, "an edge-straddling slot is still visible");
623 }
624
625 #[test]
626 fn fully_outside_visible_rect_publishes_not_visible() {
627 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
628 let mut w = build(&view);
629 let mut lctx = LayoutCtx::new();
630 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
631
632 let mut scene = NullScene::default();
633 let mut pctx = PaintCtx::new(Point::new(200.0, 0.0), Size::new(100.0, 100.0));
636 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
637 w.paint(&mut pctx, &mut scene);
638
639 let frames = pctx.take_platform_views();
640 assert_eq!(frames.len(), 1);
641 assert!(!frames[0].visible);
642 }
643
644 #[test]
645 fn no_visible_rect_constraint_means_fully_visible_with_no_clip() {
646 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
647 let mut w = build(&view);
648 let mut lctx = LayoutCtx::new();
649 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
650
651 let mut scene = NullScene::default();
652 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
653 w.paint(&mut pctx, &mut scene);
654
655 let frames = pctx.take_platform_views();
656 assert_eq!(frames[0].clip, None);
657 assert!(frames[0].visible);
658 }
659
660 #[test]
661 fn parent_culling_of_the_whole_subtree_yields_no_frame_at_all() {
662 let mut counter = 0u64;
663 let row: FlexView<()> = row().child(platform_view("dev.frust.MapFactory").size(50.0, 50.0));
664 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
665 let mut lctx = LayoutCtx::new();
666 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
667
668 let mut scene = NullScene::default();
669 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
670 pctx.constrain_visible_rect(Rect::from_origin_size(
673 Point::new(5000.0, 5000.0),
674 Size::new(10.0, 10.0),
675 ));
676 widget.paint(&mut pctx, &mut scene);
677
678 let frames = pctx.take_platform_views();
679 assert!(
680 frames.is_empty(),
681 "a culled subtree must publish no frame at all, not a hidden one"
682 );
683 }
684
685 #[test]
688 fn translucent_surface_punches_the_slot_rect() {
689 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
692 let mut w = build(&view);
693 let mut lctx = LayoutCtx::new();
694 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
695
696 let mut scene = NullScene::default();
697 let mut pctx =
698 PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0)).with_translucent(true);
699 w.paint(&mut pctx, &mut scene);
700
701 assert_eq!(
702 scene.ops,
703 vec![SceneOp::Clear {
704 origin: Point::new(20.0, 30.0),
705 size: Size::new(100.0, 80.0),
706 }],
707 "a translucent slot must clear its own absolute rect"
708 );
709 }
710
711 #[test]
712 fn opaque_surface_does_not_punch_mode_a_unchanged() {
713 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
716 let mut w = build(&view);
717 let mut lctx = LayoutCtx::new();
718 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
719
720 let mut scene = NullScene::default();
721 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
723 w.paint(&mut pctx, &mut scene);
724
725 assert!(
726 scene.ops.is_empty(),
727 "an opaque-surface slot must paint nothing (no punch), Mode A unchanged"
728 );
729 assert_eq!(pctx.take_platform_views().len(), 1);
731 }
732
733 fn clear_rects(scene: &frust_scene::Scene) -> usize {
740 scene
741 .commands()
742 .iter()
743 .filter(|c| matches!(c, frust_scene::Command::ClearRect { .. }))
744 .count()
745 }
746
747 #[test]
748 fn a_resolved_translucency_downgrade_stops_the_punch_in_the_real_display_list() {
749 fn logic(_: &mut ()) -> PlatformViewView {
756 platform_view("dev.frust.MapFactory").size(100.0, 80.0)
757 }
758
759 let mut root: frust_core::RenderRoot<(), PlatformViewView> = frust_core::RenderRoot::new();
760 let mut state = ();
761 let mut scene = frust_scene::Scene::new();
762
763 root.set_surface_translucent(true);
765 root.rebuild(&mut logic, &mut state);
766 root.layout(Size::new(200.0, 200.0));
767 {
768 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
769 root.paint(&mut builder, frust_core::FrameTime::ZERO);
770 }
771 assert_eq!(
772 clear_rects(&scene),
773 1,
774 "a translucent-resolved surface must punch exactly one slot rect"
775 );
776
777 root.set_surface_translucent(false);
779 assert!(
780 root.has_pending_change_flags(),
781 "a translucency downgrade must mark the tree dirty so the next \
782 frame actually repaints without the punch"
783 );
784 root.rebuild(&mut logic, &mut state);
785 root.layout(Size::new(200.0, 200.0));
786 scene.reset();
787 {
788 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
789 root.paint(&mut builder, frust_core::FrameTime::ZERO);
790 }
791 assert_eq!(
792 clear_rects(&scene),
793 0,
794 "an opaque-resolved surface must encode no punch at all (Mode A)"
795 );
796 }
797
798 #[cfg(debug_assertions)]
799 #[test]
800 fn debug_fill_paints_over_the_punch_on_a_translucent_surface() {
801 let view = platform_view("dev.frust.MapFactory")
804 .size(100.0, 80.0)
805 .debug_fill();
806 let mut w = build(&view);
807 let mut lctx = LayoutCtx::new();
808 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
809
810 let mut scene = NullScene::default();
811 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 80.0)).with_translucent(true);
812 w.paint(&mut pctx, &mut scene);
813
814 assert_eq!(
815 scene.ops,
816 vec![
817 SceneOp::Clear {
818 origin: Point::ZERO,
819 size: Size::new(100.0, 80.0),
820 },
821 SceneOp::Fill {
822 origin: Point::ZERO,
823 size: Size::new(100.0, 80.0),
824 },
825 ],
826 "debug_fill must paint OVER the punch, not under it"
827 );
828 }
829
830 #[test]
833 fn params_json_change_bumps_generation_exactly_once_unchanged_does_not() {
834 let prev = platform_view("dev.frust.MapFactory").params_json("a");
835 let mut w = build(&prev);
836 assert_eq!(w.params_generation, 0);
837
838 let same = platform_view("dev.frust.MapFactory").params_json("a");
839 rebuild(&prev, &same, &mut w);
840 assert_eq!(w.params_generation, 0, "unchanged params must not bump");
841
842 let changed = platform_view("dev.frust.MapFactory").params_json("b");
843 rebuild(&same, &changed, &mut w);
844 assert_eq!(w.params_generation, 1);
845
846 let changed_again = platform_view("dev.frust.MapFactory").params_json("c");
847 rebuild(&changed, &changed_again, &mut w);
848 assert_eq!(w.params_generation, 2);
849 }
850
851 #[test]
852 fn view_type_change_allocates_a_fresh_slot_id_and_resets_generation() {
853 let prev = platform_view("dev.frust.MapFactory").params_json("a");
854 let mut w = build(&prev);
855 w.params_generation = 3; let old_slot_id = w.slot_id;
857
858 let swapped = platform_view("dev.frust.OtherFactory").params_json("a");
859 rebuild(&prev, &swapped, &mut w);
860
861 assert_ne!(
862 w.slot_id, old_slot_id,
863 "a view_type swap must allocate a new slot id"
864 );
865 assert_eq!(
866 w.params_generation, 0,
867 "a swap resets generation like a fresh build"
868 );
869 assert_eq!(w.view_type, "dev.frust.OtherFactory");
870 }
871
872 #[test]
875 fn slot_id_survives_a_keyed_reorder() {
876 let mut counter = 0u64;
882 let a: FlexView<()> = row()
883 .keyed(1u64, platform_view("dev.frust.A"))
884 .keyed(2u64, platform_view("dev.frust.B"));
885 let mut widget = a.build(&mut BuildCtx::new(&mut counter));
886 let mut lctx = LayoutCtx::new();
887 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
888
889 let mut scene = NullScene::default();
890 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
891 widget.paint(&mut pctx, &mut scene);
892 let before = pctx.take_platform_views();
893 assert_eq!(before.len(), 2);
894 let (id_a, id_b) = (before[0].slot_id, before[1].slot_id);
895 assert_ne!(id_a, id_b);
896
897 let b: FlexView<()> = row()
899 .keyed(2u64, platform_view("dev.frust.B"))
900 .keyed(1u64, platform_view("dev.frust.A"));
901 b.rebuild(&a, &mut widget, &mut BuildCtx::new(&mut counter));
902 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
903
904 let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
905 widget.paint(&mut pctx2, &mut scene);
906 let after = pctx2.take_platform_views();
907 assert_eq!(after.len(), 2);
908 assert_eq!(after[0].slot_id, id_b, "key 2's slot survives the reorder");
911 assert_eq!(after[1].slot_id, id_a, "key 1's slot survives the reorder");
912 }
913
914 #[test]
917 fn shield_reports_its_absolute_painted_rect_and_paints_the_child_unchanged() {
918 let mut counter = 0u64;
922 let row: FlexView<()> = row()
923 .child(SizedBox::<()>(Some(50.0), Some(30.0)))
924 .child(shield(crate::test_support::leaf(80.0, 60.0)));
925 let padded: PaddingView<()> = Padding(EdgeInsets::all(10.0), row);
926
927 let mut widget = padded.build(&mut BuildCtx::new(&mut counter));
928 let mut lctx = LayoutCtx::new();
929 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
930
931 let mut scene = NullScene::default();
932 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
933 widget.paint(&mut pctx, &mut scene);
934
935 let expected = Rect::from_origin_size(Point::new(60.0, 10.0), Size::new(80.0, 60.0));
936 assert_eq!(pctx.take_input_shields(), vec![expected]);
937 assert_eq!(
940 scene.ops,
941 vec![SceneOp::Fill {
942 origin: Point::new(60.0, 10.0),
943 size: Size::new(80.0, 60.0),
944 }]
945 );
946 }
947
948 #[test]
949 fn two_shields_in_one_pass_both_survive() {
950 let mut counter = 0u64;
953 let row: FlexView<()> = row()
954 .child(shield(crate::test_support::leaf(20.0, 20.0)))
955 .child(shield(crate::test_support::leaf(30.0, 30.0)));
956 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
957 let mut lctx = LayoutCtx::new();
958 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
959
960 let mut scene = NullScene::default();
961 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
962 widget.paint(&mut pctx, &mut scene);
963
964 let shields = pctx.take_input_shields();
965 assert_eq!(shields.len(), 2, "both shields must reach the render root");
966 assert_eq!(shields[0].size(), Size::new(20.0, 20.0));
967 assert_eq!(shields[1].size(), Size::new(30.0, 30.0));
968 }
969
970 #[test]
971 fn shield_local_still_publishes_its_absolute_rect_escape_hatch() {
972 let view = platform_view("dev.frust.MapFactory")
976 .size(100.0, 80.0)
977 .interactive()
978 .shield_local(Rect::new(0.0, 0.0, 20.0, 10.0));
979 let mut w = build(&view);
980 let mut lctx = LayoutCtx::new();
981 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
982
983 let mut scene = NullScene::default();
984 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
985 w.paint(&mut pctx, &mut scene);
986
987 let frames = pctx.take_platform_views();
988 assert!(frames[0].interactive);
989 assert_eq!(frames[0].shields, vec![Rect::new(20.0, 30.0, 40.0, 40.0)]);
990 assert!(
991 pctx.take_input_shields().is_empty(),
992 "a manual rect is NOT reported through the auto-collection channel"
993 );
994 }
995
996 static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1001
1002 fn drain_retired() -> Vec<u64> {
1005 frust_core::widget::take_retired_slots()
1006 }
1007
1008 #[test]
1009 fn removing_a_slot_from_a_container_reports_its_id_for_prompt_retire() {
1010 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1011 let _ = drain_retired();
1012
1013 let mut counter = 0u64;
1016 let prev: FlexView<()> = row().child(platform_view("dev.frust.MapFactory"));
1017 let mut widget = prev.build(&mut BuildCtx::new(&mut counter));
1018
1019 let mut scene = NullScene::default();
1020 let mut lctx = LayoutCtx::new();
1021 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
1022 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
1023 widget.paint(&mut pctx, &mut scene);
1024 let slot_id = pctx.take_platform_views()[0].slot_id;
1025 assert!(
1026 drain_retired().is_empty(),
1027 "a live slot reports no retire while it keeps painting"
1028 );
1029
1030 let next: FlexView<()> = row().child(SizedBox::<()>(Some(10.0), Some(10.0)));
1031 next.rebuild(&prev, &mut widget, &mut BuildCtx::new(&mut counter));
1032
1033 assert_eq!(
1034 drain_retired(),
1035 vec![slot_id],
1036 "a torn-down slot reports its id exactly once"
1037 );
1038 }
1039
1040 #[test]
1041 fn a_culled_slot_reports_no_retire() {
1042 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1047 let _ = drain_retired();
1048
1049 let mut counter = 0u64;
1050 let row: FlexView<()> = row().child(platform_view("dev.frust.MapFactory").size(50.0, 50.0));
1051 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
1052 let mut lctx = LayoutCtx::new();
1053 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1054
1055 let mut scene = NullScene::default();
1056 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1057 pctx.constrain_visible_rect(Rect::from_origin_size(
1058 Point::new(5000.0, 5000.0),
1059 Size::new(10.0, 10.0),
1060 ));
1061 widget.paint(&mut pctx, &mut scene);
1062 assert!(pctx.take_platform_views().is_empty(), "the slot was culled");
1063
1064 row.rebuild(&row, &mut widget, &mut BuildCtx::new(&mut counter));
1066 assert!(
1067 drain_retired().is_empty(),
1068 "a culled-but-live slot must never be retired"
1069 );
1070 }
1071}