1use std::ops::RangeInclusive;
4
5use iced::keyboard::key::Named;
6use iced::widget::slider::{Handle, HandleShape, Rail, Status};
7use iced::widget::{self, column, row, space, text};
8use iced::{Background, Border, Color, Element, Length, Theme};
9
10use crate::inert::inert;
11use crate::keys::{self, Chord, Keymap};
12use crate::theme::{Tokens, fade, mix, radius, space as spacing, text_size};
13
14pub const HANDLE_RADIUS: f32 = 8.0;
16
17const RAIL_WIDTH: f32 = 6.0;
18
19pub trait Value:
21 Copy + PartialOrd + From<u8> + Into<f64> + num_traits::FromPrimitive + ToString
22{
23}
24
25impl<T> Value for T where
26 T: Copy + PartialOrd + From<u8> + Into<f64> + num_traits::FromPrimitive + ToString
27{
28}
29
30pub struct Slider<'a, T, Message> {
34 range: RangeInclusive<T>,
35 value: T,
36 step: Option<T>,
37 label: Option<text::Fragment<'a>>,
38 format: Option<Box<dyn Fn(T) -> String + 'a>>,
39 width: Length,
40 on_change: Option<Box<dyn Fn(T) -> Message + 'a>>,
41 on_release: Option<Message>,
42}
43
44impl<T: std::fmt::Debug, Message> std::fmt::Debug for Slider<'_, T, Message> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("Slider")
47 .field("range", &self.range)
48 .field("value", &self.value)
49 .field("step", &self.step)
50 .field("label", &self.label)
51 .field("shows_value", &self.format.is_some())
52 .field("enabled", &self.on_change.is_some())
53 .finish()
54 }
55}
56
57pub fn slider<'a, T: Value, Message>(range: RangeInclusive<T>, value: T) -> Slider<'a, T, Message> {
60 let range = normalise(range);
61 Slider {
62 value: clamp(value, &range),
63 range,
64 step: None,
65 label: None,
66 format: None,
67 width: Length::Fill,
68 on_change: None,
69 on_release: None,
70 }
71}
72
73impl<'a, T: Value, Message> Slider<'a, T, Message> {
74 pub fn step(mut self, step: T) -> Self {
76 self.step = valid_step(step, &self.range);
77 self
78 }
79
80 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
82 self.label = Some(label.into_fragment());
83 self
84 }
85
86 pub fn show_value(self) -> Self {
88 self.format_value(|value: T| value.to_string())
89 }
90
91 pub fn format_value(mut self, format: impl Fn(T) -> String + 'a) -> Self {
93 self.format = Some(Box::new(format));
94 self
95 }
96
97 pub fn width(mut self, width: impl Into<Length>) -> Self {
98 self.width = width.into();
99 self
100 }
101
102 pub fn on_change(mut self, on_change: impl Fn(T) -> Message + 'a) -> Self {
104 self.on_change = Some(Box::new(on_change));
105 self
106 }
107
108 pub fn on_release(mut self, message: Message) -> Self {
110 self.on_release = Some(message);
111 self
112 }
113
114 pub fn value(&self) -> T {
115 self.value
116 }
117
118 pub fn range(&self) -> &RangeInclusive<T> {
119 &self.range
120 }
121
122 pub fn is_enabled(&self) -> bool {
123 self.on_change.is_some()
124 }
125}
126
127impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message>
128where
129 T: Value + 'a,
130 Message: Clone + 'a,
131{
132 fn from(slider: Slider<'a, T, Message>) -> Self {
133 let enabled = slider.is_enabled();
134 let value = slider.value;
135 let step = slider.step;
136
137 let track: Element<'a, Message> = match slider.on_change {
138 Some(on_change) => {
139 let mut track = widget::slider(slider.range, value, on_change)
140 .height(HANDLE_RADIUS * 2.0)
141 .style(|theme, status| style(&Tokens::of(theme), status, true));
142 if let Some(step) = step {
143 track = track.step(step);
144 }
145 if let Some(message) = slider.on_release {
146 track = track.on_release(message);
147 }
148 track.into()
149 }
150 None => {
151 let mut track = widget::slider(slider.range, value, |_| ())
152 .height(HANDLE_RADIUS * 2.0)
153 .style(|theme, status| style(&Tokens::of(theme), status, false));
154 if let Some(step) = step {
155 track = track.step(step);
156 }
157 inert(track.into())
158 }
159 };
160
161 let shown = slider.format.map(|format| format(value));
162 if slider.label.is_none() && shown.is_none() {
163 return widget::container(track).width(slider.width).into();
164 }
165
166 let caption = move |content: text::Fragment<'a>, muted: bool| {
167 text(content)
168 .size(text_size::SM)
169 .style(move |theme: &Theme| {
170 let tokens = Tokens::of(theme);
171 let colour = if muted {
172 tokens.muted_foreground
173 } else {
174 tokens.foreground
175 };
176 text::Style {
177 color: Some(if enabled { colour } else { fade(colour, 0.5) }),
178 }
179 })
180 };
181
182 let mut header = row![].spacing(spacing::SM);
183 if let Some(label) = slider.label {
184 header = header.push(caption(label, false));
185 }
186 header = header.push(space().width(Length::Fill));
187 if let Some(shown) = shown {
188 header = header.push(caption(shown.into(), true));
189 }
190
191 column![header, track]
192 .spacing(spacing::SM)
193 .width(slider.width)
194 .into()
195 }
196}
197
198pub fn normalise<T: PartialOrd + Copy>(range: RangeInclusive<T>) -> RangeInclusive<T> {
200 let (start, end) = (*range.start(), *range.end());
201 if start > end { end..=start } else { range }
202}
203
204pub fn clamp<T: PartialOrd + Copy>(value: T, range: &RangeInclusive<T>) -> T {
206 if value < *range.start() {
207 return *range.start();
208 }
209 if value > *range.end() {
210 return *range.end();
211 }
212 value
213}
214
215pub fn valid_step<T: Value>(step: T, range: &RangeInclusive<T>) -> Option<T> {
217 if step <= T::from(0) {
218 return None;
219 }
220 let span: f64 = (*range.end()).into() - (*range.start()).into();
221 if span > 0.0 && step.into() > span {
222 return T::from_f64(span);
223 }
224 Some(step)
225}
226
227pub fn stepped<T: Value>(value: T, range: RangeInclusive<T>, step: Option<T>, steps: i32) -> T {
231 let range = normalise(range);
232 let size: f64 = step
233 .and_then(|step| valid_step(step, &range))
234 .unwrap_or_else(|| T::from(1))
235 .into();
236 let start: f64 = (*range.start()).into();
237 let end: f64 = (*range.end()).into();
238 let current: f64 = clamp(value, &range).into();
239 if steps == 0 || size <= 0.0 {
240 return clamp(value, &range);
241 }
242
243 let position = (current - start) / size;
245 let tolerance = 1e-9;
246 let index = if steps > 0 {
247 (position + tolerance).floor() + f64::from(steps)
248 } else {
249 (position - tolerance).ceil() + f64::from(steps)
250 };
251 let target = (start + index * size).clamp(start, end);
252 T::from_f64(target).map_or(value, |target| clamp(target, &range))
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
257pub enum Action {
258 Increase,
259 Decrease,
260 Min,
261 Max,
262}
263
264impl Action {
265 pub fn apply<T: Value>(self, value: T, range: RangeInclusive<T>, step: Option<T>) -> T {
268 let range = normalise(range);
269 match self {
270 Action::Increase => stepped(value, range, step, 1),
271 Action::Decrease => stepped(value, range, step, -1),
272 Action::Min => *range.start(),
273 Action::Max => *range.end(),
274 }
275 }
276}
277
278impl keys::Action for Action {
279 const ALL: &'static [Self] = &[Action::Increase, Action::Decrease, Action::Min, Action::Max];
280
281 fn defaults() -> Keymap<Self> {
282 default_keymap()
283 }
284
285 fn name(self) -> &'static str {
286 match self {
287 Action::Increase => "Increase",
288 Action::Decrease => "Decrease",
289 Action::Min => "Min",
290 Action::Max => "Max",
291 }
292 }
293
294 fn description(self) -> &'static str {
295 match self {
296 Action::Increase => "Raises the value by one step.",
297 Action::Decrease => "Lowers the value by one step.",
298 Action::Min => "Sets the value to the start of the range.",
299 Action::Max => "Sets the value to the end of the range.",
300 }
301 }
302}
303
304pub fn default_keymap() -> Keymap<Action> {
316 Keymap::new()
317 .bind(Chord::named(Named::ArrowRight), Action::Increase)
318 .bind(Chord::named(Named::ArrowUp), Action::Increase)
319 .bind(Chord::named(Named::ArrowLeft), Action::Decrease)
320 .bind(Chord::named(Named::ArrowDown), Action::Decrease)
321 .bind(Chord::named(Named::Home), Action::Min)
322 .bind(Chord::named(Named::End), Action::Max)
323}
324
325#[derive(Debug, Clone, Copy, PartialEq)]
327pub struct Colours {
328 pub filled: Color,
329 pub empty: Color,
330 pub handle: Color,
331 pub handle_border: Color,
332}
333
334pub fn colours(tokens: &Tokens, status: Status, enabled: bool) -> Colours {
336 let handle_border = match status {
337 Status::Active => tokens.primary,
338 Status::Hovered => mix(tokens.primary, tokens.background, 0.15),
339 Status::Dragged => mix(tokens.primary, tokens.background, 0.3),
340 };
341 let colours = Colours {
342 filled: tokens.primary,
343 empty: mix(tokens.muted, tokens.border, 0.5),
344 handle: tokens.background,
345 handle_border,
346 };
347
348 if enabled {
349 return colours;
350 }
351 Colours {
352 filled: fade(colours.filled, 0.5),
353 empty: fade(colours.empty, 0.5),
354 handle: colours.handle,
355 handle_border: fade(tokens.primary, 0.5),
356 }
357}
358
359pub fn style(tokens: &Tokens, status: Status, enabled: bool) -> widget::slider::Style {
361 let colours = colours(tokens, status, enabled);
362 let border_width = match (enabled, status) {
363 (true, Status::Hovered | Status::Dragged) => 2.5,
364 _ => 1.5,
365 };
366 widget::slider::Style {
367 rail: Rail {
368 backgrounds: (
369 Background::Color(colours.filled),
370 Background::Color(colours.empty),
371 ),
372 width: RAIL_WIDTH,
373 border: Border::default().rounded(radius::FULL),
374 },
375 handle: Handle {
376 shape: HandleShape::Circle {
377 radius: HANDLE_RADIUS,
378 },
379 background: Background::Color(colours.handle),
380 border_width,
381 border_color: colours.handle_border,
382 },
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::theme::{dark, light};
390
391 const STATES: [Status; 3] = [Status::Active, Status::Hovered, Status::Dragged];
392
393 #[test]
394 fn default_builder_fills_width_and_is_disabled() {
395 let s: Slider<'_, f32, ()> = slider(0.0..=1.0, 0.5);
396 assert_eq!(s.width, Length::Fill);
397 assert!(s.step.is_none());
398 assert!(s.format.is_none());
399 assert!(!s.is_enabled());
400 assert!(s.on_change(|_| ()).is_enabled());
401 }
402
403 #[test]
404 fn value_is_clamped_to_the_range() {
405 let s: Slider<'_, i32, ()> = slider(0..=10, 42);
406 assert_eq!(s.value(), 10);
407 let s: Slider<'_, i32, ()> = slider(0..=10, -3);
408 assert_eq!(s.value(), 0);
409 let s: Slider<'_, i32, ()> = slider(0..=10, 7);
410 assert_eq!(s.value(), 7);
411 }
412
413 #[test]
414 fn reversed_range_is_normalised() {
415 let s: Slider<'_, f64, ()> = slider(10.0..=0.0, 4.0);
416 assert_eq!(s.range(), &(0.0..=10.0));
417 assert_eq!(s.value(), 4.0);
418 }
419
420 #[test]
421 fn non_positive_steps_are_ignored() {
422 assert_eq!(valid_step(0.0_f32, &(0.0..=1.0)), None);
423 assert_eq!(valid_step(-0.5_f32, &(0.0..=1.0)), None);
424 assert_eq!(valid_step(0.25_f32, &(0.0..=1.0)), Some(0.25));
425 }
426
427 #[test]
428 fn step_is_capped_at_the_range_width() {
429 assert_eq!(valid_step(50_u8, &(0..=10)), Some(10));
430 assert_eq!(valid_step(5_i32, &(3..=3)), Some(5));
431 }
432
433 #[test]
434 fn builder_step_uses_validation() {
435 let s: Slider<'_, f32, ()> = slider(0.0..=1.0, 0.5).step(0.0);
436 assert!(s.step.is_none());
437 let s: Slider<'_, f32, ()> = slider(0.0..=100.0, 0.5).step(5.0);
438 assert_eq!(s.step, Some(5.0));
439 }
440
441 #[test]
442 fn stepped_moves_along_the_grid() {
443 assert_eq!(stepped(40_u8, 0..=100, None, 1), 41);
444 assert_eq!(stepped(40_u8, 0..=100, Some(5), 1), 45);
445 assert_eq!(stepped(40_u8, 0..=100, Some(5), -2), 30);
446 assert_eq!(stepped(0.5_f32, 0.0..=1.0, Some(0.25), 1), 0.75);
447 assert_eq!(stepped(7_i32, 0..=10, Some(2), 0), 7);
448 }
449
450 #[test]
451 fn stepped_snaps_off_grid_values_in_the_direction_of_travel() {
452 assert_eq!(stepped(42_u8, 0..=100, Some(5), 1), 45);
453 assert_eq!(stepped(42_u8, 0..=100, Some(5), -1), 40);
454 assert_eq!(stepped(12_i32, 10..=20, Some(4), 1), 14);
455 }
456
457 #[test]
458 fn stepped_clamps_and_reaches_the_end() {
459 assert_eq!(stepped(98_u8, 0..=100, Some(5), 1), 100);
460 assert_eq!(stepped(100_u8, 0..=100, Some(5), 1), 100);
461 assert_eq!(stepped(0_u8, 0..=100, Some(5), -1), 0);
462 assert_eq!(stepped(9_i32, 0..=10, Some(4), 1), 10);
463 assert_eq!(stepped(5_i32, RangeInclusive::new(10, 0), None, -1), 4);
464 }
465
466 #[test]
467 fn stepped_ignores_invalid_steps() {
468 assert_eq!(stepped(5_i32, 0..=10, Some(0), 1), 6);
469 assert_eq!(stepped(5_i32, 0..=10, Some(-3), -1), 4);
470 }
471
472 #[test]
473 fn actions_apply_to_the_value() {
474 assert_eq!(Action::Increase.apply(40_u8, 0..=100, Some(10)), 50);
475 assert_eq!(Action::Decrease.apply(40_u8, 0..=100, Some(10)), 30);
476 assert_eq!(Action::Min.apply(40_u8, 20..=100, None), 20);
477 assert_eq!(
478 Action::Max.apply(40_u8, RangeInclusive::new(100, 20), None),
479 100
480 );
481 }
482
483 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
484 let chord: Chord = chord.parse().unwrap();
485 keymap.resolve(chord.key(), chord.modifiers())
486 }
487
488 #[test]
489 fn default_keymap_and_overrides() {
490 let keymap = default_keymap();
491 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Increase));
492 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Increase));
493 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Decrease));
494 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Decrease));
495 assert_eq!(press(&keymap, "Home"), Some(Action::Min));
496 assert_eq!(press(&keymap, "End"), Some(Action::Max));
497
498 let custom = keymap
499 .unbind(&"ArrowUp".parse().unwrap())
500 .bind("=".parse().unwrap(), Action::Increase);
501 assert_eq!(press(&custom, "ArrowUp"), None);
502 assert_eq!(press(&custom, "="), Some(Action::Increase));
503 assert!(custom.clear().is_empty());
504 }
505
506 #[test]
507 fn show_value_formats_with_display() {
508 let s: Slider<'_, u8, ()> = slider(0..=100, 30).show_value();
509 let shown = s.format.as_ref().map(|format| format(s.value));
510 assert_eq!(shown.as_deref(), Some("30"));
511 }
512
513 #[test]
514 fn rail_fills_with_primary() {
515 for theme in [light(), dark()] {
516 let tokens = Tokens::of(&theme);
517 for status in STATES {
518 let colours = colours(&tokens, status, true);
519 assert_eq!(colours.filled, tokens.primary);
520 assert_ne!(colours.filled, colours.empty);
521 }
522 }
523 }
524
525 #[test]
526 fn interaction_changes_the_handle() {
527 for theme in [light(), dark()] {
528 let tokens = Tokens::of(&theme);
529 let active = style(&tokens, Status::Active, true);
530 let hovered = style(&tokens, Status::Hovered, true);
531 let dragged = style(&tokens, Status::Dragged, true);
532 assert_ne!(active.handle, hovered.handle);
533 assert_ne!(hovered.handle, dragged.handle);
534 }
535 }
536
537 #[test]
538 fn disabled_fades_and_ignores_interaction() {
539 for theme in [light(), dark()] {
540 let tokens = Tokens::of(&theme);
541 let active = colours(&tokens, Status::Active, true);
542 for status in STATES {
543 let disabled = colours(&tokens, status, false);
544 assert!((disabled.filled.a - active.filled.a * 0.5).abs() < 1e-6);
545 assert_eq!(disabled, colours(&tokens, Status::Active, false));
546 }
547 }
548 }
549}