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}