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