scroll/scroll.rs
1//! This example illustrates scrolling in Bevy UI.
2
3use accesskit::{Node as Accessible, Role};
4use bevy::{
5 a11y::AccessibilityNode,
6 input::mouse::{MouseScrollUnit, MouseWheel},
7 picking::hover::HoverMap,
8 prelude::*,
9 winit::WinitSettings,
10};
11
12fn main() {
13 let mut app = App::new();
14 app.add_plugins(DefaultPlugins)
15 .insert_resource(WinitSettings::desktop_app())
16 .add_systems(Startup, setup)
17 .add_systems(Update, update_scroll_position);
18
19 app.run();
20}
21
22const FONT_SIZE: f32 = 20.;
23const LINE_HEIGHT: f32 = 21.;
24
25fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
26 // Camera
27 commands.spawn((Camera2d, IsDefaultUiCamera));
28
29 // root node
30 commands
31 .spawn(Node {
32 width: Val::Percent(100.0),
33 height: Val::Percent(100.0),
34 justify_content: JustifyContent::SpaceBetween,
35 flex_direction: FlexDirection::Column,
36 ..default()
37 })
38 .insert(Pickable::IGNORE)
39 .with_children(|parent| {
40 // horizontal scroll example
41 parent
42 .spawn(Node {
43 width: Val::Percent(100.),
44 flex_direction: FlexDirection::Column,
45 ..default()
46 })
47 .with_children(|parent| {
48 // header
49 parent.spawn((
50 Text::new("Horizontally Scrolling list (Ctrl + MouseWheel)"),
51 TextFont {
52 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
53 font_size: FONT_SIZE,
54 ..default()
55 },
56 Label,
57 ));
58
59 // horizontal scroll container
60 parent
61 .spawn((
62 Node {
63 width: Val::Percent(80.),
64 margin: UiRect::all(Val::Px(10.)),
65 flex_direction: FlexDirection::Row,
66 overflow: Overflow::scroll_x(), // n.b.
67 ..default()
68 },
69 BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
70 ))
71 .with_children(|parent| {
72 for i in 0..100 {
73 parent.spawn((Text(format!("Item {i}")),
74 TextFont {
75 font: asset_server
76 .load("fonts/FiraSans-Bold.ttf"),
77 ..default()
78 },
79 Label,
80 AccessibilityNode(Accessible::new(Role::ListItem)),
81 ))
82 .insert(Node {
83 min_width: Val::Px(200.),
84 align_content: AlignContent::Center,
85 ..default()
86 })
87 .insert(Pickable {
88 should_block_lower: false,
89 ..default()
90 })
91 .observe(|
92 trigger: Trigger<Pointer<Pressed>>,
93 mut commands: Commands
94 | {
95 if trigger.event().button == PointerButton::Primary {
96 commands.entity(trigger.target()).despawn();
97 }
98 });
99 }
100 });
101 });
102
103 // container for all other examples
104 parent
105 .spawn(Node {
106 width: Val::Percent(100.),
107 height: Val::Percent(100.),
108 flex_direction: FlexDirection::Row,
109 justify_content: JustifyContent::SpaceBetween,
110 ..default()
111 })
112 .with_children(|parent| {
113 // vertical scroll example
114 parent
115 .spawn(Node {
116 flex_direction: FlexDirection::Column,
117 justify_content: JustifyContent::Center,
118 align_items: AlignItems::Center,
119 width: Val::Px(200.),
120 ..default()
121 })
122 .with_children(|parent| {
123 // Title
124 parent.spawn((
125 Text::new("Vertically Scrolling List"),
126 TextFont {
127 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
128 font_size: FONT_SIZE,
129 ..default()
130 },
131 Label,
132 ));
133 // Scrolling list
134 parent
135 .spawn((
136 Node {
137 flex_direction: FlexDirection::Column,
138 align_self: AlignSelf::Stretch,
139 height: Val::Percent(50.),
140 overflow: Overflow::scroll_y(), // n.b.
141 ..default()
142 },
143 BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
144 ))
145 .with_children(|parent| {
146 // List items
147 for i in 0..25 {
148 parent
149 .spawn(Node {
150 min_height: Val::Px(LINE_HEIGHT),
151 max_height: Val::Px(LINE_HEIGHT),
152 ..default()
153 })
154 .insert(Pickable {
155 should_block_lower: false,
156 ..default()
157 })
158 .with_children(|parent| {
159 parent
160 .spawn((
161 Text(format!("Item {i}")),
162 TextFont {
163 font: asset_server
164 .load("fonts/FiraSans-Bold.ttf"),
165 ..default()
166 },
167 Label,
168 AccessibilityNode(Accessible::new(
169 Role::ListItem,
170 )),
171 ))
172 .insert(Pickable {
173 should_block_lower: false,
174 ..default()
175 });
176 });
177 }
178 });
179 });
180
181 // Bidirectional scroll example
182 parent
183 .spawn(Node {
184 flex_direction: FlexDirection::Column,
185 justify_content: JustifyContent::Center,
186 align_items: AlignItems::Center,
187 width: Val::Px(200.),
188 ..default()
189 })
190 .with_children(|parent| {
191 // Title
192 parent.spawn((
193 Text::new("Bidirectionally Scrolling List"),
194 TextFont {
195 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
196 font_size: FONT_SIZE,
197 ..default()
198 },
199 Label,
200 ));
201 // Scrolling list
202 parent
203 .spawn((
204 Node {
205 flex_direction: FlexDirection::Column,
206 align_self: AlignSelf::Stretch,
207 height: Val::Percent(50.),
208 overflow: Overflow::scroll(), // n.b.
209 ..default()
210 },
211 BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
212 ))
213 .with_children(|parent| {
214 // Rows in each column
215 for oi in 0..10 {
216 parent
217 .spawn(Node {
218 flex_direction: FlexDirection::Row,
219 ..default()
220 })
221 .insert(Pickable::IGNORE)
222 .with_children(|parent| {
223 // Elements in each row
224 for i in 0..25 {
225 parent
226 .spawn((
227 Text(format!("Item {}", (oi * 25) + i)),
228 TextFont {
229 font: asset_server.load(
230 "fonts/FiraSans-Bold.ttf",
231 ),
232 ..default()
233 },
234 Label,
235 AccessibilityNode(Accessible::new(
236 Role::ListItem,
237 )),
238 ))
239 .insert(Pickable {
240 should_block_lower: false,
241 ..default()
242 });
243 }
244 });
245 }
246 });
247 });
248
249 // Nested scrolls example
250 parent
251 .spawn(Node {
252 flex_direction: FlexDirection::Column,
253 justify_content: JustifyContent::Center,
254 align_items: AlignItems::Center,
255 width: Val::Px(200.),
256 ..default()
257 })
258 .with_children(|parent| {
259 // Title
260 parent.spawn((
261 Text::new("Nested Scrolling Lists"),
262 TextFont {
263 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
264 font_size: FONT_SIZE,
265 ..default()
266 },
267 Label,
268 ));
269 // Outer, horizontal scrolling container
270 parent
271 .spawn((
272 Node {
273 column_gap: Val::Px(20.),
274 flex_direction: FlexDirection::Row,
275 align_self: AlignSelf::Stretch,
276 height: Val::Percent(50.),
277 overflow: Overflow::scroll_x(), // n.b.
278 ..default()
279 },
280 BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
281 ))
282 .with_children(|parent| {
283 // Inner, scrolling columns
284 for oi in 0..30 {
285 parent
286 .spawn((
287 Node {
288 flex_direction: FlexDirection::Column,
289 align_self: AlignSelf::Stretch,
290 overflow: Overflow::scroll_y(),
291 ..default()
292 },
293 BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),
294 ))
295 .insert(Pickable {
296 should_block_lower: false,
297 ..default()
298 })
299 .with_children(|parent| {
300 for i in 0..25 {
301 parent
302 .spawn((
303 Text(format!("Item {}", (oi * 25) + i)),
304 TextFont {
305 font: asset_server.load(
306 "fonts/FiraSans-Bold.ttf",
307 ),
308 ..default()
309 },
310 Label,
311 AccessibilityNode(Accessible::new(
312 Role::ListItem,
313 )),
314 ))
315 .insert(Pickable {
316 should_block_lower: false,
317 ..default()
318 });
319 }
320 });
321 }
322 });
323 });
324 });
325 });
326}
327
328/// Updates the scroll position of scrollable nodes in response to mouse input
329pub fn update_scroll_position(
330 mut mouse_wheel_events: EventReader<MouseWheel>,
331 hover_map: Res<HoverMap>,
332 mut scrolled_node_query: Query<&mut ScrollPosition>,
333 keyboard_input: Res<ButtonInput<KeyCode>>,
334) {
335 for mouse_wheel_event in mouse_wheel_events.read() {
336 let (mut dx, mut dy) = match mouse_wheel_event.unit {
337 MouseScrollUnit::Line => (
338 mouse_wheel_event.x * LINE_HEIGHT,
339 mouse_wheel_event.y * LINE_HEIGHT,
340 ),
341 MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
342 };
343
344 if keyboard_input.pressed(KeyCode::ControlLeft)
345 || keyboard_input.pressed(KeyCode::ControlRight)
346 {
347 std::mem::swap(&mut dx, &mut dy);
348 }
349
350 for (_pointer, pointer_map) in hover_map.iter() {
351 for (entity, _hit) in pointer_map.iter() {
352 if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
353 scroll_position.offset_x -= dx;
354 scroll_position.offset_y -= dy;
355 }
356 }
357 }
358 }
359}