1use 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 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 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}