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:

<img width="626" alt="nub"
src="https://github.com/user-attachments/assets/258ed8b5-1a9e-4ac5-99c2-6bf25c0ef31c">

## 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>
This commit is contained in:
ickshonpe 2024-09-27 00:10:35 +01:00 committed by GitHub
parent 35d10866b8
commit 0fe33c3bba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 191 additions and 326 deletions

View file

@ -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]

View file

@ -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

View file

@ -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"

View file

@ -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 {

View file

@ -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<ExtractedUiNodes>,
camera_query: Extract<Query<(Entity, &Camera)>>,
default_ui_camera: Extract<DefaultUiCamera>,
ui_scale: Extract<Res<UiScale>>,
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<Query<&Node>>,
) {
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
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
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<ExtractedUiNodes>,
camera_query: Extract<Query<(Entity, &Camera)>>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
ui_scale: Extract<Res<UiScale>>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<
@ -313,17 +252,12 @@ pub fn extract_uinode_images(
Option<&TargetCamera>,
&UiImage,
Option<&TextureAtlas>,
Option<&Parent>,
&Style,
),
Without<ImageScaleMode>,
>,
>,
node_query: Extract<Query<&Node>>,
) {
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
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
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<ExtractedUiNodes>,
camera_query: Extract<Query<(Entity, &Camera)>>,
default_ui_camera: Extract<DefaultUiCamera>,
ui_scale: Extract<Res<UiScale>>,
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<Query<&Node>>,
) {
let image = AssetId::<Image>::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
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
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(),
});
}

View file

@ -184,7 +184,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
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);
}

View file

@ -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<M: UiMaterial> Default for ExtractedUiMaterialNodes<M> {
}
}
#[allow(clippy::too_many_arguments)]
pub fn extract_ui_material_nodes<M: UiMaterial>(
mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,
materials: Extract<Res<Assets<M>>>,
@ -365,35 +362,20 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
(
Entity,
&Node,
&Style,
&GlobalTransform,
&Handle<M>,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
Option<&Parent>,
),
Without<BackgroundColor>,
>,
>,
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
ui_scale: Extract<Res<UiScale>>,
node_query: Extract<Query<&Node>>,
) {
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<M: UiMaterial>(
continue;
}
// Both vertical and horizontal percentage border values are calculated based on the width of the parent node
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
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<M: UiMaterial>(
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,
},

View file

@ -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,

View file

@ -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;

View file

@ -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()
},
];