Skip to main content

vertical_slider/
vertical_slider.rs

1//! Simple example showing vertical and horizontal slider widgets with snap behavior and value labels
2
3use bevy::{
4    input_focus::tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
5    picking::hover::Hovered,
6    prelude::*,
7    ui_widgets::{
8        observe, slider_self_update, Slider, SliderDragState, SliderRange, SliderThumb,
9        SliderValue, TrackClick,
10    },
11};
12
13const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
14const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
15
16fn main() {
17    App::new()
18        .add_plugins((DefaultPlugins, TabNavigationPlugin))
19        .add_systems(Startup, setup)
20        .add_systems(Update, (update_slider_visuals, update_value_labels))
21        .run();
22}
23
24#[derive(Component)]
25struct ValueLabel(Entity);
26
27#[derive(Component)]
28struct DemoSlider;
29
30#[derive(Component)]
31struct DemoSliderThumb;
32
33#[derive(Component)]
34struct VerticalSlider;
35
36fn setup(mut commands: Commands, assets: Res<AssetServer>) {
37    commands.spawn(Camera2d);
38
39    commands
40        .spawn((
41            Node {
42                width: percent(100),
43                height: percent(100),
44                align_items: AlignItems::Center,
45                justify_content: JustifyContent::Center,
46                display: Display::Flex,
47                flex_direction: FlexDirection::Row,
48                column_gap: px(50),
49                ..default()
50            },
51            TabGroup::default(),
52        ))
53        .with_children(|parent| {
54            // Vertical slider
55            parent
56                .spawn(Node {
57                    display: Display::Flex,
58                    flex_direction: FlexDirection::Column,
59                    align_items: AlignItems::Center,
60                    row_gap: px(10),
61                    ..default()
62                })
63                .with_children(|parent| {
64                    parent.spawn((
65                        Text::new("Vertical"),
66                        TextFont {
67                            font: assets.load("fonts/FiraSans-Bold.ttf").into(),
68                            font_size: FontSize::Px(20.0),
69                            ..default()
70                        },
71                        TextColor(Color::srgb(0.9, 0.9, 0.9)),
72                    ));
73
74                    let label_id = parent
75                        .spawn((
76                            Text::new("50"),
77                            TextFont {
78                                font: assets.load("fonts/FiraSans-Bold.ttf").into(),
79                                font_size: FontSize::Px(24.0),
80                                ..default()
81                            },
82                            TextColor(Color::srgb(0.9, 0.9, 0.9)),
83                        ))
84                        .id();
85
86                    parent.spawn((
87                        vertical_slider(),
88                        ValueLabel(label_id),
89                        observe(slider_self_update),
90                    ));
91                });
92
93            // Horizontal slider
94            parent
95                .spawn(Node {
96                    display: Display::Flex,
97                    flex_direction: FlexDirection::Column,
98                    align_items: AlignItems::Center,
99                    row_gap: px(10),
100                    ..default()
101                })
102                .with_children(|parent| {
103                    parent.spawn((
104                        Text::new("Horizontal"),
105                        TextFont {
106                            font: assets.load("fonts/FiraSans-Bold.ttf").into(),
107                            font_size: FontSize::Px(20.0),
108                            ..default()
109                        },
110                        TextColor(Color::srgb(0.9, 0.9, 0.9)),
111                    ));
112
113                    let label_id = parent
114                        .spawn((
115                            Text::new("50"),
116                            TextFont {
117                                font: assets.load("fonts/FiraSans-Bold.ttf").into(),
118                                font_size: FontSize::Px(24.0),
119                                ..default()
120                            },
121                            TextColor(Color::srgb(0.9, 0.9, 0.9)),
122                        ))
123                        .id();
124
125                    parent.spawn((
126                        horizontal_slider(),
127                        ValueLabel(label_id),
128                        observe(slider_self_update),
129                    ));
130                });
131        });
132}
133
134fn vertical_slider() -> impl Bundle {
135    (
136        Node {
137            display: Display::Flex,
138            flex_direction: FlexDirection::Row,
139            justify_content: JustifyContent::Center,
140            align_items: AlignItems::Stretch,
141            column_gap: px(4),
142            width: px(12),
143            height: px(200),
144            ..default()
145        },
146        DemoSlider,
147        VerticalSlider,
148        Hovered::default(),
149        Slider {
150            track_click: TrackClick::Snap,
151            ..Default::default()
152        },
153        SliderValue(50.0),
154        SliderRange::new(0.0, 100.0),
155        TabIndex(0),
156        Children::spawn((
157            Spawn((
158                Node {
159                    width: px(6),
160                    border_radius: BorderRadius::all(px(3)),
161                    ..default()
162                },
163                BackgroundColor(SLIDER_TRACK),
164            )),
165            Spawn((
166                Node {
167                    display: Display::Flex,
168                    position_type: PositionType::Absolute,
169                    top: px(12),
170                    bottom: px(0),
171                    left: px(0),
172                    right: px(0),
173                    ..default()
174                },
175                children![(
176                    DemoSliderThumb,
177                    SliderThumb,
178                    Node {
179                        display: Display::Flex,
180                        width: px(12),
181                        height: px(12),
182                        position_type: PositionType::Absolute,
183                        bottom: percent(0),
184                        border_radius: BorderRadius::MAX,
185                        ..default()
186                    },
187                    BackgroundColor(SLIDER_THUMB),
188                )],
189            )),
190        )),
191    )
192}
193
194fn horizontal_slider() -> impl Bundle {
195    (
196        Node {
197            display: Display::Flex,
198            flex_direction: FlexDirection::Column,
199            justify_content: JustifyContent::Center,
200            align_items: AlignItems::Stretch,
201            column_gap: px(4),
202            height: px(12),
203            width: px(200),
204            ..default()
205        },
206        DemoSlider,
207        Hovered::default(),
208        Slider {
209            track_click: TrackClick::Snap,
210            ..Default::default()
211        },
212        SliderValue(50.0),
213        SliderRange::new(0.0, 100.0),
214        TabIndex(0),
215        Children::spawn((
216            Spawn((
217                Node {
218                    height: px(6),
219                    border_radius: BorderRadius::all(px(3)),
220                    ..default()
221                },
222                BackgroundColor(SLIDER_TRACK),
223            )),
224            Spawn((
225                Node {
226                    display: Display::Flex,
227                    position_type: PositionType::Absolute,
228                    left: px(0),
229                    right: px(12),
230                    top: px(0),
231                    bottom: px(0),
232                    ..default()
233                },
234                children![(
235                    DemoSliderThumb,
236                    SliderThumb,
237                    Node {
238                        display: Display::Flex,
239                        width: px(12),
240                        height: px(12),
241                        position_type: PositionType::Absolute,
242                        left: percent(0),
243                        border_radius: BorderRadius::MAX,
244                        ..default()
245                    },
246                    BackgroundColor(SLIDER_THUMB),
247                )],
248            )),
249        )),
250    )
251}
252
253fn update_slider_visuals(
254    sliders: Query<
255        (
256            Entity,
257            &SliderValue,
258            &SliderRange,
259            &Hovered,
260            &SliderDragState,
261            Has<VerticalSlider>,
262        ),
263        (
264            Or<(
265                Changed<SliderValue>,
266                Changed<Hovered>,
267                Changed<SliderDragState>,
268            )>,
269            With<DemoSlider>,
270        ),
271    >,
272    children: Query<&Children>,
273    mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
274) {
275    for (slider_ent, value, range, hovered, drag_state, is_vertical) in sliders.iter() {
276        for child in children.iter_descendants(slider_ent) {
277            if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
278                && is_thumb
279            {
280                let position = range.thumb_position(value.0) * 100.0;
281                if is_vertical {
282                    thumb_node.bottom = percent(position);
283                } else {
284                    thumb_node.left = percent(position);
285                }
286
287                let is_active = hovered.0 | drag_state.dragging;
288                thumb_bg.0 = if is_active {
289                    SLIDER_THUMB.lighter(0.3)
290                } else {
291                    SLIDER_THUMB
292                };
293            }
294        }
295    }
296}
297
298fn update_value_labels(
299    sliders: Query<(&SliderValue, &ValueLabel), (Changed<SliderValue>, With<DemoSlider>)>,
300    mut texts: Query<&mut Text>,
301) {
302    for (value, label) in sliders.iter() {
303        if let Ok(mut text) = texts.get_mut(label.0) {
304            **text = format!("{:.0}", value.0);
305        }
306    }
307}