1use crate::consts::DOUBLE_CLICK;
2#[cfg(test)]
3use iced::Point;
4use iced::advanced::Shell;
5use iced::advanced::layout::{self, Layout};
6use iced::advanced::renderer;
7use iced::advanced::widget::{self, Tree, Widget};
8use iced::mouse;
9use iced::{Border, Color, Element, Event, Length, Rectangle, Size};
10use std::time::Instant;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq)]
13enum Orientation {
14 #[default]
15 Vertical,
16 Horizontal,
17}
18
19pub struct Slider<'a, Message> {
20 range: std::ops::RangeInclusive<f32>,
21 value: f32,
22 on_change: Box<dyn Fn(f32) -> Message + 'a>,
23 width: Length,
24 height: Length,
25 handle_height: f32,
26 step: Option<f32>,
27 double_click_reset: f32,
28 on_release: Option<Message>,
29 orientation: Orientation,
30}
31
32impl<'a, Message> Slider<'a, Message> {
33 pub fn new<F>(range: std::ops::RangeInclusive<f32>, value: f32, on_change: F) -> Self
34 where
35 F: Fn(f32) -> Message + 'a,
36 {
37 Self {
38 range,
39 value,
40 on_change: Box::new(on_change),
41 width: Length::Fixed(14.0),
42 height: Length::Fixed(300.0),
43 handle_height: 2.0,
44 step: None,
45 double_click_reset: 0.0,
46 on_release: None,
47 orientation: Orientation::Vertical,
48 }
49 }
50
51 pub fn width(mut self, width: Length) -> Self {
52 self.width = width;
53 self
54 }
55
56 pub fn height(mut self, height: Length) -> Self {
57 self.height = height;
58 self
59 }
60
61 pub fn step(mut self, step: f32) -> Self {
62 self.step = Some(step.abs()).filter(|step| *step > 0.0);
63 self
64 }
65
66 pub fn double_click_reset(mut self, value: f32) -> Self {
67 self.double_click_reset = value;
68 self
69 }
70
71 pub fn on_release(mut self, message: Message) -> Self {
72 self.on_release = Some(message);
73 self
74 }
75
76 pub fn horizontal(mut self) -> Self {
77 self.orientation = Orientation::Horizontal;
78 self
79 }
80}
81
82pub fn slider<'a, Message, F>(
83 range: std::ops::RangeInclusive<f32>,
84 value: f32,
85 on_change: F,
86) -> Slider<'a, Message>
87where
88 F: Fn(f32) -> Message + 'a,
89{
90 Slider::new(range, value, on_change)
91}
92
93#[derive(Default)]
94struct State {
95 is_dragging: bool,
96 last_click_at: Option<Instant>,
97 drag_start_x: f32,
98 drag_start_y: f32,
99 drag_start_value: f32,
100}
101
102impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Slider<'a, Message>
103where
104 Renderer: renderer::Renderer,
105 Message: Clone,
106{
107 fn size(&self) -> Size<Length> {
108 Size {
109 width: self.width,
110 height: self.height,
111 }
112 }
113
114 fn layout(
115 &mut self,
116 _tree: &mut Tree,
117 _renderer: &Renderer,
118 limits: &layout::Limits,
119 ) -> layout::Node {
120 let size = limits.width(self.width).height(self.height).resolve(
121 self.width,
122 self.height,
123 Size::ZERO,
124 );
125
126 layout::Node::new(size)
127 }
128
129 fn draw(
130 &self,
131 _tree: &Tree,
132 renderer: &mut Renderer,
133 _theme: &Theme,
134 _style: &renderer::Style,
135 layout: Layout<'_>,
136 _cursor: mouse::Cursor,
137 _viewport: &Rectangle,
138 ) {
139 let bounds = layout.bounds();
140 let border_width = 1.0;
141 let twice_border = border_width * 2.0;
142 let normalized =
143 (self.value - self.range.start()) / (self.range.end() - self.range.start());
144
145 let back_color = Color::from_rgb(
146 0x42 as f32 / 255.0,
147 0x46 as f32 / 255.0,
148 0x4D as f32 / 255.0,
149 );
150 let border_color = Color::from_rgb(
151 0x30 as f32 / 255.0,
152 0x33 as f32 / 255.0,
153 0x3C as f32 / 255.0,
154 );
155 let filled_color = Color::from_rgb(
156 0x29 as f32 / 255.0,
157 0x66 as f32 / 255.0,
158 0xA3 as f32 / 255.0,
159 );
160 let handle_color = Color::from_rgb(
161 0x75 as f32 / 255.0,
162 0xC2 as f32 / 255.0,
163 0xFF as f32 / 255.0,
164 );
165
166 let border_radius = 2.0;
167 let handle_filled_gap = 1.0;
168
169 renderer.fill_quad(
170 renderer::Quad {
171 bounds: Rectangle {
172 x: bounds.x,
173 y: bounds.y,
174 width: bounds.width,
175 height: bounds.height,
176 },
177 border: Border {
178 radius: border_radius.into(),
179 width: border_width,
180 color: border_color,
181 },
182 ..Default::default()
183 },
184 back_color,
185 );
186
187 match self.orientation {
188 Orientation::Vertical => {
189 let value_bounds_y = bounds.y + (self.handle_height / 2.0);
190 let value_bounds_height = bounds.height - self.handle_height;
191 let handle_offset = (value_bounds_y
192 + (value_bounds_height - twice_border) * (1.0 - normalized))
193 .round();
194
195 let filled_y_start = handle_offset + self.handle_height + handle_filled_gap;
196 let filled_height = bounds.y + bounds.height - filled_y_start;
197
198 if filled_height > 0.0 {
199 renderer.fill_quad(
200 renderer::Quad {
201 bounds: Rectangle {
202 x: bounds.x,
203 y: filled_y_start,
204 width: bounds.width,
205 height: filled_height,
206 },
207 border: Border {
208 radius: border_radius.into(),
209 width: border_width,
210 color: Color::TRANSPARENT,
211 },
212 ..Default::default()
213 },
214 filled_color,
215 );
216 }
217
218 renderer.fill_quad(
219 renderer::Quad {
220 bounds: Rectangle {
221 x: bounds.x,
222 y: handle_offset,
223 width: bounds.width,
224 height: self.handle_height + twice_border,
225 },
226 border: Border {
227 radius: border_radius.into(),
228 width: border_width,
229 color: Color::TRANSPARENT,
230 },
231 ..Default::default()
232 },
233 handle_color,
234 );
235 }
236 Orientation::Horizontal => {
237 let value_bounds_x = bounds.x + (self.handle_height / 2.0);
238 let value_bounds_width = bounds.width - self.handle_height;
239 let handle_offset =
240 (value_bounds_x + (value_bounds_width - twice_border) * normalized).round();
241
242 let filled_x_end = handle_offset - handle_filled_gap;
243 let filled_width = filled_x_end - bounds.x;
244
245 if filled_width > 0.0 {
246 renderer.fill_quad(
247 renderer::Quad {
248 bounds: Rectangle {
249 x: bounds.x,
250 y: bounds.y,
251 width: filled_width,
252 height: bounds.height,
253 },
254 border: Border {
255 radius: border_radius.into(),
256 width: border_width,
257 color: Color::TRANSPARENT,
258 },
259 ..Default::default()
260 },
261 filled_color,
262 );
263 }
264
265 renderer.fill_quad(
266 renderer::Quad {
267 bounds: Rectangle {
268 x: handle_offset - self.handle_height / 2.0 - border_width,
269 y: bounds.y,
270 width: self.handle_height + twice_border,
271 height: bounds.height,
272 },
273 border: Border {
274 radius: border_radius.into(),
275 width: border_width,
276 color: Color::TRANSPARENT,
277 },
278 ..Default::default()
279 },
280 handle_color,
281 );
282 }
283 }
284 }
285
286 fn tag(&self) -> widget::tree::Tag {
287 widget::tree::Tag::of::<State>()
288 }
289
290 fn state(&self) -> widget::tree::State {
291 widget::tree::State::new(State::default())
292 }
293
294 fn update(
295 &mut self,
296 tree: &mut Tree,
297 event: &Event,
298 layout: Layout<'_>,
299 cursor: mouse::Cursor,
300 _renderer: &Renderer,
301 _clipboard: &mut dyn iced::advanced::Clipboard,
302 shell: &mut Shell<'_, Message>,
303 _viewport: &Rectangle,
304 ) {
305 let state = tree.state.downcast_mut::<State>();
306 let bounds = layout.bounds();
307
308 match event {
309 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
310 if cursor.is_over(bounds) =>
311 {
312 let now = Instant::now();
313 let is_double_click = state
314 .last_click_at
315 .is_some_and(|last| now.duration_since(last) <= DOUBLE_CLICK);
316 state.last_click_at = Some(now);
317 state.is_dragging = true;
318 if is_double_click {
319 let default_value = self
320 .double_click_reset
321 .clamp(*self.range.start(), *self.range.end());
322 shell.publish((self.on_change)(default_value));
323 shell.capture_event();
324 } else if let Some(cursor_position) = cursor.position() {
325 state.drag_start_x = cursor_position.x;
326 state.drag_start_y = cursor_position.y;
327 state.drag_start_value = self.value;
328 }
329 }
330 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
331 if state.is_dragging =>
332 {
333 state.is_dragging = false;
334 if let Some(message) = self.on_release.as_ref() {
335 shell.publish(message.clone());
336 }
337 }
338 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
339 if state.is_dragging
340 && let Some(cursor_position) = cursor.position()
341 {
342 let range_size = self.range.end() - self.range.start();
343 let value_change = match self.orientation {
344 Orientation::Vertical => {
345 let delta_y = state.drag_start_y - cursor_position.y;
346 (delta_y / bounds.height.max(1.0)) * range_size
347 }
348 Orientation::Horizontal => {
349 let delta_x = cursor_position.x - state.drag_start_x;
350 (delta_x / bounds.width.max(1.0)) * range_size
351 }
352 };
353 let raw_value = state.drag_start_value + value_change;
354 let new_value = self.clamp_to_step(raw_value);
355 shell.publish((self.on_change)(new_value));
356 }
357 }
358 _ => {}
359 }
360 }
361}
362
363impl<'a, Message> Slider<'a, Message> {
364 #[cfg(test)]
365 fn calculate_value(&self, cursor_position: iced::Point, bounds: Rectangle) -> f32 {
366 let y = cursor_position.y - bounds.y;
367 let normalized = 1.0 - (y / bounds.height).clamp(0.0, 1.0);
368 let value = self.range.start() + normalized * (self.range.end() - self.range.start());
369 self.clamp_to_step(value)
370 }
371
372 fn clamp_to_step(&self, value: f32) -> f32 {
373 let clamped = value.clamp(*self.range.start(), *self.range.end());
374 let Some(step) = self.step else {
375 return clamped;
376 };
377
378 let start = *self.range.start();
379 let end = *self.range.end();
380 let steps = ((clamped - start) / step).round();
381 (start + steps * step).clamp(start, end)
382 }
383}
384
385impl<'a, Message, Theme, Renderer> From<Slider<'a, Message>>
386 for Element<'a, Message, Theme, Renderer>
387where
388 Message: 'a + Clone,
389 Theme: 'a,
390 Renderer: renderer::Renderer + 'a,
391{
392 fn from(slider: Slider<'a, Message>) -> Self {
393 Self::new(slider)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use iced::Event;
401 use iced::advanced::{
402 Layout, Shell, clipboard, layout,
403 widget::{self, Tree, Widget},
404 };
405 use std::time::Instant;
406
407 fn test_tree_with_state(state: State) -> Tree {
408 Tree {
409 tag: widget::tree::Tag::of::<State>(),
410 state: widget::tree::State::new(state),
411 children: Vec::new(),
412 }
413 }
414
415 #[test]
416 fn calculate_value_clamps_to_range() {
417 let slider = Slider::new(0.0..=1.0, 0.5, |value| value);
418 let bounds = Rectangle {
419 x: 10.0,
420 y: 20.0,
421 width: 14.0,
422 height: 100.0,
423 };
424
425 assert_eq!(slider.calculate_value(Point::new(15.0, 20.0), bounds), 1.0);
426 assert_eq!(slider.calculate_value(Point::new(15.0, 120.0), bounds), 0.0);
427 assert!((slider.calculate_value(Point::new(15.0, 70.0), bounds) - 0.5).abs() < 0.001);
428 }
429
430 #[test]
431 fn calculate_value_snaps_to_step() {
432 let slider = Slider::new(-90.0..=20.0, 0.0, |value| value).step(1.0);
433 let bounds = Rectangle {
434 x: 0.0,
435 y: 0.0,
436 width: 14.0,
437 height: 110.0,
438 };
439
440 assert_eq!(slider.calculate_value(Point::new(7.0, 10.4), bounds), 10.0);
441 assert_eq!(slider.calculate_value(Point::new(7.0, 10.6), bounds), 9.0);
442 }
443
444 #[cfg(debug_assertions)]
445 #[test]
446 fn update_press_starts_drag_without_publishing() {
447 let mut slider = Slider::new(0.0..=1.0, 0.5, |value| value).height(Length::Fixed(100.0));
448 let mut tree = test_tree_with_state(State::default());
449 let node = layout::Node::new(Size::new(14.0, 100.0));
450 let layout = Layout::new(&node);
451 let mut messages = Vec::new();
452 let mut shell = Shell::new(&mut messages);
453 let renderer = ();
454 let mut clipboard = clipboard::Null;
455 let viewport = Rectangle::new(Point::ORIGIN, Size::new(14.0, 100.0));
456
457 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
458 &mut slider,
459 &mut tree,
460 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
461 layout,
462 mouse::Cursor::Available(Point::new(7.0, 25.0)),
463 &renderer,
464 &mut clipboard,
465 &mut shell,
466 &viewport,
467 );
468
469 assert!(messages.is_empty());
470 }
471
472 #[cfg(debug_assertions)]
473 #[test]
474 fn update_drag_publishes_relative_value_change() {
475 let mut slider = Slider::new(0.0..=1.0, 0.5, |value| value).height(Length::Fixed(100.0));
476 let mut tree = test_tree_with_state(State::default());
477 let node = layout::Node::new(Size::new(14.0, 100.0));
478 let layout = Layout::new(&node);
479 let mut messages = Vec::new();
480 let renderer = ();
481 let mut clipboard = clipboard::Null;
482 let viewport = Rectangle::new(Point::ORIGIN, Size::new(14.0, 100.0));
483
484 {
485 let mut shell = Shell::new(&mut messages);
486 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
487 &mut slider,
488 &mut tree,
489 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
490 layout,
491 mouse::Cursor::Available(Point::new(7.0, 75.0)),
492 &renderer,
493 &mut clipboard,
494 &mut shell,
495 &viewport,
496 );
497 }
498
499 {
500 let mut shell = Shell::new(&mut messages);
501 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
502 &mut slider,
503 &mut tree,
504 &Event::Mouse(mouse::Event::CursorMoved {
505 position: Point::new(7.0, 50.0),
506 }),
507 layout,
508 mouse::Cursor::Available(Point::new(7.0, 50.0)),
509 &renderer,
510 &mut clipboard,
511 &mut shell,
512 &viewport,
513 );
514 }
515
516 assert_eq!(messages.len(), 1);
517 assert!((messages[0] - 0.75).abs() < 0.01);
518 }
519
520 #[cfg(debug_assertions)]
521 #[test]
522 fn update_double_click_resets_to_zero() {
523 let mut slider = Slider::new(-90.0..=20.0, 6.0, |value| value).height(Length::Fixed(110.0));
524 let mut tree = test_tree_with_state(State {
525 is_dragging: false,
526 last_click_at: Some(Instant::now()),
527 ..State::default()
528 });
529 let node = layout::Node::new(Size::new(14.0, 110.0));
530 let layout = Layout::new(&node);
531 let mut messages = Vec::new();
532 let mut shell = Shell::new(&mut messages);
533 let renderer = ();
534 let mut clipboard = clipboard::Null;
535 let viewport = Rectangle::new(Point::ORIGIN, Size::new(14.0, 110.0));
536
537 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
538 &mut slider,
539 &mut tree,
540 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
541 layout,
542 mouse::Cursor::Available(Point::new(7.0, 30.0)),
543 &renderer,
544 &mut clipboard,
545 &mut shell,
546 &viewport,
547 );
548
549 assert_eq!(messages, vec![0.0]);
550 }
551
552 #[cfg(debug_assertions)]
553 #[test]
554 fn update_double_click_resets_to_custom_value() {
555 let mut slider = Slider::new(0.0..=1.0, 0.2, |value| value)
556 .height(Length::Fixed(110.0))
557 .double_click_reset(0.75);
558 let mut tree = test_tree_with_state(State {
559 is_dragging: false,
560 last_click_at: Some(Instant::now()),
561 ..State::default()
562 });
563 let node = layout::Node::new(Size::new(14.0, 110.0));
564 let layout = Layout::new(&node);
565 let mut messages = Vec::new();
566 let mut shell = Shell::new(&mut messages);
567 let renderer = ();
568 let mut clipboard = clipboard::Null;
569 let viewport = Rectangle::new(Point::ORIGIN, Size::new(14.0, 110.0));
570
571 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
572 &mut slider,
573 &mut tree,
574 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
575 layout,
576 mouse::Cursor::Available(Point::new(7.0, 30.0)),
577 &renderer,
578 &mut clipboard,
579 &mut shell,
580 &viewport,
581 );
582
583 assert_eq!(messages, vec![0.75]);
584 }
585
586 #[cfg(debug_assertions)]
587 #[test]
588 fn update_publishes_release_message() {
589 let mut slider = Slider::new(0.0..=1.0, 0.5, |value| value)
590 .height(Length::Fixed(100.0))
591 .on_release(99.0);
592 let mut tree = test_tree_with_state(State {
593 is_dragging: true,
594 last_click_at: None,
595 ..State::default()
596 });
597 let node = layout::Node::new(Size::new(14.0, 100.0));
598 let layout = Layout::new(&node);
599 let mut messages = Vec::new();
600 let mut shell = Shell::new(&mut messages);
601 let renderer = ();
602 let mut clipboard = clipboard::Null;
603 let viewport = Rectangle::new(Point::ORIGIN, Size::new(14.0, 100.0));
604
605 <Slider<'_, f32> as Widget<f32, iced::Theme, ()>>::update(
606 &mut slider,
607 &mut tree,
608 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
609 layout,
610 mouse::Cursor::Unavailable,
611 &renderer,
612 &mut clipboard,
613 &mut shell,
614 &viewport,
615 );
616
617 assert_eq!(messages, vec![99.0]);
618 }
619}