Skip to content

Commit 09df19b

Browse files
authored
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()`
1 parent e54057c commit 09df19b

File tree

9 files changed

+226
-32
lines changed

9 files changed

+226
-32
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,6 +1758,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
17581758
category = "UI (User Interface)"
17591759
wasm = true
17601760

1761+
[[example]]
1762+
name = "overflow"
1763+
path = "examples/ui/overflow.rs"
1764+
1765+
[package.metadata.example.overflow]
1766+
name = "Overflow"
1767+
description = "Simple example demonstrating overflow behavior"
1768+
category = "UI (User Interface)"
1769+
wasm = true
1770+
17611771
[[example]]
17621772
name = "overflow_debug"
17631773
path = "examples/ui/overflow_debug.rs"

crates/bevy_ui/src/layout/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ mod tests {
452452
height: Val::Px(0.),
453453
},
454454
aspect_ratio: None,
455-
overflow: crate::Overflow::Hidden,
455+
overflow: crate::Overflow::clip(),
456456
gap: Size {
457457
width: Val::Px(0.),
458458
height: Val::Percent(0.),

crates/bevy_ui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ impl Plugin for UiPlugin {
106106
// NOTE: used by Style::aspect_ratio
107107
.register_type::<Option<f32>>()
108108
.register_type::<Overflow>()
109+
.register_type::<OverflowAxis>()
109110
.register_type::<PositionType>()
110111
.register_type::<Size>()
111112
.register_type::<UiRect>()

crates/bevy_ui/src/ui_node.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,18 +876,83 @@ impl Default for FlexDirection {
876876
/// Whether to show or hide overflowing items
877877
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
878878
#[reflect(PartialEq, Serialize, Deserialize)]
879-
pub enum Overflow {
879+
pub struct Overflow {
880+
/// Whether to show or clip overflowing items on the x axis
881+
pub x: OverflowAxis,
882+
/// Whether to show or clip overflowing items on the y axis
883+
pub y: OverflowAxis,
884+
}
885+
886+
impl Overflow {
887+
pub const DEFAULT: Self = Self {
888+
x: OverflowAxis::DEFAULT,
889+
y: OverflowAxis::DEFAULT,
890+
};
891+
892+
/// Show overflowing items on both axes
893+
pub const fn visible() -> Self {
894+
Self {
895+
x: OverflowAxis::Visible,
896+
y: OverflowAxis::Visible,
897+
}
898+
}
899+
900+
/// Clip overflowing items on both axes
901+
pub const fn clip() -> Self {
902+
Self {
903+
x: OverflowAxis::Clip,
904+
y: OverflowAxis::Clip,
905+
}
906+
}
907+
908+
/// Clip overflowing items on the x axis
909+
pub const fn clip_x() -> Self {
910+
Self {
911+
x: OverflowAxis::Clip,
912+
y: OverflowAxis::Visible,
913+
}
914+
}
915+
916+
/// Clip overflowing items on the y axis
917+
pub const fn clip_y() -> Self {
918+
Self {
919+
x: OverflowAxis::Visible,
920+
y: OverflowAxis::Clip,
921+
}
922+
}
923+
924+
/// Overflow is visible on both axes
925+
pub const fn is_visible(&self) -> bool {
926+
self.x.is_visible() && self.y.is_visible()
927+
}
928+
}
929+
930+
impl Default for Overflow {
931+
fn default() -> Self {
932+
Self::DEFAULT
933+
}
934+
}
935+
936+
/// Whether to show or hide overflowing items
937+
#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect, Serialize, Deserialize)]
938+
#[reflect(PartialEq, Serialize, Deserialize)]
939+
pub enum OverflowAxis {
880940
/// Show overflowing items.
881941
Visible,
882942
/// Hide overflowing items.
883-
Hidden,
943+
Clip,
884944
}
885945

886-
impl Overflow {
946+
impl OverflowAxis {
887947
pub const DEFAULT: Self = Self::Visible;
948+
949+
/// Overflow is visible on this axis
950+
pub const fn is_visible(&self) -> bool {
951+
matches!(self, Self::Visible)
952+
}
888953
}
889954

890-
impl Default for Overflow {
955+
impl Default for OverflowAxis {
891956
fn default() -> Self {
892957
Self::DEFAULT
893958
}

crates/bevy_ui/src/update.rs

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! This module contains systems that update the UI when something changes
22
3-
use crate::{CalculatedClip, Overflow, Style};
3+
use crate::{CalculatedClip, OverflowAxis, Style};
44

55
use super::Node;
66
use bevy_ecs::{
@@ -40,42 +40,48 @@ fn update_clipping(
4040
let (node, global_transform, style, maybe_calculated_clip) =
4141
node_query.get_mut(entity).unwrap();
4242

43-
// Update current node's CalculatedClip component
44-
match (maybe_calculated_clip, maybe_inherited_clip) {
45-
(None, None) => {}
46-
(Some(_), None) => {
47-
commands.entity(entity).remove::<CalculatedClip>();
48-
}
49-
(None, Some(inherited_clip)) => {
50-
commands.entity(entity).insert(CalculatedClip {
51-
clip: inherited_clip,
52-
});
53-
}
54-
(Some(mut calculated_clip), Some(inherited_clip)) => {
43+
// Update this node's CalculatedClip component
44+
if let Some(mut calculated_clip) = maybe_calculated_clip {
45+
if let Some(inherited_clip) = maybe_inherited_clip {
46+
// Replace the previous calculated clip with the inherited clipping rect
5547
if calculated_clip.clip != inherited_clip {
5648
*calculated_clip = CalculatedClip {
5749
clip: inherited_clip,
5850
};
5951
}
52+
} else {
53+
// No inherited clipping rect, remove the component
54+
commands.entity(entity).remove::<CalculatedClip>();
6055
}
56+
} else if let Some(inherited_clip) = maybe_inherited_clip {
57+
// No previous calculated clip, add a new CalculatedClip component with the inherited clipping rect
58+
commands.entity(entity).insert(CalculatedClip {
59+
clip: inherited_clip,
60+
});
6161
}
6262

6363
// Calculate new clip rectangle for children nodes
64-
let children_clip = match style.overflow {
64+
let children_clip = if style.overflow.is_visible() {
6565
// When `Visible`, children might be visible even when they are outside
6666
// the current node's boundaries. In this case they inherit the current
6767
// node's parent clip. If an ancestor is set as `Hidden`, that clip will
6868
// be used; otherwise this will be `None`.
69-
Overflow::Visible => maybe_inherited_clip,
70-
Overflow::Hidden => {
71-
let node_clip = node.logical_rect(global_transform);
72-
73-
// If `maybe_inherited_clip` is `Some`, use the intersection between
74-
// current node's clip and the inherited clip. This handles the case
75-
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
76-
// defined, use the current node's clip.
77-
Some(maybe_inherited_clip.map_or(node_clip, |c| c.intersect(node_clip)))
69+
maybe_inherited_clip
70+
} else {
71+
// If `maybe_inherited_clip` is `Some`, use the intersection between
72+
// current node's clip and the inherited clip. This handles the case
73+
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
74+
// defined, use the current node's clip.
75+
let mut node_rect = node.logical_rect(global_transform);
76+
if style.overflow.x == OverflowAxis::Visible {
77+
node_rect.min.x = -f32::INFINITY;
78+
node_rect.max.x = f32::INFINITY;
79+
}
80+
if style.overflow.y == OverflowAxis::Visible {
81+
node_rect.min.y = -f32::INFINITY;
82+
node_rect.max.y = f32::INFINITY;
7883
}
84+
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
7985
};
8086

8187
if let Ok(children) = children_query.get(entity) {

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ Example | Description
334334
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
335335
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
336336
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
337+
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
337338
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
338339
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
339340
[Text](../examples/ui/text.rs) | Illustrates creating and updating text

examples/ui/overflow.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! Simple example demonstrating overflow behavior.
2+
3+
use bevy::{prelude::*, winit::WinitSettings};
4+
5+
fn main() {
6+
App::new()
7+
.add_plugins(DefaultPlugins)
8+
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
9+
.insert_resource(WinitSettings::desktop_app())
10+
.add_systems(Startup, setup)
11+
.run();
12+
}
13+
14+
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
15+
commands.spawn(Camera2dBundle::default());
16+
17+
let text_style = TextStyle {
18+
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
19+
font_size: 20.0,
20+
color: Color::WHITE,
21+
};
22+
23+
let image = asset_server.load("branding/icon.png");
24+
25+
commands
26+
.spawn(NodeBundle {
27+
style: Style {
28+
align_items: AlignItems::Center,
29+
justify_content: JustifyContent::Center,
30+
size: Size::width(Val::Percent(100.)),
31+
..Default::default()
32+
},
33+
background_color: Color::ANTIQUE_WHITE.into(),
34+
..Default::default()
35+
})
36+
.with_children(|parent| {
37+
for overflow in [
38+
Overflow::visible(),
39+
Overflow::clip_x(),
40+
Overflow::clip_y(),
41+
Overflow::clip(),
42+
] {
43+
parent
44+
.spawn(NodeBundle {
45+
style: Style {
46+
flex_direction: FlexDirection::Column,
47+
align_items: AlignItems::Center,
48+
margin: UiRect::horizontal(Val::Px(25.)),
49+
..Default::default()
50+
},
51+
..Default::default()
52+
})
53+
.with_children(|parent| {
54+
let label = format!("{overflow:#?}");
55+
parent
56+
.spawn(NodeBundle {
57+
style: Style {
58+
padding: UiRect::all(Val::Px(10.)),
59+
margin: UiRect::bottom(Val::Px(25.)),
60+
..Default::default()
61+
},
62+
background_color: Color::DARK_GRAY.into(),
63+
..Default::default()
64+
})
65+
.with_children(|parent| {
66+
parent.spawn(TextBundle {
67+
text: Text::from_section(label, text_style.clone()),
68+
..Default::default()
69+
});
70+
});
71+
parent
72+
.spawn(NodeBundle {
73+
style: Style {
74+
size: Size::all(Val::Px(100.)),
75+
padding: UiRect {
76+
left: Val::Px(25.),
77+
top: Val::Px(25.),
78+
..Default::default()
79+
},
80+
overflow,
81+
..Default::default()
82+
},
83+
background_color: Color::GRAY.into(),
84+
..Default::default()
85+
})
86+
.with_children(|parent| {
87+
parent.spawn(ImageBundle {
88+
image: UiImage::new(image.clone()),
89+
style: Style {
90+
min_size: Size::all(Val::Px(100.)),
91+
..Default::default()
92+
},
93+
background_color: Color::WHITE.into(),
94+
..Default::default()
95+
});
96+
});
97+
});
98+
}
99+
});
100+
}

examples/ui/overflow_debug.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ fn spawn_container(
212212
size: Size::new(Val::Px(CONTAINER_SIZE), Val::Px(CONTAINER_SIZE)),
213213
align_items: AlignItems::Center,
214214
justify_content: JustifyContent::Center,
215-
overflow: Overflow::Hidden,
215+
overflow: Overflow::clip(),
216216
..default()
217217
},
218218
background_color: Color::DARK_GRAY.into(),
@@ -278,8 +278,19 @@ fn toggle_overflow(keys: Res<Input<KeyCode>>, mut containers: Query<&mut Style,
278278
if keys.just_pressed(KeyCode::O) {
279279
for mut style in &mut containers {
280280
style.overflow = match style.overflow {
281-
Overflow::Visible => Overflow::Hidden,
282-
Overflow::Hidden => Overflow::Visible,
281+
Overflow {
282+
x: OverflowAxis::Visible,
283+
y: OverflowAxis::Visible,
284+
} => Overflow::clip_y(),
285+
Overflow {
286+
x: OverflowAxis::Visible,
287+
y: OverflowAxis::Clip,
288+
} => Overflow::clip_x(),
289+
Overflow {
290+
x: OverflowAxis::Clip,
291+
y: OverflowAxis::Visible,
292+
} => Overflow::clip(),
293+
_ => Overflow::visible(),
283294
};
284295
}
285296
}

examples/ui/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
112112
flex_direction: FlexDirection::Column,
113113
align_self: AlignSelf::Stretch,
114114
size: Size::height(Val::Percent(50.)),
115-
overflow: Overflow::Hidden,
115+
overflow: Overflow::clip_y(),
116116
..default()
117117
},
118118
background_color: Color::rgb(0.10, 0.10, 0.10).into(),

0 commit comments

Comments
 (0)