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, SliverConstraints, SliverGeometry, SliverWidget,
96 View, Widget, any,
97};
98use kurbo::{Point, Rect, Size};
99#[cfg(debug_assertions)]
100use peniko::Color;
101
102#[derive(Clone, Copy, Debug, PartialEq)]
110enum SlotSize {
111 Expand,
113 Explicit(f64, f64),
115}
116
117#[cfg(debug_assertions)]
122const DEBUG_FILL_COLOR: Color = Color::from_rgba8(0xE0, 0x00, 0xE0, 0x40);
123
124pub struct PlatformViewView {
126 view_type: String,
127 params_json: String,
128 slot_size: SlotSize,
129 #[cfg(debug_assertions)]
130 debug_fill: bool,
131 semantics_label: Option<String>,
132 interactive: bool,
133 shields_local: Vec<Rect>,
134}
135
136pub fn platform_view(view_type: impl Into<String>) -> PlatformViewView {
141 PlatformViewView {
142 view_type: view_type.into(),
143 params_json: String::new(),
144 slot_size: SlotSize::Expand,
145 #[cfg(debug_assertions)]
146 debug_fill: false,
147 semantics_label: None,
148 interactive: false,
149 shields_local: Vec::new(),
150 }
151}
152
153impl PlatformViewView {
154 pub fn params_json(mut self, params_json: impl Into<String>) -> Self {
160 self.params_json = params_json.into();
161 self
162 }
163
164 pub fn size(mut self, width: f64, height: f64) -> Self {
169 self.slot_size = SlotSize::Explicit(width, height);
170 self
171 }
172
173 pub fn expand(mut self) -> Self {
177 self.slot_size = SlotSize::Expand;
178 self
179 }
180
181 #[cfg(debug_assertions)]
187 pub fn debug_fill(mut self) -> Self {
188 self.debug_fill = true;
189 self
190 }
191
192 pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
198 self.semantics_label = Some(label.into());
199 self
200 }
201
202 pub fn interactive(mut self) -> Self {
208 self.interactive = true;
209 self
210 }
211
212 pub fn shield_local(mut self, rect: Rect) -> Self {
226 self.shields_local.push(rect);
227 self
228 }
229}
230
231pub struct PlatformViewWidget {
233 slot_id: u64,
236 view_type: String,
237 params_json: String,
238 params_generation: u64,
242 slot_size: SlotSize,
243 #[cfg(debug_assertions)]
244 debug_fill: bool,
245 semantics_label: Option<String>,
246 interactive: bool,
247 shields_local: Vec<Rect>,
248}
249
250impl<State: 'static> View<State> for PlatformViewView {
251 type Element = PlatformViewWidget;
252
253 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PlatformViewWidget {
254 PlatformViewWidget {
255 slot_id: next_slot_id(),
256 view_type: self.view_type.clone(),
257 params_json: self.params_json.clone(),
258 params_generation: 0,
259 slot_size: self.slot_size,
260 #[cfg(debug_assertions)]
261 debug_fill: self.debug_fill,
262 semantics_label: self.semantics_label.clone(),
263 interactive: self.interactive,
264 shields_local: self.shields_local.clone(),
265 }
266 }
267
268 fn rebuild(
269 &self,
270 prev: &Self,
271 element: &mut PlatformViewWidget,
272 _ctx: &mut BuildCtx<'_>,
273 ) -> ChangeFlags {
274 let mut flags = ChangeFlags::NONE;
275
276 if prev.view_type != self.view_type {
277 element.slot_id = next_slot_id();
281 element.view_type = self.view_type.clone();
282 element.params_json = self.params_json.clone();
283 element.params_generation = 0;
284 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
285 } else if prev.params_json != self.params_json {
286 element.params_json = self.params_json.clone();
287 element.params_generation += 1;
288 flags |= ChangeFlags::PAINT;
289 }
290
291 if prev.slot_size != self.slot_size {
292 element.slot_size = self.slot_size;
293 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
294 }
295
296 #[cfg(debug_assertions)]
297 if prev.debug_fill != self.debug_fill {
298 element.debug_fill = self.debug_fill;
299 flags |= ChangeFlags::PAINT;
300 }
301
302 if prev.semantics_label != self.semantics_label {
303 element.semantics_label = self.semantics_label.clone();
307 }
308
309 if prev.interactive != self.interactive || prev.shields_local != self.shields_local {
310 element.interactive = self.interactive;
311 element.shields_local = self.shields_local.clone();
312 flags |= ChangeFlags::PAINT;
315 }
316
317 flags
318 }
319
320 fn teardown(&self, element: &mut PlatformViewWidget, _ctx: &mut BuildCtx<'_>) {
321 report_retired_slot(element.slot_id);
326 }
327}
328
329impl Widget for PlatformViewWidget {
330 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
331 match self.slot_size {
334 SlotSize::Explicit(width, height) => bc.constrain(Size::new(width, height)),
335 SlotSize::Expand => bc.max(),
336 }
337 }
338
339 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
340 let rect = Rect::from_origin_size(ctx.origin(), ctx.size());
341
342 let (clip, visible) = match ctx.visible_rect() {
349 Some(visible_rect) => (
350 Some(visible_rect.intersect(rect)),
351 visible_rect.overlaps(rect),
352 ),
353 None => (None, true),
354 };
355
356 ctx.publish_platform_view(PlatformViewFrame {
361 slot_id: self.slot_id,
362 view_type: self.view_type.clone(),
363 params_json: self.params_json.clone(),
364 params_generation: self.params_generation,
365 rect,
366 clip,
367 visible,
368 interactive: self.interactive,
369 shields: self
370 .shields_local
371 .iter()
372 .map(|r| *r + ctx.origin().to_vec2())
373 .collect(),
374 command_index: scene.command_index(),
375 });
376
377 if ctx.is_translucent() {
389 scene.clear_rect(ctx.origin(), ctx.size());
390 }
391
392 #[cfg(debug_assertions)]
397 if self.debug_fill {
398 scene.fill_rect(ctx.origin(), ctx.size(), DEBUG_FILL_COLOR);
399 }
400 }
401
402 fn semantics(&self, ctx: &mut SemanticsCtx) {
403 if let Some(label) = &self.semantics_label {
404 ctx.push_node(Role::GenericContainer, |node| {
405 node.set_label(label.as_str());
406 });
407 }
408 }
409}
410
411pub struct ShieldView<State: 'static> {
413 child: AnyView<State>,
414}
415
416pub fn shield<State: 'static, V: View<State>>(child: V) -> ShieldView<State> {
437 ShieldView { child: any(child) }
438}
439
440pub struct ShieldWidget {
442 child: ChildPod,
443}
444
445impl<State: 'static> View<State> for ShieldView<State> {
446 type Element = ShieldWidget;
447
448 fn build(&self, ctx: &mut BuildCtx<'_>) -> ShieldWidget {
449 ShieldWidget {
450 child: crate::authoring::build_child(&self.child, ctx),
451 }
452 }
453
454 fn rebuild(
455 &self,
456 prev: &Self,
457 element: &mut ShieldWidget,
458 ctx: &mut BuildCtx<'_>,
459 ) -> ChangeFlags {
460 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
461 }
462
463 fn teardown(&self, element: &mut ShieldWidget, ctx: &mut BuildCtx<'_>) {
464 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
465 }
466}
467
468impl Widget for ShieldWidget {
469 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
470 self.child.set_origin(Point::ZERO);
473 self.child.layout_child(ctx, bc)
474 }
475
476 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
477 self.child.paint_child(ctx, scene);
481 ctx.report_input_shield(Rect::from_origin_size(ctx.origin(), ctx.size()));
482 }
483
484 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
485 crate::authoring::route_event_single(&mut self.child, ctx, event)
486 }
487
488 fn semantics(&self, ctx: &mut SemanticsCtx) {
489 self.child.semantics_child(ctx);
492 }
493
494 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
499 if self.child.is_sliver() {
500 Some(self)
501 } else {
502 None
503 }
504 }
505
506 crate::authoring::visit_children!(child);
507}
508
509impl SliverWidget for ShieldWidget {
510 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
513 self.child.set_origin(Point::ZERO);
514 self.child.layout_sliver_child(ctx, sc).unwrap_or_else(|| {
520 debug_assert!(
521 false,
522 "ShieldWidget::layout_sliver called on a wrapper whose as_sliver answered None"
523 );
524 SliverGeometry::ZERO
525 })
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::{EdgeInsets, FlexView, Padding, PaddingView, SizedBox, row};
533 use frust_core::{BuildCtx, ChildPod};
534 use kurbo::Point;
535
536 fn build(view: &PlatformViewView) -> PlatformViewWidget {
537 let mut counter = 0u64;
538 <PlatformViewView as View<()>>::build(view, &mut BuildCtx::new(&mut counter))
539 }
540
541 fn rebuild(
542 prev: &PlatformViewView,
543 next: &PlatformViewView,
544 element: &mut PlatformViewWidget,
545 ) -> ChangeFlags {
546 let mut counter = 0u64;
547 <PlatformViewView as View<()>>::rebuild(
548 next,
549 prev,
550 element,
551 &mut BuildCtx::new(&mut counter),
552 )
553 }
554
555 #[derive(Default)]
559 struct NullScene {
560 ops: Vec<SceneOp>,
561 }
562
563 #[derive(Debug, PartialEq)]
564 enum SceneOp {
565 Clear { origin: Point, size: Size },
566 Fill { origin: Point, size: Size },
567 }
568
569 impl PaintScene for NullScene {
570 fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
571 self.ops.push(SceneOp::Fill { origin, size });
572 }
573 fn draw_text(&mut self, _origin: Point, _text: &str) {}
574 fn clear_rect(&mut self, origin: Point, size: Size) {
575 self.ops.push(SceneOp::Clear { origin, size });
576 }
577 }
578
579 #[test]
582 fn expand_fills_the_incoming_constraints_max() {
583 let view = platform_view("dev.frust.MapFactory");
584 let mut w = build(&view);
585 let mut lctx = LayoutCtx::new();
586 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
587 assert_eq!(size, Size::new(300.0, 200.0));
588 }
589
590 #[test]
591 fn explicit_size_is_clamped_into_constraints() {
592 let view = platform_view("dev.frust.MapFactory").size(1000.0, 1000.0);
593 let mut w = build(&view);
594 let mut lctx = LayoutCtx::new();
595 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
596 assert_eq!(size, Size::new(300.0, 200.0));
597 }
598
599 #[test]
602 fn publishes_absolute_rect_under_padding_flex_nesting_and_scroll_offset() {
603 let mut counter = 0u64;
604 let insets = EdgeInsets::all(10.0);
605 let row: FlexView<()> = row()
606 .child(SizedBox::<()>(Some(50.0), Some(30.0)))
607 .child(platform_view("dev.frust.MapFactory").size(80.0, 60.0));
608 let padded: PaddingView<()> = Padding(insets, row);
609
610 let widget = padded.build(&mut BuildCtx::new(&mut counter));
611 let mut lctx = LayoutCtx::new();
612
613 let mut pod = ChildPod::new(Box::new(widget));
618 pod.set_origin(Point::new(-15.0, -25.0));
619 pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
620
621 let mut scene = NullScene::default();
622 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(2000.0, 2000.0));
623 pod.paint_child(&mut pctx, &mut scene);
624
625 let frames = pctx.take_platform_views();
626 assert_eq!(frames.len(), 1);
627 let expected_origin = Point::new(-15.0 + 10.0 + 50.0, -25.0 + 10.0);
631 assert_eq!(
632 frames[0].rect,
633 Rect::from_origin_size(expected_origin, Size::new(80.0, 60.0))
634 );
635 }
636
637 #[test]
640 fn clip_is_intersected_with_visible_rect_when_straddling_edge() {
641 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
642 let mut w = build(&view);
643 let mut lctx = LayoutCtx::new();
644 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
645
646 let mut scene = NullScene::default();
647 let mut pctx = PaintCtx::new(Point::new(50.0, 0.0), Size::new(100.0, 100.0));
648 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
649 w.paint(&mut pctx, &mut scene);
650
651 let frames = pctx.take_platform_views();
652 assert_eq!(frames.len(), 1);
653 let frame: &PlatformViewFrame = &frames[0];
654 assert_eq!(
655 frame.rect,
656 Rect::from_origin_size(Point::new(50.0, 0.0), Size::new(100.0, 100.0))
657 );
658 assert_eq!(frame.clip, Some(Rect::new(50.0, 0.0, 100.0, 100.0)));
659 assert!(frame.visible, "an edge-straddling slot is still visible");
660 }
661
662 #[test]
663 fn fully_outside_visible_rect_publishes_not_visible() {
664 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
665 let mut w = build(&view);
666 let mut lctx = LayoutCtx::new();
667 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
668
669 let mut scene = NullScene::default();
670 let mut pctx = PaintCtx::new(Point::new(200.0, 0.0), Size::new(100.0, 100.0));
673 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
674 w.paint(&mut pctx, &mut scene);
675
676 let frames = pctx.take_platform_views();
677 assert_eq!(frames.len(), 1);
678 assert!(!frames[0].visible);
679 }
680
681 #[test]
682 fn no_visible_rect_constraint_means_fully_visible_with_no_clip() {
683 let view = platform_view("dev.frust.MapFactory").size(100.0, 100.0);
684 let mut w = build(&view);
685 let mut lctx = LayoutCtx::new();
686 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
687
688 let mut scene = NullScene::default();
689 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
690 w.paint(&mut pctx, &mut scene);
691
692 let frames = pctx.take_platform_views();
693 assert_eq!(frames[0].clip, None);
694 assert!(frames[0].visible);
695 }
696
697 #[test]
698 fn parent_culling_of_the_whole_subtree_yields_no_frame_at_all() {
699 let mut counter = 0u64;
700 let row: FlexView<()> = row().child(platform_view("dev.frust.MapFactory").size(50.0, 50.0));
701 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
702 let mut lctx = LayoutCtx::new();
703 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
704
705 let mut scene = NullScene::default();
706 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
707 pctx.constrain_visible_rect(Rect::from_origin_size(
710 Point::new(5000.0, 5000.0),
711 Size::new(10.0, 10.0),
712 ));
713 widget.paint(&mut pctx, &mut scene);
714
715 let frames = pctx.take_platform_views();
716 assert!(
717 frames.is_empty(),
718 "a culled subtree must publish no frame at all, not a hidden one"
719 );
720 }
721
722 #[test]
725 fn translucent_surface_punches_the_slot_rect() {
726 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
729 let mut w = build(&view);
730 let mut lctx = LayoutCtx::new();
731 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
732
733 let mut scene = NullScene::default();
734 let mut pctx =
735 PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0)).with_translucent(true);
736 w.paint(&mut pctx, &mut scene);
737
738 assert_eq!(
739 scene.ops,
740 vec![SceneOp::Clear {
741 origin: Point::new(20.0, 30.0),
742 size: Size::new(100.0, 80.0),
743 }],
744 "a translucent slot must clear its own absolute rect"
745 );
746 }
747
748 #[test]
749 fn opaque_surface_does_not_punch_mode_a_unchanged() {
750 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
753 let mut w = build(&view);
754 let mut lctx = LayoutCtx::new();
755 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
756
757 let mut scene = NullScene::default();
758 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
760 w.paint(&mut pctx, &mut scene);
761
762 assert!(
763 scene.ops.is_empty(),
764 "an opaque-surface slot must paint nothing (no punch), Mode A unchanged"
765 );
766 assert_eq!(pctx.take_platform_views().len(), 1);
768 }
769
770 fn clear_rects(scene: &frust_scene::Scene) -> usize {
777 scene
778 .commands()
779 .iter()
780 .filter(|c| matches!(c, frust_scene::Command::ClearRect { .. }))
781 .count()
782 }
783
784 #[test]
785 fn a_resolved_translucency_downgrade_stops_the_punch_in_the_real_display_list() {
786 fn logic(_: &mut ()) -> PlatformViewView {
793 platform_view("dev.frust.MapFactory").size(100.0, 80.0)
794 }
795
796 let mut root: frust_core::RenderRoot<(), PlatformViewView> = frust_core::RenderRoot::new();
797 let mut state = ();
798 let mut scene = frust_scene::Scene::new();
799
800 root.set_surface_translucent(true);
802 root.rebuild(&mut logic, &mut state);
803 root.layout(Size::new(200.0, 200.0));
804 {
805 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
806 root.paint(&mut builder, frust_core::FrameTime::ZERO);
807 }
808 assert_eq!(
809 clear_rects(&scene),
810 1,
811 "a translucent-resolved surface must punch exactly one slot rect"
812 );
813
814 root.set_surface_translucent(false);
816 assert!(
817 root.has_pending_change_flags(),
818 "a translucency downgrade must mark the tree dirty so the next \
819 frame actually repaints without the punch"
820 );
821 root.rebuild(&mut logic, &mut state);
822 root.layout(Size::new(200.0, 200.0));
823 scene.reset();
824 {
825 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
826 root.paint(&mut builder, frust_core::FrameTime::ZERO);
827 }
828 assert_eq!(
829 clear_rects(&scene),
830 0,
831 "an opaque-resolved surface must encode no punch at all (Mode A)"
832 );
833 }
834
835 #[test]
838 fn command_index_counts_the_commands_before_the_slot_and_excludes_its_punch() {
839 let view = platform_view("dev.frust.MapFactory").size(100.0, 80.0);
840 let mut w = build(&view);
841 let mut lctx = LayoutCtx::new();
842 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
843
844 let mut scene = frust_scene::Scene::new();
845 let frame = {
846 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
847 PaintScene::fill_rect(
850 &mut builder,
851 Point::ZERO,
852 Size::new(200.0, 200.0),
853 peniko::Color::BLACK,
854 );
855 PaintScene::push_clip(&mut builder, Point::ZERO, Size::new(200.0, 200.0));
856 PaintScene::pop_clip(&mut builder);
857
858 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0))
860 .with_translucent(true);
861 w.paint(&mut pctx, &mut builder);
862 let mut frames = pctx.take_platform_views();
863 assert_eq!(frames.len(), 1);
864 frames.remove(0)
865 };
866
867 assert_eq!(
868 frame.command_index, 3,
869 "the index is the count of commands recorded before the slot published"
870 );
871 assert_eq!(scene.commands().len(), 4);
875 assert!(matches!(
876 scene.commands()[frame.command_index],
877 frust_scene::Command::ClearRect { .. }
878 ));
879 }
880
881 #[test]
882 fn command_index_is_zero_under_a_recorder_scene() {
883 let view = platform_view("dev.frust.MapFactory").size(10.0, 10.0);
884 let mut w = build(&view);
885 let mut lctx = LayoutCtx::new();
886 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
887
888 let mut scene = NullScene::default();
889 scene.fill_rect(Point::ZERO, Size::new(10.0, 10.0), peniko::Color::BLACK);
890 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
891 w.paint(&mut pctx, &mut scene);
892 assert_eq!(pctx.take_platform_views()[0].command_index, 0);
893 }
894
895 #[cfg(debug_assertions)]
896 #[test]
897 fn debug_fill_paints_over_the_punch_on_a_translucent_surface() {
898 let view = platform_view("dev.frust.MapFactory")
901 .size(100.0, 80.0)
902 .debug_fill();
903 let mut w = build(&view);
904 let mut lctx = LayoutCtx::new();
905 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
906
907 let mut scene = NullScene::default();
908 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 80.0)).with_translucent(true);
909 w.paint(&mut pctx, &mut scene);
910
911 assert_eq!(
912 scene.ops,
913 vec![
914 SceneOp::Clear {
915 origin: Point::ZERO,
916 size: Size::new(100.0, 80.0),
917 },
918 SceneOp::Fill {
919 origin: Point::ZERO,
920 size: Size::new(100.0, 80.0),
921 },
922 ],
923 "debug_fill must paint OVER the punch, not under it"
924 );
925 }
926
927 #[test]
930 fn params_json_change_bumps_generation_exactly_once_unchanged_does_not() {
931 let prev = platform_view("dev.frust.MapFactory").params_json("a");
932 let mut w = build(&prev);
933 assert_eq!(w.params_generation, 0);
934
935 let same = platform_view("dev.frust.MapFactory").params_json("a");
936 rebuild(&prev, &same, &mut w);
937 assert_eq!(w.params_generation, 0, "unchanged params must not bump");
938
939 let changed = platform_view("dev.frust.MapFactory").params_json("b");
940 rebuild(&same, &changed, &mut w);
941 assert_eq!(w.params_generation, 1);
942
943 let changed_again = platform_view("dev.frust.MapFactory").params_json("c");
944 rebuild(&changed, &changed_again, &mut w);
945 assert_eq!(w.params_generation, 2);
946 }
947
948 #[test]
949 fn view_type_change_allocates_a_fresh_slot_id_and_resets_generation() {
950 let prev = platform_view("dev.frust.MapFactory").params_json("a");
951 let mut w = build(&prev);
952 w.params_generation = 3; let old_slot_id = w.slot_id;
954
955 let swapped = platform_view("dev.frust.OtherFactory").params_json("a");
956 rebuild(&prev, &swapped, &mut w);
957
958 assert_ne!(
959 w.slot_id, old_slot_id,
960 "a view_type swap must allocate a new slot id"
961 );
962 assert_eq!(
963 w.params_generation, 0,
964 "a swap resets generation like a fresh build"
965 );
966 assert_eq!(w.view_type, "dev.frust.OtherFactory");
967 }
968
969 #[test]
972 fn slot_id_survives_a_keyed_reorder() {
973 let mut counter = 0u64;
979 let a: FlexView<()> = row()
980 .keyed(1u64, platform_view("dev.frust.A"))
981 .keyed(2u64, platform_view("dev.frust.B"));
982 let mut widget = a.build(&mut BuildCtx::new(&mut counter));
983 let mut lctx = LayoutCtx::new();
984 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
985
986 let mut scene = NullScene::default();
987 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
988 widget.paint(&mut pctx, &mut scene);
989 let before = pctx.take_platform_views();
990 assert_eq!(before.len(), 2);
991 let (id_a, id_b) = (before[0].slot_id, before[1].slot_id);
992 assert_ne!(id_a, id_b);
993
994 let b: FlexView<()> = row()
996 .keyed(2u64, platform_view("dev.frust.B"))
997 .keyed(1u64, platform_view("dev.frust.A"));
998 b.rebuild(&a, &mut widget, &mut BuildCtx::new(&mut counter));
999 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1000
1001 let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1002 widget.paint(&mut pctx2, &mut scene);
1003 let after = pctx2.take_platform_views();
1004 assert_eq!(after.len(), 2);
1005 assert_eq!(after[0].slot_id, id_b, "key 2's slot survives the reorder");
1008 assert_eq!(after[1].slot_id, id_a, "key 1's slot survives the reorder");
1009 }
1010
1011 #[test]
1014 fn shield_reports_its_absolute_painted_rect_and_paints_the_child_unchanged() {
1015 let mut counter = 0u64;
1019 let row: FlexView<()> = row()
1020 .child(SizedBox::<()>(Some(50.0), Some(30.0)))
1021 .child(shield(crate::test_support::leaf(80.0, 60.0)));
1022 let padded: PaddingView<()> = Padding(EdgeInsets::all(10.0), row);
1023
1024 let mut widget = padded.build(&mut BuildCtx::new(&mut counter));
1025 let mut lctx = LayoutCtx::new();
1026 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1027
1028 let mut scene = NullScene::default();
1029 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1030 widget.paint(&mut pctx, &mut scene);
1031
1032 let expected = Rect::from_origin_size(Point::new(60.0, 10.0), Size::new(80.0, 60.0));
1033 assert_eq!(pctx.take_input_shields(), vec![expected]);
1034 assert_eq!(
1037 scene.ops,
1038 vec![SceneOp::Fill {
1039 origin: Point::new(60.0, 10.0),
1040 size: Size::new(80.0, 60.0),
1041 }]
1042 );
1043 }
1044
1045 #[test]
1046 fn two_shields_in_one_pass_both_survive() {
1047 let mut counter = 0u64;
1050 let row: FlexView<()> = row()
1051 .child(shield(crate::test_support::leaf(20.0, 20.0)))
1052 .child(shield(crate::test_support::leaf(30.0, 30.0)));
1053 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
1054 let mut lctx = LayoutCtx::new();
1055 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1056
1057 let mut scene = NullScene::default();
1058 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1059 widget.paint(&mut pctx, &mut scene);
1060
1061 let shields = pctx.take_input_shields();
1062 assert_eq!(shields.len(), 2, "both shields must reach the render root");
1063 assert_eq!(shields[0].size(), Size::new(20.0, 20.0));
1064 assert_eq!(shields[1].size(), Size::new(30.0, 30.0));
1065 }
1066
1067 #[test]
1068 fn shield_local_still_publishes_its_absolute_rect_escape_hatch() {
1069 let view = platform_view("dev.frust.MapFactory")
1073 .size(100.0, 80.0)
1074 .interactive()
1075 .shield_local(Rect::new(0.0, 0.0, 20.0, 10.0));
1076 let mut w = build(&view);
1077 let mut lctx = LayoutCtx::new();
1078 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 80.0)));
1079
1080 let mut scene = NullScene::default();
1081 let mut pctx = PaintCtx::new(Point::new(20.0, 30.0), Size::new(100.0, 80.0));
1082 w.paint(&mut pctx, &mut scene);
1083
1084 let frames = pctx.take_platform_views();
1085 assert!(frames[0].interactive);
1086 assert_eq!(frames[0].shields, vec![Rect::new(20.0, 30.0, 40.0, 40.0)]);
1087 assert!(
1088 pctx.take_input_shields().is_empty(),
1089 "a manual rect is NOT reported through the auto-collection channel"
1090 );
1091 }
1092
1093 static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1098
1099 fn drain_retired() -> Vec<u64> {
1102 frust_core::widget::take_retired_slots()
1103 }
1104
1105 #[test]
1106 fn removing_a_slot_from_a_container_reports_its_id_for_prompt_retire() {
1107 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1108 let _ = drain_retired();
1109
1110 let mut counter = 0u64;
1113 let prev: FlexView<()> = row().child(platform_view("dev.frust.MapFactory"));
1114 let mut widget = prev.build(&mut BuildCtx::new(&mut counter));
1115
1116 let mut scene = NullScene::default();
1117 let mut lctx = LayoutCtx::new();
1118 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
1119 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
1120 widget.paint(&mut pctx, &mut scene);
1121 let slot_id = pctx.take_platform_views()[0].slot_id;
1122 assert!(
1123 drain_retired().is_empty(),
1124 "a live slot reports no retire while it keeps painting"
1125 );
1126
1127 let next: FlexView<()> = row().child(SizedBox::<()>(Some(10.0), Some(10.0)));
1128 next.rebuild(&prev, &mut widget, &mut BuildCtx::new(&mut counter));
1129
1130 assert_eq!(
1131 drain_retired(),
1132 vec![slot_id],
1133 "a torn-down slot reports its id exactly once"
1134 );
1135 }
1136
1137 #[test]
1138 fn a_culled_slot_reports_no_retire() {
1139 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1144 let _ = drain_retired();
1145
1146 let mut counter = 0u64;
1147 let row: FlexView<()> = row().child(platform_view("dev.frust.MapFactory").size(50.0, 50.0));
1148 let mut widget = row.build(&mut BuildCtx::new(&mut counter));
1149 let mut lctx = LayoutCtx::new();
1150 widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(1000.0, 1000.0)));
1151
1152 let mut scene = NullScene::default();
1153 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(1000.0, 1000.0));
1154 pctx.constrain_visible_rect(Rect::from_origin_size(
1155 Point::new(5000.0, 5000.0),
1156 Size::new(10.0, 10.0),
1157 ));
1158 widget.paint(&mut pctx, &mut scene);
1159 assert!(pctx.take_platform_views().is_empty(), "the slot was culled");
1160
1161 row.rebuild(&row, &mut widget, &mut BuildCtx::new(&mut counter));
1163 assert!(
1164 drain_retired().is_empty(),
1165 "a culled-but-live slot must never be retired"
1166 );
1167 }
1168}