Skip to content
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

UI Scrolling #15291

Merged
merged 7 commits into from
Sep 23, 2024
Merged

UI Scrolling #15291

merged 7 commits into from
Sep 23, 2024

Conversation

Piefayth
Copy link
Contributor

@Piefayth Piefayth commented Sep 18, 2024

Objective

Solution

Adapted from #8104 and affords the same benefits.

Additions

  • Update scrolling on relayout (height of node or contents may have changed)
  • Make ScrollPosition component optional for ui nodes to avoid checking every node on scroll
  • Nested scrollviews

Omissions

  • Removed input handling for scrolling from bevy_ui. Users should update ScrollPosition directly.

Implementation

Adds a new ScrollPosition component. Updating this component on a Node with an overflow axis set to OverflowAxis::Scroll will reposition its children by that amount when calculating node transforms. As before, no impact on the underlying Taffy layout.

Calculating this correctly is trickier than it was in #8104 due to "Update scrolling on relayout".

Background

When ScrollPosition is updated directly by the user, it can be trivially handled in-engine by adding the parent's scroll position to the final location of each child node. However, other layout actions may result in a situation where ScrollPosition needs to be updated. Consider a 1000 pixel tall vertically scrolling list of 100 elements, each 100 pixels tall. Scrolled to the bottom, the ScrollPosition.offset_y is 9000, just enough to display the last element in the list. When removing an element from that list, the new desired ScrollPosition.offset_y is 8900, but, critically, that is not known until after the sizes and positions of the children of the scrollable node are resolved.

All user scrolling code today handles this by delaying the resolution by one frame. One notable disadvantage of this is the inability to support WinitSettings::desktop_app(), since there would need to be an input AFTER the layout change that caused the scroll position to update for the results of the scroll position update to render visually.

I propose the alternative in this PR, which allows for same-frame resolution of scrolling layout.

Resolution

Edit: Below resolution is outdated, and replaced with the simpler usage of taffy's Layout::content_size.

When recursively iterating the children of a node, each child now returns a Vec2 representing the location of their own bottom right corner. Then, [[0,0, [x,y]] represents a bounding box containing the scrollable area filled by that child. Scrollable parents aggregate those areas into the bounding box of all children, then consider that result against ScrollPosition to ensure its validity.

In the event that resolution of the layout of the children invalidates the ScrollPosition (e.g. scrolled further than there were children to scroll to), all children of that node must be recursively repositioned. The position of each child must change as a result of the change in scroll position.

Therefore, this implementation takes care to only spend the cost of the "second layout pass" when a specific node actually had a ScrollPosition forcibly updated by the layout of its children.

Testing

Examples in ui/scroll.rs. There may be more complex node/style interactions that were unconsidered.


Showcase

scroll

Alternatives

  • bevy_ui doesn't support scrolling.
  • bevy_ui implements scrolling with a one-frame delay on reactions to layout changes.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-UI Graphical user interfaces, styles, layouts, and widgets 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 labels Sep 18, 2024
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct ScrollPosition {
/// How far across the node is scrolled (0 = not scrolled / scrolled to right)
Copy link
Member

Choose a reason for hiding this comment

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

These docs should clearly describe the units.

@@ -150,6 +150,44 @@ impl Default for Node {
}
}

/// The scroll position on the node
Copy link
Member

Choose a reason for hiding this comment

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

A bit more context about how this is used would be nice.

@@ -0,0 +1,388 @@
//! This example illustrates scrolling in Bevy UI.
//!
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
//!

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Really pleased to see this adopted, and this is looking good. Just a few small suggestions.

) {
for mouse_wheel_event in mouse_wheel_events.read() {
let (mut dx, mut dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (mouse_wheel_event.x * 20., mouse_wheel_event.y * 20.),
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this 20 scale factor be pulled out into a constant and used for the font size?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, we won't get perfectly 20 pixel tall text out of that anyway. But what I DID do was define it as LINE_HEIGHT and force the scrolled children in the vertical scroll example to be exactly that height.

scroll-lines

Great idea!!

Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

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

Agree that a one-frame delay is completely unacceptable. I'm worried that the changes here introduce too much unnecessary extra complexity to ui_layout_system though and I really don't like having to query for Style again in the update_uinode_geometry_recursive function.

I've not gone through the PR in detail though, maybe there's no other way. I'll try and find time to take a longer look this weekend or on Monday.

@Piefayth
Copy link
Contributor Author

Piefayth commented Sep 20, 2024

Agree that a one-frame delay is completely unacceptable. I'm worried that the changes here introduce too much unnecessary extra complexity to ui_layout_system though and I really don't like having to query for Style again in the update_uinode_geometry_recursive function.

I've not gone through the PR in detail though, maybe there's no other way. I'll try and find time to take a longer look this weekend or on Monday.

Cool, we are aligned. I've greatly reduced the complexity thanks to @nicoburns.

Regarding Style, I did find that one of my usages of it was extraneous / leftover from my work in progress. So all I use it for now is to force the local scroll_position to zero if that axis isn't set to OverflowAxis::Scroll. This could alternatively be accomplished by moving that logic to a separate system that validates and sanitizes the actual ScrollPosition component before layout runs.

crates/bevy_ui/src/layout/mod.rs Outdated Show resolved Hide resolved
Comment on lines 379 to 390
Vec2::new(
if style.overflow.x == OverflowAxis::Scroll {
scroll_pos.offset_x
} else {
0.0
},
if style.overflow.y == OverflowAxis::Scroll {
scroll_pos.offset_y
} else {
0.0
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic seems correct and necessary to me, but perhaps is_scrollable in each axis could be copied somewhere else ahead of time if accessing Style here is undesirable.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Much simpler. Good recommendations @nicoburns!

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 20, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Sep 23, 2024
Merged via the queue into bevyengine:main with commit 55dddaf Sep 23, 2024
30 checks passed
@alice-i-cecile
Copy link
Member

Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1693 if you'd like to help out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support scrollable UI nodes (Overflow::Scroll?)
4 participants