From a047299a377f7fe2e27a0b5cb1c7858c8bcf28e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Fri, 14 Nov 2025 02:00:37 +0200 Subject: [PATCH 1/4] Add IgnoreParentClip component to support child nodes that decorate parent node --- crates/bevy_ui/src/ui_node.rs | 5 +++++ crates/bevy_ui/src/update.rs | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ac086d02a5556..1d92a41eb6a75 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -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 `` limitations, we put the link twice so we're \ diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 77659effabcf1..78fdafbb0c71d 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -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; @@ -28,6 +28,7 @@ pub fn update_clipping_system( &UiGlobalTransform, Option<&mut CalculatedClip>, Has, + Has, )>, ui_children: UiChildren, ) { @@ -38,6 +39,7 @@ pub fn update_clipping_system( &mut node_query, root_node, None, + None, ); } } @@ -51,16 +53,28 @@ fn update_clipping( &UiGlobalTransform, Option<&mut CalculatedClip>, Has, + Has, )>, entity: Entity, + mut maybe_grandparent_inherited_clip: Option, mut maybe_inherited_clip: Option, ) { - 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; @@ -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, + ); } } From 7279a9f8248e2373ffbef823d5a8a59bdcc6df7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Fri, 14 Nov 2025 12:40:43 +0200 Subject: [PATCH 2/4] Remove unnecessary `mut` --- crates/bevy_ui/src/update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 78fdafbb0c71d..72843ab136fce 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -56,7 +56,7 @@ fn update_clipping( Has, )>, entity: Entity, - mut maybe_grandparent_inherited_clip: Option, + maybe_grandparent_inherited_clip: Option, mut maybe_inherited_clip: Option, ) { let Ok(( From e6ae9e86fe441c521c57a7b4c021c1d99e3d187d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Wed, 19 Nov 2025 12:21:57 +0200 Subject: [PATCH 3/4] Added example with overlay implementation of scrollarea showcasing IgnoreParentClip --- examples/ui/scrollbars.rs | 169 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 7 deletions(-) diff --git a/examples/ui/scrollbars.rs b/examples/ui/scrollbars.rs index 96c55113179f3..c17147626b246 100644 --- a/examples/ui/scrollbars.rs +++ b/examples/ui/scrollbars.rs @@ -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, @@ -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("Scrollarea 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("Scrollarea 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 { @@ -157,6 +185,133 @@ 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 scrollarea and is positioned +/// in a way so that it is drawn on top of the scrollarea. 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")), + )), + ) +} + +/// Function inserts scrollarea overlay with scrollbars +fn scroll_area_overlay_for_overlay_demo() -> impl SpawnableList { + SpawnWith(|parent: &mut RelatedSpawner| { + // 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.), + ..Default::default() + }, + // Ignore scrollarea clip to position scrollbars outside scrollarea + // at the space provided by `Node::scrollbar_width` + IgnoreParentClip, + // Keep scrollarea overlay static while scrollarea is scrolled + IgnoreScroll(BVec2::TRUE), + // Draw scrollarea 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 { ( From 1a90ec5584aa54c71a0e7b387991364eb939f01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Wed, 19 Nov 2025 13:20:05 +0200 Subject: [PATCH 4/4] Use 'scroll area' instead of 'scrollarea' --- examples/ui/scrollbars.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/ui/scrollbars.rs b/examples/ui/scrollbars.rs index c17147626b246..81113aab4998d 100644 --- a/examples/ui/scrollbars.rs +++ b/examples/ui/scrollbars.rs @@ -58,7 +58,7 @@ fn setup_view_root(mut commands: Commands) { ..Default::default() }, Children::spawn(( - Spawn(Text::new("Scrollarea inside grid")), + Spawn(Text::new("Scroll area inside grid")), Spawn(scroll_area_inside_grid_demo()), )), )), @@ -70,7 +70,7 @@ fn setup_view_root(mut commands: Commands) { ..Default::default() }, Children::spawn(( - Spawn(Text::new("Scrollarea with overlay")), + Spawn(Text::new("Scroll area with overlay")), Spawn(scroll_area_with_overlay_demo()), )), )), @@ -193,8 +193,8 @@ fn scroll_area_inside_grid_demo() -> impl Bundle { /// - 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 scrollarea and is positioned -/// in a way so that it is drawn on top of the scrollarea. Scrollbars are inserted inside +/// - 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 @@ -235,7 +235,7 @@ fn scroll_area_with_overlay_demo() -> impl Bundle { ) } -/// Function inserts scrollarea overlay with scrollbars +/// Function inserts scroll area overlay with scrollbars fn scroll_area_overlay_for_overlay_demo() -> impl SpawnableList { SpawnWith(|parent: &mut RelatedSpawner| { // Note that we're using `SpawnWith` here because we need to get the entity id of the @@ -249,14 +249,15 @@ fn scroll_area_overlay_for_overlay_demo() -> impl SpawnableList { top: px(0), width: percent(100.), height: percent(100.), + overflow: Overflow::visible(), ..Default::default() }, - // Ignore scrollarea clip to position scrollbars outside scrollarea + // Ignore scroll area clip to position scrollbars outside scroll area // at the space provided by `Node::scrollbar_width` IgnoreParentClip, - // Keep scrollarea overlay static while scrollarea is scrolled + // Keep scroll area overlay static while scroll area is scrolled IgnoreScroll(BVec2::TRUE), - // Draw scrollarea overlay on top of ui + // Draw scroll area overlay on top of ui ZIndex(1), Children::spawn(( Spawn((