mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 12:43:34 +00:00
Clip to the UI node's content box (#15442)
# Objective Change UI clipping to respect borders and padding. Fixes #15335 ## Solution Based on #15163 1. Add a `padding` field to `Node`. 2. In `ui_layout_size` copy the padding values from taffy to `Node::padding`. 4. Determine the node's content box (The innermost part of the node excluding the padding and border). 5. In `update_clipping` perform the clipping intersection with the node's content box. ## Notes * `Rect` probably needs some helper methods for working with insets but because `Rect` and `BorderRect` are in different crates it's awkward to add them. Left for a follow up. * We could have another `Overflow` variant (probably called `Overflow::Hidden`) to that clips inside of the border box instead of the content box. Left it out here as I'm not certain about the naming or behaviour though. If this PR is adopted, it would be trivial to add a `Hidden` variant in a follow up. * Depending on UI scaling there are sometimes gaps in the layout: <img width="532" alt="rounding-bug" src="https://github.com/user-attachments/assets/cc29aa0d-44fe-403f-8f0e-cd28a8b1d1b3"> This is caused by existing bugs in `ui_layout_system`'s coordinates rounding and not anything to do with the changes in this PR. ## Testing This PR also changes the `overflow` example to display borders on the overflow nodes so you can see how this works: #### main (The image is clipped at the edges of the node, overwriting the border). <img width="722" alt="main_overflow" src="https://github.com/user-attachments/assets/eb316cd0-fff8-46ee-b481-e0cd6bab3f5c"> #### this PR (The image is clipped at the edges of the node's border). <img width="711" alt="content-box-clip" src="https://github.com/user-attachments/assets/fb302e56-9302-47b9-9a29-ec3e15fe9a9f"> ## Migration Guide Migration guide is on #15561 --------- Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
This commit is contained in:
parent
a8530ebbc8
commit
b78a060af2
4 changed files with 48 additions and 5 deletions
|
@ -363,13 +363,16 @@ with UI components as a child of an entity without UI components, your UI layout
|
||||||
node.unrounded_size = layout_size;
|
node.unrounded_size = layout_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.bypass_change_detection().border = BorderRect {
|
let taffy_rect_to_border_rect = |rect: taffy::Rect<f32>| BorderRect {
|
||||||
left: layout.border.left * inverse_target_scale_factor,
|
left: rect.left * inverse_target_scale_factor,
|
||||||
right: layout.border.right * inverse_target_scale_factor,
|
right: rect.right * inverse_target_scale_factor,
|
||||||
top: layout.border.top * inverse_target_scale_factor,
|
top: rect.top * inverse_target_scale_factor,
|
||||||
bottom: layout.border.bottom * inverse_target_scale_factor,
|
bottom: rect.bottom * inverse_target_scale_factor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border);
|
||||||
|
node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding);
|
||||||
|
|
||||||
let viewport_size = root_size.unwrap_or(node.calculated_size);
|
let viewport_size = root_size.unwrap_or(node.calculated_size);
|
||||||
|
|
||||||
if let Some(border_radius) = maybe_border_radius {
|
if let Some(border_radius) = maybe_border_radius {
|
||||||
|
|
|
@ -58,6 +58,11 @@ pub struct Node {
|
||||||
///
|
///
|
||||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||||
pub(crate) border_radius: ResolvedBorderRadius,
|
pub(crate) border_radius: ResolvedBorderRadius,
|
||||||
|
/// Resolved padding values in logical pixels
|
||||||
|
/// Padding updates bypass change detection.
|
||||||
|
///
|
||||||
|
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||||
|
pub(crate) padding: BorderRect,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
@ -175,6 +180,25 @@ impl Node {
|
||||||
bottom_right: clamp_corner(self.border_radius.bottom_left, s, b.xw()),
|
bottom_right: clamp_corner(self.border_radius.bottom_left, s, b.xw()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the thickness of the node's padding on each edge in logical pixels.
|
||||||
|
///
|
||||||
|
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||||
|
#[inline]
|
||||||
|
pub fn padding(&self) -> BorderRect {
|
||||||
|
self.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the combined inset on each edge including both padding and border thickness in logical pixels.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_inset(&self) -> BorderRect {
|
||||||
|
BorderRect {
|
||||||
|
left: self.border.left + self.padding.left,
|
||||||
|
right: self.border.right + self.padding.right,
|
||||||
|
top: self.border.top + self.padding.top,
|
||||||
|
bottom: self.border.bottom + self.padding.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
@ -186,6 +210,7 @@ impl Node {
|
||||||
unrounded_size: Vec2::ZERO,
|
unrounded_size: Vec2::ZERO,
|
||||||
border_radius: ResolvedBorderRadius::ZERO,
|
border_radius: ResolvedBorderRadius::ZERO,
|
||||||
border: BorderRect::ZERO,
|
border: BorderRect::ZERO,
|
||||||
|
padding: BorderRect::ZERO,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,21 @@ fn update_clipping(
|
||||||
// current node's clip and the inherited clip. This handles the case
|
// current node's clip and the inherited clip. This handles the case
|
||||||
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
|
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
|
||||||
// defined, use the current node's clip.
|
// defined, use the current node's clip.
|
||||||
|
|
||||||
let mut node_rect =
|
let mut node_rect =
|
||||||
Rect::from_center_size(global_transform.translation().truncate(), node.size());
|
Rect::from_center_size(global_transform.translation().truncate(), node.size());
|
||||||
|
|
||||||
|
// Content isn't clipped at the edges of the node but at the edges of its content box.
|
||||||
|
// The content box is innermost part of the node excluding the padding and border.
|
||||||
|
//
|
||||||
|
// The `content_inset` should always fit inside the `node_rect`.
|
||||||
|
// Even if it were to overflow, this won't result in a degenerate clipping rect as `Rect::intersect` clamps the intersection to an empty rect.
|
||||||
|
let content_inset = node.content_inset();
|
||||||
|
node_rect.min.x += content_inset.left;
|
||||||
|
node_rect.min.y += content_inset.top;
|
||||||
|
node_rect.max.x -= content_inset.right;
|
||||||
|
node_rect.max.y -= content_inset.bottom;
|
||||||
|
|
||||||
if style.overflow.x == OverflowAxis::Visible {
|
if style.overflow.x == OverflowAxis::Visible {
|
||||||
node_rect.min.x = -f32::INFINITY;
|
node_rect.min.x = -f32::INFINITY;
|
||||||
node_rect.max.x = f32::INFINITY;
|
node_rect.max.x = f32::INFINITY;
|
||||||
|
|
|
@ -73,9 +73,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
top: Val::Px(25.),
|
top: Val::Px(25.),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
border: UiRect::all(Val::Px(5.)),
|
||||||
overflow,
|
overflow,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
border_color: Color::BLACK.into(),
|
||||||
background_color: GRAY.into(),
|
background_color: GRAY.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue