1use bevy::prelude::*;
16
17const TRACK_WIDTH: f32 = 500.0;
18const TRACK_HEIGHT: f32 = 14.0;
19const THUMB_SIZE: f32 = 28.0;
20const THUMB_HALF: f32 = THUMB_SIZE / 2.0;
21const FILL_HEIGHT: f32 = 8.0;
22
23const SLIDER_DEFAULT_VALUE: f32 = 0.5;
24
25const COLOR_TRACK: Color = Color::srgb(0.2, 0.2, 0.2);
26const COLOR_FILL: Color = Color::srgb(0.35, 0.65, 0.35);
27const COLOR_FILL_DRAGGING: Color = Color::srgb(0.45, 0.85, 0.45);
28const COLOR_THUMB_IDLE: Color = Color::srgb(0.85, 0.55, 0.1);
29const COLOR_THUMB_HOVER: Color = Color::srgb(1.0, 0.75, 0.2);
30const COLOR_DECOY_IDLE: Color = Color::srgb(0.2, 0.45, 0.8);
31const COLOR_DECOY_HOVER: Color = Color::srgb(0.9, 0.2, 0.2);
32const COLOR_TOGGLE_ON: Color = Color::srgb(0.15, 0.65, 0.3);
33const COLOR_TOGGLE_OFF: Color = Color::srgb(0.55, 0.15, 0.15);
34const COLOR_RESET_IDLE: Color = Color::srgb(0.3, 0.3, 0.55);
35const COLOR_RESET_HOVER: Color = Color::srgb(0.5, 0.5, 0.75);
36
37#[derive(Resource)]
39struct SliderValue(f32);
40
41impl Default for SliderValue {
42 fn default() -> Self {
43 Self(SLIDER_DEFAULT_VALUE)
44 }
45}
46
47#[derive(Resource)]
49struct CaptureEnabled(bool);
50
51impl Default for CaptureEnabled {
52 fn default() -> Self {
53 Self(true)
54 }
55}
56
57#[derive(Component)]
58struct SliderThumb;
59
60#[derive(Component)]
61struct SliderFill;
62
63#[derive(Component)]
64struct SliderLabel;
65
66#[derive(Component)]
67struct ToggleLabel;
68
69#[derive(Component)]
70struct ToggleButton;
71
72#[derive(Component)]
73struct ResetButton;
74
75#[derive(Component)]
76struct DecoyButton;
77
78fn thumb_left(v: f32) -> f32 {
80 v.clamp(0.0, 1.0) * (TRACK_WIDTH - THUMB_SIZE)
81}
82
83fn fill_width(v: f32) -> f32 {
85 v.clamp(0.0, 1.0) * (TRACK_WIDTH - THUMB_SIZE) + THUMB_HALF
86}
87
88fn main() {
89 App::new()
90 .add_plugins(DefaultPlugins)
91 .init_resource::<SliderValue>()
92 .init_resource::<CaptureEnabled>()
93 .add_systems(Startup, setup)
94 .add_systems(Update, (sync_thumb_and_label, sync_fill))
95 .run();
96}
97
98fn setup(mut commands: Commands) {
99 commands.spawn(Camera2d);
100
101 commands
103 .spawn((
104 Node {
105 width: Val::Percent(100.0),
106 height: Val::Percent(100.0),
107 flex_direction: FlexDirection::Column,
108 align_items: AlignItems::Center,
109 justify_content: JustifyContent::Center,
110 row_gap: Val::Px(28.0),
111 ..default()
112 },
113 Pickable::IGNORE,
114 ))
115 .with_children(|root| {
116 root.spawn((
117 Text::new(
118 "Drag the slider over the blue buttons.\n\
119 With capture ON they stay blue. With capture OFF they incorrectly turn red.",
120 ),
121 TextFont::from_font_size(15.0),
122 TextColor(Color::srgb(0.75, 0.75, 0.75)),
123 ));
124
125 root.spawn((
126 Node {
127 flex_direction: FlexDirection::Row,
128 align_items: AlignItems::Center,
129 column_gap: Val::Px(10.0),
130 ..default()
131 },
132 Pickable::IGNORE,
133 ))
134 .with_children(|row| {
135 row.spawn((
136 Text::new("0.0"),
137 TextFont::from_font_size(13.0),
138 TextColor(Color::srgb(0.55, 0.55, 0.55)),
139 ));
140
141 row.spawn((
142 Node {
143 width: Val::Px(TRACK_WIDTH),
144 height: Val::Px(TRACK_HEIGHT),
145 position_type: PositionType::Relative,
146 align_items: AlignItems::Center,
147 ..default()
148 },
149 BackgroundColor(COLOR_TRACK),
150 Pickable::IGNORE,
151 ))
152 .with_children(|track| {
153 track.spawn((
154 SliderFill,
155 Node {
156 width: Val::Px(fill_width(SLIDER_DEFAULT_VALUE)),
157 height: Val::Px(FILL_HEIGHT),
158 position_type: PositionType::Absolute,
159 left: Val::Px(0.0),
160 ..default()
161 },
162 BackgroundColor(COLOR_FILL),
163 Pickable::IGNORE,
164 ));
165
166 track
167 .spawn((
168 SliderThumb,
169 Node {
170 width: Val::Px(THUMB_SIZE),
171 height: Val::Px(THUMB_SIZE),
172 position_type: PositionType::Absolute,
173 left: Val::Px(thumb_left(SLIDER_DEFAULT_VALUE)),
174 border_radius: BorderRadius::all(Val::Px(4.0)),
175 ..default()
176 },
177 BackgroundColor(COLOR_THUMB_IDLE),
178 ))
179 .observe(on_drag_start)
180 .observe(on_drag)
181 .observe(on_drag_end)
182 .observe(
183 |_: On<PointerOver>,
184 mut q: Single<&mut BackgroundColor, With<SliderThumb>>| {
185 q.0 = COLOR_THUMB_HOVER;
186 },
187 )
188 .observe(
189 |_: On<PointerOut>,
190 mut q: Single<&mut BackgroundColor, With<SliderThumb>>| {
191 q.0 = COLOR_THUMB_IDLE;
192 },
193 );
194 });
195
196 row.spawn((
197 Text::new("1.0"),
198 TextFont::from_font_size(13.0),
199 TextColor(Color::srgb(0.55, 0.55, 0.55)),
200 ));
201 });
202
203 root.spawn((
204 SliderLabel,
205 Text::new(format!("Value: {:.2}", SLIDER_DEFAULT_VALUE)),
206 TextFont::from_font_size(18.0),
207 ));
208
209 root.spawn((
210 Node {
211 flex_direction: FlexDirection::Row,
212 column_gap: Val::Px(16.0),
213 ..default()
214 },
215 Pickable::IGNORE,
216 ))
217 .with_children(|row| {
218 for label in ["Button A", "Button B", "Button C", "Button D", "Button E"] {
219 spawn_decoy_button(row, label);
220 }
221 });
222
223 root.spawn((
224 Node {
225 flex_direction: FlexDirection::Row,
226 column_gap: Val::Px(20.0),
227 align_items: AlignItems::Center,
228 ..default()
229 },
230 Pickable::IGNORE,
231 ))
232 .with_children(|row| {
233 row.spawn((
234 ToggleButton,
235 Node {
236 padding: UiRect::axes(Val::Px(20.0), Val::Px(10.0)),
237 border_radius: BorderRadius::all(Val::Px(6.0)),
238 ..default()
239 },
240 BackgroundColor(COLOR_TOGGLE_ON),
241 ))
242 .with_children(|btn| {
243 btn.spawn((
244 ToggleLabel,
245 Text::new("Capture: ON (click to toggle)"),
246 TextFont::from_font_size(16.0),
247 TextColor(Color::WHITE),
248 Pickable::IGNORE,
249 ));
250 })
251 .observe(on_toggle_click);
252
253 row.spawn((
254 ResetButton,
255 Node {
256 padding: UiRect::axes(Val::Px(20.0), Val::Px(10.0)),
257 border_radius: BorderRadius::all(Val::Px(6.0)),
258 ..default()
259 },
260 BackgroundColor(COLOR_RESET_IDLE),
261 ))
262 .with_children(|btn| {
263 btn.spawn((
264 Text::new("Reset"),
265 TextFont::from_font_size(16.0),
266 TextColor(Color::WHITE),
267 Pickable::IGNORE,
268 ));
269 })
270 .observe(
271 |_: On<PointerOver>, mut q: Single<&mut BackgroundColor, With<ResetButton>>| {
272 q.0 = COLOR_RESET_HOVER;
273 },
274 )
275 .observe(
276 |_: On<PointerOut>, mut q: Single<&mut BackgroundColor, With<ResetButton>>| {
277 q.0 = COLOR_RESET_IDLE;
278 },
279 )
280 .observe(on_reset_click);
281 });
282 });
283}
284
285fn spawn_decoy_button(parent: &mut ChildSpawnerCommands, label: &str) {
286 parent
287 .spawn((
288 DecoyButton,
289 Node {
290 padding: UiRect::axes(Val::Px(24.0), Val::Px(14.0)),
291 border_radius: BorderRadius::all(Val::Px(6.0)),
292 justify_content: JustifyContent::Center,
293 align_items: AlignItems::Center,
294 ..default()
295 },
296 BackgroundColor(COLOR_DECOY_IDLE),
297 ))
298 .with_child((
299 Text::new(label),
300 TextFont::from_font_size(15.0),
301 TextColor(Color::WHITE),
302 Pickable::IGNORE,
303 ))
304 .observe(
305 |event: On<PointerOver>, mut colors: Query<&mut BackgroundColor, With<DecoyButton>>| {
306 if let Ok(mut color) = colors.get_mut(event.entity) {
307 color.0 = COLOR_DECOY_HOVER;
308 }
309 },
310 )
311 .observe(
312 |event: On<PointerOut>, mut colors: Query<&mut BackgroundColor, With<DecoyButton>>| {
313 if let Ok(mut color) = colors.get_mut(event.entity) {
314 color.0 = COLOR_DECOY_IDLE;
315 }
316 },
317 );
318}
319
320fn on_drag_start(
321 event: On<PointerDragStart>,
322 capture_enabled: Res<CaptureEnabled>,
323 mut capture_map: ResMut<PointerCaptureMap>,
324 mut fill_color: Single<&mut BackgroundColor, With<SliderFill>>,
325) {
326 if capture_enabled.0 {
327 capture_map.capture(event.pointer.id, event.entity, event.hit.clone());
328 }
329 fill_color.0 = COLOR_FILL_DRAGGING;
331}
332
333fn on_drag(event: On<PointerDrag>, mut value: ResMut<SliderValue>) {
334 value.0 = (value.0 + event.delta.x / (TRACK_WIDTH - THUMB_SIZE)).clamp(0.0, 1.0);
335}
336
337fn on_drag_end(
338 event: On<PointerDragEnd>,
339 mut capture_map: ResMut<PointerCaptureMap>,
340 mut fill_color: Single<&mut BackgroundColor, With<SliderFill>>,
341) {
342 capture_map.release(event.pointer.id);
343 fill_color.0 = COLOR_FILL;
344}
345
346fn on_toggle_click(
347 _: On<PointerClick>,
348 mut capture_enabled: ResMut<CaptureEnabled>,
349 mut label: Single<&mut Text, With<ToggleLabel>>,
350 mut toggle_bg: Single<&mut BackgroundColor, With<ToggleButton>>,
351) {
352 capture_enabled.0 = !capture_enabled.0;
353 if capture_enabled.0 {
354 label.0 = "Capture: ON (click to toggle)".into();
355 toggle_bg.0 = COLOR_TOGGLE_ON;
356 } else {
357 label.0 = "Capture: OFF (click to toggle)".into();
358 toggle_bg.0 = COLOR_TOGGLE_OFF;
359 }
360}
361
362fn on_reset_click(_: On<PointerClick>, mut value: ResMut<SliderValue>) {
363 value.0 = SLIDER_DEFAULT_VALUE;
364}
365
366fn sync_thumb_and_label(
368 value: Res<SliderValue>,
369 mut thumb: Single<&mut Node, With<SliderThumb>>,
370 mut value_label: Single<&mut Text, With<SliderLabel>>,
371) {
372 thumb.left = Val::Px(thumb_left(value.0));
373 value_label.0 = format!("Value: {:.2}", value.0);
374}
375
376fn sync_fill(value: Res<SliderValue>, mut fill: Single<&mut Node, With<SliderFill>>) {
378 fill.width = Val::Px(fill_width(value.0));
379}