1use denise::Pen;
4use denise::{ElementState, InputEvent, KeyCode, Point, Rect, Role, Theme};
5
6use crate::widget::{Event, EventCtx, Handled, PaintCtx, VisualState, Widget};
7use crate::widgets::describe::{
8 Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, ROLES, Value,
9};
10use crate::widgets::style::{focus_ring, interactive_pair};
11
12#[derive(Clone, Debug)]
49pub struct Slider<M> {
50 min: f32,
51 max: f32,
52 value: f32,
53 step: Option<f32>,
54 dragging: bool,
55 message: Option<fn(f32) -> M>,
56 role: Role,
57}
58
59impl<M> Slider<M> {
60 pub fn new(min: f32, max: f32, value: f32, message: fn(f32) -> M) -> Self {
65 let (min, max) = order(min, max);
66 Self {
67 min,
68 max,
69 value: clamp(min, max, value),
70 step: None,
71 dragging: false,
72 message: Some(message),
73 role: Role::Primary,
74 }
75 }
76
77 pub fn inert(min: f32, max: f32, value: f32) -> Self {
80 let (min, max) = order(min, max);
81 Self {
82 min,
83 max,
84 value: clamp(min, max, value),
85 step: None,
86 dragging: false,
87 message: None,
88 role: Role::Primary,
89 }
90 }
91
92 pub fn with_step(mut self, step: f32) -> Self {
98 self.step = (step.is_finite() && step > 0.0).then_some(step);
99 self.value = self.settle(self.value);
100 self
101 }
102
103 pub fn with_role(mut self, role: Role) -> Self {
105 self.role = role;
106 self
107 }
108
109 #[inline]
111 pub const fn value(&self) -> f32 {
112 self.value
113 }
114
115 #[inline]
117 pub const fn range(&self) -> (f32, f32) {
118 (self.min, self.max)
119 }
120
121 pub fn set_value(&mut self, value: f32) {
128 self.value = self.settle(value);
129 }
130
131 pub fn update(&mut self, value: f32) -> bool {
133 let value = self.settle(value);
134 let changed = value != self.value;
135 self.value = value;
136 changed
137 }
138
139 pub fn set_range(&mut self, min: f32, max: f32) {
141 let (min, max) = order(min, max);
142 self.min = min;
143 self.max = max;
144 self.value = self.settle(self.value);
145 }
146
147 pub fn set_role(&mut self, role: Role) {
149 self.role = role;
150 }
151
152 #[inline]
154 pub const fn dragging(&self) -> bool {
155 self.dragging
156 }
157
158 #[inline]
160 fn span(&self) -> f32 {
161 self.max - self.min
162 }
163
164 fn settle(&self, value: f32) -> f32 {
166 let value = clamp(self.min, self.max, value);
167 let Some(step) = self.step else {
168 return value;
169 };
170 let steps = ((value - self.min) / step + 0.5) as i32;
174 clamp(self.min, self.max, self.min + steps as f32 * step)
175 }
176
177 fn fraction(&self) -> f32 {
179 let span = self.span();
180 if span <= 0.0 {
181 return 0.0;
182 }
183 ((self.value - self.min) / span).clamp(0.0, 1.0)
184 }
185
186 fn small_step(&self) -> f32 {
191 self.step.unwrap_or_else(|| self.span() / 100.0)
192 }
193
194 fn large_step(&self) -> f32 {
196 self.small_step() * 10.0
197 }
198
199 fn commit(&mut self, value: f32, ctx: &mut EventCtx<'_, M>) -> Handled {
201 let value = self.settle(value);
202 if value != self.value {
203 self.value = value;
204 if let Some(message) = self.message {
205 ctx.emit(message(value));
206 }
207 }
208 Handled::Yes
211 }
212}
213
214#[inline]
216fn order(a: f32, b: f32) -> (f32, f32) {
217 if b < a { (b, a) } else { (a, b) }
218}
219
220#[inline]
226fn clamp(min: f32, max: f32, value: f32) -> f32 {
227 if value.is_nan() {
228 min
229 } else {
230 value.clamp(min, max)
231 }
232}
233
234fn knob_size(bounds: Rect, theme: &Theme) -> i32 {
236 theme
237 .metrics
238 .size_selector
239 .min(bounds.height)
240 .min(bounds.width)
241 .max(1)
242}
243
244fn travel(bounds: Rect, diameter: i32) -> (i32, i32) {
249 let radius = diameter / 2;
250 let left = bounds.x + radius;
251 let right = bounds.right() - diameter + radius;
252 (left, right.max(left))
253}
254
255fn fraction_at(bounds: Rect, diameter: i32, x: i32) -> f32 {
257 let (left, right) = travel(bounds, diameter);
258 if right <= left {
259 return 0.0;
260 }
261 ((x - left) as f32 / (right - left) as f32).clamp(0.0, 1.0)
262}
263
264impl<M: 'static> Widget<M> for Slider<M> {
265 fn describe(&self) -> Option<&dyn DynDescribe> {
266 Some(self)
267 }
268
269 fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
270 Some(self)
271 }
272 fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
273 let bounds = ctx.bounds;
274 if bounds.is_empty() {
275 return;
276 }
277 let diameter = knob_size(bounds, ctx.theme);
278 let (left, right) = travel(bounds, diameter);
279 let centre = left + (((right - left) as f32) * self.fraction()) as i32;
280
281 let state = ctx.state.set(VisualState::PRESSED, self.dragging);
286
287 let thickness = (diameter / 4).max(2);
288 let track = Rect::new(
289 bounds.x,
290 bounds.y + (bounds.height - thickness) / 2,
291 bounds.width,
292 thickness,
293 );
294 let radius = thickness / 2;
295
296 let (unfilled, _) = interactive_pair(ctx.theme, Role::Base300, state);
297 canvas.fill_rounded_rect(track, radius, unfilled);
298
299 let (fill, rim) = interactive_pair(ctx.theme, self.role, state);
300 let filled_to = centre - track.x;
301 if filled_to > 0 {
302 let filled = Rect::new(track.x, track.y, filled_to, track.height);
303 canvas.fill_rounded_rect(filled, radius.min(filled_to / 2), fill);
304 }
305
306 let knob = Rect::new(
307 centre - diameter / 2,
308 bounds.y + (bounds.height - diameter) / 2,
309 diameter,
310 diameter,
311 );
312 canvas.fill_rounded_rect(knob, diameter / 2, fill);
313 canvas.stroke_rounded_rect(knob, diameter / 2, ctx.theme.metrics.border, rim);
317
318 if state.contains(VisualState::FOCUSED) {
319 focus_ring(
320 ctx.theme,
321 bounds,
322 ctx.theme.radius(denise::Radius::Field),
323 canvas,
324 );
325 }
326 }
327
328 fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
329 let bounds = ctx.bounds;
332 let diameter = knob_size(bounds, ctx.theme);
333 let (min, span) = (self.min, self.span());
334 let at = move |point: Point| min + fraction_at(bounds, diameter, point.x) * span;
335
336 match event {
337 Event::Input(InputEvent::PointerButton {
338 state: ElementState::Down,
339 position,
340 ..
341 })
342 | Event::Input(InputEvent::TouchDown { position, .. }) => {
343 if !bounds.contains(*position) {
344 return Handled::No;
345 }
346 self.dragging = true;
347 let value = at(*position);
348 self.commit(value, ctx)
349 }
350
351 Event::Input(InputEvent::PointerMoved { position })
355 | Event::Input(InputEvent::TouchMoved { position, .. }) => {
356 if !self.dragging {
357 return Handled::No;
358 }
359 let value = at(*position);
360 self.commit(value, ctx)
361 }
362
363 Event::Input(InputEvent::PointerButton {
364 state: ElementState::Up,
365 ..
366 })
367 | Event::Input(InputEvent::TouchUp { .. }) => {
368 if !self.dragging {
369 return Handled::No;
370 }
371 self.dragging = false;
372 Handled::Yes
373 }
374
375 Event::Input(InputEvent::Key {
376 code,
377 state: ElementState::Down,
378 ..
379 }) if ctx.state.contains(VisualState::FOCUSED) => {
380 let value = match code {
381 KeyCode::ArrowLeft | KeyCode::ArrowDown => self.value - self.small_step(),
382 KeyCode::ArrowRight | KeyCode::ArrowUp => self.value + self.small_step(),
383 KeyCode::PageDown => self.value - self.large_step(),
384 KeyCode::PageUp => self.value + self.large_step(),
385 KeyCode::Home => self.min,
386 KeyCode::End => self.max,
387 _ => return Handled::No,
388 };
389 self.commit(value, ctx)
390 }
391
392 _ => Handled::No,
393 }
394 }
395
396 fn accepts_pointer(&self) -> bool {
397 true
398 }
399
400 fn focusable(&self) -> bool {
401 true
402 }
403}
404
405impl<M> Describe for Slider<M> {
406 const KIND: &'static str = "slider";
407 const DOC: &'static str = "A value in a range, dragged along a track.";
408 const GROUP: Group = Group::Input;
409 const ICON: &'static denise::icon::Icon = &super::icons::SLIDER;
410
411 const PROPERTIES: &'static [Property] = &[
416 Property::new(
417 "min",
418 PropertyKind::Float {
419 min: f32::MIN,
420 max: f32::MAX,
421 },
422 "The low end of the range.",
423 ),
424 Property::new(
425 "max",
426 PropertyKind::Float {
427 min: f32::MIN,
428 max: f32::MAX,
429 },
430 "The high end of the range; a range given the wrong way round is put in order rather than refused.",
431 ),
432 Property::new(
433 "value",
434 PropertyKind::Float {
435 min: f32::MIN,
436 max: f32::MAX,
437 },
438 "Where the knob sits, within the range and on a step if there is one.",
439 ),
440 Property::new(
441 "step",
442 PropertyKind::Float {
443 min: 0.0,
444 max: f32::MAX,
445 },
446 "Snaps to multiples of this from `min`; continuous without it.",
447 ),
448 Property::new(
449 "on-change",
450 PropertyKind::Message(Payload::Number),
451 "Emitted with the new value when a person moves the knob.",
452 ),
453 Property::new(
454 "role",
455 PropertyKind::Enum(ROLES),
456 "Colour of the filled portion and the knob.",
457 ),
458 ];
459
460 fn get(&self, name: &str) -> Option<Value> {
461 Some(match name {
462 "min" => Value::Float(self.min),
463 "max" => Value::Float(self.max),
464 "value" => Value::Float(self.value),
465 "step" => Value::Float(self.step?),
468 "role" => Value::role(self.role),
469 _ => return None,
472 })
473 }
474
475 fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
476 match name {
477 "min" => self.set_range(value.as_float()?, self.max),
482 "max" => self.set_range(self.min, value.as_float()?),
483 "value" => self.set_value(value.as_float()?),
484 "step" => {
485 let step = value.as_float()?;
489 self.step = (step.is_finite() && step > 0.0).then_some(step);
490 self.set_value(self.value);
491 }
492 "role" => self.role = value.as_role()?,
493 "on-change" => return Err(Mismatch::Supplied),
494 _ => return Err(Mismatch::Unknown),
495 }
496 Ok(())
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use denise::theme;
504
505 fn slider() -> Slider<f32> {
506 Slider::new(0.0, 100.0, 50.0, |value| value)
507 }
508
509 #[test]
511 fn a_reversed_range_is_put_the_right_way_round() {
512 let slider: Slider<f32> = Slider::new(30.0, 16.0, 21.0, |v| v);
513 assert_eq!(slider.range(), (16.0, 30.0));
514 assert_eq!(slider.value(), 21.0);
515 }
516
517 #[test]
519 fn an_empty_range_pins_the_value_and_does_not_divide_by_zero() {
520 let mut slider: Slider<f32> = Slider::new(5.0, 5.0, 5.0, |v| v);
521 assert_eq!(slider.fraction(), 0.0);
522 slider.set_value(99.0);
523 assert_eq!(slider.value(), 5.0);
524 assert!(slider.small_step().is_finite() || slider.small_step() == 0.0);
525 }
526
527 #[test]
529 fn a_value_that_is_not_a_number_lands_at_the_low_end() {
530 let done = core::hint::black_box(0.0f32);
531 let total = core::hint::black_box(0.0f32);
532 let mut slider = slider();
533 slider.set_value(done / total);
534 assert_eq!(slider.value(), 0.0);
535 assert_eq!(slider.fraction(), 0.0);
536 }
537
538 #[test]
539 fn values_outside_the_range_are_clamped() {
540 let mut slider = slider();
541 slider.set_value(1e9);
542 assert_eq!(slider.value(), 100.0);
543 slider.set_value(-1e9);
544 assert_eq!(slider.value(), 0.0);
545 slider.set_value(f32::NEG_INFINITY);
546 assert_eq!(slider.value(), 0.0);
547 }
548
549 #[test]
553 fn a_step_snaps_to_multiples_from_the_low_end() {
554 let mut slider: Slider<f32> = Slider::new(16.0, 30.0, 21.0, |v| v).with_step(0.5);
555 slider.set_value(21.3);
556 assert_eq!(slider.value(), 21.5);
557 slider.set_value(21.2);
558 assert_eq!(slider.value(), 21.0);
559
560 slider.set_value(29.9);
562 assert!(slider.value() <= 30.0);
563 slider.set_value(16.1);
564 assert!(slider.value() >= 16.0);
565 }
566
567 #[test]
569 fn a_nonsense_step_is_ignored() {
570 for step in [0.0, -1.0, f32::NAN, f32::INFINITY] {
571 let slider: Slider<f32> = Slider::new(0.0, 10.0, 3.3, |v| v).with_step(step);
572 assert_eq!(slider.value(), 3.3, "step {step} should have been ignored");
573 }
574 }
575
576 #[test]
579 fn the_knob_stays_inside_the_rectangle_across_the_whole_travel() {
580 for width in [1, 2, 21, 200, 1920] {
581 let bounds = Rect::new(7, 3, width, 40);
582 let diameter = knob_size(bounds, &theme::DARK);
583 let (left, right) = travel(bounds, diameter);
584 assert!(right >= left, "width {width}: travel is inverted");
585 for centre in [left, (left + right) / 2, right] {
586 let knob = Rect::new(centre - diameter / 2, bounds.y, diameter, diameter);
587 assert!(
588 knob.x >= bounds.x && knob.right() <= bounds.right(),
589 "width {width}: knob at {centre} escaped {bounds:?}"
590 );
591 }
592 }
593 }
594
595 #[test]
599 fn a_pointer_past_either_end_clamps_rather_than_wrapping() {
600 let bounds = Rect::new(10, 0, 200, 40);
601 let diameter = knob_size(bounds, &theme::DARK);
602
603 assert_eq!(fraction_at(bounds, diameter, -100_000), 0.0);
604 assert_eq!(fraction_at(bounds, diameter, 100_000), 1.0);
605 assert_eq!(fraction_at(bounds, diameter, bounds.x - 1), 0.0);
606 assert_eq!(fraction_at(bounds, diameter, bounds.right() + 1), 1.0);
607
608 let mut previous = -1.0;
610 for x in bounds.x..bounds.right() {
611 let fraction = fraction_at(bounds, diameter, x);
612 assert!(fraction >= previous, "went backwards at x {x}");
613 previous = fraction;
614 }
615 }
616
617 #[test]
620 fn a_rectangle_with_no_travel_answers_zero() {
621 let bounds = Rect::new(0, 0, 4, 40);
622 let diameter = knob_size(bounds, &theme::DARK);
623 assert_eq!(fraction_at(bounds, diameter, 2), 0.0);
624 assert_eq!(fraction_at(bounds, diameter, 1000), 0.0);
625 }
626
627 #[test]
630 fn the_keyboard_steps_are_a_hundredth_and_a_tenth_of_the_range() {
631 let slider = slider();
632 assert_eq!(slider.small_step(), 1.0);
633 assert_eq!(slider.large_step(), 10.0);
634
635 let stepped: Slider<f32> = Slider::new(0.0, 10.0, 0.0, |v| v).with_step(0.25);
637 assert_eq!(stepped.small_step(), 0.25);
638 assert_eq!(stepped.large_step(), 2.5);
639 }
640
641 #[test]
644 fn the_knob_rim_is_visible_against_the_knob_in_every_theme_and_state() {
645 use denise::theme::{AA_LARGE, contrast_x100};
646
647 for theme in Theme::BUILT_IN {
648 for state in [
649 VisualState::NONE,
650 VisualState::HOVERED,
651 VisualState::PRESSED,
652 VisualState::DISABLED,
653 VisualState::FOCUSED,
654 ] {
655 let (fill, rim) = interactive_pair(&theme, Role::Primary, state);
656 let ratio = contrast_x100(fill, rim);
657 assert!(
658 ratio >= AA_LARGE,
659 "{} {state:?}: rim against knob is {ratio}, floor is {AA_LARGE}",
660 theme.name
661 );
662 }
663 }
664 }
665}