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