1use bevy::math::Rect;
49use bevy::platform::collections::HashMap;
50use bevy::prelude::*;
51use bevy::ui::{CalculatedClip, ComputedNode, OverrideClip, UiGlobalTransform};
52
53use super::{LayerMembership, PromotedLayer};
54
55#[derive(Resource, Debug, Default)]
58pub struct LayerClips {
59 pub interior: HashMap<Entity, Option<Rect>>,
66 pub quads: HashMap<Entity, Option<Rect>>,
71}
72
73#[allow(clippy::type_complexity)]
79pub fn sync_layer_clips(
80 roots: Query<(Entity, Option<&CalculatedClip>), With<PromotedLayer>>,
81 root_markers: Query<(), With<PromotedLayer>>,
82 children: Query<&Children>,
83 nodes: Query<(&Node, &ComputedNode, &UiGlobalTransform, Has<OverrideClip>)>,
84 membership: Res<LayerMembership>,
85 mut clips: ResMut<LayerClips>,
86) {
87 let clips = &mut *clips;
88 clips.interior.clear();
89 clips.quads.clear();
90 for (root, calculated_clip) in &roots {
91 if membership.node_to_layer.get(&root) != Some(&root) {
95 continue;
96 }
97 cascade(root, root, None, &children, &root_markers, &nodes, clips);
100 if membership.enclosing.get(&root) == Some(&None) {
104 clips.quads.insert(root, calculated_clip.map(|c| c.clip));
105 }
106 }
107}
108
109#[allow(clippy::type_complexity)]
117fn cascade(
118 node: Entity,
119 dfs_root: Entity,
120 mut inherited: Option<Rect>,
121 children: &Query<&Children>,
122 root_markers: &Query<(), With<PromotedLayer>>,
123 nodes: &Query<(&Node, &ComputedNode, &UiGlobalTransform, Has<OverrideClip>)>,
124 clips: &mut LayerClips,
125) {
126 let Ok((ui_node, computed, transform, has_override)) = nodes.get(node) else {
127 return; };
129 if has_override {
130 inherited = None;
131 }
132 if ui_node.display == bevy::ui::Display::None {
133 inherited = Some(Rect::default());
134 }
135 if node != dfs_root && root_markers.contains(node) {
136 clips.quads.insert(node, inherited);
137 return; }
139 clips.interior.insert(node, inherited);
140 let child_clip = children_clip(ui_node, computed, transform, inherited);
141 if let Ok(kids) = children.get(node) {
142 for &kid in kids {
143 cascade(
144 kid,
145 dfs_root,
146 child_clip,
147 children,
148 root_markers,
149 nodes,
150 clips,
151 );
152 }
153 }
154}
155
156fn children_clip(
161 node: &Node,
162 computed: &ComputedNode,
163 transform: &UiGlobalTransform,
164 inherited: Option<Rect>,
165) -> Option<Rect> {
166 if node.overflow.is_visible() {
167 return inherited;
168 }
169 let mut clip_rect = computed.resolve_clip_rect(node.overflow, node.overflow_clip_margin);
170 clip_rect.min += transform.translation;
171 clip_rect.max += transform.translation;
172 Some(inherited.map_or(clip_rect, |c| c.intersect(clip_rect)))
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::layer::{
179 LayerContentDirt, LayerGroupAlpha, LayerRepaintState, LayersRegistry, PromotionReasons,
180 resolve_layer_repaints, sync_layer_geometry,
181 };
182 use crate::protocol::NodeId;
183 use bevy::math::Affine2;
184 use bevy::ui::Overflow;
185
186 fn clip_world() -> (World, Schedule) {
190 let mut world = World::new();
191 world.init_resource::<LayerMembership>();
192 world.init_resource::<LayersRegistry>();
193 world.init_resource::<LayerRepaintState>();
194 world.init_resource::<LayerContentDirt>();
195 world.init_resource::<LayerClips>();
196 let mut schedule = Schedule::default();
197 schedule.add_systems(
198 (
199 sync_layer_geometry,
200 sync_layer_clips,
201 resolve_layer_repaints,
202 )
203 .chain(),
204 );
205 (world, schedule)
206 }
207
208 fn spawn_node(world: &mut World, size: Vec2, center: Vec2, overflow: Overflow) -> Entity {
211 world
212 .spawn((
213 Node {
214 overflow,
215 ..Default::default()
216 },
217 ComputedNode {
218 size,
219 ..Default::default()
220 },
221 UiGlobalTransform::from(Affine2::from_translation(center)),
222 ))
223 .id()
224 }
225
226 fn spawn_root(
229 world: &mut World,
230 id: NodeId,
231 size: Vec2,
232 center: Vec2,
233 overflow: Overflow,
234 ) -> Entity {
235 let e = spawn_node(world, size, center, overflow);
236 world.entity_mut(e).insert((
237 crate::bridge::RNode(id),
238 LayerGroupAlpha(1.0),
239 PromotedLayer {
240 reasons: PromotionReasons(PromotionReasons::FILTER),
241 },
242 ));
243 e
244 }
245
246 fn interior(world: &World, e: Entity) -> Option<Rect> {
247 *world
248 .resource::<LayerClips>()
249 .interior
250 .get(&e)
251 .unwrap_or_else(|| panic!("no interior clip entry for {e:?}"))
252 }
253
254 fn quad(world: &World, e: Entity) -> Option<Rect> {
255 *world
256 .resource::<LayerClips>()
257 .quads
258 .get(&e)
259 .unwrap_or_else(|| panic!("no quad clip entry for {e:?}"))
260 }
261
262 #[test]
267 fn ancestor_clip_stripped_and_moved_to_quad() {
268 let (mut world, mut schedule) = clip_world();
269 let clip = Rect::new(0.0, 0.0, 30.0, 30.0);
270 let root = spawn_root(
271 &mut world,
272 1,
273 Vec2::new(100.0, 60.0),
274 Vec2::new(50.0, 30.0),
275 Overflow::visible(),
276 );
277 let child = spawn_node(
278 &mut world,
279 Vec2::new(40.0, 20.0),
280 Vec2::new(50.0, 30.0),
281 Overflow::visible(),
282 );
283 world.entity_mut(child).insert(ChildOf(root));
284 world.entity_mut(root).insert(CalculatedClip { clip });
285 world.entity_mut(child).insert(CalculatedClip { clip });
286 let unclipped = spawn_root(
287 &mut world,
288 2,
289 Vec2::new(50.0, 50.0),
290 Vec2::new(300.0, 300.0),
291 Overflow::visible(),
292 );
293 schedule.run(&mut world);
294
295 assert_eq!(interior(&world, root), None);
296 assert_eq!(interior(&world, child), None);
297 assert_eq!(quad(&world, root), Some(clip));
298 assert_eq!(quad(&world, unclipped), None);
299 }
300
301 #[test]
305 fn root_overflow_clips_children_only() {
306 let (mut world, mut schedule) = clip_world();
307 let root = spawn_root(
308 &mut world,
309 1,
310 Vec2::splat(200.0),
311 Vec2::splat(100.0),
312 Overflow::clip(),
313 );
314 let container = spawn_node(
316 &mut world,
317 Vec2::splat(100.0),
318 Vec2::new(160.0, 100.0),
319 Overflow::clip(),
320 );
321 world.entity_mut(container).insert(ChildOf(root));
322 let grandchild = spawn_node(
323 &mut world,
324 Vec2::splat(40.0),
325 Vec2::new(160.0, 100.0),
326 Overflow::visible(),
327 );
328 world.entity_mut(grandchild).insert(ChildOf(container));
329 schedule.run(&mut world);
330
331 let root_rect = Rect::new(0.0, 0.0, 200.0, 200.0);
332 let container_rect = Rect::new(110.0, 50.0, 210.0, 150.0);
333 assert_eq!(interior(&world, root), None, "root paint unclipped");
334 assert_eq!(interior(&world, container), Some(root_rect));
335 assert_eq!(
336 interior(&world, grandchild),
337 Some(root_rect.intersect(container_rect)),
338 "interior clippers intersect"
339 );
340 }
341
342 #[test]
346 fn nested_roots_get_quad_clips_and_restart_interior() {
347 let (mut world, mut schedule) = clip_world();
348 let outer = spawn_root(
349 &mut world,
350 1,
351 Vec2::splat(300.0),
352 Vec2::splat(150.0),
353 Overflow::visible(),
354 );
355 let clipper = spawn_node(
356 &mut world,
357 Vec2::splat(100.0),
358 Vec2::splat(100.0),
359 Overflow::clip(),
360 );
361 world.entity_mut(clipper).insert(ChildOf(outer));
362 let inner = spawn_root(
363 &mut world,
364 2,
365 Vec2::splat(80.0),
366 Vec2::splat(100.0),
367 Overflow::visible(),
368 );
369 world.entity_mut(inner).insert(ChildOf(clipper));
370 let inner_child = spawn_node(
371 &mut world,
372 Vec2::splat(40.0),
373 Vec2::splat(100.0),
374 Overflow::visible(),
375 );
376 world.entity_mut(inner_child).insert(ChildOf(inner));
377 let clipper2 = spawn_node(
381 &mut world,
382 Vec2::splat(60.0),
383 Vec2::new(140.0, 100.0),
384 Overflow::clip(),
385 );
386 world.entity_mut(clipper2).insert(ChildOf(inner));
387 let innermost = spawn_root(
388 &mut world,
389 3,
390 Vec2::splat(40.0),
391 Vec2::new(140.0, 100.0),
392 Overflow::visible(),
393 );
394 world.entity_mut(innermost).insert(ChildOf(clipper2));
395 schedule.run(&mut world);
396
397 let clipper_rect = Rect::new(50.0, 50.0, 150.0, 150.0);
398 let clipper2_rect = Rect::new(110.0, 70.0, 170.0, 130.0);
399 assert_eq!(quad(&world, inner), Some(clipper_rect));
400 assert_eq!(interior(&world, inner), None, "interior restarts");
401 assert_eq!(interior(&world, inner_child), None);
402 assert_eq!(
403 quad(&world, innermost),
404 Some(clipper2_rect),
405 "inner cascade only — outer clipper must not leak through the restart"
406 );
407 assert_eq!(interior(&world, clipper), None, "outer member");
408 }
409
410 #[test]
413 fn override_clip_and_display_none() {
414 let (mut world, mut schedule) = clip_world();
415 let root = spawn_root(
416 &mut world,
417 1,
418 Vec2::splat(200.0),
419 Vec2::splat(100.0),
420 Overflow::clip(),
421 );
422 let root_rect = Rect::new(0.0, 0.0, 200.0, 200.0);
423
424 let escapee = spawn_node(
425 &mut world,
426 Vec2::splat(40.0),
427 Vec2::splat(100.0),
428 Overflow::visible(),
429 );
430 world
431 .entity_mut(escapee)
432 .insert((ChildOf(root), OverrideClip));
433 let escapee_child = spawn_node(
434 &mut world,
435 Vec2::splat(20.0),
436 Vec2::splat(100.0),
437 Overflow::visible(),
438 );
439 world.entity_mut(escapee_child).insert(ChildOf(escapee));
440
441 let hidden = spawn_node(
442 &mut world,
443 Vec2::splat(40.0),
444 Vec2::splat(100.0),
445 Overflow::visible(),
446 );
447 world.entity_mut(hidden).insert(ChildOf(root));
448 world.entity_mut(hidden).get_mut::<Node>().unwrap().display = bevy::ui::Display::None;
449 let hidden_child = spawn_node(
450 &mut world,
451 Vec2::splat(20.0),
452 Vec2::splat(100.0),
453 Overflow::visible(),
454 );
455 world.entity_mut(hidden_child).insert(ChildOf(hidden));
456
457 let plain = spawn_node(
458 &mut world,
459 Vec2::splat(40.0),
460 Vec2::splat(100.0),
461 Overflow::visible(),
462 );
463 world.entity_mut(plain).insert(ChildOf(root));
464 schedule.run(&mut world);
465
466 assert_eq!(interior(&world, escapee), None);
467 assert_eq!(interior(&world, escapee_child), None);
468 assert_eq!(interior(&world, hidden), Some(Rect::default()));
469 assert_eq!(interior(&world, hidden_child), Some(Rect::default()));
470 assert_eq!(interior(&world, plain), Some(root_rect));
471 }
472
473 #[test]
479 fn scroll_holds_cache_and_updates_quad_clip() {
480 let (mut world, mut schedule) = clip_world();
481 let clip = Rect::new(0.0, 0.0, 200.0, 100.0);
482 let root = spawn_root(
484 &mut world,
485 1,
486 Vec2::new(100.0, 60.0),
487 Vec2::new(50.0, 130.0),
488 Overflow::visible(),
489 );
490 let child = spawn_node(
491 &mut world,
492 Vec2::new(40.0, 20.0),
493 Vec2::new(50.0, 130.0),
494 Overflow::visible(),
495 );
496 world.entity_mut(child).insert(ChildOf(root));
497 world.entity_mut(root).insert(CalculatedClip { clip });
498 world.entity_mut(child).insert(CalculatedClip { clip });
499 schedule.run(&mut world);
500 schedule.run(&mut world); assert!(world.resource::<LayerRepaintState>().dirty.is_empty());
502 assert_eq!(quad(&world, root), Some(clip));
503
504 let delta = Vec2::new(0.0, -100.0);
506 for e in [root, child] {
507 let center = world.get::<UiGlobalTransform>(e).unwrap().translation + delta;
508 *world.get_mut::<UiGlobalTransform>(e).unwrap() =
509 UiGlobalTransform::from(Affine2::from_translation(center));
510 }
511 schedule.run(&mut world);
512 assert!(
513 world.resource::<LayerRepaintState>().dirty.is_empty(),
514 "scrolling must not re-capture"
515 );
516 assert_eq!(quad(&world, root), Some(clip));
517
518 let grown = Rect::new(0.0, 0.0, 200.0, 150.0);
520 world.get_mut::<CalculatedClip>(root).unwrap().clip = grown;
521 schedule.run(&mut world);
522 assert!(
523 world.resource::<LayerRepaintState>().dirty.is_empty(),
524 "a clip change must not re-capture"
525 );
526 assert_eq!(quad(&world, root), Some(grown));
527 }
528}