iced_audio/native/
v_slider.rs

1//! Display an interactive vertical slider that controls a [`NormalParam`]
2//!
3//! [`NormalParam`]: ../core/normal_param/struct.NormalParam.html
4
5use std::fmt::Debug;
6
7use iced_native::widget::tree::{self, Tree};
8use iced_native::{
9    event, keyboard, layout, mouse, touch, Clipboard, Element, Event, Layout,
10    Length, Point, Rectangle, Shell, Size, Widget,
11};
12
13use crate::core::{ModulationRange, Normal, NormalParam};
14use crate::native::{text_marks, tick_marks, SliderStatus};
15use crate::style::v_slider::StyleSheet;
16
17static DEFAULT_WIDTH: f32 = 14.0;
18static DEFAULT_SCALAR: f32 = 0.9575;
19static DEFAULT_WHEEL_SCALAR: f32 = 0.01;
20static DEFAULT_MODIFIER_SCALAR: f32 = 0.02;
21
22/// A vertical slider GUI widget that controls a [`NormalParam`]
23///
24/// a [`VSlider`] will try to fill the vertical space of its container.
25///
26/// [`NormalParam`]: ../../core/normal_param/struct.NormalParam.html
27/// [`VSlider`]: struct.VSlider.html
28#[allow(missing_debug_implementations)]
29pub struct VSlider<'a, Message, Renderer>
30where
31    Renderer: self::Renderer,
32    Renderer::Theme: StyleSheet,
33{
34    normal_param: NormalParam,
35    on_change: Box<dyn 'a + Fn(Normal) -> Message>,
36    on_grab: Option<Box<dyn 'a + FnMut() -> Option<Message>>>,
37    on_release: Option<Box<dyn 'a + FnMut() -> Option<Message>>>,
38    scalar: f32,
39    wheel_scalar: f32,
40    modifier_scalar: f32,
41    modifier_keys: keyboard::Modifiers,
42    width: Length,
43    height: Length,
44    style: <Renderer::Theme as StyleSheet>::Style,
45    tick_marks: Option<&'a tick_marks::Group>,
46    text_marks: Option<&'a text_marks::Group>,
47    mod_range_1: Option<&'a ModulationRange>,
48    mod_range_2: Option<&'a ModulationRange>,
49}
50
51impl<'a, Message, Renderer> VSlider<'a, Message, Renderer>
52where
53    Renderer: self::Renderer,
54    Renderer::Theme: StyleSheet,
55{
56    /// Creates a new [`VSlider`].
57    ///
58    /// It expects:
59    ///   * the [`NormalParam`] of the [`VSlider`]
60    ///   * a function that will be called when the [`VSlider`] is dragged.
61    ///
62    /// [`NormalParam`]: struct.NormalParam.html
63    /// [`VSlider`]: struct.VSlider.html
64    pub fn new<F>(normal_param: NormalParam, on_change: F) -> Self
65    where
66        F: 'static + Fn(Normal) -> Message,
67    {
68        VSlider {
69            normal_param,
70            on_change: Box::new(on_change),
71            on_grab: None,
72            on_release: None,
73            scalar: DEFAULT_SCALAR,
74            wheel_scalar: DEFAULT_WHEEL_SCALAR,
75            modifier_scalar: DEFAULT_MODIFIER_SCALAR,
76            modifier_keys: keyboard::Modifiers::CTRL,
77            width: Length::Fixed(DEFAULT_WIDTH),
78            height: Length::Fill,
79            style: Default::default(),
80            tick_marks: None,
81            text_marks: None,
82            mod_range_1: None,
83            mod_range_2: None,
84        }
85    }
86
87    /// Sets the grab message of the [`VSlider`].
88    /// This is called when the mouse grabs from the slider.
89    ///
90    /// Typically, the user's interaction with the slider starts when this message is produced.
91    /// This is useful for some environments so that external changes, such as automation,
92    /// don't interfer with user's changes.
93    pub fn on_grab(
94        mut self,
95        on_grab: impl 'a + FnMut() -> Option<Message>,
96    ) -> Self {
97        self.on_grab = Some(Box::new(on_grab));
98        self
99    }
100
101    /// Sets the release message of the [`VSlider`].
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(
108        mut self,
109        on_release: impl 'a + FnMut() -> Option<Message>,
110    ) -> Self {
111        self.on_release = Some(Box::new(on_release));
112        self
113    }
114
115    /// Sets the width of the [`VSlider`].
116    /// The default width is `Length::Fixed(14)`.
117    ///
118    /// [`VSlider`]: struct.VSlider.html
119    pub fn width(mut self, width: Length) -> Self {
120        self.width = width;
121        self
122    }
123
124    /// Sets the height of the [`VSlider`].
125    /// The default height is `Length::Fill`.
126    ///
127    /// [`VSlider`]: struct.VSlider.html
128    pub fn height(mut self, height: Length) -> Self {
129        self.height = height;
130        self
131    }
132
133    /// Sets the style of the [`VSlider`].
134    ///
135    /// [`VSlider`]: struct.VSlider.html
136    pub fn style(
137        mut self,
138        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
139    ) -> Self {
140        self.style = style.into();
141        self
142    }
143
144    /// Sets the modifier keys of the [`VSlider`].
145    ///
146    /// The default modifier key is `Ctrl`.
147    ///
148    /// [`VSlider`]: struct.VSlider.html
149    pub fn modifier_keys(mut self, modifier_keys: keyboard::Modifiers) -> Self {
150        self.modifier_keys = modifier_keys;
151        self
152    }
153
154    /// Sets the scalar to use when the user drags the slider per pixel.
155    ///
156    /// For example, a scalar of `0.5` will cause the slider to move half a
157    /// pixel for every pixel the mouse moves.
158    ///
159    /// The default scalar is `0.9575`.
160    ///
161    /// [`VSlider`]: struct.VSlider.html
162    pub fn scalar(mut self, scalar: f32) -> Self {
163        self.scalar = scalar;
164        self
165    }
166
167    /// Sets how much the [`Normal`] value will change for the [`VSlider`] per line scrolled
168    /// by the mouse wheel.
169    ///
170    /// This can be set to `0.0` to disable the scroll wheel from moving the parameter.
171    ///
172    /// The default value is `0.01`
173    ///
174    /// [`VSlider`]: struct.VSlider.html
175    /// [`Normal`]: ../../core/struct.Normal.html
176    pub fn wheel_scalar(mut self, wheel_scalar: f32) -> Self {
177        self.wheel_scalar = wheel_scalar;
178        self
179    }
180
181    /// Sets the scalar to use when the user drags the slider while holding down
182    /// the modifier key.
183    ///
184    /// For example, a scalar of `0.5` will cause the slider to move half a
185    /// pixel for every pixel the mouse moves.
186    ///
187    /// The default scalar is `0.02`, and the default modifier key is `Ctrl`.
188    ///
189    /// [`VSlider`]: struct.VSlider.html
190    pub fn modifier_scalar(mut self, scalar: f32) -> Self {
191        self.modifier_scalar = scalar;
192        self
193    }
194
195    /// Sets the tick marks to display. Note your [`StyleSheet`] must
196    /// also implement `tick_marks_style(&self) -> Option<tick_marks::Style>` for
197    /// them to display (which the default style does).
198    ///
199    /// [`StyleSheet`]: ../../style/v_slider/trait.StyleSheet.html
200    pub fn tick_marks(mut self, tick_marks: &'a tick_marks::Group) -> Self {
201        self.tick_marks = Some(tick_marks);
202        self
203    }
204
205    /// Sets the text marks to display. Note your [`StyleSheet`] must
206    /// also implement `text_marks_style(&self) -> Option<text_marks::Style>` for
207    /// them to display (which the default style does).
208    ///
209    /// [`StyleSheet`]: ../../style/v_slider/trait.StyleSheet.html
210    pub fn text_marks(mut self, text_marks: &'a text_marks::Group) -> Self {
211        self.text_marks = Some(text_marks);
212        self
213    }
214
215    /// Sets a [`ModulationRange`] to display. Note your [`StyleSheet`] must
216    /// also implement `mod_range_style(&self) -> Option<ModRangeStyle>` for
217    /// them to display.
218    ///
219    /// [`ModulationRange`]: ../../core/struct.ModulationRange.html
220    /// [`StyleSheet`]: ../../style/v_slider/trait.StyleSheet.html
221    pub fn mod_range(mut self, mod_range: &'a ModulationRange) -> Self {
222        self.mod_range_1 = Some(mod_range);
223        self
224    }
225
226    /// Sets a second [`ModulationRange`] to display. Note your [`StyleSheet`] must
227    /// also implement `mod_range_style_2(&self) -> Option<ModRangeStyle>` for
228    /// them to display.
229    ///
230    /// [`ModulationRange`]: ../../core/struct.ModulationRange.html
231    /// [`StyleSheet`]: ../../style/v_slider/trait.StyleSheet.html
232    pub fn mod_range_2(mut self, mod_range: &'a ModulationRange) -> Self {
233        self.mod_range_1 = Some(mod_range);
234        self
235    }
236
237    fn move_virtual_slider(
238        &mut self,
239        state: &mut State,
240        mut normal_delta: f32,
241    ) -> SliderStatus {
242        if normal_delta.abs() < f32::EPSILON {
243            return SliderStatus::Unchanged;
244        }
245
246        if state.pressed_modifiers.contains(self.modifier_keys) {
247            normal_delta *= self.modifier_scalar;
248        }
249
250        self.normal_param
251            .value
252            .set_clipped(state.continuous_normal - normal_delta);
253        state.continuous_normal = self.normal_param.value.as_f32();
254
255        SliderStatus::Moved
256    }
257
258    fn maybe_fire_on_grab(&mut self, shell: &mut Shell<'_, Message>) {
259        if let Some(message) =
260            self.on_grab.as_mut().and_then(|on_grab| on_grab())
261        {
262            shell.publish(message);
263        }
264    }
265
266    fn fire_on_change(&self, shell: &mut Shell<'_, Message>) {
267        shell.publish((self.on_change)(self.normal_param.value));
268    }
269
270    fn maybe_fire_on_release(&mut self, shell: &mut Shell<'_, Message>) {
271        if let Some(message) =
272            self.on_release.as_mut().and_then(|on_release| on_release())
273        {
274            shell.publish(message);
275        }
276    }
277}
278
279/// The local state of a [`VSlider`].
280///
281/// [`VSlider`]: struct.VSlider.html
282#[derive(Debug, Clone)]
283struct State {
284    dragging_status: Option<SliderStatus>,
285    prev_drag_y: f32,
286    prev_normal: Normal,
287    continuous_normal: f32,
288    pressed_modifiers: keyboard::Modifiers,
289    last_click: Option<mouse::Click>,
290    tick_marks_cache: crate::graphics::tick_marks::PrimitiveCache,
291    text_marks_cache: crate::graphics::text_marks::PrimitiveCache,
292}
293
294impl State {
295    /// Creates a new [`VSlider`] state.
296    ///
297    /// It expects:
298    /// * current [`Normal`] value for the [`VSlider`]
299    ///
300    /// [`Normal`]: ../../core/normal/struct.Normal.html
301    /// [`VSlider`]: struct.VSlider.html
302    fn new(normal: Normal) -> Self {
303        Self {
304            dragging_status: None,
305            prev_drag_y: 0.0,
306            prev_normal: normal,
307            continuous_normal: normal.as_f32(),
308            pressed_modifiers: Default::default(),
309            last_click: None,
310            tick_marks_cache: Default::default(),
311            text_marks_cache: Default::default(),
312        }
313    }
314}
315
316impl<'a, Message, Renderer> Widget<Message, Renderer>
317    for VSlider<'a, Message, Renderer>
318where
319    Renderer: self::Renderer,
320    Renderer::Theme: StyleSheet,
321{
322    fn tag(&self) -> tree::Tag {
323        tree::Tag::of::<State>()
324    }
325
326    fn state(&self) -> tree::State {
327        tree::State::new(State::new(self.normal_param.value))
328    }
329
330    fn width(&self) -> Length {
331        Length::Shrink
332    }
333
334    fn height(&self) -> Length {
335        self.height
336    }
337
338    fn layout(
339        &self,
340        _renderer: &Renderer,
341        limits: &layout::Limits,
342    ) -> layout::Node {
343        let limits = limits.width(self.width).height(self.height);
344
345        let size = limits.resolve(Size::ZERO);
346
347        layout::Node::new(size)
348    }
349
350    fn on_event(
351        &mut self,
352        state: &mut Tree,
353        event: Event,
354        layout: Layout<'_>,
355        cursor_position: Point,
356        _renderer: &Renderer,
357        _clipboard: &mut dyn Clipboard,
358        shell: &mut Shell<'_, Message>,
359    ) -> event::Status {
360        let state = state.state.downcast_mut::<State>();
361
362        // Update state after a discontinuity
363        if state.dragging_status.is_none()
364            && state.prev_normal != self.normal_param.value
365        {
366            state.prev_normal = self.normal_param.value;
367            state.continuous_normal = self.normal_param.value.as_f32();
368        }
369
370        match event {
371            Event::Mouse(mouse::Event::CursorMoved { .. })
372            | Event::Touch(touch::Event::FingerMoved { .. }) => {
373                if state.dragging_status.is_some() {
374                    let bounds = layout.bounds();
375                    if bounds.height > 0.0 {
376                        let normal_delta = (cursor_position.y
377                            - state.prev_drag_y)
378                            / bounds.height
379                            * self.scalar;
380
381                        state.prev_drag_y = if cursor_position.y <= bounds.y {
382                            bounds.y
383                        } else {
384                            cursor_position.y.min(bounds.y + bounds.height)
385                        };
386
387                        if self
388                            .move_virtual_slider(state, normal_delta)
389                            .was_moved()
390                        {
391                            self.fire_on_change(shell);
392
393                            state
394                                .dragging_status
395                                .as_mut()
396                                .expect("dragging_status taken")
397                                .moved();
398                        }
399
400                        return event::Status::Captured;
401                    }
402                }
403            }
404            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
405                if self.wheel_scalar == 0.0 {
406                    return event::Status::Ignored;
407                }
408
409                if layout.bounds().contains(cursor_position) {
410                    let lines = match delta {
411                        iced_native::mouse::ScrollDelta::Lines {
412                            y, ..
413                        } => y,
414                        iced_native::mouse::ScrollDelta::Pixels {
415                            y, ..
416                        } => {
417                            if y > 0.0 {
418                                1.0
419                            } else if y < 0.0 {
420                                -1.0
421                            } else {
422                                0.0
423                            }
424                        }
425                    };
426
427                    if lines != 0.0 {
428                        let normal_delta = -lines * self.wheel_scalar;
429
430                        if self
431                            .move_virtual_slider(state, normal_delta)
432                            .was_moved()
433                        {
434                            if state.dragging_status.is_none() {
435                                self.maybe_fire_on_grab(shell);
436                            }
437
438                            self.fire_on_change(shell);
439
440                            if let Some(slider_status) =
441                                state.dragging_status.as_mut()
442                            {
443                                // Widget was grabbed => keep it grabbed
444                                slider_status.moved();
445                            } else {
446                                self.maybe_fire_on_release(shell);
447                            }
448                        }
449
450                        return event::Status::Captured;
451                    }
452                }
453            }
454            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
455            | Event::Touch(touch::Event::FingerPressed { .. }) => {
456                if layout.bounds().contains(cursor_position) {
457                    let click =
458                        mouse::Click::new(cursor_position, state.last_click);
459
460                    match click.kind() {
461                        mouse::click::Kind::Single => {
462                            self.maybe_fire_on_grab(shell);
463
464                            state.dragging_status = Some(Default::default());
465                            state.prev_drag_y = cursor_position.y;
466                        }
467                        _ => {
468                            // Reset to default
469
470                            let prev_dragging_status =
471                                state.dragging_status.take();
472
473                            if self.normal_param.value
474                                != self.normal_param.default
475                            {
476                                if prev_dragging_status.is_none() {
477                                    self.maybe_fire_on_grab(shell);
478                                }
479
480                                self.normal_param.value =
481                                    self.normal_param.default;
482
483                                self.fire_on_change(shell);
484
485                                self.maybe_fire_on_release(shell);
486                            } else if prev_dragging_status.is_some() {
487                                self.maybe_fire_on_release(shell);
488                            }
489                        }
490                    }
491
492                    state.last_click = Some(click);
493
494                    return event::Status::Captured;
495                }
496            }
497            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
498            | Event::Touch(touch::Event::FingerLifted { .. })
499            | Event::Touch(touch::Event::FingerLost { .. }) => {
500                if let Some(slider_status) = state.dragging_status.take() {
501                    if self.on_grab.is_some() || slider_status.was_moved() {
502                        // maybe fire on release if `on_grab` is defined
503                        // so as to terminate the action, regardless of the actual user movement.
504                        self.maybe_fire_on_release(shell);
505                    }
506
507                    return event::Status::Captured;
508                }
509            }
510            Event::Keyboard(keyboard_event) => match keyboard_event {
511                keyboard::Event::KeyPressed { modifiers, .. } => {
512                    state.pressed_modifiers = modifiers;
513
514                    return event::Status::Captured;
515                }
516                keyboard::Event::KeyReleased { modifiers, .. } => {
517                    state.pressed_modifiers = modifiers;
518
519                    return event::Status::Captured;
520                }
521                keyboard::Event::ModifiersChanged(modifiers) => {
522                    state.pressed_modifiers = modifiers;
523
524                    return event::Status::Captured;
525                }
526                _ => {}
527            },
528            _ => {}
529        }
530
531        event::Status::Ignored
532    }
533
534    fn draw(
535        &self,
536        state: &Tree,
537        renderer: &mut Renderer,
538        theme: &Renderer::Theme,
539        _style: &iced_native::renderer::Style,
540        layout: Layout<'_>,
541        cursor_position: Point,
542        _viewport: &Rectangle,
543    ) {
544        let state = state.state.downcast_ref::<State>();
545        renderer.draw(
546            layout.bounds(),
547            cursor_position,
548            self.normal_param.value,
549            state.dragging_status.is_some(),
550            self.mod_range_1,
551            self.mod_range_2,
552            self.tick_marks,
553            self.text_marks,
554            theme,
555            &self.style,
556            &state.tick_marks_cache,
557            &state.text_marks_cache,
558        )
559    }
560}
561
562/// The renderer of a [`VSlider`].
563///
564/// Your renderer will need to implement this trait before being
565/// able to use a [`VSlider`] in your user interface.
566///
567/// [`VSlider`]: struct.VSlider.html
568pub trait Renderer: iced_native::Renderer
569where
570    Self::Theme: StyleSheet,
571{
572    /// Draws a [`VSlider`].
573    ///
574    /// It receives:
575    ///   * the bounds of the [`VSlider`]
576    ///   * the current cursor position
577    ///   * the current normal of the [`VSlider`]
578    ///   * the height of the handle in pixels
579    ///   * whether the slider is currently being dragged
580    ///   * any tick marks to display
581    ///   * any text marks to display
582    ///   * the style of the [`VSlider`]
583    ///
584    /// [`VSlider`]: struct.VSlider.html
585    #[allow(clippy::too_many_arguments)]
586    fn draw(
587        &mut self,
588        bounds: Rectangle,
589        cursor_position: Point,
590        normal: Normal,
591        dragging_status: bool,
592        mod_range_1: Option<&ModulationRange>,
593        mod_range_2: Option<&ModulationRange>,
594        tick_marks: Option<&tick_marks::Group>,
595        text_marks: Option<&text_marks::Group>,
596        style_sheet: &dyn StyleSheet<
597            Style = <Self::Theme as StyleSheet>::Style,
598        >,
599        style: &<Self::Theme as StyleSheet>::Style,
600        tick_marks_cache: &crate::tick_marks::PrimitiveCache,
601        text_marks_cache: &crate::text_marks::PrimitiveCache,
602    );
603}
604
605impl<'a, Message, Renderer> From<VSlider<'a, Message, Renderer>>
606    for Element<'a, Message, Renderer>
607where
608    Message: 'a,
609    Renderer: 'a + self::Renderer,
610    Renderer::Theme: 'a + StyleSheet,
611{
612    fn from(
613        v_slider: VSlider<'a, Message, Renderer>,
614    ) -> Element<'a, Message, Renderer> {
615        Element::new(v_slider)
616    }
617}