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..5af290a1f1545 --- /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. 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; } diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 6c3087d7576b5..688d09c047393 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -1,18 +1,23 @@ use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::visibility::Visibility; use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, entity::Entity, hierarchy::{ChildOf, Children}, 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, + prelude::BorderRect, ui_layout_system, 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. @@ -41,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. /// @@ -62,13 +67,35 @@ 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. -#[derive(Component, Debug)] -#[require(CoreScrollbarDragState)] +/// +/// 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 its size and position can be set relative to the scrolling area's +/// size and scroll position. +#[derive(Component, Debug, Default)] +#[require( + CoreScrollbarDragState, + ComputedNode, + ComputedUiTargetCamera, + ComputedUiRenderTargetInfo, + UiTransform, + BackgroundColor, + BorderColor, + FocusPolicy::Block, + Visibility, + ZIndex +)] #[derive(Reflect)] #[reflect(Component)] -pub struct CoreScrollbarThumb; +pub struct ScrollbarThumb { + /// 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. + /// + /// 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 { /// Construct a new scrollbar. @@ -100,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, @@ -159,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>, ) { @@ -179,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, @@ -221,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); @@ -233,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); @@ -244,11 +271,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< + ( + &ScrollbarThumb, + &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 +336,113 @@ 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 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.x *= 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(), + 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 +458,11 @@ 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::Layout) + .after(ui_layout_system), + ); } } diff --git a/examples/ui/scroll_and_overflow/scrollbars.rs b/examples/ui/scroll_and_overflow/scrollbars.rs index a3b1a754c32a3..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, }, }; @@ -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(); @@ -118,14 +118,13 @@ 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, + BorderColor::all(colors::GRAY3), + ScrollbarThumb { + border_radius: BorderRadius::all(px(4)), + border: px(1).all(), + }, ))), )); @@ -143,14 +142,13 @@ 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, + BorderColor::all(colors::GRAY3), + ScrollbarThumb { + border_radius: BorderRadius::all(px(4)), + border: px(1).all(), + }, ))), )); }),)), @@ -173,7 +171,7 @@ fn update_scrollbar_thumb( mut q_thumb: Query< (&mut BackgroundColor, &Hovered, &CoreScrollbarDragState), ( - With, + With, Or<(Changed, Changed)>, ), >, @@ -181,7 +179,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 @@ -200,5 +198,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); }