Skip to main content

scrollbars/
scrollbars.rs

1//! Demonstrations of scrolling and scrollbars.
2
3use bevy::{
4    ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
5    input_focus::tab_navigation::{TabGroup, TabNavigationPlugin},
6    picking::hover::Hovered,
7    prelude::*,
8    ui_widgets::{ControlOrientation, Scrollbar, ScrollbarDragState, ScrollbarThumb},
9};
10
11fn main() {
12    App::new()
13        .add_plugins((DefaultPlugins, TabNavigationPlugin))
14        .insert_resource(UiScale(1.25))
15        .add_systems(Startup, setup_view_root)
16        .add_systems(Update, update_scrollbar_thumb)
17        .run();
18}
19
20fn setup_view_root(mut commands: Commands) {
21    let camera = commands.spawn((Camera::default(), Camera2d)).id();
22
23    commands.spawn((
24        Node {
25            display: Display::Flex,
26            flex_direction: FlexDirection::Column,
27            position_type: PositionType::Absolute,
28            left: px(0),
29            top: px(0),
30            right: px(0),
31            bottom: px(0),
32            padding: UiRect::all(px(3)),
33            row_gap: px(6),
34            ..Default::default()
35        },
36        BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
37        UiTargetCamera(camera),
38        TabGroup::default(),
39        Children::spawn((Spawn(Text::new("Scrolling")), Spawn(scroll_area_demo()))),
40    ));
41}
42
43/// Create a scrolling area.
44///
45/// The "scroll area" is a container that can be scrolled. It has a nested structure which is
46/// three levels deep:
47/// - The outermost node is a grid that contains the scroll area and the scrollbars.
48/// - The scroll area is a flex container that contains the scrollable content. This
49///   is the element that has the `overflow: scroll` property.
50/// - The scrollable content consists of the elements actually displayed in the scrolling area.
51fn scroll_area_demo() -> impl Bundle {
52    (
53        // Frame element which contains the scroll area and scrollbars.
54        Node {
55            display: Display::Grid,
56            width: px(200),
57            height: px(150),
58            grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
59            grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
60            row_gap: px(2),
61            column_gap: px(2),
62            ..default()
63        },
64        Children::spawn((SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
65            // The actual scrolling area.
66            // Note that we're using `SpawnWith` here because we need to get the entity id of the
67            // scroll area in order to set the target of the scrollbars.
68            let scroll_area_id = parent
69                .spawn((
70                    Node {
71                        display: Display::Flex,
72                        flex_direction: FlexDirection::Column,
73                        padding: UiRect::all(px(4)),
74                        overflow: Overflow::scroll(),
75                        ..default()
76                    },
77                    BackgroundColor(colors::GRAY1.into()),
78                    ScrollPosition(Vec2::new(0.0, 10.0)),
79                    Children::spawn((
80                        // The actual content of the scrolling area
81                        Spawn(text_row("Alpha Wolf")),
82                        Spawn(text_row("Beta Blocker")),
83                        Spawn(text_row("Delta Sleep")),
84                        Spawn(text_row("Gamma Ray")),
85                        Spawn(text_row("Epsilon Eridani")),
86                        Spawn(text_row("Zeta Function")),
87                        Spawn(text_row("Lambda Calculus")),
88                        Spawn(text_row("Nu Metal")),
89                        Spawn(text_row("Pi Day")),
90                        Spawn(text_row("Chi Pants")),
91                        Spawn(text_row("Psi Powers")),
92                        Spawn(text_row("Omega Fatty Acid")),
93                    )),
94                ))
95                .id();
96
97            // Vertical scrollbar
98            parent.spawn((
99                Node {
100                    min_width: px(8),
101                    grid_row: GridPlacement::start(1),
102                    grid_column: GridPlacement::start(2),
103                    ..default()
104                },
105                Scrollbar {
106                    orientation: ControlOrientation::Vertical,
107                    target: scroll_area_id,
108                    min_thumb_length: 8.0,
109                },
110                Children::spawn(Spawn((
111                    Hovered::default(),
112                    BackgroundColor(colors::GRAY2.into()),
113                    BorderColor::all(colors::GRAY3),
114                    ScrollbarThumb {
115                        border_radius: BorderRadius::all(px(4)),
116                        border: px(1).all(),
117                    },
118                ))),
119            ));
120
121            // Horizontal scrollbar
122            parent.spawn((
123                Node {
124                    min_height: px(8),
125                    grid_row: GridPlacement::start(2),
126                    grid_column: GridPlacement::start(1),
127                    ..default()
128                },
129                Scrollbar {
130                    orientation: ControlOrientation::Horizontal,
131                    target: scroll_area_id,
132                    min_thumb_length: 8.0,
133                },
134                Children::spawn(Spawn((
135                    Hovered::default(),
136                    BackgroundColor(colors::GRAY2.into()),
137                    BorderColor::all(colors::GRAY3),
138                    ScrollbarThumb {
139                        border_radius: BorderRadius::all(px(4)),
140                        border: px(1).all(),
141                    },
142                ))),
143            ));
144        }),)),
145    )
146}
147
148/// Create a list row
149fn text_row(caption: &str) -> impl Bundle {
150    (
151        Text::new(caption),
152        TextFont {
153            font_size: FontSize::Px(14.0),
154            ..default()
155        },
156    )
157}
158
159// Update the color of the scrollbar thumb.
160fn update_scrollbar_thumb(
161    mut q_thumb: Query<
162        (&mut BackgroundColor, &Hovered, &ScrollbarDragState),
163        (
164            With<ScrollbarThumb>,
165            Or<(Changed<Hovered>, Changed<ScrollbarDragState>)>,
166        ),
167    >,
168) {
169    for (mut thumb_bg, Hovered(is_hovering), drag) in q_thumb.iter_mut() {
170        let color: Color = if *is_hovering || drag.dragging {
171            // If hovering, use a lighter color
172            colors::GRAY4
173        } else {
174            // Default color for the slider
175            colors::GRAY2
176        }
177        .into();
178
179        if thumb_bg.0 != color {
180            // Update the color of the thumb
181            thumb_bg.0 = color;
182        }
183    }
184}
185
186mod colors {
187    use bevy::color::Srgba;
188
189    pub const GRAY1: Srgba = Srgba::new(0.224, 0.224, 0.243, 1.0);
190    pub const GRAY2: Srgba = Srgba::new(0.486, 0.486, 0.529, 1.0);
191    pub const GRAY3: Srgba = Srgba::new(0.71, 0.71, 0.772, 1.0);
192    pub const GRAY4: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
193}