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