-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Update ComputedNode for scrollbar thumbs in update_scrollbar_thumb
#23612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c623595
02858f3
bb56003
6a52804
99b2900
06d9dee
44433cd
cc3a05c
f0f4e92
f02dab7
3eaea7c
e73ae92
f8f09f9
8229a6c
38cef91
207b87f
cd7c857
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Pointer<Press>>, | ||
| q_thumb: Query<&ChildOf, With<CoreScrollbarThumb>>, | ||
| q_thumb: Query<&ChildOf, With<ScrollbarThumb>>, | ||
| mut q_scrollbar: Query<( | ||
| &Scrollbar, | ||
| &ComputedNode, | ||
|
|
@@ -159,7 +186,7 @@ fn scrollbar_on_pointer_down( | |
|
|
||
| fn scrollbar_on_drag_start( | ||
| mut ev: On<Pointer<DragStart>>, | ||
| mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>, | ||
| mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<ScrollbarThumb>>, | ||
| 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<Pointer<Drag>>, | ||
| mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>, | ||
| mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<ScrollbarThumb>>, | ||
| mut q_scrollbar: Query<(&ComputedNode, &Scrollbar)>, | ||
| mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>, | ||
| ui_scale: Res<UiScale>, | ||
|
|
@@ -221,7 +248,7 @@ fn scrollbar_on_drag( | |
|
|
||
| fn scrollbar_on_drag_end( | ||
| mut ev: On<Pointer<DragEnd>>, | ||
| mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>, | ||
| mut q_thumb: Query<&mut CoreScrollbarDragState, With<ScrollbarThumb>>, | ||
| ) { | ||
| 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<Pointer<Cancel>>, | ||
| mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>, | ||
| mut q_thumb: Query<&mut CoreScrollbarDragState, With<ScrollbarThumb>>, | ||
| ) { | ||
| 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<CoreScrollbarThumb>>, | ||
| q_scroll_area: Query<(&ScrollPosition, &ComputedNode), Without<ScrollbarThumb>>, | ||
| q_scrollbar: Query< | ||
| (&Scrollbar, &ComputedNode, &UiGlobalTransform, &Children), | ||
| Without<ScrollbarThumb>, | ||
| >, | ||
| mut q_thumb: Query< | ||
| ( | ||
| &ScrollbarThumb, | ||
| &UiTransform, | ||
| &ComputedUiRenderTargetInfo, | ||
| &mut ComputedNode, | ||
| &mut UiGlobalTransform, | ||
| ), | ||
| With<ScrollbarThumb>, | ||
| >, | ||
| ) { | ||
| 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.), | ||
|
ickshonpe marked this conversation as resolved.
|
||
| ) | ||
| } | ||
| 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| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this actually need to be two diff lambdas for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's iffy, but generally with percentage values both axes are resolved based on the horizontal width. |
||
| 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), | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.