diff --git a/Cargo.lock b/Cargo.lock index aa5c99b3..21227d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,6 +533,7 @@ dependencies = [ "crossbeam", "egui", "egui-baseview", + "lazy_static", "nih_plug", "parking_lot", ] diff --git a/nih_plug_egui/Cargo.toml b/nih_plug_egui/Cargo.toml index 9a1eb19e..12187eac 100644 --- a/nih_plug_egui/Cargo.toml +++ b/nih_plug_egui/Cargo.toml @@ -15,4 +15,5 @@ crossbeam = "0.8" egui = "0.16" # Upstream doesn't work with the current baseview and egui versions egui-baseview = { git = "https://github.com/robbert-vdh/egui-baseview.git", branch = "fix/update-dependencies" } +lazy_static = "1.4" parking_lot = "0.12" diff --git a/nih_plug_egui/src/widgets/param_slider.rs b/nih_plug_egui/src/widgets/param_slider.rs index a8039bc2..3abd09e7 100644 --- a/nih_plug_egui/src/widgets/param_slider.rs +++ b/nih_plug_egui/src/widgets/param_slider.rs @@ -1,13 +1,25 @@ -use egui::{vec2, Response, Sense, Stroke, TextStyle, Ui, Widget}; +use egui::{vec2, Response, Sense, Stroke, TextStyle, Ui, Vec2, Widget}; +use lazy_static::lazy_static; use super::util; use nih_plug::{Param, ParamSetter}; +/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the +/// noramlized parameter. +const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015; + +lazy_static! { + static ref DRAG_NORMALIZED_START_VALUE_MEMORY_ID: egui::Id = egui::Id::new((file!(), 0)); + static ref DRAG_AMOUNT_MEMORY_ID: egui::Id = egui::Id::new((file!(), 1)); +} + /// A slider widget similar to [egui::widgets::Slider] that knows about NIH-plug parameters ranges /// and can get values for it. /// /// TODO: Vertical orientation /// TODO: Check below for more input methods that should be added +/// TODO: Decouple the logic from the drawing so we can also do things like nobs without having to +/// repeat everything pub struct ParamSlider<'a, P: Param> { param: &'a P, setter: &'a ParamSetter<'a>, @@ -46,9 +58,51 @@ impl<'a, P: Param> ParamSlider<'a, P> { .set_parameter_normalized(self.param, normalized_default); } + fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) { + // Remember the intial position when we started with the granular drag. This value gets + // reset whenever we have a normal itneraction with the slider. + let start_value = if Self::get_drag_amount_memory(ui) == 0.0 { + Self::set_drag_normalized_start_value_memory(ui, self.normalized_value()); + self.normalized_value() + } else { + Self::get_drag_normalized_start_value_memory(ui) + }; + + let total_drag_distance = drag_delta.x + Self::get_drag_amount_memory(ui); + Self::set_drag_amount_memory(ui, total_drag_distance); + + self.set_normalized_value( + (start_value + (total_drag_distance * GRANULAR_DRAG_MULTIPLIER)).clamp(0.0, 1.0), + ); + } + fn end_drag(&self) { self.setter.end_set_parameter(self.param); } + + fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 { + ui.memory() + .data + .get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID) + .unwrap_or(0.0) + } + + fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) { + ui.memory() + .data + .insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount); + } + + fn get_drag_amount_memory(ui: &Ui) -> f32 { + ui.memory() + .data + .get_temp(*DRAG_AMOUNT_MEMORY_ID) + .unwrap_or(0.0) + } + + fn set_drag_amount_memory(ui: &Ui, amount: f32) { + ui.memory().data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount); + } } impl Widget for ParamSlider<'_, P> { @@ -82,17 +136,24 @@ impl Widget for ParamSlider<'_, P> { // TODO: Optionally add alt+click for value entry? // TODO: Handle shift+drag being more granular if response.drag_started() { + // When beginning a drag or dragging normally, reset the memory used to keep track of + // our granular drag self.begin_drag(); + Self::set_drag_amount_memory(ui, 0.0); } if let Some(click_pos) = response.interact_pointer_pos() { - // Like double clicking, Ctrl+Click should reset the parameter if ui.input().modifiers.command { + // Like double clicking, Ctrl+Click should reset the parameter self.reset_param(); + } else if ui.input().modifiers.shift { + // And shift dragging should switch to a more granulra input method + self.granular_drag(ui, response.drag_delta()); } else { let proportion = egui::emath::remap_clamp(click_pos.x, response.rect.x_range(), 0.0..=1.0) as f64; self.set_normalized_value(proportion as f32); + Self::set_drag_amount_memory(ui, 0.0); } } if response.double_clicked() { diff --git a/plugins/examples/gain-gui/src/lib.rs b/plugins/examples/gain-gui/src/lib.rs index a4d1bebe..f7041d18 100644 --- a/plugins/examples/gain-gui/src/lib.rs +++ b/plugins/examples/gain-gui/src/lib.rs @@ -125,6 +125,7 @@ impl Plugin for Gain { // It's not yet fully implemented, as the text is missing. ui.label("Gain (now linked to some random skewed int)"); ui.add(widgets::ParamSlider::for_param(¶ms.some_int, setter)); + ui.add(widgets::ParamSlider::for_param(¶ms.gain, setter)); // This is a simple naieve version of a parameter slider that's not aware of how // the parmaeters work