1
0
Fork 0

Implement most of the iced ParamSlider for Vizia

This commit is contained in:
Robbert van der Helm 2022-03-19 01:17:13 +01:00
parent d8e8d80402
commit 64950055ea
6 changed files with 286 additions and 14 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -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;
}

View file

@ -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) {

View file

@ -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<f32>,
}
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<Target = Params>,
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<String> {
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),
);
}
}
}
_ => {}
}
}
}
}

View file

@ -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)
}

View file

@ -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<P: Param>(&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<P: Param>(&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<P: Param>(&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<P: Param>(&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<P: Param>(&self, param: &P) -> f32 {
unsafe {
self.context
self.raw_context
.raw_default_normalized_param_value(param.as_ptr())
}
}