Skip to content

Commit

Permalink
Implement the Overflow::Hidden style property for UI (#3296)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
Davier committed Dec 19, 2021
1 parent 9a89295 commit 3409579
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 59 deletions.
3 changes: 2 additions & 1 deletion crates/bevy_sprite/src/rect.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use bevy_math::Vec2;
use bevy_reflect::Reflect;

/// A rectangle defined by two points. There is no defined origin, so 0,0 could be anywhere
/// (top-left, bottom-left, etc)
#[repr(C)]
#[derive(Default, Clone, Copy, Debug)]
#[derive(Default, Clone, Copy, Debug, Reflect)]
pub struct Rect {
/// The beginning point of the rect
pub min: Vec2,
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
use bevy_input::InputSystem;
use bevy_math::{Rect, Size};
use bevy_transform::TransformSystem;
use update::ui_z_system;
use update::{ui_z_system, update_clipping_system};

#[derive(Default)]
pub struct UiPlugin;
Expand Down Expand Up @@ -89,6 +89,10 @@ impl Plugin for UiPlugin {
ui_z_system
.after(UiSystem::Flex)
.before(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_clipping_system.after(TransformSystem::TransformPropagate),
);

crate::render::build_ui_render(app);
Expand Down
128 changes: 93 additions & 35 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ use bevy_render::{
view::ViewUniforms,
RenderApp, RenderStage, RenderWorld,
};
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
use bevy_sprite::{Rect, SpriteAssetEvents, TextureAtlas};
use bevy_text::{DefaultTextPipeline, Text};
use bevy_transform::components::GlobalTransform;
use bevy_utils::HashMap;
use bevy_window::Windows;

use bytemuck::{Pod, Zeroable};

use crate::{Node, UiColor, UiImage};
use crate::{CalculatedClip, Node, UiColor, UiImage};

pub mod node {
pub const UI_PASS_DRIVER: &str = "ui_pass_driver";
Expand Down Expand Up @@ -120,9 +120,10 @@ pub fn build_ui_render(app: &mut App) {
pub struct ExtractedUiNode {
pub transform: Mat4,
pub color: Color,
pub rect: bevy_sprite::Rect,
pub rect: Rect,
pub image: Handle<Image>,
pub atlas_size: Option<Vec2>,
pub clip: Option<Rect>,
}

#[derive(Default)]
Expand All @@ -133,11 +134,17 @@ pub struct ExtractedUiNodes {
pub fn extract_uinodes(
mut render_world: ResMut<RenderWorld>,
images: Res<Assets<Image>>,
uinode_query: Query<(&Node, &GlobalTransform, &UiColor, &UiImage)>,
uinode_query: Query<(
&Node,
&GlobalTransform,
&UiColor,
&UiImage,
Option<&CalculatedClip>,
)>,
) {
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();
extracted_uinodes.uinodes.clear();
for (uinode, transform, color, image) in uinode_query.iter() {
for (uinode, transform, color, image, clip) in uinode_query.iter() {
let image = image.0.clone_weak();
// Skip loading images
if !images.contains(image.clone_weak()) {
Expand All @@ -152,6 +159,7 @@ pub fn extract_uinodes(
},
image,
atlas_size: None,
clip: clip.map(|clip| clip.clip),
});
}
}
Expand All @@ -161,7 +169,13 @@ pub fn extract_text_uinodes(
texture_atlases: Res<Assets<TextureAtlas>>,
text_pipeline: Res<DefaultTextPipeline>,
windows: Res<Windows>,
uinode_query: Query<(Entity, &Node, &GlobalTransform, &Text)>,
uinode_query: Query<(
Entity,
&Node,
&GlobalTransform,
&Text,
Option<&CalculatedClip>,
)>,
) {
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();

Expand All @@ -171,7 +185,7 @@ pub fn extract_text_uinodes(
1.
};

for (entity, uinode, transform, text) in uinode_query.iter() {
for (entity, uinode, transform, text, clip) in uinode_query.iter() {
// Skip if size is set to zero (e.g. when a parent is set to `Display::None`)
if uinode.size == Vec2::ZERO {
continue;
Expand Down Expand Up @@ -203,6 +217,7 @@ pub fn extract_text_uinodes(
rect,
image: texture,
atlas_size,
clip: clip.map(|clip| clip.clip),
});
}
}
Expand Down Expand Up @@ -231,15 +246,15 @@ impl Default for UiMeta {
}
}

const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
const_vec3!([-0.5, -0.5, 0.0]),
const_vec3!([0.5, 0.5, 0.0]),
const_vec3!([-0.5, 0.5, 0.0]),
const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [
const_vec3!([-0.5, -0.5, 0.0]),
const_vec3!([0.5, -0.5, 0.0]),
const_vec3!([0.5, 0.5, 0.0]),
const_vec3!([-0.5, 0.5, 0.0]),
];

const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2];

#[derive(Component)]
pub struct UiBatch {
pub range: Range<u32>,
Expand Down Expand Up @@ -279,47 +294,90 @@ pub fn prepare_uinodes(
}

let uinode_rect = extracted_uinode.rect;
let rect_size = uinode_rect.size().extend(1.0);

// Specify the corners of the node
let mut bottom_left = Vec2::new(uinode_rect.min.x, uinode_rect.max.y);
let mut top_left = uinode_rect.min;
let mut top_right = Vec2::new(uinode_rect.max.x, uinode_rect.min.y);
let mut bottom_right = uinode_rect.max;
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (extracted_uinode.transform * (pos * rect_size).extend(1.)).xyz());

// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
let positions_diff = if let Some(clip) = extracted_uinode.clip {
[
Vec2::new(
f32::max(clip.min.x - positions[0].x, 0.),
f32::max(clip.min.y - positions[0].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[1].x, 0.),
f32::max(clip.min.y - positions[1].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[2].x, 0.),
f32::min(clip.max.y - positions[2].y, 0.),
),
Vec2::new(
f32::max(clip.min.x - positions[3].x, 0.),
f32::min(clip.max.y - positions[3].y, 0.),
),
]
} else {
[Vec2::ZERO; 4]
};

let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
bottom_left /= atlas_extent;
bottom_right /= atlas_extent;
top_left /= atlas_extent;
top_right /= atlas_extent;

let uvs: [[f32; 2]; 6] = [
bottom_left.into(),
top_right.into(),
top_left.into(),
bottom_left.into(),
bottom_right.into(),
top_right.into(),
let positions_clipped = [
positions[0] + positions_diff[0].extend(0.),
positions[1] + positions_diff[1].extend(0.),
positions[2] + positions_diff[2].extend(0.),
positions[3] + positions_diff[3].extend(0.),
];

let rect_size = extracted_uinode.rect.size().extend(1.0);
// Cull nodes that are completely clipped
if positions_diff[0].x - positions_diff[1].x >= rect_size.x
|| positions_diff[1].y - positions_diff[2].y >= rect_size.y
{
continue;
}

// Clip UVs (Note: y is reversed in UV space)
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
let uvs = [
Vec2::new(
uinode_rect.min.x + positions_diff[0].x,
uinode_rect.max.y - positions_diff[0].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[1].x,
uinode_rect.max.y - positions_diff[1].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[2].x,
uinode_rect.min.y - positions_diff[2].y,
),
Vec2::new(
uinode_rect.min.x + positions_diff[3].x,
uinode_rect.min.y - positions_diff[3].y,
),
]
.map(|pos| pos / atlas_extent);

let color = extracted_uinode.color.as_linear_rgba_f32();
// encode color as a single u32 to save space
let color = (color[0] * 255.0) as u32
| ((color[1] * 255.0) as u32) << 8
| ((color[2] * 255.0) as u32) << 16
| ((color[3] * 255.0) as u32) << 24;
for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
let mut final_position = *vertex_position * rect_size;
final_position = (extracted_uinode.transform * final_position.extend(1.0)).xyz();

for i in QUAD_INDICES {
ui_meta.vertices.push(UiVertex {
position: final_position.into(),
uv: uvs[index],
position: positions_clipped[i].into(),
uv: uvs[i].into(),
color,
});
}

last_z = extracted_uinode.transform.w_axis[2];
end += QUAD_VERTEX_POSITIONS.len() as u32;
end += QUAD_INDICES.len() as u32;
}

// if start != end, there is one last batch to process
Expand Down
32 changes: 20 additions & 12 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub struct Style {
pub min_size: Size<Val>,
pub max_size: Size<Val>,
pub aspect_ratio: Option<f32>,
pub overflow: Overflow,
}

impl Default for Style {
Expand All @@ -101,6 +102,7 @@ impl Default for Style {
min_size: Size::new(Val::Auto, Val::Auto),
max_size: Size::new(Val::Auto, Val::Auto),
aspect_ratio: Default::default(),
overflow: Default::default(),
}
}
}
Expand Down Expand Up @@ -214,19 +216,19 @@ impl Default for JustifyContent {
}
}

// TODO: add support for overflow settings
// #[derive(Copy, Clone, PartialEq, Debug)]
// pub enum Overflow {
// Visible,
// Hidden,
// Scroll,
// }
#[derive(Copy, Clone, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum Overflow {
Visible,
Hidden,
// Scroll,
}

// impl Default for Overflow {
// fn default() -> Overflow {
// Overflow::Visible
// }
// }
impl Default for Overflow {
fn default() -> Overflow {
Overflow::Visible
}
}

#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -286,3 +288,9 @@ impl From<Handle<Image>> for UiImage {
Self(handle)
}
}

#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct CalculatedClip {
pub clip: bevy_sprite::Rect,
}
Loading

0 comments on commit 3409579

Please sign in to comment.