Skip to content

Specialized UI transform #16615

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 95 commits into
base: main
Choose a base branch
from

Conversation

ickshonpe
Copy link
Contributor

@ickshonpe ickshonpe commented Dec 2, 2024

Objective

Add specialized UI transform Components 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 Vals.
    • 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 Vals. 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. UiTransforms 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.

@alice-i-cecile alice-i-cecile added A-UI Graphical user interfaces, styles, layouts, and widgets X-Contentious There are nontrivial implications that should be thought through C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 2, 2024
@alice-i-cecile
Copy link
Member

alice-i-cecile commented Dec 2, 2024

I like this as an incremental move forward. We shouldn't be storing a full Transform here, but that's easy to migrate from this PR.

Should this be on ComputedNode instead though? I don't think this is intended to be user-modifiable.

@alice-i-cecile alice-i-cecile added the M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Dec 2, 2024
Copy link
Contributor

github-actions bot commented Dec 2, 2024

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

@NthTensor
Copy link
Contributor

This does seem like the least-bad option. But I thought there was code in bevy_transform to specifically filter out ui elements from propagation. Could you check if that's still there and remove it if it is?

I believe this should fix the issues big_space had with the use of Transform by ui elements, which is nice.

@ickshonpe
Copy link
Contributor Author

ickshonpe commented Dec 2, 2024

I like this as an incremental move forward. We shouldn't be storing a full Transform here, but that's easy to migrate from this PR.

Should this be on ComputedNode instead though? I don't think this is intended to be user-modifiable.

The new transform field on Node is intended to be user-modifiable. It's meant to be simar to the CSS transform property and allows users to add simple UI animations or whatever without weird hacks to get around the conflicts between the transformation propagation and UI layout update systems.

I made another branch first just before this one that removed both Transform and GlobalTransform from UI nodes and instead added the transform to ComputedNode but it felt like it would be difficult to get through review as it required lots of changes to extraction and focus and new a UI specific visibility system. Performance is much better than main with that approach though and would allow for some nice helper methods on Node like contains_point etc which would simplify a lot of the systems.

@ickshonpe
Copy link
Contributor Author

ickshonpe commented Dec 2, 2024

This does seem like the least-bad option. But I thought there was code in bevy_transform to specifically filter out ui elements from propagation. Could you check if that's still there and remove it if it is?

I believe this should fix the issues big_space had with the use of Transform by ui elements, which is nice.

I haven't seen anything like this. Afaik the only way to opt-out of transform propagation is by removing Transform and there's no UI specific filter.

@alice-i-cecile alice-i-cecile added the A-Animation Make things move and change over time label Dec 2, 2024
@BenjaminBrienen BenjaminBrienen added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 22, 2025
@alice-i-cecile alice-i-cecile removed the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Apr 23, 2025
@ickshonpe
Copy link
Contributor Author

ickshonpe commented Apr 28, 2025

Should this PR move to Draft? Was said to be nearly done, then got 12 commits merged. Also designed completely changed from initial version. I'm not sure as a reviewer when it's safe to spend time reviewing again. Thanks

Yeah my fault keep changing my mind as to how this should be implemented. I don't intend to make any more changes now though.

@NthTensor
Copy link
Contributor

Starting a review. From just the readme, this seems fantastic. Exactly what I was looking for in basically every way.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks great. This is a smaller change than I was expecting tbh. Have not run any of the examples, I'll check it out later today and approve if I don't find any bugs.

Self::ZERO
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub trait IntoVal2 {
fn px(self) -> Val2;
fn percent(self) -> Val2;
fn vw(self) -> Val2;
fn vh(self) -> Val2;
fn vmin(self) -> Val2;
fn vmax(self) -> Val2;
}
impl IntoVal2 for Vec2 {
fn px(self) -> Val2 {
Val2 { x: Val::Px(self.x), y: Val::Px(self.y) }
}
fn percent(self) -> Val2 {
Val2 { x: Val::Percent(self.x), y: Val::Percent(self.y) }
}
fn vw(self) -> Val2 {
Val2 { x: Val::Vw(self.x), y: Val::vw(self.y) }
}
fn vh(self) -> Val2 {
Val2 { x: Val::Vh(self.x), y: Val::Vh(self.y) }
}
fn vmin(self) -> Val2 {
Val2 { x: Val::Vmin(self.x), y: Val::Vmin(self.y) }
}
fn vmax(self) -> Val2 {
Val2 { x: Val::Vmax(self.x), y: Val::Vmax(self.y) }
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure but implementing these on Vec2 might be a bit controversial. I could just have Val2's px and percent methods take Vec2s instead and add more methods for the other variants instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets leave it as it is then, and I'll open up a PR adding this as follow up. I don't think it will be that controversial, and I want to add a similar pattern to some other types for conversion into Val anyway.

/// Translate the node.
pub translation: Val2,
/// Scale the node. A negative value reflects the node in that axis.
pub scale: Vec2,
Copy link
Contributor

@NthTensor NthTensor May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-uniformscale my old enemy. This may create issues with shear just like we have in 3d. It's probably fine. Non-blocking, just a call-out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep only went with scaling in both axis because it was supported with Transform/GlobalTransform and I wanted to avoid too much controversy with this PR.

Comment on lines +73 to +74
/// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you justified this change anywhere. I'm not against it, but why?

Copy link
Contributor Author

@ickshonpe ickshonpe May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the changes to replace top-left-aligned coordinates and consistantly use only object-centered coordinates.

Definitely some users find having the origin at the top-left more intuitive though, if people really hate it I'm not against changing it back in this case.

@ickshonpe
Copy link
Contributor Author

ickshonpe commented May 28, 2025

Starting a review. From just the readme, this seems fantastic. Exactly what I was looking for in basically every way.

thanks that's great, I was just about to go and beg on discord for someone to review this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Animation Make things move and change over time A-Rendering Drawing game state to the screen A-Transform Translations, rotations and scales A-UI Graphical user interfaces, styles, layouts, and widgets C-Bug An unexpected or incorrect behavior C-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through
Projects
Status: No status
Status: Widget-ready
Status: No status
Development

Successfully merging this pull request may close these issues.

Responsive translation support for UI nodes
5 participants