From 0fe33c3bbaf58ce89d85e97092fc45438d2da4ff Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 27 Sep 2024 00:10:35 +0100 Subject: [PATCH] use precomputed border values (#15163) # Objective Fixes #15142 ## Solution * Moved all the UI border geometry calculations that were scattered through the UI extraction functions into `ui_layout_system`. * Added a `border: BorderRect` field to `Node` to store the border size computed by `ui_layout_system`. * Use the border values returned from Taffy rather than calculate them ourselves during extraction. * Removed the `logical_rect` and `physical_rect` methods from `Node` the descriptions and namings are deceptive, it's better to create the rects manually instead. * Added a method `outline_radius` to `Node` that calculates the border radius of outlines. * For border values `ExtractedUiNode` takes `BorderRect` and `ResolvedBorderRadius` now instead of raw `[f32; 4]` values and converts them in `prepare_uinodes`. * Removed some unnecessary scaling and clamping of border values (#15142). * Added a `BorderRect::ZERO` constant. * Added an `outlined_node_size` method to `Node`. ## Testing Added some non-uniform borders to the border example. Everything seems to be in order: nub ## Migration Guide The `logical_rect` and `physical_rect` methods have been removed from `Node`. Use `Rect::from_center_size` with the translation and node size instead. The types of the fields border and border_radius of `ExtractedUiNode` have been changed to `BorderRect` and `ResolvedBorderRadius` respectively. --------- Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com> Co-authored-by: akimakinai <105044389+akimakinai@users.noreply.github.com> --- .../src/texture_slice/border_rect.rs | 3 + crates/bevy_ui/src/focus.rs | 5 +- crates/bevy_ui/src/layout/mod.rs | 31 +- crates/bevy_ui/src/picking_backend.rs | 7 +- crates/bevy_ui/src/render/mod.rs | 278 ++++-------------- crates/bevy_ui/src/render/ui.wgsl | 2 +- .../src/render/ui_material_pipeline.rs | 49 +-- crates/bevy_ui/src/ui_node.rs | 127 +++++--- crates/bevy_ui/src/update.rs | 3 +- examples/ui/borders.rs | 12 +- 10 files changed, 191 insertions(+), 326 deletions(-) diff --git a/crates/bevy_sprite/src/texture_slice/border_rect.rs b/crates/bevy_sprite/src/texture_slice/border_rect.rs index e32f2891c1..fc15b42851 100644 --- a/crates/bevy_sprite/src/texture_slice/border_rect.rs +++ b/crates/bevy_sprite/src/texture_slice/border_rect.rs @@ -14,6 +14,9 @@ pub struct BorderRect { } impl BorderRect { + /// An empty border with zero padding values in each direction + pub const ZERO: Self = Self::square(0.); + /// Creates a new border as a square, with identical pixel padding values on every direction #[must_use] #[inline] diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 59f75f1b3c..9e67d4d6f9 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -243,7 +243,10 @@ pub fn ui_focus_system( .map(TargetCamera::entity) .or(default_ui_camera.get())?; - let node_rect = node.node.logical_rect(node.global_transform); + let node_rect = Rect::from_center_size( + node.global_transform.translation().truncate(), + node.node.size(), + ); // Intersect with the calculated clip rect to find the bounds of the visible region of the node let visible_rect = node diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 5399e9a124..233ccd7186 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,6 +1,6 @@ use crate::{ - BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, OverflowAxis, ScrollPosition, Style, - TargetCamera, UiScale, + BorderRadius, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis, + ScrollPosition, Style, TargetCamera, UiScale, }; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, @@ -14,6 +14,7 @@ use bevy_ecs::{ use bevy_hierarchy::{Children, Parent}; use bevy_math::{UVec2, Vec2}; use bevy_render::camera::{Camera, NormalizedRenderTarget}; +use bevy_sprite::BorderRect; #[cfg(feature = "bevy_text")] use bevy_text::{CosmicBuffer, TextPipeline}; use bevy_transform::components::Transform; @@ -344,6 +345,13 @@ pub fn ui_layout_system( node.unrounded_size = layout_size; } + node.bypass_change_detection().border = BorderRect { + left: layout.border.left * inverse_target_scale_factor, + right: layout.border.right * inverse_target_scale_factor, + top: layout.border.top * inverse_target_scale_factor, + bottom: layout.border.bottom * inverse_target_scale_factor, + }; + let viewport_size = root_size.unwrap_or(node.calculated_size); if let Some(border_radius) = maybe_border_radius { @@ -355,11 +363,15 @@ pub fn ui_layout_system( if let Some(outline) = maybe_outline { // don't trigger change detection when only outlines are changed let node = node.bypass_change_detection(); - node.outline_width = outline - .width - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.); + node.outline_width = if style.display != Display::None { + outline + .width + .resolve(node.size().x, viewport_size) + .unwrap_or(0.) + .max(0.) + } else { + 0. + }; node.outline_offset = outline .offset @@ -834,7 +846,10 @@ mod tests { .fold( Option::<(Rect, bool)>::None, |option_rect, (entity, node, global_transform)| { - let current_rect = node.logical_rect(global_transform); + let current_rect = Rect::from_center_size( + global_transform.translation().truncate(), + node.size(), + ); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity:?} doesn't have a logical size" diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index e88409f27c..bb57537145 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -26,7 +26,7 @@ use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::Vec2; +use bevy_math::{Rect, Vec2}; use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_utils::hashbrown::HashMap; @@ -139,7 +139,10 @@ pub fn ui_picking( continue; }; - let node_rect = node.node.logical_rect(node.global_transform); + let node_rect = Rect::from_center_size( + node.global_transform.translation().truncate(), + node.node.size(), + ); // Nodes with Display::None have a (0., 0.) logical rect and can be ignored if node_rect.size() == Vec2::ZERO { diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 3160e65aed..c3c7fbc8da 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -3,43 +3,21 @@ mod render_pass; mod ui_material_pipeline; pub mod ui_texture_slice_pipeline; -use bevy_color::{Alpha, ColorToComponents, LinearRgba}; -use bevy_core_pipeline::{ - core_2d::{ - graph::{Core2d, Node2d}, - Camera2d, - }, - core_3d::{ - graph::{Core3d, Node3d}, - Camera3d, - }, -}; -use bevy_hierarchy::Parent; -use bevy_render::{ - render_phase::{PhaseItem, PhaseItemExtraIndex, ViewSortedRenderPhases}, - texture::{GpuImage, TRANSPARENT_IMAGE_HANDLE}, - view::ViewVisibility, - ExtractSchedule, Render, -}; -use bevy_sprite::{ImageScaleMode, SpriteAssetEvents, TextureAtlas}; -pub use pipeline::*; -pub use render_pass::*; -pub use ui_material_pipeline::*; -use ui_texture_slice_pipeline::UiTextureSlicerPlugin; - use crate::{ - graph::{NodeUi, SubGraphUi}, - BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Display, Node, Outline, Style, - TargetCamera, UiAntiAlias, UiImage, UiScale, Val, + BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Node, Outline, + ResolvedBorderRadius, TargetCamera, UiAntiAlias, UiImage, UiScale, }; - use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; -use bevy_ecs::{ - entity::{EntityHashMap, EntityHashSet}, - prelude::*, -}; -use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_color::{Alpha, ColorToComponents, LinearRgba}; +use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; +use bevy_ecs::entity::{EntityHashMap, EntityHashSet}; +use bevy_ecs::prelude::*; +use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_render::render_phase::ViewSortedRenderPhases; +use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; use bevy_render::{ camera::Camera, render_asset::RenderAssets, @@ -51,13 +29,25 @@ use bevy_render::{ view::{ExtractedView, ViewUniforms}, Extract, RenderApp, RenderSet, }; +use bevy_render::{ + render_phase::{PhaseItem, PhaseItemExtraIndex}, + texture::GpuImage, + view::ViewVisibility, + ExtractSchedule, Render, +}; use bevy_sprite::TextureAtlasLayout; +use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents, TextureAtlas}; #[cfg(feature = "bevy_text")] use bevy_text::{PositionedGlyph, Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; +use graph::{NodeUi, SubGraphUi}; +pub use pipeline::*; +pub use render_pass::*; use std::ops::Range; +pub use ui_material_pipeline::*; +use ui_texture_slice_pipeline::UiTextureSlicerPlugin; pub mod graph { use bevy_render::render_graph::{RenderLabel, RenderSubGraph}; @@ -183,11 +173,9 @@ pub struct ExtractedUiNode { // Nodes with ambiguous camera will be ignored. pub camera_entity: Entity, /// Border radius of the UI node. - /// Ordering: top left, top right, bottom right, bottom left. - pub border_radius: [f32; 4], + pub border_radius: ResolvedBorderRadius, /// Border thickness of the UI node. - /// Ordering: left, top, right, bottom. - pub border: [f32; 4], + pub border: BorderRect, pub node_type: NodeType, } @@ -198,9 +186,7 @@ pub struct ExtractedUiNodes { pub fn extract_uinode_background_colors( mut extracted_uinodes: ResMut, - camera_query: Extract>, default_ui_camera: Extract, - ui_scale: Extract>, uinode_query: Extract< Query<( Entity, @@ -210,23 +196,11 @@ pub fn extract_uinode_background_colors( Option<&CalculatedClip>, Option<&TargetCamera>, &BackgroundColor, - &Style, - Option<&Parent>, )>, >, - node_query: Extract>, ) { - for ( - entity, - uinode, - transform, - view_visibility, - clip, - camera, - background_color, - style, - parent, - ) in &uinode_query + for (entity, uinode, transform, view_visibility, clip, camera, background_color) in + &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -238,39 +212,6 @@ pub fn extract_uinode_background_colors( continue; } - let ui_logical_viewport_size = camera_query - .get(camera_entity) - .ok() - .and_then(|(_, c)| c.logical_viewport_size()) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; - - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); - let right = - resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size); - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); - let bottom = - resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - - let border = [left, top, right, bottom]; - - let border_radius = [ - uinode.border_radius.top_left, - uinode.border_radius.top_right, - uinode.border_radius.bottom_right, - uinode.border_radius.bottom_left, - ]; - extracted_uinodes.uinodes.insert( entity, ExtractedUiNode { @@ -287,8 +228,8 @@ pub fn extract_uinode_background_colors( flip_x: false, flip_y: false, camera_entity, - border, - border_radius, + border: uinode.border(), + border_radius: uinode.border_radius(), node_type: NodeType::Rect, }, ); @@ -299,9 +240,7 @@ pub fn extract_uinode_background_colors( pub fn extract_uinode_images( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, texture_atlases: Extract>>, - ui_scale: Extract>, default_ui_camera: Extract, uinode_query: Extract< Query< @@ -313,17 +252,12 @@ pub fn extract_uinode_images( Option<&TargetCamera>, &UiImage, Option<&TextureAtlas>, - Option<&Parent>, - &Style, ), Without, >, >, - node_query: Extract>, ) { - for (uinode, transform, view_visibility, clip, camera, image, atlas, parent, style) in - &uinode_query - { + for (uinode, transform, view_visibility, clip, camera, image, atlas) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -364,39 +298,6 @@ pub fn extract_uinode_images( None }; - let ui_logical_viewport_size = camera_query - .get(camera_entity) - .ok() - .and_then(|(_, c)| c.logical_viewport_size()) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; - - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); - let right = - resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size); - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); - let bottom = - resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - - let border = [left, top, right, bottom]; - - let border_radius = [ - uinode.border_radius.top_left, - uinode.border_radius.top_right, - uinode.border_radius.bottom_right, - uinode.border_radius.bottom_left, - ]; - extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { @@ -410,54 +311,18 @@ pub fn extract_uinode_images( flip_x: image.flip_x, flip_y: image.flip_y, camera_entity, - border, - border_radius, + border: uinode.border, + border_radius: uinode.border_radius, node_type: NodeType::Rect, }, ); } } -pub(crate) 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 clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { - let s = 0.5 * size + offset; - let sm = s.x.min(s.y); - 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_uinode_borders( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, default_ui_camera: Extract, - ui_scale: Extract>, uinode_query: Extract< Query<( &Node, @@ -465,12 +330,9 @@ pub fn extract_uinode_borders( &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, - Option<&Parent>, - &Style, AnyOf<(&BorderColor, &Outline)>, )>, >, - node_query: Extract>, ) { let image = AssetId::::default(); @@ -480,8 +342,6 @@ pub fn extract_uinode_borders( view_visibility, maybe_clip, maybe_camera, - maybe_parent, - style, (maybe_border_color, maybe_outline), ) in &uinode_query { @@ -494,50 +354,14 @@ pub fn extract_uinode_borders( // Skip invisible borders if !view_visibility.get() - || style.display == Display::None || maybe_border_color.is_some_and(|border_color| border_color.0.is_fully_transparent()) && maybe_outline.is_some_and(|outline| outline.color.is_fully_transparent()) { continue; } - let ui_logical_viewport_size = camera_query - .get(camera_entity) - .ok() - .and_then(|(_, c)| c.logical_viewport_size()) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; - - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = maybe_parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); - let right = - resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size); - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); - let bottom = - resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - - let border = [left, top, right, bottom]; - - let border_radius = [ - uinode.border_radius.top_left, - uinode.border_radius.top_right, - uinode.border_radius.bottom_right, - uinode.border_radius.bottom_left, - ]; - - let border_radius = clamp_radius(border_radius, uinode.size(), border.into()); - // don't extract border if no border or the node is zero-sized (a zero sized node can still have an outline). - if !uinode.is_empty() && border != [0.; 4] { + if !uinode.is_empty() && uinode.border() != BorderRect::ZERO { if let Some(border_color) = maybe_border_color { extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), @@ -555,8 +379,8 @@ pub fn extract_uinode_borders( flip_x: false, flip_y: false, camera_entity, - border_radius, - border, + border_radius: uinode.border_radius(), + border: uinode.border(), node_type: NodeType::Border, }, ); @@ -564,15 +388,7 @@ pub fn extract_uinode_borders( } if let Some(outline) = maybe_outline { - let outer_distance = uinode.outline_offset() + uinode.outline_width(); - let outline_radius = border_radius.map(|radius| { - if radius > 0. { - radius + outer_distance - } else { - 0. - } - }); - let outline_size = uinode.size() + 2. * outer_distance; + let outline_size = uinode.outlined_node_size(); extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { @@ -589,8 +405,8 @@ pub fn extract_uinode_borders( flip_x: false, flip_y: false, camera_entity, - border: [uinode.outline_width(); 4], - border_radius: outline_radius, + border: BorderRect::square(uinode.outline_width()), + border_radius: uinode.outline_radius(), node_type: NodeType::Border, }, ); @@ -775,8 +591,8 @@ pub fn extract_uinode_text( flip_x: false, flip_y: false, camera_entity, - border: [0.; 4], - border_radius: [0.; 4], + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, node_type: NodeType::Rect, }, ); @@ -1130,8 +946,18 @@ pub fn prepare_uinodes( uv: uvs[i].into(), color, flags: flags | shader_flags::CORNERS[i], - radius: extracted_uinode.border_radius, - border: extracted_uinode.border, + radius: [ + extracted_uinode.border_radius.top_left, + extracted_uinode.border_radius.top_right, + extracted_uinode.border_radius.bottom_right, + extracted_uinode.border_radius.bottom_left, + ], + border: [ + extracted_uinode.border.left, + extracted_uinode.border.top, + extracted_uinode.border.right, + extracted_uinode.border.bottom, + ], size: rect_size.xy().into(), }); } diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index ad82783d7f..c46e0b4926 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -184,7 +184,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); if enabled(in.flags, BORDER) { - return draw(in, texture_color); + return draw(in, texture_color); } else { return draw_background(in, texture_color); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index aa814cf081..b8b3d7395f 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -10,7 +10,6 @@ use bevy_ecs::{ *, }, }; -use bevy_hierarchy::Parent; use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; use bevy_render::{ extract_component::ExtractComponentPlugin, @@ -24,7 +23,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSet, }; use bevy_transform::prelude::GlobalTransform; -use bevy_window::{PrimaryWindow, Window}; use bytemuck::{Pod, Zeroable}; use crate::*; @@ -355,7 +353,6 @@ impl Default for ExtractedUiMaterialNodes { } } -#[allow(clippy::too_many_arguments)] pub fn extract_ui_material_nodes( mut extracted_uinodes: ResMut>, materials: Extract>>, @@ -365,35 +362,20 @@ pub fn extract_ui_material_nodes( ( Entity, &Node, - &Style, &GlobalTransform, &Handle, &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, - Option<&Parent>, ), Without, >, >, - windows: Extract>>, - ui_scale: Extract>, - node_query: Extract>, ) { - let ui_logical_viewport_size = windows - .get_single() - .map(Window::size) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; - // If there is only one camera, we use it as default let default_single_camera = default_ui_camera.get(); - for (entity, uinode, style, transform, handle, view_visibility, clip, camera, maybe_parent) in - uinode_query.iter() - { + for (entity, uinode, transform, handle, view_visibility, clip, camera) in uinode_query.iter() { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_single_camera) else { continue; }; @@ -408,25 +390,12 @@ pub fn extract_ui_material_nodes( continue; } - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = maybe_parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size) - / uinode.size().x; - let right = - resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size) - / uinode.size().x; - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size) - / uinode.size().y; - let bottom = - resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size) - / uinode.size().y; + let border = [ + uinode.border.left / uinode.size().x, + uinode.border.right / uinode.size().x, + uinode.border.top / uinode.size().y, + uinode.border.bottom / uinode.size().y, + ]; extracted_uinodes.uinodes.insert( entity, @@ -436,9 +405,9 @@ pub fn extract_ui_material_nodes( material: handle.id(), rect: Rect { min: Vec2::ZERO, - max: uinode.calculated_size, + max: uinode.size(), }, - border: [left, right, top, bottom], + border, clip: clip.map(|clip| clip.clip), camera_entity, }, diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index b5489b4543..358e955f0e 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2,13 +2,13 @@ use crate::{UiRect, Val}; use bevy_asset::Handle; use bevy_color::Color; use bevy_ecs::{prelude::*, system::SystemParam}; -use bevy_math::{Rect, Vec2}; +use bevy_math::{vec4, Rect, Vec2, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ camera::{Camera, RenderTarget}, texture::{Image, TRANSPARENT_IMAGE_HANDLE}, }; -use bevy_transform::prelude::GlobalTransform; +use bevy_sprite::BorderRect; use bevy_utils::warn_once; use bevy_window::{PrimaryWindow, WindowRef}; use smallvec::SmallVec; @@ -48,6 +48,11 @@ pub struct Node { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) unrounded_size: Vec2, + /// Resolved border values in logical pixels + /// Border updates bypass change detection. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + pub(crate) border: BorderRect, /// Resolved border radius values in logical pixels. /// Border radius updates bypass change detection. /// @@ -72,6 +77,8 @@ impl Node { /// The order of the node in the UI layout. /// Nodes with a higher stack index are drawn on top of and receive interactions before nodes with lower stack indices. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. pub const fn stack_index(&self) -> u32 { self.stack_index } @@ -83,54 +90,91 @@ impl Node { self.unrounded_size } - /// Returns the size of the node in physical pixels based on the given scale factor and `UiScale`. - #[inline] - pub fn physical_size(&self, scale_factor: f32, ui_scale: f32) -> Vec2 { - Vec2::new( - self.calculated_size.x * scale_factor * ui_scale, - self.calculated_size.y * scale_factor * ui_scale, - ) - } - - /// Returns the logical pixel coordinates of the UI node, based on its [`GlobalTransform`]. - #[inline] - pub fn logical_rect(&self, transform: &GlobalTransform) -> Rect { - Rect::from_center_size(transform.translation().truncate(), self.size()) - } - - /// Returns the physical pixel coordinates of the UI node, based on its [`GlobalTransform`] and the scale factor. - #[inline] - pub fn physical_rect( - &self, - transform: &GlobalTransform, - scale_factor: f32, - ui_scale: f32, - ) -> Rect { - let rect = self.logical_rect(transform); - Rect { - min: Vec2::new( - rect.min.x * scale_factor * ui_scale, - rect.min.y * scale_factor * ui_scale, - ), - max: Vec2::new( - rect.max.x * scale_factor * ui_scale, - rect.max.y * scale_factor * ui_scale, - ), - } - } - - #[inline] /// Returns the thickness of the UI node's outline in logical pixels. /// If this value is negative or `0.` then no outline will be rendered. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] pub fn outline_width(&self) -> f32 { self.outline_width } - #[inline] /// Returns the amount of space between the outline and the edge of the node in logical pixels. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] pub fn outline_offset(&self) -> f32 { self.outline_offset } + + /// Returns the size of the node when including its outline. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] + pub fn outlined_node_size(&self) -> Vec2 { + self.size() + 2. * (self.outline_offset + self.outline_width) + } + + /// Returns the border radius for each corner of the outline + /// An outline's border radius is derived from the node's border-radius + /// so that the outline wraps the border equally at all points. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] + pub fn outline_radius(&self) -> ResolvedBorderRadius { + let outer_distance = self.outline_width + self.outline_offset; + let compute_radius = |radius| { + if radius > 0. { + radius + outer_distance + } else { + 0. + } + }; + ResolvedBorderRadius { + top_left: compute_radius(self.border_radius.top_left), + top_right: compute_radius(self.border_radius.top_right), + bottom_left: compute_radius(self.border_radius.bottom_left), + bottom_right: compute_radius(self.border_radius.bottom_right), + } + } + + /// Returns the thickness of the node's border on each edge in logical pixels. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] + pub fn border(&self) -> BorderRect { + self.border + } + + /// Returns the border radius for each of the node's corners in logical pixels. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + #[inline] + pub fn border_radius(&self) -> ResolvedBorderRadius { + self.border_radius + } + + /// Returns the inner border radius for each of the node's corners in logical pixels. + pub fn inner_radius(&self) -> ResolvedBorderRadius { + fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { + let s = 0.5 * size + offset; + let sm = s.x.min(s.y); + r.min(sm) + } + let b = vec4( + self.border.left, + self.border.top, + self.border.right, + self.border.bottom, + ); + let s = self.size() - b.xy() - b.zw(); + ResolvedBorderRadius { + top_left: clamp_corner(self.border_radius.top_left, s, b.xy()), + top_right: clamp_corner(self.border_radius.top_right, s, b.zy()), + bottom_left: clamp_corner(self.border_radius.bottom_right, s, b.zw()), + bottom_right: clamp_corner(self.border_radius.bottom_left, s, b.xw()), + } + } } impl Node { @@ -141,6 +185,7 @@ impl Node { outline_offset: 0., unrounded_size: Vec2::ZERO, border_radius: ResolvedBorderRadius::ZERO, + border: BorderRect::ZERO, }; } @@ -2316,7 +2361,7 @@ impl BorderRadius { /// Represents the resolved border radius values for a UI node. /// /// The values are in logical pixels. -#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Reflect)] pub struct ResolvedBorderRadius { pub top_left: f32, pub top_right: f32, diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index e93e8214b9..d0448388ab 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -80,7 +80,8 @@ fn update_clipping( // current node's clip and the inherited clip. This handles the case // of nested `Overflow::Hidden` nodes. If parent `clip` is not // defined, use the current node's clip. - let mut node_rect = node.logical_rect(global_transform); + let mut node_rect = + Rect::from_center_size(global_transform.translation().truncate(), node.size()); if style.overflow.x == OverflowAxis::Visible { node_rect.min.x = -f32::INFINITY; node_rect.max.x = f32::INFINITY; diff --git a/examples/ui/borders.rs b/examples/ui/borders.rs index e7bda08ddc..c306a9f594 100644 --- a/examples/ui/borders.rs +++ b/examples/ui/borders.rs @@ -77,17 +77,17 @@ fn setup(mut commands: Commands) { UiRect::horizontal(Val::Px(10.)), UiRect::vertical(Val::Px(10.)), UiRect { - left: Val::Px(10.), + left: Val::Px(20.), top: Val::Px(10.), ..Default::default() }, UiRect { left: Val::Px(10.), - bottom: Val::Px(10.), + bottom: Val::Px(20.), ..Default::default() }, UiRect { - right: Val::Px(10.), + right: Val::Px(20.), top: Val::Px(10.), ..Default::default() }, @@ -98,7 +98,7 @@ fn setup(mut commands: Commands) { }, UiRect { right: Val::Px(10.), - top: Val::Px(10.), + top: Val::Px(20.), bottom: Val::Px(10.), ..Default::default() }, @@ -109,7 +109,7 @@ fn setup(mut commands: Commands) { ..Default::default() }, UiRect { - left: Val::Px(10.), + left: Val::Px(20.), right: Val::Px(10.), top: Val::Px(10.), ..Default::default() @@ -117,7 +117,7 @@ fn setup(mut commands: Commands) { UiRect { left: Val::Px(10.), right: Val::Px(10.), - bottom: Val::Px(10.), + bottom: Val::Px(20.), ..Default::default() }, ];