Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui_widgets/src/scrollbar.rs
6849 views
1
use bevy_app::{App, Plugin, PostUpdate};
2
use bevy_ecs::{
3
component::Component,
4
entity::Entity,
5
hierarchy::{ChildOf, Children},
6
observer::On,
7
query::{With, Without},
8
reflect::ReflectComponent,
9
system::{Query, Res},
10
};
11
use bevy_math::Vec2;
12
use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press};
13
use bevy_reflect::{prelude::ReflectDefault, Reflect};
14
use bevy_ui::{
15
ComputedNode, ComputedUiRenderTargetInfo, Node, ScrollPosition, UiGlobalTransform, UiScale, Val,
16
};
17
18
/// Used to select the orientation of a scrollbar, slider, or other oriented control.
19
// TODO: Move this to a more central place.
20
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
21
#[reflect(PartialEq, Clone, Default)]
22
pub enum ControlOrientation {
23
/// Horizontal orientation (stretching from left to right)
24
Horizontal,
25
/// Vertical orientation (stretching from top to bottom)
26
#[default]
27
Vertical,
28
}
29
30
/// A headless scrollbar widget, which can be used to build custom scrollbars.
31
///
32
/// Scrollbars operate differently than the other UI widgets in a number of respects.
33
///
34
/// Unlike sliders, scrollbars don't have an [`AccessibilityNode`](bevy_a11y::AccessibilityNode)
35
/// component, nor can they have keyboard focus. This is because scrollbars are usually used in
36
/// conjunction with a scrollable container, which is itself accessible and focusable. This also
37
/// means that scrollbars don't accept keyboard events, which is also the responsibility of the
38
/// scrollable container.
39
///
40
/// Scrollbars don't emit notification events; instead they modify the scroll position of the target
41
/// entity directly.
42
///
43
/// 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
45
/// scrollbar will directly update the position and size of this entity; the application is free to
46
/// set any other style properties as desired.
47
///
48
/// The application is free to position the scrollbars relative to the scrolling container however
49
/// it wants: it can overlay them on top of the scrolling content, or use a grid layout to displace
50
/// the content to make room for the scrollbars.
51
#[derive(Component, Debug, Reflect)]
52
#[reflect(Component)]
53
pub struct Scrollbar {
54
/// Entity being scrolled.
55
pub target: Entity,
56
/// Whether the scrollbar is vertical or horizontal.
57
pub orientation: ControlOrientation,
58
/// Minimum length of the scrollbar thumb, in pixel units, in the direction parallel to the main
59
/// scrollbar axis. The scrollbar will resize the thumb entity based on the proportion of
60
/// visible size to content size, but no smaller than this. This prevents the thumb from
61
/// disappearing in cases where the ratio of content size to visible size is large.
62
pub min_thumb_length: f32,
63
}
64
65
/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of
66
/// the scrollbar). This should be a child of the scrollbar entity.
67
#[derive(Component, Debug)]
68
#[require(CoreScrollbarDragState)]
69
#[derive(Reflect)]
70
#[reflect(Component)]
71
pub struct CoreScrollbarThumb;
72
73
impl Scrollbar {
74
/// Construct a new scrollbar.
75
///
76
/// # Arguments
77
///
78
/// * `target` - The scrollable entity that this scrollbar will control.
79
/// * `orientation` - The orientation of the scrollbar (horizontal or vertical).
80
/// * `min_thumb_length` - The minimum size of the scrollbar's thumb, in pixels.
81
pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self {
82
Self {
83
target,
84
orientation,
85
min_thumb_length,
86
}
87
}
88
}
89
90
/// Component used to manage the state of a scrollbar during dragging. This component is
91
/// inserted on the thumb entity.
92
#[derive(Component, Default, Reflect)]
93
#[reflect(Component, Default)]
94
pub struct CoreScrollbarDragState {
95
/// Whether the scrollbar is currently being dragged.
96
pub dragging: bool,
97
/// The value of the scrollbar when dragging started.
98
drag_origin: f32,
99
}
100
101
fn scrollbar_on_pointer_down(
102
mut ev: On<Pointer<Press>>,
103
q_thumb: Query<&ChildOf, With<CoreScrollbarThumb>>,
104
mut q_scrollbar: Query<(
105
&Scrollbar,
106
&ComputedNode,
107
&ComputedUiRenderTargetInfo,
108
&UiGlobalTransform,
109
)>,
110
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
111
ui_scale: Res<UiScale>,
112
) {
113
if q_thumb.contains(ev.entity) {
114
// If they click on the thumb, do nothing. This will be handled by the drag event.
115
ev.propagate(false);
116
} else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.entity) {
117
// If they click on the scrollbar track, page up or down.
118
ev.propagate(false);
119
120
// Convert to widget-local coordinates.
121
let local_pos = transform.try_inverse().unwrap().transform_point2(
122
ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0,
123
) + node.size() * 0.5;
124
125
// Bail if we don't find the target entity.
126
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
127
return;
128
};
129
130
// Convert the click coordinates into a scroll position. If it's greater than the
131
// current scroll position, scroll forward by one step (visible size) otherwise scroll
132
// back.
133
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
134
let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
135
let max_range = (content_size - visible_size).max(Vec2::ZERO);
136
137
fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) {
138
*scroll_pos =
139
(*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range);
140
}
141
142
match scrollbar.orientation {
143
ControlOrientation::Horizontal => {
144
if node.size().x > 0. {
145
let click_pos = local_pos.x * content_size.x / node.size().x;
146
adjust_scroll_pos(&mut scroll_pos.x, click_pos, visible_size.x, max_range.x);
147
}
148
}
149
ControlOrientation::Vertical => {
150
if node.size().y > 0. {
151
let click_pos = local_pos.y * content_size.y / node.size().y;
152
adjust_scroll_pos(&mut scroll_pos.y, click_pos, visible_size.y, max_range.y);
153
}
154
}
155
}
156
}
157
}
158
159
fn scrollbar_on_drag_start(
160
mut ev: On<Pointer<DragStart>>,
161
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
162
q_scrollbar: Query<&Scrollbar>,
163
q_scroll_area: Query<&ScrollPosition>,
164
) {
165
if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.entity) {
166
ev.propagate(false);
167
if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent)
168
&& let Ok(scroll_area) = q_scroll_area.get(scrollbar.target)
169
{
170
drag.dragging = true;
171
drag.drag_origin = match scrollbar.orientation {
172
ControlOrientation::Horizontal => scroll_area.x,
173
ControlOrientation::Vertical => scroll_area.y,
174
};
175
}
176
}
177
}
178
179
fn scrollbar_on_drag(
180
mut ev: On<Pointer<Drag>>,
181
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
182
mut q_scrollbar: Query<(&ComputedNode, &Scrollbar)>,
183
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
184
ui_scale: Res<UiScale>,
185
) {
186
if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.entity)
187
&& let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent)
188
{
189
ev.propagate(false);
190
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
191
return;
192
};
193
194
if drag.dragging {
195
let distance = ev.event().distance / ui_scale.0;
196
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
197
let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
198
let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE);
199
200
match scrollbar.orientation {
201
ControlOrientation::Horizontal => {
202
let range = (content_size.x - visible_size.x).max(0.);
203
scroll_pos.x = (drag.drag_origin
204
+ (distance.x * content_size.x) / scrollbar_size.x)
205
.clamp(0., range);
206
}
207
ControlOrientation::Vertical => {
208
let range = (content_size.y - visible_size.y).max(0.);
209
scroll_pos.y = (drag.drag_origin
210
+ (distance.y * content_size.y) / scrollbar_size.y)
211
.clamp(0., range);
212
}
213
};
214
}
215
}
216
}
217
218
fn scrollbar_on_drag_end(
219
mut ev: On<Pointer<DragEnd>>,
220
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
221
) {
222
if let Ok(mut drag) = q_thumb.get_mut(ev.entity) {
223
ev.propagate(false);
224
if drag.dragging {
225
drag.dragging = false;
226
}
227
}
228
}
229
230
fn scrollbar_on_drag_cancel(
231
mut ev: On<Pointer<Cancel>>,
232
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
233
) {
234
if let Ok(mut drag) = q_thumb.get_mut(ev.entity) {
235
ev.propagate(false);
236
if drag.dragging {
237
drag.dragging = false;
238
}
239
}
240
}
241
242
fn update_scrollbar_thumb(
243
q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>,
244
q_scrollbar: Query<(&Scrollbar, &ComputedNode, &Children)>,
245
mut q_thumb: Query<&mut Node, With<CoreScrollbarThumb>>,
246
) {
247
for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() {
248
let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else {
249
continue;
250
};
251
252
// Size of the visible scrolling area.
253
let visible_size = scroll_area.1.size() * scroll_area.1.inverse_scale_factor;
254
255
// Size of the scrolling content.
256
let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor;
257
258
// Length of the scrollbar track.
259
let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor;
260
261
fn size_and_pos(
262
content_size: f32,
263
visible_size: f32,
264
track_length: f32,
265
min_size: f32,
266
offset: f32,
267
) -> (f32, f32) {
268
let thumb_size = if content_size > visible_size {
269
(track_length * visible_size / content_size)
270
.max(min_size)
271
.min(track_length)
272
} else {
273
track_length
274
};
275
276
let thumb_pos = if content_size > visible_size {
277
offset * (track_length - thumb_size) / (content_size - visible_size)
278
} else {
279
0.
280
};
281
282
(thumb_size, thumb_pos)
283
}
284
285
for child in children {
286
if let Ok(mut thumb) = q_thumb.get_mut(*child) {
287
match scrollbar.orientation {
288
ControlOrientation::Horizontal => {
289
let (thumb_size, thumb_pos) = size_and_pos(
290
content_size.x,
291
visible_size.x,
292
track_length.x,
293
scrollbar.min_thumb_length,
294
scroll_area.0.x,
295
);
296
297
thumb.top = Val::Px(0.);
298
thumb.bottom = Val::Px(0.);
299
thumb.left = Val::Px(thumb_pos);
300
thumb.width = Val::Px(thumb_size);
301
}
302
ControlOrientation::Vertical => {
303
let (thumb_size, thumb_pos) = size_and_pos(
304
content_size.y,
305
visible_size.y,
306
track_length.y,
307
scrollbar.min_thumb_length,
308
scroll_area.0.y,
309
);
310
311
thumb.left = Val::Px(0.);
312
thumb.right = Val::Px(0.);
313
thumb.top = Val::Px(thumb_pos);
314
thumb.height = Val::Px(thumb_size);
315
}
316
};
317
}
318
}
319
}
320
}
321
322
/// Plugin that adds the observers for the [`Scrollbar`] widget.
323
pub struct ScrollbarPlugin;
324
325
impl Plugin for ScrollbarPlugin {
326
fn build(&self, app: &mut App) {
327
app.add_observer(scrollbar_on_pointer_down)
328
.add_observer(scrollbar_on_drag_start)
329
.add_observer(scrollbar_on_drag_end)
330
.add_observer(scrollbar_on_drag_cancel)
331
.add_observer(scrollbar_on_drag)
332
.add_systems(PostUpdate, update_scrollbar_thumb);
333
}
334
}
335
336