Skip to content

Commit 33dbf93

Browse files
ickshonpekfc35alice-i-cecile
authored
Update ComputedNode for scrollbar thumbs in update_scrollbar_thumb (#23612)
# Objective Update `ComputedNode` for scrollbar thumbs in `update_scrollbar_thumb` to avoid delays and system ambiguities. ## Solution * Removed `Node` from scrollbar thumb node entities. * Renamed `CoreScrollbarThumb` to `ScrollbarThumb`. * `ScrollbarThumb` now requires all the UI components and gains `border` and `border_radius` fields. * `update_scrollbar_thumb` now updates `ScrollbarThumb` node's `UiGlobalTransform` and `ComputedNode` in `PostLayout`. ## Testing ``` cargo run --example scrollbars ``` We need a better scrollbars example, added an issue #23622. --------- Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent 8c3facc commit 33dbf93

4 files changed

Lines changed: 196 additions & 87 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: "CoreScrollbarThumb is now `ScrollbarThumb` and scroll bar thumb layout is updated separately"
3+
pull_requests: [23612]
4+
---
5+
6+
`CoreScrollbarThumb` has been renamed to `ScrollbarThumb`.
7+
8+
`ScrollbarThumb` nodes are now laid out after `ui_layout_system` by `update_scrollbar_thumb`. `ScrollbarThumb` entities do
9+
not have a `Node` component. The only layout options are for borders, which can be set using `ScrollbarThumb`'s new
10+
`border` and `border_radius` fields.

crates/bevy_ui_render/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ pub fn extract_uinode_borders(
567567
uinode_query: Extract<
568568
Query<(
569569
Entity,
570-
&Node,
570+
Option<&Node>,
571571
&ComputedNode,
572572
&UiGlobalTransform,
573573
&InheritedVisibility,
@@ -593,7 +593,7 @@ pub fn extract_uinode_borders(
593593
) in &uinode_query
594594
{
595595
// Skip invisible borders and removed nodes
596-
if !inherited_visibility.get() || node.display == Display::None {
596+
if !inherited_visibility.get() || node.is_some_and(|node| node.display == Display::None) {
597597
continue;
598598
}
599599

crates/bevy_ui_widgets/src/scrollbar.rs

Lines changed: 168 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
use bevy_app::{App, Plugin, PostUpdate};
2+
use bevy_camera::visibility::Visibility;
23
use 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};
1215
use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press};
1316
use bevy_reflect::{prelude::ReflectDefault, Reflect};
1417
use 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

73100
impl Scrollbar {
74101
/// Construct a new scrollbar.
@@ -100,7 +127,7 @@ pub struct CoreScrollbarDragState {
100127

101128
fn 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

160187
fn 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

180207
fn 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

222249
fn 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

234261
fn 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

246273
fn 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

Comments
 (0)