From 64950055ea0eabf2ef8ebf13ca6df36ec6db7996 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 19 Mar 2022 01:17:13 +0100 Subject: [PATCH] Implement most of the iced ParamSlider for Vizia --- Cargo.lock | 12 +- nih_plug_vizia/assets/theme.css | 23 +++ nih_plug_vizia/src/widgets.rs | 3 + nih_plug_vizia/src/widgets/param_slider.rs | 220 +++++++++++++++++++++ nih_plug_vizia/src/widgets/util.rs | 20 +- src/context.rs | 22 ++- 6 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 nih_plug_vizia/src/widgets/param_slider.rs diff --git a/Cargo.lock b/Cargo.lock index 2df37f9d..0b27f852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbfe11fe19ff083c48923cf179540e8cd0535903dc35e178a1fdeeb59aef51f" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -3605,7 +3605,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vizia" version = "0.1.0" -source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#d8c36fcc91516492d5d43e43b77f2a64738d4d28" +source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#e65871171bbc0ef589a228497cf8a32624d979c8" dependencies = [ "vizia_baseview", "vizia_core", @@ -3614,7 +3614,7 @@ dependencies = [ [[package]] name = "vizia_baseview" version = "0.1.0" -source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#d8c36fcc91516492d5d43e43b77f2a64738d4d28" +source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#e65871171bbc0ef589a228497cf8a32624d979c8" dependencies = [ "baseview", "femtovg", @@ -3626,7 +3626,7 @@ dependencies = [ [[package]] name = "vizia_core" version = "0.1.0" -source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#d8c36fcc91516492d5d43e43b77f2a64738d4d28" +source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#e65871171bbc0ef589a228497cf8a32624d979c8" dependencies = [ "bitflags", "copypasta", @@ -3649,7 +3649,7 @@ dependencies = [ [[package]] name = "vizia_derive" version = "0.1.0" -source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#d8c36fcc91516492d5d43e43b77f2a64738d4d28" +source = "git+https://github.com/robbert-vdh/vizia.git?branch=feature/baseview-modifiers#e65871171bbc0ef589a228497cf8a32624d979c8" dependencies = [ "proc-macro2", "quote", diff --git a/nih_plug_vizia/assets/theme.css b/nih_plug_vizia/assets/theme.css index c89965c7..606f7bf2 100644 --- a/nih_plug_vizia/assets/theme.css +++ b/nih_plug_vizia/assets/theme.css @@ -1 +1,24 @@ /* Default styling for the widgets included in nih_plug_vizia */ + +param-slider { + height: 30px; + width: 180px; + border-color: #0a0a0a; + border-width: 1px; + background-color: transparent; + transition: background-color 0.1 0; +} + +/* WTB Sass */ +param-slider:active { + background-color: #8080801a; + transition: background-color 0.1 0; +} +param-slider:hover { + background-color: #8080801a; + transition: background-color 0.1 0; +} + +param-slider .fill { + background-color: #c4c4c4; +} diff --git a/nih_plug_vizia/src/widgets.rs b/nih_plug_vizia/src/widgets.rs index 7ab5acc2..fdc73071 100644 --- a/nih_plug_vizia/src/widgets.rs +++ b/nih_plug_vizia/src/widgets.rs @@ -11,8 +11,11 @@ use std::sync::Arc; use vizia::{Context, Model}; +mod param_slider; pub mod util; +pub use param_slider::ParamSlider; + /// Register the default theme for the widgets exported by this module. This is automatically called /// for you when using [`create_vizia_editor()`][super::create_vizia_editor()]. pub fn register_theme(cx: &mut Context) { diff --git a/nih_plug_vizia/src/widgets/param_slider.rs b/nih_plug_vizia/src/widgets/param_slider.rs new file mode 100644 index 00000000..e6d4d64d --- /dev/null +++ b/nih_plug_vizia/src/widgets/param_slider.rs @@ -0,0 +1,220 @@ +//! A slider that integrates with NIH-plug's [`Param`] types. + +use nih_plug::param::internals::ParamPtr; +use nih_plug::prelude::{Param, ParamSetter}; +use vizia::*; + +use super::util::{self, ModifiersExt}; +use super::RawParamEvent; + +/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the +/// noramlized parameter. +const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; + +/// A slider that integrates with NIH-plug's [`Param`] types. +/// +/// 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 +/// TODO: Implement ALt+Click text input in this version +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, + + /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a + /// text value should not initiate a drag. + drag_active: bool, + /// Whether the next click is a double click. Vizia will send a double click event followed by a + /// regular mouse down event when double clicking. + is_double_click: bool, + /// We keep track of the start coordinate holding down Shift while dragging for higher precision + /// dragging. This is a `None` value when granular dragging is not active. + granular_drag_start_x: Option, +} + +impl ParamSlider { + /// Creates a new [`ParamSlider`] for the given parameter. To accomdate 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`), the `ParamSetter` for retrieving the + /// parameter's default value, and a projection function that maps the `Params` object to the + /// parameter you want to display a widget for. + pub fn new<'a, L, Params, P, F>( + cx: &'a mut Context, + params: L, + setter: &ParamSetter, + params_to_param: F, + ) -> Handle<'a, Self> + where + L: Lens, + F: 'static + Fn(&Params) -> &P + Copy, + Params: 'static, + P: Param, + { + let param_display_value_lens = params + .clone() + .map(move |params| params_to_param(params).to_string()); + let normalized_param_value_lens = params + .clone() + .map(move |params| params_to_param(params).normalized_value()); + + // 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 appraoch 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 = unsafe { + setter + .raw_context + .raw_default_normalized_param_value(param_ptr) + }; + let step_count = *params + .map(move |params| params_to_param(params).step_count()) + .get(cx); + let draw_fill_from_default = step_count.is_none() && (0.45..=0.55).contains(&default_value); + + Self { + param_ptr, + + drag_active: false, + is_double_click: false, + granular_drag_start_x: None, + } + .build2(cx, |cx| { + ZStack::new(cx, move |cx| { + // The filled bar portion + Element::new(cx).class("fill").height(Stretch(1.0)).bind( + normalized_param_value_lens, + move |handle, value| { + let current_value = *value.get(handle.cx); + if draw_fill_from_default { + handle + .left(Percentage(default_value.min(current_value) * 100.0)) + .right(Percentage( + 100.0 - (default_value.max(current_value) * 100.0), + )); + } else { + handle + .left(Percentage(0.0)) + .right(Percentage(100.0 - (current_value * 100.0))); + } + }, + ); + + // Only draw the text input widget when it gets focussed. Otherwise, overlay the label with + // the slider. + // TODO: Text entry stuff + Label::new(cx, param_display_value_lens) + .height(Stretch(1.0)) + .width(Stretch(1.0)); + }); + }) + } + + /// 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 Context, 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.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, + )); + } + } +} + +impl View for ParamSlider { + fn element(&self) -> Option { + Some(String::from("param-slider")) + } + + fn event(&mut self, cx: &mut Context, event: &mut Event) { + if let Some(window_event) = event.message.downcast() { + // FIXME: Handle shift releasing, I don't see an event for that here + match window_event { + WindowEvent::MouseDown(MouseButton::Left) => { + // Ctrl+Click and double click should reset the parameter instead of initiating + // a drag operation + // TODO: Handle Alt+Click for text entry + if cx.modifiers.command() || self.is_double_click { + self.is_double_click = false; + + cx.emit(RawParamEvent::BeginSetParameter(self.param_ptr)); + cx.emit(RawParamEvent::ResetParameter(self.param_ptr)); + cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + } else { + self.drag_active = true; + cx.capture(); + cx.current.set_active(cx, true); + + // 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)); + if cx.modifiers.shift() { + self.granular_drag_start_x = Some(cx.mouse.cursorx); + } else { + self.granular_drag_start_x = None; + self.set_normalized_value( + cx, + util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx), + ); + } + } + } + WindowEvent::MouseDoubleClick(MouseButton::Left) => { + // Vizia will send a regular mouse down after this, so we'll handle the reset + // there + self.is_double_click = true; + } + WindowEvent::MouseUp(MouseButton::Left) => { + if self.drag_active { + self.drag_active = false; + cx.release(); + cx.current.set_active(cx, false); + + cx.emit(RawParamEvent::EndSetParameter(self.param_ptr)); + } + } + WindowEvent::MouseMove(x, _y) => { + if self.drag_active { + // If shift is being held then the drag should be more granular instead of + // absolute + if cx.modifiers.shift() { + let drag_start_x = + *self.granular_drag_start_x.get_or_insert(cx.mouse.cursorx); + + self.set_normalized_value( + cx, + util::remap_current_entity_x_coordinate( + cx, + drag_start_x + (*x - drag_start_x) * GRANULAR_DRAG_MULTIPLIER, + ), + ); + } else { + self.granular_drag_start_x = None; + + self.set_normalized_value( + cx, + util::remap_current_entity_x_coordinate(cx, *x), + ); + } + } + } + _ => {} + } + } + } +} diff --git a/nih_plug_vizia/src/widgets/util.rs b/nih_plug_vizia/src/widgets/util.rs index 3ae8ece3..7ba915ff 100644 --- a/nih_plug_vizia/src/widgets/util.rs +++ b/nih_plug_vizia/src/widgets/util.rs @@ -1,6 +1,6 @@ //! Utilities for writing VIZIA widgets. -use vizia::Modifiers; +use vizia::{Context, Modifiers}; /// An extension trait for [`Modifiers`] that adds platform-independent getters. pub trait ModifiersExt { @@ -33,3 +33,21 @@ impl ModifiersExt for Modifiers { self.contains(Modifiers::SHIFT) } } + +/// Remap an x-coordinate to a `[0, 1]` value within the current entity's bounding box. The value +/// will be clamped to `[0, 1]` if it isn't already in that range. +/// +/// FIXME: These functions probably include borders, we dont' want that +pub fn remap_current_entity_x_coordinate(cx: &Context, x_coord: f32) -> f32 { + let x_pos = cx.cache.get_posx(cx.current); + let width = cx.cache.get_width(cx.current); + ((x_coord - x_pos) / width).clamp(0.0, 1.0) +} + +/// Remap an y-coordinate to a `[0, 1]` value within the current entity's bounding box. The value +/// will be clamped to `[0, 1]` if it isn't already in that range. +pub fn remap_current_entity_y_coordinate(cx: &Context, y_coord: f32) -> f32 { + let y_pos = cx.cache.get_posy(cx.current); + let height = cx.cache.get_height(cx.current); + ((y_coord - y_pos) / height).clamp(0.0, 1.0) +} diff --git a/src/context.rs b/src/context.rs index 045c52d6..5adabdc2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -142,7 +142,7 @@ pub struct Transport { /// the host and reflected in the plugin's [`Params`][crate::param::internals::Params] object. These /// functions should only be called from the main thread. pub struct ParamSetter<'a> { - context: &'a dyn GuiContext, + pub raw_context: &'a dyn GuiContext, } // TODO: These conversions have not really been tested yet, there might be an error in there somewhere @@ -335,13 +335,15 @@ impl Transport { impl<'a> ParamSetter<'a> { pub fn new(context: &'a dyn GuiContext) -> Self { - Self { context } + Self { + raw_context: context, + } } /// Inform the host that you will start automating a parmater. This needs to be called before /// calling [`set_parameter()`][Self::set_parameter()] for the specified parameter. pub fn begin_set_parameter(&self, param: &P) { - unsafe { self.context.raw_begin_set_parameter(param.as_ptr()) }; + unsafe { self.raw_context.raw_begin_set_parameter(param.as_ptr()) }; } /// Set a parameter to the specified parameter value. You will need to call @@ -356,7 +358,10 @@ impl<'a> ParamSetter<'a> { pub fn set_parameter(&self, param: &P, value: P::Plain) { let ptr = param.as_ptr(); let normalized = param.preview_normalized(value); - unsafe { self.context.raw_set_parameter_normalized(ptr, normalized) }; + unsafe { + self.raw_context + .raw_set_parameter_normalized(ptr, normalized) + }; } /// Set a parameter to an already normalized value. Works exactly the same as @@ -368,21 +373,24 @@ impl<'a> ParamSetter<'a> { /// normalized value known to the host matches `param.normalized_value()`. pub fn set_parameter_normalized(&self, param: &P, normalized: f32) { let ptr = param.as_ptr(); - unsafe { self.context.raw_set_parameter_normalized(ptr, normalized) }; + unsafe { + self.raw_context + .raw_set_parameter_normalized(ptr, normalized) + }; } /// Inform the host that you are done automating a parameter. This needs to be called after one /// or more [`set_parameter()`][Self::set_parameter()] calls for a parameter so the host knows /// the automation gesture has finished. pub fn end_set_parameter(&self, param: &P) { - unsafe { self.context.raw_end_set_parameter(param.as_ptr()) }; + unsafe { self.raw_context.raw_end_set_parameter(param.as_ptr()) }; } /// Retrieve the default value for a parameter, in case you forgot. The value is already /// normalized to `[0, 1]`. This is useful when implementing GUIs, and it does not perform a callback. pub fn default_normalized_param_value(&self, param: &P) -> f32 { unsafe { - self.context + self.raw_context .raw_default_normalized_param_value(param.as_ptr()) } }