From c62359509717c57eb366293b52edf97808fcfcc3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 31 Mar 2026 23:16:41 +0100 Subject: [PATCH 01/15] Removed `Node` from scrollbar thumb nodes, instead `CoreScrollbarThumb` requires all the UI components and gains a `border_radius` field. `update_scrollbar_thumb` updates its `UiGlobalTransform` and `ComputedNode` in `PostLayout`. --- crates/bevy_ui_widgets/src/scrollbar.rs | 177 ++++++++++++------ examples/ui/scroll_and_overflow/scrollbars.rs | 13 +- 2 files changed, 122 insertions(+), 68 deletions(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 6c3087d7576b5..4c4c3cc778035 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -1,4 +1,5 @@ use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::visibility::Visibility; use bevy_ecs::{ component::Component, entity::Entity, @@ -6,13 +7,16 @@ use bevy_ecs::{ observer::On, query::{With, Without}, reflect::ReflectComponent, + schedule::IntoScheduleConfigs, system::{Query, Res}, }; -use bevy_math::Vec2; +use bevy_math::{Affine2, Vec2}; use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{ - ComputedNode, ComputedUiRenderTargetInfo, Node, ScrollPosition, UiGlobalTransform, UiScale, Val, + BackgroundColor, BorderColor, BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, + ComputedUiTargetCamera, ContentSize, FocusPolicy, ScrollPosition, UiGlobalTransform, UiScale, + UiSystems, UiTransform, ZIndex, }; /// Used to select the orientation of a scrollbar, slider, or other oriented control. @@ -64,11 +68,27 @@ pub struct Scrollbar { /// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of /// the scrollbar). This should be a child of the scrollbar entity. -#[derive(Component, Debug)] -#[require(CoreScrollbarDragState)] +#[derive(Component, Debug, Default)] +#[require( + CoreScrollbarDragState, + ComputedNode, + ContentSize, + ComputedUiTargetCamera, + ComputedUiRenderTargetInfo, + UiTransform, + BackgroundColor, + BorderColor, + FocusPolicy, + ScrollPosition, + Visibility, + ZIndex +)] #[derive(Reflect)] #[reflect(Component)] -pub struct CoreScrollbarThumb; +pub struct CoreScrollbarThumb { + /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`update_scrollbar_thumb`]. + pub border_radius: BorderRadius, +} impl Scrollbar { /// Construct a new scrollbar. @@ -244,11 +264,23 @@ fn scrollbar_on_drag_cancel( } fn update_scrollbar_thumb( - q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>, - q_scrollbar: Query<(&Scrollbar, &ComputedNode, &Children)>, - mut q_thumb: Query<&mut Node, With>, + q_scroll_area: Query<(&ScrollPosition, &ComputedNode), Without>, + q_scrollbar: Query< + (&Scrollbar, &ComputedNode, &UiGlobalTransform, &Children), + Without, + >, + mut q_thumb: Query< + ( + &CoreScrollbarThumb, + &UiTransform, + &ComputedUiRenderTargetInfo, + &mut ComputedNode, + &mut UiGlobalTransform, + ), + With, + >, ) { - for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() { + for (scrollbar, scrollbar_node, scrollbar_transform, children) in q_scrollbar.iter() { let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else { continue; }; @@ -297,57 +329,77 @@ fn update_scrollbar_thumb( } for child in children { - if let Ok(mut thumb) = q_thumb.get_mut(*child) { - match scrollbar.orientation { - ControlOrientation::Horizontal => { - let (thumb_size, thumb_pos) = size_and_pos( - content_size.x, - visible_size.x, - track_length.x, - scrollbar.min_thumb_length, - scroll_area.0.x, - ); - - let top = Val::Px(0.); - let bottom = Val::Px(0.); - let left = Val::Px(thumb_pos); - let width = Val::Px(thumb_size); - if top != thumb.top - || bottom != thumb.bottom - || left != thumb.left - || width != thumb.width - { - thumb.top = top; - thumb.bottom = bottom; - thumb.left = left; - thumb.width = width; - } - } - ControlOrientation::Vertical => { - let (thumb_size, thumb_pos) = size_and_pos( - content_size.y, - visible_size.y, - track_length.y, - scrollbar.min_thumb_length, - scroll_area.0.y, - ); - - let left = Val::Px(0.); - let right = Val::Px(0.); - let top = Val::Px(thumb_pos); - let height = Val::Px(thumb_size); - if thumb.left != left - || thumb.right != right - || thumb.top != top - || thumb.height != height - { - thumb.left = left; - thumb.right = right; - thumb.top = top; - thumb.height = height; - } - } - }; + let Ok(( + thumb, + thumb_transform, + target_info, + mut thumb_node, + mut thumb_global_transform, + )) = q_thumb.get_mut(*child) + else { + continue; + }; + + let (thumb_logical_size, thumb_center) = match scrollbar.orientation { + ControlOrientation::Horizontal => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.x, + visible_size.x, + track_length.x, + scrollbar.min_thumb_length, + scroll_area.0.x, + ); + ( + Vec2::new(thumb_size, track_length.y), + Vec2::new(thumb_pos + 0.5 * (thumb_size - track_length.x), 0.), + ) + } + ControlOrientation::Vertical => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.y, + visible_size.y, + track_length.y, + scrollbar.min_thumb_length, + scroll_area.0.y, + ); + ( + Vec2::new(track_length.x, thumb_size), + Vec2::new(0., thumb_pos + 0.5 * (thumb_size - track_length.y)), + ) + } + }; + + let inverse_scale_factor = target_info.scale_factor().recip(); + let thumb_physical_size = thumb_logical_size * target_info.scale_factor(); + + if thumb_node.size != thumb_physical_size + || thumb_node.unrounded_size != thumb_physical_size + || thumb_node.inverse_scale_factor != inverse_scale_factor + { + thumb_node.size = thumb_physical_size; + thumb_node.unrounded_size = thumb_physical_size; + thumb_node.inverse_scale_factor = inverse_scale_factor; + } + + let border_radius = thumb.border_radius.resolve( + target_info.scale_factor(), + thumb_physical_size, + target_info.physical_size().as_vec2(), + ); + if thumb_node.border_radius != border_radius { + thumb_node.border_radius = border_radius; + } + + let new_transform = scrollbar_transform.affine() + * thumb_transform.compute_affine( + target_info.scale_factor(), + thumb_physical_size, + target_info.physical_size().as_vec2(), + ) + * Affine2::from_translation(thumb_center * target_info.scale_factor()); + + if thumb_global_transform.affine() != new_transform { + *thumb_global_transform = new_transform.into(); } } } @@ -363,6 +415,9 @@ impl Plugin for ScrollbarPlugin { .add_observer(scrollbar_on_drag_end) .add_observer(scrollbar_on_drag_cancel) .add_observer(scrollbar_on_drag) - .add_systems(PostUpdate, update_scrollbar_thumb); + .add_systems( + PostUpdate, + update_scrollbar_thumb.in_set(UiSystems::PostLayout), + ); } } diff --git a/examples/ui/scroll_and_overflow/scrollbars.rs b/examples/ui/scroll_and_overflow/scrollbars.rs index a3b1a754c32a3..694425c2f327b 100644 --- a/examples/ui/scroll_and_overflow/scrollbars.rs +++ b/examples/ui/scroll_and_overflow/scrollbars.rs @@ -118,14 +118,11 @@ fn scroll_area_demo() -> impl Bundle { 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, + CoreScrollbarThumb { + border_radius: BorderRadius::all(px(4)), + }, ))), )); @@ -150,7 +147,9 @@ fn scroll_area_demo() -> impl Bundle { }, Hovered::default(), BackgroundColor(colors::GRAY2.into()), - CoreScrollbarThumb, + CoreScrollbarThumb { + border_radius: BorderRadius::all(px(4)), + }, ))), )); }),)), From 02858f33dafd13265c36c0fa329dff4526dc6edf Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 31 Mar 2026 23:39:46 +0100 Subject: [PATCH 02/15] fixed doc link --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 4c4c3cc778035..72a244e689ebb 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -86,7 +86,7 @@ pub struct Scrollbar { #[derive(Reflect)] #[reflect(Component)] pub struct CoreScrollbarThumb { - /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`update_scrollbar_thumb`]. + /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::PostLayout`]. pub border_radius: BorderRadius, } From 6a52804d92fa9e7b7b65716356f5c1c4c1cd9664 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:25:53 +0100 Subject: [PATCH 03/15] Removed `ContentSize` and `ScrollPosition` requirements from `CoreScrollThumb`. Added a `border: UiRect` field, which is used to set the thumb's `ComputedNode::border` in `update_scrollbar_thumb`. --- crates/bevy_ui_widgets/src/scrollbar.rs | 57 ++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 72a244e689ebb..aac3d082bfb68 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -1,6 +1,7 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_camera::visibility::Visibility; use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, entity::Entity, hierarchy::{ChildOf, Children}, @@ -14,9 +15,9 @@ use bevy_math::{Affine2, Vec2}; use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{ - BackgroundColor, BorderColor, BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, - ComputedUiTargetCamera, ContentSize, FocusPolicy, ScrollPosition, UiGlobalTransform, UiScale, - UiSystems, UiTransform, ZIndex, + prelude::BorderRect, BackgroundColor, BorderColor, BorderRadius, ComputedNode, + ComputedUiRenderTargetInfo, ComputedUiTargetCamera, FocusPolicy, ScrollPosition, + UiGlobalTransform, UiRect, UiScale, UiSystems, UiTransform, Val, ZIndex, }; /// Used to select the orientation of a scrollbar, slider, or other oriented control. @@ -66,20 +67,22 @@ pub struct Scrollbar { pub min_thumb_length: f32, } -/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of +/// This component indicates that the entity is a scrollbar thumb (the moving, draggable part of /// the scrollbar). This should be a child of the scrollbar entity. +/// +/// A `CoreScrollbarThumb` UI node does not have a `Node` component. It only has `Border` and `BorderRadius` stlyling properties. +/// Its layout is handled after `ui_layout_system` in `update_scroll_thumb` so that it's size and position can be set relative to the scrolling area's +/// size and scroll position. #[derive(Component, Debug, Default)] #[require( CoreScrollbarDragState, ComputedNode, - ContentSize, ComputedUiTargetCamera, ComputedUiRenderTargetInfo, UiTransform, BackgroundColor, BorderColor, - FocusPolicy, - ScrollPosition, + FocusPolicy::Block, Visibility, ZIndex )] @@ -88,6 +91,10 @@ pub struct Scrollbar { pub struct CoreScrollbarThumb { /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::PostLayout`]. pub border_radius: BorderRadius, + /// Thickness of the thumb node's border. + /// + /// If the border is too large to fit inside the thumb node, the width of the border will be scaled to fit. + pub border: UiRect, } impl Scrollbar { @@ -390,6 +397,42 @@ fn update_scrollbar_thumb( thumb_node.border_radius = border_radius; } + let resolve_border_val = |val: Val| { + val.resolve( + target_info.scale_factor(), + thumb_physical_size.x, + target_info.physical_size().as_vec2(), + ) + .unwrap_or(0.) + }; + + let mut resolved_border = BorderRect { + min_inset: Vec2::new( + resolve_border_val(thumb.border.left), + resolve_border_val(thumb.border.top), + ), + max_inset: Vec2::new( + resolve_border_val(thumb.border.right), + resolve_border_val(thumb.border.bottom), + ), + }; + + if thumb_node.size.x < resolved_border.min_inset.x + resolved_border.max_inset.x { + let r = + thumb_node.size.x / (resolved_border.min_inset.x + resolved_border.max_inset.x); + resolved_border.min_inset.x *= r; + resolved_border.max_inset.y *= r; + } + + if thumb_node.size.y < resolved_border.min_inset.y + resolved_border.max_inset.y { + let r = + thumb_node.size.y / (resolved_border.min_inset.y + resolved_border.max_inset.y); + resolved_border.min_inset.y *= r; + resolved_border.max_inset.y *= r; + } + + thumb_node.bypass_change_detection().border = resolved_border; + let new_transform = scrollbar_transform.affine() * thumb_transform.compute_affine( target_info.scale_factor(), From 99b290099ed0d79515b5bc7f460fc89b754ce2a2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:43:31 +0100 Subject: [PATCH 04/15] Updated `scrollbars` example to give the thumb a border. --- examples/ui/scroll_and_overflow/scrollbars.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/ui/scroll_and_overflow/scrollbars.rs b/examples/ui/scroll_and_overflow/scrollbars.rs index 694425c2f327b..500f64097c8a4 100644 --- a/examples/ui/scroll_and_overflow/scrollbars.rs +++ b/examples/ui/scroll_and_overflow/scrollbars.rs @@ -21,7 +21,7 @@ fn main() { InputDispatchPlugin, TabNavigationPlugin, )) - .insert_resource(UiScale(1.25)) + .insert_resource(UiScale(2.)) .add_systems(Startup, setup_view_root) .add_systems(Update, update_scrollbar_thumb) .run(); @@ -120,8 +120,15 @@ fn scroll_area_demo() -> impl Bundle { Children::spawn(Spawn(( Hovered::default(), BackgroundColor(colors::GRAY2.into()), + BorderColor::all(colors::GRAY3), CoreScrollbarThumb { border_radius: BorderRadius::all(px(4)), + border: UiRect { + left: px(3.), + right: px(1.), + top: px(3.), + bottom: px(1.), + }, }, ))), )); @@ -140,15 +147,17 @@ fn scroll_area_demo() -> impl Bundle { 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()), + BorderColor::all(colors::GRAY3), CoreScrollbarThumb { border_radius: BorderRadius::all(px(4)), + border: UiRect { + left: px(3.), + right: px(1.), + top: px(3.), + bottom: px(1.), + }, }, ))), )); @@ -180,7 +189,7 @@ fn update_scrollbar_thumb( for (mut thumb_bg, Hovered(is_hovering), drag) in q_thumb.iter_mut() { let color: Color = if *is_hovering || drag.dragging { // If hovering, use a lighter color - colors::GRAY3 + colors::GRAY4 } else { // Default color for the slider colors::GRAY2 @@ -199,5 +208,6 @@ mod colors { pub const GRAY1: Srgba = Srgba::new(0.224, 0.224, 0.243, 1.0); pub const GRAY2: Srgba = Srgba::new(0.486, 0.486, 0.529, 1.0); - pub const GRAY3: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0); + pub const GRAY3: Srgba = Srgba::new(0.71, 0.71, 0.772, 1.0); + pub const GRAY4: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0); } From 06d9deed758271936bb648dafa3845cbd0aec4a6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:44:17 +0100 Subject: [PATCH 05/15] Made ` extract_uinode_borders` `Node` query optional --- crates/bevy_ui_render/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 5a8ea10e2d31e..012f206629c8e 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -567,7 +567,7 @@ pub fn extract_uinode_borders( uinode_query: Extract< Query<( Entity, - &Node, + Option<&Node>, &ComputedNode, &UiGlobalTransform, &InheritedVisibility, @@ -593,7 +593,7 @@ pub fn extract_uinode_borders( ) in &uinode_query { // Skip invisible borders and removed nodes - if !inherited_visibility.get() || node.display == Display::None { + if !inherited_visibility.get() || node.is_some_and(|node| node.display == Display::None) { continue; } From 44433cdc5c3eb47aa1022e68f7108e8eff8f1026 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:44:27 +0100 Subject: [PATCH 06/15] adjust example --- examples/ui/scroll_and_overflow/scrollbars.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/examples/ui/scroll_and_overflow/scrollbars.rs b/examples/ui/scroll_and_overflow/scrollbars.rs index 500f64097c8a4..a57abf599c2ea 100644 --- a/examples/ui/scroll_and_overflow/scrollbars.rs +++ b/examples/ui/scroll_and_overflow/scrollbars.rs @@ -123,12 +123,7 @@ fn scroll_area_demo() -> impl Bundle { BorderColor::all(colors::GRAY3), CoreScrollbarThumb { border_radius: BorderRadius::all(px(4)), - border: UiRect { - left: px(3.), - right: px(1.), - top: px(3.), - bottom: px(1.), - }, + border: px(1).all(), }, ))), )); @@ -152,12 +147,7 @@ fn scroll_area_demo() -> impl Bundle { BorderColor::all(colors::GRAY3), CoreScrollbarThumb { border_radius: BorderRadius::all(px(4)), - border: UiRect { - left: px(3.), - right: px(1.), - top: px(3.), - bottom: px(1.), - }, + border: px(1).all(), }, ))), )); From cc3a05c2a52cfbbfdf9403c5fc2f8abaa7afdc0d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:52:16 +0100 Subject: [PATCH 07/15] Added migration guide --- .../CoreScrollThumb-is-now-ScrollThumb.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 _release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md diff --git a/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md b/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md new file mode 100644 index 0000000000000..ced9b1ece0f3a --- /dev/null +++ b/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md @@ -0,0 +1,10 @@ +--- +title: "CoreScrollbarThumb is now `ScrollbarThumb` and scroll bar thumb layout is updated separately" +pull_requests: [23612] +--- + +`CoreScrollbarThumb` has been renamed to `ScrollbarThumb`. + +`ScrollbarThumb` nodes are now laid out after `ui_layout_system` by `update_scrollbar_thumb`. `ScrollbarThumb` entities do +not have a `Node` component. The only layout options are for borders, which can be set using `ScrollbarThumb`'s new +`border` and `border_radius` fields. \ No newline at end of file From f02dab7203b64d9d64197a58395e77d56246e85e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 10:53:30 +0100 Subject: [PATCH 08/15] Renamed `CoreScrollbarThumb` to `ScrollbarThumb` --- crates/bevy_ui_widgets/src/scrollbar.rs | 20 +++++++++---------- examples/ui/scroll_and_overflow/scrollbars.rs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index aac3d082bfb68..080789fbf5072 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -88,7 +88,7 @@ pub struct Scrollbar { )] #[derive(Reflect)] #[reflect(Component)] -pub struct CoreScrollbarThumb { +pub struct ScrollbarThumb { /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::PostLayout`]. pub border_radius: BorderRadius, /// Thickness of the thumb node's border. @@ -127,7 +127,7 @@ pub struct CoreScrollbarDragState { fn scrollbar_on_pointer_down( mut ev: On>, - q_thumb: Query<&ChildOf, With>, + q_thumb: Query<&ChildOf, With>, mut q_scrollbar: Query<( &Scrollbar, &ComputedNode, @@ -186,7 +186,7 @@ fn scrollbar_on_pointer_down( fn scrollbar_on_drag_start( mut ev: On>, - mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, q_scrollbar: Query<&Scrollbar>, q_scroll_area: Query<&ScrollPosition>, ) { @@ -206,7 +206,7 @@ fn scrollbar_on_drag_start( fn scrollbar_on_drag( mut ev: On>, - mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, mut q_scrollbar: Query<(&ComputedNode, &Scrollbar)>, mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, ui_scale: Res, @@ -248,7 +248,7 @@ fn scrollbar_on_drag( fn scrollbar_on_drag_end( mut ev: On>, - mut q_thumb: Query<&mut CoreScrollbarDragState, With>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { if let Ok(mut drag) = q_thumb.get_mut(ev.entity) { ev.propagate(false); @@ -260,7 +260,7 @@ fn scrollbar_on_drag_end( fn scrollbar_on_drag_cancel( mut ev: On>, - mut q_thumb: Query<&mut CoreScrollbarDragState, With>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { if let Ok(mut drag) = q_thumb.get_mut(ev.entity) { ev.propagate(false); @@ -271,20 +271,20 @@ fn scrollbar_on_drag_cancel( } fn update_scrollbar_thumb( - q_scroll_area: Query<(&ScrollPosition, &ComputedNode), Without>, + q_scroll_area: Query<(&ScrollPosition, &ComputedNode), Without>, q_scrollbar: Query< (&Scrollbar, &ComputedNode, &UiGlobalTransform, &Children), - Without, + Without, >, mut q_thumb: Query< ( - &CoreScrollbarThumb, + &ScrollbarThumb, &UiTransform, &ComputedUiRenderTargetInfo, &mut ComputedNode, &mut UiGlobalTransform, ), - With, + With, >, ) { for (scrollbar, scrollbar_node, scrollbar_transform, children) in q_scrollbar.iter() { diff --git a/examples/ui/scroll_and_overflow/scrollbars.rs b/examples/ui/scroll_and_overflow/scrollbars.rs index a57abf599c2ea..b1e68569357dd 100644 --- a/examples/ui/scroll_and_overflow/scrollbars.rs +++ b/examples/ui/scroll_and_overflow/scrollbars.rs @@ -9,7 +9,7 @@ use bevy::{ picking::hover::Hovered, prelude::*, ui_widgets::{ - ControlOrientation, CoreScrollbarDragState, CoreScrollbarThumb, Scrollbar, ScrollbarPlugin, + ControlOrientation, CoreScrollbarDragState, Scrollbar, ScrollbarPlugin, ScrollbarThumb, }, }; @@ -121,7 +121,7 @@ fn scroll_area_demo() -> impl Bundle { Hovered::default(), BackgroundColor(colors::GRAY2.into()), BorderColor::all(colors::GRAY3), - CoreScrollbarThumb { + ScrollbarThumb { border_radius: BorderRadius::all(px(4)), border: px(1).all(), }, @@ -145,7 +145,7 @@ fn scroll_area_demo() -> impl Bundle { Hovered::default(), BackgroundColor(colors::GRAY2.into()), BorderColor::all(colors::GRAY3), - CoreScrollbarThumb { + ScrollbarThumb { border_radius: BorderRadius::all(px(4)), border: px(1).all(), }, @@ -171,7 +171,7 @@ fn update_scrollbar_thumb( mut q_thumb: Query< (&mut BackgroundColor, &Hovered, &CoreScrollbarDragState), ( - With, + With, Or<(Changed, Changed)>, ), >, From 3eaea7c384c07b7db4d657859ad2a58813a19be6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 11:00:20 +0100 Subject: [PATCH 09/15] Moved `update_scrollbar_thumb` to `UiSystems::Layout` after `ui_layout_system`. --- crates/bevy_ui_widgets/src/scrollbar.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 080789fbf5072..20a793b2bed86 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -15,8 +15,8 @@ use bevy_math::{Affine2, Vec2}; use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{ - prelude::BorderRect, BackgroundColor, BorderColor, BorderRadius, ComputedNode, - ComputedUiRenderTargetInfo, ComputedUiTargetCamera, FocusPolicy, ScrollPosition, + prelude::BorderRect, ui_layout_system, BackgroundColor, BorderColor, BorderRadius, + ComputedNode, ComputedUiRenderTargetInfo, ComputedUiTargetCamera, FocusPolicy, ScrollPosition, UiGlobalTransform, UiRect, UiScale, UiSystems, UiTransform, Val, ZIndex, }; @@ -460,7 +460,9 @@ impl Plugin for ScrollbarPlugin { .add_observer(scrollbar_on_drag) .add_systems( PostUpdate, - update_scrollbar_thumb.in_set(UiSystems::PostLayout), + update_scrollbar_thumb + .in_set(UiSystems::Layout) + .after(ui_layout_system), ); } } From e73ae92dfe5e2df4ac4fc4ee06c7c8113428a493 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 16:11:32 +0100 Subject: [PATCH 10/15] Update crates/bevy_ui_widgets/src/scrollbar.rs Co-authored-by: Kevin Chen --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 20a793b2bed86..e5903c69cde8c 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -421,7 +421,7 @@ fn update_scrollbar_thumb( let r = thumb_node.size.x / (resolved_border.min_inset.x + resolved_border.max_inset.x); resolved_border.min_inset.x *= r; - resolved_border.max_inset.y *= r; + resolved_border.max_inset.x *= r; } if thumb_node.size.y < resolved_border.min_inset.y + resolved_border.max_inset.y { From f8f09f90a7c3a7eeca44d434b9e2afebc71f696b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 16:11:51 +0100 Subject: [PATCH 11/15] Update crates/bevy_ui_widgets/src/scrollbar.rs Co-authored-by: Kevin Chen --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index e5903c69cde8c..ef1574c2b22b0 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -70,7 +70,7 @@ pub struct Scrollbar { /// This component indicates that the entity is a scrollbar thumb (the moving, draggable part of /// the scrollbar). This should be a child of the scrollbar entity. /// -/// A `CoreScrollbarThumb` UI node does not have a `Node` component. It only has `Border` and `BorderRadius` stlyling properties. +/// A `ScrollbarThumb` UI node does not have a `Node` component. It only has `Border` and `BorderRadius` styling properties. /// Its layout is handled after `ui_layout_system` in `update_scroll_thumb` so that it's size and position can be set relative to the scrolling area's /// size and scroll position. #[derive(Component, Debug, Default)] From 8229a6c6bc540846e34e85e00ee7cbf485536aa7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 16:12:07 +0100 Subject: [PATCH 12/15] Update crates/bevy_ui_widgets/src/scrollbar.rs Co-authored-by: Kevin Chen --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index ef1574c2b22b0..d441d8ed008ac 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -71,7 +71,7 @@ pub struct Scrollbar { /// the scrollbar). This should be a child of the scrollbar entity. /// /// A `ScrollbarThumb` UI node does not have a `Node` component. It only has `Border` and `BorderRadius` styling properties. -/// Its layout is handled after `ui_layout_system` in `update_scroll_thumb` so that it's size and position can be set relative to the scrolling area's +/// Its layout is handled after `ui_layout_system` in `update_scroll_thumb` so that its size and position can be set relative to the scrolling area's /// size and scroll position. #[derive(Component, Debug, Default)] #[require( From 38cef9114024b09a678f7f74e8ee7207effd125c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 2 Apr 2026 16:12:26 +0100 Subject: [PATCH 13/15] Update crates/bevy_ui_widgets/src/scrollbar.rs Co-authored-by: Kevin Chen --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index d441d8ed008ac..ccff961edee5e 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -89,7 +89,7 @@ pub struct Scrollbar { #[derive(Reflect)] #[reflect(Component)] pub struct ScrollbarThumb { - /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::PostLayout`]. + /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::Layout`]. pub border_radius: BorderRadius, /// Thickness of the thumb node's border. /// From 207b87f40d01a08c9923428202f541ea6b103ec8 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 2 Apr 2026 08:29:19 -0700 Subject: [PATCH 14/15] Newline at end of file --- .../migration-guides/CoreScrollThumb-is-now-ScrollThumb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md b/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md index ced9b1ece0f3a..5af290a1f1545 100644 --- a/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md +++ b/_release-content/migration-guides/CoreScrollThumb-is-now-ScrollThumb.md @@ -7,4 +7,4 @@ pull_requests: [23612] `ScrollbarThumb` nodes are now laid out after `ui_layout_system` by `update_scrollbar_thumb`. `ScrollbarThumb` entities do not have a `Node` component. The only layout options are for borders, which can be set using `ScrollbarThumb`'s new -`border` and `border_radius` fields. \ No newline at end of file +`border` and `border_radius` fields. From cd7c85706fe913e30e0cbbe1f775103c5519eaa9 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 2 Apr 2026 08:29:57 -0700 Subject: [PATCH 15/15] Missed doc link rename --- crates/bevy_ui_widgets/src/scrollbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index ccff961edee5e..688d09c047393 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -46,7 +46,7 @@ pub enum ControlOrientation { /// entity directly. /// /// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb, -/// which is marked with the [`CoreScrollbarThumb`] component. Other children are ignored. The core +/// which is marked with the [`ScrollbarThumb`] component. Other children are ignored. The core /// scrollbar will directly update the position and size of this entity; the application is free to /// set any other style properties as desired. ///