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:
ickshonpe 2023-04-17 23:23:52 +01:00 committed by GitHub
parent e54057c50d
commit 09df19bcad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 34 deletions

View file

@ -1758,6 +1758,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
category = "UI (User Interface)"
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]]
name = "overflow_debug"
path = "examples/ui/overflow_debug.rs"

View file

@ -452,7 +452,7 @@ mod tests {
height: Val::Px(0.),
},
aspect_ratio: None,
overflow: crate::Overflow::Hidden,
overflow: crate::Overflow::clip(),
gap: Size {
width: Val::Px(0.),
height: Val::Percent(0.),

View file

@ -106,6 +106,7 @@ impl Plugin for UiPlugin {
// NOTE: used by Style::aspect_ratio
.register_type::<Option<f32>>()
.register_type::<Overflow>()
.register_type::<OverflowAxis>()
.register_type::<PositionType>()
.register_type::<Size>()
.register_type::<UiRect>()

View file

@ -876,15 +876,55 @@ impl Default for FlexDirection {
/// Whether to show or hide overflowing items
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub enum Overflow {
/// Show overflowing items.
Visible,
/// Hide overflowing items.
Hidden,
pub struct Overflow {
/// Whether to show or clip overflowing items on the x axis
pub x: OverflowAxis,
/// Whether to show or clip overflowing items on the y axis
pub y: OverflowAxis,
}
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 {
@ -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
#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]

View file

@ -1,6 +1,6 @@
//! 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 bevy_ecs::{
@ -40,42 +40,48 @@ fn update_clipping(
let (node, global_transform, style, maybe_calculated_clip) =
node_query.get_mut(entity).unwrap();
// Update current node's CalculatedClip component
match (maybe_calculated_clip, maybe_inherited_clip) {
(None, None) => {}
(Some(_), None) => {
commands.entity(entity).remove::<CalculatedClip>();
}
(None, Some(inherited_clip)) => {
commands.entity(entity).insert(CalculatedClip {
clip: inherited_clip,
});
}
(Some(mut calculated_clip), Some(inherited_clip)) => {
// Update this node's CalculatedClip component
if let Some(mut calculated_clip) = maybe_calculated_clip {
if let Some(inherited_clip) = maybe_inherited_clip {
// Replace the previous calculated clip with the inherited clipping rect
if calculated_clip.clip != inherited_clip {
*calculated_clip = CalculatedClip {
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
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
// 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
// be used; otherwise this will be `None`.
Overflow::Visible => maybe_inherited_clip,
Overflow::Hidden => {
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
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
// defined, use the current node's clip.
Some(maybe_inherited_clip.map_or(node_clip, |c| c.intersect(node_clip)))
maybe_inherited_clip
} else {
// If `maybe_inherited_clip` is `Some`, use the intersection between
// current node's clip and the inherited clip. This handles the case
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
// defined, use the current node's clip.
let mut node_rect = node.logical_rect(global_transform);
if style.overflow.x == OverflowAxis::Visible {
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) {

View file

@ -334,6 +334,7 @@ Example | Description
[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
[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
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
[Text](../examples/ui/text.rs) | Illustrates creating and updating text

100
examples/ui/overflow.rs Normal file
View 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()
});
});
});
}
});
}

View file

@ -212,7 +212,7 @@ fn spawn_container(
size: Size::new(Val::Px(CONTAINER_SIZE), Val::Px(CONTAINER_SIZE)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
overflow: Overflow::Hidden,
overflow: Overflow::clip(),
..default()
},
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) {
for mut style in &mut containers {
style.overflow = match style.overflow {
Overflow::Visible => Overflow::Hidden,
Overflow::Hidden => Overflow::Visible,
Overflow {
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(),
};
}
}

View file

@ -112,7 +112,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Stretch,
size: Size::height(Val::Percent(50.)),
overflow: Overflow::Hidden,
overflow: Overflow::clip_y(),
..default()
},
background_color: Color::rgb(0.10, 0.10, 0.10).into(),