1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! A toggleable button that integrates with NIH-plug's [`Param`] types.

use nih_plug::prelude::Param;
use vizia::prelude::*;

use super::param_base::ParamWidgetBase;

/// A toggleable button that integrates with NIH-plug's [`Param`] types. Only makes sense with
/// [`BoolParam`][nih_plug::prelude::BoolParam]s. Clicking on the button will toggle between the
/// parameter's minimum and maximum value. The `:checked` pseudoclass indicates whether or not the
/// button is currently pressed.
#[derive(Lens)]
pub struct ParamButton {
    param_base: ParamWidgetBase,

    // These fields are set through modifiers:
    /// Whether or not to listen to scroll events for changing the parameter's value in steps.
    use_scroll_wheel: bool,
    /// A specific label to use instead of displaying the parameter's value.
    label_override: Option<String>,

    /// The number of (fractional) scrolled lines that have not yet been turned into parameter
    /// change events. This is needed to support trackpads with smooth scrolling.
    scrolled_lines: f32,
}

impl ParamButton {
    /// Creates a new [`ParamButton`] for the given parameter. See
    /// [`ParamSlider`][super::ParamSlider] for more information on this function's arguments.
    pub fn new<L, Params, P, FMap>(
        cx: &mut Context,
        params: L,
        params_to_param: FMap,
    ) -> Handle<Self>
    where
        L: Lens<Target = Params> + Clone,
        Params: 'static,
        P: Param + 'static,
        FMap: Fn(&Params) -> &P + Copy + 'static,
    {
        Self {
            param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param),

            use_scroll_wheel: true,
            label_override: None,

            scrolled_lines: 0.0,
        }
        .build(
            cx,
            ParamWidgetBase::build_view(params.clone(), params_to_param, move |cx, param_data| {
                Binding::new(cx, Self::label_override, move |cx, label_override| {
                    match label_override.get(cx) {
                        Some(label_override) => Label::new(cx, &label_override),
                        None => Label::new(cx, param_data.param().name()),
                    }
                    .hoverable(false);
                })
            }),
        )
        // We'll add the `:checked` pseudoclass when the button is pressed
        // NOTE: We use the normalized value _with modulation_ for this. There's no convenient way
        //       to show both modulated and unmodulated values here.
        .checked(ParamWidgetBase::make_lens(
            params,
            params_to_param,
            |param| param.modulated_normalized_value() >= 0.5,
        ))
    }

    /// Set the parameter's normalized value to either 0.0 or 1.0 depending on its current value.
    fn toggle_value(&self, cx: &mut EventContext) {
        let current_value = self.param_base.unmodulated_normalized_value();
        let new_value = if current_value >= 0.5 { 0.0 } else { 1.0 };

        self.param_base.begin_set_parameter(cx);
        self.param_base.set_normalized_value(cx, new_value);
        self.param_base.end_set_parameter(cx);
    }
}

impl View for ParamButton {
    fn element(&self) -> Option<&'static str> {
        Some("param-button")
    }

    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|window_event, meta| match window_event {
            // We don't need special double and triple click handling
            WindowEvent::MouseDown(MouseButton::Left)
            | WindowEvent::MouseDoubleClick(MouseButton::Left)
            | WindowEvent::MouseTripleClick(MouseButton::Left) => {
                self.toggle_value(cx);
                meta.consume();
            }
            WindowEvent::MouseScroll(_scroll_x, scroll_y) if self.use_scroll_wheel => {
                // With a regular scroll wheel `scroll_y` will only ever be -1 or 1, but with smooth
                // scrolling trackpads being a thing `scroll_y` could be anything.
                self.scrolled_lines += scroll_y;

                if self.scrolled_lines.abs() >= 1.0 {
                    self.param_base.begin_set_parameter(cx);

                    if self.scrolled_lines >= 1.0 {
                        self.param_base.set_normalized_value(cx, 1.0);
                        self.scrolled_lines -= 1.0;
                    } else {
                        self.param_base.set_normalized_value(cx, 0.0);
                        self.scrolled_lines += 1.0;
                    }

                    self.param_base.end_set_parameter(cx);
                }

                meta.consume();
            }
            _ => {}
        });
    }
}

/// Extension methods for [`ParamButton`] handles.
pub trait ParamButtonExt {
    /// Don't respond to scroll wheel events. Useful when this button is used as part of a scrolling
    /// view.
    fn disable_scroll_wheel(self) -> Self;

    /// Change the colors scheme for a bypass button. This simply adds the `bypass` class.
    fn for_bypass(self) -> Self;

    /// Change the label used for the button. If this is not set, then the parameter's name will be
    /// used.
    fn with_label(self, value: impl Into<String>) -> Self;
}

impl ParamButtonExt for Handle<'_, ParamButton> {
    fn disable_scroll_wheel(self) -> Self {
        self.modify(|param_slider: &mut ParamButton| param_slider.use_scroll_wheel = false)
    }

    fn for_bypass(self) -> Self {
        self.class("bypass")
    }

    fn with_label(self, value: impl Into<String>) -> Self {
        self.modify(|param_button: &mut ParamButton| {
            param_button.label_override = Some(value.into())
        })
    }
}