Specialized UI transform (#16615)

# Objective

Add specialized UI transform `Component`s and fix some related problems:
* Animating UI elements by modifying the `Transform` component of UI
nodes doesn't work very well because `ui_layout_system` overwrites the
translations each frame. The `overflow_debug` example uses a horrible
hack where it copies the transform into the position that'll likely
cause a panic if any users naively copy it.
* Picking ignores rotation and scaling and assumes UI nodes are always
axis aligned.
* The clipping geometry stored in `CalculatedClip` is wrong for rotated
and scaled elements.
* Transform propagation is unnecessary for the UI, the transforms can be
updated during layout updates.
* The UI internals use both object-centered and top-left-corner-based
coordinates systems for UI nodes. Depending on the context you have to
add or subtract the half-size sometimes before transforming between
coordinate spaces. We should just use one system consistantly so that
the transform can always be directly applied.
* `Transform` doesn't support responsive coordinates.

## Solution

* Unrequire `Transform` from `Node`.
* New components `UiTransform`, `UiGlobalTransform`:
- `Node` requires `UiTransform`, `UiTransform` requires
`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`.
* New helper functions on `ComputedNode` for mapping between viewport
and local node space.
* The cursor position is transformed to local node space during picking
so that it respects rotations and scalings.
* To check if the cursor hovers a node recursively walk up the tree to
the root checking if any of the ancestor nodes clip the point at the
cursor. If the point is clipped the interaction is ignored.
* Use object-centered coordinates for UI nodes.
* `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).
* Replaced the `normalized_visible_node_rect: Rect` field of
`RelativeCursorPosition` with `cursor_over: bool`, which is set to true
when the cursor is over an unclipped point on the node. The visible area
of the node is not necessarily a rectangle, so the previous
implementation didn't work.

This should fix all the logical bugs with non-axis aligned interactions
and clipping. Rendering still needs changes but they are far outside the
scope of this PR.

Tried and abandoned two other approaches:
* New `transform` field on `Node`, require `GlobalTransform` on `Node`,
and unrequire `Transform` on `Node`. Unrequiring `Transform` opts out of
transform propagation so there is then no conflict with updating the
`GlobalTransform` in `ui_layout_system`. This was a nice change in its
simplicity but potentially confusing for users I think, all the
`GlobalTransform` docs mention `Transform` and having special rules for
how it's updated just for the UI is unpleasently surprising.
* New `transform` field on `Node`. Unrequire `Transform` on `Node`. New
`transform: Affine2` field on `ComputedNode`.
This was okay but I think most users want a separate specialized UI
transform components. The fat `ComputedNode` doesn't work well with
change detection.

Fixes #18929, #18930

## Testing

There is an example you can look at: 
```
cargo run --example ui_transform
```

Sometimes in the example if you press the rotate button couple of times
the first glyph from the top label disappears , I'm not sure what's
causing it yet but I don't think it's related to this PR.

##  Migration Guide
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.

`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.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe 2025-06-09 20:05:49 +01:00 committed by GitHub
parent bf8868b7b7
commit 4836c7868c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 869 additions and 267 deletions

View File

@ -3543,6 +3543,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"

View File

@ -1,6 +1,7 @@
use crate::{
experimental::UiChildren,
prelude::{Button, Label},
ui_transform::UiGlobalTransform,
widget::{ImageNode, TextUiReader},
ComputedNode,
};
@ -13,11 +14,9 @@ use bevy_ecs::{
system::{Commands, Query},
world::Ref,
};
use bevy_math::Vec3Swizzles;
use bevy_render::camera::CameraUpdateSystems;
use bevy_transform::prelude::GlobalTransform;
use accesskit::{Node, Rect, Role};
use bevy_render::camera::CameraUpdateSystems;
fn calc_label(
text_reader: &mut TextUiReader,
@ -40,12 +39,12 @@ fn calc_bounds(
mut nodes: Query<(
&mut AccessibilityNode,
Ref<ComputedNode>,
Ref<GlobalTransform>,
Ref<UiGlobalTransform>,
)>,
) {
for (mut accessible, node, transform) in &mut nodes {
if node.is_changed() || transform.is_changed() {
let center = transform.translation().xy();
let center = transform.translation;
let half_size = 0.5 * node.size;
let min = center - half_size;
let max = center + half_size;

View File

@ -1,18 +1,21 @@
use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack};
use crate::{
picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode,
ComputedNodeTarget, Node, UiStack,
};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::{ContainsEntity, Entity},
hierarchy::ChildOf,
prelude::{Component, With},
query::QueryData,
reflect::ReflectComponent,
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_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};
use smallvec::SmallVec;
@ -67,12 +70,12 @@ 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.
///
/// 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(
@ -81,8 +84,8 @@ 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,
/// 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<Vec2>,
@ -90,9 +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.normalized
.is_some_and(|position| self.normalized_visible_node_rect.contains(position))
pub fn cursor_over(&self) -> bool {
self.cursor_over
}
}
@ -133,11 +135,10 @@ pub struct State {
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
transform: &'static UiGlobalTransform,
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,
}
@ -154,6 +155,8 @@ pub fn ui_focus_system(
touches_input: Res<Touches>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) {
let primary_window = primary_window.iter().next();
@ -234,46 +237,30 @@ 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(),
);
// 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 cursor_position = camera_cursor_positions.get(&camera_entity);
let contains_cursor = cursor_position.is_some_and(|point| {
node.node.contains_point(*node.transform, *point)
&& clip_check_recursive(*point, *entity, &clipping_query, &child_of_query)
});
// 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)
(node_rect.size().cmpgt(Vec2::ZERO).all())
.then_some((*cursor_position - node_rect.min) / node_rect.size())
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 {
normalized_visible_node_rect: visible_rect.normalize(node_rect),
normalized: relative_cursor_position,
cursor_over: contains_cursor,
normalized: normalized_cursor_position,
};
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,
)
});
// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
{
@ -284,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);
}
@ -334,26 +322,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.
}

View File

@ -448,6 +448,8 @@ impl RepeatedGridTrack {
#[cfg(test)]
mod tests {
use bevy_math::Vec2;
use super::*;
#[test]
@ -523,7 +525,7 @@ mod tests {
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
};
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);
@ -661,7 +663,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)),

View File

@ -1,5 +1,6 @@
use crate::{
experimental::{UiChildren, UiRootNodes},
ui_transform::{UiGlobalTransform, UiTransform},
BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node,
Outline, OverflowAxis, ScrollPosition,
};
@ -12,9 +13,9 @@ use bevy_ecs::{
system::{Commands, Query, ResMut},
world::Ref,
};
use bevy_math::Vec2;
use bevy_math::{Affine2, Vec2};
use bevy_sprite::BorderRect;
use bevy_transform::components::Transform;
use thiserror::Error;
use tracing::warn;
use ui_surface::UiSurface;
@ -81,9 +82,10 @@ pub fn ui_layout_system(
)>,
computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>,
ui_children: UiChildren,
mut node_transform_query: Query<(
mut node_update_query: Query<(
&mut ComputedNode,
&mut Transform,
&UiTransform,
&mut UiGlobalTransform,
&Node,
Option<&LayoutConfig>,
Option<&BorderRadius>,
@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout
&mut ui_surface,
true,
computed_target.physical_size().as_vec2(),
&mut node_transform_query,
Affine2::IDENTITY,
&mut node_update_query,
&ui_children,
computed_target.scale_factor.recip(),
Vec2::ZERO,
@ -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,
target_size: Vec2,
node_transform_query: &mut Query<(
mut inherited_transform: Affine2,
node_update_query: &mut Query<(
&mut ComputedNode,
&mut Transform,
&UiTransform,
&mut UiGlobalTransform,
&Node,
Option<&LayoutConfig>,
Option<&BorderRadius>,
@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout
) {
if let Ok((
mut node,
mut transform,
transform,
mut global_transform,
style,
maybe_layout_config,
maybe_border_radius,
maybe_outline,
maybe_scroll_position,
)) = node_transform_query.get_mut(entity)
)) = node_update_query.get_mut(entity)
{
let use_rounding = maybe_layout_config
.map(|layout_config| layout_config.use_rounding)
@ -224,10 +230,11 @@ with UI components as a child of an entity without UI components, your UI layout
let layout_size = Vec2::new(layout.size.width, layout.size.height);
// Taffy layout position of the top-left corner of the node, relative to its parent.
let layout_location = Vec2::new(layout.location.x, layout.location.y);
// The position of the center of the node, stored in the node's transform
let node_center =
// The position of the center of the node relative to its top-left corner.
let local_center =
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
// only trigger change detection when the new values are different
@ -253,6 +260,16 @@ 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);
// Computer the node's new global transform
let mut local_transform =
transform.compute_affine(inverse_target_scale_factor, layout_size, target_size);
local_transform.translation += local_center;
inherited_transform *= local_transform;
if inherited_transform != **global_transform {
*global_transform = inherited_transform.into();
}
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(
@ -290,10 +307,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(
@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout
ui_surface,
use_rounding,
target_size,
node_transform_query,
inherited_transform,
node_update_query,
ui_children,
inverse_target_scale_factor,
layout_size,
@ -356,10 +370,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,
@ -684,23 +695,20 @@ mod tests {
ui_schedule.run(&mut world);
let overlap_check = world
.query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without<ChildOf>>()
.query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without<ChildOf>>()
.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, 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!(
global_transform.affine(),
GlobalTransform::default().affine(),
"root ui node {entity} global transform is not populated"
*transform,
UiGlobalTransform::default(),
"root ui node {entity} transform is not populated"
);
let Some((rect, is_overlapping)) = option_rect else {
return Some((current_rect, false));

View File

@ -18,6 +18,7 @@ pub mod widget;
pub mod gradients;
#[cfg(feature = "bevy_ui_picking_backend")]
pub mod picking_backend;
pub mod ui_transform;
use bevy_derive::{Deref, DerefMut};
#[cfg(feature = "bevy_ui_picking_backend")]
@ -42,6 +43,7 @@ pub use measurement::*;
pub use render::*;
pub use ui_material::*;
pub use ui_node::*;
pub use ui_transform::*;
use widget::{ImageNode, ImageNodeSize, ViewportNode};
@ -64,6 +66,7 @@ pub mod prelude {
gradients::*,
ui_material::*,
ui_node::*,
ui_transform::*,
widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode},
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
},

View File

@ -24,14 +24,13 @@
#![deny(missing_docs)]
use crate::{focus::pick_rounded_rect, prelude::*, UiStack};
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::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin {
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
transform: &'static UiGlobalTransform,
pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}
@ -110,6 +108,8 @@ pub fn ui_picking(
ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) {
// For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
@ -181,43 +181,33 @@ pub fn ui_picking(
continue;
};
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node_rect.size() == Vec2::ZERO {
if node.node.size() == Vec2::ZERO {
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
// (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 relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
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,
if node.node.contains_point(*node.transform, *cursor_position)
&& clip_check_recursive(
*cursor_position,
*node_entity,
&clipping_query,
&child_of_query,
)
{
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push((*node_entity, relative_cursor_position));
.push((
*node_entity,
node.transform.inverse().transform_point2(*cursor_position)
/ node.node.size(),
));
}
}
}
@ -262,3 +252,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
}

View File

@ -2,6 +2,7 @@
use core::{hash::Hash, ops::Range};
use crate::prelude::UiGlobalTransform;
use crate::{
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems,
ResolvedBorderRadius, TransparentUi, Val,
@ -18,7 +19,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 +30,6 @@ use bevy_render::{
view::*,
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
@ -211,7 +211,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<Rect>,
pub extracted_camera_entity: Entity,
@ -236,7 +236,7 @@ pub fn extract_shadows(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
&BoxShadow,
Option<&CalculatedClip>,
@ -302,7 +302,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: Affine2::from(transform) * Affine2::from_translation(offset),
color: drop_shadow.color.into(),
bounds: shadow_size + 6. * blur_radius,
clip: clip.map(|clip| clip.clip),
@ -405,11 +405,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)
@ -443,7 +447,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 π
@ -492,7 +496,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(),
});
}

View File

@ -1,5 +1,6 @@
use crate::shader_flags;
use crate::ui_node::ComputedNodeTarget;
use crate::ui_transform::UiGlobalTransform;
use crate::CalculatedClip;
use crate::ComputedNode;
use bevy_asset::AssetId;
@ -16,7 +17,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;
@ -62,9 +62,9 @@ pub fn extract_debug_overlay(
Query<(
Entity,
&ComputedNode,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&GlobalTransform,
&ComputedNodeTarget,
)>,
>,
@ -76,7 +76,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, transform, visibility, maybe_clip, computed_target) in &uinode_query {
if !debug_options.show_hidden && !visibility.get() {
continue;
}
@ -102,7 +102,7 @@ pub fn extract_debug_overlay(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: transform.compute_matrix(),
transform: transform.into(),
flip_x: false,
flip_y: false,
border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()),

View File

@ -17,8 +17,9 @@ use bevy_ecs::{
use bevy_image::prelude::*;
use bevy_math::{
ops::{cos, sin},
FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles,
FloatOrd, Rect, Vec2,
};
use bevy_math::{Affine2, Vec2Swizzles};
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::*,
@ -29,7 +30,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::shader_flags::BORDER_ALL;
@ -238,7 +238,7 @@ pub enum ResolvedGradient {
pub struct ExtractedGradient {
pub stack_index: u32,
pub transform: Mat4,
pub transform: Affine2,
pub rect: Rect,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
@ -354,7 +354,7 @@ pub fn extract_gradients(
Entity,
&ComputedNode,
&ComputedNodeTarget,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
AnyOf<(&BackgroundGradient, &BorderGradient)>,
@ -414,7 +414,7 @@ pub fn extract_gradients(
border_radius: uinode.border_radius,
border: uinode.border,
node_type,
transform: transform.compute_matrix(),
transform: transform.into(),
},
main_entity: entity.into(),
render_entity: commands.spawn(TemporaryRenderEntity).id(),
@ -439,7 +439,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -487,7 +487,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -541,7 +541,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -675,12 +675,16 @@ pub fn prepare_gradient(
*item.batch_range_mut() = item_index as u32..item_index as u32 + 1;
let uinode_rect = gradient.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| (gradient.transform * (pos * rect_size).extend(1.)).xyz());
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy());
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
gradient
.transform
.transform_point2(pos * rect_size)
.extend(0.)
});
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// 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)
@ -721,7 +725,7 @@ pub fn prepare_gradient(
corner_points[3] + positions_diff[3],
];
let transformed_rect_size = gradient.transform.transform_vector3(rect_size);
let transformed_rect_size = gradient.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 π

View File

@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline;
mod debug_overlay;
mod gradient;
use crate::prelude::UiGlobalTransform;
use crate::widget::{ImageNode, ViewportNode};
use crate::{
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode,
ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias,
@ -22,7 +24,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};
use bevy_render::load_shader_library;
use bevy_render::render_graph::{NodeRunError, RenderGraphContext};
use bevy_render::render_phase::ViewSortedRenderPhases;
@ -243,7 +245,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 {
@ -253,7 +255,7 @@ pub enum ExtractedUiItem {
}
pub struct ExtractedGlyph {
pub transform: Mat4,
pub transform: Affine2,
pub rect: Rect,
}
@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: transform.compute_matrix(),
transform: transform.into(),
flip_x: false,
flip_y: false,
border: uinode.border(),
@ -403,7 +405,7 @@ pub fn extract_uinode_images(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -467,7 +469,7 @@ pub fn extract_uinode_images(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling,
transform: transform.compute_matrix(),
transform: transform.into(),
flip_x: image.flip_x,
flip_y: image.flip_y,
border: uinode.border,
@ -487,7 +489,7 @@ pub fn extract_uinode_borders(
Entity,
&Node,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -503,7 +505,7 @@ pub fn extract_uinode_borders(
entity,
node,
computed_node,
global_transform,
transform,
inherited_visibility,
maybe_clip,
camera,
@ -567,7 +569,7 @@ pub fn extract_uinode_borders(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: global_transform.compute_matrix(),
transform: transform.into(),
flip_x: false,
flip_y: false,
border: computed_node.border(),
@ -600,7 +602,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: transform.into(),
atlas_scaling: None,
flip_x: false,
flip_y: false,
@ -749,7 +751,7 @@ pub fn extract_viewport_nodes(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -792,7 +794,7 @@ pub fn extract_viewport_nodes(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: transform.compute_matrix(),
transform: transform.into(),
flip_x: false,
flip_y: false,
border: uinode.border(),
@ -812,7 +814,7 @@ pub fn extract_text_sections(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -830,7 +832,7 @@ pub fn extract_text_sections(
for (
entity,
uinode,
global_transform,
transform,
inherited_visibility,
clip,
camera,
@ -847,8 +849,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 = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size());
for (
i,
@ -866,7 +867,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,
});
@ -910,8 +911,8 @@ pub fn extract_text_shadows(
Query<(
Entity,
&ComputedNode,
&UiGlobalTransform,
&ComputedNodeTarget,
&GlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&TextLayoutInfo,
@ -924,16 +925,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, 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`)
if !inherited_visibility.get() || uinode.is_empty() {
@ -944,9 +937,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 node_transform = Affine2::from(*transform)
* Affine2::from_translation(
-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(),
);
for (
@ -965,7 +958,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: node_transform * Affine2::from_translation(*position),
rect,
});
@ -998,7 +991,7 @@ pub fn extract_text_background_colors(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors(
continue;
};
let transform = global_transform.affine()
* bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.));
let transform =
Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size());
for &(section_entity, rect) in text_layout_info.section_rects.iter() {
let Ok(text_background_color) = text_background_colors_query.get(section_entity) else {
@ -1042,7 +1035,7 @@ pub fn extract_text_background_colors(
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: transform * Mat4::from_translation(rect.center().extend(0.)),
transform: transform * Affine2::from_translation(rect.center()),
flip_x: false,
flip_y: false,
border: uinode.border(),
@ -1093,11 +1086,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];
@ -1321,12 +1314,12 @@ pub fn prepare_uinodes(
let mut uinode_rect = extracted_uinode.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| (*transform * (pos * rect_size).extend(1.)).xyz());
let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy());
.map(|pos| transform.transform_point2(pos * rect_size).extend(0.));
let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// 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)
@ -1367,7 +1360,7 @@ 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(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 π
@ -1448,7 +1441,7 @@ pub fn prepare_uinodes(
border_radius.bottom_left,
],
border: [border.left, border.top, border.right, border.bottom],
size: rect_size.xy().into(),
size: rect_size.into(),
point: points[i].into(),
});
}
@ -1470,13 +1463,14 @@ pub fn prepare_uinodes(
let color = extracted_uinode.color.to_f32_array();
for glyph in &extracted_uinodes.glyphs[range.clone()] {
let glyph_rect = glyph.rect;
let size = glyph.rect.size();
let rect_size = glyph_rect.size().extend(1.0);
let rect_size = glyph_rect.size();
// 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 * glyph_rect.size())
.extend(0.)
});
let positions_diff = if let Some(clip) = extracted_uinode.clip {
@ -1511,7 +1505,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);
if positions_diff[0].x - positions_diff[1].x
>= transformed_rect_size.x.abs()
|| positions_diff[1].y - positions_diff[2].y
@ -1548,7 +1542,7 @@ pub fn prepare_uinodes(
flags: shader_flags::TEXTURED | shader_flags::CORNERS[i],
radius: [0.0; 4],
border: [0.0; 4],
size: size.into(),
size: rect_size.into(),
point: [0.0; 2],
});
}

View File

@ -1,9 +1,7 @@
use core::{hash::Hash, marker::PhantomData, ops::Range};
use crate::*;
use bevy_asset::*;
use bevy_ecs::{
prelude::Component,
prelude::{Component, With},
query::ROQueryItem,
system::{
lifetimeless::{Read, SRes},
@ -11,24 +9,22 @@ 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::{
extract_component::ExtractComponentPlugin,
globals::{GlobalsBuffer, GlobalsUniform},
load_shader_library,
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::{MainEntity, TemporaryRenderEntity},
view::*,
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_render::{
load_shader_library,
sync_world::{MainEntity, TemporaryRenderEntity},
};
use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use core::{hash::Hash, marker::PhantomData, ops::Range};
/// Adds the necessary ECS resources and render logic to enable rendering entities using the given
/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types).
@ -321,7 +317,7 @@ impl<P: PhaseItem, M: UiMaterial> RenderCommand<P> for DrawUiMaterialNode<M> {
pub struct ExtractedUiMaterialNode<M: UiMaterial> {
pub stack_index: u32,
pub transform: Mat4,
pub transform: Affine2,
pub rect: Rect,
pub border: BorderRect,
pub border_radius: ResolvedBorderRadius,
@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&MaterialNode<M>,
&InheritedVisibility,
Option<&CalculatedClip>,
@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
extracted_uinodes.uinodes.push(ExtractedUiMaterialNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: computed_node.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
material: handle.id(),
rect: Rect {
min: Vec2::ZERO,
@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
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 {
@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
];
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 π

View File

@ -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};
@ -11,7 +12,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 +26,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems,
};
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;
@ -218,7 +218,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<Rect>,
pub image: AssetId<Image>,
@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -306,7 +306,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: transform.into(),
color: image.color.into(),
rect: Rect {
min: Vec2::ZERO,
@ -497,11 +497,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)
@ -536,7 +537,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 π

View File

@ -1,4 +1,7 @@
use crate::{FocusPolicy, UiRect, Val};
use crate::{
ui_transform::{UiGlobalTransform, UiTransform},
FocusPolicy, UiRect, Val,
};
use bevy_color::{Alpha, Color};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, system::SystemParam};
@ -9,7 +12,6 @@ use bevy_render::{
view::Visibility,
};
use bevy_sprite::BorderRect;
use bevy_transform::components::Transform;
use bevy_utils::once;
use bevy_window::{PrimaryWindow, WindowRef};
use core::{f32, num::NonZero};
@ -229,6 +231,73 @@ 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, transform: UiGlobalTransform, point: Vec2) -> bool {
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 {
[
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.
}
/// 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<Vec2> {
self.size
.cmpgt(Vec2::ZERO)
.all()
.then(|| transform.try_inverse())
.flatten()
.map(|transform| transform.transform_point2(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, 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 {
@ -323,12 +392,12 @@ impl From<Vec2> for ScrollPosition {
#[require(
ComputedNode,
ComputedNodeTarget,
UiTransform,
BackgroundColor,
BorderColor,
BorderRadius,
FocusPolicy,
ScrollPosition,
Transform,
Visibility,
ZIndex
)]

View File

@ -0,0 +1,191 @@
use crate::Val;
use bevy_derive::Deref;
use bevy_ecs::component::Component;
use bevy_ecs::prelude::ReflectComponent;
use bevy_math::Affine2;
use bevy_math::Rot2;
use bevy_math::Vec2;
use bevy_reflect::prelude::*;
/// A pair of [`Val`]s used to represent a 2-dimensional size or offset.
#[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 Val2 {
/// 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: Val,
/// Translate the node along the y-axis.
/// `Val::Percent` values are resolved based on the computed height of the UI Node.
/// `Val::Auto` is resolved to `0.`.
pub y: Val,
}
impl Val2 {
pub const ZERO: Self = Self {
x: Val::ZERO,
y: Val::ZERO,
};
/// Creates a new [`Val2`] where both components are in logical pixels
pub const fn px(x: f32, y: f32) -> Self {
Self {
x: Val::Px(x),
y: Val::Px(y),
}
}
/// Creates a new [`Val2`] where both components are percentage values
pub const fn percent(x: f32, y: f32) -> Self {
Self {
x: Val::Percent(x),
y: Val::Percent(y),
}
}
/// Creates a new [`Val2`]
pub const fn new(x: Val, y: Val) -> Self {
Self { x, y }
}
/// Resolves this [`Val2`] 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 Val2 {
fn default() -> Self {
Self::ZERO
}
}
/// Relative 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(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[require(UiGlobalTransform)]
pub struct UiTransform {
/// Translate the node.
pub translation: Val2,
/// Scale the node. A negative value reflects the node in that axis.
pub scale: Vec2,
/// Rotate the node clockwise.
pub rotation: Rot2,
}
impl UiTransform {
pub const IDENTITY: Self = Self {
translation: Val2::ZERO,
scale: Vec2::ONE,
rotation: Rot2::IDENTITY,
};
/// Creates a UI transform representing a rotation.
pub fn from_rotation(rotation: Rot2) -> Self {
Self {
rotation,
..Self::IDENTITY
}
}
/// Creates a UI transform representing a responsive translation.
pub fn from_translation(translation: Val2) -> Self {
Self {
translation,
..Self::IDENTITY
}
}
/// Creates a UI transform representing a scaling.
pub fn from_scale(scale: Vec2) -> Self {
Self {
scale,
..Self::IDENTITY
}
}
/// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size`
/// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale.
pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 {
Affine2::from_scale_angle_translation(
self.scale,
self.rotation.as_radians(),
self.translation
.resolve(scale_factor, base_size, target_size),
)
}
}
impl Default for UiTransform {
fn default() -> Self {
Self::IDENTITY
}
}
/// 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)
#[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)
}
}
impl UiGlobalTransform {
/// If the transform is invertible returns its inverse.
/// Otherwise returns `None`.
#[inline]
pub fn try_inverse(&self) -> Option<Affine2> {
(self.matrix2.determinant() != 0.).then_some(self.inverse())
}
}
impl From<Affine2> for UiGlobalTransform {
fn from(value: Affine2) -> Self {
Self(value)
}
}
impl From<UiGlobalTransform> for Affine2 {
fn from(value: UiGlobalTransform) -> Self {
value.0
}
}
impl From<&UiGlobalTransform> for Affine2 {
fn from(value: &UiGlobalTransform) -> Self {
value.0
}
}

View File

@ -2,6 +2,7 @@
use crate::{
experimental::{UiChildren, UiRootNodes},
ui_transform::UiGlobalTransform,
CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale,
UiTargetCamera,
};
@ -17,7 +18,6 @@ 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(
@ -26,7 +26,7 @@ pub fn update_clipping_system(
mut node_query: Query<(
&Node,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
Option<&mut CalculatedClip>,
)>,
ui_children: UiChildren,
@ -48,14 +48,13 @@ fn update_clipping(
node_query: &mut Query<(
&Node,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
Option<&mut CalculatedClip>,
)>,
entity: Entity,
mut maybe_inherited_clip: Option<Rect>,
) {
let Ok((node, computed_node, global_transform, maybe_calculated_clip)) =
node_query.get_mut(entity)
let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity)
else {
return;
};
@ -91,10 +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(
global_transform.translation().truncate(),
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`].
//

View File

@ -571,6 +571,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
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support

View File

@ -228,7 +228,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
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 {
..Default::default()
},
UiTransform {
rotation: Rot2::radians(0.25 * PI),
..Default::default()
},
BorderRadius::all(Val::Px(10.)),
Outline {
width: Val::Px(2.),

View File

@ -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() {
@ -41,16 +40,16 @@ struct AnimationState {
struct Container(u8);
trait UpdateTransform {
fn update(&self, t: f32, transform: &mut Transform);
fn update(&self, t: f32, transform: &mut UiTransform);
}
#[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, 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.);
}
}
@ -58,7 +57,7 @@ impl UpdateTransform for Move {
struct Scale;
impl UpdateTransform for Scale {
fn update(&self, t: f32, transform: &mut Transform) {
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);
}
@ -68,9 +67,8 @@ impl UpdateTransform for Scale {
struct Rotate;
impl UpdateTransform for Rotate {
fn update(&self, t: f32, transform: &mut Transform) {
transform.rotation =
Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians());
fn update(&self, t: f32, transform: &mut UiTransform) {
transform.rotation = Rot2::radians(ops::cos(t * TAU) * 45.0);
}
}
@ -175,10 +173,6 @@ fn spawn_container(
update_transform: impl UpdateTransform + Component,
spawn_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
let mut transform = Transform::default();
update_transform.update(0.0, &mut transform);
parent
.spawn((
Node {
@ -198,11 +192,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 +224,10 @@ fn update_animation(
fn update_transform<T: UpdateTransform + Component>(
animation: Res<AnimationState>,
mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>,
mut containers: Query<(&mut UiTransform, &T)>,
) {
for (mut transform, mut node, computed_node, update_transform) in &mut containers {
for (mut transform, 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());
}
}

View File

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

302
examples/ui/ui_transform.rs Normal file
View File

@ -0,0 +1,302 @@
//! 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::*;
use core::f32::consts::FRAC_PI_8;
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 Rot2);
/// 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<Interaction>, With<Button>),
>,
mut rotator_query: Query<&mut UiTransform, With<TargetNode>>,
) {
for (interaction, mut color, maybe_rotate, maybe_scale) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
*color = PRESSED_BUTTON.into();
if let Some(step) = maybe_rotate {
for mut transform in rotator_query.iter_mut() {
transform.rotation *= step.0;
}
}
if let Some(step) = maybe_scale {
for mut transform in rotator_query.iter_mut() {
transform.scale += step.0;
transform.scale =
transform.scale.clamp(Vec2::splat(0.25), Vec2::splat(3.0));
}
}
}
Interaction::Hovered => {
*color = HOVERED_BUTTON.into();
}
Interaction::None => {
*color = NORMAL_BUTTON.into();
}
}
}
}
// move the rotating panel when the arrow keys are pressed
fn translation_system(
time: Res<Time>,
input: Res<ButtonInput<KeyCode>>,
mut translation_query: Query<&mut UiTransform, With<TargetNode>>,
) {
let controls = [
(KeyCode::ArrowLeft, -Vec2::X),
(KeyCode::ArrowRight, Vec2::X),
(KeyCode::ArrowUp, -Vec2::Y),
(KeyCode::ArrowDown, Vec2::Y),
];
for &(key_code, direction) in &controls {
if input.pressed(key_code) {
for mut transform in translation_query.iter_mut() {
let d = direction * 50.0 * time.delta_secs();
let (Val::Px(x), Val::Px(y)) = (transform.translation.x, transform.translation.y)
else {
continue;
};
let x = (x + d.x).clamp(-150., 150.);
let y = (y + d.y).clamp(-150., 150.);
transform.translation = Val2::px(x, y);
}
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// UI camera
commands.spawn(Camera2d);
// Root node filling the whole screen
commands.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::BLACK),
children![(
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceEvenly,
column_gap: Val::Px(25.0),
row_gap: Val::Px(25.0),
..default()
},
BackgroundColor(Color::BLACK),
children![
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
row_gap: Val::Px(10.0),
column_gap: Val::Px(10.0),
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::BLACK),
GlobalZIndex(1),
children![
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
RotateButton(Rot2::radians(-FRAC_PI_8)),
children![(Text::new("<--"), TextColor(Color::BLACK),)]
),
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
ScaleButton(-0.25),
children![(Text::new("-"), TextColor(Color::BLACK),)]
),
]
),
// Target node with its own set of buttons
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
width: Val::Px(300.0),
height: Val::Px(300.0),
..default()
},
BackgroundColor(DARK_GRAY.into()),
TargetNode,
children![
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
children![(Text::new("Top"), TextColor(Color::BLACK))]
),
(
Node {
align_self: AlignSelf::Stretch,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
},
children![
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
UiTransform::from_rotation(Rot2::radians(
-std::f32::consts::FRAC_PI_2
)),
children![(Text::new("Left"), TextColor(Color::BLACK),)]
),
(
Node {
width: Val::Px(100.),
height: Val::Px(100.),
..Default::default()
},
ImageNode {
image: asset_server.load("branding/icon.png"),
image_mode: NodeImageMode::Stretch,
..default()
}
),
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
UiTransform::from_rotation(Rot2::radians(
core::f32::consts::FRAC_PI_2
)),
BackgroundColor(Color::WHITE),
children![(Text::new("Right"), TextColor(Color::BLACK))]
),
]
),
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
UiTransform::from_rotation(Rot2::radians(std::f32::consts::PI)),
children![(Text::new("Bottom"), TextColor(Color::BLACK),)]
),
]
),
// Right column of controls
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
row_gap: Val::Px(10.0),
column_gap: Val::Px(10.0),
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::BLACK),
GlobalZIndex(1),
children![
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
RotateButton(Rot2::radians(FRAC_PI_8)),
children![(Text::new("-->"), TextColor(Color::BLACK),)]
),
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
ScaleButton(0.25),
children![(Text::new("+"), TextColor(Color::BLACK),)]
),
]
)
]
)],
));
}

View File

@ -0,0 +1,6 @@
---
title: RelativeCursorPosition is object-centered
pull_requests: [16615]
---
`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 a visible section of the UI node.

View File

@ -0,0 +1,32 @@
---
title: Specialized UI transform
pull_requests: [16615]
---
Bevy UI now uses specialized 2D UI transform components `UiTransform` and `UiGlobalTransform` in place of `Transform` and `GlobalTransform`.
UiTransform is a 2d-only equivalent of Transform with a responsive translation in `Val`s. `UiGlobalTransform` newtypes `Affine2` and is updated in `ui_layout_system
`Node` now requires `UiTransform` instead of `Transform`. `UiTransform` requires `UiGlobalTransform`.
The `UiTransform` equivalent of the `Transform`:
```rust
Transform {
translation: Vec3 { x, y, z },
rotation:Quat::from_rotation_z(radians),
scale,
}
```
is
```rust
UiTransform {
translation: Val2::px(x, y),
rotation: Rot2::from_rotation(radians),
scale: scale.xy(),
}
```
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.

View File

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