mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 13:43:04 +00:00
Overflow clip margin (#15561)
# Objective Limited implementation of the CSS property `overflow-clip-margin` https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-margin Allows you to control the visible area for clipped content when using overfllow-clip, -hidden, or -scroll and expand it with a margin. Based on #15442 Fixes #15468 ## Solution Adds a new field to Style: `overflow_clip_margin: OverflowClipMargin`. The field is ignored unless overflow-clip, -hidden or -scroll is set on at least one axis. `OverflowClipMargin` has these associated constructor functions: ``` pub const fn content_box() -> Self; pub const fn padding_box() -> Self; pub const fn border_box() -> Self; ``` You can also use the method `with_margin` to increases the size of the visible area: ``` commands .spawn(NodeBundle { style: Style { width: Val::Px(100.), height: Val::Px(100.), padding: UiRect::all(Val::Px(20.)), border: UiRect::all(Val::Px(5.)), overflow: Overflow::clip(), overflow_clip_margin: OverflowClipMargin::border_box().with_margin(25.), ..Default::default() }, border_color: Color::BLACK.into(), background_color: GRAY.into(), ..Default::default() }) ``` `with_margin` expects a length in logical pixels, negative values are clamped to zero. ## Notes * To keep this PR as simple as possible I omitted responsive margin values support. This could be added in a follow up if we want it. * CSS also supports a `margin-box` option but we don't have access to the margin values in `Node` so it's probably not feasible to implement atm. ## Testing ```cargo run --example overflow_clip_margin``` <img width="396" alt="overflow-clip-margin" src="https://github.com/user-attachments/assets/07b51cd6-a565-4451-87a0-fa079429b04b"> ## Migration Guide Style has a new field `OverflowClipMargin`. It allows users to set the visible area for clipped content when using overflow-clip, -hidden, or -scroll and expand it with a margin. There are three associated constructor functions `content_box`, `padding_box` and `border_box`: * `content_box`: elements painted outside of the content box area (the innermost part of the node excluding the padding and border) of the node are clipped. This is the new default behaviour. * `padding_box`: elements painted outside outside of the padding area of the node are clipped. * `border_box`: elements painted outside of the bounds of the node are clipped. This matches the behaviour from Bevy 0.14. There is also a `with_margin` method that increases the size of the visible area by the given number in logical pixels, negative margin values are clamped to zero. `OverflowClipMargin` is ignored unless overflow-clip, -hidden or -scroll is also set on at least one axis of the UI node. --------- Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
This commit is contained in:
parent
87c33da139
commit
6d3965f520
6 changed files with 216 additions and 15 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -2977,6 +2977,18 @@ description = "Simple example demonstrating overflow behavior"
|
||||||
category = "UI (User Interface)"
|
category = "UI (User Interface)"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "overflow_clip_margin"
|
||||||
|
path = "examples/ui/overflow_clip_margin.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.overflow_clip_margin]
|
||||||
|
name = "Overflow Clip Margin"
|
||||||
|
description = "Simple example demonstrating the OverflowClipMargin style property"
|
||||||
|
category = "UI (User Interface)"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "overflow_debug"
|
name = "overflow_debug"
|
||||||
path = "examples/ui/overflow_debug.rs"
|
path = "examples/ui/overflow_debug.rs"
|
||||||
|
|
|
@ -492,6 +492,7 @@ mod tests {
|
||||||
max_height: Val::ZERO,
|
max_height: Val::ZERO,
|
||||||
aspect_ratio: None,
|
aspect_ratio: None,
|
||||||
overflow: crate::Overflow::clip(),
|
overflow: crate::Overflow::clip(),
|
||||||
|
overflow_clip_margin: crate::OverflowClipMargin::default(),
|
||||||
column_gap: Val::ZERO,
|
column_gap: Val::ZERO,
|
||||||
row_gap: Val::ZERO,
|
row_gap: Val::ZERO,
|
||||||
grid_auto_flow: GridAutoFlow::ColumnDense,
|
grid_auto_flow: GridAutoFlow::ColumnDense,
|
||||||
|
|
|
@ -308,6 +308,11 @@ pub struct Style {
|
||||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
|
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
|
||||||
pub overflow: Overflow,
|
pub overflow: Overflow,
|
||||||
|
|
||||||
|
/// How the bounds of clipped content should be determined
|
||||||
|
///
|
||||||
|
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-margin>
|
||||||
|
pub overflow_clip_margin: OverflowClipMargin,
|
||||||
|
|
||||||
/// The horizontal position of the left edge of the node.
|
/// The horizontal position of the left edge of the node.
|
||||||
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
|
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
|
||||||
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
|
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
|
||||||
|
@ -585,6 +590,7 @@ impl Style {
|
||||||
max_height: Val::Auto,
|
max_height: Val::Auto,
|
||||||
aspect_ratio: None,
|
aspect_ratio: None,
|
||||||
overflow: Overflow::DEFAULT,
|
overflow: Overflow::DEFAULT,
|
||||||
|
overflow_clip_margin: OverflowClipMargin::DEFAULT,
|
||||||
row_gap: Val::ZERO,
|
row_gap: Val::ZERO,
|
||||||
column_gap: Val::ZERO,
|
column_gap: Val::ZERO,
|
||||||
grid_auto_flow: GridAutoFlow::DEFAULT,
|
grid_auto_flow: GridAutoFlow::DEFAULT,
|
||||||
|
@ -1042,6 +1048,78 @@ impl Default for OverflowAxis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bounds of the visible area when a UI node is clipped.
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
|
||||||
|
#[reflect(Default, PartialEq)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serialize",
|
||||||
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
|
reflect(Serialize, Deserialize)
|
||||||
|
)]
|
||||||
|
pub struct OverflowClipMargin {
|
||||||
|
/// Visible unclipped area
|
||||||
|
pub visual_box: OverflowClipBox,
|
||||||
|
/// Width of the margin on each edge of the visual box in logical pixels.
|
||||||
|
/// The width of the margin will be zero if a negative value is set.
|
||||||
|
pub margin: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverflowClipMargin {
|
||||||
|
pub const DEFAULT: Self = Self {
|
||||||
|
visual_box: OverflowClipBox::ContentBox,
|
||||||
|
margin: 0.,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Clip any content that overflows outside the content box
|
||||||
|
pub const fn content_box() -> Self {
|
||||||
|
Self {
|
||||||
|
visual_box: OverflowClipBox::ContentBox,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip any content that overflows outside the padding box
|
||||||
|
pub const fn padding_box() -> Self {
|
||||||
|
Self {
|
||||||
|
visual_box: OverflowClipBox::PaddingBox,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip any content that overflows outside the border box
|
||||||
|
pub const fn border_box() -> Self {
|
||||||
|
Self {
|
||||||
|
visual_box: OverflowClipBox::BorderBox,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a margin on each edge of the visual box in logical pixels.
|
||||||
|
/// The width of the margin will be zero if a negative value is set.
|
||||||
|
pub const fn with_margin(mut self, margin: f32) -> Self {
|
||||||
|
self.margin = margin;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to determine the bounds of the visible area when a UI node is clipped.
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Reflect)]
|
||||||
|
#[reflect(Default, PartialEq)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serialize",
|
||||||
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
|
reflect(Serialize, Deserialize)
|
||||||
|
)]
|
||||||
|
pub enum OverflowClipBox {
|
||||||
|
/// Clip any content that overflows outside the content box
|
||||||
|
#[default]
|
||||||
|
ContentBox,
|
||||||
|
/// Clip any content that overflows outside the padding box
|
||||||
|
PaddingBox,
|
||||||
|
/// Clip any content that overflows outside the border box
|
||||||
|
BorderBox,
|
||||||
|
}
|
||||||
|
|
||||||
/// The strategy used to position this node
|
/// The strategy used to position this node
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect)]
|
||||||
#[reflect(Default, PartialEq)]
|
#[reflect(Default, PartialEq)]
|
||||||
|
|
|
@ -9,6 +9,7 @@ use bevy_ecs::{
|
||||||
system::{Commands, Query},
|
system::{Commands, Query},
|
||||||
};
|
};
|
||||||
use bevy_math::Rect;
|
use bevy_math::Rect;
|
||||||
|
use bevy_sprite::BorderRect;
|
||||||
use bevy_transform::components::GlobalTransform;
|
use bevy_transform::components::GlobalTransform;
|
||||||
use bevy_utils::HashSet;
|
use bevy_utils::HashSet;
|
||||||
|
|
||||||
|
@ -80,29 +81,35 @@ fn update_clipping(
|
||||||
// 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 clip_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.
|
// Content isn't clipped at the edges of the node but at the edges of the region specified by [`Style::overflow_clip_margin`].
|
||||||
// The content box is innermost part of the node excluding the padding and border.
|
|
||||||
//
|
//
|
||||||
// The `content_inset` should always fit inside the `node_rect`.
|
// `clip_inset` should always fit inside `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.
|
// Even if `clip_inset` were to overflow, we won't return a degenerate result as `Rect::intersect` will clamp the intersection, leaving it empty.
|
||||||
let content_inset = node.content_inset();
|
let clip_inset = match style.overflow_clip_margin.visual_box {
|
||||||
node_rect.min.x += content_inset.left;
|
crate::OverflowClipBox::BorderBox => BorderRect::ZERO,
|
||||||
node_rect.min.y += content_inset.top;
|
crate::OverflowClipBox::ContentBox => node.content_inset(),
|
||||||
node_rect.max.x -= content_inset.right;
|
crate::OverflowClipBox::PaddingBox => node.border(),
|
||||||
node_rect.max.y -= content_inset.bottom;
|
};
|
||||||
|
|
||||||
|
clip_rect.min.x += clip_inset.left;
|
||||||
|
clip_rect.min.y += clip_inset.top;
|
||||||
|
clip_rect.max.x -= clip_inset.right;
|
||||||
|
clip_rect.max.y -= clip_inset.bottom;
|
||||||
|
|
||||||
|
clip_rect = clip_rect.inflate(style.overflow_clip_margin.margin.max(0.));
|
||||||
|
|
||||||
if style.overflow.x == OverflowAxis::Visible {
|
if style.overflow.x == OverflowAxis::Visible {
|
||||||
node_rect.min.x = -f32::INFINITY;
|
clip_rect.min.x = -f32::INFINITY;
|
||||||
node_rect.max.x = f32::INFINITY;
|
clip_rect.max.x = f32::INFINITY;
|
||||||
}
|
}
|
||||||
if style.overflow.y == OverflowAxis::Visible {
|
if style.overflow.y == OverflowAxis::Visible {
|
||||||
node_rect.min.y = -f32::INFINITY;
|
clip_rect.min.y = -f32::INFINITY;
|
||||||
node_rect.max.y = f32::INFINITY;
|
clip_rect.max.y = f32::INFINITY;
|
||||||
}
|
}
|
||||||
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
|
Some(maybe_inherited_clip.map_or(clip_rect, |c| c.intersect(clip_rect)))
|
||||||
};
|
};
|
||||||
|
|
||||||
for child in ui_children.iter_ui_children(entity) {
|
for child in ui_children.iter_ui_children(entity) {
|
||||||
|
|
|
@ -506,6 +506,7 @@ Example | Description
|
||||||
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
|
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
|
||||||
[Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy
|
[Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy
|
||||||
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
|
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
|
||||||
|
[Overflow Clip Margin](../examples/ui/overflow_clip_margin.rs) | Simple example demonstrating the OverflowClipMargin style property
|
||||||
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
||||||
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||||
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
|
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
|
||||||
|
|
102
examples/ui/overflow_clip_margin.rs
Normal file
102
examples/ui/overflow_clip_margin.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
//! Simple example demonstrating the `OverflowClipMargin` style property.
|
||||||
|
|
||||||
|
use bevy::{color::palettes::css::*, prelude::*, winit::WinitSettings};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
commands.spawn(Camera2d);
|
||||||
|
|
||||||
|
let image = asset_server.load("branding/icon.png");
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Percent(100.),
|
||||||
|
height: Val::Percent(100.),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
row_gap: Val::Px(40.),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: ANTIQUE_WHITE.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
for overflow_clip_margin in [
|
||||||
|
OverflowClipMargin::border_box().with_margin(25.),
|
||||||
|
OverflowClipMargin::border_box(),
|
||||||
|
OverflowClipMargin::padding_box(),
|
||||||
|
OverflowClipMargin::content_box(),
|
||||||
|
] {
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
column_gap: Val::Px(20.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
padding: UiRect::all(Val::Px(10.)),
|
||||||
|
margin: UiRect::bottom(Val::Px(25.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::srgb(0.25, 0.25, 0.25).into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_child(Text(format!("{overflow_clip_margin:#?}")));
|
||||||
|
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
margin: UiRect::top(Val::Px(10.)),
|
||||||
|
width: Val::Px(100.),
|
||||||
|
height: Val::Px(100.),
|
||||||
|
padding: UiRect::all(Val::Px(20.)),
|
||||||
|
border: UiRect::all(Val::Px(5.)),
|
||||||
|
overflow: Overflow::clip(),
|
||||||
|
overflow_clip_margin,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
border_color: Color::BLACK.into(),
|
||||||
|
background_color: GRAY.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
min_width: Val::Px(50.),
|
||||||
|
min_height: Val::Px(50.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: LIGHT_CYAN.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_child(ImageBundle {
|
||||||
|
image: UiImage::new(image.clone()),
|
||||||
|
style: Style {
|
||||||
|
min_width: Val::Px(100.),
|
||||||
|
min_height: Val::Px(100.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue