From 09df19bcadb52d2f4dbbc899aef74cafa9091538 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 17 Apr 2023 23:23:52 +0100 Subject: [PATCH] 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. overflow --- ## 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()` --- Cargo.toml | 10 +++ crates/bevy_ui/src/layout/convert.rs | 2 +- crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/ui_node.rs | 77 +++++++++++++++++++-- crates/bevy_ui/src/update.rs | 52 ++++++++------ examples/README.md | 1 + examples/ui/overflow.rs | 100 +++++++++++++++++++++++++++ examples/ui/overflow_debug.rs | 17 ++++- examples/ui/ui.rs | 2 +- 9 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 examples/ui/overflow.rs diff --git a/Cargo.toml b/Cargo.toml index 7df77f3ffd..1110809061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 62114d5c28..41b2fbcffb 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -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.), diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index fbffc9a8c0..7d29d859d1 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -106,6 +106,7 @@ impl Plugin for UiPlugin { // NOTE: used by Style::aspect_ratio .register_type::>() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 4d9e76c277..f2093ba14f 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -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)] diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index d7ed301e42..1da4395b55 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -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::(); - } - (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::(); } + } 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) { diff --git a/examples/README.md b/examples/README.md index 5f83b7f1a5..c04d30c330 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/ui/overflow.rs b/examples/ui/overflow.rs new file mode 100644 index 0000000000..e476bd3f43 --- /dev/null +++ b/examples/ui/overflow.rs @@ -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) { + 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() + }); + }); + }); + } + }); +} diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 14e3e0d1f3..b4d9b4afdf 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -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>, 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(), }; } } diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index f2761193c3..bf12b1a52e 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -112,7 +112,7 @@ fn setup(mut commands: Commands, asset_server: Res) { 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(),