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::{Catalog, Handle, HandleShape, Status, Style, StyleFn, default};
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::touch;
42use crate::core::widget::tree::{self, Tree};
43use crate::core::window;
44use crate::core::{
45    self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Widget,
46};
47
48/// An vertical bar and a handle that selects a single value from a range of
49/// values.
50///
51/// A [`VerticalSlider`] will try to fill the vertical space of its container.
52///
53/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
54/// to 1 unit.
55///
56/// # Example
57/// ```no_run
58/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
59/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
60/// #
61/// use iced::widget::vertical_slider;
62///
63/// struct State {
64///    value: f32,
65/// }
66///
67/// #[derive(Debug, Clone)]
68/// enum Message {
69///     ValueChanged(f32),
70/// }
71///
72/// fn view(state: &State) -> Element<'_, Message> {
73///     vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into()
74/// }
75///
76/// fn update(state: &mut State, message: Message) {
77///     match message {
78///         Message::ValueChanged(value) => {
79///             state.value = value;
80///         }
81///     }
82/// }
83/// ```
84pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
85where
86    Theme: Catalog,
87{
88    range: RangeInclusive<T>,
89    step: T,
90    shift_step: Option<T>,
91    value: T,
92    default: Option<T>,
93    on_change: Box<dyn Fn(T) -> Message + 'a>,
94    on_release: Option<Message>,
95    width: f32,
96    height: Length,
97    class: Theme::Class<'a>,
98    status: Option<Status>,
99}
100
101impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
102where
103    T: Copy + From<u8> + std::cmp::PartialOrd,
104    Message: Clone,
105    Theme: Catalog,
106{
107    /// The default width of a [`VerticalSlider`].
108    pub const DEFAULT_WIDTH: f32 = 16.0;
109
110    /// Creates a new [`VerticalSlider`].
111    ///
112    /// It expects:
113    ///   * an inclusive range of possible values
114    ///   * the current value of the [`VerticalSlider`]
115    ///   * a function that will be called when the [`VerticalSlider`] is dragged.
116    ///     It receives the new value of the [`VerticalSlider`] and must produce a
117    ///     `Message`.
118    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
119    where
120        F: 'a + Fn(T) -> Message,
121    {
122        let value = if value >= *range.start() {
123            value
124        } else {
125            *range.start()
126        };
127
128        let value = if value <= *range.end() {
129            value
130        } else {
131            *range.end()
132        };
133
134        VerticalSlider {
135            value,
136            default: None,
137            range,
138            step: T::from(1),
139            shift_step: None,
140            on_change: Box::new(on_change),
141            on_release: None,
142            width: Self::DEFAULT_WIDTH,
143            height: Length::Fill,
144            class: Theme::default(),
145            status: None,
146        }
147    }
148
149    /// Sets the optional default value for the [`VerticalSlider`].
150    ///
151    /// If set, the [`VerticalSlider`] will reset to this value when ctrl-clicked or command-clicked.
152    pub fn default(mut self, default: impl Into<T>) -> Self {
153        self.default = Some(default.into());
154        self
155    }
156
157    /// Sets the release message of the [`VerticalSlider`].
158    /// This is called when the mouse is released from the slider.
159    ///
160    /// Typically, the user's interaction with the slider is finished when this message is produced.
161    /// This is useful if you need to spawn a long-running task from the slider's result, where
162    /// the default on_change message could create too many events.
163    pub fn on_release(mut self, on_release: Message) -> Self {
164        self.on_release = Some(on_release);
165        self
166    }
167
168    /// Sets the width of the [`VerticalSlider`].
169    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
170        self.width = width.into().0;
171        self
172    }
173
174    /// Sets the height of the [`VerticalSlider`].
175    pub fn height(mut self, height: impl Into<Length>) -> Self {
176        self.height = height.into();
177        self
178    }
179
180    /// Sets the step size of the [`VerticalSlider`].
181    pub fn step(mut self, step: T) -> Self {
182        self.step = step;
183        self
184    }
185
186    /// Sets the optional "shift" step for the [`VerticalSlider`].
187    ///
188    /// If set, this value is used as the step while the shift key is pressed.
189    pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
190        self.shift_step = Some(shift_step.into());
191        self
192    }
193
194    /// Sets the style of the [`VerticalSlider`].
195    #[must_use]
196    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
197    where
198        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
199    {
200        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
201        self
202    }
203
204    /// Sets the style class of the [`VerticalSlider`].
205    #[cfg(feature = "advanced")]
206    #[must_use]
207    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
208        self.class = class.into();
209        self
210    }
211}
212
213impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
214    for VerticalSlider<'_, T, Message, Theme>
215where
216    T: Copy + Into<f64> + num_traits::FromPrimitive,
217    Message: Clone,
218    Theme: Catalog,
219    Renderer: core::Renderer,
220{
221    fn tag(&self) -> tree::Tag {
222        tree::Tag::of::<State>()
223    }
224
225    fn state(&self) -> tree::State {
226        tree::State::new(State::default())
227    }
228
229    fn size(&self) -> Size<Length> {
230        Size {
231            width: Length::Shrink,
232            height: self.height,
233        }
234    }
235
236    fn layout(
237        &mut self,
238        _tree: &mut Tree,
239        _renderer: &Renderer,
240        limits: &layout::Limits,
241    ) -> layout::Node {
242        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        _clipboard: &mut dyn Clipboard,
253        shell: &mut Shell<'_, Message>,
254        _viewport: &Rectangle,
255    ) {
256        let state = tree.state.downcast_mut::<State>();
257        let is_dragging = state.is_dragging;
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.y >= bounds.y + bounds.height {
264                Some(*self.range.start())
265            } else if cursor_position.y <= bounds.y {
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                .into();
274
275                let start = (*self.range.start()).into();
276                let end = (*self.range.end()).into();
277
278                let percent =
279                    1.0 - f64::from(cursor_position.y - bounds.y) / f64::from(bounds.height);
280
281                let steps = (percent * (end - start) / step).round();
282                let value = steps * step + start;
283
284                T::from_f64(value.min(end))
285            }
286        };
287
288        let increment = |value: T| -> Option<T> {
289            let step = if state.keyboard_modifiers.shift() {
290                self.shift_step.unwrap_or(self.step)
291            } else {
292                self.step
293            }
294            .into();
295
296            let steps = (value.into() / step).round();
297            let new_value = step * (steps + 1.0);
298
299            if new_value > (*self.range.end()).into() {
300                return Some(*self.range.end());
301            }
302
303            T::from_f64(new_value)
304        };
305
306        let decrement = |value: T| -> Option<T> {
307            let step = if state.keyboard_modifiers.shift() {
308                self.shift_step.unwrap_or(self.step)
309            } else {
310                self.step
311            }
312            .into();
313
314            let steps = (value.into() / step).round();
315            let new_value = step * (steps - 1.0);
316
317            if new_value < (*self.range.start()).into() {
318                return Some(*self.range.start());
319            }
320
321            T::from_f64(new_value)
322        };
323
324        let change = |new_value: T| {
325            if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
326                shell.publish((self.on_change)(new_value));
327
328                self.value = new_value;
329            }
330        };
331
332        match event {
333            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
334            | Event::Touch(touch::Event::FingerPressed { .. }) => {
335                if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
336                    if state.keyboard_modifiers.control() || state.keyboard_modifiers.command() {
337                        let _ = self.default.map(change);
338                        state.is_dragging = false;
339                    } else {
340                        let _ = locate(cursor_position).map(change);
341                        state.is_dragging = true;
342                    }
343
344                    shell.capture_event();
345                }
346            }
347            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
348            | Event::Touch(touch::Event::FingerLifted { .. })
349            | Event::Touch(touch::Event::FingerLost { .. }) => {
350                if is_dragging {
351                    if let Some(on_release) = self.on_release.clone() {
352                        shell.publish(on_release);
353                    }
354                    state.is_dragging = false;
355                }
356            }
357            Event::Mouse(mouse::Event::CursorMoved { .. })
358            | Event::Touch(touch::Event::FingerMoved { .. }) => {
359                if is_dragging {
360                    let _ = cursor.land().position().and_then(locate).map(change);
361
362                    shell.capture_event();
363                }
364            }
365            Event::Mouse(mouse::Event::WheelScrolled { delta })
366                if state.keyboard_modifiers.control() =>
367            {
368                if cursor.is_over(layout.bounds()) {
369                    let delta = match *delta {
370                        mouse::ScrollDelta::Lines { x: _, y } => y,
371                        mouse::ScrollDelta::Pixels { x: _, y } => y,
372                    };
373
374                    if delta < 0.0 {
375                        let _ = decrement(current_value).map(change);
376                    } else {
377                        let _ = increment(current_value).map(change);
378                    }
379
380                    shell.capture_event();
381                }
382            }
383            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
384                if cursor.is_over(layout.bounds()) {
385                    match key {
386                        Key::Named(key::Named::ArrowUp) => {
387                            let _ = increment(current_value).map(change);
388                            shell.capture_event();
389                        }
390                        Key::Named(key::Named::ArrowDown) => {
391                            let _ = decrement(current_value).map(change);
392                            shell.capture_event();
393                        }
394                        _ => (),
395                    }
396                }
397            }
398            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
399                state.keyboard_modifiers = *modifiers;
400            }
401            _ => {}
402        }
403
404        let current_status = if state.is_dragging {
405            Status::Dragged
406        } else if cursor.is_over(layout.bounds()) {
407            Status::Hovered
408        } else {
409            Status::Active
410        };
411
412        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
413            self.status = Some(current_status);
414        } else if self.status.is_some_and(|status| status != current_status) {
415            shell.request_redraw();
416        }
417    }
418
419    fn draw(
420        &self,
421        _tree: &Tree,
422        renderer: &mut Renderer,
423        theme: &Theme,
424        _style: &renderer::Style,
425        layout: Layout<'_>,
426        _cursor: mouse::Cursor,
427        _viewport: &Rectangle,
428    ) {
429        let bounds = layout.bounds();
430
431        let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
432
433        let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
434            HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
435            HandleShape::Rectangle {
436                width,
437                border_radius,
438            } => (f32::from(width), bounds.width, border_radius),
439        };
440
441        let value = self.value.into() as f32;
442        let (range_start, range_end) = {
443            let (start, end) = self.range.clone().into_inner();
444
445            (start.into() as f32, end.into() as f32)
446        };
447
448        let offset = if range_start >= range_end {
449            0.0
450        } else {
451            (bounds.height - handle_width) * (value - range_end) / (range_start - range_end)
452        };
453
454        let rail_x = bounds.x + bounds.width / 2.0;
455
456        renderer.fill_quad(
457            renderer::Quad {
458                bounds: Rectangle {
459                    x: rail_x - style.rail.width / 2.0,
460                    y: bounds.y,
461                    width: style.rail.width,
462                    height: offset + handle_width / 2.0,
463                },
464                border: style.rail.border,
465                ..renderer::Quad::default()
466            },
467            style.rail.backgrounds.1,
468        );
469
470        renderer.fill_quad(
471            renderer::Quad {
472                bounds: Rectangle {
473                    x: rail_x - style.rail.width / 2.0,
474                    y: bounds.y + offset + handle_width / 2.0,
475                    width: style.rail.width,
476                    height: bounds.height - offset - handle_width / 2.0,
477                },
478                border: style.rail.border,
479                ..renderer::Quad::default()
480            },
481            style.rail.backgrounds.0,
482        );
483
484        renderer.fill_quad(
485            renderer::Quad {
486                bounds: Rectangle {
487                    x: rail_x - handle_height / 2.0,
488                    y: bounds.y + offset,
489                    width: handle_height,
490                    height: handle_width,
491                },
492                border: Border {
493                    radius: handle_border_radius,
494                    width: style.handle.border_width,
495                    color: style.handle.border_color,
496                },
497                ..renderer::Quad::default()
498            },
499            style.handle.background,
500        );
501    }
502
503    fn mouse_interaction(
504        &self,
505        tree: &Tree,
506        layout: Layout<'_>,
507        cursor: mouse::Cursor,
508        _viewport: &Rectangle,
509        _renderer: &Renderer,
510    ) -> mouse::Interaction {
511        let state = tree.state.downcast_ref::<State>();
512
513        if state.is_dragging {
514            // FIXME: Fall back to `Pointer` on Windows
515            // See https://github.com/rust-windowing/winit/issues/1043
516            if cfg!(target_os = "windows") {
517                mouse::Interaction::Pointer
518            } else {
519                mouse::Interaction::Grabbing
520            }
521        } else if cursor.is_over(layout.bounds()) {
522            if cfg!(target_os = "windows") {
523                mouse::Interaction::Pointer
524            } else {
525                mouse::Interaction::Grab
526            }
527        } else {
528            mouse::Interaction::default()
529        }
530    }
531}
532
533impl<'a, T, Message, Theme, Renderer> From<VerticalSlider<'a, T, Message, Theme>>
534    for Element<'a, Message, Theme, Renderer>
535where
536    T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
537    Message: Clone + 'a,
538    Theme: Catalog + 'a,
539    Renderer: core::Renderer + 'a,
540{
541    fn from(
542        slider: VerticalSlider<'a, T, Message, Theme>,
543    ) -> Element<'a, Message, Theme, Renderer> {
544        Element::new(slider)
545    }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
549struct State {
550    is_dragging: bool,
551    keyboard_modifiers: keyboard::Modifiers,
552}