iced_native/widget/
vertical_slider.rs

1//! Display an interactive selector of a single value from a range of values.
2//!
3//! A [`VerticalSlider`] has some local [`State`].
4use std::ops::RangeInclusive;
5
6pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
7
8use crate::event::{self, Event};
9use crate::widget::tree::{self, Tree};
10use crate::{
11    layout, mouse, renderer, touch, Clipboard, Color, Element, Layout, Length,
12    Pixels, Point, Rectangle, Shell, Size, Widget,
13};
14
15/// An vertical bar and a handle that selects a single value from a range of
16/// values.
17///
18/// A [`VerticalSlider`] will try to fill the vertical space of its container.
19///
20/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
21/// to 1 unit.
22///
23/// # Example
24/// ```
25/// # use iced_native::widget::vertical_slider;
26/// # use iced_native::renderer::Null;
27/// #
28/// # type VerticalSlider<'a, T, Message> = vertical_slider::VerticalSlider<'a, T, Message, Null>;
29/// #
30/// #[derive(Clone)]
31/// pub enum Message {
32///     SliderChanged(f32),
33/// }
34///
35/// let value = 50.0;
36///
37/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
38/// ```
39#[allow(missing_debug_implementations)]
40pub struct VerticalSlider<'a, T, Message, Renderer>
41where
42    Renderer: crate::Renderer,
43    Renderer::Theme: StyleSheet,
44{
45    range: RangeInclusive<T>,
46    step: T,
47    value: T,
48    on_change: Box<dyn Fn(T) -> Message + 'a>,
49    on_release: Option<Message>,
50    width: f32,
51    height: Length,
52    style: <Renderer::Theme as StyleSheet>::Style,
53}
54
55impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer>
56where
57    T: Copy + From<u8> + std::cmp::PartialOrd,
58    Message: Clone,
59    Renderer: crate::Renderer,
60    Renderer::Theme: StyleSheet,
61{
62    /// The default width of a [`VerticalSlider`].
63    pub const DEFAULT_WIDTH: f32 = 22.0;
64
65    /// Creates a new [`VerticalSlider`].
66    ///
67    /// It expects:
68    ///   * an inclusive range of possible values
69    ///   * the current value of the [`VerticalSlider`]
70    ///   * a function that will be called when the [`VerticalSlider`] is dragged.
71    ///   It receives the new value of the [`VerticalSlider`] and must produce a
72    ///   `Message`.
73    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
74    where
75        F: 'a + Fn(T) -> Message,
76    {
77        let value = if value >= *range.start() {
78            value
79        } else {
80            *range.start()
81        };
82
83        let value = if value <= *range.end() {
84            value
85        } else {
86            *range.end()
87        };
88
89        VerticalSlider {
90            value,
91            range,
92            step: T::from(1),
93            on_change: Box::new(on_change),
94            on_release: None,
95            width: Self::DEFAULT_WIDTH,
96            height: Length::Fill,
97            style: Default::default(),
98        }
99    }
100
101    /// Sets the release message of the [`VerticalSlider`].
102    /// This is called when the mouse is released from the slider.
103    ///
104    /// Typically, the user's interaction with the slider is finished when this message is produced.
105    /// This is useful if you need to spawn a long-running task from the slider's result, where
106    /// the default on_change message could create too many events.
107    pub fn on_release(mut self, on_release: Message) -> Self {
108        self.on_release = Some(on_release);
109        self
110    }
111
112    /// Sets the width of the [`VerticalSlider`].
113    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
114        self.width = width.into().0;
115        self
116    }
117
118    /// Sets the height of the [`VerticalSlider`].
119    pub fn height(mut self, height: impl Into<Length>) -> Self {
120        self.height = height.into();
121        self
122    }
123
124    /// Sets the style of the [`VerticalSlider`].
125    pub fn style(
126        mut self,
127        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
128    ) -> Self {
129        self.style = style.into();
130        self
131    }
132
133    /// Sets the step size of the [`VerticalSlider`].
134    pub fn step(mut self, step: T) -> Self {
135        self.step = step;
136        self
137    }
138}
139
140impl<'a, T, Message, Renderer> Widget<Message, Renderer>
141    for VerticalSlider<'a, T, Message, Renderer>
142where
143    T: Copy + Into<f64> + num_traits::FromPrimitive,
144    Message: Clone,
145    Renderer: crate::Renderer,
146    Renderer::Theme: StyleSheet,
147{
148    fn tag(&self) -> tree::Tag {
149        tree::Tag::of::<State>()
150    }
151
152    fn state(&self) -> tree::State {
153        tree::State::new(State::new())
154    }
155
156    fn width(&self) -> Length {
157        Length::Shrink
158    }
159
160    fn height(&self) -> Length {
161        self.height
162    }
163
164    fn layout(
165        &self,
166        _renderer: &Renderer,
167        limits: &layout::Limits,
168    ) -> layout::Node {
169        let limits = limits.width(self.width).height(self.height);
170        let size = limits.resolve(Size::ZERO);
171
172        layout::Node::new(size)
173    }
174
175    fn on_event(
176        &mut self,
177        tree: &mut Tree,
178        event: Event,
179        layout: Layout<'_>,
180        cursor_position: Point,
181        _renderer: &Renderer,
182        _clipboard: &mut dyn Clipboard,
183        shell: &mut Shell<'_, Message>,
184    ) -> event::Status {
185        update(
186            event,
187            layout,
188            cursor_position,
189            shell,
190            tree.state.downcast_mut::<State>(),
191            &mut self.value,
192            &self.range,
193            self.step,
194            self.on_change.as_ref(),
195            &self.on_release,
196        )
197    }
198
199    fn draw(
200        &self,
201        tree: &Tree,
202        renderer: &mut Renderer,
203        theme: &Renderer::Theme,
204        _style: &renderer::Style,
205        layout: Layout<'_>,
206        cursor_position: Point,
207        _viewport: &Rectangle,
208    ) {
209        draw(
210            renderer,
211            layout,
212            cursor_position,
213            tree.state.downcast_ref::<State>(),
214            self.value,
215            &self.range,
216            theme,
217            &self.style,
218        )
219    }
220
221    fn mouse_interaction(
222        &self,
223        tree: &Tree,
224        layout: Layout<'_>,
225        cursor_position: Point,
226        _viewport: &Rectangle,
227        _renderer: &Renderer,
228    ) -> mouse::Interaction {
229        mouse_interaction(
230            layout,
231            cursor_position,
232            tree.state.downcast_ref::<State>(),
233        )
234    }
235}
236
237impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>>
238    for Element<'a, Message, Renderer>
239where
240    T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
241    Message: 'a + Clone,
242    Renderer: 'a + crate::Renderer,
243    Renderer::Theme: StyleSheet,
244{
245    fn from(
246        slider: VerticalSlider<'a, T, Message, Renderer>,
247    ) -> Element<'a, Message, Renderer> {
248        Element::new(slider)
249    }
250}
251
252/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
253/// accordingly.
254pub fn update<Message, T>(
255    event: Event,
256    layout: Layout<'_>,
257    cursor_position: Point,
258    shell: &mut Shell<'_, Message>,
259    state: &mut State,
260    value: &mut T,
261    range: &RangeInclusive<T>,
262    step: T,
263    on_change: &dyn Fn(T) -> Message,
264    on_release: &Option<Message>,
265) -> event::Status
266where
267    T: Copy + Into<f64> + num_traits::FromPrimitive,
268    Message: Clone,
269{
270    let is_dragging = state.is_dragging;
271
272    let mut change = || {
273        let bounds = layout.bounds();
274        let new_value = if cursor_position.y >= bounds.y + bounds.height {
275            *range.start()
276        } else if cursor_position.y <= bounds.y {
277            *range.end()
278        } else {
279            let step = step.into();
280            let start = (*range.start()).into();
281            let end = (*range.end()).into();
282
283            let percent = 1.0
284                - f64::from(cursor_position.y - bounds.y)
285                    / f64::from(bounds.height);
286
287            let steps = (percent * (end - start) / step).round();
288            let value = steps * step + start;
289
290            if let Some(value) = T::from_f64(value) {
291                value
292            } else {
293                return;
294            }
295        };
296
297        if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
298            shell.publish((on_change)(new_value));
299
300            *value = new_value;
301        }
302    };
303
304    match event {
305        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
306        | Event::Touch(touch::Event::FingerPressed { .. }) => {
307            if layout.bounds().contains(cursor_position) {
308                change();
309                state.is_dragging = true;
310
311                return event::Status::Captured;
312            }
313        }
314        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
315        | Event::Touch(touch::Event::FingerLifted { .. })
316        | Event::Touch(touch::Event::FingerLost { .. }) => {
317            if is_dragging {
318                if let Some(on_release) = on_release.clone() {
319                    shell.publish(on_release);
320                }
321                state.is_dragging = false;
322
323                return event::Status::Captured;
324            }
325        }
326        Event::Mouse(mouse::Event::CursorMoved { .. })
327        | Event::Touch(touch::Event::FingerMoved { .. }) => {
328            if is_dragging {
329                change();
330
331                return event::Status::Captured;
332            }
333        }
334        _ => {}
335    }
336
337    event::Status::Ignored
338}
339
340/// Draws a [`VerticalSlider`].
341pub fn draw<T, R>(
342    renderer: &mut R,
343    layout: Layout<'_>,
344    cursor_position: Point,
345    state: &State,
346    value: T,
347    range: &RangeInclusive<T>,
348    style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
349    style: &<R::Theme as StyleSheet>::Style,
350) where
351    T: Into<f64> + Copy,
352    R: crate::Renderer,
353    R::Theme: StyleSheet,
354{
355    let bounds = layout.bounds();
356    let is_mouse_over = bounds.contains(cursor_position);
357
358    let style = if state.is_dragging {
359        style_sheet.dragging(style)
360    } else if is_mouse_over {
361        style_sheet.hovered(style)
362    } else {
363        style_sheet.active(style)
364    };
365
366    let (handle_width, handle_height, handle_border_radius) = match style
367        .handle
368        .shape
369    {
370        HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
371        HandleShape::Rectangle {
372            width,
373            border_radius,
374        } => (f32::from(width), bounds.width, border_radius),
375    };
376
377    let value = value.into() as f32;
378    let (range_start, range_end) = {
379        let (start, end) = range.clone().into_inner();
380
381        (start.into() as f32, end.into() as f32)
382    };
383
384    let offset = if range_start >= range_end {
385        0.0
386    } else {
387        (bounds.height - handle_width) * (value - range_end)
388            / (range_start - range_end)
389    };
390
391    let rail_x = bounds.x + bounds.width / 2.0;
392
393    renderer.fill_quad(
394        renderer::Quad {
395            bounds: Rectangle {
396                x: rail_x - style.rail.width / 2.0,
397                y: bounds.y,
398                width: style.rail.width,
399                height: offset + handle_width / 2.0,
400            },
401            border_radius: Default::default(),
402            border_width: 0.0,
403            border_color: Color::TRANSPARENT,
404        },
405        style.rail.colors.1,
406    );
407
408    renderer.fill_quad(
409        renderer::Quad {
410            bounds: Rectangle {
411                x: rail_x - style.rail.width / 2.0,
412                y: bounds.y + offset + handle_width / 2.0,
413                width: style.rail.width,
414                height: bounds.height - offset - handle_width / 2.0,
415            },
416            border_radius: Default::default(),
417            border_width: 0.0,
418            border_color: Color::TRANSPARENT,
419        },
420        style.rail.colors.0,
421    );
422
423    renderer.fill_quad(
424        renderer::Quad {
425            bounds: Rectangle {
426                x: rail_x - handle_height / 2.0,
427                y: bounds.y + offset,
428                width: handle_height,
429                height: handle_width,
430            },
431            border_radius: handle_border_radius.into(),
432            border_width: style.handle.border_width,
433            border_color: style.handle.border_color,
434        },
435        style.handle.color,
436    );
437}
438
439/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
440pub fn mouse_interaction(
441    layout: Layout<'_>,
442    cursor_position: Point,
443    state: &State,
444) -> mouse::Interaction {
445    let bounds = layout.bounds();
446    let is_mouse_over = bounds.contains(cursor_position);
447
448    if state.is_dragging {
449        mouse::Interaction::Grabbing
450    } else if is_mouse_over {
451        mouse::Interaction::Grab
452    } else {
453        mouse::Interaction::default()
454    }
455}
456
457/// The local state of a [`VerticalSlider`].
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
459pub struct State {
460    is_dragging: bool,
461}
462
463impl State {
464    /// Creates a new [`State`].
465    pub fn new() -> State {
466        State::default()
467    }
468}