Skip to content

Conversation

@PPakalns
Copy link
Contributor

@PPakalns PPakalns commented Nov 14, 2025

Objective

Implement UI nodes that can ignore parent node clipping restrictions, but still are restricted by grandparent node clipping restrictions.

Useful for creating scroll-areas that utilize scrollbar-width provided by taffy. Using IgnoreParentClip it is possible to avoid two element hierarchy requirement for scroll-areas. Instead scrollbar overlay can be inserted as child element with position: absolute; width: 100%; height: 100%; z-index: 1; ignore-scroll: true. Then scrollbars can be inserted inside this overlay element. If we want to place scrollbars on space provided by scrollbar-width, negative offset is needed, e.g. right: -8px. This goes outside clipping rect of scrollarea. Therefore functionality to ignore parent element clipping rect is needed.

Solution

Introduce IgnoreParentClip component that instructs clipping rect calculations to use grand parent clipping rect in calculations for ui entities with this component.

Testing

  • Created scrollareas in personal project that are dynamically added to overflowing elements without requiring special multi level scrollarea hierarchy. (Current bevy examples requires wrapper element that defines 2x2 grid).

@PPakalns PPakalns marked this pull request as ready for review November 14, 2025 10:44
@PPakalns
Copy link
Contributor Author

Can be used to implement scrollarea that utilizes taffy scrollbar_size:

scroll-area-auto-attached.mov

@rossleonardy
Copy link
Contributor

Is OverrideClip not suitable? With the scrollbar it basically seems like just exiting clipping altogether is an adequate solution––What is the situation where you want the grandparents clipping box to affect the element but not the parents?

@PPakalns
Copy link
Contributor Author

@rossleonardy

In case of OverrideClip
For example in provided video. Floating window content has overflow: clip. If I reduce width of the window, scrollbar will float outside the window (is not clipped),which doesn't look good.

Another option would be to set Node overflow_clip_margin. But that has the same problem, if I increase the size of clip rect it can grow outside grandparent clipping rect and would result in scrollbar floating outside the floating window.

@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 D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 16, 2025
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.

I don't think this is the right approach. Instead the scrollbars should just be rendered for the parent element of the scrolling area, which precludes the need for this opt out.

@rossleonardy
Copy link
Contributor

@rossleonardy

In case of OverrideClip For example in provided video. Floating window content has overflow: clip. If I reduce width of the window, scrollbar will float outside the window (is not clipped),which doesn't look good.

Another option would be to set Node overflow_clip_margin. But that has the same problem, if I increase the size of clip rect it can grow outside grandparent clipping rect and would result in scrollbar floating outside the floating window.

I see. I think the correct answer here is better ergonomics around adding children to the right place or something like react portals, or simply living with the boilerplate of wrappers. I don't disagree with any of the code or the implementation itself, but that i think this kind of corner-case feature is very easy to have regressions on or make future changes to clipping more difficult

@PPakalns
Copy link
Contributor Author

PPakalns commented Nov 17, 2025

@rossleonardy @ickshonpe

Thanks for your comments. I understand your point of view. I will tell you my experience with current bevy ui scrollarea approach.


I would argue against requiring unnecessary ui node hierarchy where it is not needed. Taffy layout already handles scroll overflow and even provides space for scrollbars.

No need for any portals, boilerplate, parent elements.

I am currently creating complex ui interfaces in bevy ui: https://github.com/PPakalns/bevy_immediate/

I have already worked with a ton of scrollareas in my ui and I see problems with the current "grid with content and scrollbars" approach or with any "wrapper node approach".

  1. It is hard to experiment with layout. Currently to make an element into a scrollarea code changes are needed to add, remove wrapper element. If you use approach where just overflow: scroll property controls whether something is or is not scrollarea, you can easily play around with different layouts with minimal changes. Especially if you use hot reloadable stylesheets like bevy_flair. And systems to insert, remove scrollareas when needed.

  2. It is hard to think about the resulting layout with wrapper node approach. Every time when I want to add scrollarea, suddenly styling must be split into two seperate parts.

    • For outer element
    • For inner element
      If I remove scrollarea somewhere, node styling values now must be merged together.
      If you have tried to do this for a while, it is very time consuming and it is very easy to make mistakes.
  3. Taffy layout system already provides space where scrollbars could be drawn. This is easiest method to utilize this space without introducing very convulated code. With this method inserting scrollarea overlay as child element is easy and works with existing bevy_ui_widget scrollbar implementation.

  4. MOST IMPORTANT:
    It is a lot easier to create a layout using web tools and to migrate it over to bevy without introducing additional hierarchy.
    Introducing "additional" hierarchy breaks a lot of layouts and make them more complex.

IgnoreParentClip is the only needed change to resolve these issues.

I think that these changes or similar functionality would be very, very useful to simplify ui creation in bevy. ❤️

@PPakalns
Copy link
Contributor Author

PPakalns commented Nov 17, 2025

@ickshonpe

I don't think this is the right approach. Instead the scrollbars should just be rendered for the parent element of the scrolling area, which precludes the need for this opt out.

There is no easy way to correctly position scrollbars at correct place when rendering them in parent element without requiring additional wrapper element.

Of course this could be done by manually positioning position: absolute; element on top of it with left, top, width, height properties, but that would introduce 1 frame delay and unnecessary taffy layout calculations or manual value changes to ComputedNode, Node, UiTransform which would be a lot more difficult to implement.

@PPakalns
Copy link
Contributor Author

PPakalns commented Nov 17, 2025

@rossleonardy

i think this kind of corner-case feature is very easy to have regressions on or make future changes to clipping more difficult

I can migrate existing Scrollbars example or add a new example with this approach.

@PPakalns
Copy link
Contributor Author

PPakalns commented Nov 19, 2025

Added an example for Scrollbars.

example

Check out how much simpler the possible api is for defining scroll areas:
Only need to set overflow: scroll; scrollbar_width: 8px; and add overlay element.

Example:

fn scroll_area_with_overlay_demo() -> impl Bundle {
(
// The scroll area with scrollable content
Node {
display: Display::Flex,
overflow: Overflow::scroll(),
scrollbar_width: 8.,
width: px(200),
height: px(150),
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(4)),
..default()
},
BackgroundColor(colors::GRAY1.into()),
ScrollPosition(Vec2::new(0.0, 10.0)),
Children::spawn((
// Add scroll area overlay to this element
scroll_area_overlay_for_overlay_demo(),
//
// The actual content of the scrolling area
Spawn(text_row("Alpha Wolf")),
Spawn(text_row("Beta Blocker")),
Spawn(text_row("Delta Sleep")),
Spawn(text_row("Gamma Ray")),
Spawn(text_row("Epsilon Eridani")),
Spawn(text_row("Zeta Function")),
Spawn(text_row("Lambda Calculus")),
Spawn(text_row("Nu Metal")),
Spawn(text_row("Pi Day")),
Spawn(text_row("Chi Pants")),
Spawn(text_row("Psi Powers")),
// Spawn(text_row("Omega Fatty Acid")),
)),
)
}

P.S. Grid example of course could be simplified too with wrapper function. But remember that grid element and scroll element requires separate styling and it is a lot less ergonomic.

Sadly children macro doesn't support SpawnWith, so additional changes would be needed to get full power out of this approach when using children!. I myself am using systems to insert, remove this overlay automatically.

@ickshonpe
Copy link
Contributor

ickshonpe commented Nov 21, 2025

@ickshonpe

I don't think this is the right approach. Instead the scrollbars should just be rendered for the parent element of the scrolling area, which precludes the need for this opt out.

There is no easy way to correctly position scrollbars at correct place when rendering them in parent element without requiring additional wrapper element.

Of course this could be done by manually positioning position: absolute; element on top of it with left, top, width, height properties, but that would introduce 1 frame delay and unnecessary taffy layout calculations or manual value changes to ComputedNode, Node, UiTransform which would be a lot more difficult to implement.

My reply was a bit misphrased. With "parent element of the scrolling area", the parent would be the UI node with overflow_scroll: Overflow::scroll() and the scrolling area would be the content that is being scrolled that is added as its children.

In this model there wouldn't be any need for nodes representing the scrollbars at all, which avoids the synchronisation and clipping problems.

@ickshonpe
Copy link
Contributor

ickshonpe commented Nov 21, 2025

And I think even with a scrolling area widget, it's simpler to have a parent node that contains the scrolling area, with child nodes representing the scrolling view, horizontal scrollbar, and vertical scrollbar.

So a construction something like (very loosely):

(
    scrolling_widget(),
    children![
        (
            // Suspect it might be simplest to always spawn scrollbar nodes, and disable them with `Display::None` when the content isn't overflowing.
            horizontal_scrollbar(),
            vertical_scrollbar(),
            (
                scrolling_view(),
                children![
                    text_row("Alpha Wolf"),
                    text_row("Beta Blocker"),
                    text_row("Delta Sleep"),
                    text_row("Gamma Ray"),
                    text_row("Epsilon Eridani"),
                    text_row("Zeta Function"),
                    text_row("Lambda Calculus"),
                    text_row("Nu Metal"),
                    text_row("Pi Day"),
                    text_row("Chi Pants"),
                    text_row("Psi Powers"),
                ],
            )
        ),
    ],
)

I don't like the IgnoreParentClip opt-out. If you don't want a node to be clipped, don't parent it to a node which clips its descendants. Even OverrideClip feels like a compromise, but it has a clear use case, and any alternative probably wouldn't make it in time for a 0.18 release.

Comment on lines +202 to +236
fn scroll_area_with_overlay_demo() -> impl Bundle {
(
// The scroll area with scrollable content
Node {
display: Display::Flex,
overflow: Overflow::scroll(),
scrollbar_width: 8.,
width: px(200),
height: px(150),
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(4)),
..default()
},
BackgroundColor(colors::GRAY1.into()),
ScrollPosition(Vec2::new(0.0, 10.0)),
Children::spawn((
// Add scroll area overlay to this element
scroll_area_overlay_for_overlay_demo(),
//
// The actual content of the scrolling area
Spawn(text_row("Alpha Wolf")),
Spawn(text_row("Beta Blocker")),
Spawn(text_row("Delta Sleep")),
Spawn(text_row("Gamma Ray")),
Spawn(text_row("Epsilon Eridani")),
Spawn(text_row("Zeta Function")),
Spawn(text_row("Lambda Calculus")),
Spawn(text_row("Nu Metal")),
Spawn(text_row("Pi Day")),
Spawn(text_row("Chi Pants")),
Spawn(text_row("Psi Powers")),
// Spawn(text_row("Omega Fatty Acid")),
)),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I dislike this design with the overlay stored as a sibling of the content, as it requires the user to be mindful of the overlay node. Ideally the construction of the scrolling view widget should be completely opaque to the user.

So here instead I'd prefer a composition something like:

Suggested change
fn scroll_area_with_overlay_demo() -> impl Bundle {
(
// The scroll area with scrollable content
Node {
display: Display::Flex,
overflow: Overflow::scroll(),
scrollbar_width: 8.,
width: px(200),
height: px(150),
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(4)),
..default()
},
BackgroundColor(colors::GRAY1.into()),
ScrollPosition(Vec2::new(0.0, 10.0)),
Children::spawn((
// Add scroll area overlay to this element
scroll_area_overlay_for_overlay_demo(),
//
// The actual content of the scrolling area
Spawn(text_row("Alpha Wolf")),
Spawn(text_row("Beta Blocker")),
Spawn(text_row("Delta Sleep")),
Spawn(text_row("Gamma Ray")),
Spawn(text_row("Epsilon Eridani")),
Spawn(text_row("Zeta Function")),
Spawn(text_row("Lambda Calculus")),
Spawn(text_row("Nu Metal")),
Spawn(text_row("Pi Day")),
Spawn(text_row("Chi Pants")),
Spawn(text_row("Psi Powers")),
// Spawn(text_row("Omega Fatty Acid")),
)),
)
}
fn scroll_area_with_overlay_demo() -> impl Bundle {
(
// The scroll area with scrollable content
Node {
display: Display::Flex,
overflow: Overflow::scroll(),
scrollbar_width: 8.,
width: px(200),
height: px(150),
padding: UiRect::all(px(4)),
..default()
},
BackgroundColor(colors::GRAY1.into()),
ScrollPosition(Vec2::new(0.0, 10.0)),
Children::spawn((
// Add scroll area overlay to this element
scroll_area_overlay_for_overlay_demo(),
Spawn((
Node {
flex_direction: FlexDirection::Column,
..default()
},
Children::spawn((
// The actual content of the scrolling area
Spawn(text_row("Alpha Wolf")),
Spawn(text_row("Beta Blocker")),
Spawn(text_row("Delta Sleep")),
Spawn(text_row("Gamma Ray")),
Spawn(text_row("Epsilon Eridani")),
Spawn(text_row("Zeta Function")),
Spawn(text_row("Lambda Calculus")),
Spawn(text_row("Nu Metal")),
Spawn(text_row("Pi Day")),
Spawn(text_row("Chi Pants")),
Spawn(text_row("Psi Powers")),
// Spawn(text_row("Omega Fatty Acid")),
)),
)),
)),
)
}

Copy link
Contributor Author

@PPakalns PPakalns Nov 21, 2025

Choose a reason for hiding this comment

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

This again introduces multi layer layout that makes getting the right layout behavior working correctly more difficult. (at least in my experience) And requires ui styling to be split for outer and inner node.

If you play around with scrollareas that can shrink or expand only to the required size, it is easy to see that introducing additional hierarchy makes it a lot harder to predict how layout will behave.

This was the reason why I want to get rid of requiring additional hierarchy for scroll areas.

Instead users can simply add overflow: clip and scrollbar_width where necessary.

And users of scroll area can write a system that automatically inserts scroll area overlay where it's needed.

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 can even add that logic to the example, it is a very simple system and additional marker component to track if overlay has been inserted.

@PPakalns
Copy link
Contributor Author

@ickshonpe

I think we have two different problems that we are trying to resolve. 😄

In my case: I just want to simplify scroll area use as much as possible, but with keeping full customization of how they are styled and work.

  1. By being able to make an element into a scroll area by changing overflow: scroll property. Everything else can be automatically managed by systems that inserts, removes scroll area overlay.
  2. I want to keep full functionality of bevy ui to customize these scroll areas, scroll bars however I want.

This was the simplest approach to get this simple mechanism working. I do not need to modify layout, introduce additional ui element hierarchy to turn something into a scroll area.

It works great with hot reloading solutions like bevy subsecond or bevy flair. It make creating ui interfaces a lot easier.

@PPakalns
Copy link
Contributor Author

PPakalns commented Nov 21, 2025

I don't like the IgnoreParentClip opt-out. If you don't want a node to be clipped, don't parent it to a node which clips its descendants. Even OverrideClip feels like a compromise, but it has a clear use case, and any alternative probably wouldn't make it in time for a 0.18 release.

I don't see any other approach where we can fully reuse bevy ui functionality to get maximal customization out of the box. There is no easy alternative way that I know without additional hierarchy to automatically place scroll bars at correct place that work with so many use cases that i have mentioned.

If you do not like introducing additional Component corner case.

Maybe we can merge together OverrideClip with IgnoreParentClip and I can add it as boolean parameter to OverrideClip that controls whether all clipping is cancelled or just the parent one.

Like this is such a simple feature that enables so many use cases with such a small change to clipping logic.

@PPakalns
Copy link
Contributor Author

Additional use case for this feature

Adding decorations to elements in front and background (even dynamically) without requiring changes in ui element hierarchy.
These elements still can be placed inside Scroll areas, floating windows, etc. and their decorations will be correctly clipped.

Basically IgnoreParentClip can be used for all use cases of OverrideClip , but in situations where clipping still must happen, because it is inserted inside some other clipped ui element.

Maybe IgnoreParentClip functionality should just be a parameter inside OverrideClip 🤔

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 D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants