Split UI Overflow
by axis (#8095)
# Objective Split the UI overflow enum so that overflow can be set for each axis separately. ## Solution Change `Overflow` from an enum to a struct with `x` and `y` `OverflowAxis` fields, where `OverflowAxis` is an enum with `Clip` and `Visible` variants. Modify `update_clipping` to calculate clipping for each axis separately. If only one axis is clipped, the other axis is given infinite bounds. <img width="642" alt="overflow" src="https://user-images.githubusercontent.com/27962798/227592983-568cf76f-7e40-48c4-a511-43c886f5e431.PNG"> --- ## Changelog * Split the UI overflow implementation so overflow can be set for each axis separately. * Added the enum `OverflowAxis` with `Clip` and `Visible` variants. * Changed `Overflow` to a struct with `x` and `y` fields of type `OverflowAxis`. * `Overflow` has new methods `visible()` and `hidden()` that replace its previous `Clip` and `Visible` variants. * Added `Overflow` helper methods `clip_x()` and `clip_y()` that return a new `Overflow` value with the given axis clipped. * Modified `update_clipping` so it calculates clipping for each axis separately. If a node is only clipped on a single axis, the other axis is given `-f32::INFINITY` to `f32::INFINITY` clipping bounds. ## Migration Guide The `Style` property `Overflow` is now a struct with `x` and `y` fields, that allow for per-axis overflow control. Use these helper functions to replace the variants of `Overflow`: * Replace `Overflow::Visible` with `Overflow::visible()` * Replace `Overflow::Hidden` with `Overflow::clip()`
This commit is contained in:
parent
e54057c50d
commit
09df19bcad
10
Cargo.toml
10
Cargo.toml
@ -1758,6 +1758,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
|
|||||||
category = "UI (User Interface)"
|
category = "UI (User Interface)"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "overflow"
|
||||||
|
path = "examples/ui/overflow.rs"
|
||||||
|
|
||||||
|
[package.metadata.example.overflow]
|
||||||
|
name = "Overflow"
|
||||||
|
description = "Simple example demonstrating overflow behavior"
|
||||||
|
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"
|
||||||
|
@ -452,7 +452,7 @@ mod tests {
|
|||||||
height: Val::Px(0.),
|
height: Val::Px(0.),
|
||||||
},
|
},
|
||||||
aspect_ratio: None,
|
aspect_ratio: None,
|
||||||
overflow: crate::Overflow::Hidden,
|
overflow: crate::Overflow::clip(),
|
||||||
gap: Size {
|
gap: Size {
|
||||||
width: Val::Px(0.),
|
width: Val::Px(0.),
|
||||||
height: Val::Percent(0.),
|
height: Val::Percent(0.),
|
||||||
|
@ -106,6 +106,7 @@ impl Plugin for UiPlugin {
|
|||||||
// NOTE: used by Style::aspect_ratio
|
// NOTE: used by Style::aspect_ratio
|
||||||
.register_type::<Option<f32>>()
|
.register_type::<Option<f32>>()
|
||||||
.register_type::<Overflow>()
|
.register_type::<Overflow>()
|
||||||
|
.register_type::<OverflowAxis>()
|
||||||
.register_type::<PositionType>()
|
.register_type::<PositionType>()
|
||||||
.register_type::<Size>()
|
.register_type::<Size>()
|
||||||
.register_type::<UiRect>()
|
.register_type::<UiRect>()
|
||||||
|
@ -876,15 +876,55 @@ impl Default for FlexDirection {
|
|||||||
/// Whether to show or hide overflowing items
|
/// Whether to show or hide overflowing items
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
|
||||||
#[reflect(PartialEq, Serialize, Deserialize)]
|
#[reflect(PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Overflow {
|
pub struct Overflow {
|
||||||
/// Show overflowing items.
|
/// Whether to show or clip overflowing items on the x axis
|
||||||
Visible,
|
pub x: OverflowAxis,
|
||||||
/// Hide overflowing items.
|
/// Whether to show or clip overflowing items on the y axis
|
||||||
Hidden,
|
pub y: OverflowAxis,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Overflow {
|
impl Overflow {
|
||||||
pub const DEFAULT: Self = Self::Visible;
|
pub const DEFAULT: Self = Self {
|
||||||
|
x: OverflowAxis::DEFAULT,
|
||||||
|
y: OverflowAxis::DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Show overflowing items on both axes
|
||||||
|
pub const fn visible() -> Self {
|
||||||
|
Self {
|
||||||
|
x: OverflowAxis::Visible,
|
||||||
|
y: OverflowAxis::Visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip overflowing items on both axes
|
||||||
|
pub const fn clip() -> Self {
|
||||||
|
Self {
|
||||||
|
x: OverflowAxis::Clip,
|
||||||
|
y: OverflowAxis::Clip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip overflowing items on the x axis
|
||||||
|
pub const fn clip_x() -> Self {
|
||||||
|
Self {
|
||||||
|
x: OverflowAxis::Clip,
|
||||||
|
y: OverflowAxis::Visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip overflowing items on the y axis
|
||||||
|
pub const fn clip_y() -> Self {
|
||||||
|
Self {
|
||||||
|
x: OverflowAxis::Visible,
|
||||||
|
y: OverflowAxis::Clip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overflow is visible on both axes
|
||||||
|
pub const fn is_visible(&self) -> bool {
|
||||||
|
self.x.is_visible() && self.y.is_visible()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Overflow {
|
impl Default for Overflow {
|
||||||
@ -893,6 +933,31 @@ impl Default for Overflow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to show or hide overflowing items
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
|
||||||
|
#[reflect(PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum OverflowAxis {
|
||||||
|
/// Show overflowing items.
|
||||||
|
Visible,
|
||||||
|
/// Hide overflowing items.
|
||||||
|
Clip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverflowAxis {
|
||||||
|
pub const DEFAULT: Self = Self::Visible;
|
||||||
|
|
||||||
|
/// Overflow is visible on this axis
|
||||||
|
pub const fn is_visible(&self) -> bool {
|
||||||
|
matches!(self, Self::Visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OverflowAxis {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The strategy used to position this node
|
/// The strategy used to position this node
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Reflect)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Reflect)]
|
||||||
#[reflect(PartialEq, Serialize, Deserialize)]
|
#[reflect(PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! This module contains systems that update the UI when something changes
|
//! This module contains systems that update the UI when something changes
|
||||||
|
|
||||||
use crate::{CalculatedClip, Overflow, Style};
|
use crate::{CalculatedClip, OverflowAxis, Style};
|
||||||
|
|
||||||
use super::Node;
|
use super::Node;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
@ -40,42 +40,48 @@ fn update_clipping(
|
|||||||
let (node, global_transform, style, maybe_calculated_clip) =
|
let (node, global_transform, style, maybe_calculated_clip) =
|
||||||
node_query.get_mut(entity).unwrap();
|
node_query.get_mut(entity).unwrap();
|
||||||
|
|
||||||
// Update current node's CalculatedClip component
|
// Update this node's CalculatedClip component
|
||||||
match (maybe_calculated_clip, maybe_inherited_clip) {
|
if let Some(mut calculated_clip) = maybe_calculated_clip {
|
||||||
(None, None) => {}
|
if let Some(inherited_clip) = maybe_inherited_clip {
|
||||||
(Some(_), None) => {
|
// Replace the previous calculated clip with the inherited clipping rect
|
||||||
commands.entity(entity).remove::<CalculatedClip>();
|
|
||||||
}
|
|
||||||
(None, Some(inherited_clip)) => {
|
|
||||||
commands.entity(entity).insert(CalculatedClip {
|
|
||||||
clip: inherited_clip,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
(Some(mut calculated_clip), Some(inherited_clip)) => {
|
|
||||||
if calculated_clip.clip != inherited_clip {
|
if calculated_clip.clip != inherited_clip {
|
||||||
*calculated_clip = CalculatedClip {
|
*calculated_clip = CalculatedClip {
|
||||||
clip: inherited_clip,
|
clip: inherited_clip,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No inherited clipping rect, remove the component
|
||||||
|
commands.entity(entity).remove::<CalculatedClip>();
|
||||||
}
|
}
|
||||||
|
} else if let Some(inherited_clip) = maybe_inherited_clip {
|
||||||
|
// No previous calculated clip, add a new CalculatedClip component with the inherited clipping rect
|
||||||
|
commands.entity(entity).insert(CalculatedClip {
|
||||||
|
clip: inherited_clip,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new clip rectangle for children nodes
|
// Calculate new clip rectangle for children nodes
|
||||||
let children_clip = match style.overflow {
|
let children_clip = if style.overflow.is_visible() {
|
||||||
// When `Visible`, children might be visible even when they are outside
|
// When `Visible`, children might be visible even when they are outside
|
||||||
// the current node's boundaries. In this case they inherit the current
|
// the current node's boundaries. In this case they inherit the current
|
||||||
// node's parent clip. If an ancestor is set as `Hidden`, that clip will
|
// node's parent clip. If an ancestor is set as `Hidden`, that clip will
|
||||||
// be used; otherwise this will be `None`.
|
// be used; otherwise this will be `None`.
|
||||||
Overflow::Visible => maybe_inherited_clip,
|
maybe_inherited_clip
|
||||||
Overflow::Hidden => {
|
} else {
|
||||||
let node_clip = node.logical_rect(global_transform);
|
// If `maybe_inherited_clip` is `Some`, use the intersection between
|
||||||
|
// current node's clip and the inherited clip. This handles the case
|
||||||
// If `maybe_inherited_clip` is `Some`, use the intersection between
|
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
|
||||||
// current node's clip and the inherited clip. This handles the case
|
// defined, use the current node's clip.
|
||||||
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
|
let mut node_rect = node.logical_rect(global_transform);
|
||||||
// defined, use the current node's clip.
|
if style.overflow.x == OverflowAxis::Visible {
|
||||||
Some(maybe_inherited_clip.map_or(node_clip, |c| c.intersect(node_clip)))
|
node_rect.min.x = -f32::INFINITY;
|
||||||
|
node_rect.max.x = f32::INFINITY;
|
||||||
}
|
}
|
||||||
|
if style.overflow.y == OverflowAxis::Visible {
|
||||||
|
node_rect.min.y = -f32::INFINITY;
|
||||||
|
node_rect.max.y = f32::INFINITY;
|
||||||
|
}
|
||||||
|
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(children) = children_query.get(entity) {
|
if let Ok(children) = children_query.get(entity) {
|
||||||
|
@ -334,6 +334,7 @@ Example | Description
|
|||||||
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
|
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
|
||||||
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
|
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
|
||||||
[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)
|
||||||
|
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
|
||||||
[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
|
||||||
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
||||||
|
100
examples/ui/overflow.rs
Normal file
100
examples/ui/overflow.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
//! Simple example demonstrating overflow behavior.
|
||||||
|
|
||||||
|
use bevy::{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(Camera2dBundle::default());
|
||||||
|
|
||||||
|
let text_style = TextStyle {
|
||||||
|
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||||
|
font_size: 20.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = asset_server.load("branding/icon.png");
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
size: Size::width(Val::Percent(100.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::ANTIQUE_WHITE.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
for overflow in [
|
||||||
|
Overflow::visible(),
|
||||||
|
Overflow::clip_x(),
|
||||||
|
Overflow::clip_y(),
|
||||||
|
Overflow::clip(),
|
||||||
|
] {
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
margin: UiRect::horizontal(Val::Px(25.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
let label = format!("{overflow:#?}");
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
padding: UiRect::all(Val::Px(10.)),
|
||||||
|
margin: UiRect::bottom(Val::Px(25.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::DARK_GRAY.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle {
|
||||||
|
text: Text::from_section(label, text_style.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
size: Size::all(Val::Px(100.)),
|
||||||
|
padding: UiRect {
|
||||||
|
left: Val::Px(25.),
|
||||||
|
top: Val::Px(25.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
overflow,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::GRAY.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(ImageBundle {
|
||||||
|
image: UiImage::new(image.clone()),
|
||||||
|
style: Style {
|
||||||
|
min_size: Size::all(Val::Px(100.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::WHITE.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -212,7 +212,7 @@ fn spawn_container(
|
|||||||
size: Size::new(Val::Px(CONTAINER_SIZE), Val::Px(CONTAINER_SIZE)),
|
size: Size::new(Val::Px(CONTAINER_SIZE), Val::Px(CONTAINER_SIZE)),
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
overflow: Overflow::Hidden,
|
overflow: Overflow::clip(),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
background_color: Color::DARK_GRAY.into(),
|
background_color: Color::DARK_GRAY.into(),
|
||||||
@ -278,8 +278,19 @@ fn toggle_overflow(keys: Res<Input<KeyCode>>, mut containers: Query<&mut Style,
|
|||||||
if keys.just_pressed(KeyCode::O) {
|
if keys.just_pressed(KeyCode::O) {
|
||||||
for mut style in &mut containers {
|
for mut style in &mut containers {
|
||||||
style.overflow = match style.overflow {
|
style.overflow = match style.overflow {
|
||||||
Overflow::Visible => Overflow::Hidden,
|
Overflow {
|
||||||
Overflow::Hidden => Overflow::Visible,
|
x: OverflowAxis::Visible,
|
||||||
|
y: OverflowAxis::Visible,
|
||||||
|
} => Overflow::clip_y(),
|
||||||
|
Overflow {
|
||||||
|
x: OverflowAxis::Visible,
|
||||||
|
y: OverflowAxis::Clip,
|
||||||
|
} => Overflow::clip_x(),
|
||||||
|
Overflow {
|
||||||
|
x: OverflowAxis::Clip,
|
||||||
|
y: OverflowAxis::Visible,
|
||||||
|
} => Overflow::clip(),
|
||||||
|
_ => Overflow::visible(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
flex_direction: FlexDirection::Column,
|
flex_direction: FlexDirection::Column,
|
||||||
align_self: AlignSelf::Stretch,
|
align_self: AlignSelf::Stretch,
|
||||||
size: Size::height(Val::Percent(50.)),
|
size: Size::height(Val::Percent(50.)),
|
||||||
overflow: Overflow::Hidden,
|
overflow: Overflow::clip_y(),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
background_color: Color::rgb(0.10, 0.10, 0.10).into(),
|
background_color: Color::rgb(0.10, 0.10, 0.10).into(),
|
||||||
|
Loading…
Reference in New Issue
Block a user