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::{Axis, EdgeInsets, FlexView, Padding, PaddingView, Row, SizedBox, keyed};
496 use frust_core::{BuildCtx, ChildPod, any};
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(vec![
569 any(SizedBox::<()>(Some(50.0), Some(30.0))),
570 any(platform_view("dev.frust.MapFactory").size(80.0, 60.0)),
571 ]);
572 let padded: PaddingView<()> = Padding(insets, row);
573
574 let widget = padded.build(&mut BuildCtx::new(&mut counter));
575 let mut lctx = LayoutCtx::new();
576
577 let mut pod = ChildPod::new(Box::new(widget));
582 pod.set_origin(Point::new(-15.0, -25.0));
583 pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
584
585 let mut scene = NullScene::default();
586 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(2000.0, 2000.0));
587 pod.paint_child(&mut pctx, &mut scene);
588
589 let frames = pctx.take_platform_views();
590 assert_eq!(frames.len(), 1);
591 let expected_origin = Point::new(-15.0 + 10.0 + 50.0, -25.0 + 10.0);
595 assert_eq!(
596 frames[0].rect,
597 Rect::from_origin_size(expected_origin, Size::new(80.0, 60.0))
598 );
599 }
600
601 #[test]
604 fn clip_is_intersected_with_visible_rect_when_straddling_edge() {
605 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
606 let mut w = build(&view);
607 let mut lctx = LayoutCtx::new();
608 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
609
610 let mut scene = NullScene::default();
611 let mut pctx = PaintCtx::new(Point::new(50.0, 0.0), Size::new(100.0, 100.0));
612 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
613 w.paint(&mut pctx, &mut scene);
614
615 let frames = pctx.take_platform_views();
616 assert_eq!(frames.len(), 1);
617 let frame: &PlatformViewFrame = &frames[0];
618 assert_eq!(
619 frame.rect,
620 Rect::from_origin_size(Point::new(50.0, 0.0), Size::new(100.0, 100.0))
621 );
622 assert_eq!(frame.clip, Some(Rect::new(50.0, 0.0, 100.0, 100.0)));
623 assert!(frame.visible, "an edge-straddling slot is still visible");
624 }
625
626 #[test]
627 fn fully_outside_visible_rect_publishes_not_visible() {
628 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
629 let mut w = build(&view);
630 let mut lctx = LayoutCtx::new();
631 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
632
633 let mut scene = NullScene::default();
634 let mut pctx = PaintCtx::new(Point::new(200.0, 0.0), Size::new(100.0, 100.0));
637 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
638 w.paint(&mut pctx, &mut scene);
639
640 let frames = pctx.take_platform_views();
641 assert_eq!(frames.len(), 1);
642 assert!(!frames[0].visible);
643 }
644
645 #[test]
646 fn no_visible_rect_constraint_means_fully_visible_with_no_clip() {
647 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
648 let mut w = build(&view);
649 let mut lctx = LayoutCtx::new();
650 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
651
652 let mut scene = NullScene::default();
653 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
654 w.paint(&mut pctx, &mut scene);
655
656 let frames = pctx.take_platform_views();
657 assert_eq!(frames[0].clip, None);
658 assert!(frames[0].visible);
659 }
660
661 #[test]
662 fn parent_culling_of_the_whole_subtree_yields_no_frame_at_all() {
663 let mut counter = 0u64;
664 let row: FlexView<()> = Row(vec![any(
665 platform_view("dev.frust.MapFactory").size(50.0, 50.0)
666 )]);
667 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
668 let mut lctx = LayoutCtx::new();
669 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
670
671 let mut scene = NullScene::default();
672 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
673 pctx.constrain_visible_rect(Rect::from_origin_size(
676 Point::new(5000.0, 5000.0),
677 Size::new(10.0, 10.0),
678 ));
679 widget.paint(&mut pctx, &mut scene);
680
681 let frames = pctx.take_platform_views();
682 assert!(
683 frames.is_empty(),
684 "a culled subtree must publish no frame at all, not a hidden one"
685 );
686 }
687
688 #[test]
691 fn translucent_surface_punches_the_slot_rect() {
692 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
695 let mut w = build(&view);
696 let mut lctx = LayoutCtx::new();
697 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
698
699 let mut scene = NullScene::default();
700 let mut pctx =
701 PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0)).with_translucent(true);
702 w.paint(&mut pctx, &mut scene);
703
704 assert_eq!(
705 scene.ops,
706 vec![SceneOp::Clear {
707 origin: Point::new(20.0, 30.0),
708 size: Size::new(100.0, 80.0),
709 }],
710 "a translucent slot must clear its own absolute rect"
711 );
712 }
713
714 #[test]
715 fn opaque_surface_does_not_punch_mode_a_unchanged() {
716 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
719 let mut w = build(&view);
720 let mut lctx = LayoutCtx::new();
721 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
722
723 let mut scene = NullScene::default();
724 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
726 w.paint(&mut pctx, &mut scene);
727
728 assert!(
729 scene.ops.is_empty(),
730 "an opaque-surface slot must paint nothing (no punch), Mode A unchanged"
731 );
732 assert_eq!(pctx.take_platform_views().len(), 1);
734 }
735
736 fn clear_rects(scene: &frust_scene::Scene) -> usize {
743 scene
744 .commands()
745 .iter()
746 .filter(|c| matches!(c, frust_scene::Command::ClearRect { .. }))
747 .count()
748 }
749
750 #[test]
751 fn a_resolved_translucency_downgrade_stops_the_punch_in_the_real_display_list() {
752 fn logic(_: &mut ()) -> PlatformViewView {
759 platform_view("dev.frust.MapFactory").size(100.0, 80.0)
760 }
761
762 let mut root: frust_core::RenderRoot<(), PlatformViewView> = frust_core::RenderRoot::new();
763 let mut state = ();
764 let mut scene = frust_scene::Scene::new();
765
766 root.set_surface_translucent(true);
768 root.rebuild(&mut logic, &mut state);
769 root.layout(Size::new(200.0, 200.0));
770 {
771 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
772 root.paint(&mut builder, frust_core::FrameTime::ZERO);
773 }
774 assert_eq!(
775 clear_rects(&scene),
776 1,
777 "a translucent-resolved surface must punch exactly one slot rect"
778 );
779
780 root.set_surface_translucent(false);
782 assert!(
783 root.has_pending_change_flags(),
784 "a translucency downgrade must mark the tree dirty so the next \
785 frame actually repaints without the punch"
786 );
787 root.rebuild(&mut logic, &mut state);
788 root.layout(Size::new(200.0, 200.0));
789 scene.reset();
790 {
791 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
792 root.paint(&mut builder, frust_core::FrameTime::ZERO);
793 }
794 assert_eq!(
795 clear_rects(&scene),
796 0,
797 "an opaque-resolved surface must encode no punch at all (Mode A)"
798 );
799 }
800
801 #[cfg(debug_assertions)]
802 #[test]
803 fn debug_fill_paints_over_the_punch_on_a_translucent_surface() {
804 let view = platform_view("dev.frust.MapFactory")
807 .size(100.0, 80.0)
808 .debug_fill();
809 let mut w = build(&view);
810 let mut lctx = LayoutCtx::new();
811 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
812
813 let mut scene = NullScene::default();
814 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 80.0)).with_translucent(true);
815 w.paint(&mut pctx, &mut scene);
816
817 assert_eq!(
818 scene.ops,
819 vec![
820 SceneOp::Clear {
821 origin: Point::ZERO,
822 size: Size::new(100.0, 80.0),
823 },
824 SceneOp::Fill {
825 origin: Point::ZERO,
826 size: Size::new(100.0, 80.0),
827 },
828 ],
829 "debug_fill must paint OVER the punch, not under it"
830 );
831 }
832
833 #[test]
836 fn params_json_change_bumps_generation_exactly_once_unchanged_does_not() {
837 let prev = platform_view("dev.frust.MapFactory").params_json("a");
838 let mut w = build(&prev);
839 assert_eq!(w.params_generation, 0);
840
841 let same = platform_view("dev.frust.MapFactory").params_json("a");
842 rebuild(&prev, &same, &mut w);
843 assert_eq!(w.params_generation, 0, "unchanged params must not bump");
844
845 let changed = platform_view("dev.frust.MapFactory").params_json("b");
846 rebuild(&same, &changed, &mut w);
847 assert_eq!(w.params_generation, 1);
848
849 let changed_again = platform_view("dev.frust.MapFactory").params_json("c");
850 rebuild(&changed, &changed_again, &mut w);
851 assert_eq!(w.params_generation, 2);
852 }
853
854 #[test]
855 fn view_type_change_allocates_a_fresh_slot_id_and_resets_generation() {
856 let prev = platform_view("dev.frust.MapFactory").params_json("a");
857 let mut w = build(&prev);
858 w.params_generation = 3; let old_slot_id = w.slot_id;
860
861 let swapped = platform_view("dev.frust.OtherFactory").params_json("a");
862 rebuild(&prev, &swapped, &mut w);
863
864 assert_ne!(
865 w.slot_id, old_slot_id,
866 "a view_type swap must allocate a new slot id"
867 );
868 assert_eq!(
869 w.params_generation, 0,
870 "a swap resets generation like a fresh build"
871 );
872 assert_eq!(w.view_type, "dev.frust.OtherFactory");
873 }
874
875 #[test]
878 fn slot_id_survives_a_keyed_reorder() {
879 let mut counter = 0u64;
885 let a: FlexView<()> = FlexView::new(
886 Axis::Horizontal,
887 vec![
888 keyed(1u64, platform_view("dev.frust.A")),
889 keyed(2u64, platform_view("dev.frust.B")),
890 ],
891 );
892 let mut widget = a.build(&mut BuildCtx::new(&mut counter));
893 let mut lctx = LayoutCtx::new();
894 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
895
896 let mut scene = NullScene::default();
897 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
898 widget.paint(&mut pctx, &mut scene);
899 let before = pctx.take_platform_views();
900 assert_eq!(before.len(), 2);
901 let (id_a, id_b) = (before[0].slot_id, before[1].slot_id);
902 assert_ne!(id_a, id_b);
903
904 let b: FlexView<()> = FlexView::new(
906 Axis::Horizontal,
907 vec![
908 keyed(2u64, platform_view("dev.frust.B")),
909 keyed(1u64, platform_view("dev.frust.A")),
910 ],
911 );
912 b.rebuild(&a, &mut widget, &mut BuildCtx::new(&mut counter));
913 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
914
915 let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
916 widget.paint(&mut pctx2, &mut scene);
917 let after = pctx2.take_platform_views();
918 assert_eq!(after.len(), 2);
919 assert_eq!(after[0].slot_id, id_b, "key 2's slot survives the reorder");
922 assert_eq!(after[1].slot_id, id_a, "key 1's slot survives the reorder");
923 }
924
925 #[test]
928 fn shield_reports_its_absolute_painted_rect_and_paints_the_child_unchanged() {
929 let mut counter = 0u64;
933 let row: FlexView<()> = Row(vec![
934 any(SizedBox::<()>(Some(50.0), Some(30.0))),
935 any(shield(crate::test_support::leaf(80.0, 60.0))),
936 ]);
937 let padded: PaddingView<()> = Padding(EdgeInsets::all(10.0), row);
938
939 let mut widget = padded.build(&mut BuildCtx::new(&mut counter));
940 let mut lctx = LayoutCtx::new();
941 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
942
943 let mut scene = NullScene::default();
944 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
945 widget.paint(&mut pctx, &mut scene);
946
947 let expected = Rect::from_origin_size(Point::new(60.0, 10.0), Size::new(80.0, 60.0));
948 assert_eq!(pctx.take_input_shields(), vec![expected]);
949 assert_eq!(
952 scene.ops,
953 vec![SceneOp::Fill {
954 origin: Point::new(60.0, 10.0),
955 size: Size::new(80.0, 60.0),
956 }]
957 );
958 }
959
960 #[test]
961 fn two_shields_in_one_pass_both_survive() {
962 let mut counter = 0u64;
965 let row: FlexView<()> = Row(vec![
966 any(shield(crate::test_support::leaf(20.0, 20.0))),
967 any(shield(crate::test_support::leaf(30.0, 30.0))),
968 ]);
969 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
970 let mut lctx = LayoutCtx::new();
971 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
972
973 let mut scene = NullScene::default();
974 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
975 widget.paint(&mut pctx, &mut scene);
976
977 let shields = pctx.take_input_shields();
978 assert_eq!(shields.len(), 2, "both shields must reach the render root");
979 assert_eq!(shields[0].size(), Size::new(20.0, 20.0));
980 assert_eq!(shields[1].size(), Size::new(30.0, 30.0));
981 }
982
983 #[test]
984 fn shield_local_still_publishes_its_absolute_rect_escape_hatch() {
985 let view = platform_view("dev.frust.MapFactory")
989 .size(100.0, 80.0)
990 .interactive()
991 .shield_local(Rect::new(0.0, 0.0, 20.0, 10.0));
992 let mut w = build(&view);
993 let mut lctx = LayoutCtx::new();
994 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
995
996 let mut scene = NullScene::default();
997 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
998 w.paint(&mut pctx, &mut scene);
999
1000 let frames = pctx.take_platform_views();
1001 assert!(frames[0].interactive);
1002 assert_eq!(frames[0].shields, vec![Rect::new(20.0, 30.0, 40.0, 40.0)]);
1003 assert!(
1004 pctx.take_input_shields().is_empty(),
1005 "a manual rect is NOT reported through the auto-collection channel"
1006 );
1007 }
1008
1009 static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1014
1015 fn drain_retired() -> Vec<u64> {
1018 frust_core::widget::take_retired_slots()
1019 }
1020
1021 #[test]
1022 fn removing_a_slot_from_a_container_reports_its_id_for_prompt_retire() {
1023 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1024 let _ = drain_retired();
1025
1026 let mut counter = 0u64;
1029 let prev: FlexView<()> = Row(vec![any(platform_view("dev.frust.MapFactory"))]);
1030 let mut widget = prev.build(&mut BuildCtx::new(&mut counter));
1031
1032 let mut scene = NullScene::default();
1033 let mut lctx = LayoutCtx::new();
1034 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
1035 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
1036 widget.paint(&mut pctx, &mut scene);
1037 let slot_id = pctx.take_platform_views()[0].slot_id;
1038 assert!(
1039 drain_retired().is_empty(),
1040 "a live slot reports no retire while it keeps painting"
1041 );
1042
1043 let next: FlexView<()> = Row(vec![any(SizedBox::<()>(Some(10.0), Some(10.0)))]);
1044 next.rebuild(&prev, &mut widget, &mut BuildCtx::new(&mut counter));
1045
1046 assert_eq!(
1047 drain_retired(),
1048 vec![slot_id],
1049 "a torn-down slot reports its id exactly once"
1050 );
1051 }
1052
1053 #[test]
1054 fn a_culled_slot_reports_no_retire() {
1055 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1060 let _ = drain_retired();
1061
1062 let mut counter = 0u64;
1063 let row: FlexView<()> = Row(vec![any(
1064 platform_view("dev.frust.MapFactory").size(50.0, 50.0)
1065 )]);
1066 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
1067 let mut lctx = LayoutCtx::new();
1068 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1069
1070 let mut scene = NullScene::default();
1071 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1072 pctx.constrain_visible_rect(Rect::from_origin_size(
1073 Point::new(5000.0, 5000.0),
1074 Size::new(10.0, 10.0),
1075 ));
1076 widget.paint(&mut pctx, &mut scene);
1077 assert!(pctx.take_platform_views().is_empty(), "the slot was culled");
1078
1079 row.rebuild(&row, &mut widget, &mut BuildCtx::new(&mut counter));
1081 assert!(
1082 drain_retired().is_empty(),
1083 "a culled-but-live slot must never be retired"
1084 );
1085 }
1086}