diff --git a/nih_plug_vizia/src/widgets.rs b/nih_plug_vizia/src/widgets.rs index f1cd9653..16a8da12 100644 --- a/nih_plug_vizia/src/widgets.rs +++ b/nih_plug_vizia/src/widgets.rs @@ -12,6 +12,7 @@ use vizia::prelude::*; use super::ViziaState; mod generic_ui; +pub mod param_base; mod param_button; mod param_slider; mod peak_meter; diff --git a/nih_plug_vizia/src/widgets/param_base.rs b/nih_plug_vizia/src/widgets/param_base.rs new file mode 100644 index 00000000..22ef49a9 --- /dev/null +++ b/nih_plug_vizia/src/widgets/param_base.rs @@ -0,0 +1,229 @@ +//! A base widget for creating other widgets that integrate with NIH-plug's [`Param`] types. + +use nih_plug::prelude::*; +use vizia::prelude::*; + +use super::RawParamEvent; + +/// A helper for creating parameter widgets. The general idea is that a parameter widget struct can +/// adds a `ParamWidgetBase` field on its struct, and then calls [`ParamWidgetBase::view()`] in its +/// view build function. The stored `ParamWidgetbBase` object can then be used in the widget's event +/// handlers to interact with the parameter. +#[derive(Lens)] +pub struct ParamWidgetBase { + /// We're not allowed to store a reference to the parameter internally, at least not in the + /// struct that implements [`View`]. + param_ptr: ParamPtr, +} + +/// Data and lenses that can be used to draw the parameter widget. The [`param`][Self::param] field +/// should only be used for looking up static data. Prefer the [`make_lens()`][Self::make_lens()] +/// function for binding parameter data to element properties. +pub struct ParamWidgetData +where + L: Lens + Clone, + Params: 'static, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, +{ + // HACK: This needs to be a static reference because of the way bindings in Vizia works. This + // feels very wrong, but I don't think there is an alternative. The field is not `pub` + // for this reason. + param: &'static P, + params: L, + params_to_param: FMap, +} + +impl Clone for ParamWidgetData +where + L: Lens + Clone, + Params: 'static, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, +{ + fn clone(&self) -> Self { + Self { + param: self.param, + params: self.params.clone(), + params_to_param: self.params_to_param, + } + } +} + +impl ParamWidgetData +where + L: Lens + Clone, + Params: 'static, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, +{ + /// The parameter in question. This can be used for querying static information about the + /// parameter. Don't use this to get the parameter's current value, use the lenses instead. + pub fn param(&self) -> &P { + self.param + } + + /// Create a lens from a parameter's field. This can be used to bind one of the parameter's + /// value getters to a property. + pub fn make_lens(&self, f: F) -> impl Lens + where + F: Fn(&P) -> R + Clone + 'static, + R: Clone + 'static, + { + let params_to_param = self.params_to_param; + + self.params.clone().map(move |params| { + let param = params_to_param(params); + f(param) + }) + } +} + +/// Generate a [`ParamWidgetData`] function that forwards the function call to the underlying +/// `ParamPtr`. +macro_rules! param_ptr_forward( + (pub fn $method:ident(&self $(, $arg_name:ident: $arg_ty:ty)*) -> $ret:ty) => { + /// Calls the corresponding method on the underlying [`ParamPtr`] object. + pub fn $method(&self $(, $arg_name: $arg_ty)*) -> $ret { + unsafe { self.param_ptr.$method($($arg_name),*) } + } + }; +); + +impl ParamWidgetBase { + /// Creates a [`ParamWidgetBase`] for the given parameter. This can be stored on a widget object + /// and used as part of the widget's event handling. To accommodate VIZIA's mapping system, + /// you'll need to provide a lens containing your `Params` implementation object (check out how + /// the `Data` struct is used in `gain_gui_vizia`) and a projection function that maps the + /// `Params` object to the parameter you want to display a widget for. Parameter changes are + /// handled by emitting [`ParamEvent`][super::ParamEvent]s which are automatically handled by + /// the VIZIA wrapper. + pub fn new(cx: &mut Context, params: L, params_to_param: FMap) -> Self + where + L: Lens + Clone, + Params: 'static, + P: Param, + FMap: Fn(&Params) -> &P + Copy + 'static, + { + // We need to do a bit of a nasty and erase the lifetime bound by going through a raw + // ParamPtr. Vizia requires all lens data to be 'static and Clone. + let param_ptr = params + .map(move |params| params_to_param(params).as_ptr()) + .get(cx); + + Self { param_ptr } + } + + /// Create a view using the a parameter's data. This is not tied to a particular + /// [`ParamWidgetBase`] instance, but it allows you to easily create lenses for the parameter's + /// values and access static parameter data. + /// + /// This can be used directly as an argument to [`View::build()`]. + pub fn view( + params: L, + params_to_param: FMap, + content: F, + ) -> impl FnOnce(&mut Context) -> R + where + L: Lens + Clone, + Params: 'static, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, + F: FnOnce(&mut Context, ParamWidgetData) -> R, + { + move |cx| { + // We'll provide the raw `&P` to the callbacks to make creating parameter widgets more + // convenient. + // SAFETY: This &P won't outlive this function, and in the context of NIH-plug &P will + // outlive the editor + let param: &P = unsafe { + &*params + .clone() + .map(move |params| params_to_param(params) as *const P) + .get(cx) + }; + + // The widget can use this to access data parameter data and to create lenses for working + // with the parameter's values + let param_data = ParamWidgetData { + param, + params, + params_to_param, + }; + + content(cx, param_data) + } + } + + /// Convenience function for using [`ParamWidgetData::make_lens()`]. Whenever possible, + /// [`view()`][Self::view()] should be used instead. + pub fn make_lens( + params: L, + params_to_param: FMap, + f: F, + ) -> impl Lens + where + L: Lens + Clone, + Params: 'static, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, + F: Fn(&P) -> R + Clone + 'static, + R: Clone + 'static, + { + params.map(move |params| { + let param = params_to_param(params); + f(param) + }) + } + + /// Start an automation gesture. This **must** be called before `set_normalized_value()` + /// is called. Usually this is done on mouse down. + pub fn begin_set_parameter(&self, cx: &mut EventContext) { + cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); + } + + /// Set the normalized value for a parameter if that would change the parameter's plain value + /// (to avoid unnecessary duplicate parameter changes). `begin_set_parameter()` **must** be + /// called before this is called to start an automation gesture, and `end_set_parameter()` must + /// be called at the end of the gesture. + pub fn set_normalized_value(&self, cx: &mut EventContext, normalized_value: f32) { + // This snaps to the nearest plain value if the parameter is stepped in some way. + // TODO: As an optimization, we could add a `const CONTINUOUS: bool` to the parameter to + // avoid this normalized->plain->normalized conversion for parameters that don't need + // it + let plain_value = unsafe { self.param_ptr.preview_plain(normalized_value) }; + let current_plain_value = unsafe { self.param_ptr.unmodulated_plain_value() }; + if plain_value != current_plain_value { + // For the aforementioned snapping + let normalized_plain_value = unsafe { self.param_ptr.preview_normalized(plain_value) }; + cx.emit(RawParamEvent::SetParameterNormalized( + self.param_ptr, + normalized_plain_value, + )); + } + } + + /// End an automation gesture. This must be called at the end of a gesture, after zero or more + /// `set_normalized_value()` calls. Usually this is done on mouse down. + pub fn end_set_parameter(&self, cx: &mut EventContext) { + cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + } + + param_ptr_forward!(pub fn name(&self) -> &str); + param_ptr_forward!(pub fn unit(&self) -> &'static str); + param_ptr_forward!(pub fn poly_modulation_id(&self) -> Option); + param_ptr_forward!(pub fn plain_value(&self) -> f32); + param_ptr_forward!(pub fn unmodulated_plain_value(&self) -> f32); + param_ptr_forward!(pub fn normalized_value(&self) -> f32); + param_ptr_forward!(pub fn unmodulated_normalized_value(&self) -> f32); + param_ptr_forward!(pub fn default_plain_value(&self) -> f32); + param_ptr_forward!(pub fn default_normalized_value(&self) -> f32); + param_ptr_forward!(pub fn step_count(&self) -> Option); + param_ptr_forward!(pub fn previous_normalized_step(&self, from: f32) -> f32); + param_ptr_forward!(pub fn next_normalized_step(&self, from: f32) -> f32); + param_ptr_forward!(pub fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String); + param_ptr_forward!(pub fn string_to_normalized_value(&self, string: &str) -> Option); + param_ptr_forward!(pub fn preview_normalized(&self, plain: f32) -> f32); + param_ptr_forward!(pub fn preview_plain(&self, normalized: f32) -> f32); + param_ptr_forward!(pub fn flags(&self) -> ParamFlags); +} diff --git a/nih_plug_vizia/src/widgets/param_button.rs b/nih_plug_vizia/src/widgets/param_button.rs index 949a2423..e299d3a8 100644 --- a/nih_plug_vizia/src/widgets/param_button.rs +++ b/nih_plug_vizia/src/widgets/param_button.rs @@ -1,9 +1,9 @@ //! A toggleable button that integrates with NIH-plug's [`Param`] types. -use nih_plug::prelude::{Param, ParamPtr}; +use nih_plug::prelude::Param; use vizia::prelude::*; -use super::RawParamEvent; +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 @@ -11,9 +11,7 @@ use super::RawParamEvent; /// button is currently pressed. #[derive(Lens)] pub struct ParamButton { - // We're not allowed to store a reference to the parameter internally, at least not in the - // struct that implements [`View`] - param_ptr: ParamPtr, + param_base: ParamWidgetBase, } impl ParamButton { @@ -23,45 +21,45 @@ impl ParamButton { /// `Params` object to the parameter you want to display a widget for. Parameter changes are /// handled by emitting [`ParamEvent`][super::ParamEvent]s which are automatically handled by /// the VIZIA wrapper. - pub fn new(cx: &mut Context, params: L, params_to_param: F) -> Handle + pub fn new( + cx: &mut Context, + params: L, + params_to_param: FMap, + ) -> Handle where L: Lens + Clone, - F: 'static + Fn(&Params) -> &P + Copy, Params: 'static, - P: Param, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, { - let param_ptr = params - .clone() - .map(move |params| params_to_param(params).as_ptr()) - .get(cx); - let param_name = params - .clone() - .map(move |params| params_to_param(params).name().to_owned()) - .get(cx); - // 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. - let param_value_lens = params.map(move |params| params_to_param(params).normalized_value()); + let param_value_lens = + ParamWidgetBase::make_lens(params.clone(), params_to_param, |param| { + param.normalized_value() + }); - Self { param_ptr } - .build(cx, move |cx| { - Label::new(cx, ¶m_name); - }) - .checked(param_value_lens.map(|v| v >= &0.5)) + Self { + param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param), + } + .build( + cx, + ParamWidgetBase::view(params, params_to_param, move |cx, param_data| { + Label::new(cx, param_data.param().name()); + }), + ) + .checked(param_value_lens.map(|v| v >= &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 = unsafe { self.param_ptr.unmodulated_normalized_value() }; + let current_value = self.param_base.unmodulated_normalized_value(); let new_value = if current_value >= 0.5 { 0.0 } else { 1.0 }; - cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); - cx.emit(RawParamEvent::SetParameterNormalized( - self.param_ptr, - new_value, - )); - cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + self.param_base.begin_set_parameter(cx); + self.param_base.set_normalized_value(cx, new_value); + self.param_base.end_set_parameter(cx); } } diff --git a/nih_plug_vizia/src/widgets/param_slider.rs b/nih_plug_vizia/src/widgets/param_slider.rs index 85d75e95..190f683a 100644 --- a/nih_plug_vizia/src/widgets/param_slider.rs +++ b/nih_plug_vizia/src/widgets/param_slider.rs @@ -1,10 +1,10 @@ //! A slider that integrates with NIH-plug's [`Param`] types. -use nih_plug::prelude::{Param, ParamPtr}; +use nih_plug::prelude::Param; use vizia::prelude::*; +use super::param_base::ParamWidgetBase; use super::util::{self, ModifiersExt}; -use super::RawParamEvent; /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the /// normalized parameter. @@ -14,13 +14,9 @@ const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; /// [`set_style()`][ParamSliderExt::set_style()] method to change how the value gets displayed. /// /// TODO: Handle scrolling for steps (and shift+scroll for smaller steps?) -/// TODO: We may want to add a couple dedicated event handlers if it seems like those would be -/// useful, having a completely self contained widget is perfectly fine for now though #[derive(Lens)] pub struct ParamSlider { - // We're not allowed to store a reference to the parameter internally, at least not in the - // struct that implements [`View`] - param_ptr: ParamPtr, + param_base: ParamWidgetBase, /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a /// text value should not initiate a drag. @@ -74,33 +70,22 @@ impl ParamSlider { /// the VIZIA wrapper. /// /// See [`ParamSliderExt`] for additional options. - pub fn new(cx: &mut Context, params: L, params_to_param: F) -> Handle + pub fn new( + cx: &mut Context, + params: L, + params_to_param: FMap, + ) -> Handle where L: Lens + Clone, - F: 'static + Fn(&Params) -> &P + Copy, Params: 'static, - P: Param, + P: Param + 'static, + FMap: Fn(&Params) -> &P + Copy + 'static, { // We'll visualize the difference between the current value and the default value if the // default value lies somewhere in the middle and the parameter is continuous. Otherwise // this approach looks a bit jarring. - // We need to do a bit of a nasty and erase the lifetime bound by going through the raw - // GuiContext and a ParamPtr. - let param_ptr = params - .clone() - .map(move |params| params_to_param(params).as_ptr()) - .get(cx); - let default_value = params - .clone() - .map(move |params| params_to_param(params).default_normalized_value()) - .get(cx); - let step_count = params - .clone() - .map(move |params| params_to_param(params).step_count()) - .get(cx); - Self { - param_ptr, + param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param), drag_active: false, granular_drag_start_x_value: None, @@ -108,263 +93,251 @@ impl ParamSlider { style: ParamSliderStyle::Centered, text_input_active: false, } - .build(cx, move |cx| { - Binding::new(cx, ParamSlider::style, move |cx, style| { - let style = style.get(cx); - let draw_fill_from_default = matches!(style, ParamSliderStyle::Centered) - && step_count.is_none() - && (0.45..=0.55).contains(&default_value); + .build( + cx, + ParamWidgetBase::view(params, params_to_param, move |cx, param_data| { + Binding::new(cx, ParamSlider::style, move |cx, style| { + let style = style.get(cx); - // Only draw the text input widget when it gets focussed. Otherwise, overlay the - // label with the slider. Creating the textbox based on - // `ParamSliderInternal::text_input_active` lets us focus the textbox when it gets - // created. - let params = params.clone(); - Binding::new( - cx, - ParamSlider::text_input_active, - move |cx, text_input_active| { - // Can't use `.to_string()` here as that would include the modulation. - let param_display_value_lens = params.clone().map(move |params| { - let param = params_to_param(params); - param.normalized_value_to_string( - param.unmodulated_normalized_value(), - true, - ) - }); - let param_preview_display_value_lens = { - let params = params.clone(); - move |normalized_value| { - params.clone().map(move |params| { - params_to_param(params) - .normalized_value_to_string(normalized_value, true) - }) - } - }; - let unmodulated_normalized_param_value_lens = - params.clone().map(move |params| { - params_to_param(params).unmodulated_normalized_value() - }); + let default_value = param_data.param().default_normalized_value(); + let step_count = param_data.param().step_count(); - // The resulting tuple `(start_t, delta)` corresponds to the start and the - // signed width of the bar. `start_t` is in `[0, 1]`, and `delta` is in - // `[-1, 1]`. - let fill_start_delta_lens = - unmodulated_normalized_param_value_lens.map(move |current_value| { - match style { - ParamSliderStyle::Centered if draw_fill_from_default => { - let delta = (default_value - current_value).abs(); - ( - default_value.min(*current_value), - // Don't draw the filled portion at all if it - // could have been a rounding error since those - // slivers just look weird - if delta >= 1e-3 { delta } else { 0.0 }, - ) - } - ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => { - (0.0, *current_value) - } - ParamSliderStyle::CurrentStep { even: true } - | ParamSliderStyle::CurrentStepLabeled { even: true } - if step_count.is_some() => - { - // Assume the normalized value is distributed evenly - // across the range. - let step_count = step_count.unwrap() as f32; - let discrete_values = step_count + 1.0; - let previous_step = - (current_value * step_count) / discrete_values; - (previous_step, discrete_values.recip()) - } - ParamSliderStyle::CurrentStep { .. } - | ParamSliderStyle::CurrentStepLabeled { .. } => { - let previous_step = unsafe { - param_ptr.previous_normalized_step(*current_value) - }; - let next_step = unsafe { - param_ptr.next_normalized_step(*current_value) - }; - ( - (previous_step + current_value) / 2.0, - ((next_step - current_value) - + (current_value - previous_step)) - / 2.0, - ) - } - } - }); - // If the parameter is being modulated by the host (this only works for CLAP - // plugins with hosts that support this), then this is the difference - // between the 'true' value and the current value after modulation has been - // applied. - let modulation_start_delta_lens = params.clone().map(move |params| { + // Can't use `.to_string()` here as that would include the modulation. + let unmodulated_normalized_value_lens = + param_data.make_lens(|param| param.unmodulated_normalized_value()); + let display_value_lens = param_data.make_lens(|param| { + param.normalized_value_to_string(param.unmodulated_normalized_value(), true) + }); + + // This is used to draw labels for `CurrentStepLabeled` + let make_preview_value_lens = { + let param_data = param_data.clone(); + move |normalized_value| { + param_data.make_lens(move |param| { + param.normalized_value_to_string(normalized_value, true) + }) + } + }; + + // The resulting tuple `(start_t, delta)` corresponds to the start and the + // signed width of the bar. `start_t` is in `[0, 1]`, and `delta` is in + // `[-1, 1]`. + let draw_fill_from_default = matches!(style, ParamSliderStyle::Centered) + && step_count.is_none() + && (0.45..=0.55).contains(&default_value); + let fill_start_delta_lens = unmodulated_normalized_value_lens.map({ + let param_data = param_data.clone(); + + move |current_value| { match style { - // Don't show modulation for stepped parameters since it wouldn't - // make a lot of sense visually - ParamSliderStyle::CurrentStep { .. } - | ParamSliderStyle::CurrentStepLabeled { .. } => (0.0, 0.0), - ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => { - let param = params_to_param(params); - let modulation_start = param.unmodulated_normalized_value(); + ParamSliderStyle::Centered if draw_fill_from_default => { + let delta = (default_value - current_value).abs(); + + // Don't draw the filled portion at all if it could have been a + // rounding error since those slivers just look weird ( - modulation_start, - param.normalized_value() - modulation_start, + default_value.min(*current_value), + if delta >= 1e-3 { delta } else { 0.0 }, + ) + } + ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => { + (0.0, *current_value) + } + ParamSliderStyle::CurrentStep { even: true } + | ParamSliderStyle::CurrentStepLabeled { even: true } + if step_count.is_some() => + { + // Assume the normalized value is distributed evenly + // across the range. + let step_count = step_count.unwrap() as f32; + let discrete_values = step_count + 1.0; + let previous_step = + (current_value * step_count) / discrete_values; + + (previous_step, discrete_values.recip()) + } + ParamSliderStyle::CurrentStep { .. } + | ParamSliderStyle::CurrentStepLabeled { .. } => { + let previous_step = + param_data.param().previous_normalized_step(*current_value); + let next_step = + param_data.param().next_normalized_step(*current_value); + + ( + (previous_step + current_value) / 2.0, + ((next_step - current_value) + + (current_value - previous_step)) + / 2.0, ) } } - }); + } + }); - if text_input_active.get(cx) { - Textbox::new(cx, param_display_value_lens) - .class("value-entry") - .on_submit(|cx, string, success| { - if success { - cx.emit(ParamSliderEvent::TextInput(string)) - } else { - cx.emit(ParamSliderEvent::CancelTextInput); - } - }) - .on_build(|cx| { - cx.emit(TextEvent::StartEdit); - cx.emit(TextEvent::SelectAll); - }) - // `.child_space(Stretch(1.0))` no longer works - .class("align_center") - .child_top(Stretch(1.0)) - .child_bottom(Stretch(1.0)) - .height(Stretch(1.0)) - .width(Stretch(1.0)); - } else { - ZStack::new(cx, move |cx| { - // The filled bar portion. This can be visualized in a couple - // different ways depending on the current style property. See - // [`ParamSliderStyle`]. - Element::new(cx) - .class("fill") + // If the parameter is being modulated by the host (this only works for CLAP + // plugins with hosts that support this), then this is the difference + // between the 'true' value and the current value after modulation has been + // applied. + let modulation_start_delta_lens = param_data.make_lens(move |param| { + match style { + // Don't show modulation for stepped parameters since it wouldn't + // make a lot of sense visually + ParamSliderStyle::CurrentStep { .. } + | ParamSliderStyle::CurrentStepLabeled { .. } => (0.0, 0.0), + ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => { + let modulation_start = param.unmodulated_normalized_value(); + + ( + modulation_start, + param.normalized_value() - modulation_start, + ) + } + } + }); + + // Only draw the text input widget when it gets focussed. Otherwise, overlay the + // label with the slider. Creating the textbox based on + // `ParamSliderInternal::text_input_active` lets us focus the textbox when it gets + // created. + Binding::new( + cx, + ParamSlider::text_input_active, + move |cx, text_input_active| { + if text_input_active.get(cx) { + Textbox::new(cx, display_value_lens.clone()) + .class("value-entry") + .on_submit(|cx, string, success| { + if success { + cx.emit(ParamSliderEvent::TextInput(string)) + } else { + cx.emit(ParamSliderEvent::CancelTextInput); + } + }) + .on_build(|cx| { + cx.emit(TextEvent::StartEdit); + cx.emit(TextEvent::SelectAll); + }) + // `.child_space(Stretch(1.0))` no longer works + .class("align_center") + .child_top(Stretch(1.0)) + .child_bottom(Stretch(1.0)) .height(Stretch(1.0)) - .left( - fill_start_delta_lens - .clone() - .map(|(start_t, _)| Percentage(start_t * 100.0)), - ) - .width( - fill_start_delta_lens - .clone() - .map(|(_, delta)| Percentage(delta * 100.0)), - ) - // Hovering is handled on the param slider as a whole, this - // should not affect that - .hoverable(false); + .width(Stretch(1.0)); + } else { + let display_value_lens = display_value_lens.clone(); + let fill_start_delta_lens = fill_start_delta_lens.clone(); + let modulation_start_delta_lens = + modulation_start_delta_lens.clone(); + let make_preview_value_lens = make_preview_value_lens.clone(); - // If the parameter is being modulated, then we'll display another - // filled bar showing the current modulation delta - // VIZIA's bindings make this a bit, uh, difficult to read - Element::new(cx) - .class("fill") - .class("fill--modulation") - .height(Stretch(1.0)) - .visibility( - modulation_start_delta_lens - .clone() - .map(|(_, delta)| *delta != 0.0), - ) - // Widths cannot be negative, so we need to compensate the start - // position if the width does happen to be negative - .width( - modulation_start_delta_lens - .clone() - .map(|(_, delta)| Percentage(delta.abs() * 100.0)), - ) - .left(modulation_start_delta_lens.clone().map( - |(start_t, delta)| { - if *delta < 0.0 { - Percentage((start_t + delta) * 100.0) - } else { - Percentage(start_t * 100.0) - } - }, - )) - .hoverable(false); + ZStack::new(cx, move |cx| { + // The filled bar portion. This can be visualized in a couple + // different ways depending on the current style property. See + // [`ParamSliderStyle`]. + Element::new(cx) + .class("fill") + .height(Stretch(1.0)) + .left( + fill_start_delta_lens + .clone() + .map(|(start_t, _)| Percentage(start_t * 100.0)), + ) + .width( + fill_start_delta_lens + .map(|(_, delta)| Percentage(delta * 100.0)), + ) + // Hovering is handled on the param slider as a whole, this + // should not affect that + .hoverable(false); - // Either display the current value, or display all values over the - // parameter's steps - // TODO: Do the same thing as in the iced widget where we draw the - // text overlapping the fill area slightly differently. We can - // set the cip region directly in vizia. - match (style, step_count) { - ( - ParamSliderStyle::CurrentStepLabeled { .. }, - Some(step_count), - ) => { - HStack::new(cx, |cx| { - // There are step_count + 1 possible values for a - // discrete parameter - for value in 0..step_count + 1 { - let normalized_value = - value as f32 / step_count as f32; - Label::new( - cx, - param_preview_display_value_lens( - normalized_value, - ), - ) + // If the parameter is being modulated, then we'll display another + // filled bar showing the current modulation delta + // VIZIA's bindings make this a bit, uh, difficult to read + Element::new(cx) + .class("fill") + .class("fill--modulation") + .height(Stretch(1.0)) + .visibility( + modulation_start_delta_lens + .clone() + .map(|(_, delta)| *delta != 0.0), + ) + // Widths cannot be negative, so we need to compensate the start + // position if the width does happen to be negative + .width( + modulation_start_delta_lens + .clone() + .map(|(_, delta)| Percentage(delta.abs() * 100.0)), + ) + .left(modulation_start_delta_lens.map( + |(start_t, delta)| { + if *delta < 0.0 { + Percentage((start_t + delta) * 100.0) + } else { + Percentage(start_t * 100.0) + } + }, + )) + .hoverable(false); + + // Either display the current value, or display all values over the + // parameter's steps + // TODO: Do the same thing as in the iced widget where we draw the + // text overlapping the fill area slightly differently. We can + // set the cip region directly in vizia. + match (style, step_count) { + ( + ParamSliderStyle::CurrentStepLabeled { .. }, + Some(step_count), + ) => { + HStack::new(cx, |cx| { + // There are step_count + 1 possible values for a + // discrete parameter + for value in 0..step_count + 1 { + let normalized_value = + value as f32 / step_count as f32; + let preview_lens = + make_preview_value_lens(normalized_value); + + Label::new(cx, preview_lens) + .class("value") + .class("value--multiple") + .child_space(Stretch(1.0)) + .height(Stretch(1.0)) + .width(Stretch(1.0)) + .hoverable(false); + } + }) + .height(Stretch(1.0)) + .width(Stretch(1.0)) + .hoverable(false); + } + _ => { + Label::new(cx, display_value_lens) .class("value") - .class("value--multiple") + .class("value--single") .child_space(Stretch(1.0)) .height(Stretch(1.0)) .width(Stretch(1.0)) .hoverable(false); - } - }) - .height(Stretch(1.0)) - .width(Stretch(1.0)) - .hoverable(false); - } - _ => { - Label::new(cx, param_display_value_lens) - .class("value") - .class("value--single") - .child_space(Stretch(1.0)) - .height(Stretch(1.0)) - .width(Stretch(1.0)) - .hoverable(false); - } - }; - }) - .hoverable(false); - } - }, - ); - }); - }) + } + }; + }) + .hoverable(false); + } + }, + ); + }); + }), + ) } - /// Set the normalized value for a parameter if that would change the parameter's plain value - /// (to avoid unnecessary duplicate parameter changes). The begin- and end set parameter - /// messages need to be sent before calling this function. - fn set_normalized_value(&self, cx: &mut EventContext, normalized_value: f32) { - // This snaps to the nearest plain value if the parameter is stepped in some way. - // TODO: As an optimization, we could add a `const CONTINUOUS: bool` to the parameter to - // avoid this normalized->plain->normalized conversion for parameters that don't need - // it - let plain_value = unsafe { self.param_ptr.preview_plain(normalized_value) }; - let current_plain_value = unsafe { self.param_ptr.unmodulated_plain_value() }; - if plain_value != current_plain_value { - // For the aforementioned snapping - let normalized_plain_value = unsafe { self.param_ptr.preview_normalized(plain_value) }; - cx.emit(RawParamEvent::SetParameterNormalized( - self.param_ptr, - normalized_plain_value, - )); - } - } - - /// `set_normalized_value()`, but resulting from a mouse drag. When using the 'even' stepped - /// slider styles from [`ParamSliderStyle`] this will remap the normalized range to match up - /// with the fill value display. + /// `self.param_base.set_normalized_value()`, but resulting from a mouse drag. When using the + /// 'even' stepped slider styles from [`ParamSliderStyle`] this will remap the normalized range + /// to match up with the fill value display. This still needs to be wrapped in a parameter + /// automation gesture. fn set_normalized_value_drag(&self, cx: &mut EventContext, normalized_value: f32) { - let normalized_value = match (self.style, unsafe { self.param_ptr.step_count() }) { + let normalized_value = match (self.style, self.param_base.step_count()) { ( ParamSliderStyle::CurrentStep { even: true } | ParamSliderStyle::CurrentStepLabeled { even: true }, @@ -380,7 +353,7 @@ impl ParamSlider { _ => normalized_value, }; - self.set_normalized_value(cx, normalized_value); + self.param_base.set_normalized_value(cx, normalized_value); } } @@ -398,12 +371,10 @@ impl View for ParamSlider { meta.consume(); } ParamSliderEvent::TextInput(string) => { - if let Some(normalized_value) = - unsafe { self.param_ptr.string_to_normalized_value(string) } - { - cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); - self.set_normalized_value(cx, normalized_value); - cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + if let Some(normalized_value) = self.param_base.string_to_normalized_value(string) { + self.param_base.begin_set_parameter(cx); + self.param_base.set_normalized_value(cx, normalized_value); + self.param_base.end_set_parameter(cx); } self.text_input_active = false; @@ -426,12 +397,9 @@ impl View for ParamSlider { } else if cx.modifiers.command() { // Ctrl+Click and double click should reset the parameter instead of initiating // a drag operation - cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); - cx.emit(RawParamEvent::SetParameterNormalized( - self.param_ptr, - unsafe { self.param_ptr.default_normalized_value() }, - )); - cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + self.param_base.begin_set_parameter(cx); + self.param_base.set_normalized_value(cx, self.param_base.default_normalized_value()); + self.param_base.end_set_parameter(cx); } else { self.drag_active = true; cx.capture(); @@ -441,11 +409,10 @@ impl View for ParamSlider { // When holding down shift while clicking on a parameter we want to granuarly // edit the parameter without jumping to a new value - cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); + self.param_base.begin_set_parameter(cx); if cx.modifiers.shift() { - self.granular_drag_start_x_value = Some((cx.mouse.cursorx, unsafe { - self.param_ptr.unmodulated_normalized_value() - })); + self.granular_drag_start_x_value = Some((cx.mouse.cursorx, self.param_base.unmodulated_normalized_value() +)); } else { self.granular_drag_start_x_value = None; self.set_normalized_value_drag( @@ -460,12 +427,9 @@ impl View for ParamSlider { WindowEvent::MouseDoubleClick(MouseButton::Left) => { // Ctrl+Click and double click should reset the parameter instead of initiating // a drag operation - cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); - cx.emit(RawParamEvent::SetParameterNormalized( - self.param_ptr, - unsafe { self.param_ptr.default_normalized_value() }, - )); - cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + self.param_base.begin_set_parameter(cx); + self.param_base.set_normalized_value(cx, self.param_base.default_normalized_value()); + self.param_base.end_set_parameter(cx); meta.consume(); } @@ -475,7 +439,7 @@ impl View for ParamSlider { cx.release(); cx.set_active(false); - cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + self.param_base.end_set_parameter(cx); meta.consume(); } @@ -487,9 +451,8 @@ impl View for ParamSlider { if cx.modifiers.shift() { let (drag_start_x, drag_start_value) = *self.granular_drag_start_x_value.get_or_insert_with(|| { - (cx.mouse.cursorx, unsafe { - self.param_ptr.unmodulated_normalized_value() - }) + (cx.mouse.cursorx, self.param_base.unmodulated_normalized_value() +) }); self.set_normalized_value_drag( @@ -516,10 +479,7 @@ impl View for ParamSlider { // position if self.drag_active && self.granular_drag_start_x_value.is_some() { self.granular_drag_start_x_value = None; - self.set_normalized_value( - cx, - util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx), - ); + self.param_base.set_normalized_value(cx, util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx)); } } _ => {}