11use bevy_app:: { App , Plugin , PostUpdate } ;
2+ use bevy_camera:: visibility:: Visibility ;
23use bevy_ecs:: {
4+ change_detection:: DetectChangesMut ,
35 component:: Component ,
46 entity:: Entity ,
57 hierarchy:: { ChildOf , Children } ,
68 observer:: On ,
79 query:: { With , Without } ,
810 reflect:: ReflectComponent ,
11+ schedule:: IntoScheduleConfigs ,
912 system:: { Query , Res } ,
1013} ;
11- use bevy_math:: Vec2 ;
14+ use bevy_math:: { Affine2 , Vec2 } ;
1215use bevy_picking:: events:: { Cancel , Drag , DragEnd , DragStart , Pointer , Press } ;
1316use bevy_reflect:: { prelude:: ReflectDefault , Reflect } ;
1417use bevy_ui:: {
15- ComputedNode , ComputedUiRenderTargetInfo , Node , ScrollPosition , UiGlobalTransform , UiScale , Val ,
18+ prelude:: BorderRect , ui_layout_system, BackgroundColor , BorderColor , BorderRadius ,
19+ ComputedNode , ComputedUiRenderTargetInfo , ComputedUiTargetCamera , FocusPolicy , ScrollPosition ,
20+ UiGlobalTransform , UiRect , UiScale , UiSystems , UiTransform , Val , ZIndex ,
1621} ;
1722
1823/// Used to select the orientation of a scrollbar, slider, or other oriented control.
@@ -41,7 +46,7 @@ pub enum ControlOrientation {
4146/// entity directly.
4247///
4348/// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb,
44- /// which is marked with the [`CoreScrollbarThumb `] component. Other children are ignored. The core
49+ /// which is marked with the [`ScrollbarThumb `] component. Other children are ignored. The core
4550/// scrollbar will directly update the position and size of this entity; the application is free to
4651/// set any other style properties as desired.
4752///
@@ -62,13 +67,35 @@ pub struct Scrollbar {
6267 pub min_thumb_length : f32 ,
6368}
6469
65- /// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of
70+ /// This component indicates that the entity is a scrollbar thumb (the moving, draggable part of
6671/// the scrollbar). This should be a child of the scrollbar entity.
67- #[ derive( Component , Debug ) ]
68- #[ require( CoreScrollbarDragState ) ]
72+ ///
73+ /// A `ScrollbarThumb` UI node does not have a `Node` component. It only has `Border` and `BorderRadius` styling properties.
74+ /// 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
75+ /// size and scroll position.
76+ #[ derive( Component , Debug , Default ) ]
77+ #[ require(
78+ CoreScrollbarDragState ,
79+ ComputedNode ,
80+ ComputedUiTargetCamera ,
81+ ComputedUiRenderTargetInfo ,
82+ UiTransform ,
83+ BackgroundColor ,
84+ BorderColor ,
85+ FocusPolicy :: Block ,
86+ Visibility ,
87+ ZIndex
88+ ) ]
6989#[ derive( Reflect ) ]
7090#[ reflect( Component ) ]
71- pub struct CoreScrollbarThumb ;
91+ pub struct ScrollbarThumb {
92+ /// Border radius of the scrollbar thumb, used to update [`ComputedNode::border_radius`] in [`UiSystems::Layout`].
93+ pub border_radius : BorderRadius ,
94+ /// Thickness of the thumb node's border.
95+ ///
96+ /// If the border is too large to fit inside the thumb node, the width of the border will be scaled to fit.
97+ pub border : UiRect ,
98+ }
7299
73100impl Scrollbar {
74101 /// Construct a new scrollbar.
@@ -100,7 +127,7 @@ pub struct CoreScrollbarDragState {
100127
101128fn scrollbar_on_pointer_down (
102129 mut ev : On < Pointer < Press > > ,
103- q_thumb : Query < & ChildOf , With < CoreScrollbarThumb > > ,
130+ q_thumb : Query < & ChildOf , With < ScrollbarThumb > > ,
104131 mut q_scrollbar : Query < (
105132 & Scrollbar ,
106133 & ComputedNode ,
@@ -159,7 +186,7 @@ fn scrollbar_on_pointer_down(
159186
160187fn scrollbar_on_drag_start (
161188 mut ev : On < Pointer < DragStart > > ,
162- mut q_thumb : Query < ( & ChildOf , & mut CoreScrollbarDragState ) , With < CoreScrollbarThumb > > ,
189+ mut q_thumb : Query < ( & ChildOf , & mut CoreScrollbarDragState ) , With < ScrollbarThumb > > ,
163190 q_scrollbar : Query < & Scrollbar > ,
164191 q_scroll_area : Query < & ScrollPosition > ,
165192) {
@@ -179,7 +206,7 @@ fn scrollbar_on_drag_start(
179206
180207fn scrollbar_on_drag (
181208 mut ev : On < Pointer < Drag > > ,
182- mut q_thumb : Query < ( & ChildOf , & mut CoreScrollbarDragState ) , With < CoreScrollbarThumb > > ,
209+ mut q_thumb : Query < ( & ChildOf , & mut CoreScrollbarDragState ) , With < ScrollbarThumb > > ,
183210 mut q_scrollbar : Query < ( & ComputedNode , & Scrollbar ) > ,
184211 mut q_scroll_pos : Query < ( & mut ScrollPosition , & ComputedNode ) , Without < Scrollbar > > ,
185212 ui_scale : Res < UiScale > ,
@@ -221,7 +248,7 @@ fn scrollbar_on_drag(
221248
222249fn scrollbar_on_drag_end (
223250 mut ev : On < Pointer < DragEnd > > ,
224- mut q_thumb : Query < & mut CoreScrollbarDragState , With < CoreScrollbarThumb > > ,
251+ mut q_thumb : Query < & mut CoreScrollbarDragState , With < ScrollbarThumb > > ,
225252) {
226253 if let Ok ( mut drag) = q_thumb. get_mut ( ev. entity ) {
227254 ev. propagate ( false ) ;
@@ -233,7 +260,7 @@ fn scrollbar_on_drag_end(
233260
234261fn scrollbar_on_drag_cancel (
235262 mut ev : On < Pointer < Cancel > > ,
236- mut q_thumb : Query < & mut CoreScrollbarDragState , With < CoreScrollbarThumb > > ,
263+ mut q_thumb : Query < & mut CoreScrollbarDragState , With < ScrollbarThumb > > ,
237264) {
238265 if let Ok ( mut drag) = q_thumb. get_mut ( ev. entity ) {
239266 ev. propagate ( false ) ;
@@ -244,11 +271,23 @@ fn scrollbar_on_drag_cancel(
244271}
245272
246273fn update_scrollbar_thumb (
247- q_scroll_area : Query < ( & ScrollPosition , & ComputedNode ) > ,
248- q_scrollbar : Query < ( & Scrollbar , & ComputedNode , & Children ) > ,
249- mut q_thumb : Query < & mut Node , With < CoreScrollbarThumb > > ,
274+ q_scroll_area : Query < ( & ScrollPosition , & ComputedNode ) , Without < ScrollbarThumb > > ,
275+ q_scrollbar : Query <
276+ ( & Scrollbar , & ComputedNode , & UiGlobalTransform , & Children ) ,
277+ Without < ScrollbarThumb > ,
278+ > ,
279+ mut q_thumb : Query <
280+ (
281+ & ScrollbarThumb ,
282+ & UiTransform ,
283+ & ComputedUiRenderTargetInfo ,
284+ & mut ComputedNode ,
285+ & mut UiGlobalTransform ,
286+ ) ,
287+ With < ScrollbarThumb > ,
288+ > ,
250289) {
251- for ( scrollbar, scrollbar_node, children) in q_scrollbar. iter ( ) {
290+ for ( scrollbar, scrollbar_node, scrollbar_transform , children) in q_scrollbar. iter ( ) {
252291 let Ok ( scroll_area) = q_scroll_area. get ( scrollbar. target ) else {
253292 continue ;
254293 } ;
@@ -297,57 +336,113 @@ fn update_scrollbar_thumb(
297336 }
298337
299338 for child in children {
300- if let Ok ( mut thumb) = q_thumb. get_mut ( * child) {
301- match scrollbar. orientation {
302- ControlOrientation :: Horizontal => {
303- let ( thumb_size, thumb_pos) = size_and_pos (
304- content_size. x ,
305- visible_size. x ,
306- track_length. x ,
307- scrollbar. min_thumb_length ,
308- scroll_area. 0 . x ,
309- ) ;
310-
311- let top = Val :: Px ( 0. ) ;
312- let bottom = Val :: Px ( 0. ) ;
313- let left = Val :: Px ( thumb_pos) ;
314- let width = Val :: Px ( thumb_size) ;
315- if top != thumb. top
316- || bottom != thumb. bottom
317- || left != thumb. left
318- || width != thumb. width
319- {
320- thumb. top = top;
321- thumb. bottom = bottom;
322- thumb. left = left;
323- thumb. width = width;
324- }
325- }
326- ControlOrientation :: Vertical => {
327- let ( thumb_size, thumb_pos) = size_and_pos (
328- content_size. y ,
329- visible_size. y ,
330- track_length. y ,
331- scrollbar. min_thumb_length ,
332- scroll_area. 0 . y ,
333- ) ;
334-
335- let left = Val :: Px ( 0. ) ;
336- let right = Val :: Px ( 0. ) ;
337- let top = Val :: Px ( thumb_pos) ;
338- let height = Val :: Px ( thumb_size) ;
339- if thumb. left != left
340- || thumb. right != right
341- || thumb. top != top
342- || thumb. height != height
343- {
344- thumb. left = left;
345- thumb. right = right;
346- thumb. top = top;
347- thumb. height = height;
348- }
349- }
350- } ;
339+ let Ok ( (
340+ thumb,
341+ thumb_transform,
342+ target_info,
343+ mut thumb_node,
344+ mut thumb_global_transform,
345+ ) ) = q_thumb. get_mut ( * child)
346+ else {
347+ continue ;
348+ } ;
349+
350+ let ( thumb_logical_size, thumb_center) = match scrollbar. orientation {
351+ ControlOrientation :: Horizontal => {
352+ let ( thumb_size, thumb_pos) = size_and_pos (
353+ content_size. x ,
354+ visible_size. x ,
355+ track_length. x ,
356+ scrollbar. min_thumb_length ,
357+ scroll_area. 0 . x ,
358+ ) ;
359+ (
360+ Vec2 :: new ( thumb_size, track_length. y ) ,
361+ Vec2 :: new ( thumb_pos + 0.5 * ( thumb_size - track_length. x ) , 0. ) ,
362+ )
363+ }
364+ ControlOrientation :: Vertical => {
365+ let ( thumb_size, thumb_pos) = size_and_pos (
366+ content_size. y ,
367+ visible_size. y ,
368+ track_length. y ,
369+ scrollbar. min_thumb_length ,
370+ scroll_area. 0 . y ,
371+ ) ;
372+ (
373+ Vec2 :: new ( track_length. x , thumb_size) ,
374+ Vec2 :: new ( 0. , thumb_pos + 0.5 * ( thumb_size - track_length. y ) ) ,
375+ )
376+ }
377+ } ;
378+
379+ let inverse_scale_factor = target_info. scale_factor ( ) . recip ( ) ;
380+ let thumb_physical_size = thumb_logical_size * target_info. scale_factor ( ) ;
381+
382+ if thumb_node. size != thumb_physical_size
383+ || thumb_node. unrounded_size != thumb_physical_size
384+ || thumb_node. inverse_scale_factor != inverse_scale_factor
385+ {
386+ thumb_node. size = thumb_physical_size;
387+ thumb_node. unrounded_size = thumb_physical_size;
388+ thumb_node. inverse_scale_factor = inverse_scale_factor;
389+ }
390+
391+ let border_radius = thumb. border_radius . resolve (
392+ target_info. scale_factor ( ) ,
393+ thumb_physical_size,
394+ target_info. physical_size ( ) . as_vec2 ( ) ,
395+ ) ;
396+ if thumb_node. border_radius != border_radius {
397+ thumb_node. border_radius = border_radius;
398+ }
399+
400+ let resolve_border_val = |val : Val | {
401+ val. resolve (
402+ target_info. scale_factor ( ) ,
403+ thumb_physical_size. x ,
404+ target_info. physical_size ( ) . as_vec2 ( ) ,
405+ )
406+ . unwrap_or ( 0. )
407+ } ;
408+
409+ let mut resolved_border = BorderRect {
410+ min_inset : Vec2 :: new (
411+ resolve_border_val ( thumb. border . left ) ,
412+ resolve_border_val ( thumb. border . top ) ,
413+ ) ,
414+ max_inset : Vec2 :: new (
415+ resolve_border_val ( thumb. border . right ) ,
416+ resolve_border_val ( thumb. border . bottom ) ,
417+ ) ,
418+ } ;
419+
420+ if thumb_node. size . x < resolved_border. min_inset . x + resolved_border. max_inset . x {
421+ let r =
422+ thumb_node. size . x / ( resolved_border. min_inset . x + resolved_border. max_inset . x ) ;
423+ resolved_border. min_inset . x *= r;
424+ resolved_border. max_inset . x *= r;
425+ }
426+
427+ if thumb_node. size . y < resolved_border. min_inset . y + resolved_border. max_inset . y {
428+ let r =
429+ thumb_node. size . y / ( resolved_border. min_inset . y + resolved_border. max_inset . y ) ;
430+ resolved_border. min_inset . y *= r;
431+ resolved_border. max_inset . y *= r;
432+ }
433+
434+ thumb_node. bypass_change_detection ( ) . border = resolved_border;
435+
436+ let new_transform = scrollbar_transform. affine ( )
437+ * thumb_transform. compute_affine (
438+ target_info. scale_factor ( ) ,
439+ thumb_physical_size,
440+ target_info. physical_size ( ) . as_vec2 ( ) ,
441+ )
442+ * Affine2 :: from_translation ( thumb_center * target_info. scale_factor ( ) ) ;
443+
444+ if thumb_global_transform. affine ( ) != new_transform {
445+ * thumb_global_transform = new_transform. into ( ) ;
351446 }
352447 }
353448 }
@@ -363,6 +458,11 @@ impl Plugin for ScrollbarPlugin {
363458 . add_observer ( scrollbar_on_drag_end)
364459 . add_observer ( scrollbar_on_drag_cancel)
365460 . add_observer ( scrollbar_on_drag)
366- . add_systems ( PostUpdate , update_scrollbar_thumb) ;
461+ . add_systems (
462+ PostUpdate ,
463+ update_scrollbar_thumb
464+ . in_set ( UiSystems :: Layout )
465+ . after ( ui_layout_system) ,
466+ ) ;
367467 }
368468}
0 commit comments