From 76c048068f81458ea7fb96c19b34cbb24815f5b8 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Dec 2024 19:19:38 +0000 Subject: [PATCH 01/64] * Added a `transform: Transform` field to `Node`. * Required `GlobalTransform` instead of `Transform` on `Node`. * Update node's global transforms during layout updates. --- crates/bevy_ui/src/layout/convert.rs | 3 +++ crates/bevy_ui/src/layout/mod.rs | 35 +++++++++++++++++----------- crates/bevy_ui/src/lib.rs | 2 +- crates/bevy_ui/src/ui_node.rs | 10 +++++--- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index cbe790f3f592b..1fb0ca17603f5 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -436,6 +436,8 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { + use bevy_transform::components::Transform; + use super::*; #[test] @@ -509,6 +511,7 @@ mod tests { ], grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), + transform: Transform::IDENTITY, }; let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index ca077a9e567db..0ce54ce7dc3fe 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -13,10 +13,10 @@ use bevy_ecs::{ world::Ref, }; use bevy_hierarchy::{Children, Parent}; -use bevy_math::{UVec2, Vec2}; +use bevy_math::{Mat4, UVec2, Vec2}; use bevy_render::camera::{Camera, NormalizedRenderTarget}; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; +use bevy_transform::components::GlobalTransform; use bevy_utils::tracing::warn; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use derive_more::derive::{Display, Error, From}; @@ -124,10 +124,10 @@ pub fn ui_layout_system( computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, mut removed_components: UiLayoutSystemRemovedComponentParam, - mut node_transform_query: Query<( + mut node_update_query: Query<( &mut ComputedNode, - &mut Transform, &Node, + &mut GlobalTransform, Option<&BorderRadius>, Option<&Outline>, Option<&ScrollPosition>, @@ -303,7 +303,8 @@ with UI components as a child of an entity without UI components, your UI layout *root, &mut ui_surface, None, - &mut node_transform_query, + Mat4::IDENTITY, + &mut node_update_query, &ui_children, inverse_target_scale_factor, Vec2::ZERO, @@ -321,10 +322,11 @@ with UI components as a child of an entity without UI components, your UI layout entity: Entity, ui_surface: &mut UiSurface, root_size: Option, - node_transform_query: &mut Query<( + mut transform: Mat4, + node_update_query: &mut Query<( &mut ComputedNode, - &mut Transform, &Node, + &mut GlobalTransform, Option<&BorderRadius>, Option<&Outline>, Option<&ScrollPosition>, @@ -336,12 +338,12 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, - mut transform, style, + mut global_transform, maybe_border_radius, maybe_outline, maybe_scroll_position, - )) = node_transform_query.get_mut(entity) + )) = node_update_query.get_mut(entity) { let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity) else { return; @@ -372,9 +374,17 @@ with UI components as a child of an entity without UI components, your UI layout bottom: rect.bottom, }; + transform *= + style.transform.compute_matrix() * Mat4::from_translation(node_center.extend(0.)); + node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + let new_transform = GlobalTransform::from(transform); + if new_transform != *global_transform { + *global_transform = new_transform; + } + let viewport_size = root_size.unwrap_or(node.size); if let Some(border_radius) = maybe_border_radius { @@ -410,10 +420,6 @@ with UI components as a child of an entity without UI components, your UI layout .max(0.); } - if transform.translation.truncate() != node_center { - transform.translation = node_center.extend(0.); - } - let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { Vec2::new( @@ -447,7 +453,8 @@ with UI components as a child of an entity without UI components, your UI layout child_uinode, ui_surface, Some(viewport_size), - node_transform_query, + transform, + node_update_query, ui_children, inverse_target_scale_factor, layout_size, diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index eb2ede9fb0645..decd196ea3288 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -189,7 +189,7 @@ impl Plugin for UiPlugin { let ui_layout_system_config = ui_layout_system .in_set(UiSystem::Layout) - .before(TransformSystem::TransformPropagate); + .in_set(TransformSystem::TransformPropagate); let ui_layout_system_config = ui_layout_system_config // Text and Text2D operate on disjoint sets of entities diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index b35bf42dfad9c..234cd3a00dbbe 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -9,7 +9,7 @@ use bevy_render::{ view::Visibility, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; +use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::warn_once; use bevy_window::{PrimaryWindow, WindowRef}; use core::num::NonZero; @@ -305,9 +305,9 @@ impl From<&Vec2> for ScrollPosition { BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, - ZIndex + ZIndex, + GlobalTransform )] #[reflect(Component, Default, PartialEq, Debug)] #[cfg_attr( @@ -586,6 +586,9 @@ pub struct Node { /// /// pub grid_column: GridPlacement, + + /// Rotate, scale, skew, or translate an element. + pub transform: Transform, } impl Node { @@ -628,6 +631,7 @@ impl Node { grid_auto_columns: Vec::new(), grid_column: GridPlacement::DEFAULT, grid_row: GridPlacement::DEFAULT, + transform: Transform::IDENTITY, }; } From 6847c514f55a5d9f1e6c0f66f1d0f3a3c1d35215 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Dec 2024 19:19:50 +0000 Subject: [PATCH 02/64] Fixed overflow_debug example --- examples/ui/overflow_debug.rs | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index cfa1fb4348b40..bf3be0a1c9a77 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -41,16 +41,16 @@ struct AnimationState { struct Container(u8); trait UpdateTransform { - fn update(&self, t: f32, transform: &mut Transform); + fn update(&self, t: f32, transform: &mut Node); } #[derive(Component)] struct Move; impl UpdateTransform for Move { - fn update(&self, t: f32, transform: &mut Transform) { - transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; - transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; + fn update(&self, t: f32, node: &mut Node) { + node.transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; + node.transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; } } @@ -58,9 +58,9 @@ impl UpdateTransform for Move { struct Scale; impl UpdateTransform for Scale { - fn update(&self, t: f32, transform: &mut Transform) { - transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); - transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); + fn update(&self, t: f32, node: &mut Node) { + node.transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); + node.transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); } } @@ -68,8 +68,8 @@ impl UpdateTransform for Scale { struct Rotate; impl UpdateTransform for Rotate { - fn update(&self, t: f32, transform: &mut Transform) { - transform.rotation = + fn update(&self, t: f32, node: &mut Node) { + node.transform.rotation = Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians()); } } @@ -175,10 +175,6 @@ fn spawn_container( update_transform: impl UpdateTransform + Component, spawn_children: impl FnOnce(&mut ChildBuilder), ) { - let mut transform = Transform::default(); - - update_transform.update(0.0, &mut transform); - parent .spawn(( Node { @@ -198,11 +194,8 @@ fn spawn_container( Node { align_items: AlignItems::Center, justify_content: JustifyContent::Center, - top: Val::Px(transform.translation.x), - left: Val::Px(transform.translation.y), ..default() }, - transform, update_transform, )) .with_children(spawn_children); @@ -233,13 +226,10 @@ fn update_animation( fn update_transform( animation: Res, - mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>, + mut containers: Query<(&mut Node, &T)>, ) { - for (mut transform, mut node, computed_node, update_transform) in &mut containers { - update_transform.update(animation.t, &mut transform); - - node.left = Val::Px(transform.translation.x * computed_node.inverse_scale_factor()); - node.top = Val::Px(transform.translation.y * computed_node.inverse_scale_factor()); + for (mut node, update_transform) in &mut containers { + update_transform.update(animation.t, &mut node); } } From ff39bd47747a85bd90e7f8f064c36c50f86fc441 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Dec 2024 19:42:20 +0000 Subject: [PATCH 03/64] Use logical units for `Node`'s translation --- crates/bevy_ui/src/layout/mod.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 0ce54ce7dc3fe..28ee8770b450c 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -374,15 +374,16 @@ with UI components as a child of an entity without UI components, your UI layout bottom: rect.bottom, }; - transform *= - style.transform.compute_matrix() * Mat4::from_translation(node_center.extend(0.)); - node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); - let new_transform = GlobalTransform::from(transform); - if new_transform != *global_transform { - *global_transform = new_transform; + let mut node_transform = style.transform; + node_transform.translation /= inverse_target_scale_factor; + transform *= + node_transform.compute_matrix() * Mat4::from_translation(node_center.extend(0.)); + let new_global_transform = GlobalTransform::from(transform); + if new_global_transform != *global_transform { + *global_transform = new_global_transform; } let viewport_size = root_size.unwrap_or(node.size); From f6a3dc8af2f2939a03bb9cf8bb853ef5e9774984 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Dec 2024 20:36:24 +0000 Subject: [PATCH 04/64] fix for ambiguity detection --- crates/bevy_ui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index decd196ea3288..eb2ede9fb0645 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -189,7 +189,7 @@ impl Plugin for UiPlugin { let ui_layout_system_config = ui_layout_system .in_set(UiSystem::Layout) - .in_set(TransformSystem::TransformPropagate); + .before(TransformSystem::TransformPropagate); let ui_layout_system_config = ui_layout_system_config // Text and Text2D operate on disjoint sets of entities From 67e8315f33485f32c9bc88a1ea9c532e46510d30 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 17 Feb 2025 13:18:06 +0000 Subject: [PATCH 05/64] Fixes for merge --- crates/bevy_ui/src/ui_node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 65e4aaacb90e4..9d819c246415b 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -10,7 +10,7 @@ use bevy_render::{ view::VisibilityClass, }; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::num::NonZero; @@ -330,7 +330,7 @@ impl From for ScrollPosition { FocusPolicy, ScrollPosition, Visibility, - GlobalTransform + GlobalTransform, VisibilityClass, ZIndex )] From 10833bf9df35fff234b931a5354155add27fc891 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 5 Mar 2025 21:23:40 +0000 Subject: [PATCH 06/64] Changed the `Val::resolve` method to always return always return a value in physical pixels. This required adding an extra parameter for the scale factor. --- crates/bevy_ui/src/geometry.rs | 58 ++++++++++++++++++++------------ crates/bevy_ui/src/layout/mod.rs | 34 ++++++++++--------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index a5749e467a6d7..635dc8ce54d6f 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -255,15 +255,19 @@ pub enum ValArithmeticError { } impl Val { - /// Resolves a [`Val`] from the given context values and returns this as an [`f32`]. - /// The [`Val::Px`] value (if present), `parent_size` and `viewport_size` should all be in the same coordinate space. - /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. + /// Resolves this [`Val`] to a value in physical pixels from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. /// - /// **Note:** If a [`Val::Px`] is resolved, its inner value is returned unchanged. - pub fn resolve(self, parent_size: f32, viewport_size: Vec2) -> Result { + /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. + pub fn resolve( + self, + scale_factor: f32, + parent_size: f32, + viewport_size: Vec2, + ) -> Result { match self { Val::Percent(value) => Ok(parent_size * value / 100.0), - Val::Px(value) => Ok(value), + Val::Px(value) => Ok(value * scale_factor), Val::Vw(value) => Ok(viewport_size.x * value / 100.0), Val::Vh(value) => Ok(viewport_size.y * value / 100.0), Val::VMin(value) => Ok(viewport_size.min_element() * value / 100.0), @@ -697,7 +701,7 @@ mod tests { fn val_evaluate() { let size = 250.; let viewport_size = vec2(1000., 500.); - let result = Val::Percent(80.).resolve(size, viewport_size).unwrap(); + let result = Val::Percent(80.).resolve(1., size, viewport_size).unwrap(); assert_eq!(result, size * 0.8); } @@ -706,7 +710,7 @@ mod tests { fn val_resolve_px() { let size = 250.; let viewport_size = vec2(1000., 500.); - let result = Val::Px(10.).resolve(size, viewport_size).unwrap(); + let result = Val::Px(10.).resolve(1., size, viewport_size).unwrap(); assert_eq!(result, 10.); } @@ -719,33 +723,45 @@ mod tests { for value in (-10..10).map(|value| value as f32) { // for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`. assert_eq!( - Val::Vw(value).resolve(size, viewport_size), - Val::Vh(value).resolve(size, viewport_size) + Val::Vw(value).resolve(1., size, viewport_size), + Val::Vh(value).resolve(1., size, viewport_size) ); assert_eq!( - Val::VMin(value).resolve(size, viewport_size), - Val::VMax(value).resolve(size, viewport_size) + Val::VMin(value).resolve(1., size, viewport_size), + Val::VMax(value).resolve(1., size, viewport_size) ); assert_eq!( - Val::VMin(value).resolve(size, viewport_size), - Val::Vw(value).resolve(size, viewport_size) + Val::VMin(value).resolve(1., size, viewport_size), + Val::Vw(value).resolve(1., size, viewport_size) ); } let viewport_size = vec2(1000., 500.); - assert_eq!(Val::Vw(100.).resolve(size, viewport_size).unwrap(), 1000.); - assert_eq!(Val::Vh(100.).resolve(size, viewport_size).unwrap(), 500.); - assert_eq!(Val::Vw(60.).resolve(size, viewport_size).unwrap(), 600.); - assert_eq!(Val::Vh(40.).resolve(size, viewport_size).unwrap(), 200.); - assert_eq!(Val::VMin(50.).resolve(size, viewport_size).unwrap(), 250.); - assert_eq!(Val::VMax(75.).resolve(size, viewport_size).unwrap(), 750.); + assert_eq!( + Val::Vw(100.).resolve(1., size, viewport_size).unwrap(), + 1000. + ); + assert_eq!( + Val::Vh(100.).resolve(1., size, viewport_size).unwrap(), + 500. + ); + assert_eq!(Val::Vw(60.).resolve(1., size, viewport_size).unwrap(), 600.); + assert_eq!(Val::Vh(40.).resolve(1., size, viewport_size).unwrap(), 200.); + assert_eq!( + Val::VMin(50.).resolve(1., size, viewport_size).unwrap(), + 250. + ); + assert_eq!( + Val::VMax(75.).resolve(1., size, viewport_size).unwrap(), + 750. + ); } #[test] fn val_auto_is_non_evaluable() { let size = 250.; let viewport_size = vec2(1000., 500.); - let resolve_auto = Val::Auto.resolve(size, viewport_size); + let resolve_auto = Val::Auto.resolve(1., size, viewport_size); assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable)); } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 1b74b90f33a5c..6d6186bf4bdd1 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,7 +1,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, - Outline, OverflowAxis, ScrollPosition, Val, + Outline, OverflowAxis, ScrollPosition, }; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, @@ -268,24 +268,28 @@ with UI components as a child of an entity without UI components, your UI layout // don't trigger change detection when only outlines are changed let node = node.bypass_change_detection(); node.outline_width = if style.display != Display::None { - match outline.width { - Val::Px(w) => Val::Px(w / inverse_target_scale_factor), - width => width, - } - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.) + outline + .width + .resolve( + inverse_target_scale_factor.recip(), + node.size().x, + viewport_size, + ) + .unwrap_or(0.) + .max(0.) } else { 0. }; - node.outline_offset = match outline.offset { - Val::Px(offset) => Val::Px(offset / inverse_target_scale_factor), - offset => offset, - } - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.); + node.outline_offset = outline + .offset + .resolve( + inverse_target_scale_factor.recip(), + node.size().x, + viewport_size, + ) + .unwrap_or(0.) + .max(0.); } if transform.translation.truncate() != node_center { From 78a997210bb2d11c7faf0e82772c4fb73a4887d3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 10:46:45 +0100 Subject: [PATCH 07/64] Split up the `Transform` field into separate fields for translation, scale and rotation and made the translation a responsive value. --- crates/bevy_ui/src/ui_node.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 3a50ed589774d..0c69c5af4edb7 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -621,8 +621,19 @@ pub struct Node { /// pub grid_column: GridPlacement, - /// Rotate, scale, skew, or translate an element. - pub transform: Transform, + /// Translation along the x-axis. + /// `Val::percent` values are resolved based on the computed width of the Ui Node. + x_translation: Val, + + /// Translation along the y-axis. + /// `Val::percent` values are resolved based on the computed width of the Ui Node. + y_translation: Val, + + /// Resize an element. A negative value reflects the node in that axis. + scale: Vec2, + + /// Rotate an element clockwise by the given value in radians. + rotation: f32, } impl Node { @@ -666,7 +677,10 @@ impl Node { grid_auto_columns: Vec::new(), grid_column: GridPlacement::DEFAULT, grid_row: GridPlacement::DEFAULT, - transform: Transform::IDENTITY, + x_translation: Val::ZERO, + y_translation: Val::ZERO, + scale: Vec2::ONE, + rotation: 0., }; } From 81d94337a361cc1a593c785896fc30407ad28748 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 11:24:36 +0100 Subject: [PATCH 08/64] Made new `Node` fields `pub`. --- crates/bevy_ui/src/ui_node.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 0c69c5af4edb7..ec7580cf4d85c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -10,7 +10,7 @@ use bevy_render::{ view::VisibilityClass, }; use bevy_sprite::BorderRect; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_transform::components::GlobalTransform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::num::NonZero; @@ -621,19 +621,21 @@ pub struct Node { /// pub grid_column: GridPlacement, - /// Translation along the x-axis. - /// `Val::percent` values are resolved based on the computed width of the Ui Node. - x_translation: Val, + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x_translation: Val, - /// Translation along the y-axis. - /// `Val::percent` values are resolved based on the computed width of the Ui Node. - y_translation: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub y_translation: Val, - /// Resize an element. A negative value reflects the node in that axis. - scale: Vec2, + /// Resize the node. A negative value reflects the node in that axis. + pub scale: Vec2, - /// Rotate an element clockwise by the given value in radians. - rotation: f32, + /// Rotate the node clockwise by the given value in radians. + pub rotation: f32, } impl Node { From abeda2ba900f3a1908cedc482cecfec930e6c189 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 11:25:28 +0100 Subject: [PATCH 09/64] Updated layout to use new transform fields. --- crates/bevy_ui/src/layout/convert.rs | 11 ++++++---- crates/bevy_ui/src/layout/mod.rs | 31 ++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 8f64f697e8c6d..cc3e5aa8251d4 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -448,7 +448,7 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { - use bevy_transform::components::Transform; + use bevy_math::Vec2; use super::*; @@ -524,9 +524,12 @@ mod tests { ], grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), - transform: Transform::IDENTITY, + x_translation: Val::ZERO, + y_translation: Val::ZERO, + scale: Vec2::ONE, + rotation: 0., }; - let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); + let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -664,7 +667,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.)); + let context = LayoutContext::new(2.0, Vec2::new(800., 600.)); let cases = [ (Val::Auto, LengthPercentage::Length(0.)), (Val::Percent(1.), LengthPercentage::Percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index c1cf5f0571642..d1a81607af79d 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ world::Ref, }; -use bevy_math::{Mat4, Vec2}; +use bevy_math::{Affine3A, Quat, Vec2, Vec3}; use bevy_sprite::BorderRect; use bevy_transform::components::GlobalTransform; use thiserror::Error; @@ -176,7 +176,7 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, None, - Mat4::IDENTITY, + Affine3A::IDENTITY, &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), @@ -192,7 +192,7 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, root_size: Option, - mut transform: Mat4, + mut transform: Affine3A, node_update_query: &mut Query<( &mut ComputedNode, &Node, @@ -256,17 +256,30 @@ with UI components as a child of an entity without UI components, your UI layout node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); - let mut node_transform = style.transform; - node_transform.translation /= inverse_target_scale_factor; - transform *= - node_transform.compute_matrix() * Mat4::from_translation(node_center.extend(0.)); + let viewport_size = root_size.unwrap_or(node.size); + + let resolve_translation = |mut val: Val, extent: f32, viewport_size: Vec2| { + if let Val::Px(ref mut value) = val { + *value /= inverse_target_scale_factor + } + val.resolve(extent, viewport_size).unwrap_or(0.) + }; + + let node_transform = Affine3A::from_scale_rotation_translation( + style.scale.extend(1.), + Quat::from_rotation_z(style.rotation), + Vec3::new( + resolve_translation(style.x_translation, layout_size.x, viewport_size), + resolve_translation(style.y_translation, layout_size.y, viewport_size), + 0., + ), + ); + transform *= node_transform * Affine3A::from_translation(node_center.extend(0.)); let new_global_transform = GlobalTransform::from(transform); if new_global_transform != *global_transform { *global_transform = new_global_transform; } - let viewport_size = root_size.unwrap_or(node.size); - if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius node.bypass_change_detection().border_radius = border_radius.resolve( From 2c98ce5dbab1817ebcb039fe5926f16cc8467f59 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 11:25:37 +0100 Subject: [PATCH 10/64] Fixed examples --- examples/testbed/full_ui.rs | 5 ++++- examples/ui/overflow_debug.rs | 12 +++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 4c28cf04cd60f..dfdaace5748a0 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -207,7 +207,10 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( ImageNode::new(asset_server.load("branding/bevy_logo_light.png")), // Uses the transform to rotate the logo image by 45 degrees - Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)), + Node { + rotation: 0.25 * PI, + ..Default::default() + }, BorderRadius::all(Val::Px(10.)), Outline { width: Val::Px(2.), diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 41a324ee3e9b4..147f3d3a63c10 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -4,7 +4,6 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*, ui::widget: use std::f32::consts::{FRAC_PI_2, PI, TAU}; const CONTAINER_SIZE: f32 = 150.0; -const HALF_CONTAINER_SIZE: f32 = CONTAINER_SIZE / 2.0; const LOOP_LENGTH: f32 = 4.0; fn main() { @@ -49,8 +48,8 @@ struct Move; impl UpdateTransform for Move { fn update(&self, t: f32, node: &mut Node) { - node.transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; - node.transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; + node.x_translation = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.); + node.y_translation = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.); } } @@ -59,8 +58,8 @@ struct Scale; impl UpdateTransform for Scale { fn update(&self, t: f32, node: &mut Node) { - node.transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); - node.transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); + node.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); + node.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); } } @@ -69,8 +68,7 @@ struct Rotate; impl UpdateTransform for Rotate { fn update(&self, t: f32, node: &mut Node) { - node.transform.rotation = - Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians()); + node.rotation = (ops::cos(t * TAU) * 45.0).to_radians(); } } From 54fb3ac765edfbcd5c6b6fdca2663faf190b4696 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 11:53:35 +0100 Subject: [PATCH 11/64] negate rotation before applying it in layout updates --- crates/bevy_ui/src/layout/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index d1a81607af79d..6ca372513e553 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -267,7 +267,7 @@ with UI components as a child of an entity without UI components, your UI layout let node_transform = Affine3A::from_scale_rotation_translation( style.scale.extend(1.), - Quat::from_rotation_z(style.rotation), + Quat::from_rotation_z(-style.rotation), Vec3::new( resolve_translation(style.x_translation, layout_size.x, viewport_size), resolve_translation(style.y_translation, layout_size.y, viewport_size), From 8825583c784a5ec28a6c949d3881da1c8551d1b4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 11:54:27 +0100 Subject: [PATCH 12/64] Added `TransformedNode` type --- crates/bevy_ui/src/ui_node.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ec7580cf4d85c..97a0607eaf7ab 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -230,6 +230,14 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + #[inline] + pub const fn with_transform<'a>( + &'a self, + transform: &'a GlobalTransform, + ) -> TransformedNode<'a> { + TransformedNode(self, transform) + } } impl ComputedNode { @@ -253,6 +261,14 @@ impl Default for ComputedNode { } } +pub struct TransformedNode<'a>(&'a ComputedNode, &'a GlobalTransform); + +impl TransformedNode<'_> { + pub fn relative_position(&self, point: Vec2) -> Vec2 { + self.1.transform_point(point.extend(0.)).truncate() + } +} + /// The scroll position of the node. /// /// Updating the values of `ScrollPosition` will reposition the children of the node by the offset amount. From 085316ff8de67fab4dccca479cee15e639d7de8f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 12:39:54 +0100 Subject: [PATCH 13/64] Addeded `transform: Affine2` field to `ComputedNode`. Removed `GlobalTransform` require from `Node`. Updated layout and render modules. --- crates/bevy_ui/src/layout/mod.rs | 27 +++++------ crates/bevy_ui/src/render/mod.rs | 82 +++++++++++++------------------- crates/bevy_ui/src/ui_node.rs | 23 ++------- 3 files changed, 48 insertions(+), 84 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 6ca372513e553..15523080e2ef4 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -13,9 +13,8 @@ use bevy_ecs::{ world::Ref, }; -use bevy_math::{Affine3A, Quat, Vec2, Vec3}; +use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -85,7 +84,6 @@ pub fn ui_layout_system( mut node_update_query: Query<( &mut ComputedNode, &Node, - &mut GlobalTransform, Option<&LayoutConfig>, Option<&BorderRadius>, Option<&Outline>, @@ -176,7 +174,7 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, None, - Affine3A::IDENTITY, + Affine2::IDENTITY, &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), @@ -192,11 +190,10 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, root_size: Option, - mut transform: Affine3A, + mut transform: Affine2, node_update_query: &mut Query<( &mut ComputedNode, &Node, - &mut GlobalTransform, Option<&LayoutConfig>, Option<&BorderRadius>, Option<&Outline>, @@ -210,7 +207,6 @@ with UI components as a child of an entity without UI components, your UI layout if let Ok(( mut node, style, - mut global_transform, maybe_layout_config, maybe_border_radius, maybe_outline, @@ -265,19 +261,18 @@ with UI components as a child of an entity without UI components, your UI layout val.resolve(extent, viewport_size).unwrap_or(0.) }; - let node_transform = Affine3A::from_scale_rotation_translation( - style.scale.extend(1.), - Quat::from_rotation_z(-style.rotation), - Vec3::new( + let node_transform = Affine2::from_scale_angle_translation( + style.scale, + -style.rotation, + Vec2::new( resolve_translation(style.x_translation, layout_size.x, viewport_size), resolve_translation(style.y_translation, layout_size.y, viewport_size), - 0., ), ); - transform *= node_transform * Affine3A::from_translation(node_center.extend(0.)); - let new_global_transform = GlobalTransform::from(transform); - if new_global_transform != *global_transform { - *global_transform = new_global_transform; + transform *= node_transform * Affine2::from_translation(node_center); + + if transform != node.transform { + node.transform = transform; } if let Some(border_radius) = maybe_border_radius { diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8cb61cde217e8..7a96cfec91039 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -21,7 +21,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles}; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::renderer::RenderContext; @@ -228,7 +228,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -238,7 +238,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -329,7 +329,6 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -340,9 +339,7 @@ pub fn extract_uinode_background_colors( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, transform, inherited_visibility, clip, camera, background_color) in - &uinode_query - { + for (entity, uinode, inherited_visibility, clip, camera, background_color) in &uinode_query { // Skip invisible backgrounds if !inherited_visibility.get() || background_color.0.is_fully_transparent() @@ -368,7 +365,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: uinode.transform, flip_x: false, flip_y: false, border: uinode.border(), @@ -388,7 +385,6 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -398,7 +394,7 @@ pub fn extract_uinode_images( camera_map: Extract, ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &uinode_query { + for (entity, uinode, inherited_visibility, clip, camera, image) in &uinode_query { // Skip invisible images if !inherited_visibility.get() || image.color.is_fully_transparent() @@ -452,7 +448,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: uinode.transform, flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -472,7 +468,6 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -488,7 +483,6 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, inherited_visibility, maybe_clip, camera, @@ -520,7 +514,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: global_transform.compute_matrix(), + transform: computed_node.transform, flip_x: false, flip_y: false, border: computed_node.border(), @@ -552,7 +546,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: computed_node.transform, atlas_scaling: None, flip_x: false, flip_y: false, @@ -700,7 +694,6 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -715,16 +708,8 @@ pub fn extract_text_sections( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - global_transform, - inherited_visibility, - clip, - camera, - computed_block, - text_layout_info, - ) in &uinode_query + for (entity, uinode, inherited_visibility, clip, camera, computed_block, text_layout_info) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -735,8 +720,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = uinode.transform * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -754,7 +738,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -799,7 +783,6 @@ pub fn extract_text_shadows( Entity, &ComputedNode, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -812,16 +795,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -832,9 +807,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let transform = uinode.transform + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -853,7 +828,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -1140,8 +1115,13 @@ pub fn prepare_uinodes( let rect_size = uinode_rect.size().extend(1.0); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + { + transform + .transform_point2(pos.truncate() * rect_size.truncate()) + .extend(0.) + } + }); let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); // Calculate the effect of clipping @@ -1183,7 +1163,8 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = + transform.transform_vector2(uinode_rect.size()); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1292,7 +1273,10 @@ pub fn prepare_uinodes( // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos.truncate() * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1327,7 +1311,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size.truncate()); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 97a0607eaf7ab..bc5d56033e9bc 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2,7 +2,7 @@ use crate::{FocusPolicy, UiRect, Val}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; -use bevy_math::{vec4, Rect, UVec2, Vec2, Vec4Swizzles}; +use bevy_math::{vec4, Affine2, Rect, UVec2, Vec2, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ camera::{Camera, RenderTarget}, @@ -10,7 +10,6 @@ use bevy_render::{ view::VisibilityClass, }; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::num::NonZero; @@ -76,6 +75,8 @@ pub struct ComputedNode { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub inverse_scale_factor: f32, + /// Transform for this node + pub transform: Affine2, } impl ComputedNode { @@ -230,14 +231,6 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } - - #[inline] - pub const fn with_transform<'a>( - &'a self, - transform: &'a GlobalTransform, - ) -> TransformedNode<'a> { - TransformedNode(self, transform) - } } impl ComputedNode { @@ -252,6 +245,7 @@ impl ComputedNode { border: BorderRect::ZERO, padding: BorderRect::ZERO, inverse_scale_factor: 1., + transform: Affine2::IDENTITY, }; } @@ -261,14 +255,6 @@ impl Default for ComputedNode { } } -pub struct TransformedNode<'a>(&'a ComputedNode, &'a GlobalTransform); - -impl TransformedNode<'_> { - pub fn relative_position(&self, point: Vec2) -> Vec2 { - self.1.transform_point(point.extend(0.)).truncate() - } -} - /// The scroll position of the node. /// /// Updating the values of `ScrollPosition` will reposition the children of the node by the offset amount. @@ -346,7 +332,6 @@ impl From for ScrollPosition { FocusPolicy, ScrollPosition, Visibility, - GlobalTransform, VisibilityClass, ZIndex )] From 0566eeb1d59844792155d211c4e6b4b9ab562bcd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 12:59:54 +0100 Subject: [PATCH 14/64] Updated remaining rendering modules --- crates/bevy_ui/src/render/box_shadow.rs | 24 ++++++++++--------- crates/bevy_ui/src/render/debug_overlay.rs | 5 ++-- crates/bevy_ui/src/render/mod.rs | 18 +++++++------- .../src/render/ui_material_pipeline.rs | 21 ++++++++-------- .../src/render/ui_texture_slice_pipeline.rs | 19 +++++++-------- 5 files changed, 43 insertions(+), 44 deletions(-) diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 7ed98550380e7..80ae75849b826 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -18,7 +18,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ @@ -29,7 +29,6 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSet, }; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -215,7 +214,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub bounds: Vec2, pub clip: Option, pub extracted_camera_entity: Entity, @@ -240,7 +239,6 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, - &GlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -251,7 +249,7 @@ pub fn extract_shadows( ) { let mut mapping = camera_map.get_mapper(); - for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query { + for (entity, uinode, visibility, box_shadow, clip, camera) in &box_shadow_query { // Skip if no visible shadows if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() { continue; @@ -306,7 +304,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), + transform: uinode.transform * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), @@ -409,11 +407,15 @@ pub fn prepare_shadows( .get(item.index) .filter(|n| item.entity() == n.render_entity) { - let rect_size = box_shadow.bounds.extend(1.0); + let rect_size = box_shadow.bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + box_shadow + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -447,7 +449,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size); + let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -496,7 +498,7 @@ pub fn prepare_shadows( size: box_shadow.size.into(), radius, blur: box_shadow.blur_radius, - bounds: rect_size.xy().into(), + bounds: rect_size.into(), }); } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index 79001f3ba1982..8486732b91568 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -63,7 +63,6 @@ pub fn extract_debug_overlay( &ComputedNode, &InheritedVisibility, Option<&CalculatedClip>, - &GlobalTransform, &ComputedNodeTarget, )>, >, @@ -75,7 +74,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { + for (entity, uinode, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -101,7 +100,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: uinode.transform, flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 7a96cfec91039..6cbb5384efc76 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -21,7 +21,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3Swizzles}; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::renderer::RenderContext; @@ -892,11 +892,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1118,11 +1118,11 @@ pub fn prepare_uinodes( let positions = QUAD_VERTEX_POSITIONS.map(|pos| { { transform - .transform_point2(pos.truncate() * rect_size.truncate()) + .transform_point2(pos * rect_size.truncate()) .extend(0.) } }); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size.xy()); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1275,7 +1275,7 @@ pub fn prepare_uinodes( let positions = QUAD_VERTEX_POSITIONS.map(|pos| { glyph .transform - .transform_point2(pos.truncate() * glyph_rect.size()) + .transform_point2(pos * glyph_rect.size()) .extend(0.) }); diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index fb893b390ea62..03a701ddc2c89 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; use bevy_render::{ extract_component::ExtractComponentPlugin, @@ -24,7 +24,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSet, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; pub const UI_MATERIAL_SHADER_HANDLE: Handle = @@ -337,7 +336,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -372,7 +371,6 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -383,9 +381,7 @@ pub fn extract_ui_material_nodes( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, computed_node, transform, handle, inherited_visibility, clip, camera) in - uinode_query.iter() - { + for (entity, computed_node, handle, inherited_visibility, clip, camera) in uinode_query.iter() { // skip invisible nodes if !inherited_visibility.get() || computed_node.is_empty() { continue; @@ -403,7 +399,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: computed_node.transform, material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -475,10 +471,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -512,7 +511,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index d8da926709c82..e9f5b8ba1a1cf 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +25,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSet, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -224,7 +223,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -252,7 +251,6 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -263,7 +261,7 @@ pub fn extract_ui_texture_slices( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &slicers_query { + for (entity, uinode, inherited_visibility, clip, camera, image) in &slicers_query { // Skip invisible images if !inherited_visibility.get() || image.color.is_fully_transparent() @@ -312,7 +310,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: uinode.transform, color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -503,11 +501,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -542,7 +541,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π From 3dfcdfc07ea449974d66a37b6202f08150bd2d06 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 13:08:52 +0100 Subject: [PATCH 15/64] Fixed update module --- crates/bevy_ui/src/update.rs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd1ae..fbfee23e88038 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -17,18 +17,12 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( mut commands: Commands, root_nodes: UiRootNodes, - mut node_query: Query<( - &Node, - &ComputedNode, - &GlobalTransform, - Option<&mut CalculatedClip>, - )>, + mut node_query: Query<(&Node, &ComputedNode, Option<&mut CalculatedClip>)>, ui_children: UiChildren, ) { for root_node in root_nodes.iter() { @@ -45,18 +39,11 @@ pub fn update_clipping_system( fn update_clipping( commands: &mut Commands, ui_children: &UiChildren, - node_query: &mut Query<( - &Node, - &ComputedNode, - &GlobalTransform, - Option<&mut CalculatedClip>, - )>, + node_query: &mut Query<(&Node, &ComputedNode, Option<&mut CalculatedClip>)>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) - else { + let Ok((node, computed_node, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +78,8 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = + Rect::from_center_size(computed_node.transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // From 6c7442816011b288f1e8cdaf580b36a50b31e2fc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 13:09:01 +0100 Subject: [PATCH 16/64] Fixed accessibility module --- crates/bevy_ui/src/accessibility.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 3b0e6538f00f0..fd6ecdb8e0006 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -13,9 +13,7 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_math::Vec3Swizzles; use bevy_render::camera::CameraUpdateSystem; -use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; @@ -36,16 +34,10 @@ fn calc_label( name.map(String::into_boxed_str) } -fn calc_bounds( - mut nodes: Query<( - &mut AccessibilityNode, - Ref, - Ref, - )>, -) { - for (mut accessible, node, transform) in &mut nodes { - if node.is_changed() || transform.is_changed() { - let center = transform.translation().xy(); +fn calc_bounds(mut nodes: Query<(&mut AccessibilityNode, Ref)>) { + for (mut accessible, node) in &mut nodes { + if node.is_changed() { + let center = node.transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; From 1441201cc9b9ed89ae0b6950b3bc416ccc8b3817 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 13:26:44 +0100 Subject: [PATCH 17/64] fixed picking --- crates/bevy_ui/src/focus.rs | 8 ++------ crates/bevy_ui/src/picking_backend.rs | 7 +------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 9242bf1380a38..ef5dae80e9812 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -12,7 +12,6 @@ use bevy_math::{Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_transform::components::GlobalTransform; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -133,7 +132,6 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, @@ -234,10 +232,8 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); + let node_rect = + Rect::from_center_size(node.node.transform.translation, 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/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index d0b60d9a469a9..bd0592324ad7e 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -31,7 +31,6 @@ use bevy_math::{Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; -use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; use bevy_picking::backend::prelude::*; @@ -91,7 +90,6 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, pickable: Option<&'static Pickable>, calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, @@ -186,10 +184,7 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); + let node_rect = Rect::from_center_size(node.node.transform.translation, node.node.size()); // Nodes with Display::None have a (0., 0.) logical rect and can be ignored if node_rect.size() == Vec2::ZERO { From 72438676c8b6a6da5b79de65e72843cba2dfb03d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 13:34:08 +0100 Subject: [PATCH 18/64] Updated doc comments --- crates/bevy_ui/src/ui_node.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index bc5d56033e9bc..e1f622b6e4a13 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -75,7 +75,9 @@ pub struct ComputedNode { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub inverse_scale_factor: f32, - /// Transform for this node + /// Transform for the node + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. pub transform: Affine2, } From 96ddbc8f479cb880ab8d75cf95c9c5fa4b992ca2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 14:12:59 +0100 Subject: [PATCH 19/64] Added contains_point helper functions to ComputedNode --- crates/bevy_ui/src/focus.rs | 31 +------------------------- crates/bevy_ui/src/picking_backend.rs | 8 ++----- crates/bevy_ui/src/ui_node.rs | 32 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index ef5dae80e9812..b3fa4fdadc127 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -262,13 +262,7 @@ pub fn ui_focus_system( }; let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| { - pick_rounded_rect( - *point - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) - }); + && cursor_position.is_some_and(|point| node.node.contains_point(*point)); // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position @@ -330,26 +324,3 @@ pub fn ui_focus_system( } } } - -// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with -// the given size and border radius. -// -// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. -pub(crate) fn pick_rounded_rect( - point: Vec2, - size: Vec2, - border_radius: ResolvedBorderRadius, -) -> bool { - let [top, bottom] = if point.x < 0. { - [border_radius.top_left, border_radius.bottom_left] - } else { - [border_radius.top_right, border_radius.bottom_right] - }; - let r = if point.y < 0. { top } else { bottom }; - - let corner_to_point = point.abs() - 0.5 * size; - let q = corner_to_point + r; - let l = q.max(Vec2::ZERO).length(); - let m = q.max_element().min(0.); - l + m - r < 0. -} diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index bd0592324ad7e..0eeb2b1002e36 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,7 +24,7 @@ #![deny(missing_docs)] -use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; +use crate::{prelude::*, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::{Rect, Vec2}; @@ -208,11 +208,7 @@ pub fn ui_picking( if visible_rect .normalize(node_rect) .contains(relative_cursor_position) - && pick_rounded_rect( - *cursor_position - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) + && node.node.contains_point(*cursor_position) { hit_nodes .entry((camera_entity, *pointer_id)) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index e1f622b6e4a13..1d061b3f5a9a5 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -233,6 +233,38 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, point: Vec2) -> bool { + let local_point = self.transform.inverse().transform_point2(point); + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + pub fn transform_point(&self, point: Vec2) -> Vec2 { + self.transform.inverse().transform_point2(point) - 0.5 * self.size + } + + pub fn normalize_point(&self, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then_some(self.transform_point(point) / self.size) + } } impl ComputedNode { From 8112c795ca852cd2196f12fb55f0aca434bb6544 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 14:13:30 +0100 Subject: [PATCH 20/64] Removed unused --- crates/bevy_ui/src/focus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index b3fa4fdadc127..c5efb0c3907d6 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,4 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; +use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, UiStack}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, From 430b657cc792d94b5f1e9fe9de043d4358b2b733 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 16:45:48 +0100 Subject: [PATCH 21/64] updated ComputedNode::transform doc comment --- crates/bevy_ui/src/ui_node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1d061b3f5a9a5..1b5799b2e574c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -75,7 +75,7 @@ pub struct ComputedNode { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub inverse_scale_factor: f32, - /// Transform for the node + /// Transform from coordinates local to the node to global UI coordinates /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub transform: Affine2, From 1f3d3afa0df59892998fea8e44b26c18574a03d3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 23:12:19 +0100 Subject: [PATCH 22/64] Fixed `focus` module. Removed `normalized_visible_node_rect` field from `RelativeCursorPosition` --- crates/bevy_ui/src/focus.rs | 132 +++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 26 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index c5efb0c3907d6..3cf35beb04089 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,7 +1,8 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, UiStack}; +use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, + hierarchy::ChildOf, prelude::{Component, With}, query::QueryData, reflect::ReflectComponent, @@ -12,6 +13,7 @@ use bevy_math::{Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; +use bevy_sprite::BorderRect; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -80,21 +82,11 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// Visible area of the Node relative to the size of the entire Node. - pub normalized_visible_node_rect: Rect, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, } -impl RelativeCursorPosition { - /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.normalized - .is_some_and(|position| self.normalized_visible_node_rect.contains(position)) - } -} - /// Describes whether the node should block interactions with lower nodes #[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] @@ -135,7 +127,6 @@ pub struct NodeQuery { interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -152,6 +143,8 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, + clipping_query: Query<(&ComputedNode, &Node)>, + child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -232,16 +225,65 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = - Rect::from_center_size(node.node.transform.translation, node.node.size()); + let cursor_position = camera_cursor_positions.get(&camera_entity); + + fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &Node)>, + child_of_query: &Query<&ChildOf>, + ) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, node)) = clipping_query.get(parent) { + // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); + let mut clip_rect = + Rect::from_center_size(Vec2::ZERO, 0.5 * computed_node.size); - let cursor_position = camera_cursor_positions.get(&camera_entity); + let clip_inset = match node.overflow_clip_margin.visual_box { + crate::OverflowClipBox::BorderBox => BorderRect::ZERO, + crate::OverflowClipBox::ContentBox => computed_node.content_inset(), + crate::OverflowClipBox::PaddingBox => computed_node.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if node.overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if node.overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + if !clip_rect + .contains(computed_node.transform.inverse().transform_point2(point)) + { + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // point unclipped by all ancestors + true + } + + let contains_cursor = cursor_position.is_some_and(|point| { + node.node.contains_point(*point) + && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) + }); + + // parent_query.get(current) + + // while let Ok(parent) = parent_query.get(current) { + + // current = parent.0; + // } // The mouse position relative to the node // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner @@ -250,20 +292,15 @@ pub fn ui_focus_system( // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) + node.node.normalize_point(*cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - normalized_visible_node_rect: visible_rect.normalize(node_rect), normalized: relative_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| node.node.contains_point(*point)); - // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position { @@ -324,3 +361,46 @@ pub fn ui_focus_system( } } } + +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, node)) = clipping_query.get(parent) { + // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists + + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, 0.5 * computed_node.size); + + let clip_inset = match node.overflow_clip_margin.visual_box { + crate::OverflowClipBox::BorderBox => BorderRect::ZERO, + crate::OverflowClipBox::ContentBox => computed_node.content_inset(), + crate::OverflowClipBox::PaddingBox => computed_node.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if node.overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if node.overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + if !clip_rect.contains(computed_node.transform.inverse().transform_point2(point)) { + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // point unclipped by all ancestors + true +} From bcb875740b12a4bb78879bd501fd567748d48b1d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 24 Apr 2025 23:29:08 +0100 Subject: [PATCH 23/64] Changed picking_backend to check mouse over recursively --- crates/bevy_ui/src/focus.rs | 55 +-------------------------- crates/bevy_ui/src/picking_backend.rs | 33 +++++++++------- 2 files changed, 21 insertions(+), 67 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 3cf35beb04089..86052c62506a5 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,4 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack}; +use crate::{ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, @@ -227,64 +227,11 @@ pub fn ui_focus_system( let cursor_position = camera_cursor_positions.get(&camera_entity); - fn clip_check_recursive( - point: Vec2, - entity: Entity, - clipping_query: &Query<'_, '_, (&ComputedNode, &Node)>, - child_of_query: &Query<&ChildOf>, - ) -> bool { - if let Ok(child_of) = child_of_query.get(entity) { - let parent = child_of.0; - if let Ok((computed_node, node)) = clipping_query.get(parent) { - // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - - let mut clip_rect = - Rect::from_center_size(Vec2::ZERO, 0.5 * computed_node.size); - - let clip_inset = match node.overflow_clip_margin.visual_box { - crate::OverflowClipBox::BorderBox => BorderRect::ZERO, - crate::OverflowClipBox::ContentBox => computed_node.content_inset(), - crate::OverflowClipBox::PaddingBox => computed_node.border(), - }; - - clip_rect.min.x += clip_inset.left; - clip_rect.min.y += clip_inset.top; - clip_rect.max.x -= clip_inset.right; - clip_rect.max.y -= clip_inset.bottom; - - if node.overflow.x == OverflowAxis::Visible { - clip_rect.min.x = -f32::INFINITY; - clip_rect.max.x = f32::INFINITY; - } - if node.overflow.y == OverflowAxis::Visible { - clip_rect.min.y = -f32::INFINITY; - clip_rect.max.y = f32::INFINITY; - } - - if !clip_rect - .contains(computed_node.transform.inverse().transform_point2(point)) - { - return false; - } - } - return clip_check_recursive(point, parent, clipping_query, child_of_query); - } - // point unclipped by all ancestors - true - } - let contains_cursor = cursor_position.is_some_and(|point| { node.node.contains_point(*point) && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) }); - // parent_query.get(current) - - // while let Ok(parent) = parent_query.get(current) { - - // current = parent.0; - // } - // The mouse position relative to the node // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 0eeb2b1002e36..271ab11859949 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,7 +24,7 @@ #![deny(missing_docs)] -use crate::{prelude::*, UiStack}; +use crate::{clip_check_recursive, prelude::*, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::{Rect, Vec2}; @@ -91,7 +91,6 @@ pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, pickable: Option<&'static Pickable>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -113,6 +112,8 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, + clipping_query: Query<(&ComputedNode, &Node)>, + child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); @@ -191,11 +192,11 @@ pub fn ui_picking( continue; } - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); + // // Intersect with the calculated clip rect to find the bounds of the visible region of the node + // let visible_rect = node + // .calculated_clip + // .map(|clip| node_rect.intersect(clip.clip)) + // .unwrap_or(node_rect); let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); @@ -203,13 +204,19 @@ pub fn ui_picking( // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); + let Some(relative_cursor_position) = node.node.normalize_point(*cursor_position) else { + continue; + }; + + let contains_cursor = node.node.contains_point(*cursor_position) + && clip_check_recursive( + *cursor_position, + *node_entity, + &clipping_query, + &child_of_query, + ); - if visible_rect - .normalize(node_rect) - .contains(relative_cursor_position) - && node.node.contains_point(*cursor_position) - { + if contains_cursor { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() From bd59b49807c1c1dd259eb64344b622c6979ba295 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 10:17:37 +0100 Subject: [PATCH 24/64] Removed comments. --- crates/bevy_ui/src/picking_backend.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 271ab11859949..d35ba3428857a 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -192,12 +192,6 @@ pub fn ui_picking( continue; } - // // Intersect with the calculated clip rect to find the bounds of the visible region of the node - // let visible_rect = node - // .calculated_clip - // .map(|clip| node_rect.intersect(clip.clip)) - // .unwrap_or(node_rect); - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); // The mouse position relative to the node From 09879ecfca9625901ad7801ad3e4cb4ca5d302fd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 10:29:28 +0100 Subject: [PATCH 25/64] Updated `ui_root_node_should_act_like_position_absolute` test --- crates/bevy_ui/src/layout/mod.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 15523080e2ef4..606b31780ab5d 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -367,7 +367,7 @@ mod tests { use bevy_core_pipeline::core_2d::Camera2d; use bevy_ecs::{prelude::*, system::RunSystemOnce}; use bevy_image::Image; - use bevy_math::{Rect, UVec2, Vec2}; + use bevy_math::{Affine2, Rect, UVec2, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; @@ -699,23 +699,21 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without>() + .query_filtered::<(Entity, &ComputedNode), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node, global_transform)| { - let current_rect = Rect::from_center_size( - global_transform.translation().truncate(), - node.size(), - ); + |option_rect, (entity, node)| { + let current_rect = + Rect::from_center_size(node.transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - global_transform.affine(), - GlobalTransform::default().affine(), - "root ui node {entity} global transform is not populated" + node.transform, + Affine2::default(), + "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { return Some((current_rect, false)); From af637c46b41f78535553ce0800c887e80994759c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 10:31:13 +0100 Subject: [PATCH 26/64] Removed unused --- crates/bevy_ui/src/layout/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 606b31780ab5d..f814856dbfa2e 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -371,10 +371,7 @@ mod tests { use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::{ - prelude::GlobalTransform, - systems::{propagate_parent_transforms, sync_simple_transforms}, - }; + use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; use bevy_utils::prelude::default; use bevy_window::{ PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, From 7f5f9985f1e1be2a552be698f4f9b68ecf117a0b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 10:54:58 +0100 Subject: [PATCH 27/64] Added `UiVec`, `UiTransform`, `UiGlobalTransform` types. --- crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/ui_transform.rs | 91 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 crates/bevy_ui/src/ui_transform.rs diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 56462b6952f23..203e5d1e01f4a 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -17,6 +17,7 @@ pub mod widget; #[cfg(feature = "bevy_ui_picking_backend")] pub mod picking_backend; +pub mod ui_transform; use bevy_derive::{Deref, DerefMut}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000000..1ff3a1ab0b156 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,91 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +pub struct UiVec { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + y: Val, +} + +impl UiVec { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } +} + +impl Default for UiVec { + fn default() -> Self { + Self::ZERO + } +} + +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiTransform { + /// Translate the node. + pub translation: UiVec, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise by the given value in radians. + pub rotation: f32, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: UiVec::ZERO, + scale: Vec2::ONE, + rotation: 0., + }; +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} From ebeaed2c5a453b11b51e753cf68a48d3359ea64b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 11:08:01 +0100 Subject: [PATCH 28/64] Update `UiGlobalTransform` in ui_layout_system --- crates/bevy_ui/src/layout/mod.rs | 26 +++++++++++++++++--------- crates/bevy_ui/src/ui_transform.rs | 10 +++++++--- crates/bevy_ui/src/update.rs | 18 +++++++++++++++--- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index f814856dbfa2e..b26c13d015c42 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,6 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::{UiGlobalTransform, UiTransform}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, Outline, OverflowAxis, ScrollPosition, Val, }; @@ -83,6 +84,8 @@ pub fn ui_layout_system( ui_children: UiChildren, mut node_update_query: Query<( &mut ComputedNode, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -190,9 +193,11 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, root_size: Option, - mut transform: Affine2, + mut propagated_transform: Affine2, node_update_query: &mut Query<( &mut ComputedNode, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -206,6 +211,8 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, + transform, + mut global_transform, style, maybe_layout_config, maybe_border_radius, @@ -262,17 +269,18 @@ with UI components as a child of an entity without UI components, your UI layout }; let node_transform = Affine2::from_scale_angle_translation( - style.scale, - -style.rotation, + transform.scale, + -transform.rotation, Vec2::new( - resolve_translation(style.x_translation, layout_size.x, viewport_size), - resolve_translation(style.y_translation, layout_size.y, viewport_size), + resolve_translation(transform.translation.x, layout_size.x, viewport_size), + resolve_translation(transform.translation.y, layout_size.y, viewport_size), ), ); - transform *= node_transform * Affine2::from_translation(node_center); + propagated_transform *= node_transform * Affine2::from_translation(node_center); - if transform != node.transform { - node.transform = transform; + if propagated_transform != node.transform { + node.transform = propagated_transform; + global_transform.0 = propagated_transform; } if let Some(border_radius) = maybe_border_radius { @@ -347,7 +355,7 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface, use_rounding, Some(viewport_size), - transform, + propagated_transform, node_update_query, ui_children, inverse_target_scale_factor, diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 1ff3a1ab0b156..c904eab537cfd 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -11,11 +11,11 @@ pub struct UiVec { /// Translate the node along the x-axis. /// `Val::Percent` values are resolved based on the computed width of the Ui Node. /// `Val::Auto` is resolved to `0.`. - x: Val, + pub x: Val, /// Translate the node along the y-axis. /// `Val::Percent` values are resolved based on the computed width of the Ui Node. /// `Val::Auto` is resolved to `0.`. - y: Val, + pub y: Val, } impl UiVec { @@ -37,6 +37,10 @@ impl UiVec { y: Val::Percent(y), } } + + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } } impl Default for UiVec { @@ -82,7 +86,7 @@ impl Default for UiTransform { derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct UiGlobalTransform(Affine2); +pub struct UiGlobalTransform(pub Affine2); impl Default for UiGlobalTransform { fn default() -> Self { diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index fbfee23e88038..9fa399f08444a 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -22,7 +23,12 @@ use bevy_sprite::BorderRect; pub fn update_clipping_system( mut commands: Commands, root_nodes: UiRootNodes, - mut node_query: Query<(&Node, &ComputedNode, Option<&mut CalculatedClip>)>, + mut node_query: Query<( + &Node, + &ComputedNode, + &UiGlobalTransform, + Option<&mut CalculatedClip>, + )>, ui_children: UiChildren, ) { for root_node in root_nodes.iter() { @@ -39,11 +45,17 @@ pub fn update_clipping_system( fn update_clipping( commands: &mut Commands, ui_children: &UiChildren, - node_query: &mut Query<(&Node, &ComputedNode, Option<&mut CalculatedClip>)>, + node_query: &mut Query<( + &Node, + &ComputedNode, + &UiGlobalTransform, + Option<&mut CalculatedClip>, + )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, maybe_calculated_clip)) = node_query.get_mut(entity) else { + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) + else { return; }; From 8def248aa3e9f88f316ac41a3ea7817da8977955 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 11:59:28 +0100 Subject: [PATCH 29/64] Update everything to use new UI transform components --- crates/bevy_ui/src/accessibility.rs | 15 ++++-- crates/bevy_ui/src/focus.rs | 27 ++++++++--- crates/bevy_ui/src/layout/convert.rs | 4 -- crates/bevy_ui/src/layout/mod.rs | 16 +++---- crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/picking_backend.rs | 13 ++++-- crates/bevy_ui/src/render/box_shadow.rs | 6 ++- crates/bevy_ui/src/render/mod.rs | 41 ++++++++++++----- .../src/render/ui_material_pipeline.rs | 8 +++- .../src/render/ui_texture_slice_pipeline.rs | 6 ++- crates/bevy_ui/src/ui_node.rs | 46 ++++++------------- crates/bevy_ui/src/ui_transform.rs | 45 +++++++++++++++++- crates/bevy_ui/src/update.rs | 3 +- examples/testbed/full_ui.rs | 3 ++ examples/ui/overflow_debug.rs | 24 +++++----- 15 files changed, 163 insertions(+), 95 deletions(-) diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index fd6ecdb8e0006..a8b0d8b032d8e 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, + ui_transform::UiGlobalTransform, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -34,10 +35,16 @@ fn calc_label( name.map(String::into_boxed_str) } -fn calc_bounds(mut nodes: Query<(&mut AccessibilityNode, Ref)>) { - for (mut accessible, node) in &mut nodes { - if node.is_changed() { - let center = node.transform.translation; +fn calc_bounds( + mut nodes: Query<( + &mut AccessibilityNode, + Ref, + Ref, + )>, +) { + for (mut accessible, node, transform) in &mut nodes { + if node.is_changed() || transform.is_changed() { + let center = transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 86052c62506a5..776ff6d6cd277 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,6 @@ -use crate::{ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack}; +use crate::{ + ui_transform::UiGlobalTransform, ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, @@ -82,11 +84,20 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { + /// True if the cursor position is over an unclipped section of the Node. + pub mouse_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, } +impl RelativeCursorPosition { + /// A helper function to check if the mouse is over the node + pub fn mouse_over(&self) -> bool { + self.mouse_over + } +} + /// Describes whether the node should block interactions with lower nodes #[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] @@ -124,6 +135,7 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, + transform: &'static UiGlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, @@ -143,7 +155,7 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, - clipping_query: Query<(&ComputedNode, &Node)>, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -228,7 +240,7 @@ pub fn ui_focus_system( let cursor_position = camera_cursor_positions.get(&camera_entity); let contains_cursor = cursor_position.is_some_and(|point| { - node.node.contains_point(*point) + node.node.contains_point(*node.transform, *point) && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) }); @@ -239,12 +251,13 @@ pub fn ui_focus_system( // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - node.node.normalize_point(*cursor_position) + node.node.normalize_point(*node.transform, *cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { + mouse_over: contains_cursor, normalized: relative_cursor_position, }; @@ -312,12 +325,12 @@ pub fn ui_focus_system( pub fn clip_check_recursive( point: Vec2, entity: Entity, - clipping_query: &Query<'_, '_, (&ComputedNode, &Node)>, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, child_of_query: &Query<&ChildOf>, ) -> bool { if let Ok(child_of) = child_of_query.get(entity) { let parent = child_of.0; - if let Ok((computed_node, node)) = clipping_query.get(parent) { + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists let mut clip_rect = Rect::from_center_size(Vec2::ZERO, 0.5 * computed_node.size); @@ -342,7 +355,7 @@ pub fn clip_check_recursive( clip_rect.max.y = f32::INFINITY; } - if !clip_rect.contains(computed_node.transform.inverse().transform_point2(point)) { + if !clip_rect.contains(transform.inverse().transform_point2(point)) { return false; } } diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index cc3e5aa8251d4..53c03113b9b60 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -524,10 +524,6 @@ mod tests { ], grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), - x_translation: Val::ZERO, - y_translation: Val::ZERO, - scale: Vec2::ONE, - rotation: 0., }; let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b26c13d015c42..9a08dade813b1 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -278,8 +278,7 @@ with UI components as a child of an entity without UI components, your UI layout ); propagated_transform *= node_transform * Affine2::from_translation(node_center); - if propagated_transform != node.transform { - node.transform = propagated_transform; + if propagated_transform != global_transform.0 { global_transform.0 = propagated_transform; } @@ -375,7 +374,7 @@ mod tests { use bevy_core_pipeline::core_2d::Camera2d; use bevy_ecs::{prelude::*, system::RunSystemOnce}; use bevy_image::Image; - use bevy_math::{Affine2, Rect, UVec2, Vec2}; + use bevy_math::{Rect, UVec2, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; @@ -704,20 +703,19 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode), Without>() + .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node)| { - let current_rect = - Rect::from_center_size(node.transform.translation, node.size()); + |option_rect, (entity, node, transform)| { + let current_rect = Rect::from_center_size(transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - node.transform, - Affine2::default(), + *transform, + UiGlobalTransform::default(), "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 203e5d1e01f4a..7324a1346f1b6 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -60,6 +60,7 @@ pub mod prelude { geometry::*, ui_material::*, ui_node::*, + ui_transform::*, widget::{Button, ImageNode, Label, NodeImageMode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index d35ba3428857a..d792ec0992aec 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,7 +24,7 @@ #![deny(missing_docs)] -use crate::{clip_check_recursive, prelude::*, UiStack}; +use crate::{clip_check_recursive, prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::{Rect, Vec2}; @@ -90,6 +90,7 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, + transform: &'static UiGlobalTransform, pickable: Option<&'static Pickable>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, @@ -112,7 +113,7 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, - clipping_query: Query<(&ComputedNode, &Node)>, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position @@ -185,7 +186,7 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size(node.node.transform.translation, node.node.size()); + let node_rect = Rect::from_center_size(node.transform.translation, node.node.size()); // Nodes with Display::None have a (0., 0.) logical rect and can be ignored if node_rect.size() == Vec2::ZERO { @@ -198,11 +199,13 @@ pub fn ui_picking( // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let Some(relative_cursor_position) = node.node.normalize_point(*cursor_position) else { + let Some(relative_cursor_position) = + node.node.normalize_point(*node.transform, *cursor_position) + else { continue; }; - let contains_cursor = node.node.contains_point(*cursor_position) + let contains_cursor = node.node.contains_point(*node.transform, *cursor_position) && clip_check_recursive( *cursor_position, *node_entity, diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 80ae75849b826..9dcb502889cf3 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystem, ResolvedBorderRadius, TransparentUi, Val, @@ -239,6 +240,7 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -249,7 +251,7 @@ pub fn extract_shadows( ) { let mut mapping = camera_map.get_mapper(); - for (entity, uinode, visibility, box_shadow, clip, camera) in &box_shadow_query { + for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query { // Skip if no visible shadows if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() { continue; @@ -304,7 +306,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: uinode.transform * Affine2::from_translation(offset), + transform: transform.0 * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 6cbb5384efc76..5c1cef50c7e7d 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -7,6 +7,7 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; +use crate::prelude::UiGlobalTransform; use crate::widget::ImageNode; use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, @@ -329,6 +330,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -339,7 +341,9 @@ pub fn extract_uinode_background_colors( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, inherited_visibility, clip, camera, background_color) in &uinode_query { + for (entity, uinode, transform, inherited_visibility, clip, camera, background_color) in + &uinode_query + { // Skip invisible backgrounds if !inherited_visibility.get() || background_color.0.is_fully_transparent() @@ -365,7 +369,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: uinode.transform, + transform: transform.0, flip_x: false, flip_y: false, border: uinode.border(), @@ -385,6 +389,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -394,7 +399,7 @@ pub fn extract_uinode_images( camera_map: Extract, ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, inherited_visibility, clip, camera, image) in &uinode_query { + for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &uinode_query { // Skip invisible images if !inherited_visibility.get() || image.color.is_fully_transparent() @@ -448,7 +453,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: uinode.transform, + transform: transform.0, flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -468,6 +473,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -483,6 +489,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, + transform, inherited_visibility, maybe_clip, camera, @@ -514,7 +521,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: computed_node.transform, + transform: transform.0, flip_x: false, flip_y: false, border: computed_node.border(), @@ -546,7 +553,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: computed_node.transform, + transform: transform.0, atlas_scaling: None, flip_x: false, flip_y: false, @@ -694,6 +701,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -708,8 +716,16 @@ pub fn extract_text_sections( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, inherited_visibility, clip, camera, computed_block, text_layout_info) in - &uinode_query + for ( + entity, + uinode, + transform, + inherited_visibility, + clip, + camera, + computed_block, + text_layout_info, + ) in &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -720,7 +736,7 @@ pub fn extract_text_sections( continue; }; - let transform = uinode.transform * Affine2::from_translation(-0.5 * uinode.size()); + let transform = transform.0 * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -782,6 +798,7 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, &InheritedVisibility, Option<&CalculatedClip>, @@ -795,7 +812,7 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, target, inherited_visibility, clip, text_layout_info, shadow) in + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) @@ -807,7 +824,7 @@ pub fn extract_text_shadows( continue; }; - let transform = uinode.transform + let node_transform = transform.0 * Affine2::from_translation( -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); @@ -828,7 +845,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Affine2::from_translation(*position), + transform: node_transform * Affine2::from_translation(*position), rect, }); diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 03a701ddc2c89..7e44471fc2924 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, marker::PhantomData, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_ecs::{ @@ -371,6 +372,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -381,7 +383,9 @@ pub fn extract_ui_material_nodes( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, computed_node, handle, inherited_visibility, clip, camera) in uinode_query.iter() { + for (entity, computed_node, transform, handle, inherited_visibility, clip, camera) in + uinode_query.iter() + { // skip invisible nodes if !inherited_visibility.get() || computed_node.is_empty() { continue; @@ -399,7 +403,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: computed_node.transform, + transform: transform.0, material: handle.id(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index e9f5b8ba1a1cf..16830fbf2187d 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -251,6 +252,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -261,7 +263,7 @@ pub fn extract_ui_texture_slices( ) { let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, inherited_visibility, clip, camera, image) in &slicers_query { + for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &slicers_query { // Skip invisible images if !inherited_visibility.get() || image.color.is_fully_transparent() @@ -310,7 +312,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: uinode.transform, + transform: transform.0, color: image.color.into(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1b5799b2e574c..7b44c14ccf46b 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,8 +1,11 @@ -use crate::{FocusPolicy, UiRect, Val}; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; -use bevy_math::{vec4, Affine2, Rect, UVec2, Vec2, Vec4Swizzles}; +use bevy_math::{vec4, Rect, UVec2, Vec2, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ camera::{Camera, RenderTarget}, @@ -75,10 +78,6 @@ pub struct ComputedNode { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub inverse_scale_factor: f32, - /// Transform from coordinates local to the node to global UI coordinates - /// - /// Automatically calculated by [`super::layout::ui_layout_system`]. - pub transform: Affine2, } impl ComputedNode { @@ -237,8 +236,8 @@ impl ComputedNode { // Returns true if `point` within the node. // // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. - pub fn contains_point(&self, point: Vec2) -> bool { - let local_point = self.transform.inverse().transform_point2(point); + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let local_point = transform.inverse().transform_point2(point); let [top, bottom] = if local_point.x < 0. { [self.border_radius.top_left, self.border_radius.bottom_left] } else { @@ -255,15 +254,15 @@ impl ComputedNode { l + m - r < 0. } - pub fn transform_point(&self, point: Vec2) -> Vec2 { - self.transform.inverse().transform_point2(point) - 0.5 * self.size + pub fn transform_point(&self, transform: UiGlobalTransform, point: Vec2) -> Vec2 { + transform.inverse().transform_point2(point) - 0.5 * self.size } - pub fn normalize_point(&self, point: Vec2) -> Option { + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { self.size .cmpgt(Vec2::ZERO) .all() - .then_some(self.transform_point(point) / self.size) + .then_some(self.transform_point(transform, point) / self.size) } } @@ -279,7 +278,6 @@ impl ComputedNode { border: BorderRect::ZERO, padding: BorderRect::ZERO, inverse_scale_factor: 1., - transform: Affine2::IDENTITY, }; } @@ -360,6 +358,8 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, + UiGlobalTransform, BackgroundColor, BorderColor, BorderRadius, @@ -655,22 +655,6 @@ pub struct Node { /// /// pub grid_column: GridPlacement, - - /// Translate the node along the x-axis. - /// `Val::Percent` values are resolved based on the computed width of the Ui Node. - /// `Val::Auto` is resolved to `0.`. - pub x_translation: Val, - - /// Translate the node along the y-axis. - /// `Val::Percent` values are resolved based on the computed width of the Ui Node. - /// `Val::Auto` is resolved to `0.`. - pub y_translation: Val, - - /// Resize the node. A negative value reflects the node in that axis. - pub scale: Vec2, - - /// Rotate the node clockwise by the given value in radians. - pub rotation: f32, } impl Node { @@ -714,10 +698,6 @@ impl Node { grid_auto_columns: Vec::new(), grid_column: GridPlacement::DEFAULT, grid_row: GridPlacement::DEFAULT, - x_translation: Val::ZERO, - y_translation: Val::ZERO, - scale: Vec2::ONE, - rotation: 0., }; } diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index c904eab537cfd..eed7010852fc8 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -1,5 +1,8 @@ +use std::f32::consts::PI; + use crate::Val; use bevy_derive::Deref; +use bevy_derive::DerefMut; use bevy_ecs::component::Component; use bevy_ecs::prelude::ReflectComponent; use bevy_math::Affine2; @@ -49,6 +52,9 @@ impl Default for UiVec { } } +/// 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( @@ -56,6 +62,7 @@ impl Default for UiVec { derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] +#[require(UiGlobalTransform)] pub struct UiTransform { /// Translate the node. pub translation: UiVec, @@ -71,6 +78,38 @@ impl UiTransform { scale: Vec2::ONE, rotation: 0., }; + + /// Creates a UI transform representing a rotation in `angle` radians. + pub fn from_angle(angle: f32) -> Self { + Self { + rotation: angle, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a rotation in `angle` degrees. + pub fn from_angle_deg(angle: f32) -> Self { + Self { + rotation: PI * angle / 180., + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a translation + pub fn from_translation(translation: UiVec) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } } impl Default for UiTransform { @@ -79,7 +118,11 @@ impl Default for UiTransform { } } -#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +/// 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref, DerefMut)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 9fa399f08444a..c0e9d09d7ba61 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -90,8 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = - Rect::from_center_size(computed_node.transform.translation, computed_node.size()); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index dfdaace5748a0..23e31c9a9c338 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -208,6 +208,9 @@ fn setup(mut commands: Commands, asset_server: Res) { ImageNode::new(asset_server.load("branding/bevy_logo_light.png")), // Uses the transform to rotate the logo image by 45 degrees Node { + ..Default::default() + }, + UiTransform { rotation: 0.25 * PI, ..Default::default() }, diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 147f3d3a63c10..d7e58e9c83c0a 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -40,16 +40,16 @@ struct AnimationState { struct Container(u8); trait UpdateTransform { - fn update(&self, t: f32, transform: &mut Node); + fn update(&self, t: f32, transform: &mut UiTransform); } #[derive(Component)] struct Move; impl UpdateTransform for Move { - fn update(&self, t: f32, node: &mut Node) { - node.x_translation = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.); - node.y_translation = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.); + fn update(&self, t: f32, transform: &mut UiTransform) { + transform.translation.x = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.); + transform.translation.y = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.); } } @@ -57,9 +57,9 @@ impl UpdateTransform for Move { struct Scale; impl UpdateTransform for Scale { - fn update(&self, t: f32, node: &mut Node) { - node.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); - node.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); + fn update(&self, t: f32, transform: &mut UiTransform) { + transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); + transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); } } @@ -67,8 +67,8 @@ impl UpdateTransform for Scale { struct Rotate; impl UpdateTransform for Rotate { - fn update(&self, t: f32, node: &mut Node) { - node.rotation = (ops::cos(t * TAU) * 45.0).to_radians(); + fn update(&self, t: f32, transform: &mut UiTransform) { + transform.rotation = (ops::cos(t * TAU) * 45.0).to_radians(); } } @@ -224,10 +224,10 @@ fn update_animation( fn update_transform( animation: Res, - mut containers: Query<(&mut Node, &T)>, + mut containers: Query<(&mut UiTransform, &T)>, ) { - for (mut node, update_transform) in &mut containers { - update_transform.update(animation.t, &mut node); + for (mut transform, update_transform) in &mut containers { + update_transform.update(animation.t, &mut transform); } } From 0c4ef878e98cceb7f762ebf93dfb092fb3b8eac9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 12:12:24 +0100 Subject: [PATCH 30/64] Added resolve method to `UiVec` --- crates/bevy_ui/src/ui_transform.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index eed7010852fc8..5ee763e4205d7 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -27,6 +27,7 @@ impl UiVec { y: Val::ZERO, }; + /// Creates a new [`UiVec`] where both components are in logical pixels pub const fn px(x: f32, y: f32) -> Self { Self { x: Val::Px(x), @@ -34,6 +35,7 @@ impl UiVec { } } + /// Creates a new [`UiVec`] where both components are precentage values pub const fn percent(x: f32, y: f32) -> Self { Self { x: Val::Percent(x), @@ -41,9 +43,25 @@ impl UiVec { } } + /// Creates a new [`UiVec`] pub const fn new(x: Val, y: Val) -> Self { Self { x, y } } + + /// Resolves this [`UiVec`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } } impl Default for UiVec { @@ -52,7 +70,7 @@ impl Default for UiVec { } } -/// 2D transform for UI nodes +/// Relative 2D transform for UI nodes /// /// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] @@ -95,7 +113,7 @@ impl UiTransform { } } - /// Creates a UI transform representing a translation + /// Creates a UI transform representing a responsive translation. pub fn from_translation(translation: UiVec) -> Self { Self { translation, @@ -103,7 +121,7 @@ impl UiTransform { } } - /// Creates a UI transform representing a scaling + /// Creates a UI transform representing a scaling. pub fn from_scale(scale: Vec2) -> Self { Self { scale, @@ -118,7 +136,7 @@ impl Default for UiTransform { } } -/// 2D transform for UI nodes +/// Absolute 2D transform for UI nodes /// /// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) /// in [`ui_layout_system`](crate::layout::ui_layout_system) From 129c2796111bf380f34af99d0c8859a0fc469fca Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 12:14:03 +0100 Subject: [PATCH 31/64] Use resolve function in ui_layout_system --- crates/bevy_ui/src/layout/mod.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 0531f03290a5e..216bd16abd83d 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -261,19 +261,13 @@ with UI components as a child of an entity without UI components, your UI layout let viewport_size = root_size.unwrap_or(node.size); - let resolve_translation = |mut val: Val, extent: f32, viewport_size: Vec2| { - if let Val::Px(ref mut value) = val { - *value /= inverse_target_scale_factor - } - val.resolve(extent, viewport_size).unwrap_or(0.) - }; - let node_transform = Affine2::from_scale_angle_translation( transform.scale, -transform.rotation, - Vec2::new( - resolve_translation(transform.translation.x, layout_size.x, viewport_size), - resolve_translation(transform.translation.y, layout_size.y, viewport_size), + transform.translation.resolve( + inverse_target_scale_factor, + layout_size, + viewport_size, ), ); propagated_transform *= node_transform * Affine2::from_translation(node_center); From 27c0718e99c4e7491e0f0ffe4cf2ac9ef7d205f6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 12:18:25 +0100 Subject: [PATCH 32/64] Fixed spelling mistake --- crates/bevy_ui/src/ui_transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 5ee763e4205d7..b17a09395249c 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -35,7 +35,7 @@ impl UiVec { } } - /// Creates a new [`UiVec`] where both components are precentage values + /// Creates a new [`UiVec`] where both components are percentage values pub const fn percent(x: f32, y: f32) -> Self { Self { x: Val::Percent(x), From 4c6f9c649e763dc68a2083dc343bce14783d003a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 12:32:02 +0100 Subject: [PATCH 33/64] Fixed UI debug overlay extraction --- crates/bevy_ui/src/render/debug_overlay.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index 8486732b91568..dc150a2f53f67 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,4 +1,5 @@ use crate::ui_node::ComputedNodeTarget; +use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; use bevy_asset::AssetId; @@ -15,7 +16,6 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use super::ExtractedUiItem; use super::ExtractedUiNode; @@ -61,6 +61,7 @@ pub fn extract_debug_overlay( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -74,7 +75,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -100,7 +101,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: uinode.transform, + transform: transform.0, flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), From a42e295a3e9490b615ce7fc63afe79980852910f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 12:38:45 +0100 Subject: [PATCH 34/64] Fixed `UiVec` attributes --- crates/bevy_ui/src/ui_transform.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index b17a09395249c..73f765ba68880 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -10,6 +10,12 @@ use bevy_math::Vec2; use bevy_reflect::prelude::*; #[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct UiVec { /// Translate the node along the x-axis. /// `Val::Percent` values are resolved based on the computed width of the Ui Node. From fa613c36836c9db2e2565ce98d6eede5c931cb81 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 14:50:45 +0100 Subject: [PATCH 35/64] Use object-centered coordinates for uinodes instead of inconsistantly switching between corner-based and object-centered coords. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). --- crates/bevy_ui/src/focus.rs | 13 +++++++------ crates/bevy_ui/src/picking_backend.rs | 4 ++-- crates/bevy_ui/src/ui_node.rs | 4 +++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 776ff6d6cd277..a00451c2578b5 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -70,8 +70,8 @@ impl Default for Interaction { } } -/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right -/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5) /// /// It can be used alongside [`Interaction`] to get the position of the press. /// @@ -245,9 +245,9 @@ pub fn ui_focus_system( }); // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = cursor_position.and_then(|cursor_position| { + let normalized_cursor_position = cursor_position.and_then(|cursor_position| { // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) @@ -258,7 +258,7 @@ pub fn ui_focus_system( // clicking let relative_cursor_position_component = RelativeCursorPosition { mouse_over: contains_cursor, - normalized: relative_cursor_position, + normalized: normalized_cursor_position, }; // Save the relative cursor position to the correct component @@ -271,7 +271,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered || (relative_cursor_position.is_none()) + if *interaction == Interaction::Hovered + || (normalized_cursor_position.is_none()) { interaction.set_if_neq(Interaction::None); } diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index d792ec0992aec..1a6975835f93c 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -199,7 +199,7 @@ pub fn ui_picking( // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let Some(relative_cursor_position) = + let Some(normalized_cursor_position) = node.node.normalize_point(*node.transform, *cursor_position) else { continue; @@ -217,7 +217,7 @@ pub fn ui_picking( hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, relative_cursor_position)); + .push((*node_entity, normalized_cursor_position)); } } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 7b44c14ccf46b..f7bbbf0d37dbd 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -254,10 +254,12 @@ impl ComputedNode { l + m - r < 0. } + /// Transform a point to node space with the center of the node at the origin and the corners at [+/-self.size.x, +/-self.self.y] pub fn transform_point(&self, transform: UiGlobalTransform, point: Vec2) -> Vec2 { - transform.inverse().transform_point2(point) - 0.5 * self.size + transform.inverse().transform_point2(point) } + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { self.size .cmpgt(Vec2::ZERO) From 14fe83a3d16cc7d0ebe545fce618f2e85a986d00 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 14:57:37 +0100 Subject: [PATCH 36/64] Changed std import for core --- crates/bevy_ui/src/ui_transform.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 73f765ba68880..bcba176789abb 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -1,5 +1,3 @@ -use std::f32::consts::PI; - use crate::Val; use bevy_derive::Deref; use bevy_derive::DerefMut; @@ -8,6 +6,7 @@ use bevy_ecs::prelude::ReflectComponent; use bevy_math::Affine2; use bevy_math::Vec2; use bevy_reflect::prelude::*; +use core::f32::consts::PI; #[derive(Debug, PartialEq, Clone, Copy, Reflect)] #[reflect(Default, PartialEq, Debug, Clone)] From 60ebe6dad5f209d8dc1dd34d54dae7558fff825b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 15:32:23 +0100 Subject: [PATCH 37/64] Fixed redundant explicit link target --- crates/bevy_ui/src/focus.rs | 2 +- crates/bevy_ui/src/ui_node.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index a00451c2578b5..e92458267488a 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -75,7 +75,7 @@ impl Default for Interaction { /// /// It can be used alongside [`Interaction`] to get the position of the press. /// -/// The component is updated when it is in the same entity with [`Node`](crate::Node). +/// The component is updated when it is in the same entity with [`Node`]. #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f7bbbf0d37dbd..26b352c91d7c2 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -361,7 +361,6 @@ impl From for ScrollPosition { ComputedNode, ComputedNodeTarget, UiTransform, - UiGlobalTransform, BackgroundColor, BorderColor, BorderRadius, From 8fa6db5935a7a468bf57b56685bd713a25a618da Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 16:17:53 +0100 Subject: [PATCH 38/64] Renamed `mouse_over` to `cursor_over` --- crates/bevy_ui/src/focus.rs | 10 +++++----- examples/ui/relative_cursor_position.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index e92458267488a..468b7f0ed0057 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -84,8 +84,8 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// True if the cursor position is over an unclipped section of the Node. - pub mouse_over: bool, + /// True if the cursor position is over an unclipped area of the Node. + pub cursor_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, @@ -93,8 +93,8 @@ pub struct RelativeCursorPosition { impl RelativeCursorPosition { /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.mouse_over + pub fn cursor_over(&self) -> bool { + self.cursor_over } } @@ -257,7 +257,7 @@ pub fn ui_focus_system( // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - mouse_over: contains_cursor, + cursor_over: contains_cursor, normalized: normalized_cursor_position, }; diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index 796e810895cf1..5346918257368 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -78,7 +78,7 @@ fn relative_cursor_position_system( "unknown".to_string() }; - text_color.0 = if relative_cursor_position.mouse_over() { + text_color.0 = if relative_cursor_position.cursor_over() { Color::srgb(0.1, 0.9, 0.1) } else { Color::srgb(0.9, 0.1, 0.1) From 5ff2495fec209bc9ab756e7c93ec738b69d31688 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:00:03 +0100 Subject: [PATCH 39/64] Added draft release note --- release-content/release-notes/specialized_ui_transform.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 release-content/release-notes/specialized_ui_transform.md diff --git a/release-content/release-notes/specialized_ui_transform.md b/release-content/release-notes/specialized_ui_transform.md new file mode 100644 index 0000000000000..a4df074237658 --- /dev/null +++ b/release-content/release-notes/specialized_ui_transform.md @@ -0,0 +1,7 @@ +--- +title: Specialized UI Transform +authors: ["@Ickshonpe"] +pull_requests: [16615] +--- + +In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations. From 25cdfaae37fe2c6f0cf4ee9571b2c4a818fe1dfc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:05:05 +0100 Subject: [PATCH 40/64] Fix for md lint --- release-content/release-notes/specialized_ui_transform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/specialized_ui_transform.md b/release-content/release-notes/specialized_ui_transform.md index a4df074237658..722c7282423c7 100644 --- a/release-content/release-notes/specialized_ui_transform.md +++ b/release-content/release-notes/specialized_ui_transform.md @@ -4,4 +4,4 @@ authors: ["@Ickshonpe"] pull_requests: [16615] --- -In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations. +In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations. \ No newline at end of file From 106929372585562de52f8c9e5adbd13752d75389 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:09:04 +0100 Subject: [PATCH 41/64] md fix --- release-content/release-notes/specialized_ui_transform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/specialized_ui_transform.md b/release-content/release-notes/specialized_ui_transform.md index 722c7282423c7..c1c7ae4811234 100644 --- a/release-content/release-notes/specialized_ui_transform.md +++ b/release-content/release-notes/specialized_ui_transform.md @@ -4,4 +4,4 @@ authors: ["@Ickshonpe"] pull_requests: [16615] --- -In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations. \ No newline at end of file +In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations. From dc73857a6650f0017c7631b476677d333a459e2f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:41:43 +0100 Subject: [PATCH 42/64] Added `resolve_clip_rect` helper method to `ComputedNode. --- crates/bevy_ui/src/focus.rs | 33 +++++++-------------------------- crates/bevy_ui/src/ui_node.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 468b7f0ed0057..3cd186a0df2fd 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -323,6 +323,7 @@ pub fn ui_focus_system( } } +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. pub fn clip_check_recursive( point: Vec2, entity: Entity, @@ -332,36 +333,16 @@ pub fn clip_check_recursive( if let Ok(child_of) = child_of_query.get(entity) { let parent = child_of.0; if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { - // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - - let mut clip_rect = Rect::from_center_size(Vec2::ZERO, 0.5 * computed_node.size); - - let clip_inset = match node.overflow_clip_margin.visual_box { - crate::OverflowClipBox::BorderBox => BorderRect::ZERO, - crate::OverflowClipBox::ContentBox => computed_node.content_inset(), - crate::OverflowClipBox::PaddingBox => computed_node.border(), - }; - - clip_rect.min.x += clip_inset.left; - clip_rect.min.y += clip_inset.top; - clip_rect.max.x -= clip_inset.right; - clip_rect.max.y -= clip_inset.bottom; - - if node.overflow.x == OverflowAxis::Visible { - clip_rect.min.x = -f32::INFINITY; - clip_rect.max.x = f32::INFINITY; - } - if node.overflow.y == OverflowAxis::Visible { - clip_rect.min.y = -f32::INFINITY; - clip_rect.max.y = f32::INFINITY; - } - - if !clip_rect.contains(transform.inverse().transform_point2(point)) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking return false; } } return clip_check_recursive(point, parent, clipping_query, child_of_query); } - // point unclipped by all ancestors + // Reached root, point unclipped by all ancestors true } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 26b352c91d7c2..1d949d5be6c65 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -266,6 +266,37 @@ impl ComputedNode { .all() .then_some(self.transform_point(transform, point) / self.size) } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, 0.5 * self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { From 447844ede48f0d02448bfe921c7e289086fd6227 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:43:16 +0100 Subject: [PATCH 43/64] Moved `clip_check_recursive` to `picking_backend` module. --- crates/bevy_ui/src/focus.rs | 30 +++------------------------ crates/bevy_ui/src/picking_backend.rs | 26 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 3cd186a0df2fd..f55cbb92b8f50 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,5 +1,6 @@ use crate::{ - ui_transform::UiGlobalTransform, ComputedNode, ComputedNodeTarget, Node, OverflowAxis, UiStack, + picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, + ComputedNodeTarget, Node, UiStack, }; use bevy_ecs::{ change_detection::DetectChangesMut, @@ -11,11 +12,10 @@ use bevy_ecs::{ system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_sprite::BorderRect; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -322,27 +322,3 @@ pub fn ui_focus_system( } } } - -/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. -pub fn clip_check_recursive( - point: Vec2, - entity: Entity, - clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, - child_of_query: &Query<&ChildOf>, -) -> bool { - if let Ok(child_of) = child_of_query.get(entity) { - let parent = child_of.0; - if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { - if !computed_node - .resolve_clip_rect(node.overflow, node.overflow_clip_margin) - .contains(transform.inverse().transform_point2(point)) - { - // The point is clipped and should be ignored by picking - return false; - } - } - return clip_check_recursive(point, parent, clipping_query, child_of_query); - } - // Reached root, point unclipped by all ancestors - true -} diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 1a6975835f93c..531b65ed20ce3 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,7 +24,7 @@ #![deny(missing_docs)] -use crate::{clip_check_recursive, prelude::*, ui_transform::UiGlobalTransform, UiStack}; +use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::{Rect, Vec2}; @@ -262,3 +262,27 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} From a289ca209de87faa12ffe792643d8b5390bd91dc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 17:57:57 +0100 Subject: [PATCH 44/64] Clean up picking_backend --- crates/bevy_ui/src/picking_backend.rs | 23 ++++++++++------------- crates/bevy_ui/src/ui_node.rs | 7 +------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 531b65ed20ce3..24d0bd7e0ea40 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -195,29 +195,26 @@ pub fn ui_picking( let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); - // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Find the normalized cursor position relative to the node. + // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let Some(normalized_cursor_position) = - node.node.normalize_point(*node.transform, *cursor_position) - else { - continue; - }; - - let contains_cursor = node.node.contains_point(*node.transform, *cursor_position) + if node.node.contains_point(*node.transform, *cursor_position) && clip_check_recursive( *cursor_position, *node_entity, &clipping_query, &child_of_query, - ); - - if contains_cursor { + ) + { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, normalized_cursor_position)); + .push(( + *node_entity, + node.transform.inverse().transform_point2(*cursor_position) + / node.node.size(), + )); } } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1d949d5be6c65..2f35818d4eae0 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -254,17 +254,12 @@ impl ComputedNode { l + m - r < 0. } - /// Transform a point to node space with the center of the node at the origin and the corners at [+/-self.size.x, +/-self.self.y] - pub fn transform_point(&self, transform: UiGlobalTransform, point: Vec2) -> Vec2 { - transform.inverse().transform_point2(point) - } - /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { self.size .cmpgt(Vec2::ZERO) .all() - .then_some(self.transform_point(transform, point) / self.size) + .then_some(transform.inverse().transform_point2(point) / self.size) } /// Resolve the node's clipping rect in local space From c1e9a6b0ae56d5355b2aa92016344a2bec9527f5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 18:03:23 +0100 Subject: [PATCH 45/64] Removed unneeded variable --- crates/bevy_ui/src/picking_backend.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 24d0bd7e0ea40..21119bd8147df 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -186,10 +186,8 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size(node.transform.translation, node.node.size()); - // Nodes with Display::None have a (0., 0.) logical rect and can be ignored - if node_rect.size() == Vec2::ZERO { + if node.node.size() == Vec2::ZERO { continue; } From 04933ec4395c90d4467790f76edcd71acc7e6b32 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 18:09:25 +0100 Subject: [PATCH 46/64] Removed unused import --- crates/bevy_ui/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 21119bd8147df..e8ce90fddab27 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -27,7 +27,7 @@ use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; From 4f28bc1a85bb5625209b7ed9e854c7fa36f38bf0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 19:43:20 +0100 Subject: [PATCH 47/64] Fixed `resolve_clip_rect`, Rect::from_center_size takes a size, not a half_size. --- crates/bevy_ui/src/ui_node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2f35818d4eae0..9008c7a675939 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -268,7 +268,7 @@ impl ComputedNode { overflow: Overflow, overflow_clip_margin: OverflowClipMargin, ) -> Rect { - let mut clip_rect = Rect::from_center_size(Vec2::ZERO, 0.5 * self.size); + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); let clip_inset = match overflow_clip_margin.visual_box { OverflowClipBox::BorderBox => BorderRect::ZERO, From 8318286016b996c6a7de2160b6bdc920d6bc3004 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 23:11:14 +0100 Subject: [PATCH 48/64] Made `UiGlobalTransform`'s inner value private. Check transform is invertible before transforms to node space. --- crates/bevy_ui/src/layout/mod.rs | 4 +-- crates/bevy_ui/src/render/box_shadow.rs | 2 +- crates/bevy_ui/src/render/mod.rs | 12 +++---- .../src/render/ui_material_pipeline.rs | 2 +- .../src/render/ui_texture_slice_pipeline.rs | 2 +- crates/bevy_ui/src/ui_node.rs | 11 +++++-- crates/bevy_ui/src/ui_transform.rs | 32 +++++++++++++++++-- 7 files changed, 49 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 216bd16abd83d..1fcc4dee9001b 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -272,8 +272,8 @@ with UI components as a child of an entity without UI components, your UI layout ); propagated_transform *= node_transform * Affine2::from_translation(node_center); - if propagated_transform != global_transform.0 { - global_transform.0 = propagated_transform; + if propagated_transform != **global_transform { + *global_transform = propagated_transform.into(); } if let Some(border_radius) = maybe_border_radius { diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 9dcb502889cf3..074c7b99f8670 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -306,7 +306,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.0 * Affine2::from_translation(offset), + transform: Affine2::from(transform) * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 5c1cef50c7e7d..fb7cc3eab23db 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -369,7 +369,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.0, + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -453,7 +453,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.0, + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -521,7 +521,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.0, + transform: transform.into(), flip_x: false, flip_y: false, border: computed_node.border(), @@ -553,7 +553,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: transform.0, + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, @@ -736,7 +736,7 @@ pub fn extract_text_sections( continue; }; - let transform = transform.0 * Affine2::from_translation(-0.5 * uinode.size()); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -824,7 +824,7 @@ pub fn extract_text_shadows( continue; }; - let node_transform = transform.0 + let node_transform = Affine2::from(*transform) * Affine2::from_translation( -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 7e44471fc2924..261aae63eb03f 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -403,7 +403,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.0, + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 16830fbf2187d..1239d583c1108 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -312,7 +312,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.0, + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 9008c7a675939..aaf2bea0c1a5d 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -237,7 +237,12 @@ impl ComputedNode { // // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { - let local_point = transform.inverse().transform_point2(point); + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; let [top, bottom] = if local_point.x < 0. { [self.border_radius.top_left, self.border_radius.bottom_left] } else { @@ -259,7 +264,9 @@ impl ComputedNode { self.size .cmpgt(Vec2::ZERO) .all() - .then_some(transform.inverse().transform_point2(point) / self.size) + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) } /// Resolve the node's clipping rect in local space diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index bcba176789abb..04280be6f0c7a 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -1,6 +1,5 @@ use crate::Val; use bevy_derive::Deref; -use bevy_derive::DerefMut; use bevy_ecs::component::Component; use bevy_ecs::prelude::ReflectComponent; use bevy_math::Affine2; @@ -145,17 +144,44 @@ impl Default for UiTransform { /// /// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) /// in [`ui_layout_system`](crate::layout::ui_layout_system) -#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref, DerefMut)] +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct UiGlobalTransform(pub Affine2); +pub struct UiGlobalTransform(Affine2); impl Default for UiGlobalTransform { fn default() -> Self { Self(Affine2::IDENTITY) } } + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} From e32cdb1715a76d2d36bf44972a562600f18df2eb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 23:24:52 +0100 Subject: [PATCH 49/64] Fixed accessor in debug_overlay --- crates/bevy_ui/src/render/debug_overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index dc150a2f53f67..b89625a5ba791 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -101,7 +101,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.0, + transform: transform.into(), flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), From b9440e26d7b2386224aca31f77d36c06a9f15f8f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 23:30:25 +0100 Subject: [PATCH 50/64] Added migration guide --- .../migration-guides/specialized_ui_transform.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 release-content/migration-guides/specialized_ui_transform.md diff --git a/release-content/migration-guides/specialized_ui_transform.md b/release-content/migration-guides/specialized_ui_transform.md new file mode 100644 index 0000000000000..7b92a228733bb --- /dev/null +++ b/release-content/migration-guides/specialized_ui_transform.md @@ -0,0 +1,10 @@ +--- +title: Specialized Ui Transform +pull_requests: [16615] +--- + +In Bevy UI Transform and GlobalTransform have been replaced by UiTransform and UiGlobalTransform. UiTransform is a specialized 2D UI transform which supports responsive translations. Val::Px values are equivalent to the previous unitless translation components. + +In previous versions the Transforms of UI nodes would be overwritten by ui_layout_system each frame. UiTransforms aren't modified, so there is no longer any need for systems that cache and rewrite the transform for translated UI elements. + +RelativeCursorPosition's coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its normalized_visible_node_rect field has been removed and replaced with a boolean value cursor_over which is set to true when the cursor is hovering an unclipped area of the UI node. \ No newline at end of file From 2ac47f5062d1e148dc7e70024ab273d31c229d6e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 25 Apr 2025 23:40:21 +0100 Subject: [PATCH 51/64] Added trailing line to migration guide. --- release-content/migration-guides/specialized_ui_transform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/migration-guides/specialized_ui_transform.md b/release-content/migration-guides/specialized_ui_transform.md index 7b92a228733bb..b28edccf0bebe 100644 --- a/release-content/migration-guides/specialized_ui_transform.md +++ b/release-content/migration-guides/specialized_ui_transform.md @@ -7,4 +7,4 @@ In Bevy UI Transform and GlobalTransform have been replaced by UiTransform and U In previous versions the Transforms of UI nodes would be overwritten by ui_layout_system each frame. UiTransforms aren't modified, so there is no longer any need for systems that cache and rewrite the transform for translated UI elements. -RelativeCursorPosition's coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its normalized_visible_node_rect field has been removed and replaced with a boolean value cursor_over which is set to true when the cursor is hovering an unclipped area of the UI node. \ No newline at end of file +RelativeCursorPosition's coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its normalized_visible_node_rect field has been removed and replaced with a boolean value cursor_over which is set to true when the cursor is hovering an unclipped area of the UI node. From e91c90102cad55deb92db3f36e457e59ab7c1d3c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 26 Apr 2025 07:39:45 +0100 Subject: [PATCH 52/64] updated migration guide --- .../migration-guides/specialized_ui_transform.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release-content/migration-guides/specialized_ui_transform.md b/release-content/migration-guides/specialized_ui_transform.md index b28edccf0bebe..183e1f9525da4 100644 --- a/release-content/migration-guides/specialized_ui_transform.md +++ b/release-content/migration-guides/specialized_ui_transform.md @@ -2,9 +2,9 @@ title: Specialized Ui Transform pull_requests: [16615] --- +New specialized 2D UI transform components `UiTransform` and `UiGlobalTransform`. `UiTransform` is a 2d-only equivalent of `Transform` with a translation in `Val`s. `UiGlobalTransform` newtypes `Affine2` and is updated in `ui_layout_system`. +`Node` now requires `UiTransform` instead of `Transform`. `UiTransform` requires `UiGlobalTransform`. -In Bevy UI Transform and GlobalTransform have been replaced by UiTransform and UiGlobalTransform. UiTransform is a specialized 2D UI transform which supports responsive translations. Val::Px values are equivalent to the previous unitless translation components. +In previous versions of Bevy `ui_layout_system` would overwrite UI node's `Transform::translation` each frame. `UiTransform`s aren't overwritten and there is no longer any need for systems that cache and rewrite the transform for translated UI elements. -In previous versions the Transforms of UI nodes would be overwritten by ui_layout_system each frame. UiTransforms aren't modified, so there is no longer any need for systems that cache and rewrite the transform for translated UI elements. - -RelativeCursorPosition's coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its normalized_visible_node_rect field has been removed and replaced with a boolean value cursor_over which is set to true when the cursor is hovering an unclipped area of the UI node. +`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering an unclipped area of the UI node. \ No newline at end of file From c6726fa6c8b93605fd03855bb55e14f0d3e05315 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 26 Apr 2025 07:44:30 +0100 Subject: [PATCH 53/64] Fix migration-guide --- release-content/migration-guides/specialized_ui_transform.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/migration-guides/specialized_ui_transform.md b/release-content/migration-guides/specialized_ui_transform.md index 183e1f9525da4..774f2eb67e7d1 100644 --- a/release-content/migration-guides/specialized_ui_transform.md +++ b/release-content/migration-guides/specialized_ui_transform.md @@ -5,6 +5,6 @@ pull_requests: [16615] New specialized 2D UI transform components `UiTransform` and `UiGlobalTransform`. `UiTransform` is a 2d-only equivalent of `Transform` with a translation in `Val`s. `UiGlobalTransform` newtypes `Affine2` and is updated in `ui_layout_system`. `Node` now requires `UiTransform` instead of `Transform`. `UiTransform` requires `UiGlobalTransform`. -In previous versions of Bevy `ui_layout_system` would overwrite UI node's `Transform::translation` each frame. `UiTransform`s aren't overwritten and there is no longer any need for systems that cache and rewrite the transform for translated UI elements. +In previous versions of Bevy `ui_layout_system` would overwrite UI node's `Transform::translation` each frame. `UiTransform`s aren't overwritten and there is no longer any need for systems that cache and rewrite the transform for translated UI elements. -`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering an unclipped area of the UI node. \ No newline at end of file +`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering an unclipped area of the UI node. From fb9fc0ff0575ef4c15b0c6ec477af78844a4ab2f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 27 Apr 2025 11:46:15 +0100 Subject: [PATCH 54/64] Added `ui_transform` example --- Cargo.toml | 11 + examples/README.md | 1 + examples/ui/ui_transform.rs | 392 ++++++++++++++++++++++++++++++++++++ 3 files changed, 404 insertions(+) create mode 100644 examples/ui/ui_transform.rs diff --git a/Cargo.toml b/Cargo.toml index 8bb16b741db86..e34c97b5d48bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3488,6 +3488,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/examples/README.md b/examples/README.md index d0e33d957f2d4..343acbc4a3bd5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -564,6 +564,7 @@ Example | Description [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI +[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements. [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. diff --git a/examples/ui/ui_transform.rs b/examples/ui/ui_transform.rs new file mode 100644 index 0000000000000..b62b790eb9cf5 --- /dev/null +++ b/examples/ui/ui_transform.rs @@ -0,0 +1,392 @@ +//! An example demonstrating how to translate, rotate and scale UI elements. +use bevy::color::palettes::css::DARK_GRAY; +use bevy::color::palettes::css::RED; +use bevy::color::palettes::css::YELLOW; +use bevy::prelude::*; + +// #[derive(Resource)] +// struct Center(Entity); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, button_system) + .add_systems(Update, translation_system) + .run(); +} + +const NORMAL_BUTTON: Color = Color::WHITE; +const HOVERED_BUTTON: Color = Color::Srgba(YELLOW); +const PRESSED_BUTTON: Color = Color::Srgba(RED); + +/// A button that rotates the target node +#[derive(Component)] +pub struct RotateButton(pub f32); + +/// A button that scales the target node +#[derive(Component)] +pub struct ScaleButton(pub f32); + +/// Marker component so the systems know which entities to translate, rotate and scale +#[derive(Component)] +pub struct TargetNode; + +/// Handles button interactions +fn button_system( + mut interaction_query: Query< + ( + &Interaction, + &mut BackgroundColor, + Option<&RotateButton>, + Option<&ScaleButton>, + ), + (Changed, With