scrollbars/
scrollbars.rs

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