use bevy::math::Rect;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::ui::{CalculatedClip, ComputedNode, OverrideClip, UiGlobalTransform};
use super::{LayerMembership, PromotedLayer};
#[derive(Resource, Debug, Default)]
pub struct LayerClips {
pub interior: HashMap<Entity, Option<Rect>>,
pub quads: HashMap<Entity, Option<Rect>>,
}
#[allow(clippy::type_complexity)]
pub fn sync_layer_clips(
roots: Query<(Entity, Option<&CalculatedClip>), With<PromotedLayer>>,
root_markers: Query<(), With<PromotedLayer>>,
children: Query<&Children>,
nodes: Query<(&Node, &ComputedNode, &UiGlobalTransform, Has<OverrideClip>)>,
membership: Res<LayerMembership>,
mut clips: ResMut<LayerClips>,
) {
let clips = &mut *clips;
clips.interior.clear();
clips.quads.clear();
for (root, calculated_clip) in &roots {
if membership.node_to_layer.get(&root) != Some(&root) {
continue;
}
cascade(root, root, None, &children, &root_markers, &nodes, clips);
if membership.enclosing.get(&root) == Some(&None) {
clips.quads.insert(root, calculated_clip.map(|c| c.clip));
}
}
}
#[allow(clippy::type_complexity)]
fn cascade(
node: Entity,
dfs_root: Entity,
mut inherited: Option<Rect>,
children: &Query<&Children>,
root_markers: &Query<(), With<PromotedLayer>>,
nodes: &Query<(&Node, &ComputedNode, &UiGlobalTransform, Has<OverrideClip>)>,
clips: &mut LayerClips,
) {
let Ok((ui_node, computed, transform, has_override)) = nodes.get(node) else {
return; };
if has_override {
inherited = None;
}
if ui_node.display == bevy::ui::Display::None {
inherited = Some(Rect::default());
}
if node != dfs_root && root_markers.contains(node) {
clips.quads.insert(node, inherited);
return; }
clips.interior.insert(node, inherited);
let child_clip = children_clip(ui_node, computed, transform, inherited);
if let Ok(kids) = children.get(node) {
for &kid in kids {
cascade(
kid,
dfs_root,
child_clip,
children,
root_markers,
nodes,
clips,
);
}
}
}
fn children_clip(
node: &Node,
computed: &ComputedNode,
transform: &UiGlobalTransform,
inherited: Option<Rect>,
) -> Option<Rect> {
if node.overflow.is_visible() {
return inherited;
}
let mut clip_rect = computed.resolve_clip_rect(node.overflow, node.overflow_clip_margin);
clip_rect.min += transform.translation;
clip_rect.max += transform.translation;
Some(inherited.map_or(clip_rect, |c| c.intersect(clip_rect)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layer::{
LayerContentDirt, LayerGroupAlpha, LayerRepaintState, LayersRegistry, PromotionReasons,
resolve_layer_repaints, sync_layer_geometry,
};
use crate::protocol::NodeId;
use bevy::math::Affine2;
use bevy::ui::Overflow;
fn clip_world() -> (World, Schedule) {
let mut world = World::new();
world.init_resource::<LayerMembership>();
world.init_resource::<LayersRegistry>();
world.init_resource::<LayerRepaintState>();
world.init_resource::<LayerContentDirt>();
world.init_resource::<LayerClips>();
let mut schedule = Schedule::default();
schedule.add_systems(
(
sync_layer_geometry,
sync_layer_clips,
resolve_layer_repaints,
)
.chain(),
);
(world, schedule)
}
fn spawn_node(world: &mut World, size: Vec2, center: Vec2, overflow: Overflow) -> Entity {
world
.spawn((
Node {
overflow,
..Default::default()
},
ComputedNode {
size,
..Default::default()
},
UiGlobalTransform::from(Affine2::from_translation(center)),
))
.id()
}
fn spawn_root(
world: &mut World,
id: NodeId,
size: Vec2,
center: Vec2,
overflow: Overflow,
) -> Entity {
let e = spawn_node(world, size, center, overflow);
world.entity_mut(e).insert((
crate::bridge::RNode(id),
LayerGroupAlpha(1.0),
PromotedLayer {
reasons: PromotionReasons(PromotionReasons::FILTER),
},
));
e
}
fn interior(world: &World, e: Entity) -> Option<Rect> {
*world
.resource::<LayerClips>()
.interior
.get(&e)
.unwrap_or_else(|| panic!("no interior clip entry for {e:?}"))
}
fn quad(world: &World, e: Entity) -> Option<Rect> {
*world
.resource::<LayerClips>()
.quads
.get(&e)
.unwrap_or_else(|| panic!("no quad clip entry for {e:?}"))
}
#[test]
fn ancestor_clip_stripped_and_moved_to_quad() {
let (mut world, mut schedule) = clip_world();
let clip = Rect::new(0.0, 0.0, 30.0, 30.0);
let root = spawn_root(
&mut world,
1,
Vec2::new(100.0, 60.0),
Vec2::new(50.0, 30.0),
Overflow::visible(),
);
let child = spawn_node(
&mut world,
Vec2::new(40.0, 20.0),
Vec2::new(50.0, 30.0),
Overflow::visible(),
);
world.entity_mut(child).insert(ChildOf(root));
world.entity_mut(root).insert(CalculatedClip { clip });
world.entity_mut(child).insert(CalculatedClip { clip });
let unclipped = spawn_root(
&mut world,
2,
Vec2::new(50.0, 50.0),
Vec2::new(300.0, 300.0),
Overflow::visible(),
);
schedule.run(&mut world);
assert_eq!(interior(&world, root), None);
assert_eq!(interior(&world, child), None);
assert_eq!(quad(&world, root), Some(clip));
assert_eq!(quad(&world, unclipped), None);
}
#[test]
fn root_overflow_clips_children_only() {
let (mut world, mut schedule) = clip_world();
let root = spawn_root(
&mut world,
1,
Vec2::splat(200.0),
Vec2::splat(100.0),
Overflow::clip(),
);
let container = spawn_node(
&mut world,
Vec2::splat(100.0),
Vec2::new(160.0, 100.0),
Overflow::clip(),
);
world.entity_mut(container).insert(ChildOf(root));
let grandchild = spawn_node(
&mut world,
Vec2::splat(40.0),
Vec2::new(160.0, 100.0),
Overflow::visible(),
);
world.entity_mut(grandchild).insert(ChildOf(container));
schedule.run(&mut world);
let root_rect = Rect::new(0.0, 0.0, 200.0, 200.0);
let container_rect = Rect::new(110.0, 50.0, 210.0, 150.0);
assert_eq!(interior(&world, root), None, "root paint unclipped");
assert_eq!(interior(&world, container), Some(root_rect));
assert_eq!(
interior(&world, grandchild),
Some(root_rect.intersect(container_rect)),
"interior clippers intersect"
);
}
#[test]
fn nested_roots_get_quad_clips_and_restart_interior() {
let (mut world, mut schedule) = clip_world();
let outer = spawn_root(
&mut world,
1,
Vec2::splat(300.0),
Vec2::splat(150.0),
Overflow::visible(),
);
let clipper = spawn_node(
&mut world,
Vec2::splat(100.0),
Vec2::splat(100.0),
Overflow::clip(),
);
world.entity_mut(clipper).insert(ChildOf(outer));
let inner = spawn_root(
&mut world,
2,
Vec2::splat(80.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(inner).insert(ChildOf(clipper));
let inner_child = spawn_node(
&mut world,
Vec2::splat(40.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(inner_child).insert(ChildOf(inner));
let clipper2 = spawn_node(
&mut world,
Vec2::splat(60.0),
Vec2::new(140.0, 100.0),
Overflow::clip(),
);
world.entity_mut(clipper2).insert(ChildOf(inner));
let innermost = spawn_root(
&mut world,
3,
Vec2::splat(40.0),
Vec2::new(140.0, 100.0),
Overflow::visible(),
);
world.entity_mut(innermost).insert(ChildOf(clipper2));
schedule.run(&mut world);
let clipper_rect = Rect::new(50.0, 50.0, 150.0, 150.0);
let clipper2_rect = Rect::new(110.0, 70.0, 170.0, 130.0);
assert_eq!(quad(&world, inner), Some(clipper_rect));
assert_eq!(interior(&world, inner), None, "interior restarts");
assert_eq!(interior(&world, inner_child), None);
assert_eq!(
quad(&world, innermost),
Some(clipper2_rect),
"inner cascade only — outer clipper must not leak through the restart"
);
assert_eq!(interior(&world, clipper), None, "outer member");
}
#[test]
fn override_clip_and_display_none() {
let (mut world, mut schedule) = clip_world();
let root = spawn_root(
&mut world,
1,
Vec2::splat(200.0),
Vec2::splat(100.0),
Overflow::clip(),
);
let root_rect = Rect::new(0.0, 0.0, 200.0, 200.0);
let escapee = spawn_node(
&mut world,
Vec2::splat(40.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world
.entity_mut(escapee)
.insert((ChildOf(root), OverrideClip));
let escapee_child = spawn_node(
&mut world,
Vec2::splat(20.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(escapee_child).insert(ChildOf(escapee));
let hidden = spawn_node(
&mut world,
Vec2::splat(40.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(hidden).insert(ChildOf(root));
world.entity_mut(hidden).get_mut::<Node>().unwrap().display = bevy::ui::Display::None;
let hidden_child = spawn_node(
&mut world,
Vec2::splat(20.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(hidden_child).insert(ChildOf(hidden));
let plain = spawn_node(
&mut world,
Vec2::splat(40.0),
Vec2::splat(100.0),
Overflow::visible(),
);
world.entity_mut(plain).insert(ChildOf(root));
schedule.run(&mut world);
assert_eq!(interior(&world, escapee), None);
assert_eq!(interior(&world, escapee_child), None);
assert_eq!(interior(&world, hidden), Some(Rect::default()));
assert_eq!(interior(&world, hidden_child), Some(Rect::default()));
assert_eq!(interior(&world, plain), Some(root_rect));
}
#[test]
fn scroll_holds_cache_and_updates_quad_clip() {
let (mut world, mut schedule) = clip_world();
let clip = Rect::new(0.0, 0.0, 200.0, 100.0);
let root = spawn_root(
&mut world,
1,
Vec2::new(100.0, 60.0),
Vec2::new(50.0, 130.0),
Overflow::visible(),
);
let child = spawn_node(
&mut world,
Vec2::new(40.0, 20.0),
Vec2::new(50.0, 130.0),
Overflow::visible(),
);
world.entity_mut(child).insert(ChildOf(root));
world.entity_mut(root).insert(CalculatedClip { clip });
world.entity_mut(child).insert(CalculatedClip { clip });
schedule.run(&mut world);
schedule.run(&mut world); assert!(world.resource::<LayerRepaintState>().dirty.is_empty());
assert_eq!(quad(&world, root), Some(clip));
let delta = Vec2::new(0.0, -100.0);
for e in [root, child] {
let center = world.get::<UiGlobalTransform>(e).unwrap().translation + delta;
*world.get_mut::<UiGlobalTransform>(e).unwrap() =
UiGlobalTransform::from(Affine2::from_translation(center));
}
schedule.run(&mut world);
assert!(
world.resource::<LayerRepaintState>().dirty.is_empty(),
"scrolling must not re-capture"
);
assert_eq!(quad(&world, root), Some(clip));
let grown = Rect::new(0.0, 0.0, 200.0, 150.0);
world.get_mut::<CalculatedClip>(root).unwrap().clip = grown;
schedule.run(&mut world);
assert!(
world.resource::<LayerRepaintState>().dirty.is_empty(),
"a clip change must not re-capture"
);
assert_eq!(quad(&world, root), Some(grown));
}
}