Skip to main content

iced_widget/
slider.rs

1//! Sliders let users set a value by moving an indicator.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # use iced::widget::Widget;
7//! #
8//! use iced::widget::slider;
9//!
10//! struct State {
11//!    value: f32,
12//! }
13//!
14//! #[derive(Debug, Clone)]
15//! enum Message {
16//!     ValueChanged(f32),
17//! }
18//!
19//! fn view(state: &State) -> impl Widget<Message> {
20//!     slider(0.0..=100.0, state.value, Message::ValueChanged)
21//! }
22//!
23//! fn update(state: &mut State, message: Message) {
24//!     match message {
25//!         Message::ValueChanged(value) => {
26//!             state.value = value;
27//!         }
28//!     }
29//! }
30//! ```
31use crate::core::border::{self, Border};
32use crate::core::keyboard;
33use crate::core::keyboard::key::{self, Key};
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::touch;
38use crate::core::widget::Meta;
39use crate::core::widget::tree::{self, Tree};
40use crate::core::window;
41use crate::core::{
42    self, Background, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme,
43    Widget,
44};
45
46use std::ops::RangeInclusive;
47
48/// An horizontal bar and a handle that selects a single value from a range of
49/// values.
50///
51/// A [`Slider`] will try to fill the horizontal space of its container.
52///
53/// The [`Slider`] range of numeric values is generic and its step size defaults
54/// to 1 unit.
55///
56/// Note: Under the hood values are converted to/from f64 so only values representable exactly as an f64
57/// are possible to select via the slider. However it is likely that the precision of the slider at these
58/// scales is already less than the precision lost from the f64 representation.
59///
60/// # Example
61/// ```no_run
62/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
63/// # use iced::widget::Widget;
64/// #
65/// use iced::widget::slider;
66///
67/// struct State {
68///    value: f32,
69/// }
70///
71/// #[derive(Debug, Clone)]
72/// enum Message {
73///     ValueChanged(f32),
74/// }
75///
76/// fn view(state: &State) -> impl Widget<Message> {
77///     slider(0.0..=100.0, state.value, Message::ValueChanged)
78/// }
79///
80/// fn update(state: &mut State, message: Message) {
81///     match message {
82///         Message::ValueChanged(value) => {
83///             state.value = value;
84///         }
85///     }
86/// }
87/// ```
88pub struct Slider<'a, T, Message, Theme = crate::Theme>
89where
90    Theme: Catalog,
91{
92    range: RangeInclusive<T>,
93    step: f64,
94    shift_step: Option<f64>,
95    value: T,
96    default: Option<T>,
97    on_change: Box<dyn Fn(T) -> Message + 'a>,
98    on_release: Option<Message>,
99    width: Length,
100    height: f32,
101    class: Theme::Class<'a>,
102    status: Option<Status>,
103}
104
105impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
106where
107    T: Copy + PartialOrd,
108    Message: Clone,
109    Theme: Catalog,
110{
111    /// The default height of a [`Slider`].
112    pub const DEFAULT_HEIGHT: f32 = 16.0;
113
114    /// Creates a new [`Slider`].
115    ///
116    /// It expects:
117    ///   * an inclusive range of possible values
118    ///   * the current value of the [`Slider`]
119    ///   * a function that will be called when the [`Slider`] is dragged.
120    ///     It receives the new value of the [`Slider`] and must produce a
121    ///     `Message`.
122    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
123    where
124        F: 'a + Fn(T) -> Message,
125    {
126        let value = if value >= *range.start() {
127            value
128        } else {
129            *range.start()
130        };
131
132        let value = if value <= *range.end() {
133            value
134        } else {
135            *range.end()
136        };
137
138        Slider {
139            value,
140            default: None,
141            range,
142            step: 1.0,
143            shift_step: None,
144            on_change: Box::new(on_change),
145            on_release: None,
146            width: Length::Fill,
147            height: Self::DEFAULT_HEIGHT,
148            class: Theme::default(),
149            status: None,
150        }
151    }
152
153    /// Sets the optional default value for the [`Slider`].
154    ///
155    /// If set, the [`Slider`] will reset to this value when ctrl-clicked or command-clicked.
156    pub fn default(mut self, default: impl Into<T>) -> Self {
157        self.default = Some(default.into());
158        self
159    }
160
161    /// Sets the release message of the [`Slider`].
162    /// This is called when the mouse is released from the slider.
163    ///
164    /// Typically, the user's interaction with the slider is finished when this message is produced.
165    /// This is useful if you need to spawn a long-running task from the slider's result, where
166    /// the default on_change message could create too many events.
167    pub fn on_release(mut self, on_release: Message) -> Self {
168        self.on_release = Some(on_release);
169        self
170    }
171
172    /// Sets the width of the [`Slider`].
173    pub fn width(mut self, width: impl Into<Length>) -> Self {
174        self.width = width.into();
175        self
176    }
177
178    /// Sets the height of the [`Slider`].
179    pub fn height(mut self, height: impl Into<Pixels>) -> Self {
180        self.height = height.into().0;
181        self
182    }
183
184    /// Sets the step size of the [`Slider`].
185    pub fn step(mut self, step: impl num_traits::AsPrimitive<f64>) -> Self {
186        self.step = step.as_();
187        self
188    }
189
190    /// Sets the optional "shift" step for the [`Slider`].
191    ///
192    /// If set, this value is used as the step while the shift key is pressed.
193    pub fn shift_step(mut self, shift_step: impl num_traits::AsPrimitive<f64>) -> Self {
194        self.shift_step = Some(shift_step.as_());
195        self
196    }
197
198    /// Sets the style of the [`Slider`].
199    #[must_use]
200    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
201    where
202        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
203    {
204        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
205        self
206    }
207
208    /// Sets the style class of the [`Slider`].
209    #[cfg(feature = "advanced")]
210    #[must_use]
211    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
212        self.class = class.into();
213        self
214    }
215}
216
217impl<T, Message, Theme> Meta for Slider<'_, T, Message, Theme> where Theme: Catalog {}
218
219impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Slider<'_, T, Message, Theme>
220where
221    T: Copy + num_traits::AsPrimitive<f64> + num_traits::FromPrimitive,
222    Message: Clone,
223    Theme: Catalog,
224    Renderer: core::Renderer,
225{
226    fn tag(&self) -> tree::Tag {
227        tree::Tag::of::<State>()
228    }
229
230    fn state(&self) -> tree::State {
231        tree::State::new(State::default())
232    }
233
234    fn size(&self) -> Size<Length> {
235        Size {
236            width: self.width,
237            height: Length::Fit,
238        }
239    }
240
241    fn layout(&mut self, tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits) {
242        tree.size = layout::atomic(limits, self.width, self.height);
243    }
244
245    fn update(
246        &mut self,
247        tree: &mut Tree,
248        event: &Event,
249        layout: Layout,
250        cursor: mouse::Cursor,
251        _renderer: &Renderer,
252        shell: &mut Shell<'_, Message>,
253        _viewport: &Rectangle,
254    ) {
255        let state = tree.state.downcast_mut::<State>();
256
257        let mut update = || {
258            let current_value = self.value;
259
260            let locate = |cursor_position: Point| -> Option<T> {
261                let bounds = layout.bounds();
262
263                if cursor_position.x <= bounds.x {
264                    Some(*self.range.start())
265                } else if cursor_position.x >= bounds.x + bounds.width {
266                    Some(*self.range.end())
267                } else {
268                    let step = if state.keyboard_modifiers.shift() {
269                        self.shift_step.unwrap_or(self.step)
270                    } else {
271                        self.step
272                    };
273
274                    let start = (*self.range.start()).as_();
275                    let end = (*self.range.end()).as_();
276
277                    let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width);
278
279                    let steps = (percent * (end - start) / step).round();
280                    let value = steps * step + start;
281
282                    T::from_f64(value.min(end))
283                }
284            };
285
286            let increment = |value: T| -> Option<T> {
287                let step = if state.keyboard_modifiers.shift() {
288                    self.shift_step.unwrap_or(self.step)
289                } else {
290                    self.step
291                };
292
293                let steps = (value.as_() / step).round();
294                let new_value = step * (steps + 1.0);
295
296                if new_value > (*self.range.end()).as_() {
297                    return Some(*self.range.end());
298                }
299
300                T::from_f64(new_value)
301            };
302
303            let decrement = |value: T| -> Option<T> {
304                let step = if state.keyboard_modifiers.shift() {
305                    self.shift_step.unwrap_or(self.step)
306                } else {
307                    self.step
308                };
309
310                let steps = (value.as_() / step).round();
311                let new_value = step * (steps - 1.0);
312
313                if new_value < (*self.range.start()).as_() {
314                    return Some(*self.range.start());
315                }
316
317                T::from_f64(new_value)
318            };
319
320            let change = |new_value: T| {
321                if (self.value.as_() - new_value.as_()).abs() > f64::EPSILON {
322                    shell.publish((self.on_change)(new_value));
323
324                    self.value = new_value;
325                }
326            };
327
328            match &event {
329                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
330                | Event::Touch(touch::Event::FingerPressed { .. }) => {
331                    if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
332                        if state.keyboard_modifiers.command() {
333                            let _ = self.default.map(change);
334                            state.is_dragging = false;
335                        } else {
336                            let _ = locate(cursor_position).map(change);
337                            state.is_dragging = true;
338                        }
339
340                        shell.capture_event();
341                    }
342                }
343                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
344                | Event::Touch(touch::Event::FingerLifted { .. })
345                | Event::Touch(touch::Event::FingerLost { .. })
346                    if state.is_dragging =>
347                {
348                    if let Some(on_release) = self.on_release.clone() {
349                        shell.publish(on_release);
350                    }
351                    state.is_dragging = false;
352                }
353                Event::Mouse(mouse::Event::CursorMoved { .. })
354                | Event::Touch(touch::Event::FingerMoved { .. })
355                    if state.is_dragging =>
356                {
357                    let _ = cursor.observe().position().and_then(locate).map(change);
358
359                    shell.capture_event();
360                }
361                Event::Mouse(mouse::Event::WheelScrolled { delta })
362                    if state.keyboard_modifiers.control() && cursor.is_over(layout.bounds()) =>
363                {
364                    let delta = match delta {
365                        mouse::ScrollDelta::Lines { x: _, y } => y,
366                        mouse::ScrollDelta::Pixels { x: _, y } => y,
367                    };
368
369                    if *delta < 0.0 {
370                        let _ = decrement(current_value).map(change);
371                    } else {
372                        let _ = increment(current_value).map(change);
373                    }
374
375                    shell.capture_event();
376                }
377                Event::Keyboard(keyboard::Event::KeyPressed { key, .. })
378                    if cursor.is_over(layout.bounds()) =>
379                {
380                    match key {
381                        Key::Named(key::Named::ArrowUp) => {
382                            let _ = increment(current_value).map(change);
383                            shell.capture_event();
384                        }
385                        Key::Named(key::Named::ArrowDown) => {
386                            let _ = decrement(current_value).map(change);
387                            shell.capture_event();
388                        }
389                        _ => (),
390                    }
391                }
392                Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
393                    state.keyboard_modifiers = *modifiers;
394                }
395                _ => {}
396            }
397        };
398
399        update();
400
401        let current_status = if state.is_dragging {
402            Status::Dragged
403        } else if cursor.is_over(layout.bounds()) {
404            Status::Hovered
405        } else {
406            Status::Active
407        };
408
409        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
410            self.status = Some(current_status);
411        } else if self.status.is_some_and(|status| status != current_status) {
412            shell.request_redraw();
413        }
414    }
415
416    fn draw(
417        &self,
418        _tree: &Tree,
419        renderer: &mut Renderer,
420        theme: &Theme,
421        _style: &renderer::Style,
422        layout: Layout,
423        _cursor: mouse::Cursor,
424        _viewport: &Rectangle,
425    ) {
426        let bounds = layout.bounds();
427
428        let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
429
430        let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
431            HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
432            HandleShape::Rectangle {
433                width,
434                border_radius,
435            } => (f32::from(width), bounds.height, border_radius),
436        };
437
438        let value = self.value.as_() as f32;
439        let (range_start, range_end) = {
440            let (start, end) = self.range.clone().into_inner();
441
442            (start.as_() as f32, end.as_() as f32)
443        };
444
445        let offset = if range_start >= range_end {
446            0.0
447        } else {
448            (bounds.width - handle_width) * (value - range_start) / (range_end - range_start)
449        };
450
451        let rail_y = bounds.y + bounds.height / 2.0;
452
453        renderer.fill_quad(
454            renderer::Quad {
455                bounds: Rectangle {
456                    x: bounds.x,
457                    y: rail_y - style.rail.width / 2.0,
458                    width: offset + handle_width / 2.0,
459                    height: style.rail.width,
460                },
461                border: style.rail.border,
462                ..renderer::Quad::default()
463            },
464            style.rail.backgrounds.0,
465        );
466
467        renderer.fill_quad(
468            renderer::Quad {
469                bounds: Rectangle {
470                    x: bounds.x + offset + handle_width / 2.0,
471                    y: rail_y - style.rail.width / 2.0,
472                    width: bounds.width - offset - handle_width / 2.0,
473                    height: style.rail.width,
474                },
475                border: style.rail.border,
476                ..renderer::Quad::default()
477            },
478            style.rail.backgrounds.1,
479        );
480
481        renderer.fill_quad(
482            renderer::Quad {
483                bounds: Rectangle {
484                    x: bounds.x + offset,
485                    y: rail_y - handle_height / 2.0,
486                    width: handle_width,
487                    height: handle_height,
488                },
489                border: Border {
490                    radius: handle_border_radius,
491                    width: style.handle.border_width,
492                    color: style.handle.border_color,
493                },
494                ..renderer::Quad::default()
495            },
496            style.handle.background,
497        );
498    }
499
500    fn mouse_interaction(
501        &self,
502        tree: &Tree,
503        layout: Layout,
504        cursor: mouse::Cursor,
505        _viewport: &Rectangle,
506        _renderer: &Renderer,
507    ) -> mouse::Interaction {
508        let state = tree.state.downcast_ref::<State>();
509
510        if state.is_dragging {
511            // FIXME: Fall back to `Pointer` on Windows
512            // See https://github.com/rust-windowing/winit/issues/1043
513            if cfg!(target_os = "windows") {
514                mouse::Interaction::Pointer
515            } else {
516                mouse::Interaction::Grabbing
517            }
518        } else if cursor.is_over(layout.bounds()) {
519            if cfg!(target_os = "windows") {
520                mouse::Interaction::Pointer
521            } else {
522                mouse::Interaction::Grab
523            }
524        } else {
525            mouse::Interaction::default()
526        }
527    }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
531struct State {
532    is_dragging: bool,
533    keyboard_modifiers: keyboard::Modifiers,
534}
535
536/// The possible status of a [`Slider`].
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub enum Status {
539    /// The [`Slider`] can be interacted with.
540    Active,
541    /// The [`Slider`] is being hovered.
542    Hovered,
543    /// The [`Slider`] is being dragged.
544    Dragged,
545}
546
547/// The appearance of a slider.
548#[derive(Debug, Clone, Copy, PartialEq)]
549pub struct Style {
550    /// The colors of the rail of the slider.
551    pub rail: Rail,
552    /// The appearance of the [`Handle`] of the slider.
553    pub handle: Handle,
554}
555
556impl Style {
557    /// Changes the [`HandleShape`] of the [`Style`] to a circle
558    /// with the given radius.
559    pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
560        self.handle.shape = HandleShape::Circle {
561            radius: radius.into().0,
562        };
563        self
564    }
565}
566
567/// The appearance of a slider rail
568#[derive(Debug, Clone, Copy, PartialEq)]
569pub struct Rail {
570    /// The backgrounds of the rail of the slider.
571    pub backgrounds: (Background, Background),
572    /// The width of the stroke of a slider rail.
573    pub width: f32,
574    /// The border of the rail.
575    pub border: Border,
576}
577
578/// The appearance of the handle of a slider.
579#[derive(Debug, Clone, Copy, PartialEq)]
580pub struct Handle {
581    /// The shape of the handle.
582    pub shape: HandleShape,
583    /// The [`Background`] of the handle.
584    pub background: Background,
585    /// The border width of the handle.
586    pub border_width: f32,
587    /// The border [`Color`] of the handle.
588    pub border_color: Color,
589}
590
591/// The shape of the handle of a slider.
592#[derive(Debug, Clone, Copy, PartialEq)]
593pub enum HandleShape {
594    /// A circular handle.
595    Circle {
596        /// The radius of the circle.
597        radius: f32,
598    },
599    /// A rectangular shape.
600    Rectangle {
601        /// The width of the rectangle.
602        width: u16,
603        /// The border radius of the corners of the rectangle.
604        border_radius: border::Radius,
605    },
606}
607
608/// The theme catalog of a [`Slider`].
609pub trait Catalog: Sized {
610    /// The item class of the [`Catalog`].
611    type Class<'a>;
612
613    /// The default class produced by the [`Catalog`].
614    fn default<'a>() -> Self::Class<'a>;
615
616    /// The [`Style`] of a class with the given status.
617    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
618}
619
620/// A styling function for a [`Slider`].
621pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
622
623impl Catalog for Theme {
624    type Class<'a> = StyleFn<'a, Self>;
625
626    fn default<'a>() -> Self::Class<'a> {
627        Box::new(default)
628    }
629
630    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
631        class(self, status)
632    }
633}
634
635/// The default style of a [`Slider`].
636pub fn default(theme: &Theme, status: Status) -> Style {
637    let palette = theme.palette();
638
639    let color = match status {
640        Status::Active => palette.primary.base.color,
641        Status::Hovered => palette.primary.strong.color,
642        Status::Dragged => palette.primary.weak.color,
643    };
644
645    Style {
646        rail: Rail {
647            backgrounds: (color.into(), palette.background.strong.color.into()),
648            width: 4.0,
649            border: Border {
650                radius: 2.0.into(),
651                width: 0.0,
652                color: Color::TRANSPARENT,
653            },
654        },
655        handle: Handle {
656            shape: HandleShape::Circle { radius: 7.0 },
657            background: color.into(),
658            border_color: Color::TRANSPARENT,
659            border_width: 0.0,
660        },
661    }
662}