Implement most of the iced ParamSlider for Vizia
This commit is contained in:
parent
d8e8d80402
commit
64950055ea
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
220
nih_plug_vizia/src/widgets/param_slider.rs
Normal file
220
nih_plug_vizia/src/widgets/param_slider.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue