Skip to content

Commit 3409579

Browse files
committed
Implement the Overflow::Hidden style property for UI (#3296)
# Objective This PR implements the `overflow` style property in `bevy_ui`. When set to `Overflow::Hidden`, the children of that node are clipped so that overflowing parts are not rendered. This is an important building block for UI widgets. ## Solution Clipping is done on the CPU so that it does not break batching. The clip regions update was implemented as a separate system for clarity, but it could be merged with the other UI systems to avoid doing an additional tree traversal. (I don't think it's important until we fix the layout performance issues though). A scrolling list was added to the `ui_pipelined` example to showcase `Overflow::Hidden`. For the sake of simplicity, it can only be scrolled with a mouse.
1 parent 9a89295 commit 3409579

File tree

6 files changed

+320
-59
lines changed

6 files changed

+320
-59
lines changed

crates/bevy_sprite/src/rect.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use bevy_math::Vec2;
2+
use bevy_reflect::Reflect;
23

34
/// A rectangle defined by two points. There is no defined origin, so 0,0 could be anywhere
45
/// (top-left, bottom-left, etc)
56
#[repr(C)]
6-
#[derive(Default, Clone, Copy, Debug)]
7+
#[derive(Default, Clone, Copy, Debug, Reflect)]
78
pub struct Rect {
89
/// The beginning point of the rect
910
pub min: Vec2,

crates/bevy_ui/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
2626
use bevy_input::InputSystem;
2727
use bevy_math::{Rect, Size};
2828
use bevy_transform::TransformSystem;
29-
use update::ui_z_system;
29+
use update::{ui_z_system, update_clipping_system};
3030

3131
#[derive(Default)]
3232
pub struct UiPlugin;
@@ -89,6 +89,10 @@ impl Plugin for UiPlugin {
8989
ui_z_system
9090
.after(UiSystem::Flex)
9191
.before(TransformSystem::TransformPropagate),
92+
)
93+
.add_system_to_stage(
94+
CoreStage::PostUpdate,
95+
update_clipping_system.after(TransformSystem::TransformPropagate),
9296
);
9397

9498
crate::render::build_ui_render(app);

crates/bevy_ui/src/render/mod.rs

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ use bevy_render::{
2626
view::ViewUniforms,
2727
RenderApp, RenderStage, RenderWorld,
2828
};
29-
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
29+
use bevy_sprite::{Rect, SpriteAssetEvents, TextureAtlas};
3030
use bevy_text::{DefaultTextPipeline, Text};
3131
use bevy_transform::components::GlobalTransform;
3232
use bevy_utils::HashMap;
3333
use bevy_window::Windows;
3434

3535
use bytemuck::{Pod, Zeroable};
3636

37-
use crate::{Node, UiColor, UiImage};
37+
use crate::{CalculatedClip, Node, UiColor, UiImage};
3838

3939
pub mod node {
4040
pub const UI_PASS_DRIVER: &str = "ui_pass_driver";
@@ -120,9 +120,10 @@ pub fn build_ui_render(app: &mut App) {
120120
pub struct ExtractedUiNode {
121121
pub transform: Mat4,
122122
pub color: Color,
123-
pub rect: bevy_sprite::Rect,
123+
pub rect: Rect,
124124
pub image: Handle<Image>,
125125
pub atlas_size: Option<Vec2>,
126+
pub clip: Option<Rect>,
126127
}
127128

128129
#[derive(Default)]
@@ -133,11 +134,17 @@ pub struct ExtractedUiNodes {
133134
pub fn extract_uinodes(
134135
mut render_world: ResMut<RenderWorld>,
135136
images: Res<Assets<Image>>,
136-
uinode_query: Query<(&Node, &GlobalTransform, &UiColor, &UiImage)>,
137+
uinode_query: Query<(
138+
&Node,
139+
&GlobalTransform,
140+
&UiColor,
141+
&UiImage,
142+
Option<&CalculatedClip>,
143+
)>,
137144
) {
138145
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();
139146
extracted_uinodes.uinodes.clear();
140-
for (uinode, transform, color, image) in uinode_query.iter() {
147+
for (uinode, transform, color, image, clip) in uinode_query.iter() {
141148
let image = image.0.clone_weak();
142149
// Skip loading images
143150
if !images.contains(image.clone_weak()) {
@@ -152,6 +159,7 @@ pub fn extract_uinodes(
152159
},
153160
image,
154161
atlas_size: None,
162+
clip: clip.map(|clip| clip.clip),
155163
});
156164
}
157165
}
@@ -161,7 +169,13 @@ pub fn extract_text_uinodes(
161169
texture_atlases: Res<Assets<TextureAtlas>>,
162170
text_pipeline: Res<DefaultTextPipeline>,
163171
windows: Res<Windows>,
164-
uinode_query: Query<(Entity, &Node, &GlobalTransform, &Text)>,
172+
uinode_query: Query<(
173+
Entity,
174+
&Node,
175+
&GlobalTransform,
176+
&Text,
177+
Option<&CalculatedClip>,
178+
)>,
165179
) {
166180
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();
167181

@@ -171,7 +185,7 @@ pub fn extract_text_uinodes(
171185
1.
172186
};
173187

174-
for (entity, uinode, transform, text) in uinode_query.iter() {
188+
for (entity, uinode, transform, text, clip) in uinode_query.iter() {
175189
// Skip if size is set to zero (e.g. when a parent is set to `Display::None`)
176190
if uinode.size == Vec2::ZERO {
177191
continue;
@@ -203,6 +217,7 @@ pub fn extract_text_uinodes(
203217
rect,
204218
image: texture,
205219
atlas_size,
220+
clip: clip.map(|clip| clip.clip),
206221
});
207222
}
208223
}
@@ -231,15 +246,15 @@ impl Default for UiMeta {
231246
}
232247
}
233248

234-
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
235-
const_vec3!([-0.5, -0.5, 0.0]),
236-
const_vec3!([0.5, 0.5, 0.0]),
237-
const_vec3!([-0.5, 0.5, 0.0]),
249+
const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [
238250
const_vec3!([-0.5, -0.5, 0.0]),
239251
const_vec3!([0.5, -0.5, 0.0]),
240252
const_vec3!([0.5, 0.5, 0.0]),
253+
const_vec3!([-0.5, 0.5, 0.0]),
241254
];
242255

256+
const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2];
257+
243258
#[derive(Component)]
244259
pub struct UiBatch {
245260
pub range: Range<u32>,
@@ -279,47 +294,90 @@ pub fn prepare_uinodes(
279294
}
280295

281296
let uinode_rect = extracted_uinode.rect;
297+
let rect_size = uinode_rect.size().extend(1.0);
282298

283299
// Specify the corners of the node
284-
let mut bottom_left = Vec2::new(uinode_rect.min.x, uinode_rect.max.y);
285-
let mut top_left = uinode_rect.min;
286-
let mut top_right = Vec2::new(uinode_rect.max.x, uinode_rect.min.y);
287-
let mut bottom_right = uinode_rect.max;
300+
let positions = QUAD_VERTEX_POSITIONS
301+
.map(|pos| (extracted_uinode.transform * (pos * rect_size).extend(1.)).xyz());
302+
303+
// Calculate the effect of clipping
304+
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
305+
let positions_diff = if let Some(clip) = extracted_uinode.clip {
306+
[
307+
Vec2::new(
308+
f32::max(clip.min.x - positions[0].x, 0.),
309+
f32::max(clip.min.y - positions[0].y, 0.),
310+
),
311+
Vec2::new(
312+
f32::min(clip.max.x - positions[1].x, 0.),
313+
f32::max(clip.min.y - positions[1].y, 0.),
314+
),
315+
Vec2::new(
316+
f32::min(clip.max.x - positions[2].x, 0.),
317+
f32::min(clip.max.y - positions[2].y, 0.),
318+
),
319+
Vec2::new(
320+
f32::max(clip.min.x - positions[3].x, 0.),
321+
f32::min(clip.max.y - positions[3].y, 0.),
322+
),
323+
]
324+
} else {
325+
[Vec2::ZERO; 4]
326+
};
288327

289-
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
290-
bottom_left /= atlas_extent;
291-
bottom_right /= atlas_extent;
292-
top_left /= atlas_extent;
293-
top_right /= atlas_extent;
294-
295-
let uvs: [[f32; 2]; 6] = [
296-
bottom_left.into(),
297-
top_right.into(),
298-
top_left.into(),
299-
bottom_left.into(),
300-
bottom_right.into(),
301-
top_right.into(),
328+
let positions_clipped = [
329+
positions[0] + positions_diff[0].extend(0.),
330+
positions[1] + positions_diff[1].extend(0.),
331+
positions[2] + positions_diff[2].extend(0.),
332+
positions[3] + positions_diff[3].extend(0.),
302333
];
303334

304-
let rect_size = extracted_uinode.rect.size().extend(1.0);
335+
// Cull nodes that are completely clipped
336+
if positions_diff[0].x - positions_diff[1].x >= rect_size.x
337+
|| positions_diff[1].y - positions_diff[2].y >= rect_size.y
338+
{
339+
continue;
340+
}
341+
342+
// Clip UVs (Note: y is reversed in UV space)
343+
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
344+
let uvs = [
345+
Vec2::new(
346+
uinode_rect.min.x + positions_diff[0].x,
347+
uinode_rect.max.y - positions_diff[0].y,
348+
),
349+
Vec2::new(
350+
uinode_rect.max.x + positions_diff[1].x,
351+
uinode_rect.max.y - positions_diff[1].y,
352+
),
353+
Vec2::new(
354+
uinode_rect.max.x + positions_diff[2].x,
355+
uinode_rect.min.y - positions_diff[2].y,
356+
),
357+
Vec2::new(
358+
uinode_rect.min.x + positions_diff[3].x,
359+
uinode_rect.min.y - positions_diff[3].y,
360+
),
361+
]
362+
.map(|pos| pos / atlas_extent);
363+
305364
let color = extracted_uinode.color.as_linear_rgba_f32();
306365
// encode color as a single u32 to save space
307366
let color = (color[0] * 255.0) as u32
308367
| ((color[1] * 255.0) as u32) << 8
309368
| ((color[2] * 255.0) as u32) << 16
310369
| ((color[3] * 255.0) as u32) << 24;
311-
for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
312-
let mut final_position = *vertex_position * rect_size;
313-
final_position = (extracted_uinode.transform * final_position.extend(1.0)).xyz();
370+
371+
for i in QUAD_INDICES {
314372
ui_meta.vertices.push(UiVertex {
315-
position: final_position.into(),
316-
uv: uvs[index],
373+
position: positions_clipped[i].into(),
374+
uv: uvs[i].into(),
317375
color,
318376
});
319377
}
320378

321379
last_z = extracted_uinode.transform.w_axis[2];
322-
end += QUAD_VERTEX_POSITIONS.len() as u32;
380+
end += QUAD_INDICES.len() as u32;
323381
}
324382

325383
// if start != end, there is one last batch to process

crates/bevy_ui/src/ui_node.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub struct Style {
7676
pub min_size: Size<Val>,
7777
pub max_size: Size<Val>,
7878
pub aspect_ratio: Option<f32>,
79+
pub overflow: Overflow,
7980
}
8081

8182
impl Default for Style {
@@ -101,6 +102,7 @@ impl Default for Style {
101102
min_size: Size::new(Val::Auto, Val::Auto),
102103
max_size: Size::new(Val::Auto, Val::Auto),
103104
aspect_ratio: Default::default(),
105+
overflow: Default::default(),
104106
}
105107
}
106108
}
@@ -214,19 +216,19 @@ impl Default for JustifyContent {
214216
}
215217
}
216218

217-
// TODO: add support for overflow settings
218-
// #[derive(Copy, Clone, PartialEq, Debug)]
219-
// pub enum Overflow {
220-
// Visible,
221-
// Hidden,
222-
// Scroll,
223-
// }
219+
#[derive(Copy, Clone, PartialEq, Debug, Reflect, Serialize, Deserialize)]
220+
#[reflect_value(PartialEq, Serialize, Deserialize)]
221+
pub enum Overflow {
222+
Visible,
223+
Hidden,
224+
// Scroll,
225+
}
224226

225-
// impl Default for Overflow {
226-
// fn default() -> Overflow {
227-
// Overflow::Visible
228-
// }
229-
// }
227+
impl Default for Overflow {
228+
fn default() -> Overflow {
229+
Overflow::Visible
230+
}
231+
}
230232

231233
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
232234
#[reflect_value(PartialEq, Serialize, Deserialize)]
@@ -286,3 +288,9 @@ impl From<Handle<Image>> for UiImage {
286288
Self(handle)
287289
}
288290
}
291+
292+
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
293+
#[reflect(Component)]
294+
pub struct CalculatedClip {
295+
pub clip: bevy_sprite::Rect,
296+
}

0 commit comments

Comments
 (0)