From ee9e3701f3c739f0883528310729d50244256193 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 14 Mar 2022 14:16:47 +0100 Subject: [PATCH] Implement dragging for the parameter slider --- Cargo.lock | 2 +- nih_plug_iced/src/widgets.rs | 2 +- nih_plug_iced/src/widgets/param_slider.rs | 94 ++++++++++++++++++++--- nih_plug_iced/src/widgets/util.rs | 16 +++- plugins/examples/gain-gui/src/editor.rs | 9 ++- 5 files changed, 106 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 961e41fe..eac4a89f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,7 +1396,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "iced_baseview" version = "0.0.3" -source = "git+https://github.com/robbert-vdh/iced_baseview.git?branch=feature/update-baseview#b32cc3f2a4dafee4e67b3e482b3794bb16f98752" +source = "git+https://github.com/robbert-vdh/iced_baseview.git?branch=feature/update-baseview#5d09ba42d28923d144c8b7339a9f3f5e1252b37b" dependencies = [ "baseview", "copypasta", diff --git a/nih_plug_iced/src/widgets.rs b/nih_plug_iced/src/widgets.rs index 58f6c399..69a259ad 100644 --- a/nih_plug_iced/src/widgets.rs +++ b/nih_plug_iced/src/widgets.rs @@ -7,7 +7,7 @@ use nih_plug::param::internals::ParamPtr; -mod param_slider; +pub mod param_slider; pub mod util; pub use param_slider::ParamSlider; diff --git a/nih_plug_iced/src/widgets/param_slider.rs b/nih_plug_iced/src/widgets/param_slider.rs index b4b4b434..0f774b10 100644 --- a/nih_plug_iced/src/widgets/param_slider.rs +++ b/nih_plug_iced/src/widgets/param_slider.rs @@ -1,8 +1,8 @@ //! A slider that integrates with NIH-plug's [`Param`] types. use crate::{ - alignment, backend, event, layout, mouse, renderer, text, Clipboard, Color, Element, Event, - Layout, Length, Point, Rectangle, Shell, Size, Widget, + alignment, backend, event, layout, mouse, renderer, text, touch, Clipboard, Color, Element, + Event, Layout, Length, Point, Rectangle, Shell, Size, Widget, }; use nih_plug::prelude::{GuiContext, Param, ParamSetter}; @@ -12,7 +12,13 @@ use super::ParamMessage; /// A slider that integrates with NIH-plug's [`Param`] types. /// /// TODO: There are currently no styling options at all +/// TODO: Handle Shift+drag for granular drag +/// TODO: Handle Ctrl+click for reset +/// TODO: Handle Double click for reset +/// TODO: Handle Alt+click for text entry pub struct ParamSlider<'a, P: Param, Renderer: text::Renderer> { + state: &'a mut State, + param: &'a P, /// We'll visualize the parameter's current value by drawing the difference between the current /// normalized value and the default normalized value. @@ -24,12 +30,20 @@ pub struct ParamSlider<'a, P: Param, Renderer: text::Renderer> { font: Renderer::Font, } +/// State for a [`ParamSlider`]. +#[derive(Debug, Default)] +pub struct State { + drag_active: bool, +} + impl<'a, P: Param, Renderer: text::Renderer> ParamSlider<'a, P, Renderer> { /// Creates a new [`ParamSlider`] for the given parameter. - pub fn new(param: &'a P, context: &'a dyn GuiContext) -> Self { + pub fn new(state: &'a mut State, param: &'a P, context: &'a dyn GuiContext) -> Self { let setter = ParamSetter::new(context); Self { + state, + param, setter, @@ -85,14 +99,54 @@ impl<'a, P: Param, Renderer: text::Renderer> Widget fn on_event( &mut self, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, + event: Event, + layout: Layout<'_>, + cursor_position: Point, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, ParamMessage>, + shell: &mut Shell<'_, ParamMessage>, ) -> event::Status { - // TODO: Handle interaction + let bounds = layout.bounds(); + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if bounds.contains(cursor_position) { + shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); + self.state.drag_active = true; + + // Immediately trigger a parameter update if the value would be different + self.set_normalized_value( + shell, + util::remap_rect_x_coordinate(&bounds, cursor_position.x), + ); + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { + if bounds.contains(cursor_position) { + shell.publish(ParamMessage::EndSetParameter(self.param.as_ptr())); + self.state.drag_active = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if self.state.drag_active && bounds.contains(cursor_position) { + self.set_normalized_value( + shell, + util::remap_rect_x_coordinate(&bounds, cursor_position.x), + ); + + return event::Status::Captured; + } + } + _ => {} + } + event::Status::Ignored } @@ -145,11 +199,11 @@ impl<'a, P: Param, Renderer: text::Renderer> Widget // We'll visualize the difference between the current value and the default value let current_value = self.param.normalized_value(); - let fill_start_x = util::remap_rect_x( + let fill_start_x = util::remap_rect_x_t( &bounds, self.setter.default_normalized_param_value(self.param), ); - let fill_end_x = util::remap_rect_x(&bounds, current_value); + let fill_end_x = util::remap_rect_x_t(&bounds, current_value); let fill_color = Color::from_rgb8(196, 196, 196); let fill_rect = Rectangle { @@ -204,6 +258,26 @@ impl<'a, P: Param, Renderer: text::Renderer> Widget } } +impl<'a, P: Param, Renderer: text::Renderer> ParamSlider<'a, P, Renderer> { + /// 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, shell: &mut Shell<'_, ParamMessage>, 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 = self.param.preview_plain(normalized_value); + let current_plain_value = self.param.plain_value(); + if plain_value != current_plain_value { + shell.publish(ParamMessage::SetParameterNormalized( + self.param.as_ptr(), + normalized_value, + )); + } + } +} + impl<'a, P: Param> ParamSlider<'a, P, backend::Renderer> { /// Convert this [`ParamSlider`] into an [`Element`] with the correct message. You should have a /// variant on your own message type that wraps around [`ParamMessage`] so you can forward those diff --git a/nih_plug_iced/src/widgets/util.rs b/nih_plug_iced/src/widgets/util.rs index 76c1131a..e755635b 100644 --- a/nih_plug_iced/src/widgets/util.rs +++ b/nih_plug_iced/src/widgets/util.rs @@ -4,12 +4,24 @@ use crate::Rectangle; /// Remap a `[0, 1]` value to an x-coordinate within this rectangle. The value will be clamped to /// `[0, 1]` if it isn't already in that range. -pub fn remap_rect_x(rect: &Rectangle, t: f32) -> f32 { +pub fn remap_rect_x_t(rect: &Rectangle, t: f32) -> f32 { rect.x + (rect.width * t.clamp(0.0, 1.0)) } /// Remap a `[0, 1]` value to a y-coordinate within this rectangle. The value will be clamped to /// `[0, 1]` if it isn't already in that range. -pub fn remap_rect_y(rect: &Rectangle, t: f32) -> f32 { +pub fn remap_rect_y_t(rect: &Rectangle, t: f32) -> f32 { rect.y + (rect.height * t.clamp(0.0, 1.0)) } + +/// Remap an x-coordinate to a `[0, 1]` value within this rectangle. The value will be clamped to +/// `[0, 1]` if it isn't already in that range. +pub fn remap_rect_x_coordinate(rect: &Rectangle, x_coord: f32) -> f32 { + ((x_coord - rect.x) / rect.width).clamp(0.0, 1.0) +} + +/// Remap a y-coordinate to a `[0, 1]` value within this rectangle. The value will be clamped to +/// `[0, 1]` if it isn't already in that range. +pub fn remap_rect_y_coordinate(rect: &Rectangle, y_coord: f32) -> f32 { + ((y_coord - rect.y) / rect.height).clamp(0.0, 1.0) +} diff --git a/plugins/examples/gain-gui/src/editor.rs b/plugins/examples/gain-gui/src/editor.rs index 379f361d..df7ec75d 100644 --- a/plugins/examples/gain-gui/src/editor.rs +++ b/plugins/examples/gain-gui/src/editor.rs @@ -1,5 +1,5 @@ use nih_plug::prelude::{Editor, GuiContext}; -use nih_plug_iced::widgets::{ParamMessage, ParamSlider}; +use nih_plug_iced::widgets as nih_widgets; use nih_plug_iced::*; use std::pin::Pin; use std::sync::Arc; @@ -22,13 +22,14 @@ struct GainEditor { params: Pin>, context: Arc, + gain_slider_state: nih_widgets::param_slider::State, meter_dummy_state: widget::button::State, } #[derive(Debug, Clone, Copy)] enum Message { /// Update a parameter's value. - ParamUpdate(ParamMessage), + ParamUpdate(nih_widgets::ParamMessage), } impl IcedEditor for GainEditor { @@ -43,6 +44,8 @@ impl IcedEditor for GainEditor { let editor = GainEditor { params, context, + + gain_slider_state: Default::default(), meter_dummy_state: widget::button::State::new(), }; @@ -84,7 +87,7 @@ impl IcedEditor for GainEditor { .vertical_alignment(alignment::Vertical::Center), ) .push( - ParamSlider::new(&self.params.gain, self.context.as_ref()).map(Message::ParamUpdate) + nih_widgets::ParamSlider::new(&mut self.gain_slider_state, &self.params.gain, self.context.as_ref()).map(Message::ParamUpdate) // Button::new(&mut self.gain_dummy_state, Text::new("Gain")) // .height(30.into()) // .width(180.into()),