1
0
Fork 0

Move Vizia view param wrangling to helper struct

This cleans up the main widget's code a lot. We can move some more
behavior to this helper to reduce duplication.
This commit is contained in:
Robbert van der Helm 2022-11-06 19:13:08 +01:00
parent f7230b9f43
commit 14cb1cb679
4 changed files with 513 additions and 325 deletions

View file

@ -12,6 +12,7 @@ use vizia::prelude::*;
use super::ViziaState; use super::ViziaState;
mod generic_ui; mod generic_ui;
pub mod param_base;
mod param_button; mod param_button;
mod param_slider; mod param_slider;
mod peak_meter; mod peak_meter;

View file

@ -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<L, Params, P, FMap>
where
L: Lens<Target = Params> + 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<L, Params, P, FMap> Clone for ParamWidgetData<L, Params, P, FMap>
where
L: Lens<Target = Params> + 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<L, Params, P, FMap> ParamWidgetData<L, Params, P, FMap>
where
L: Lens<Target = Params> + 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<R, F>(&self, f: F) -> impl Lens<Target = R>
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<L, Params, P, FMap>(cx: &mut Context, params: L, params_to_param: FMap) -> Self
where
L: Lens<Target = Params> + 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<L, Params, P, FMap, F, R>(
params: L,
params_to_param: FMap,
content: F,
) -> impl FnOnce(&mut Context) -> R
where
L: Lens<Target = Params> + Clone,
Params: 'static,
P: Param + 'static,
FMap: Fn(&Params) -> &P + Copy + 'static,
F: FnOnce(&mut Context, ParamWidgetData<L, Params, P, FMap>) -> 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<L, Params, P, FMap, F, R>(
params: L,
params_to_param: FMap,
f: F,
) -> impl Lens<Target = R>
where
L: Lens<Target = Params> + 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<u32>);
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<usize>);
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<f32>);
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);
}

View file

@ -1,9 +1,9 @@
//! A toggleable button that integrates with NIH-plug's [`Param`] types. //! 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 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 /// 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 /// [`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. /// button is currently pressed.
#[derive(Lens)] #[derive(Lens)]
pub struct ParamButton { pub struct ParamButton {
// We're not allowed to store a reference to the parameter internally, at least not in the param_base: ParamWidgetBase,
// struct that implements [`View`]
param_ptr: ParamPtr,
} }
impl ParamButton { impl ParamButton {
@ -23,45 +21,45 @@ impl ParamButton {
/// `Params` object to the parameter you want to display a widget for. Parameter changes are /// `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 /// handled by emitting [`ParamEvent`][super::ParamEvent]s which are automatically handled by
/// the VIZIA wrapper. /// the VIZIA wrapper.
pub fn new<L, Params, P, F>(cx: &mut Context, params: L, params_to_param: F) -> Handle<Self> pub fn new<L, Params, P, FMap>(
cx: &mut Context,
params: L,
params_to_param: FMap,
) -> Handle<Self>
where where
L: Lens<Target = Params> + Clone, L: Lens<Target = Params> + Clone,
F: 'static + Fn(&Params) -> &P + Copy,
Params: 'static, 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 // 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 // NOTE: We use the normalized value _with modulation_ for this. There's no convenient way
// to show both modulated and unmodulated values here. // 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 } Self {
.build(cx, move |cx| { param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param),
Label::new(cx, &param_name); }
}) .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)) .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. /// 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) { 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 }; let new_value = if current_value >= 0.5 { 0.0 } else { 1.0 };
cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); self.param_base.begin_set_parameter(cx);
cx.emit(RawParamEvent::SetParameterNormalized( self.param_base.set_normalized_value(cx, new_value);
self.param_ptr, self.param_base.end_set_parameter(cx);
new_value,
));
cx.emit(RawParamEvent::EndSetParameter(self.param_ptr));
} }
} }

View file

@ -1,10 +1,10 @@
//! A slider that integrates with NIH-plug's [`Param`] types. //! 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 vizia::prelude::*;
use super::param_base::ParamWidgetBase;
use super::util::{self, ModifiersExt}; use super::util::{self, ModifiersExt};
use super::RawParamEvent;
/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the
/// normalized parameter. /// 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. /// [`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: 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)] #[derive(Lens)]
pub struct ParamSlider { pub struct ParamSlider {
// We're not allowed to store a reference to the parameter internally, at least not in the param_base: ParamWidgetBase,
// struct that implements [`View`]
param_ptr: ParamPtr,
/// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a
/// text value should not initiate a drag. /// text value should not initiate a drag.
@ -74,33 +70,22 @@ impl ParamSlider {
/// the VIZIA wrapper. /// the VIZIA wrapper.
/// ///
/// See [`ParamSliderExt`] for additional options. /// See [`ParamSliderExt`] for additional options.
pub fn new<L, Params, P, F>(cx: &mut Context, params: L, params_to_param: F) -> Handle<Self> pub fn new<L, Params, P, FMap>(
cx: &mut Context,
params: L,
params_to_param: FMap,
) -> Handle<Self>
where where
L: Lens<Target = Params> + Clone, L: Lens<Target = Params> + Clone,
F: 'static + Fn(&Params) -> &P + Copy,
Params: 'static, 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 // 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 // default value lies somewhere in the middle and the parameter is continuous. Otherwise
// this approach looks a bit jarring. // 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 { Self {
param_ptr, param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param),
drag_active: false, drag_active: false,
granular_drag_start_x_value: None, granular_drag_start_x_value: None,
@ -108,57 +93,50 @@ impl ParamSlider {
style: ParamSliderStyle::Centered, style: ParamSliderStyle::Centered,
text_input_active: false, text_input_active: false,
} }
.build(cx, move |cx| { .build(
cx,
ParamWidgetBase::view(params, params_to_param, move |cx, param_data| {
Binding::new(cx, ParamSlider::style, move |cx, style| { Binding::new(cx, ParamSlider::style, move |cx, style| {
let style = style.get(cx); 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);
// Only draw the text input widget when it gets focussed. Otherwise, overlay the let default_value = param_data.param().default_normalized_value();
// label with the slider. Creating the textbox based on let step_count = param_data.param().step_count();
// `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. // Can't use `.to_string()` here as that would include the modulation.
let param_display_value_lens = params.clone().map(move |params| { let unmodulated_normalized_value_lens =
let param = params_to_param(params); param_data.make_lens(|param| param.unmodulated_normalized_value());
param.normalized_value_to_string( let display_value_lens = param_data.make_lens(|param| {
param.unmodulated_normalized_value(), param.normalized_value_to_string(param.unmodulated_normalized_value(), true)
true,
)
}); });
let param_preview_display_value_lens = {
let params = params.clone(); // This is used to draw labels for `CurrentStepLabeled`
let make_preview_value_lens = {
let param_data = param_data.clone();
move |normalized_value| { move |normalized_value| {
params.clone().map(move |params| { param_data.make_lens(move |param| {
params_to_param(params) param.normalized_value_to_string(normalized_value, true)
.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()
});
// The resulting tuple `(start_t, delta)` corresponds to the start and the // 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 // signed width of the bar. `start_t` is in `[0, 1]`, and `delta` is in
// `[-1, 1]`. // `[-1, 1]`.
let fill_start_delta_lens = let draw_fill_from_default = matches!(style, ParamSliderStyle::Centered)
unmodulated_normalized_param_value_lens.map(move |current_value| { && 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 { match style {
ParamSliderStyle::Centered if draw_fill_from_default => { ParamSliderStyle::Centered if draw_fill_from_default => {
let delta = (default_value - current_value).abs(); 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
( (
default_value.min(*current_value), 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 }, if delta >= 1e-3 { delta } else { 0.0 },
) )
} }
@ -175,16 +153,16 @@ impl ParamSlider {
let discrete_values = step_count + 1.0; let discrete_values = step_count + 1.0;
let previous_step = let previous_step =
(current_value * step_count) / discrete_values; (current_value * step_count) / discrete_values;
(previous_step, discrete_values.recip()) (previous_step, discrete_values.recip())
} }
ParamSliderStyle::CurrentStep { .. } ParamSliderStyle::CurrentStep { .. }
| ParamSliderStyle::CurrentStepLabeled { .. } => { | ParamSliderStyle::CurrentStepLabeled { .. } => {
let previous_step = unsafe { let previous_step =
param_ptr.previous_normalized_step(*current_value) param_data.param().previous_normalized_step(*current_value);
}; let next_step =
let next_step = unsafe { param_data.param().next_normalized_step(*current_value);
param_ptr.next_normalized_step(*current_value)
};
( (
(previous_step + current_value) / 2.0, (previous_step + current_value) / 2.0,
((next_step - current_value) ((next_step - current_value)
@ -193,20 +171,22 @@ impl ParamSlider {
) )
} }
} }
}
}); });
// If the parameter is being modulated by the host (this only works for CLAP // 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 // plugins with hosts that support this), then this is the difference
// between the 'true' value and the current value after modulation has been // between the 'true' value and the current value after modulation has been
// applied. // applied.
let modulation_start_delta_lens = params.clone().map(move |params| { let modulation_start_delta_lens = param_data.make_lens(move |param| {
match style { match style {
// Don't show modulation for stepped parameters since it wouldn't // Don't show modulation for stepped parameters since it wouldn't
// make a lot of sense visually // make a lot of sense visually
ParamSliderStyle::CurrentStep { .. } ParamSliderStyle::CurrentStep { .. }
| ParamSliderStyle::CurrentStepLabeled { .. } => (0.0, 0.0), | ParamSliderStyle::CurrentStepLabeled { .. } => (0.0, 0.0),
ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => { ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => {
let param = params_to_param(params);
let modulation_start = param.unmodulated_normalized_value(); let modulation_start = param.unmodulated_normalized_value();
( (
modulation_start, modulation_start,
param.normalized_value() - modulation_start, param.normalized_value() - modulation_start,
@ -215,8 +195,16 @@ impl ParamSlider {
} }
}); });
// 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) { if text_input_active.get(cx) {
Textbox::new(cx, param_display_value_lens) Textbox::new(cx, display_value_lens.clone())
.class("value-entry") .class("value-entry")
.on_submit(|cx, string, success| { .on_submit(|cx, string, success| {
if success { if success {
@ -236,6 +224,12 @@ impl ParamSlider {
.height(Stretch(1.0)) .height(Stretch(1.0))
.width(Stretch(1.0)); .width(Stretch(1.0));
} else { } 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();
ZStack::new(cx, move |cx| { ZStack::new(cx, move |cx| {
// The filled bar portion. This can be visualized in a couple // The filled bar portion. This can be visualized in a couple
// different ways depending on the current style property. See // different ways depending on the current style property. See
@ -250,7 +244,6 @@ impl ParamSlider {
) )
.width( .width(
fill_start_delta_lens fill_start_delta_lens
.clone()
.map(|(_, delta)| Percentage(delta * 100.0)), .map(|(_, delta)| Percentage(delta * 100.0)),
) )
// Hovering is handled on the param slider as a whole, this // Hovering is handled on the param slider as a whole, this
@ -276,7 +269,7 @@ impl ParamSlider {
.clone() .clone()
.map(|(_, delta)| Percentage(delta.abs() * 100.0)), .map(|(_, delta)| Percentage(delta.abs() * 100.0)),
) )
.left(modulation_start_delta_lens.clone().map( .left(modulation_start_delta_lens.map(
|(start_t, delta)| { |(start_t, delta)| {
if *delta < 0.0 { if *delta < 0.0 {
Percentage((start_t + delta) * 100.0) Percentage((start_t + delta) * 100.0)
@ -303,12 +296,10 @@ impl ParamSlider {
for value in 0..step_count + 1 { for value in 0..step_count + 1 {
let normalized_value = let normalized_value =
value as f32 / step_count as f32; value as f32 / step_count as f32;
Label::new( let preview_lens =
cx, make_preview_value_lens(normalized_value);
param_preview_display_value_lens(
normalized_value, Label::new(cx, preview_lens)
),
)
.class("value") .class("value")
.class("value--multiple") .class("value--multiple")
.child_space(Stretch(1.0)) .child_space(Stretch(1.0))
@ -322,7 +313,7 @@ impl ParamSlider {
.hoverable(false); .hoverable(false);
} }
_ => { _ => {
Label::new(cx, param_display_value_lens) Label::new(cx, display_value_lens)
.class("value") .class("value")
.class("value--single") .class("value--single")
.child_space(Stretch(1.0)) .child_space(Stretch(1.0))
@ -337,34 +328,16 @@ impl ParamSlider {
}, },
); );
}); });
}) }),
)
} }
/// Set the normalized value for a parameter if that would change the parameter's plain value /// `self.param_base.set_normalized_value()`, but resulting from a mouse drag. When using the
/// (to avoid unnecessary duplicate parameter changes). The begin- and end set parameter /// 'even' stepped slider styles from [`ParamSliderStyle`] this will remap the normalized range
/// messages need to be sent before calling this function. /// to match up with the fill value display. This still needs to be wrapped in a parameter
fn set_normalized_value(&self, cx: &mut EventContext, normalized_value: f32) { /// automation gesture.
// 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.
fn set_normalized_value_drag(&self, cx: &mut EventContext, normalized_value: f32) { 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::CurrentStep { even: true }
| ParamSliderStyle::CurrentStepLabeled { even: true }, | ParamSliderStyle::CurrentStepLabeled { even: true },
@ -380,7 +353,7 @@ impl ParamSlider {
_ => normalized_value, _ => 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(); meta.consume();
} }
ParamSliderEvent::TextInput(string) => { ParamSliderEvent::TextInput(string) => {
if let Some(normalized_value) = if let Some(normalized_value) = self.param_base.string_to_normalized_value(string) {
unsafe { self.param_ptr.string_to_normalized_value(string) } self.param_base.begin_set_parameter(cx);
{ self.param_base.set_normalized_value(cx, normalized_value);
cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); self.param_base.end_set_parameter(cx);
self.set_normalized_value(cx, normalized_value);
cx.emit(RawParamEvent::EndSetParameter(self.param_ptr));
} }
self.text_input_active = false; self.text_input_active = false;
@ -426,12 +397,9 @@ impl View for ParamSlider {
} else if cx.modifiers.command() { } else if cx.modifiers.command() {
// Ctrl+Click and double click should reset the parameter instead of initiating // Ctrl+Click and double click should reset the parameter instead of initiating
// a drag operation // a drag operation
cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); self.param_base.begin_set_parameter(cx);
cx.emit(RawParamEvent::SetParameterNormalized( self.param_base.set_normalized_value(cx, self.param_base.default_normalized_value());
self.param_ptr, self.param_base.end_set_parameter(cx);
unsafe { self.param_ptr.default_normalized_value() },
));
cx.emit(RawParamEvent::EndSetParameter(self.param_ptr));
} else { } else {
self.drag_active = true; self.drag_active = true;
cx.capture(); cx.capture();
@ -441,11 +409,10 @@ impl View for ParamSlider {
// When holding down shift while clicking on a parameter we want to granuarly // When holding down shift while clicking on a parameter we want to granuarly
// edit the parameter without jumping to a new value // 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() { if cx.modifiers.shift() {
self.granular_drag_start_x_value = Some((cx.mouse.cursorx, unsafe { self.granular_drag_start_x_value = Some((cx.mouse.cursorx, self.param_base.unmodulated_normalized_value()
self.param_ptr.unmodulated_normalized_value() ));
}));
} else { } else {
self.granular_drag_start_x_value = None; self.granular_drag_start_x_value = None;
self.set_normalized_value_drag( self.set_normalized_value_drag(
@ -460,12 +427,9 @@ impl View for ParamSlider {
WindowEvent::MouseDoubleClick(MouseButton::Left) => { WindowEvent::MouseDoubleClick(MouseButton::Left) => {
// Ctrl+Click and double click should reset the parameter instead of initiating // Ctrl+Click and double click should reset the parameter instead of initiating
// a drag operation // a drag operation
cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); self.param_base.begin_set_parameter(cx);
cx.emit(RawParamEvent::SetParameterNormalized( self.param_base.set_normalized_value(cx, self.param_base.default_normalized_value());
self.param_ptr, self.param_base.end_set_parameter(cx);
unsafe { self.param_ptr.default_normalized_value() },
));
cx.emit(RawParamEvent::EndSetParameter(self.param_ptr));
meta.consume(); meta.consume();
} }
@ -475,7 +439,7 @@ impl View for ParamSlider {
cx.release(); cx.release();
cx.set_active(false); cx.set_active(false);
cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); self.param_base.end_set_parameter(cx);
meta.consume(); meta.consume();
} }
@ -487,9 +451,8 @@ impl View for ParamSlider {
if cx.modifiers.shift() { if cx.modifiers.shift() {
let (drag_start_x, drag_start_value) = let (drag_start_x, drag_start_value) =
*self.granular_drag_start_x_value.get_or_insert_with(|| { *self.granular_drag_start_x_value.get_or_insert_with(|| {
(cx.mouse.cursorx, unsafe { (cx.mouse.cursorx, self.param_base.unmodulated_normalized_value()
self.param_ptr.unmodulated_normalized_value() )
})
}); });
self.set_normalized_value_drag( self.set_normalized_value_drag(
@ -516,10 +479,7 @@ impl View for ParamSlider {
// position // position
if self.drag_active && self.granular_drag_start_x_value.is_some() { if self.drag_active && self.granular_drag_start_x_value.is_some() {
self.granular_drag_start_x_value = None; self.granular_drag_start_x_value = None;
self.set_normalized_value( self.param_base.set_normalized_value(cx, util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx));
cx,
util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx),
);
} }
} }
_ => {} _ => {}