From 3b08da0f099e1e9ae09c5163f440e2d4d4bb38c1 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 16 Mar 2022 01:17:12 +0100 Subject: [PATCH] Add a generic UI widget for iced --- nih_plug_iced/src/widgets.rs | 1 + nih_plug_iced/src/widgets/generic_ui.rs | 301 ++++++++++++++++++++++ nih_plug_iced/src/widgets/param_slider.rs | 130 +++++----- nih_plug_iced/src/widgets/peak_meter.rs | 4 +- 4 files changed, 369 insertions(+), 67 deletions(-) create mode 100644 nih_plug_iced/src/widgets/generic_ui.rs diff --git a/nih_plug_iced/src/widgets.rs b/nih_plug_iced/src/widgets.rs index 4d35164c..5f910bf6 100644 --- a/nih_plug_iced/src/widgets.rs +++ b/nih_plug_iced/src/widgets.rs @@ -7,6 +7,7 @@ use nih_plug::param::internals::ParamPtr; +pub mod generic_ui; pub mod param_slider; pub mod peak_meter; pub mod util; diff --git a/nih_plug_iced/src/widgets/generic_ui.rs b/nih_plug_iced/src/widgets/generic_ui.rs new file mode 100644 index 00000000..1a5add77 --- /dev/null +++ b/nih_plug_iced/src/widgets/generic_ui.rs @@ -0,0 +1,301 @@ +//! A simple generic UI widget that renders all parameters in a [`Params`] object as a scrollable +//! list of sliders and labels. + +use atomic_refcell::AtomicRefCell; +use iced_baseview::{Column, Row}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::pin::Pin; + +use nih_plug::param::internals::ParamPtr; +use nih_plug::prelude::{GuiContext, Param, Params}; + +use super::{ParamMessage, ParamSlider}; +use crate::backend::Renderer; +use crate::text::Renderer as TextRenderer; +use crate::{ + alignment, event, layout, renderer, widget, Alignment, Clipboard, Element, Event, Layout, + Length, Point, Rectangle, Scrollable, Shell, Text, Widget, +}; + +/// A widget that can be used to create a generic UI with. This is used in conjuction with empty +/// structs to emulate existential types. +pub trait ParamWidget { + /// The type of state stores by this parameter type. + type State: Default; + + /// Create an [`Element`] for a widget for the specified parameter. + fn into_widget_element<'a, P: Param>( + param: &'a P, + context: &'a dyn GuiContext, + state: &'a mut Self::State, + ) -> Element<'a, ParamMessage>; + + /// The same as [`into_widget_element()`][Self::into_widget_element()], but for a `ParamPtr`. + /// + /// # Safety + /// + /// Undefined behavior of the `ParamPtr` does not point to a valid parameter. + unsafe fn into_widget_element_raw<'a>( + param: &ParamPtr, + context: &'a dyn GuiContext, + state: &'a mut Self::State, + ) -> Element<'a, ParamMessage> { + match param { + ParamPtr::FloatParam(p) => Self::into_widget_element(&**p, context, state), + ParamPtr::IntParam(p) => Self::into_widget_element(&**p, context, state), + ParamPtr::BoolParam(p) => Self::into_widget_element(&**p, context, state), + ParamPtr::EnumParam(p) => Self::into_widget_element(&**p, context, state), + } + } +} + +/// Create a generic UI using [`ParamSlider`]s. +#[derive(Default)] +pub struct GenericSlider; + +/// A list of scrollable widgets for every paramter in a [`Params`] object. The [`ParamWidget`] type +/// determines what widget to use for this. +/// +/// TODO: There's no way to configure the individual widgets. +pub struct GenericUi<'a, W: ParamWidget> { + state: &'a mut State, + + params: Pin<&'a dyn Params>, + context: &'a dyn GuiContext, + + width: Length, + height: Length, + max_width: u32, + max_height: u32, + + /// We don't emit any messages or store the actual widgets, but iced requires us to define some + /// message type anyways. + _phantom: PhantomData, +} + +/// State for a [`GenericUi`]. +#[derive(Debug, Default)] +pub struct State { + /// The internal state for each parameter's widget. + scrollable_state: AtomicRefCell, + /// The internal state for each parameter's widget. + widget_state: AtomicRefCell>, +} + +impl<'a, W> GenericUi<'a, W> +where + W: ParamWidget, +{ + /// Creates a new [`GenericUi`] for all provided parameters. + pub fn new( + state: &'a mut State, + params: Pin<&'a dyn Params>, + context: &'a dyn GuiContext, + ) -> Self { + Self { + state, + + params, + context, + + width: Length::Fill, + height: Length::Fill, + max_width: u32::MAX, + max_height: u32::MAX, + + _phantom: PhantomData, + } + } + + /// Sets the width of the [`GenericUi`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`GenericUi`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the maximum width of the [`GenericUi`]. + pub fn max_width(mut self, width: u32) -> Self { + self.max_width = width; + self + } + + /// Sets the maximum height of the [`GenericUi`]. + pub fn max_height(mut self, height: u32) -> Self { + self.max_height = height; + self + } + + /// Create a temporary [`Scrollable`]. This needs to be created on demand because it needs to + /// mutably borrow the `Scrollable`'s widget state. + fn with_scrollable_widget( + &'a self, + scrollable_state: &'a mut widget::scrollable::State, + widget_state: &'a mut HashMap, + renderer: R, + f: F, + ) -> T + where + F: FnOnce(Scrollable<'a, ParamMessage>, R) -> T, + R: Borrow, + { + let text_size = renderer.borrow().default_size(); + let spacing = (text_size as f32 * 0.2).round() as u16; + let padding = (text_size as f32 * 0.5).round() as u16; + + let mut scrollable = Scrollable::new(scrollable_state) + .width(self.width) + .height(self.height) + .max_width(self.max_width) + .max_height(self.max_height) + .spacing(spacing) + .padding(padding) + .align_items(Alignment::Center); + + let param_map = self.params.param_map(); + let param_ids = self.params.param_ids(); + + // Make sure we already have widget state for each widget + for param_ptr in param_map.values() { + if !widget_state.contains_key(param_ptr) { + widget_state.insert(*param_ptr, Default::default()); + } + } + + for param_id in param_ids { + let param_ptr = param_map[param_id]; + // SAFETY: We only borrow each item once, and the plugin framework statically asserted + // that parameter indices are unique and this widget state cannot outlive this + // function + let widget_state: &'a mut W::State = + unsafe { &mut *(widget_state.get_mut(¶m_ptr).unwrap() as *mut _) }; + + // Show the label next to the parameter for better use of the space + scrollable = scrollable.push( + Row::new() + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(spacing * 2) + .push( + Text::new(unsafe { param_ptr.name() }) + .height(20.into()) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Right) + .vertical_alignment(alignment::Vertical::Center), + ) + .push(unsafe { + W::into_widget_element_raw(¶m_ptr, self.context, widget_state) + }), + ); + } + + f(scrollable, renderer) + } +} + +impl<'a, W> Widget for GenericUi<'a, W> +where + W: ParamWidget, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + let mut scrollable_state = self.state.scrollable_state.borrow_mut(); + let mut widget_state = self.state.widget_state.borrow_mut(); + self.with_scrollable_widget( + &mut scrollable_state, + &mut widget_state, + renderer, + |scrollable, _| scrollable.layout(renderer, limits), + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let mut scrollable_state = self.state.scrollable_state.borrow_mut(); + let mut widget_state = self.state.widget_state.borrow_mut(); + self.with_scrollable_widget( + &mut scrollable_state, + &mut widget_state, + renderer, + |scrollable, renderer| { + scrollable.draw(renderer, style, layout, cursor_position, viewport) + }, + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, ParamMessage>, + ) -> event::Status { + let mut scrollable_state = self.state.scrollable_state.borrow_mut(); + let mut widget_state = self.state.widget_state.borrow_mut(); + self.with_scrollable_widget( + &mut scrollable_state, + &mut widget_state, + renderer, + |mut scrollable, _| { + scrollable.on_event(event, layout, cursor_position, renderer, clipboard, shell) + }, + ) + } +} + +impl ParamWidget for GenericSlider { + type State = super::param_slider::State; + + fn into_widget_element<'a, P: Param>( + param: &'a P, + context: &'a dyn GuiContext, + state: &'a mut Self::State, + ) -> Element<'a, ParamMessage> { + ParamSlider::new(state, param, context).into() + } +} + +impl<'a, W: ParamWidget> GenericUi<'a, W> { + /// Convert this [`GenericUi`] 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 + /// messages to + /// [`IcedEditor::handle_param_message()`][crate::IcedEditor::handle_param_message()]. + pub fn map(self, f: F) -> Element<'a, Message> + where + Message: 'static, + F: Fn(ParamMessage) -> Message + 'static, + { + Element::from(self).map(f) + } +} + +impl<'a, W> From> for Element<'a, ParamMessage> +where + W: ParamWidget, +{ + fn from(widget: GenericUi<'a, W>) -> Self { + Element::new(widget) + } +} diff --git a/nih_plug_iced/src/widgets/param_slider.rs b/nih_plug_iced/src/widgets/param_slider.rs index 643296a2..bcbe3078 100644 --- a/nih_plug_iced/src/widgets/param_slider.rs +++ b/nih_plug_iced/src/widgets/param_slider.rs @@ -141,6 +141,71 @@ impl<'a, P: Param> ParamSlider<'a, P> { self.font = font; self } + + /// Create a temporary [`TextInput`] hooked up to [`State::text_input_value`] and outputting + /// [`TextInputMessage`] messages and do something with it. This can be used to + fn with_text_input(&self, layout: Layout, renderer: R, current_value: &str, f: F) -> T + where + F: FnOnce(TextInput<'_, TextInputMessage>, Layout, R) -> T, + R: Borrow, + { + let mut text_input_state = self.state.text_input_state.borrow_mut(); + text_input_state.focus(); + + let text_size = self + .text_size + .unwrap_or_else(|| renderer.borrow().default_size()); + let text_width = renderer + .borrow() + .measure_width(current_value, text_size, self.font); + let text_input = TextInput::new( + &mut text_input_state, + "", + current_value, + TextInputMessage::Value, + ) + .font(self.font) + .size(text_size) + .width(Length::Units(text_width.ceil() as u16)) + .style(TextInputStyle) + .on_submit(TextInputMessage::Submit); + + // Make sure to not draw over the borders, and center the text + let offset_node = layout::Node::with_children( + Size { + width: text_width, + height: layout.bounds().size().height - (BORDER_WIDTH * 2.0), + }, + vec![layout::Node::new(layout.bounds().size())], + ); + let offset_layout = Layout::with_offset( + Vector { + x: layout.bounds().center_x() - (text_width / 2.0), + y: layout.position().y + BORDER_WIDTH, + }, + &offset_node, + ); + + f(text_input, offset_layout, 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> Widget for ParamSlider<'a, P> { @@ -480,71 +545,6 @@ impl<'a, P: Param> ParamSlider<'a, P> { { Element::from(self).map(f) } - - /// Create a temporary [`TextInput`] hooked up to [`State::text_input_value`] and outputting - /// [`TextInputMessage`] messages and do something with it. This can be used to - fn with_text_input(&self, layout: Layout, renderer: R, current_value: &str, f: F) -> T - where - F: FnOnce(TextInput<'_, TextInputMessage>, Layout, R) -> T, - R: Borrow, - { - let mut text_input_state = self.state.text_input_state.borrow_mut(); - text_input_state.focus(); - - let text_size = self - .text_size - .unwrap_or_else(|| renderer.borrow().default_size()); - let text_width = renderer - .borrow() - .measure_width(current_value, text_size, self.font); - let text_input = TextInput::new( - &mut text_input_state, - "", - current_value, - TextInputMessage::Value, - ) - .font(self.font) - .size(text_size) - .width(Length::Units(text_width.ceil() as u16)) - .style(TextInputStyle) - .on_submit(TextInputMessage::Submit); - - // Make sure to not draw over the borders, and center the text - let offset_node = layout::Node::with_children( - Size { - width: text_width, - height: layout.bounds().size().height - (BORDER_WIDTH * 2.0), - }, - vec![layout::Node::new(layout.bounds().size())], - ); - let offset_layout = Layout::with_offset( - Vector { - x: layout.bounds().center_x() - (text_width / 2.0), - y: layout.position().y + BORDER_WIDTH, - }, - &offset_node, - ); - - f(text_input, offset_layout, 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> From> for Element<'a, ParamMessage> { diff --git a/nih_plug_iced/src/widgets/peak_meter.rs b/nih_plug_iced/src/widgets/peak_meter.rs index 30e62bb1..211930ee 100644 --- a/nih_plug_iced/src/widgets/peak_meter.rs +++ b/nih_plug_iced/src/widgets/peak_meter.rs @@ -290,9 +290,9 @@ where } } -impl<'a, Message: 'a> From> for Element<'a, Message> +impl<'a, Message> From> for Element<'a, Message> where - Message: Clone, + Message: 'a + Clone, { fn from(widget: PeakMeter<'a, Message>) -> Self { Element::new(widget)