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