diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index aca0c10b016a1..fa49fc46b5ee4 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -321,6 +321,82 @@ impl UiRect { ..Default::default() } } + + /// Returns the [`UiRect`] with its `left` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Vw(25.)).with_left(Val::Px(10.); + /// assert_eq!(ui_rect.left, Val::Vw(10.)); + /// assert_eq!(ui_rect.right, Val::Px(25.)); + /// assert_eq!(ui_rect.top, Val::Vw(25.)); + /// assert_eq!(ui_rect.bottom, Val::Vw(25.0)); + /// ``` + #[inline] + pub fn with_left(mut self, left: Val) -> UiRect { + self.left = left; + self + } + + /// Returns the [`UiRect`] with its `right` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Vw(25.)).with_right(Val::Px(10.); + /// assert_eq!(ui_rect.left, Val::Vw(25.)); + /// assert_eq!(ui_rect.right, Val::Px(10.)); + /// assert_eq!(ui_rect.top, Val::Vw(25.)); + /// assert_eq!(ui_rect.bottom, Val::Vw(25.0)); + /// ``` + #[inline] + pub fn with_right(mut self, right: Val) -> UiRect { + self.right = right; + self + } + + /// Returns the [`UiRect`] with its `top` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Vw(25.)).with_top(Val::Px(10.); + /// assert_eq!(ui_rect.left, Val::Vw(25.)); + /// assert_eq!(ui_rect.right, Val::Vw(25.)); + /// assert_eq!(ui_rect.top, Val::Px(10.)); + /// assert_eq!(ui_rect.bottom, Val::Vw(25.0)); + /// ``` + #[inline] + pub fn with_top(mut self, top: Val) -> UiRect { + self.top = top; + self + } + + /// Returns the [`UiRect`] with its `bottom` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Vw(25.)).with_bottom(Val::Px(10.); + /// assert_eq!(ui_rect.left, Val::Vw(25.)); + /// assert_eq!(ui_rect.right, Val::Vw(25.)); + /// assert_eq!(ui_rect.top, Val::Vw(25.)); + /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); + /// ``` + #[inline] + pub fn with_bottom(mut self, bottom: Val) -> UiRect { + self.bottom = bottom; + self + } } impl Default for UiRect { diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index d3fe37679849d..0a8ccfd4ce9d1 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -465,6 +465,7 @@ mod tests { ], grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), + border_radius: Default::default(), }; let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); let taffy_style = from_style(&viewport_values, &bevy_style); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index d5669288c3db4..d77c91ea728a8 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -117,6 +117,8 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index a8c82a4828893..14a3f97da0342 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -87,6 +87,8 @@ pub struct ImageBundle { /// /// Combines with `UiImage` to tint the provided image. pub background_color: BackgroundColor, + /// The color of the Node's border + pub border_color: BorderColor, /// The image of the node pub image: UiImage, /// The size of the image in pixels diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 66fa5e15be7be..52b5a95c3ad0f 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,15 +8,15 @@ use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; -use crate::UiTextureAtlasImage; use crate::{ prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack, }; -use crate::{ContentSize, Style, Val}; +use crate::{ContentSize, Style, UiShadow, Val}; +use crate::{UiBorderRadius, UiScale, UiTextureAtlasImage}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; use bevy_ecs::prelude::*; -use bevy_math::{Mat4, Rect, UVec4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_reflect::TypeUuid; use bevy_render::texture::DEFAULT_IMAGE_HANDLE; use bevy_render::{ @@ -58,6 +58,7 @@ pub const UI_SHADER_HANDLE: HandleUntyped = #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystem { ExtractNode, + ExtractAtlasNode, } pub fn build_ui_render(app: &mut App) { @@ -81,7 +82,6 @@ pub fn build_ui_render(app: &mut App) { extract_default_ui_camera_view::, extract_default_ui_camera_view::, extract_uinodes.in_set(RenderUiSystem::ExtractNode), - extract_atlas_uinodes.after(RenderUiSystem::ExtractNode), extract_uinode_borders.after(RenderUiSystem::ExtractNode), #[cfg(feature = "bevy_text")] extract_text_uinodes.after(RenderUiSystem::ExtractNode), @@ -149,6 +149,13 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { ui_graph } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NodeType { + Rect, + Shadow, + Border, +} + pub struct ExtractedUiNode { pub stack_index: usize, pub transform: Mat4, @@ -159,6 +166,9 @@ pub struct ExtractedUiNode { pub clip: Option, pub flip_x: bool, pub flip_y: bool, + pub border_radius: [f32; 4], + pub border: [f32; 4], + pub node_type: NodeType, } #[derive(Resource, Default)] @@ -166,98 +176,257 @@ pub struct ExtractedUiNodes { pub uinodes: Vec, } -pub fn extract_atlas_uinodes( +#[inline] +fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { + match value { + Val::Auto => 0., + Val::Px(px) => px.max(0.), + Val::Percent(percent) => (parent_width * percent / 100.).max(0.), + Val::Vw(percent) => (viewport_size.x * percent / 100.).max(0.), + Val::Vh(percent) => (viewport_size.y * percent / 100.).max(0.), + Val::VMin(percent) => (viewport_size.min_element() * percent / 100.).max(0.), + Val::VMax(percent) => (viewport_size.max_element() * percent / 100.).max(0.), + } +} + +#[inline] +fn resolve_border_radius( + &values: &UiBorderRadius, + node_size: Vec2, + viewport_size: Vec2, + ui_scale: f64, +) -> [f32; 4] { + let max_radius = 0.5 * node_size.min_element() * ui_scale as f32; + <[Val; 4]>::from(values).map(|value| { + match value { + Val::Auto => 0., + Val::Px(px) => ui_scale as f32 * px, + Val::Percent(percent) => node_size.min_element() * percent / 100., + Val::Vw(percent) => viewport_size.x * percent / 100., + Val::Vh(percent) => viewport_size.y * percent / 100., + Val::VMin(percent) => viewport_size.min_element() * percent / 100., + Val::VMax(percent) => viewport_size.max_element() * percent / 100., + } + .clamp(0., max_radius) + }) +} + +#[inline] +fn resolve_shadow_offset( + x: Val, + y: Val, + node_size: Vec2, + viewport_size: Vec2, + ui_scale: f64, +) -> Vec2 { + [(x, node_size.x), (y, node_size.y)] + .map(|(value, size)| match value { + Val::Auto => 0., + Val::Px(px) => ui_scale as f32 * px, + Val::Percent(percent) => percent / 100. * size, + Val::Vw(percent) => viewport_size.x * percent / 100., + Val::Vh(percent) => viewport_size.y * percent / 100., + Val::VMin(percent) => viewport_size.min_element() * percent / 100., + Val::VMax(percent) => viewport_size.max_element() * percent / 100., + }) + .into() +} + +#[inline] +fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { + let s = 0.5 * size + offset; + let sm = s.x.min(s.y); + return r.min(sm); +} + +#[inline] +fn clamp_radius( + [top_left, top_right, bottom_right, bottom_left]: [f32; 4], + size: Vec2, + border: Vec4, +) -> [f32; 4] { + let s = size - border.xy() - border.zw(); + [ + clamp_corner(top_left, s, border.xy()), + clamp_corner(top_right, s, border.zy()), + clamp_corner(bottom_right, s, border.zw()), + clamp_corner(bottom_left, s, border.xw()), + ] +} + +pub fn extract_uinodes( mut extracted_uinodes: ResMut, + windows: Extract>>, images: Extract>>, texture_atlases: Extract>>, - + ui_scale: Extract>, ui_stack: Extract>, uinode_query: Extract< - Query< - ( - &Node, - &GlobalTransform, - &BackgroundColor, - &ComputedVisibility, - Option<&CalculatedClip>, - &Handle, - &UiTextureAtlasImage, - ), - Without, - >, + Query<( + &Node, + &GlobalTransform, + &Style, + &BackgroundColor, + &ComputedVisibility, + Option<&CalculatedClip>, + Option<&UiShadow>, + Option<&UiImage>, + Option<&Handle>, + Option<&UiTextureAtlasImage>, + )>, >, + parent_node_query: Extract>>, ) { + extracted_uinodes.uinodes.clear(); + + let viewport_size = windows + .get_single() + .map(|window| Vec2::new(window.resolution.width(), window.resolution.height())) + .unwrap_or(Vec2::ZERO) + * ui_scale.scale as f32; + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, transform, color, visibility, clip, texture_atlas_handle, atlas_image)) = - uinode_query.get(*entity) + if let Ok(( + uinode, + global_transform, + style, + background_color, + visibility, + clip, + maybe_shadow, + maybe_image, + maybe_atlas, + maybe_atlas_image, + )) = uinode_query.get(*entity) { - // Skip invisible and completely transparent nodes - if !visibility.is_visible() || color.0.a() == 0.0 { + if !visibility.is_visible() || uinode.size().x <= 0. || uinode.size().y <= 0. { continue; } - let (mut atlas_rect, mut atlas_size, image) = - if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let (image, flip_x, flip_y, atlas_size, rect) = if let Some(image) = maybe_image { + // Skip loading images + let texture = if !images.contains(&image.texture) { + DEFAULT_IMAGE_HANDLE.typed() + } else { + image.texture.clone_weak() + }; + ( + texture, + image.flip_x, + image.flip_y, + None, + Rect { + max: uinode.size(), + ..Default::default() + }, + ) + } else if let (Some(texture_atlas_handle), Some(atlas_image)) = + (maybe_atlas, maybe_atlas_image) + { + texture_atlases.get(texture_atlas_handle) + .map_or( + (DEFAULT_IMAGE_HANDLE.typed(), false, false, None, Rect { max: uinode.size(), ..Default::default() }), + |texture_atlas| { let atlas_rect = *texture_atlas - .textures - .get(atlas_image.index) - .unwrap_or_else(|| { - panic!( - "Atlas index {:?} does not exist for texture atlas handle {:?}.", - atlas_image.index, - texture_atlas_handle.id(), - ) - }); + .textures + .get(atlas_image.index) + .unwrap_or_else(|| { + panic!( + "Atlas index {:?} does not exist for texture atlas handle {:?}.", + atlas_image.index, + texture_atlas_handle.id(), + ) + }); + let scale = uinode.size() / atlas_rect.size(); ( - atlas_rect, - texture_atlas.size, texture_atlas.texture.clone(), - ) - } else { - // Atlas not present in assets resource (should this warn the user?) - continue; - }; + atlas_image.flip_x, + atlas_image.flip_y, + Some(texture_atlas.size * scale), + Rect { + min: atlas_rect.min * scale, + max: atlas_rect.max * scale, + }, + )}) + } else { + ( + DEFAULT_IMAGE_HANDLE.typed(), + false, + false, + None, + Rect { + max: uinode.size(), + ..Default::default() + }, + ) + }; - // Skip loading images - if !images.contains(&image) { - continue; - } + let transform = global_transform.compute_matrix(); - let scale = uinode.size() / atlas_rect.size(); - atlas_rect.min *= scale; - atlas_rect.max *= scale; - atlas_size *= scale; + let border_radius = resolve_border_radius( + &style.border_radius, + uinode.calculated_size, + viewport_size, + ui_scale.scale, + ); - extracted_uinodes.uinodes.push(ExtractedUiNode { + if let Some(shadow) = maybe_shadow { + let shadow_blur_radius = 10.; + let offset = resolve_shadow_offset( + shadow.x_offset, + shadow.y_offset, + uinode.size(), + viewport_size, + ui_scale.scale, + ); + let extracted_node = ExtractedUiNode { + stack_index, + transform: transform * Mat4::from_translation(offset.extend(0.)), + color: shadow.color, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size() * shadow.scale + Vec2::splat(2. * shadow_blur_radius), + }, + image: image.clone_weak(), + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x, + flip_y, + border_radius, + border: [ + shadow_blur_radius, + shadow_blur_radius, + shadow_blur_radius, + shadow_blur_radius, + ], + node_type: NodeType::Shadow, + }; + extracted_uinodes.uinodes.push(extracted_node); + } + + let extracted_node = ExtractedUiNode { stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: atlas_rect, + transform, + color: background_color.0, + rect, + image: image.clone_weak(), + atlas_size, clip: clip.map(|clip| clip.clip), - image, - atlas_size: Some(atlas_size), - flip_x: atlas_image.flip_x, - flip_y: atlas_image.flip_y, - }); + flip_x, + flip_y, + border_radius, + border: [0.; 4], + node_type: NodeType::Rect, + }; + extracted_uinodes.uinodes.push(extracted_node); } } } -fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { - match value { - Val::Auto => 0., - Val::Px(px) => px.max(0.), - Val::Percent(percent) => (parent_width * percent / 100.).max(0.), - Val::Vw(percent) => (viewport_size.x * percent / 100.).max(0.), - Val::Vh(percent) => (viewport_size.y * percent / 100.).max(0.), - Val::VMin(percent) => (viewport_size.min_element() * percent / 100.).max(0.), - Val::VMax(percent) => (viewport_size.max_element() * percent / 100.).max(0.), - } -} - pub fn extract_uinode_borders( mut extracted_uinodes: ResMut, windows: Extract>>, + ui_scale: Extract>, ui_stack: Extract>, uinode_query: Extract< Query< @@ -280,17 +449,18 @@ pub fn extract_uinode_borders( let viewport_size = windows .get_single() .map(|window| Vec2::new(window.resolution.width(), window.resolution.height())) - .unwrap_or(Vec2::ZERO); + .unwrap_or(Vec2::ZERO) + * ui_scale.scale as f32; for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((node, global_transform, style, border_color, parent, visibility, clip)) = + if let Ok((uinode, global_transform, style, border_color, parent, visibility, clip)) = uinode_query.get(*entity) { // Skip invisible borders if !visibility.is_visible() || border_color.0.a() == 0.0 - || node.size().x <= 0. - || node.size().y <= 0. + || uinode.size().x <= 0. + || uinode.size().y <= 0. { continue; } @@ -306,114 +476,44 @@ pub fn extract_uinode_borders( let top = resolve_border_thickness(style.border.top, parent_width, viewport_size); let bottom = resolve_border_thickness(style.border.bottom, parent_width, viewport_size); - // Calculate the border rects, ensuring no overlap. - // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. - let max = 0.5 * node.size(); - let min = -max; - let inner_min = min + Vec2::new(left, top); - let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); - let border_rects = [ - // Left border - Rect { - min, - max: Vec2::new(inner_min.x, max.y), - }, - // Right border - Rect { - min: Vec2::new(inner_max.x, min.y), - max, - }, - // Top border - Rect { - min: Vec2::new(inner_min.x, min.y), - max: Vec2::new(inner_max.x, inner_min.y), - }, - // Bottom border - Rect { - min: Vec2::new(inner_min.x, inner_max.y), - max: Vec2::new(inner_max.x, max.y), - }, - ]; + let border = [left, top, right, bottom]; let transform = global_transform.compute_matrix(); - for edge in border_rects { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0, - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image: image.clone_weak(), - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - }); - } - } - } - } -} - -pub fn extract_uinodes( - mut extracted_uinodes: ResMut, - images: Extract>>, - ui_stack: Extract>, - uinode_query: Extract< - Query< - ( - &Node, - &GlobalTransform, - &BackgroundColor, - Option<&UiImage>, - &ComputedVisibility, - Option<&CalculatedClip>, - ), - Without, - >, - >, -) { - extracted_uinodes.uinodes.clear(); - - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, transform, color, maybe_image, visibility, clip)) = - uinode_query.get(*entity) - { - // Skip invisible and completely transparent nodes - if !visibility.is_visible() || color.0.a() == 0.0 { - continue; - } + let border_radius = resolve_border_radius( + &style.border_radius, + uinode.calculated_size, + viewport_size, + ui_scale.scale, + ); - let (image, flip_x, flip_y) = if let Some(image) = maybe_image { - // Skip loading images - if !images.contains(&image.texture) { - continue; - } - (image.texture.clone_weak(), image.flip_x, image.flip_y) - } else { - (DEFAULT_IMAGE_HANDLE.typed().clone_weak(), false, false) - }; + let border_radius = clamp_radius(border_radius, uinode.size(), border.into()); + let transform = global_transform.compute_matrix(); extracted_uinodes.uinodes.push(ExtractedUiNode { stack_index, - transform: transform.compute_matrix(), - color: color.0, + // This translates the uinode's transform to the center of the current border rectangle + transform, + color: border_color.0, rect: Rect { min: Vec2::ZERO, - max: uinode.calculated_size, + max: uinode.size(), }, - clip: clip.map(|clip| clip.clip), - image, + image: image.clone_weak(), atlas_size: None, - flip_x, - flip_y, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + border_radius: resolve_border_radius( + &style.border_radius, + uinode.calculated_size, + viewport_size, + ui_scale.scale, + ), + border: [left, top, right, bottom], + node_type: NodeType::Border, }); - }; + } } } @@ -540,6 +640,9 @@ pub fn extract_text_uinodes( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, + border_radius: [0.; 4], + border: [0.; 4], + node_type: NodeType::Rect, }); } } @@ -547,17 +650,21 @@ pub fn extract_text_uinodes( } #[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], pub color: [f32; 4], - pub mode: u32, + pub flags: u32, + pub radius: [f32; 4], + pub border: [f32; 4], + pub size: [f32; 2], } #[derive(Resource)] pub struct UiMeta { vertices: BufferVec, + indices: BufferVec, view_bind_group: Option, } @@ -565,6 +672,7 @@ impl Default for UiMeta { fn default() -> Self { Self { vertices: BufferVec::new(BufferUsages::VERTEX), + indices: BufferVec::new(BufferUsages::INDEX), view_bind_group: None, } } @@ -586,8 +694,24 @@ pub struct UiBatch { pub z: f32, } -const TEXTURED_QUAD: u32 = 0; -const UNTEXTURED_QUAD: u32 = 1; +/// The values here should match the values for the constants in `ui.wgsl` +pub mod shader_flags { + pub const UNTEXTURED: u32 = 0; + pub const TEXTURED: u32 = 1; + pub const BOX_SHADOW: u32 = 2; + pub const DISABLE_AA: u32 = 4; + pub const CORNERS: [u32; 4] = [ + // top left + 0, + // top right + 8, + // bottom right + 8 | 16, + // bottom left + 16, + ]; + pub const BORDER: u32 = 32; +} pub fn prepare_uinodes( mut commands: Commands, @@ -597,6 +721,7 @@ pub fn prepare_uinodes( mut extracted_uinodes: ResMut, ) { ui_meta.vertices.clear(); + ui_meta.indices.clear(); // sort by ui stack index, starting from the deepest node extracted_uinodes @@ -607,14 +732,12 @@ pub fn prepare_uinodes( let mut end = 0; let mut current_batch_image = DEFAULT_IMAGE_HANDLE.typed(); let mut last_z = 0.0; + let mut vertex_index = 0; - #[inline] - fn is_textured(image: &Handle) -> bool { - image.id() != DEFAULT_IMAGE_HANDLE.id() - } + let is_textured = |image: &Handle| image.id() != DEFAULT_IMAGE_HANDLE.id(); for extracted_uinode in &extracted_uinodes.uinodes { - let mode = if is_textured(&extracted_uinode.image) { + let mut flags = if is_textured(&extracted_uinode.image) { if current_batch_image.id() != extracted_uinode.image.id() { if is_textured(¤t_batch_image) && start != end { commands.spawn(UiBatch { @@ -626,11 +749,11 @@ pub fn prepare_uinodes( } current_batch_image = extracted_uinode.image.clone_weak(); } - TEXTURED_QUAD + shader_flags::TEXTURED } else { // Untextured `UiBatch`es are never spawned within the loop. // If all the `extracted_uinodes` are untextured a single untextured UiBatch will be spawned after the loop terminates. - UNTEXTURED_QUAD + shader_flags::UNTEXTURED }; let mut uinode_rect = extracted_uinode.rect; @@ -689,57 +812,74 @@ pub fn prepare_uinodes( continue; } } - let uvs = if mode == UNTEXTURED_QUAD { - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] - } else { - let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); - if extracted_uinode.flip_x { - std::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); - positions_diff[0].x *= -1.; - positions_diff[1].x *= -1.; - positions_diff[2].x *= -1.; - positions_diff[3].x *= -1.; - } - if extracted_uinode.flip_y { - std::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); - positions_diff[0].y *= -1.; - positions_diff[1].y *= -1.; - positions_diff[2].y *= -1.; - positions_diff[3].y *= -1.; + let uvs = { + if flags == shader_flags::UNTEXTURED { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + } else { + let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); + if extracted_uinode.flip_x { + std::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); + positions_diff[0].x *= -1.; + positions_diff[1].x *= -1.; + positions_diff[2].x *= -1.; + positions_diff[3].x *= -1.; + } + if extracted_uinode.flip_y { + std::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); + positions_diff[0].y *= -1.; + positions_diff[1].y *= -1.; + positions_diff[2].y *= -1.; + positions_diff[3].y *= -1.; + } + [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) } - [ - Vec2::new( - uinode_rect.min.x + positions_diff[0].x, - uinode_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[1].x, - uinode_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[2].x, - uinode_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - uinode_rect.min.x + positions_diff[3].x, - uinode_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent) }; let color = extracted_uinode.color.as_linear_rgba_f32(); - for i in QUAD_INDICES { - ui_meta.vertices.push(UiVertex { + match extracted_uinode.node_type { + NodeType::Border => flags |= shader_flags::BORDER, + NodeType::Shadow => flags |= shader_flags::BOX_SHADOW, + _ => {} + } + + for i in 0..4 { + let ui_vertex = UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), color, - mode, - }); + flags: flags | shader_flags::CORNERS[i], + radius: extracted_uinode.border_radius, + border: extracted_uinode.border, + size: transformed_rect_size.xy().into(), + }; + ui_meta.vertices.push(ui_vertex); + } + + for &index in QUAD_INDICES.iter() { + ui_meta.indices.push(vertex_index + index as u32); } + vertex_index += 4; last_z = extracted_uinode.transform.w_axis[2]; - end += QUAD_INDICES.len() as u32; + end += 6; } // if start != end, there is one last batch to process @@ -752,6 +892,7 @@ pub fn prepare_uinodes( } ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); } #[derive(Resource, Default)] diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index f6b4b0cc3c1ea..2c40e58a8bfba 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -79,6 +79,12 @@ impl SpecializedRenderPipeline for UiPipeline { VertexFormat::Float32x4, // mode VertexFormat::Uint32, + // radius + VertexFormat::Float32x4, + // thickness + VertexFormat::Float32x4, + // size + VertexFormat::Float32x2, ], ); let shader_defs = Vec::new(); diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 90e7b6059cecc..19c980ba419e9 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -179,8 +179,14 @@ impl RenderCommand

for DrawUiNode { ui_meta: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); - pass.draw(batch.range.clone(), 0..1); + let ui_meta = ui_meta.into_inner(); + pass.set_vertex_buffer(0, ui_meta.vertices.buffer().unwrap().slice(..)); + pass.set_index_buffer( + ui_meta.indices.buffer().unwrap().slice(..), + 0, + bevy_render::render_resource::IndexFormat::Uint32, + ); + pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success } } diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index 36e18f201604a..466a5c9871f7c 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -1,6 +1,15 @@ #import bevy_render::view View -const TEXTURED_QUAD: u32 = 0u; +const TEXTURED = 1u; +const BOX_SHADOW = 2u; +const DISABLE_AA = 4u; +const RIGHT_VERTEX = 8u; +const BOTTOM_VERTEX = 16u; +const BORDER: u32 = 32u; + +fn enabled(flags: u32, mask: u32) -> bool { + return (flags & mask) != 0u; +} @group(0) @binding(0) var view: View; @@ -8,7 +17,13 @@ var view: View; struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, - @location(3) @interpolate(flat) mode: u32, + @location(2) @interpolate(flat) size: vec2, + @location(3) @interpolate(flat) flags: u32, + @location(4) @interpolate(flat) radius: vec4, + @location(5) @interpolate(flat) border: vec4, + + // position relative to the center of the rectangle + @location(6) point: vec2, @builtin(position) position: vec4, }; @@ -17,29 +32,298 @@ fn vertex( @location(0) vertex_position: vec3, @location(1) vertex_uv: vec2, @location(2) vertex_color: vec4, - @location(3) mode: u32, + @location(3) flags: u32, + + // radius.x = top left radius, .y = top right, .z = bottom right, .w = bottom left + @location(4) radius: vec4, + + // border.x = left width, .y = top, .z = right, .w = bottom + @location(5) border: vec4, + + @location(6) size: vec2, ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; - out.position = view.view_proj * vec4(vertex_position, 1.0); + out.position = view.view_proj * vec4(vertex_position, 1.0); out.color = vertex_color; - out.mode = mode; + out.flags = flags; + out.radius = radius; + out.size = size; + out.border = border; + var point = 0.49999 * size; + if (flags & RIGHT_VERTEX) == 0u { + point.x *= -1.; + } + if (flags & BOTTOM_VERTEX) == 0u { + point.y *= -1.; + } + out.point = point; + return out; } @group(1) @binding(0) var sprite_texture: texture_2d; + @group(1) @binding(1) var sprite_sampler: sampler; -@fragment -fn fragment(in: VertexOutput) -> @location(0) vec4 { - // textureSample can only be called in unform control flow, not inside an if branch. - var color = textureSample(sprite_texture, sprite_sampler, in.uv); - if in.mode == TEXTURED_QUAD { - color = in.color * color; +fn sigmoid(t: f32) -> f32 { + return 1.0 / (1.0 + exp(-t)); +} + +// The returned value is the shortest distance from the given point to the boundary of the rounded box. +// Negative values indicate that the point is inside the rounded box, positive values that the point is outside, and zero is exactly on the boundary. +// arguments +// point -> The function will return the distance from this point to the closest point on the boundary. +// size -> The maximum width and height of the box. +// corner_radii -> The radius of each rounded corner. Ordered counter clockwise starting top left: +// x = top left, y = top right, z = bottom right, w = bottom left. +fn sd_rounded_box(point: vec2, size: vec2, corner_radii: vec4) -> f32 { + // if 0.0 < y then select bottom left (w) and bottom right corner radius (z) + // else select top left (x) and top right corner radius (y) + let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y); + // w and z are swapped so that both pairs are in left to right order, otherwise this second select statement would return the incorrect value for the bottom pair. + let radius = select(rs.x, rs.y, 0.0 < point.x); + // Vector from the corner closest to the point, to the point + let corner_to_point = abs(point) - 0.5 * size; + // Vector from the center of the radius circle to the point + let q = corner_to_point + radius; + // length from center of the radius circle to the point, 0s a component if the point is not within the quadrant of the radius circle that is part of the curved corner. + let l = length(max(q, vec2(0.0))); + let m = min(max(q.x, q.y), 0.0); + return l + m - radius; +} + +fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { + let inner_size = size - inset.xy - inset.zw; + let inner_center = inset.xy + 0.5 * inner_size - 0.5 *size; + let inner_point = point - inner_center; + + var r = radius; + + // top left corner + r.x = r.x - max(inset.x, inset.y); + + // top right corner + r.y = r.y - max(inset.z, inset.y); + + // bottom right corner + r.z = r.z - max(inset.z, inset.w); + + // bottom left corner + r.w = r.w - max(inset.x, inset.w); + + let half_size = inner_size * 0.5; + let min = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min)); + + return sd_rounded_box(inner_point, inner_size, r); +} + +#ifdef CLAMP_INNER_CURVES +fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { + let inner_size = size - inset.xy - inset.zw; + let inner_center = inset.xy + 0.5 * inner_size - 0.5 *size; + let inner_point = point - inner_center; + + var r = radius; + + + if 0. < min(inset.x, inset.y) || inset.x + inset.y <= 0. { + // top left corner + r.x = r.x - max(inset.x, inset.y); + } else { + r.x = 0.; + } + + if 0. < min(inset.z, inset.y) || inset.z + inset.y <= 0.{ + // top right corner + r.y = r.y - max(inset.z, inset.y); + } else { + r.y = 0.; + } + + if 0. < min(inset.z, inset.w) || inset.z + inset.w <= 0. { + // bottom right corner + r.z = r.z - max(inset.z, inset.w); + } else { + r.z = 0.; + } + + if 0. < min(inset.x, inset.w) || inset.x + inset.w <= 0. { + // bottom left corner + r.w = r.w - max(inset.x, inset.w); } else { - color = in.color; + r.w = 0.; } - return color; + + let half_size = inner_size * 0.5; + let min = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min)); + + return sd_rounded_box(inner_point, inner_size, r); +} +#endif + +const RED: vec4 = vec4(1., 0., 0., 1.); +const GREEN: vec4 = vec4(0., 1., 0., 1.); +const BLUE: vec4 = vec4(0., 0., 1., 1.); +const WHITE = vec4(1., 1., 1., 1.); +const BLACK = vec4(0., 0., 0., 1.); + +// draw the border in white, rest of rect black +fn draw_border(in: VertexOutput) -> vec4 { + // Distance from external border. Positive values outside. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Distance from internal border. Positive values inside. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Distance from border, positive values inside border. + let border = max(-internal_distance, external_distance); + + if border < 0.0 { + return WHITE; + } else { + return BLACK; + } +} + +// draw just the interior in white, rest of rect black +fn draw_interior(in: VertexOutput) -> vec4 { + // Distance from external border. Positive values outside. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Distance from internal border. Positive values inside. + // let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Distance from border, positive values inside border. + + if external_distance < 0.0 { + return WHITE; + } else { + return BLACK; + } +} + +// Draw all the geometry +fn draw_test(in: VertexOutput) -> vec4 { + // Distance from external border. Negative inside + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Distance from internal border. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Distance from border. + let border = max(-internal_distance, external_distance); + + // Draw the area outside the border in green + if 0.0 < external_distance { + return GREEN; + } + + // Draw the area inside the border in white + if border < 0.0 { + return WHITE; + } + + // draw the interior in blue + if internal_distance < 0.0 { + return BLUE; + } + + // fill anything else with red (the presence of any red is a bug). + return RED; +} + +fn draw_no_aa(in: VertexOutput) -> vec4 { + let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + // negative value => point inside external border + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + // negative value => point inside internal border + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + // negative value => point inside border + let border = max(external_distance, -internal_distance); + + if enabled(in.flags, BOX_SHADOW) { + // copied from kayak + var rect_dist = 1.0 - sigmoid(sd_rounded_box(in.point,in.size - in.border.x * 2.0, in.radius)); + let color = in.color.rgb; + return vec4(color, in.color.a * rect_dist * 1.42); + } + + if enabled(in.flags, BORDER) { + if border < 0.0 { + return color; + } else { + return vec4(0.0); + } + } + + if external_distance < 0.0 { + return color; + } + + return vec4(0.0); +} + +fn draw(in: VertexOutput) -> vec4 { + let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); + + // Only use the color sampled from the texture if the TEXTURED flag is enabled. + // This allows us to draw both textured and untextured shapes together in the same batch. + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + // Signed distances. The magnitude is the distance of the point from the edge of the shape. + // * Negative values indicate that the point is inside the shape. + // * Zero values indicate the point is on on the edge of the shape. + // * Positive values indicate the point is outside the shape. + + // Signed distance from the exterior boundary. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Signed distance from the border's internal edge (the signed distance is negative if the point is inside the rect but not on the border). + // If the border size is set to zero, this is the same as as the external distance. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Signed distance from the border (the intersection of the rect with its border) + // Points inside the border have negative signed distance. Any point outside the border, whether outside the outside edge, or inside the inner edge have positive signed distance. + let border_distance = max(external_distance, -internal_distance); + + // The fwidth function returns an approximation of the rate of change of the signed distance value + // that is used to ensure that the smooth alpha transition created by smoothstep occurs over a range of distance values + // that is proportional to how quickly the distance is changing. + let fborder = fwidth(border_distance); + let fexternal = fwidth(external_distance); + + if enabled(in.flags, BOX_SHADOW) { + // The item is a shadow + + // copied from kayak + var rect_dist = 1.0 - sigmoid(sd_rounded_box(in.point,in.size - in.border.x * 2.0, in.radius)); + return vec4(in.color.rgb, in.color.a * rect_dist * 1.42); + } + + if enabled(in.flags, BORDER) { + // The item is a border + + // At external edges with no border, border_distance is equal to zero. + // This select statement ensures we only perform anti-aliasing where a non-zero width border is present, otherwise an outline about the external boundary would be drawn even without a border. + let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance); + return vec4(color.rgb, t * color.a); + } + + // The item is a rectangle, draw normally with anti-aliasing at the edges + let t = 1. - smoothstep(0.0, fexternal, external_distance); + return vec4(color.rgb, t * color.a); +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + return draw(in); } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 532007ed3b752..dfbecda993675 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -575,6 +575,38 @@ pub struct Style { /// /// pub grid_column: GridPlacement, + + /// Used to add rounded corners to a UI node. You can set a UI node to have uniformly rounded corners + /// or specify different radii for each corner. If a given radius exceeds half the length of the smallest dimension between the node's height or width, + /// the radius will calculated as half the smallest dimension. + /// + /// Elliptical nodes are not supported yet. Percentage values are based on the node's smallest dimension, either width or height. + /// + /// # Example + /// ``` + /// # use bevy_ui::{Style, UiRect, UiBorderRadius, Val}; + /// let style = Style { + /// // Set a uniform border radius of 10 logical pixels + /// border_radius: UiBorderRadius::all(Val::Px(10.)), + /// ..Default::default() + /// }; + /// let style = Style { + /// border_radius: UiBorderRadius { + /// // The top left corner will be rounded with a radius of 10 logical pixels. + /// top_left: Val::Px(10.), + /// // Percentage values are based on the node's smallest dimension, either width or height. + /// top_right: Val::Percent(20.), + /// // Viewport coordinates can also be used. + /// bottom_left: Val::Vw(10.), + /// // The bottom right corner will be unrounded. + /// ..Default::default() + /// }, + /// ..Default::default() + /// }; + /// ``` + /// + /// + pub border_radius: UiBorderRadius, } impl Style { @@ -617,6 +649,7 @@ impl Style { grid_auto_columns: Vec::new(), grid_column: GridPlacement::DEFAULT, grid_row: GridPlacement::DEFAULT, + border_radius: UiBorderRadius::DEFAULT, }; } @@ -1683,6 +1716,212 @@ impl Default for ZIndex { } } +/// Radii for rounded corner edges. +/// * A corner set to a 0 value will be right angled. +/// * The value is clamped to between 0 and half the length of the shortest side of the node before being used. +/// * `Val::AUTO` is resolved to `Val::Px(0.)`. +#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq)] +pub struct UiBorderRadius { + pub top_left: Val, + pub top_right: Val, + pub bottom_left: Val, + pub bottom_right: Val, +} + +impl Default for UiBorderRadius { + fn default() -> Self { + Self::DEFAULT + } +} + +impl UiBorderRadius { + pub const DEFAULT: Self = Self::ZERO; + + /// Zero curvature. All the corners will be right angled. + pub const ZERO: Self = Self { + top_left: Val::Px(0.), + top_right: Val::Px(0.), + bottom_right: Val::Px(0.), + bottom_left: Val::Px(0.), + }; + + /// Maximum curvature. The Ui Node will take a capsule shape or circular if width and height are equal. + pub const MAX: Self = Self { + top_left: Val::Px(f32::MAX), + top_right: Val::Px(f32::MAX), + bottom_right: Val::Px(f32::MAX), + bottom_left: Val::Px(f32::MAX), + }; + + #[inline] + /// Set all four corners to the same curvature. + pub const fn all(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_left: radius, + bottom_right: radius, + } + } + + #[inline] + pub fn new(top_left: Val, top_right: Val, bottom_right: Val, bottom_left: Val) -> Self { + Self { + top_left, + top_right, + bottom_right, + bottom_left, + } + } + + #[inline] + /// Sets the radii to logical pixel values. + pub fn px(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radii to percentage values. + pub fn percent(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radius for the top left corner. + /// Remaining corners will be right-angled. + pub fn top_left(radius: Val) -> Self { + Self { + top_left: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radius for the top right corner. + /// Remaining corners will be right-angled. + pub fn top_right(radius: Val) -> Self { + Self { + top_right: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radius for the bottom right corner. + /// Remaining corners will be right-angled. + pub fn bottom_right(radius: Val) -> Self { + Self { + bottom_right: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radius for the bottom left corner. + /// Remaining corners will be right-angled. + pub fn bottom_left(radius: Val) -> Self { + Self { + bottom_left: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radii for the top left and bottom left corners. + /// Remaining corners will be right-angled. + pub fn left(radius: Val) -> Self { + Self { + top_left: radius, + bottom_left: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radii for the top right and bottom right corners. + /// Remaining corners will be right-angled. + pub fn right(radius: Val) -> Self { + Self { + top_right: radius, + bottom_right: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radii for the top left and top right corners. + /// Remaining corners will be right-angled. + pub fn top(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + ..Default::default() + } + } + + #[inline] + /// Sets the radii for the bottom left and bottom right corners. + /// Remaining corners will be right-angled. + pub fn bottom(radius: Val) -> Self { + Self { + bottom_left: radius, + bottom_right: radius, + ..Default::default() + } + } +} + +impl From for [Val; 4] { + fn from(value: UiBorderRadius) -> Self { + [ + value.top_left, + value.top_right, + value.bottom_right, + value.bottom_left, + ] + } +} + +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default)] +pub struct UiShadow { + /// x translation of the shadow relative to the uinode + /// + /// percentage values are based on the width of the uinode + pub x_offset: Val, + /// y translation of the shadow relative to the uinode + /// + /// percentage values are based on the height of the uinode + pub y_offset: Val, + /// The size of the uinode is multiplied by `scale` to find the size of the shadow + pub scale: Vec2, + /// The color of the shadow + pub color: Color, +} + +// Probably no really good choices for defaults, might need a resource holding a global shadow default setting. +impl Default for UiShadow { + fn default() -> Self { + Self { + x_offset: Val::Px(16.0), + y_offset: Val::Px(16.0), + scale: Vec2::ONE, + color: Color::BLACK.with_a(0.5), + } + } +} + #[cfg(test)] mod tests { use crate::ValArithmeticError; diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index 08055514bd825..b344827ccaa79 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -399,6 +399,8 @@ mod menu { width: Val::Px(250.0), height: Val::Px(65.0), margin: UiRect::all(Val::Px(20.0)), + border_radius: UiBorderRadius::all(Val::Px(20.)), + border: UiRect::all(Val::Px(5.)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() @@ -437,9 +439,12 @@ mod menu { style: Style { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, + border_radius: UiBorderRadius::all(Val::Percent(10.)), + border: UiRect::all(Val::Px(10.)), ..default() }, background_color: Color::CRIMSON.into(), + border_color: Color::MAROON.into(), ..default() }) .with_children(|parent| { @@ -468,6 +473,7 @@ mod menu { ButtonBundle { style: button_style.clone(), background_color: NORMAL_BUTTON.into(), + border_color: Color::MAROON.into(), ..default() }, MenuButtonAction::Play, @@ -489,6 +495,7 @@ mod menu { ButtonBundle { style: button_style.clone(), background_color: NORMAL_BUTTON.into(), + border_color: Color::MAROON.into(), ..default() }, MenuButtonAction::Settings, @@ -510,6 +517,7 @@ mod menu { ButtonBundle { style: button_style, background_color: NORMAL_BUTTON.into(), + border_color: Color::MAROON.into(), ..default() }, MenuButtonAction::Quit, @@ -698,6 +706,7 @@ mod menu { width: Val::Px(200.0), height: Val::Px(65.0), margin: UiRect::all(Val::Px(20.0)), + border_radius: UiBorderRadius::max(), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 28846a7f1e248..2af1e49be2c61 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -3,6 +3,9 @@ //! To start the demo without text run //! `cargo run --example many_buttons --release no-text` //! +//! To start the demo without borders run +//! `cargo run --example many_buttons --release no-borders` +//! //| To do a full layout update each frame run //! `cargo run --example many_buttons --release recompute-layout` //! @@ -87,22 +90,40 @@ fn setup(mut commands: Commands) { }) .with_children(|commands| { let spawn_text = std::env::args().all(|arg| arg != "no-text"); + let border = if std::env::args().all(|arg| arg != "no-borders") { + UiRect::all(Val::Percent(10. / count_f)) + } else { + UiRect::DEFAULT + }; for i in 0..count { for j in 0..count { let color = as_rainbow(j % i.max(1)).into(); - spawn_button(commands, color, count_f, i, j, spawn_text); + let border_color = as_rainbow(i % j.max(1)).into(); + spawn_button( + commands, + color, + count_f, + i, + j, + spawn_text, + border, + border_color, + ); } } }); } +#[allow(clippy::too_many_arguments)] fn spawn_button( commands: &mut ChildBuilder, - color: BackgroundColor, + background_color: BackgroundColor, total: f32, i: usize, j: usize, spawn_text: bool, + border: UiRect, + border_color: BorderColor, ) { let width = 90.0 / total; let mut builder = commands.spawn(( @@ -114,12 +135,14 @@ fn spawn_button( left: Val::Percent(100.0 / total * j as f32), align_items: AlignItems::Center, position_type: PositionType::Absolute, + border, ..default() }, - background_color: color, + background_color, + border_color, ..default() }, - IdleColor(color), + IdleColor(background_color), )); if spawn_text { diff --git a/examples/ui/borders.rs b/examples/ui/borders.rs index bf6cac544ce75..365668ff9b570 100644 --- a/examples/ui/borders.rs +++ b/examples/ui/borders.rs @@ -89,25 +89,50 @@ fn setup(mut commands: Commands) { style: Style { width: Val::Px(10.), height: Val::Px(10.), + border_radius: UiBorderRadius::all(Val::Px(5.)), ..Default::default() }, background_color: Color::YELLOW.into(), ..Default::default() }) .id(); + let border = borders[i % borders.len()]; + let border_radius = UiBorderRadius::px( + if border.left != Val::Px(0.) && border.top != Val::Px(0.) { + f32::MAX + } else { + 0. + }, + if border.right != Val::Px(0.) && border.top != Val::Px(0.) { + f32::MAX + } else { + 0. + }, + if border.right != Val::Px(0.) && border.bottom != Val::Px(0.) { + f32::MAX + } else { + 0. + }, + if border.left != Val::Px(0.) && border.bottom != Val::Px(0.) { + f32::MAX + } else { + 0. + }, + ); let bordered_node = commands .spawn(NodeBundle { style: Style { width: Val::Px(50.), height: Val::Px(50.), - border: borders[i % borders.len()], + border, margin: UiRect::all(Val::Px(2.)), align_items: AlignItems::Center, justify_content: JustifyContent::Center, + border_radius, ..Default::default() }, - background_color: Color::BLUE.into(), - border_color: Color::WHITE.with_a(0.5).into(), + background_color: Color::MAROON.into(), + border_color: Color::CRIMSON.into(), ..Default::default() }) .add_child(inner_spot) diff --git a/examples/ui/button.rs b/examples/ui/button.rs index 288ad8d2c32f9..f43977d408021 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -75,6 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res) { justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, + border_radius: UiBorderRadius::all(Val::Px(10.)), ..default() }, border_color: BorderColor(Color::BLACK), diff --git a/examples/ui/overflow.rs b/examples/ui/overflow.rs index 12c922c92d993..291accce4bd72 100644 --- a/examples/ui/overflow.rs +++ b/examples/ui/overflow.rs @@ -85,16 +85,30 @@ fn setup(mut commands: Commands, asset_server: Res) { ..Default::default() }) .with_children(|parent| { - parent.spawn(ImageBundle { - image: UiImage::new(image.clone()), - style: Style { - min_width: Val::Px(100.), - min_height: Val::Px(100.), + parent + .spawn(NodeBundle { + style: Style { + min_width: Val::Px(100.), + min_height: Val::Px(100.), + border: UiRect::all(Val::Px(20.)), + //border_radius: UiBorderRadius::all(Val::Px(30.)), + ..Default::default() + }, + border_color: Color::WHITE.into(), ..Default::default() - }, - background_color: Color::WHITE.into(), - ..Default::default() - }); + }) + .with_children(|parent| { + parent.spawn(ImageBundle { + image: UiImage::new(image.clone()), + style: Style { + min_width: Val::Px(100.), + min_height: Val::Px(100.), + ..Default::default() + }, + background_color: Color::WHITE.into(), + ..Default::default() + }); + }); }); }); } diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 562a892bd93c1..3f809cfef24b3 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -86,11 +86,13 @@ fn setup(mut commands: Commands, asset_server: Res) { style: Style { flex_direction: FlexDirection::Column, align_items: AlignItems::Stretch, - padding: UiRect::all(Val::Px(10.)), + border: UiRect::all(Val::Px(10.)), margin: UiRect::top(Val::Px(50.)), + border_radius: UiBorderRadius::all(Val::Px(25.)), ..Default::default() }, - background_color: Color::YELLOW.into(), + border_color: Color::YELLOW.into(), + background_color: Color::BLACK.into(), ..Default::default() }) .with_children(|parent| { @@ -114,6 +116,7 @@ fn spawn_bar(parent: &mut ChildBuilder) { flex_basis: Val::Percent(100.0), align_self: AlignSelf::Stretch, padding: UiRect::all(Val::Px(10.)), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..Default::default() }, background_color: Color::YELLOW.into(), @@ -127,6 +130,7 @@ fn spawn_bar(parent: &mut ChildBuilder) { width: Val::Percent(100.), height: Val::Px(100.), padding: UiRect::all(Val::Px(4.)), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..Default::default() }, background_color: Color::BLACK.into(), @@ -136,6 +140,7 @@ fn spawn_bar(parent: &mut ChildBuilder) { parent.spawn(( NodeBundle { style: Style { + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..Default::default() }, background_color: Color::WHITE.into(), @@ -163,7 +168,7 @@ fn spawn_button_row(parent: &mut ChildBuilder, constraint: Constraint, text_styl align_items: AlignItems::Stretch, ..Default::default() }, - background_color: Color::BLACK.into(), + //background_color: Color::BLACK.into(), ..Default::default() }) .with_children(|parent| { @@ -175,7 +180,6 @@ fn spawn_button_row(parent: &mut ChildBuilder, constraint: Constraint, text_styl padding: UiRect::all(Val::Px(2.)), ..Default::default() }, - //background_color: Color::RED.into(), ..Default::default() }) .with_children(|parent| { @@ -244,6 +248,7 @@ fn spawn_button( justify_content: JustifyContent::Center, border: UiRect::all(Val::Px(2.)), margin: UiRect::horizontal(Val::Px(2.)), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..Default::default() }, background_color: if active { @@ -263,6 +268,7 @@ fn spawn_button( style: Style { width: Val::Px(100.), justify_content: JustifyContent::Center, + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..Default::default() }, background_color: if active { diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index f1bfbcd622cc1..590c0529fdc8e 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -42,43 +42,36 @@ fn setup(mut commands: Commands, asset_server: Res) { style: Style { width: Val::Px(200.), border: UiRect::all(Val::Px(2.)), + justify_content: JustifyContent::Center, + border_radius: UiBorderRadius::all(Val::Px(20.)), ..default() }, - background_color: Color::rgb(0.65, 0.65, 0.65).into(), + border_color: Color::rgb(0.65, 0.65, 0.65).into(), + background_color: Color::rgb(0.15, 0.15, 0.15).into(), ..default() }) .with_children(|parent| { // left vertical fill (content) - parent - .spawn(NodeBundle { - style: Style { - width: Val::Percent(100.), - ..default() + + // text + parent.spawn(( + TextBundle::from_section( + "Text Example", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 30.0, + color: Color::WHITE, }, - background_color: Color::rgb(0.15, 0.15, 0.15).into(), + ) + .with_style(Style { + margin: UiRect::all(Val::Px(5.)), ..default() - }) - .with_children(|parent| { - // text - parent.spawn(( - TextBundle::from_section( - "Text Example", - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 30.0, - color: Color::WHITE, - }, - ) - .with_style(Style { - margin: UiRect::all(Val::Px(5.)), - ..default() - }), - // Because this is a distinct label widget and - // not button/list item text, this is necessary - // for accessibility to treat the text accordingly. - Label, - )); - }); + }), + // Because this is a distinct label widget and + // not button/list item text, this is necessary + // for accessibility to treat the text accordingly. + Label, + )); }); // right vertical fill parent @@ -114,6 +107,8 @@ fn setup(mut commands: Commands, asset_server: Res) { align_self: AlignSelf::Stretch, height: Val::Percent(50.), overflow: Overflow::clip_y(), + margin: UiRect::axes(Val::Px(10.), Val::Px(5.)), + border_radius: UiBorderRadius::all(Val::Px(20.)), ..default() }, background_color: Color::rgb(0.10, 0.10, 0.10).into(), @@ -155,31 +150,37 @@ fn setup(mut commands: Commands, asset_server: Res) { }); }); parent - .spawn(NodeBundle { - style: Style { - width: Val::Px(200.0), - height: Val::Px(200.0), - position_type: PositionType::Absolute, - left: Val::Px(210.), - bottom: Val::Px(10.), - border: UiRect::all(Val::Px(20.)), + .spawn(( + NodeBundle { + style: Style { + width: Val::Px(200.0), + height: Val::Px(200.0), + position_type: PositionType::Absolute, + left: Val::Px(210.), + bottom: Val::Px(10.), + border_radius: UiBorderRadius::all(Val::Px(40.)), + padding: UiRect::all(Val::Px(20.)), + ..default() + }, + background_color: Color::rgb(0.4, 0.4, 1.).into(), ..default() }, - border_color: Color::GREEN.into(), - background_color: Color::rgb(0.4, 0.4, 1.).into(), - ..default() - }) + UiShadow { + ..Default::default() + }, + )) .with_children(|parent| { parent.spawn(NodeBundle { style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), + flex_grow: 1., + border_radius: UiBorderRadius::all(Val::Px(20.)), ..default() }, background_color: Color::rgb(0.8, 0.8, 1.).into(), ..default() }); }); + // render order test: reddest in the back, whitest in the front (flex center) parent .spawn(NodeBundle { @@ -189,72 +190,106 @@ fn setup(mut commands: Commands, asset_server: Res) { position_type: PositionType::Absolute, align_items: AlignItems::Center, justify_content: JustifyContent::Center, + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), ..default() }, ..default() }) .with_children(|parent| { parent - .spawn(NodeBundle { - style: Style { - width: Val::Px(100.0), - height: Val::Px(100.0), + .spawn(( + NodeBundle { + style: Style { + width: Val::Px(100.0), + height: Val::Px(100.0), + left: Val::Px(-10.), + bottom: Val::Px(-10.), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), + ..default() + }, + background_color: Color::rgb(1.0, 0.0, 0.).into(), ..default() }, - background_color: Color::rgb(1.0, 0.0, 0.).into(), - ..default() - }) + UiShadow { + ..Default::default() + }, + )) .with_children(|parent| { - parent.spawn(NodeBundle { - style: Style { - // Take the size of the parent node. - width: Val::Percent(100.0), - height: Val::Percent(100.0), - position_type: PositionType::Absolute, - left: Val::Px(20.), - bottom: Val::Px(20.), + parent.spawn(( + NodeBundle { + style: Style { + // Take the size of the parent node. + width: Val::Percent(100.0), + height: Val::Percent(100.0), + position_type: PositionType::Absolute, + left: Val::Px(30.), + bottom: Val::Px(20.), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), + ..default() + }, + background_color: Color::rgb(1.0, 0.3, 0.3).into(), ..default() }, - background_color: Color::rgb(1.0, 0.3, 0.3).into(), - ..default() - }); - parent.spawn(NodeBundle { - style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - position_type: PositionType::Absolute, - left: Val::Px(40.), - bottom: Val::Px(40.), + UiShadow { + ..Default::default() + }, + )); + parent.spawn(( + NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + position_type: PositionType::Absolute, + left: Val::Px(70.), + bottom: Val::Px(50.), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), + ..default() + }, + background_color: Color::rgb(1.0, 0.5, 0.5).into(), ..default() }, - background_color: Color::rgb(1.0, 0.5, 0.5).into(), - ..default() - }); - parent.spawn(NodeBundle { - style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - position_type: PositionType::Absolute, - left: Val::Px(60.), - bottom: Val::Px(60.), + UiShadow { + ..Default::default() + }, + )); + parent.spawn(( + NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + position_type: PositionType::Absolute, + left: Val::Px(110.), + bottom: Val::Px(80.), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), + ..default() + }, + background_color: Color::rgb(1.0, 0.7, 0.7).into(), ..default() }, - background_color: Color::rgb(1.0, 0.7, 0.7).into(), - ..default() - }); + UiShadow { + ..Default::default() + }, + )); // alpha test - parent.spawn(NodeBundle { - style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - position_type: PositionType::Absolute, - left: Val::Px(80.), - bottom: Val::Px(80.), + parent.spawn(( + NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + position_type: PositionType::Absolute, + left: Val::Px(150.), + bottom: Val::Px(110.), + border_radius: UiBorderRadius::all(Val::Px(f32::MAX)), + ..default() + }, + background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(), ..default() }, - background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(), - ..default() - }); + UiShadow { + color: Color::rgba(0.9, 0.8, 0.8, 0.05), + ..Default::default() + }, + )); }); }); // bevy logo (flex center) @@ -265,6 +300,7 @@ fn setup(mut commands: Commands, asset_server: Res) { position_type: PositionType::Absolute, justify_content: JustifyContent::Center, align_items: AlignItems::FlexStart, + border_radius: UiBorderRadius::all(Val::Px(40.)), ..default() }, ..default() diff --git a/examples/ui/ui_scaling.rs b/examples/ui/ui_scaling.rs index 2b4c8551e6e83..3fc4b57209d35 100644 --- a/examples/ui/ui_scaling.rs +++ b/examples/ui/ui_scaling.rs @@ -54,6 +54,9 @@ fn setup(mut commands: Commands, asset_server: ResMut) { style: Style { width: Val::Px(40.0), height: Val::Px(40.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + border_radius: UiBorderRadius::all(Val::Px(10.)), ..default() }, background_color: Color::RED.into(), diff --git a/examples/ui/ui_texture_atlas.rs b/examples/ui/ui_texture_atlas.rs index 31ba99e450f0a..44e67b8538633 100644 --- a/examples/ui/ui_texture_atlas.rs +++ b/examples/ui/ui_texture_atlas.rs @@ -50,16 +50,21 @@ fn setup( ..default() }) .with_children(|parent| { - parent.spawn((AtlasImageBundle { - style: Style { - width: Val::Px(256.), - height: Val::Px(256.), + parent.spawn(( + AtlasImageBundle { + style: Style { + width: Val::Px(256.), + height: Val::Px(256.), + border: UiRect::all(Val::Px(16.)), + border_radius: UiBorderRadius::all(Val::Px(16.)), + ..default() + }, + texture_atlas: texture_atlas_handle, + texture_atlas_image: UiTextureAtlasImage::default(), ..default() }, - texture_atlas: texture_atlas_handle, - texture_atlas_image: UiTextureAtlasImage::default(), - ..default() - },)); + BorderColor(Color::RED), + )); parent.spawn(TextBundle::from_sections([ TextSection::new("press ".to_string(), text_style.clone()), TextSection::new(