1use std::cell::Cell;
13use std::rc::Rc;
14use std::sync::Arc;
15
16use crate::draw_ctx::DrawCtx;
17use crate::event::{Event, EventResult, Key, MouseButton};
18use crate::geometry::{Rect, Size};
19use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
20use crate::text::Font;
21use crate::widget::{paint_subtree, Widget};
22use crate::widgets::label::{Label, LabelAlign};
23use crate::widgets::slider_math::{
24 best_in_range_f64, clamp_value_to_range, normalized_from_value, value_from_normalized,
25 SliderSpec,
26};
27
28mod paint;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum SliderOrientation {
33 Horizontal,
34 Vertical,
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub enum SliderClamping {
40 Never,
42 Edits,
45 #[default]
47 Always,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum HandleShape {
53 Circle,
55 Rect { aspect_ratio: f64 },
58}
59
60impl Default for HandleShape {
61 fn default() -> Self {
62 Self::Circle
63 }
64}
65
66const AIM_RADIUS: f64 = 1.5;
72const VERT_LEN: f64 = 140.0;
74
75const TRACK_H: f64 = 4.0;
76const THUMB_R: f64 = 7.0;
77const WIDGET_H: f64 = 22.0;
81const VALUE_W: f64 = 44.0;
85const VALUE_GAP: f64 = 6.0;
87
88#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
103#[derive(Clone, Debug)]
104pub struct SliderProps {
105 pub value: f64,
106 pub min: f64,
107 pub max: f64,
108 pub step: f64,
109 pub show_value: bool,
110 pub decimals: Option<usize>,
113 pub font_size: f64,
114}
115
116impl Default for SliderProps {
117 fn default() -> Self {
118 Self {
119 value: 0.0,
120 min: 0.0,
121 max: 1.0,
122 step: 0.01,
123 show_value: true,
124 decimals: None,
125 font_size: 12.0,
126 }
127 }
128}
129
130pub struct Slider {
132 bounds: Rect,
133 children: Vec<Box<dyn Widget>>, base: WidgetBase,
135 pub props: SliderProps,
137 dragging: bool,
138 focused: bool,
139 hovered: bool,
140 on_change: Option<Box<dyn FnMut(f64)>>,
141 value_cell: Option<Rc<Cell<f64>>>,
147 value_label: Label,
151 last_value_text: String,
154
155 spec: SliderSpec,
158 clamping: SliderClamping,
159 smart_aim: bool,
160 orientation: SliderOrientation,
161 handle_shape: HandleShape,
162 integer: bool,
164 suffix: String,
166 trailing_fill: bool,
170}
171
172impl Slider {
173 pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
174 let v = clamp_value_to_range(value, min, max);
177 let default_step = {
182 let s = (max - min) / 100.0;
183 if s.is_finite() {
184 s
185 } else {
186 0.0
187 }
188 };
189 let font_size = 12.0;
190 let value_label = Label::new("", Arc::clone(&font))
191 .with_font_size(font_size)
192 .with_align(LabelAlign::Right);
193 Self {
194 bounds: Rect::default(),
195 children: Vec::new(),
196 base: WidgetBase::new(),
197 props: SliderProps {
198 value: v,
199 min,
200 max,
201 step: default_step,
202 show_value: true,
203 decimals: None,
204 font_size,
205 },
206 dragging: false,
207 focused: false,
208 hovered: false,
209 on_change: None,
210 value_cell: None,
211 value_label,
212 last_value_text: String::new(),
213 spec: SliderSpec::default(),
214 clamping: SliderClamping::default(),
215 smart_aim: true,
216 orientation: SliderOrientation::Horizontal,
217 handle_shape: HandleShape::Circle,
218 integer: false,
219 suffix: String::new(),
220 trailing_fill: true,
221 }
222 }
223
224 pub fn with_step(mut self, step: f64) -> Self {
225 self.props.step = step;
226 self
227 }
228
229 pub fn with_logarithmic(mut self, logarithmic: bool) -> Self {
234 self.spec.logarithmic = logarithmic;
235 self
236 }
237
238 pub fn with_smallest_positive(mut self, smallest_positive: f64) -> Self {
241 self.spec.smallest_positive = smallest_positive;
242 self
243 }
244
245 pub fn with_largest_finite(mut self, largest_finite: f64) -> Self {
248 self.spec.largest_finite = largest_finite;
249 self
250 }
251
252 pub fn with_clamping(mut self, clamping: SliderClamping) -> Self {
254 self.clamping = clamping;
255 self
256 }
257
258 pub fn with_smart_aim(mut self, smart_aim: bool) -> Self {
260 self.smart_aim = smart_aim;
261 self
262 }
263
264 pub fn with_orientation(mut self, orientation: SliderOrientation) -> Self {
266 self.orientation = orientation;
267 self
268 }
269
270 pub fn with_handle_shape(mut self, shape: HandleShape) -> Self {
272 self.handle_shape = shape;
273 self
274 }
275
276 pub fn with_integer(mut self, integer: bool) -> Self {
280 self.integer = integer;
281 if integer {
282 self.spec.smallest_positive = 1.0;
283 }
284 self
285 }
286
287 pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
289 self.suffix = suffix.into();
290 self
291 }
292
293 pub fn with_trailing_fill(mut self, trailing_fill: bool) -> Self {
295 self.trailing_fill = trailing_fill;
296 self
297 }
298
299 pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
306 self.props.value = clamp_value_to_range(cell.get(), self.props.min, self.props.max);
308 self.value_cell = Some(cell);
309 self
310 }
311 pub fn with_show_value(mut self, show: bool) -> Self {
312 self.props.show_value = show;
313 self
314 }
315
316 pub fn with_decimals(mut self, decimals: usize) -> Self {
319 self.props.decimals = Some(decimals);
320 self
321 }
322
323 pub fn with_margin(mut self, m: Insets) -> Self {
324 self.base.margin = m;
325 self
326 }
327 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
328 self.base.h_anchor = h;
329 self
330 }
331 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
332 self.base.v_anchor = v;
333 self
334 }
335 pub fn with_min_size(mut self, s: Size) -> Self {
336 self.base.min_size = s;
337 self
338 }
339 pub fn with_max_size(mut self, s: Size) -> Self {
340 self.base.max_size = s;
341 self
342 }
343
344 pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
345 self.on_change = Some(Box::new(cb));
346 self
347 }
348
349 pub fn value(&self) -> f64 {
350 self.props.value
351 }
352
353 pub fn set_value(&mut self, v: f64) {
354 if v.is_nan() {
357 return;
358 }
359 self.props.value = self.commit(v);
360 if let Some(cell) = &self.value_cell {
361 cell.set(self.props.value);
362 }
363 }
364
365 fn fire(&mut self) {
366 let v = self.props.value;
367 if let Some(cell) = &self.value_cell {
368 cell.set(v);
369 }
370 if let Some(cb) = self.on_change.as_mut() {
371 cb(v);
372 }
373 }
374
375 fn is_vertical(&self) -> bool {
376 matches!(self.orientation, SliderOrientation::Vertical)
377 }
378
379 fn handle_extent(&self) -> f64 {
382 match self.handle_shape {
383 HandleShape::Circle => THUMB_R,
384 HandleShape::Rect { aspect_ratio } => THUMB_R * aspect_ratio,
385 }
386 }
387
388 fn track_right(&self) -> f64 {
392 let reserved = if self.props.show_value {
393 VALUE_W + VALUE_GAP
394 } else {
395 0.0
396 };
397 (self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
398 }
399
400 fn position_range(&self) -> (f64, f64) {
403 let hr = self.handle_extent();
404 if self.is_vertical() {
405 let bottom = self.bounds.height - hr; let top = hr; (bottom, top)
408 } else {
409 (THUMB_R, self.track_right()) }
411 }
412
413 fn normalized(&self) -> f64 {
414 normalized_from_value(self.props.value, self.props.min, self.props.max, &self.spec)
415 }
416
417 fn thumb_pos(&self) -> f64 {
419 let (p0, p1) = self.position_range();
420 p0 + self.normalized().clamp(0.0, 1.0) * (p1 - p0)
421 }
422
423 fn value_from_pixel(&self, px: f64) -> f64 {
424 let (p0, p1) = self.position_range();
425 let n = if (p1 - p0).abs() < f64::EPSILON {
426 0.0
427 } else {
428 ((px - p0) / (p1 - p0)).clamp(0.0, 1.0)
429 };
430 value_from_normalized(n, self.props.min, self.props.max, &self.spec)
431 }
432
433 fn commit(&self, mut value: f64) -> f64 {
439 if value.is_nan() {
440 return self.props.value;
441 }
442 if self.clamping != SliderClamping::Never {
443 value = clamp_value_to_range(value, self.props.min, self.props.max);
444 }
445 if self.props.step > 0.0 && self.props.step.is_finite() && self.props.min.is_finite() {
449 let start = self.props.min;
450 value = start + ((value - start) / self.props.step).round() * self.props.step;
451 }
452 if self.integer {
453 value = value.round();
454 }
455 value
456 }
457
458 fn pointer_value(&self, px: f64) -> f64 {
460 let raw = if self.smart_aim {
461 best_in_range_f64(
462 self.value_from_pixel(px - AIM_RADIUS),
463 self.value_from_pixel(px + AIM_RADIUS),
464 )
465 } else {
466 self.value_from_pixel(px)
467 };
468 self.commit(raw)
469 }
470
471 fn nudge(&self, dir: f64) -> f64 {
473 if self.props.step > 0.0 {
474 return self.commit(self.props.value + dir * self.props.step);
475 }
476 let (p0, p1) = self.position_range();
477 let cur = self.thumb_pos();
478 let px = cur + dir * (p1 - p0).signum();
479 let raw = if self.smart_aim {
480 best_in_range_f64(
481 self.value_from_pixel(px - 0.49),
482 self.value_from_pixel(px + 0.49),
483 )
484 } else {
485 self.value_from_pixel(px)
486 };
487 self.commit(raw)
488 }
489
490 fn auto_decimals(&self) -> usize {
492 if self.integer {
493 return 0;
494 }
495 if self.props.step >= 1.0 {
496 0
497 } else if self.props.step >= 0.1 {
498 1
499 } else if self.props.step >= 0.01 {
500 2
501 } else if self.props.step > 0.0 {
502 3
503 } else {
504 let v = self.props.value.abs();
507 if v == 0.0 || !v.is_finite() {
508 2
509 } else {
510 (3 - v.log10().floor() as i32).clamp(0, 6) as usize
511 }
512 }
513 }
514
515 fn format_value(&self) -> String {
517 let decimals = self.props.decimals.unwrap_or_else(|| self.auto_decimals());
518 format!("{:.*}{}", decimals, self.props.value, self.suffix)
519 }
520
521}
522
523impl Widget for Slider {
524 fn type_name(&self) -> &'static str {
525 "Slider"
526 }
527 fn bounds(&self) -> Rect {
528 self.bounds
529 }
530 fn set_bounds(&mut self, b: Rect) {
531 self.bounds = b;
532 }
533 fn children(&self) -> &[Box<dyn Widget>] {
534 &self.children
535 }
536 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
537 &mut self.children
538 }
539
540 #[cfg(feature = "reflect")]
541 fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
542 Some(&self.props)
543 }
544 #[cfg(feature = "reflect")]
545 fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
546 Some(&mut self.props)
547 }
548
549 fn is_focusable(&self) -> bool {
550 true
551 }
552
553 fn margin(&self) -> Insets {
554 self.base.margin
555 }
556 fn widget_base(&self) -> Option<&WidgetBase> {
557 Some(&self.base)
558 }
559 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
560 Some(&mut self.base)
561 }
562 fn h_anchor(&self) -> HAnchor {
563 self.base.h_anchor
564 }
565 fn v_anchor(&self) -> VAnchor {
566 self.base.v_anchor
567 }
568 fn min_size(&self) -> Size {
569 self.base.min_size
570 }
571 fn max_size(&self) -> Size {
572 self.base.max_size
573 }
574
575 fn layout(&mut self, available: Size) -> Size {
576 if !self.dragging {
581 if let Some(cell) = &self.value_cell {
582 let raw = cell.get();
583 if !raw.is_nan() {
586 self.props.value = if self.clamping == SliderClamping::Always {
587 clamp_value_to_range(raw, self.props.min, self.props.max)
588 } else {
589 raw
590 };
591 }
592 }
593 }
594
595 if self.props.show_value {
600 let new_text = self.format_value();
601 if new_text != self.last_value_text {
602 self.value_label.set_text(new_text.clone());
603 self.last_value_text = new_text;
604 }
605 let lh = self.props.font_size * 1.5;
608 let _ = self.value_label.layout(Size::new(VALUE_W, lh));
609 self.value_label
610 .set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
611 }
612
613 if self.is_vertical() {
614 Size::new(available.width, VERT_LEN)
615 } else {
616 Size::new(available.width, WIDGET_H)
617 }
618 }
619
620 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
621 if self.is_vertical() {
622 self.paint_vertical(ctx);
623 } else {
624 self.paint_horizontal(ctx);
625 }
626 }
627
628 fn on_event(&mut self, event: &Event) -> EventResult {
629 match event {
630 Event::MouseMove { pos } => {
631 let was = self.hovered;
632 self.hovered = self.hit_test(*pos);
633 if self.dragging {
634 let px = if self.is_vertical() { pos.y } else { pos.x };
635 self.props.value = self.pointer_value(px);
636 self.fire();
637 crate::animation::request_draw();
638 return EventResult::Consumed;
639 }
640 if was != self.hovered {
641 crate::animation::request_draw();
642 return EventResult::Consumed;
643 }
644 EventResult::Ignored
645 }
646 Event::MouseDown {
647 button: MouseButton::Left,
648 pos,
649 ..
650 } => {
651 self.dragging = true;
652 let px = if self.is_vertical() { pos.y } else { pos.x };
653 self.props.value = self.pointer_value(px);
654 self.fire();
655 crate::animation::request_draw();
656 EventResult::Consumed
657 }
658 Event::MouseUp {
659 button: MouseButton::Left,
660 ..
661 } => {
662 let was = self.dragging;
663 self.dragging = false;
664 if was {
665 crate::animation::request_draw();
666 }
667 EventResult::Consumed
668 }
669 Event::KeyDown { key, .. } => {
670 let dir = match (key, self.is_vertical()) {
673 (Key::ArrowLeft, false) => -1.0,
674 (Key::ArrowRight, false) => 1.0,
675 (Key::ArrowDown, true) => -1.0,
676 (Key::ArrowUp, true) => 1.0,
677 _ => 0.0,
678 };
679 if dir != 0.0 {
680 self.props.value = self.nudge(dir);
681 self.fire();
682 crate::animation::request_draw();
683 EventResult::Consumed
684 } else {
685 EventResult::Ignored
686 }
687 }
688 Event::FocusGained => {
689 let was = self.focused;
690 self.focused = true;
691 if !was {
692 crate::animation::request_draw();
693 EventResult::Consumed
694 } else {
695 EventResult::Ignored
696 }
697 }
698 Event::FocusLost => {
699 let was_focused = self.focused;
700 let was_dragging = self.dragging;
701 self.focused = false;
702 self.dragging = false;
703 if was_focused || was_dragging {
704 crate::animation::request_draw();
705 EventResult::Consumed
706 } else {
707 EventResult::Ignored
708 }
709 }
710 _ => EventResult::Ignored,
711 }
712 }
713}
714
715#[cfg(test)]
716mod tests;