1use super::*;
4use iced_widget::core::keyboard;
5use iced_widget::core::keyboard::key::{self, Key};
6use iced_widget::slider::{Catalog as SliderCatalog, HandleShape, Status, Style, StyleFn};
7use num_traits::FromPrimitive;
8use std::fmt;
9use std::ops::RangeInclusive;
10
11pub struct Slider<'a, T, Message> {
13 range: RangeInclusive<T>,
14 step: T,
15 shift_step: Option<T>,
16 value: T,
17 default: Option<T>,
18 on_change: Box<dyn Fn(T) -> Message + 'a>,
19 on_release: Option<Message>,
20 width: Length,
21 height: f32,
22 class: StyleFn<'a, Theme>,
23 status: Option<Status>,
24}
25
26impl<T, Message> fmt::Debug for Slider<'_, T, Message>
27where
28 T: fmt::Debug,
29{
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.debug_struct("Slider")
32 .field("range", &self.range)
33 .field("step", &self.step)
34 .field("shift_step", &self.shift_step)
35 .field("value", &self.value)
36 .field("default", &self.default)
37 .field("width", &self.width)
38 .field("height", &self.height)
39 .field("status", &self.status)
40 .finish_non_exhaustive()
41 }
42}
43
44impl<'a, T, Message> Slider<'a, T, Message>
45where
46 T: Copy + From<u8> + PartialOrd,
47 Message: Clone + 'a,
48{
49 fn new(range: RangeInclusive<T>, value: T, on_change: impl Fn(T) -> Message + 'a) -> Self {
50 let value = clamped_value(&range, value);
51
52 Self {
53 range,
54 step: T::from(1),
55 shift_step: None,
56 value,
57 default: None,
58 on_change: Box::new(on_change),
59 on_release: None,
60 width: Length::Fill,
61 height: tokens::component::slider::STATE_LAYER_SIZE,
62 class: Box::new(slider_style::default),
63 status: None,
64 }
65 }
66
67 pub fn default(mut self, default: impl Into<T>) -> Self {
69 self.default = Some(default.into());
70 self
71 }
72
73 pub fn on_release(mut self, on_release: Message) -> Self {
75 self.on_release = Some(on_release);
76 self
77 }
78
79 pub fn width(mut self, width: impl Into<Length>) -> Self {
81 self.width = width.into();
82 self
83 }
84
85 pub fn height(mut self, height: impl Into<Pixels>) -> Self {
87 self.height = height.into().0;
88 self
89 }
90
91 pub fn step(mut self, step: impl Into<T>) -> Self {
93 self.step = step.into();
94 self
95 }
96
97 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
99 self.shift_step = Some(shift_step.into());
100 self
101 }
102
103 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self {
105 self.class = Box::new(style);
106 self
107 }
108}
109
110pub fn continuous<'a, T, Message>(
111 range: RangeInclusive<T>,
112 value: T,
113 on_change: impl Fn(T) -> Message + 'a,
114) -> Slider<'a, T, Message>
115where
116 T: Copy + From<u8> + PartialOrd,
117 Message: Clone + 'a,
118{
119 Slider::new(range, value, on_change)
120}
121
122impl<'a, T, Message, Renderer> Widget<Message, Theme, Renderer> for Slider<'a, T, Message>
123where
124 T: Copy + Into<f64> + FromPrimitive,
125 Message: Clone,
126 Renderer: iced_widget::core::Renderer,
127{
128 fn tag(&self) -> tree::Tag {
129 tree::Tag::of::<State>()
130 }
131
132 fn state(&self) -> tree::State {
133 tree::State::new(State::default())
134 }
135
136 fn size(&self) -> Size<Length> {
137 Size {
138 width: self.width,
139 height: Length::Shrink,
140 }
141 }
142
143 fn layout(
144 &mut self,
145 _tree: &mut Tree,
146 _renderer: &Renderer,
147 limits: &layout::Limits,
148 ) -> layout::Node {
149 layout::atomic(limits, self.width, self.height)
150 }
151
152 fn update(
153 &mut self,
154 tree: &mut Tree,
155 event: &Event,
156 layout: Layout<'_>,
157 cursor: mouse::Cursor,
158 _renderer: &Renderer,
159 _clipboard: &mut dyn Clipboard,
160 shell: &mut Shell<'_, Message>,
161 _viewport: &Rectangle,
162 ) {
163 let state = tree.state.downcast_mut::<State>();
164
165 let mut update = || {
166 let current_value = self.value;
167
168 let locate = |cursor_position: Point| -> Option<T> {
169 let bounds = layout.bounds();
170
171 if cursor_position.x <= bounds.x {
172 Some(*self.range.start())
173 } else if cursor_position.x >= bounds.x + bounds.width {
174 Some(*self.range.end())
175 } else {
176 let step = if state.keyboard_modifiers.shift() {
177 self.shift_step.unwrap_or(self.step)
178 } else {
179 self.step
180 }
181 .into();
182
183 let start = (*self.range.start()).into();
184 let end = (*self.range.end()).into();
185 let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width);
186 let steps = (percent * (end - start) / step).round();
187 let value = steps * step + start;
188
189 T::from_f64(value.min(end))
190 }
191 };
192
193 let increment = |value: T| -> Option<T> {
194 let step = if state.keyboard_modifiers.shift() {
195 self.shift_step.unwrap_or(self.step)
196 } else {
197 self.step
198 }
199 .into();
200
201 let steps = (value.into() / step).round();
202 let new_value = step * (steps + 1.0);
203
204 if new_value > (*self.range.end()).into() {
205 return Some(*self.range.end());
206 }
207
208 T::from_f64(new_value)
209 };
210
211 let decrement = |value: T| -> Option<T> {
212 let step = if state.keyboard_modifiers.shift() {
213 self.shift_step.unwrap_or(self.step)
214 } else {
215 self.step
216 }
217 .into();
218
219 let steps = (value.into() / step).round();
220 let new_value = step * (steps - 1.0);
221
222 if new_value < (*self.range.start()).into() {
223 return Some(*self.range.start());
224 }
225
226 T::from_f64(new_value)
227 };
228
229 let mut change = |new_value: T| {
230 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
231 shell.publish((self.on_change)(new_value));
232 self.value = new_value;
233 }
234 };
235
236 match &event {
237 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
238 | Event::Touch(touch::Event::FingerPressed { .. }) => {
239 if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
240 if state.keyboard_modifiers.command() {
241 if let Some(default) = self.default {
242 change(default);
243 }
244
245 state.is_dragging = false;
246 } else {
247 if let Some(value) = locate(cursor_position) {
248 change(value);
249 }
250
251 state.is_dragging = true;
252 }
253
254 shell.capture_event();
255 }
256 }
257 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
258 | Event::Touch(touch::Event::FingerLifted { .. })
259 | Event::Touch(touch::Event::FingerLost { .. })
260 if state.is_dragging =>
261 {
262 if let Some(on_release) = self.on_release.clone() {
263 shell.publish(on_release);
264 }
265
266 state.is_dragging = false;
267 }
268 Event::Mouse(mouse::Event::CursorMoved { .. })
269 | Event::Touch(touch::Event::FingerMoved { .. })
270 if state.is_dragging =>
271 {
272 if let Some(value) = cursor.land().position().and_then(locate) {
273 change(value);
274 }
275
276 shell.capture_event();
277 }
278 Event::Mouse(mouse::Event::WheelScrolled { delta })
279 if state.keyboard_modifiers.control() && cursor.is_over(layout.bounds()) =>
280 {
281 let delta = match delta {
282 mouse::ScrollDelta::Lines { x: _, y } => y,
283 mouse::ScrollDelta::Pixels { x: _, y } => y,
284 };
285
286 if *delta < 0.0 {
287 if let Some(value) = decrement(current_value) {
288 change(value);
289 }
290 } else if let Some(value) = increment(current_value) {
291 change(value);
292 }
293
294 shell.capture_event();
295 }
296 Event::Keyboard(keyboard::Event::KeyPressed { key, .. })
297 if cursor.is_over(layout.bounds()) =>
298 {
299 match key {
300 Key::Named(key::Named::ArrowUp) => {
301 if let Some(value) = increment(current_value) {
302 change(value);
303 }
304
305 shell.capture_event();
306 }
307 Key::Named(key::Named::ArrowDown) => {
308 if let Some(value) = decrement(current_value) {
309 change(value);
310 }
311
312 shell.capture_event();
313 }
314 _ => (),
315 }
316 }
317 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
318 state.keyboard_modifiers = *modifiers;
319 }
320 _ => {}
321 }
322 };
323
324 update();
325
326 let current_status = if state.is_dragging {
327 Status::Dragged
328 } else if cursor.is_over(layout.bounds()) {
329 Status::Hovered
330 } else {
331 Status::Active
332 };
333
334 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
335 self.status = Some(current_status);
336 } else if self.status.is_some_and(|status| status != current_status) {
337 shell.request_redraw();
338 }
339 }
340
341 fn draw(
342 &self,
343 _tree: &Tree,
344 renderer: &mut Renderer,
345 theme: &Theme,
346 _style: &renderer::Style,
347 layout: Layout<'_>,
348 _cursor: mouse::Cursor,
349 _viewport: &Rectangle,
350 ) {
351 let bounds = layout.bounds();
352 let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
353 let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
354 HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
355 HandleShape::Rectangle {
356 width,
357 border_radius,
358 } => (f32::from(width), bounds.height, border_radius),
359 };
360 let thumb_bounds =
361 thumb_bounds_with_size(bounds, self.value, &self.range, handle_width, handle_height);
362 let rail_y = bounds.y + bounds.height / 2.0;
363
364 renderer.fill_quad(
365 renderer::Quad {
366 bounds: Rectangle {
367 x: bounds.x,
368 y: rail_y - style.rail.width / 2.0,
369 width: thumb_bounds.x - bounds.x + handle_width / 2.0,
370 height: style.rail.width,
371 },
372 border: style.rail.border,
373 ..renderer::Quad::default()
374 },
375 style.rail.backgrounds.0,
376 );
377
378 renderer.fill_quad(
379 renderer::Quad {
380 bounds: Rectangle {
381 x: thumb_bounds.x + handle_width / 2.0,
382 y: rail_y - style.rail.width / 2.0,
383 width: bounds.width - (thumb_bounds.x - bounds.x) - handle_width / 2.0,
384 height: style.rail.width,
385 },
386 border: style.rail.border,
387 ..renderer::Quad::default()
388 },
389 style.rail.backgrounds.1,
390 );
391
392 renderer.fill_quad(
393 renderer::Quad {
394 bounds: thumb_bounds,
395 border: Border {
396 radius: handle_border_radius,
397 width: style.handle.border_width,
398 color: style.handle.border_color,
399 },
400 shadow: thumb_shadow(theme),
401 ..renderer::Quad::default()
402 },
403 style.handle.background,
404 );
405 }
406
407 fn mouse_interaction(
408 &self,
409 tree: &Tree,
410 layout: Layout<'_>,
411 cursor: mouse::Cursor,
412 _viewport: &Rectangle,
413 _renderer: &Renderer,
414 ) -> mouse::Interaction {
415 let state = tree.state.downcast_ref::<State>();
416
417 if state.is_dragging {
418 if cfg!(target_os = "windows") {
419 mouse::Interaction::Pointer
420 } else {
421 mouse::Interaction::Grabbing
422 }
423 } else if cursor.is_over(layout.bounds()) {
424 if cfg!(target_os = "windows") {
425 mouse::Interaction::Pointer
426 } else {
427 mouse::Interaction::Grab
428 }
429 } else {
430 mouse::Interaction::default()
431 }
432 }
433}
434
435impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>>
436 for Element<'a, Message, Theme, Renderer>
437where
438 T: Copy + Into<f64> + FromPrimitive + 'a,
439 Message: Clone + 'a,
440 Renderer: iced_widget::core::Renderer + 'a,
441{
442 fn from(slider: Slider<'a, T, Message>) -> Self {
443 Element::new(slider)
444 }
445}
446
447fn clamped_value<T>(range: &RangeInclusive<T>, value: T) -> T
448where
449 T: Copy + PartialOrd,
450{
451 if value < *range.start() {
452 *range.start()
453 } else if value > *range.end() {
454 *range.end()
455 } else {
456 value
457 }
458}
459
460fn thumb_bounds_with_size<T>(
461 bounds: Rectangle,
462 value: T,
463 range: &RangeInclusive<T>,
464 handle_width: f32,
465 handle_height: f32,
466) -> Rectangle
467where
468 T: Copy + Into<f64>,
469{
470 let range_start = (*range.start()).into() as f32;
471 let range_end = (*range.end()).into() as f32;
472 let offset = if range_start >= range_end {
473 0.0
474 } else {
475 let value = (value.into() as f32).clamp(range_start, range_end);
476
477 (bounds.width - handle_width) * (value - range_start) / (range_end - range_start)
478 };
479 let rail_y = bounds.y + bounds.height / 2.0;
480
481 Rectangle {
482 x: bounds.x + offset,
483 y: rail_y - handle_height / 2.0,
484 width: handle_width,
485 height: handle_height,
486 }
487}
488
489fn thumb_shadow(theme: &Theme) -> iced_widget::core::Shadow {
490 crate::utils::shadow_from_elevation(
491 tokens::component::slider::HANDLE_ELEVATION,
492 theme.colors().shadow,
493 )
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
497struct State {
498 is_dragging: bool,
499 keyboard_modifiers: keyboard::Modifiers,
500}
501
502#[cfg(test)]
503#[path = "../../../tests/widget/component/slider.rs"]
504mod tests;