1use core::ops::RangeInclusive;
2
3use accesskit::{Orientation, Role};
4use bevy_a11y::AccessibilityNode;
5use bevy_app::{App, Plugin};
6use bevy_ecs::event::EntityEvent;
7use bevy_ecs::hierarchy::Children;
8use bevy_ecs::lifecycle::Insert;
9use bevy_ecs::query::Has;
10use bevy_ecs::system::Res;
11use bevy_ecs::world::DeferredWorld;
12use bevy_ecs::{
13 component::Component,
14 observer::On,
15 query::With,
16 reflect::ReflectComponent,
17 system::{Commands, Query},
18};
19use bevy_input::keyboard::{KeyCode, KeyboardInput};
20use bevy_input::ButtonState;
21use bevy_input_focus::FocusedInput;
22use bevy_log::warn_once;
23use bevy_math::ops;
24use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
25use bevy_reflect::{prelude::ReflectDefault, Reflect};
26use bevy_ui::{
27 ComputedNode, ComputedUiRenderTargetInfo, InteractionDisabled, UiGlobalTransform, UiScale,
28};
29
30use crate::ValueChange;
31use bevy_ecs::entity::Entity;
32
33#[derive(Debug, Default, PartialEq, Clone, Copy, Reflect)]
35#[reflect(Clone, PartialEq, Default)]
36pub enum TrackClick {
37 #[default]
39 Drag,
40 Step,
42 Snap,
44}
45
46#[derive(Component, Debug, Default)]
76#[require(
77 AccessibilityNode(accesskit::Node::new(Role::Slider)),
78 CoreSliderDragState,
79 SliderValue,
80 SliderRange,
81 SliderStep
82)]
83pub struct Slider {
84 pub track_click: TrackClick,
86 }
88
89#[derive(Component, Debug, Default)]
91pub struct SliderThumb;
92
93#[derive(Component, Debug, Default, PartialEq, Clone, Copy)]
95#[component(immutable)]
96pub struct SliderValue(pub f32);
97
98#[derive(Component, Debug, PartialEq, Clone, Copy)]
100#[component(immutable)]
101pub struct SliderRange {
102 start: f32,
104 end: f32,
106}
107
108impl SliderRange {
109 pub fn new(start: f32, end: f32) -> Self {
111 if end < start {
112 warn_once!(
113 "Expected SliderRange::start ({}) <= SliderRange::end ({})",
114 start,
115 end
116 );
117 }
118 Self { start, end }
119 }
120
121 pub fn from_range(range: RangeInclusive<f32>) -> Self {
123 let (start, end) = range.into_inner();
124 Self { start, end }
125 }
126
127 pub fn start(&self) -> f32 {
129 self.start
130 }
131
132 pub fn with_start(&self, start: f32) -> Self {
134 Self::new(start, self.end)
135 }
136
137 pub fn end(&self) -> f32 {
139 self.end
140 }
141
142 pub fn with_end(&self, end: f32) -> Self {
144 Self::new(self.start, end)
145 }
146
147 pub fn span(&self) -> f32 {
149 self.end - self.start
150 }
151
152 pub fn center(&self) -> f32 {
154 (self.start + self.end) / 2.0
155 }
156
157 pub fn clamp(&self, value: f32) -> f32 {
159 value.clamp(self.start, self.end)
160 }
161
162 pub fn thumb_position(&self, value: f32) -> f32 {
165 if self.end > self.start {
166 (value - self.start) / (self.end - self.start)
167 } else {
168 0.5
169 }
170 }
171}
172
173impl Default for SliderRange {
174 fn default() -> Self {
175 Self {
176 start: 0.0,
177 end: 1.0,
178 }
179 }
180}
181
182#[derive(Component, Debug, PartialEq, Clone)]
185#[component(immutable)]
186#[derive(Reflect)]
187#[reflect(Component)]
188pub struct SliderStep(pub f32);
189
190impl Default for SliderStep {
191 fn default() -> Self {
192 Self(1.0)
193 }
194}
195
196#[derive(Component, Debug, Default, Clone, Copy, Reflect)]
206#[reflect(Component, Default)]
207pub struct SliderPrecision(pub i32);
208
209impl SliderPrecision {
210 fn round(&self, value: f32) -> f32 {
211 let factor = ops::powf(10.0_f32, self.0 as f32);
212 (value * factor).round() / factor
213 }
214}
215
216#[derive(Component, Default, Reflect)]
218#[reflect(Component)]
219pub struct CoreSliderDragState {
220 pub dragging: bool,
222
223 offset: f32,
225}
226
227pub(crate) fn slider_on_pointer_down(
228 mut press: On<Pointer<Press>>,
229 q_slider: Query<(
230 &Slider,
231 &SliderValue,
232 &SliderRange,
233 &SliderStep,
234 Option<&SliderPrecision>,
235 &ComputedNode,
236 &ComputedUiRenderTargetInfo,
237 &UiGlobalTransform,
238 Has<InteractionDisabled>,
239 )>,
240 q_thumb: Query<&ComputedNode, With<SliderThumb>>,
241 q_children: Query<&Children>,
242 mut commands: Commands,
243 ui_scale: Res<UiScale>,
244) {
245 if q_thumb.contains(press.entity) {
246 press.propagate(false);
248 } else if let Ok((
249 slider,
250 value,
251 range,
252 step,
253 precision,
254 node,
255 node_target,
256 transform,
257 disabled,
258 )) = q_slider.get(press.entity)
259 {
260 press.propagate(false);
262
263 if disabled {
264 return;
265 }
266
267 let thumb_size = q_children
269 .iter_descendants(press.entity)
270 .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
271 .unwrap_or(0.0);
272
273 let local_pos = transform.try_inverse().unwrap().transform_point2(
275 press.pointer_location.position * node_target.scale_factor() / ui_scale.0,
276 );
277 let track_width = node.size().x - thumb_size;
278 let click_val = if track_width > 0. {
280 local_pos.x * range.span() / track_width + range.center()
281 } else {
282 0.
283 };
284
285 let new_value = range.clamp(match slider.track_click {
287 TrackClick::Drag => {
288 return;
289 }
290 TrackClick::Step => {
291 if click_val < value.0 {
292 value.0 - step.0
293 } else {
294 value.0 + step.0
295 }
296 }
297 TrackClick::Snap => precision
298 .map(|prec| prec.round(click_val))
299 .unwrap_or(click_val),
300 });
301
302 commands.trigger(ValueChange {
303 source: press.entity,
304 value: new_value,
305 });
306 }
307}
308
309pub(crate) fn slider_on_drag_start(
310 mut drag_start: On<Pointer<DragStart>>,
311 mut q_slider: Query<
312 (
313 &SliderValue,
314 &mut CoreSliderDragState,
315 Has<InteractionDisabled>,
316 ),
317 With<Slider>,
318 >,
319) {
320 if let Ok((value, mut drag, disabled)) = q_slider.get_mut(drag_start.entity) {
321 drag_start.propagate(false);
322 if !disabled {
323 drag.dragging = true;
324 drag.offset = value.0;
325 }
326 }
327}
328
329pub(crate) fn slider_on_drag(
330 mut event: On<Pointer<Drag>>,
331 mut q_slider: Query<
332 (
333 &ComputedNode,
334 &SliderRange,
335 Option<&SliderPrecision>,
336 &UiGlobalTransform,
337 &mut CoreSliderDragState,
338 Has<InteractionDisabled>,
339 ),
340 With<Slider>,
341 >,
342 q_thumb: Query<&ComputedNode, With<SliderThumb>>,
343 q_children: Query<&Children>,
344 mut commands: Commands,
345 ui_scale: Res<UiScale>,
346) {
347 if let Ok((node, range, precision, transform, drag, disabled)) = q_slider.get_mut(event.entity)
348 {
349 event.propagate(false);
350 if drag.dragging && !disabled {
351 let mut distance = event.distance / ui_scale.0;
352 distance.y *= -1.;
353 let distance = transform.transform_vector2(distance);
354 let thumb_size = q_children
356 .iter_descendants(event.entity)
357 .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
358 .unwrap_or(0.0);
359 let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
360 let span = range.span();
361 let new_value = if span > 0. {
362 drag.offset + (distance.x * span) / slider_width
363 } else {
364 range.start() + span * 0.5
365 };
366 let rounded_value = range.clamp(
367 precision
368 .map(|prec| prec.round(new_value))
369 .unwrap_or(new_value),
370 );
371
372 commands.trigger(ValueChange {
373 source: event.entity,
374 value: rounded_value,
375 });
376 }
377 }
378}
379
380pub(crate) fn slider_on_drag_end(
381 mut drag_end: On<Pointer<DragEnd>>,
382 mut q_slider: Query<(&Slider, &mut CoreSliderDragState)>,
383) {
384 if let Ok((_slider, mut drag)) = q_slider.get_mut(drag_end.entity) {
385 drag_end.propagate(false);
386 if drag.dragging {
387 drag.dragging = false;
388 }
389 }
390}
391
392fn slider_on_key_input(
393 mut focused_input: On<FocusedInput<KeyboardInput>>,
394 q_slider: Query<
395 (
396 &SliderValue,
397 &SliderRange,
398 &SliderStep,
399 Has<InteractionDisabled>,
400 ),
401 With<Slider>,
402 >,
403 mut commands: Commands,
404) {
405 if let Ok((value, range, step, disabled)) = q_slider.get(focused_input.focused_entity) {
406 let input_event = &focused_input.input;
407 if !disabled && input_event.state == ButtonState::Pressed {
408 let new_value = match input_event.key_code {
409 KeyCode::ArrowLeft => range.clamp(value.0 - step.0),
410 KeyCode::ArrowRight => range.clamp(value.0 + step.0),
411 KeyCode::Home => range.start(),
412 KeyCode::End => range.end(),
413 _ => {
414 return;
415 }
416 };
417 focused_input.propagate(false);
418 commands.trigger(ValueChange {
419 source: focused_input.focused_entity,
420 value: new_value,
421 });
422 }
423 }
424}
425
426pub(crate) fn slider_on_insert(insert: On<Insert, Slider>, mut world: DeferredWorld) {
427 let mut entity = world.entity_mut(insert.entity);
428 if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
429 accessibility.set_orientation(Orientation::Horizontal);
430 }
431}
432
433pub(crate) fn slider_on_insert_value(insert: On<Insert, SliderValue>, mut world: DeferredWorld) {
434 let mut entity = world.entity_mut(insert.entity);
435 let value = entity.get::<SliderValue>().unwrap().0;
436 if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
437 accessibility.set_numeric_value(value.into());
438 }
439}
440
441pub(crate) fn slider_on_insert_range(insert: On<Insert, SliderRange>, mut world: DeferredWorld) {
442 let mut entity = world.entity_mut(insert.entity);
443 let range = *entity.get::<SliderRange>().unwrap();
444 if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
445 accessibility.set_min_numeric_value(range.start().into());
446 accessibility.set_max_numeric_value(range.end().into());
447 }
448}
449
450pub(crate) fn slider_on_insert_step(insert: On<Insert, SliderStep>, mut world: DeferredWorld) {
451 let mut entity = world.entity_mut(insert.entity);
452 let step = entity.get::<SliderStep>().unwrap().0;
453 if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
454 accessibility.set_numeric_value_step(step.into());
455 }
456}
457
458#[derive(EntityEvent, Clone)]
489pub struct SetSliderValue {
490 pub entity: Entity,
492 pub change: SliderValueChange,
494}
495
496#[derive(Clone)]
498pub enum SliderValueChange {
499 Absolute(f32),
501 Relative(f32),
503 RelativeStep(f32),
505}
506
507fn slider_on_set_value(
508 set_slider_value: On<SetSliderValue>,
509 q_slider: Query<(&SliderValue, &SliderRange, Option<&SliderStep>), With<Slider>>,
510 mut commands: Commands,
511) {
512 if let Ok((value, range, step)) = q_slider.get(set_slider_value.entity) {
513 let new_value = match set_slider_value.change {
514 SliderValueChange::Absolute(new_value) => range.clamp(new_value),
515 SliderValueChange::Relative(delta) => range.clamp(value.0 + delta),
516 SliderValueChange::RelativeStep(delta) => {
517 range.clamp(value.0 + delta * step.map(|s| s.0).unwrap_or_default())
518 }
519 };
520 commands.trigger(ValueChange {
521 source: set_slider_value.entity,
522 value: new_value,
523 });
524 }
525}
526
527pub fn slider_self_update(value_change: On<ValueChange<f32>>, mut commands: Commands) {
531 commands
532 .entity(value_change.source)
533 .insert(SliderValue(value_change.value));
534}
535
536pub struct SliderPlugin;
538
539impl Plugin for SliderPlugin {
540 fn build(&self, app: &mut App) {
541 app.add_observer(slider_on_pointer_down)
542 .add_observer(slider_on_drag_start)
543 .add_observer(slider_on_drag_end)
544 .add_observer(slider_on_drag)
545 .add_observer(slider_on_key_input)
546 .add_observer(slider_on_insert)
547 .add_observer(slider_on_insert_value)
548 .add_observer(slider_on_insert_range)
549 .add_observer(slider_on_insert_step)
550 .add_observer(slider_on_set_value);
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_slider_precision_rounding() {
560 let precision_2dp = SliderPrecision(2);
562 assert_eq!(precision_2dp.round(1.234567), 1.23);
563 assert_eq!(precision_2dp.round(1.235), 1.24);
564
565 let precision_0dp = SliderPrecision(0);
567 assert_eq!(precision_0dp.round(1.4), 1.0);
568
569 let precision_neg1 = SliderPrecision(-1);
571 assert_eq!(precision_neg1.round(14.0), 10.0);
572 }
573}