Skip to main content

iced_widget/
vertical_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//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
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) -> Element<'_, Message> {
20//!     slider(0.0..=100.0, state.value, Message::ValueChanged).into()
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 std::ops::RangeInclusive;
32
33pub use crate::slider::{Handle, HandleShape, Rail, Style};
34
35use crate::core::border::Border;
36use crate::core::keyboard;
37use crate::core::keyboard::key::{self, Key};
38use crate::core::layout::{self, Layout};
39use crate::core::mouse;
40use crate::core::renderer;
41use crate::core::theme::palette;
42use crate::core::touch;
43use crate::core::widget::Operation;
44use crate::core::widget::operation::accessible::{Accessible, Orientation, Role, Value};
45use crate::core::widget::operation::focusable::Focusable;
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49    self, Color, Element, Event, Length, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme,
50    Widget,
51};
52
53/// An vertical bar and a handle that selects a single value from a range of
54/// values.
55///
56/// A [`VerticalSlider`] will try to fill the vertical space of its container.
57///
58/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
59/// to 1 unit.
60///
61/// # Example
62/// ```no_run
63/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
64/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
65/// #
66/// use iced::widget::vertical_slider;
67///
68/// struct State {
69///    value: f32,
70/// }
71///
72/// #[derive(Debug, Clone)]
73/// enum Message {
74///     ValueChanged(f32),
75/// }
76///
77/// fn view(state: &State) -> Element<'_, Message> {
78///     vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into()
79/// }
80///
81/// fn update(state: &mut State, message: Message) {
82///     match message {
83///         Message::ValueChanged(value) => {
84///             state.value = value;
85///         }
86///     }
87/// }
88/// ```
89pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
90where
91    Theme: Catalog,
92{
93    range: RangeInclusive<T>,
94    step: T,
95    shift_step: Option<T>,
96    value: T,
97    default: Option<T>,
98    on_change: Box<dyn Fn(T) -> Message + 'a>,
99    on_release: Option<Message>,
100    width: f32,
101    height: Length,
102    class: Theme::Class<'a>,
103    status: Option<Status>,
104}
105
106impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
107where
108    T: Copy + From<u8> + std::cmp::PartialOrd,
109    Message: Clone,
110    Theme: Catalog,
111{
112    /// The default width of a [`VerticalSlider`].
113    pub const DEFAULT_WIDTH: f32 = 16.0;
114
115    /// Creates a new [`VerticalSlider`].
116    ///
117    /// It expects:
118    ///   * an inclusive range of possible values
119    ///   * the current value of the [`VerticalSlider`]
120    ///   * a function that will be called when the [`VerticalSlider`] is dragged.
121    ///     It receives the new value of the [`VerticalSlider`] and must produce a
122    ///     `Message`.
123    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
124    where
125        F: 'a + Fn(T) -> Message,
126    {
127        let value = if value >= *range.start() {
128            value
129        } else {
130            *range.start()
131        };
132
133        let value = if value <= *range.end() {
134            value
135        } else {
136            *range.end()
137        };
138
139        VerticalSlider {
140            value,
141            default: None,
142            range,
143            step: T::from(1),
144            shift_step: None,
145            on_change: Box::new(on_change),
146            on_release: None,
147            width: Self::DEFAULT_WIDTH,
148            height: Length::Fill,
149            class: Theme::default(),
150            status: None,
151        }
152    }
153
154    /// Sets the optional default value for the [`VerticalSlider`].
155    ///
156    /// If set, the [`VerticalSlider`] will reset to this value when ctrl-clicked or command-clicked.
157    pub fn default(mut self, default: impl Into<T>) -> Self {
158        self.default = Some(default.into());
159        self
160    }
161
162    /// Sets the release message of the [`VerticalSlider`].
163    /// This is called when the mouse is released from the slider.
164    ///
165    /// Typically, the user's interaction with the slider is finished when this message is produced.
166    /// This is useful if you need to spawn a long-running task from the slider's result, where
167    /// the default on_change message could create too many events.
168    pub fn on_release(mut self, on_release: Message) -> Self {
169        self.on_release = Some(on_release);
170        self
171    }
172
173    /// Sets the width of the [`VerticalSlider`].
174    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
175        self.width = width.into().0;
176        self
177    }
178
179    /// Sets the height of the [`VerticalSlider`].
180    pub fn height(mut self, height: impl Into<Length>) -> Self {
181        self.height = height.into();
182        self
183    }
184
185    /// Sets the step size of the [`VerticalSlider`].
186    pub fn step(mut self, step: T) -> Self {
187        self.step = step;
188        self
189    }
190
191    /// Sets the optional "shift" step for the [`VerticalSlider`].
192    ///
193    /// If set, this value is used as the step while the shift key is pressed.
194    pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
195        self.shift_step = Some(shift_step.into());
196        self
197    }
198
199    /// Sets the style of the [`VerticalSlider`].
200    #[must_use]
201    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
202    where
203        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
204    {
205        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
206        self
207    }
208
209    /// Sets the style class of the [`VerticalSlider`].
210    #[cfg(feature = "advanced")]
211    #[must_use]
212    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
213        self.class = class.into();
214        self
215    }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
219struct State {
220    is_dragging: bool,
221    is_focused: bool,
222    focus_visible: bool,
223    keyboard_modifiers: keyboard::Modifiers,
224}
225
226impl Focusable for State {
227    fn is_focused(&self) -> bool {
228        self.is_focused
229    }
230
231    fn focus(&mut self) {
232        self.is_focused = true;
233        self.focus_visible = true;
234    }
235
236    fn unfocus(&mut self) {
237        self.is_focused = false;
238        self.focus_visible = false;
239    }
240}
241
242impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
243    for VerticalSlider<'_, T, Message, Theme>
244where
245    T: Copy + Into<f64> + num_traits::FromPrimitive,
246    Message: Clone,
247    Theme: Catalog,
248    Renderer: core::Renderer,
249{
250    fn tag(&self) -> tree::Tag {
251        tree::Tag::of::<State>()
252    }
253
254    fn state(&self) -> tree::State {
255        tree::State::new(State::default())
256    }
257
258    fn size(&self) -> Size<Length> {
259        Size {
260            width: Length::Shrink,
261            height: self.height,
262        }
263    }
264
265    fn layout(
266        &mut self,
267        _tree: &mut Tree,
268        _renderer: &Renderer,
269        limits: &layout::Limits,
270    ) -> layout::Node {
271        layout::atomic(limits, self.width, self.height)
272    }
273
274    fn operate(
275        &mut self,
276        tree: &mut Tree,
277        layout: Layout<'_>,
278        _renderer: &Renderer,
279        operation: &mut dyn Operation,
280    ) {
281        let state = tree.state.downcast_mut::<State>();
282
283        operation.accessible(
284            None,
285            layout.bounds(),
286            &Accessible {
287                role: Role::Slider,
288                value: Some(Value::Numeric {
289                    current: self.value.into(),
290                    min: (*self.range.start()).into(),
291                    max: (*self.range.end()).into(),
292                    step: Some(self.step.into()),
293                }),
294                orientation: Some(Orientation::Vertical),
295                ..Accessible::default()
296            },
297        );
298
299        operation.focusable(None, layout.bounds(), state);
300    }
301
302    fn update(
303        &mut self,
304        tree: &mut Tree,
305        event: &Event,
306        layout: Layout<'_>,
307        cursor: mouse::Cursor,
308        _renderer: &Renderer,
309        shell: &mut Shell<'_, Message>,
310        _viewport: &Rectangle,
311    ) {
312        let state = tree.state.downcast_mut::<State>();
313        let is_dragging = state.is_dragging;
314        let current_value = self.value;
315
316        let locate = |cursor_position: Point| -> Option<T> {
317            let bounds = layout.bounds();
318
319            if cursor_position.y >= bounds.y + bounds.height {
320                Some(*self.range.start())
321            } else if cursor_position.y <= bounds.y {
322                Some(*self.range.end())
323            } else {
324                let step = if state.keyboard_modifiers.shift() {
325                    self.shift_step.unwrap_or(self.step)
326                } else {
327                    self.step
328                }
329                .into();
330
331                let start = (*self.range.start()).into();
332                let end = (*self.range.end()).into();
333
334                let percent =
335                    1.0 - f64::from(cursor_position.y - bounds.y) / f64::from(bounds.height);
336
337                let steps = (percent * (end - start) / step).round();
338                let value = steps * step + start;
339
340                T::from_f64(value.min(end))
341            }
342        };
343
344        let increment = |value: T| -> Option<T> {
345            let step = if state.keyboard_modifiers.shift() {
346                self.shift_step.unwrap_or(self.step)
347            } else {
348                self.step
349            }
350            .into();
351
352            let steps = (value.into() / step).round();
353            let new_value = step * (steps + 1.0);
354
355            if new_value > (*self.range.end()).into() {
356                return Some(*self.range.end());
357            }
358
359            T::from_f64(new_value)
360        };
361
362        let decrement = |value: T| -> Option<T> {
363            let step = if state.keyboard_modifiers.shift() {
364                self.shift_step.unwrap_or(self.step)
365            } else {
366                self.step
367            }
368            .into();
369
370            let steps = (value.into() / step).round();
371            let new_value = step * (steps - 1.0);
372
373            if new_value < (*self.range.start()).into() {
374                return Some(*self.range.start());
375            }
376
377            T::from_f64(new_value)
378        };
379
380        let page_increment = |value: T| -> Option<T> {
381            let step = self.step.into() * 10.0;
382            let new_value = value.into() + step;
383
384            if new_value > (*self.range.end()).into() {
385                return Some(*self.range.end());
386            }
387
388            T::from_f64(new_value)
389        };
390
391        let page_decrement = |value: T| -> Option<T> {
392            let step = self.step.into() * 10.0;
393            let new_value = value.into() - step;
394
395            if new_value < (*self.range.start()).into() {
396                return Some(*self.range.start());
397            }
398
399            T::from_f64(new_value)
400        };
401
402        let mut change = |new_value: T| {
403            if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
404                shell.publish((self.on_change)(new_value));
405
406                self.value = new_value;
407            }
408        };
409
410        match event {
411            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
412            | Event::Touch(touch::Event::FingerPressed { .. }) => {
413                if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
414                    if state.keyboard_modifiers.control() || state.keyboard_modifiers.command() {
415                        let _ = self.default.map(change);
416                        state.is_dragging = false;
417                    } else {
418                        let _ = locate(cursor_position).map(change);
419                        state.is_dragging = true;
420                    }
421
422                    state.is_focused = true;
423                    state.focus_visible = false;
424
425                    shell.capture_event();
426                } else {
427                    state.is_focused = false;
428                    state.focus_visible = false;
429                }
430            }
431            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
432            | Event::Touch(touch::Event::FingerLifted { .. })
433            | Event::Touch(touch::Event::FingerLost { .. }) => {
434                if is_dragging {
435                    if let Some(on_release) = self.on_release.clone() {
436                        shell.publish(on_release);
437                    }
438                    state.is_dragging = false;
439                }
440            }
441            Event::Mouse(mouse::Event::CursorMoved { .. })
442            | Event::Touch(touch::Event::FingerMoved { .. }) => {
443                if is_dragging {
444                    let _ = cursor.land().position().and_then(locate).map(change);
445
446                    shell.capture_event();
447                }
448            }
449            Event::Mouse(mouse::Event::WheelScrolled { delta })
450                if state.keyboard_modifiers.control() =>
451            {
452                if cursor.is_over(layout.bounds()) {
453                    let delta = match *delta {
454                        mouse::ScrollDelta::Lines { x: _, y } => y,
455                        mouse::ScrollDelta::Pixels { x: _, y } => y,
456                    };
457
458                    if delta < 0.0 {
459                        let _ = decrement(current_value).map(change);
460                    } else {
461                        let _ = increment(current_value).map(change);
462                    }
463
464                    shell.capture_event();
465                }
466            }
467            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
468                if cursor.is_over(layout.bounds()) || state.is_focused {
469                    match key {
470                        Key::Named(key::Named::ArrowUp | key::Named::ArrowRight) => {
471                            let _ = increment(current_value).map(change);
472                            shell.capture_event();
473                        }
474                        Key::Named(key::Named::ArrowDown | key::Named::ArrowLeft) => {
475                            let _ = decrement(current_value).map(change);
476                            shell.capture_event();
477                        }
478                        Key::Named(key::Named::PageUp) => {
479                            let _ = page_increment(current_value).map(change);
480                            shell.capture_event();
481                        }
482                        Key::Named(key::Named::PageDown) => {
483                            let _ = page_decrement(current_value).map(change);
484                            shell.capture_event();
485                        }
486                        Key::Named(key::Named::Home) => {
487                            change(*self.range.start());
488                            shell.capture_event();
489                        }
490                        Key::Named(key::Named::End) => {
491                            change(*self.range.end());
492                            shell.capture_event();
493                        }
494                        Key::Named(key::Named::Escape) => {
495                            if state.is_focused {
496                                state.is_focused = false;
497                                state.focus_visible = false;
498                                shell.capture_event();
499                            }
500                        }
501                        _ => (),
502                    }
503                }
504            }
505            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
506                state.keyboard_modifiers = *modifiers;
507            }
508            _ => {}
509        }
510
511        let current_status = if state.is_dragging {
512            Status::Dragged
513        } else if state.focus_visible {
514            Status::Focused
515        } else if cursor.is_over(layout.bounds()) {
516            Status::Hovered
517        } else {
518            Status::Active
519        };
520
521        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
522            self.status = Some(current_status);
523        } else if self.status.is_some_and(|status| status != current_status) {
524            shell.request_redraw();
525        }
526    }
527
528    fn draw(
529        &self,
530        _tree: &Tree,
531        renderer: &mut Renderer,
532        theme: &Theme,
533        _style: &renderer::Style,
534        layout: Layout<'_>,
535        _cursor: mouse::Cursor,
536        _viewport: &Rectangle,
537    ) {
538        let bounds = layout.bounds();
539
540        let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
541
542        let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
543            HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
544            HandleShape::Rectangle {
545                width,
546                border_radius,
547            } => (f32::from(width), bounds.width, border_radius),
548        };
549
550        let value = self.value.into() as f32;
551        let (range_start, range_end) = {
552            let (start, end) = self.range.clone().into_inner();
553
554            (start.into() as f32, end.into() as f32)
555        };
556
557        let offset = if range_start >= range_end {
558            0.0
559        } else {
560            (bounds.height - handle_width) * (value - range_end) / (range_start - range_end)
561        };
562
563        let rail_x = bounds.x + bounds.width / 2.0;
564
565        renderer.fill_quad(
566            renderer::Quad {
567                bounds: Rectangle {
568                    x: rail_x - style.rail.width / 2.0,
569                    y: bounds.y,
570                    width: style.rail.width,
571                    height: offset + handle_width / 2.0,
572                },
573                border: style.rail.border,
574                ..renderer::Quad::default()
575            },
576            style.rail.backgrounds.1,
577        );
578
579        renderer.fill_quad(
580            renderer::Quad {
581                bounds: Rectangle {
582                    x: rail_x - style.rail.width / 2.0,
583                    y: bounds.y + offset + handle_width / 2.0,
584                    width: style.rail.width,
585                    height: bounds.height - offset - handle_width / 2.0,
586                },
587                border: style.rail.border,
588                ..renderer::Quad::default()
589            },
590            style.rail.backgrounds.0,
591        );
592
593        renderer.fill_quad(
594            renderer::Quad {
595                bounds: Rectangle {
596                    x: rail_x - handle_height / 2.0,
597                    y: bounds.y + offset,
598                    width: handle_height,
599                    height: handle_width,
600                },
601                border: Border {
602                    radius: handle_border_radius,
603                    width: style.handle.border_width,
604                    color: style.handle.border_color,
605                },
606                shadow: style.handle.shadow,
607                ..renderer::Quad::default()
608            },
609            style.handle.background,
610        );
611    }
612
613    fn mouse_interaction(
614        &self,
615        tree: &Tree,
616        layout: Layout<'_>,
617        cursor: mouse::Cursor,
618        _viewport: &Rectangle,
619        _renderer: &Renderer,
620    ) -> mouse::Interaction {
621        let state = tree.state.downcast_ref::<State>();
622
623        if state.is_dragging {
624            // FIXME: Fall back to `Pointer` on Windows
625            // See https://github.com/rust-windowing/winit/issues/1043
626            if cfg!(target_os = "windows") {
627                mouse::Interaction::Pointer
628            } else {
629                mouse::Interaction::Grabbing
630            }
631        } else if cursor.is_over(layout.bounds()) {
632            if cfg!(target_os = "windows") {
633                mouse::Interaction::Pointer
634            } else {
635                mouse::Interaction::Grab
636            }
637        } else {
638            mouse::Interaction::default()
639        }
640    }
641}
642
643impl<'a, T, Message, Theme, Renderer> From<VerticalSlider<'a, T, Message, Theme>>
644    for Element<'a, Message, Theme, Renderer>
645where
646    T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
647    Message: Clone + 'a,
648    Theme: Catalog + 'a,
649    Renderer: core::Renderer + 'a,
650{
651    fn from(
652        slider: VerticalSlider<'a, T, Message, Theme>,
653    ) -> Element<'a, Message, Theme, Renderer> {
654        Element::new(slider)
655    }
656}
657
658/// The possible status of a [`VerticalSlider`].
659#[derive(Debug, Clone, Copy, PartialEq, Eq)]
660pub enum Status {
661    /// The [`VerticalSlider`] can be interacted with.
662    Active,
663    /// The [`VerticalSlider`] is being hovered.
664    Hovered,
665    /// The [`VerticalSlider`] is being dragged.
666    Dragged,
667    /// The [`VerticalSlider`] has keyboard focus.
668    Focused,
669}
670
671/// The theme catalog of a [`VerticalSlider`].
672pub trait Catalog: Sized {
673    /// The item class of the [`Catalog`].
674    type Class<'a>;
675
676    /// The default class produced by the [`Catalog`].
677    fn default<'a>() -> Self::Class<'a>;
678
679    /// The [`Style`] of a class with the given status.
680    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
681}
682
683/// A styling function for a [`VerticalSlider`].
684pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
685
686impl Catalog for Theme {
687    type Class<'a> = StyleFn<'a, Self>;
688
689    fn default<'a>() -> Self::Class<'a> {
690        Box::new(default)
691    }
692
693    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
694        class(self, status)
695    }
696}
697
698/// The default style of a [`VerticalSlider`].
699pub fn default(theme: &Theme, status: Status) -> Style {
700    let palette = theme.palette();
701
702    let color = match status {
703        Status::Active => palette.primary.base.color,
704        Status::Hovered => palette.primary.strong.color,
705        Status::Dragged => palette.primary.weak.color,
706        Status::Focused => palette.primary.strong.color,
707    };
708
709    Style {
710        rail: Rail {
711            backgrounds: (color.into(), palette.background.strong.color.into()),
712            width: 4.0,
713            border: Border {
714                radius: 2.0.into(),
715                width: 0.0,
716                color: Color::TRANSPARENT,
717            },
718        },
719        handle: Handle {
720            shape: HandleShape::Circle { radius: 7.0 },
721            background: color.into(),
722            border_color: match status {
723                Status::Focused => {
724                    let accent = palette.primary.strong.color;
725                    let page_bg = palette.background.base.color;
726                    palette::focus_border_color(color, accent, page_bg)
727                }
728                _ => Color::TRANSPARENT,
729            },
730            border_width: match status {
731                Status::Focused => 2.0,
732                _ => 0.0,
733            },
734            shadow: match status {
735                Status::Focused => palette::focus_shadow(
736                    palette.primary.strong.color,
737                    palette.background.base.color,
738                ),
739                _ => Shadow::default(),
740            },
741        },
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use crate::core::widget::operation::focusable::Focusable;
749
750    #[test]
751    fn focusable_trait() {
752        let mut state = State::default();
753        assert!(!state.is_focused());
754        assert!(!state.focus_visible);
755        state.focus();
756        assert!(state.is_focused());
757        assert!(state.focus_visible);
758        state.unfocus();
759        assert!(!state.is_focused());
760        assert!(!state.focus_visible);
761    }
762
763    #[test]
764    fn default_state_not_focused() {
765        let state = State::default();
766        assert!(!state.is_focused);
767        assert!(!state.is_dragging);
768        assert!(!state.focus_visible);
769    }
770
771    #[test]
772    fn focus_independent_of_drag() {
773        let mut state = State::default();
774
775        state.focus();
776        assert!(!state.is_dragging);
777
778        state.is_dragging = true;
779        assert!(state.is_focused());
780
781        state.unfocus();
782        assert!(state.is_dragging);
783    }
784}