Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/scroll_and_overflow/scroll.rs
9331 views
1
//! This example illustrates scrolling in Bevy UI.
2
3
use accesskit::{Node as Accessible, Role};
4
use bevy::{
5
a11y::AccessibilityNode,
6
color::palettes::css::{BLACK, BLUE, RED},
7
ecs::spawn::SpawnIter,
8
input::mouse::{MouseScrollUnit, MouseWheel},
9
picking::hover::HoverMap,
10
prelude::*,
11
};
12
13
fn main() {
14
let mut app = App::new();
15
16
app.add_plugins(DefaultPlugins)
17
.add_systems(Startup, setup)
18
.add_systems(Update, send_scroll_events)
19
.add_observer(on_scroll_handler);
20
21
app.run();
22
}
23
24
const LINE_HEIGHT: f32 = 21.;
25
26
/// Injects scroll events into the UI hierarchy.
27
fn send_scroll_events(
28
mut mouse_wheel_reader: MessageReader<MouseWheel>,
29
hover_map: Res<HoverMap>,
30
keyboard_input: Res<ButtonInput<KeyCode>>,
31
mut commands: Commands,
32
) {
33
for mouse_wheel in mouse_wheel_reader.read() {
34
let mut delta = -Vec2::new(mouse_wheel.x, mouse_wheel.y);
35
36
if mouse_wheel.unit == MouseScrollUnit::Line {
37
delta *= LINE_HEIGHT;
38
}
39
40
if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) {
41
std::mem::swap(&mut delta.x, &mut delta.y);
42
}
43
44
for pointer_map in hover_map.values() {
45
for entity in pointer_map.keys().copied() {
46
commands.trigger(Scroll { entity, delta });
47
}
48
}
49
}
50
}
51
52
/// UI scrolling event.
53
#[derive(EntityEvent, Debug)]
54
#[entity_event(propagate, auto_propagate)]
55
struct Scroll {
56
entity: Entity,
57
/// Scroll delta in logical coordinates.
58
delta: Vec2,
59
}
60
61
fn on_scroll_handler(
62
mut scroll: On<Scroll>,
63
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
64
) {
65
let Ok((mut scroll_position, node, computed)) = query.get_mut(scroll.entity) else {
66
return;
67
};
68
69
let max_offset = (computed.content_size() - computed.size()) * computed.inverse_scale_factor();
70
71
let delta = &mut scroll.delta;
72
if node.overflow.x == OverflowAxis::Scroll && delta.x != 0. {
73
// Is this node already scrolled all the way in the direction of the scroll?
74
let max = if delta.x > 0. {
75
scroll_position.x >= max_offset.x
76
} else {
77
scroll_position.x <= 0.
78
};
79
80
if !max {
81
scroll_position.x += delta.x;
82
// Consume the X portion of the scroll delta.
83
delta.x = 0.;
84
}
85
}
86
87
if node.overflow.y == OverflowAxis::Scroll && delta.y != 0. {
88
// Is this node already scrolled all the way in the direction of the scroll?
89
let max = if delta.y > 0. {
90
scroll_position.y >= max_offset.y
91
} else {
92
scroll_position.y <= 0.
93
};
94
95
if !max {
96
scroll_position.y += delta.y;
97
// Consume the Y portion of the scroll delta.
98
delta.y = 0.;
99
}
100
}
101
102
// Stop propagating when the delta is fully consumed.
103
if *delta == Vec2::ZERO {
104
scroll.propagate(false);
105
}
106
}
107
108
const FONT_SIZE: FontSize = FontSize::Px(20.);
109
110
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
111
// Camera
112
commands.spawn((Camera2d, IsDefaultUiCamera));
113
114
// Font
115
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
116
117
// root node
118
commands
119
.spawn(Node {
120
width: percent(100),
121
height: percent(100),
122
justify_content: JustifyContent::SpaceBetween,
123
flex_direction: FlexDirection::Column,
124
..default()
125
})
126
.with_children(|parent| {
127
// horizontal scroll example
128
parent
129
.spawn(Node {
130
width: percent(100),
131
flex_direction: FlexDirection::Column,
132
..default()
133
})
134
.with_children(|parent| {
135
// header
136
parent.spawn((
137
Text::new("Horizontally Scrolling list (Ctrl + MouseWheel)"),
138
TextFont {
139
font: font_handle.clone().into(),
140
font_size: FONT_SIZE,
141
..default()
142
},
143
Label,
144
));
145
146
// horizontal scroll container
147
parent
148
.spawn((
149
Node {
150
width: percent(80),
151
margin: UiRect::all(px(10)),
152
flex_direction: FlexDirection::Row,
153
overflow: Overflow::scroll_x(), // n.b.
154
..default()
155
},
156
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
157
))
158
.with_children(|parent| {
159
for i in 0..100 {
160
parent
161
.spawn((
162
Text(format!("Item {i}")),
163
TextFont {
164
font: font_handle.clone().into(),
165
..default()
166
},
167
Label,
168
AccessibilityNode(Accessible::new(Role::ListItem)),
169
Node {
170
min_width: px(200),
171
align_content: AlignContent::Center,
172
..default()
173
},
174
))
175
.observe(
176
|press: On<Pointer<Press>>, mut commands: Commands| {
177
if press.event().button == PointerButton::Primary {
178
commands.entity(press.entity).despawn();
179
}
180
},
181
);
182
}
183
});
184
});
185
186
// container for all other examples
187
parent.spawn((
188
Node {
189
width: percent(100),
190
height: percent(100),
191
flex_direction: FlexDirection::Row,
192
justify_content: JustifyContent::SpaceBetween,
193
..default()
194
},
195
children![
196
vertically_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),
197
bidirectional_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),
198
bidirectional_scrolling_list_with_sticky(
199
asset_server.load("fonts/FiraSans-Bold.ttf")
200
),
201
nested_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),
202
],
203
));
204
});
205
}
206
207
fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
208
(
209
Node {
210
flex_direction: FlexDirection::Column,
211
justify_content: JustifyContent::Center,
212
align_items: AlignItems::Center,
213
width: px(200),
214
..default()
215
},
216
children![
217
(
218
// Title
219
Text::new("Vertically Scrolling List"),
220
TextFont {
221
font: font_handle.clone().into(),
222
font_size: FONT_SIZE,
223
..default()
224
},
225
Label,
226
),
227
(
228
// Scrolling list
229
Node {
230
flex_direction: FlexDirection::Column,
231
align_self: AlignSelf::Stretch,
232
height: percent(50),
233
overflow: Overflow::scroll_y(), // n.b.
234
scrollbar_width: 20.,
235
..default()
236
},
237
#[cfg(feature = "bevy_ui_debug")]
238
UiDebugOptions {
239
enabled: true,
240
outline_border_box: false,
241
outline_padding_box: false,
242
outline_content_box: false,
243
outline_scrollbars: true,
244
line_width: 2.,
245
line_color_override: None,
246
show_hidden: false,
247
show_clipped: true,
248
ignore_border_radius: true,
249
},
250
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
251
Children::spawn(SpawnIter((0..25).map(move |i| {
252
(
253
Node {
254
min_height: px(LINE_HEIGHT),
255
max_height: px(LINE_HEIGHT),
256
..default()
257
},
258
children![(
259
Text(format!("Item {i}")),
260
TextFont {
261
font: font_handle.clone().into(),
262
..default()
263
},
264
Label,
265
AccessibilityNode(Accessible::new(Role::ListItem)),
266
)],
267
)
268
})))
269
),
270
],
271
)
272
}
273
274
fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
275
(
276
Node {
277
flex_direction: FlexDirection::Column,
278
justify_content: JustifyContent::Center,
279
align_items: AlignItems::Center,
280
width: px(200),
281
..default()
282
},
283
children![
284
(
285
Text::new("Bidirectionally Scrolling List"),
286
TextFont {
287
font: font_handle.clone().into(),
288
font_size: FONT_SIZE,
289
..default()
290
},
291
Label,
292
),
293
(
294
Node {
295
flex_direction: FlexDirection::Column,
296
align_self: AlignSelf::Stretch,
297
height: percent(50),
298
overflow: Overflow::scroll(), // n.b.
299
..default()
300
},
301
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
302
Children::spawn(SpawnIter((0..25).map(move |oi| {
303
(
304
Node {
305
flex_direction: FlexDirection::Row,
306
..default()
307
},
308
Children::spawn(SpawnIter((0..10).map({
309
let value = font_handle.clone();
310
move |i| {
311
(
312
Text(format!("Item {}", (oi * 10) + i)),
313
TextFont::from(value.clone()),
314
Label,
315
AccessibilityNode(Accessible::new(Role::ListItem)),
316
)
317
}
318
}))),
319
)
320
})))
321
)
322
],
323
)
324
}
325
326
fn bidirectional_scrolling_list_with_sticky(font_handle: Handle<Font>) -> impl Bundle {
327
(
328
Node {
329
flex_direction: FlexDirection::Column,
330
justify_content: JustifyContent::Center,
331
align_items: AlignItems::Center,
332
width: px(200),
333
..default()
334
},
335
children![
336
(
337
Text::new("Bidirectionally Scrolling List With Sticky Nodes"),
338
TextFont {
339
font: font_handle.clone().into(),
340
font_size: FONT_SIZE,
341
..default()
342
},
343
Label,
344
),
345
(
346
Node {
347
display: Display::Grid,
348
align_self: AlignSelf::Stretch,
349
height: percent(50),
350
overflow: Overflow::scroll(), // n.b.
351
grid_template_columns: RepeatedGridTrack::auto(30),
352
..default()
353
},
354
Children::spawn(SpawnIter(
355
(0..30)
356
.flat_map(|y| (0..30).map(move |x| (y, x)))
357
.map(move |(y, x)| {
358
let value = font_handle.clone();
359
// Simple sticky nodes at top and left sides of UI node
360
// can be achieved by combining such effects as
361
// IgnoreScroll, ZIndex, BackgroundColor for child UI nodes.
362
let ignore_scroll = BVec2 {
363
x: x == 0,
364
y: y == 0,
365
};
366
let (z_index, background_color, role) = match (x == 0, y == 0) {
367
(true, true) => (2, RED, Role::RowHeader),
368
(true, false) => (1, BLUE, Role::RowHeader),
369
(false, true) => (1, BLUE, Role::ColumnHeader),
370
(false, false) => (0, BLACK, Role::Cell),
371
};
372
(
373
Text(format!("|{},{}|", y, x)),
374
TextFont::from(value.clone()),
375
TextLayout {
376
linebreak: LineBreak::NoWrap,
377
..default()
378
},
379
Label,
380
AccessibilityNode(Accessible::new(role)),
381
IgnoreScroll(ignore_scroll),
382
ZIndex(z_index),
383
BackgroundColor(Color::Srgba(background_color)),
384
)
385
})
386
))
387
)
388
],
389
)
390
}
391
392
fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
393
(
394
Node {
395
flex_direction: FlexDirection::Column,
396
justify_content: JustifyContent::Center,
397
align_items: AlignItems::Center,
398
width: px(200),
399
..default()
400
},
401
children![
402
(
403
// Title
404
Text::new("Nested Scrolling Lists"),
405
TextFont {
406
font: font_handle.clone().into(),
407
font_size: FONT_SIZE,
408
..default()
409
},
410
Label,
411
),
412
(
413
// Outer, bi-directional scrolling container
414
Node {
415
column_gap: px(20),
416
flex_direction: FlexDirection::Row,
417
align_self: AlignSelf::Stretch,
418
height: percent(50),
419
overflow: Overflow::scroll(),
420
..default()
421
},
422
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
423
// Inner, scrolling columns
424
Children::spawn(SpawnIter((0..5).map(move |oi| {
425
(
426
Node {
427
flex_direction: FlexDirection::Column,
428
align_self: AlignSelf::Stretch,
429
height: percent(200. / 5. * (oi as f32 + 1.)),
430
overflow: Overflow::scroll_y(),
431
..default()
432
},
433
BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),
434
Children::spawn(SpawnIter((0..20).map({
435
let value = font_handle.clone();
436
move |i| {
437
(
438
Text(format!("Item {}", (oi * 20) + i)),
439
TextFont::from(value.clone()),
440
Label,
441
AccessibilityNode(Accessible::new(Role::ListItem)),
442
)
443
}
444
}))),
445
)
446
})))
447
)
448
],
449
)
450
}
451
452