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