Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,11 @@ pub struct CalculatedClip {
#[derive(Component)]
pub struct OverrideClip;

/// UI node entities with this component will ignore parent node clipping rect,
/// instead the node will inherit the grandparent node clipping rect.
#[derive(Component)]
pub struct IgnoreParentClip;

#[expect(
rustdoc::redundant_explicit_links,
reason = "To go around the `<code>` limitations, we put the link twice so we're \
Expand Down
29 changes: 25 additions & 4 deletions crates/bevy_ui/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
experimental::{UiChildren, UiRootNodes},
ui_transform::UiGlobalTransform,
CalculatedClip, ComputedUiRenderTargetInfo, ComputedUiTargetCamera, DefaultUiCamera, Display,
Node, OverflowAxis, OverrideClip, UiScale, UiTargetCamera,
IgnoreParentClip, Node, OverflowAxis, OverrideClip, UiScale, UiTargetCamera,
};

use super::ComputedNode;
Expand All @@ -28,6 +28,7 @@ pub fn update_clipping_system(
&UiGlobalTransform,
Option<&mut CalculatedClip>,
Has<OverrideClip>,
Has<IgnoreParentClip>,
)>,
ui_children: UiChildren,
) {
Expand All @@ -38,6 +39,7 @@ pub fn update_clipping_system(
&mut node_query,
root_node,
None,
None,
);
}
}
Expand All @@ -51,16 +53,28 @@ fn update_clipping(
&UiGlobalTransform,
Option<&mut CalculatedClip>,
Has<OverrideClip>,
Has<IgnoreParentClip>,
)>,
entity: Entity,
maybe_grandparent_inherited_clip: Option<Rect>,
mut maybe_inherited_clip: Option<Rect>,
) {
let Ok((node, computed_node, transform, maybe_calculated_clip, has_override_clip)) =
node_query.get_mut(entity)
let Ok((
node,
computed_node,
transform,
maybe_calculated_clip,
has_override_clip,
has_ignore_parent_clip,
)) = node_query.get_mut(entity)
else {
return;
};

if has_ignore_parent_clip {
maybe_inherited_clip = maybe_grandparent_inherited_clip;
}

// If the UI node entity has an `OverrideClip` component, discard any inherited clip rect
if has_override_clip {
maybe_inherited_clip = None;
Expand Down Expand Up @@ -129,7 +143,14 @@ fn update_clipping(
};

for child in ui_children.iter_ui_children(entity) {
update_clipping(commands, ui_children, node_query, child, children_clip);
update_clipping(
commands,
ui_children,
node_query,
child,
maybe_inherited_clip,
children_clip,
);
}
}

Expand Down
170 changes: 163 additions & 7 deletions examples/ui/scrollbars.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Demonstrations of scrolling and scrollbars.

use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
ecs::{
relationship::RelatedSpawner,
spawn::{SpawnWith, SpawnableList},
},
input_focus::{
tab_navigation::{TabGroup, TabNavigationPlugin},
InputDispatchPlugin,
Expand Down Expand Up @@ -33,32 +36,57 @@ fn setup_view_root(mut commands: Commands) {
commands.spawn((
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceAround,
position_type: PositionType::Absolute,
left: px(0),
top: px(0),
right: px(0),
bottom: px(0),
padding: UiRect::all(px(3)),
row_gap: px(6),
..Default::default()
},
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
UiTargetCamera(camera),
TabGroup::default(),
Children::spawn((Spawn(Text::new("Scrolling")), Spawn(scroll_area_demo()))),
Children::spawn((
Spawn((
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(6),
..Default::default()
},
Children::spawn((
Spawn(Text::new("Scroll area inside grid")),
Spawn(scroll_area_inside_grid_demo()),
)),
)),
Spawn((
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(6),
..Default::default()
},
Children::spawn((
Spawn(Text::new("Scroll area with overlay")),
Spawn(scroll_area_with_overlay_demo()),
)),
)),
)),
));
}

/// Create a scrolling area.
/// Create a scrolling area inside grid.
///
/// The "scroll area" is a container that can be scrolled. It has a nested structure which is
/// The "scroll area" is a container that can be scrolled. In this demo it has a nested structure which is
/// three levels deep:
/// - The outermost node is a grid that contains the scroll area and the scrollbars.
/// - The scroll area is a flex container that contains the scrollable content. This
/// is the element that has the `overflow: scroll` property.
/// - The scrollable content consists of the elements actually displayed in the scrolling area.
fn scroll_area_demo() -> impl Bundle {
fn scroll_area_inside_grid_demo() -> impl Bundle {
(
// Frame element which contains the scroll area and scrollbars.
Node {
Expand Down Expand Up @@ -157,6 +185,134 @@ fn scroll_area_demo() -> impl Bundle {
)
}

/// Create a scrolling area with overlay.
///
/// The "scroll area" is a container that can be scrolled. In this demo it doesn't require
/// a wrapper element as in `scroll_area_inside_grid_demo`. Instead scrollbars are drawn
/// inside overlay element. It has the following structure:
/// - The scroll area is a flex container that contains the scrollable content. This
/// is the element that has the `overflow: scroll` property.
/// - The scrollable content consists of the elements actually displayed in the scrolling area.
/// - The scroll area overlay is inserted as child element inside scroll area and is positioned
/// in a way so that it is drawn on top of the scroll area. Scrollbars are inserted inside
/// overlay.
///
/// Scrollbars can be positioned on top of the content or explicit space can be allocated using
/// `Node::scrollbar_width` property.
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")),
)),
)
}
Comment on lines +202 to +236
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.


/// Function inserts scroll area overlay with scrollbars
fn scroll_area_overlay_for_overlay_demo() -> impl SpawnableList<ChildOf> {
SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
// Note that we're using `SpawnWith` here because we need to get the entity id of the
// scroll area in order to set the target of the scrollbars.
let scroll_area_id = parent.target_entity();
parent.spawn((
// Overlay is positioned on top of overlay.
Node {
position_type: PositionType::Absolute,
left: px(0),
top: px(0),
width: percent(100.),
height: percent(100.),
overflow: Overflow::visible(),
..Default::default()
},
// Ignore scroll area clip to position scrollbars outside scroll area
// at the space provided by `Node::scrollbar_width`
IgnoreParentClip,
// Keep scroll area overlay static while scroll area is scrolled
IgnoreScroll(BVec2::TRUE),
// Draw scroll area overlay on top of ui
ZIndex(1),
Children::spawn((
Spawn((
Node {
position_type: PositionType::Absolute,
width: px(8),
right: px(-8),
height: percent(100.),
..default()
},
Scrollbar {
orientation: ControlOrientation::Vertical,
target: scroll_area_id,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Node {
position_type: PositionType::Absolute,
border_radius: BorderRadius::all(px(4)),
..default()
},
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
CoreScrollbarThumb,
))),
)),
Spawn((
Node {
position_type: PositionType::Absolute,
height: px(8),
bottom: px(-8),
width: percent(100.),
..default()
},
Scrollbar {
orientation: ControlOrientation::Horizontal,
target: scroll_area_id,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Node {
position_type: PositionType::Absolute,
border_radius: BorderRadius::all(px(4)),
..default()
},
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
CoreScrollbarThumb,
))),
)),
)),
));
})
}

/// Create a list row
fn text_row(caption: &str) -> impl Bundle {
(
Expand Down