diff --git a/Cargo.toml b/Cargo.toml index 6b70605d9..26b0df555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/robbert-vdh/nih-plug" [workspace] resolver = "2" + members = [ "nih_plug_derive", "nih_plug_egui", @@ -57,7 +58,14 @@ assert_process_allocs = ["dep:assert_no_alloc"] # Enables an export target for standalone binaries through the # `nih_export_standalone()` function. Disabled by default as this requires # building additional dependencies for audio and MIDI handling. -standalone = ["dep:baseview", "dep:clap", "dep:cpal", "dep:jack", "dep:midir", "dep:rtrb"] +standalone = [ + "dep:baseview", + "dep:clap", + "dep:cpal", + "dep:jack", + "dep:midir", + "dep:rtrb", +] # Enables the `nih_export_vst3!()` macro. Enabled by default. This feature # exists mostly for GPL-compliance reasons, since even if you don't use the VST3 # wrapper you might otherwise still include a couple (unused) symbols from the @@ -86,7 +94,7 @@ backtrace = "0.3.65" bitflags = "1.3" cfg-if = "1.0" # This supports CLAP 1.2.2 -clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } +clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } crossbeam = "0.8" log = { version = "0.4", features = ["std", "release_max_level_info"] } midi-consts = "0.1" @@ -98,14 +106,20 @@ serde_json = "1.0" widestring = "1.0.0-beta.1" # Used for the `assert_process_allocs` feature -assert_no_alloc = { git = "https://github.com/robbert-vdh/rust-assert-no-alloc.git", branch = "feature/nested-permit-forbid", features = ["backtrace", "log"], optional = true } +assert_no_alloc = { git = "https://github.com/robbert-vdh/rust-assert-no-alloc.git", branch = "feature/nested-permit-forbid", features = [ + "backtrace", + "log", +], optional = true } # Used for the `standalone` feature # NOTE: OpenGL support is not needed here, but rust-analyzer gets confused when # some crates do use it and others don't -baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "579130ecb4f9f315ae52190af42f0ea46aeaa4a2", features = ["opengl"], optional = true } +baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "579130ecb4f9f315ae52190af42f0ea46aeaa4a2", default_features = false, optional = true } # All the claps! -clap = { version = "4.1.8", features = ["derive", "wrap_help"], optional = true } +clap = { version = "4.1.8", features = [ + "derive", + "wrap_help", +], optional = true } cpal = { version = "0.15", optional = true } jack = { version = "0.11.4", optional = true } midir = { version = "0.9.1", optional = true } diff --git a/nih_plug_iced/Cargo.toml b/nih_plug_iced/Cargo.toml index 7b2c98395..723159a08 100644 --- a/nih_plug_iced/Cargo.toml +++ b/nih_plug_iced/Cargo.toml @@ -8,7 +8,7 @@ license = "ISC" description = "An adapter to use iced GUIs with NIH-plug" [features] -default = ["opengl"] +default = [] # NOTE: wgpu support has been removed from the iced-baseview fork out because # this old iced version uses a wgpu version that doesn't pin the web-sys @@ -20,53 +20,33 @@ default = ["opengl"] # wgpu = ["iced_baseview/wgpu", "baseview/opengl"] # Use OpenGL instead of wgpu for the rendering. This should increase platform # compatibility at the cost of some iced features not being available. -opengl = ["iced_baseview/glow"] +# opengl = ["iced_baseview/glow"] # Enables a debug view in native platforms (press F12) debug = ["iced_baseview/debug"] -# # Enables the `Image` widget, only supported by the wgpu backend -# wgpu_image = ["iced_baseview/wgpu_image"] -# # Enables the `Svg` widget, only supported by the wgpu backend -# wgpu_svg = ["iced_baseview/wgpu_svg"] - -# # Enables the `Canvas` widget for the wgpu backend -# wgpu_canvas = ["iced_baseview/wgpu_canvas"] -# Enables the `Canvas` widget for the OpenGL backend -opengl_canvas = ["iced_baseview/glow_canvas"] - -# # Enables the `QRCode` widget for the wgpu backend -# wgpu_qr_code = ["iced_baseview/wgpu_qr_code"] -# Enables the `QRCode` widget for the OpenGL backend -opengl_qr_code = ["iced_baseview/glow_qr_code"] - -# # Enables using system fonts for the wgpu backend -# wgpu_default_system_font = ["iced_baseview/wgpu_default_system_font"] -# Enables using system fonts for the OpenGL backend -opengl_default_system_font = ["iced_baseview/glow_default_system_font"] - -# Enables advanced color conversion via `palette` -palette = ["iced_baseview/palette"] - -# Enables `tokio` as the `executor::Default` on native platforms -tokio = ["iced_baseview/tokio"] -# Enables `async-std` as the `executor::Default` on native platforms -async-std = ["iced_baseview/async-std"] -# Enables `smol` as the `executor::Default` on native platforms -smol = ["iced_baseview/smol"] +wgpu = ["iced_baseview/wgpu"] +# Enables the `Image` widget, only supported by the wgpu backend +image = ["iced_baseview/image"] +# Enables the `Svg` widget, only supported by the wgpu backend +svg = ["iced_baseview/svg"] +# Enables the `Canvas` widget for the wgpu backend +canvas = ["iced_baseview/canvas"] [dependencies] -nih_plug = { path = "..", default-features = false } +nih_plug = { path = "..", default_features = false } nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" } # The currently targeted version of baseview uses a different version of # `raw_window_handle` than NIH-plug, so we need to manually convert between them -raw-window-handle = "0.4" +raw-window-handle = "0.5" atomic_refcell = "0.1" -baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "1d9806d5bd92275d0d8142d9c9c90198757b9b25" } +baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "579130ecb4f9f315ae52190af42f0ea46aeaa4a2" } crossbeam = "0.8" -# This targets iced 0.4 -iced_baseview = { git = "https://github.com/robbert-vdh/iced_baseview.git", branch = "feature/update-baseview", default-features = false } +# This targets iced 0.13 +iced_baseview = { git = "https://github.com/BillyDM/iced_baseview.git", rev = "a0842a8205c852127981e27de6342cf2e665562f", default_features = false } +futures-util = "0.3" + # To make the state persistable serde = { version = "1.0", features = ["derive"] } diff --git a/nih_plug_iced/README.md b/nih_plug_iced/README.md index 548a9d1a3..c048e5803 100644 --- a/nih_plug_iced/README.md +++ b/nih_plug_iced/README.md @@ -9,7 +9,7 @@ configurations. To use wgpu instead, include the crate with the following options: ```toml -nih_plug_iced = { git = "https://github.com/robbert-vdh/nih-plug.git", default-features = false, features = ["wgpu"] } +nih_plug_iced = { git = "https://github.com/robbert-vdh/nih-plug.git", default_features = false, features = ["wgpu"] } ``` Iced has many more optional features. Check the `Cargo.toml` file for more diff --git a/nih_plug_iced/src/assets.rs b/nih_plug_iced/src/assets.rs index 9a6db7172..6a136b3f9 100644 --- a/nih_plug_iced/src/assets.rs +++ b/nih_plug_iced/src/assets.rs @@ -1,46 +1,37 @@ //! Binary assets for use with `nih_plug_iced`. -use crate::Font; +use std::borrow::Cow; + +use crate::core::Font; // This module provides a re-export and simple font wrappers around the re-exported fonts. pub use nih_plug_assets::*; -pub const NOTO_SANS_REGULAR: Font = Font::External { - name: "Noto Sans Regular", - bytes: fonts::NOTO_SANS_REGULAR, -}; - -pub const NOTO_SANS_REGULAR_ITALIC: Font = Font::External { - name: "Noto Sans Regular Italic", - bytes: fonts::NOTO_SANS_REGULAR_ITALIC, -}; - -pub const NOTO_SANS_THIN: Font = Font::External { - name: "Noto Sans Thin", - bytes: fonts::NOTO_SANS_THIN, -}; - -pub const NOTO_SANS_THIN_ITALIC: Font = Font::External { - name: "Noto Sans Thin Italic", - bytes: fonts::NOTO_SANS_THIN_ITALIC, -}; - -pub const NOTO_SANS_LIGHT: Font = Font::External { - name: "Noto Sans Light", - bytes: fonts::NOTO_SANS_LIGHT, -}; - -pub const NOTO_SANS_LIGHT_ITALIC: Font = Font::External { - name: "Noto Sans Light Italic", - bytes: fonts::NOTO_SANS_LIGHT_ITALIC, -}; - -pub const NOTO_SANS_BOLD: Font = Font::External { - name: "Noto Sans Bold", - bytes: fonts::NOTO_SANS_BOLD, -}; - -pub const NOTO_SANS_BOLD_ITALIC: Font = Font::External { - name: "Noto Sans Bold Italic", - bytes: fonts::NOTO_SANS_BOLD_ITALIC, -}; +pub const NOTO_SANS_REGULAR: Font = Font::with_name("Noto Sans Regular"); +pub const NOTO_SANS_REGULAR_ITALIC: Font = Font::with_name("Noto Sans Regular Italic"); +pub const NOTO_SANS_THIN: Font = Font::with_name("Noto Sans Thin"); +pub const NOTO_SANS_THIN_ITALIC: Font = Font::with_name("Noto Sans Thin Italic"); +pub const NOTO_SANS_LIGHT: Font = Font::with_name("Noto Sans Light"); +pub const NOTO_SANS_LIGHT_ITALIC: Font = Font::with_name("Noto Sans Light Italic"); +pub const NOTO_SANS_BOLD: Font = Font::with_name("Noto Sans Bold"); +pub const NOTO_SANS_BOLD_ITALIC: Font = Font::with_name("Noto Sans Bold Italic"); + +/// Useful for initializing the Settings, like this: +/// ```rust,ignore +/// Settings { +/// ... +/// fonts: noto_sans_fonts_data().into_iter().collect(), +/// } +/// ``` +pub const fn noto_sans_fonts_data() -> [Cow<'static, [u8]>; 8] { + [ + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_REGULAR), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_REGULAR_ITALIC), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_THIN), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_THIN_ITALIC), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_LIGHT), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_LIGHT_ITALIC), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_BOLD), + Cow::Borrowed(nih_plug_assets::fonts::NOTO_SANS_BOLD_ITALIC), + ] +} diff --git a/nih_plug_iced/src/editor.rs b/nih_plug_iced/src/editor.rs index d2984d270..050034048 100644 --- a/nih_plug_iced/src/editor.rs +++ b/nih_plug_iced/src/editor.rs @@ -1,20 +1,22 @@ //! And [`Editor`] implementation for iced. -use baseview::{WindowOpenOptions, WindowScalePolicy}; +use ::baseview::{WindowOpenOptions, WindowScalePolicy}; use crossbeam::atomic::AtomicCell; use crossbeam::channel; -pub use iced_baseview::*; +use iced_baseview::settings::IcedBaseviewSettings; use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle}; -use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::{borrow::Cow, sync::atomic::Ordering}; use crate::{wrapper, IcedEditor, IcedState, ParameterUpdate}; +pub use iced_baseview::*; + /// An [`Editor`] implementation that renders an iced [`Application`]. pub(crate) struct IcedEditorWrapper { pub(crate) iced_state: Arc, pub(crate) initialization_flags: E::InitializationFlags, + pub(crate) fonts: Vec>, /// The scaling factor reported by the host, if any. On macOS this will never be set and we /// should use the system scaling factor instead. @@ -25,32 +27,6 @@ pub(crate) struct IcedEditorWrapper { pub(crate) parameter_updates_receiver: Arc>, } -/// This version of `baseview` uses a different version of `raw_window_handle than NIH-plug, so we -/// need to adapt it ourselves. -struct ParentWindowHandleAdapter(nih_plug::editor::ParentWindowHandle); - -unsafe impl HasRawWindowHandle for ParentWindowHandleAdapter { - fn raw_window_handle(&self) -> RawWindowHandle { - match self.0 { - ParentWindowHandle::X11Window(window) => { - let mut handle = raw_window_handle::XcbHandle::empty(); - handle.window = window; - RawWindowHandle::Xcb(handle) - } - ParentWindowHandle::AppKitNsView(ns_view) => { - let mut handle = raw_window_handle::AppKitHandle::empty(); - handle.ns_view = ns_view; - RawWindowHandle::AppKit(handle) - } - ParentWindowHandle::Win32Hwnd(hwnd) => { - let mut handle = raw_window_handle::Win32Handle::empty(); - handle.hwnd = hwnd; - RawWindowHandle::Win32(handle) - } - } - } -} - impl Editor for IcedEditorWrapper { fn spawn( &self, @@ -62,8 +38,14 @@ impl Editor for IcedEditorWrapper { // TODO: iced_baseview does not have gracefuly error handling for context creation failures. // This will panic if the context could not be created. - let window = IcedWindow::>::open_parented( - &ParentWindowHandleAdapter(parent), + let window = iced_baseview::open_parented::, _>( + &parent, + // We use this wrapper to be able to pass the GUI context to the editor + ( + context, + self.parameter_updates_receiver.clone(), + self.initialization_flags.clone(), + ), Settings { window: WindowOpenOptions { title: String::from("iced window"), @@ -74,41 +56,13 @@ impl Editor for IcedEditorWrapper { scale: scaling_factor .map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)) .unwrap_or(WindowScalePolicy::SystemScaleFactor), - - #[cfg(feature = "opengl")] - gl_config: Some(baseview::gl::GlConfig { - // FIXME: glow_glyph forgot to add an `#extension`, so this won't work under - // OpenGL 3.2 at the moment. With that change applied this should work on - // OpenGL 3.2/macOS. - version: (3, 3), - red_bits: 8, - blue_bits: 8, - green_bits: 8, - alpha_bits: 8, - depth_bits: 24, - stencil_bits: 8, - samples: None, - srgb: true, - double_buffer: true, - vsync: true, - ..Default::default() - }), - // FIXME: Rust analyzer always thinks baseview/opengl is enabled even if we - // don't explicitly enable it, so you'd get a compile error if this line - // is missing - #[cfg(not(feature = "opengl"))] - gl_config: None, }, iced_baseview: IcedBaseviewSettings { ignore_non_modifier_keys: false, always_redraw: true, }, - // We use this wrapper to be able to pass the GUI context to the editor - flags: ( - context, - self.parameter_updates_receiver.clone(), - self.initialization_flags.clone(), - ), + fonts: self.fonts.clone(), + ..Default::default() }, ); @@ -154,7 +108,7 @@ impl Editor for IcedEditorWrapper { /// The window handle used for [`IcedEditorWrapper`]. struct IcedEditorHandle { iced_state: Arc, - window: iced_baseview::WindowHandle, + window: iced_baseview::window::WindowHandle, } /// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around diff --git a/nih_plug_iced/src/lib.rs b/nih_plug_iced/src/lib.rs index a8ae536b5..d548c23b1 100644 --- a/nih_plug_iced/src/lib.rs +++ b/nih_plug_iced/src/lib.rs @@ -39,7 +39,7 @@ //! fn new( //! params: Self::InitializationFlags, //! context: Arc, -//! ) -> (Self, Command) { +//! ) -> (Self, Task) { //! let editor = FooEditor { //! params, //! context, @@ -47,7 +47,7 @@ //! foo_slider_state: Default::default(), //! }; //! -//! (editor, Command::none()) +//! (editor, Task::none()) //! } //! //! fn context(&self) -> &dyn GuiContext { @@ -56,14 +56,13 @@ //! //! fn update( //! &mut self, -//! _window: &mut WindowQueue, //! message: Self::Message, -//! ) -> Command { +//! ) -> Task { //! match message { //! Message::ParamUpdate(message) => self.handle_param_message(message), //! } //! -//! Command::none() +//! Task::none() //! } //! //! fn view(&mut self) -> Element<'_, Self::Message> { @@ -89,12 +88,14 @@ //! } //! ``` -use baseview::WindowScalePolicy; +use ::baseview::WindowScalePolicy; use crossbeam::atomic::AtomicCell; use crossbeam::channel; +use iced_baseview::futures::Subscription; use nih_plug::params::persist::PersistentField; use nih_plug::prelude::{Editor, GuiContext}; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; // This doesn't need to be re-export but otherwise the compiler complains about // `hidden_glob_reexports` pub use std::fmt::Debug; @@ -126,6 +127,7 @@ mod wrapper; pub fn create_iced_editor( iced_state: Arc, initialization_flags: E::InitializationFlags, + fonts: Vec>, ) -> Option> { // We need some way to communicate parameter changes to the `IcedEditor` since parameter updates // come from outside of the editor's reactive model. This contains only capacity to store only @@ -147,6 +149,7 @@ pub fn create_iced_editor( parameter_updates_sender, parameter_updates_receiver: Arc::new(parameter_updates_receiver), + fonts, })) } @@ -162,12 +165,14 @@ pub trait IcedEditor: 'static + Send + Sync + Sized { type Message: 'static + Clone + Debug + Send; /// See [`Application::Flags`]. type InitializationFlags: 'static + Clone + Send + Sync; + /// See [`Application::Theme`] + type Theme: Default + DefaultStyle; /// See [`Application::new`]. This also receivs the GUI context in addition to the flags. fn new( initialization_fags: Self::InitializationFlags, context: Arc, - ) -> (Self, Command); + ) -> (Self, Task); /// Returns a reference to the GUI context. /// [`handle_param_message()`][Self::handle_param_message()] uses this to interact with the @@ -177,11 +182,7 @@ pub trait IcedEditor: 'static + Send + Sync + Sized { /// See [`Application::update`]. When receiving the variant that contains a /// [`widgets::ParamMessage`] you can call /// [`handle_param_message()`][Self::handle_param_message()] to handle the parameter update. - fn update( - &mut self, - window: &mut WindowQueue, - message: Self::Message, - ) -> Command; + fn update(&mut self, message: Self::Message) -> Task; /// See [`Application::subscription`]. fn subscription( @@ -192,13 +193,21 @@ pub trait IcedEditor: 'static + Send + Sync + Sized { } /// See [`Application::view`]. - fn view(&mut self) -> Element<'_, Self::Message>; + fn view(&self) -> Element<'_, Self::Message, Self::Theme, Renderer>; /// See [`Application::background_color`]. fn background_color(&self) -> Color { Color::WHITE } + fn theme(&self) -> Self::Theme { + Self::Theme::default() + } + + fn title(&self) -> String { + "nih_plug plugin".to_owned() + } + /// See [`Application::scale_policy`]. /// /// TODO: Is this needed? Editors shouldn't change the scale policy. @@ -206,20 +215,6 @@ pub trait IcedEditor: 'static + Send + Sync + Sized { WindowScalePolicy::SystemScaleFactor } - /// See [`Application::renderer_settings`]. - fn renderer_settings() -> iced_baseview::backend::settings::Settings { - iced_baseview::backend::settings::Settings { - // Enable some anti-aliasing by default. Since GUIs are likely very simple and most of - // the work will be on the CPU anyways this should not affect performance much. - antialiasing: Some(iced_baseview::backend::settings::Antialiasing::MSAAx4), - // Use Noto Sans as the default font as that renders a bit more cleanly than the default - // Lato font. This crate also contains other weights and versions of this font you can - // use for individual widgets. - default_font: Some(crate::assets::fonts::NOTO_SANS_REGULAR), - ..iced_baseview::backend::settings::Settings::default() - } - } - /// Handle a parameter update using the GUI context. fn handle_param_message(&self, message: ParamMessage) { // We can't use the fancy ParamSetter here because this needs to be type erased diff --git a/nih_plug_iced/src/widgets/generic_ui.rs b/nih_plug_iced/src/widgets/generic_ui.rs index 8e975a678..9a9df38d4 100644 --- a/nih_plug_iced/src/widgets/generic_ui.rs +++ b/nih_plug_iced/src/widgets/generic_ui.rs @@ -1,33 +1,29 @@ //! 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 std::borrow::Borrow; -use std::collections::HashMap; +use crate::core::widget::{Id, Operation, Tree}; +use crate::core::{ + alignment, event, layout, renderer, text, Clipboard, Element, Layout, Length, Rectangle, Shell, + Size, Widget, +}; +use crate::widget::{self, row, scrollable, Column, Scrollable, Space}; use std::marker::PhantomData; use std::sync::Arc; use nih_plug::prelude::{Param, ParamFlags, ParamPtr, 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, Row, Scrollable, Shell, Space, 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; - +pub trait ParamWidget { /// Create an [`Element`] for a widget for the specified parameter. fn into_widget_element<'a, P: Param>( param: &'a P, - state: &'a mut Self::State, - ) -> Element<'a, ParamMessage>; + ) -> Element<'a, ParamMessage, Theme, Renderer> + where + Theme: 'a, + Renderer: 'a; /// The same as [`into_widget_element()`][Self::into_widget_element()], but for a `ParamPtr`. /// @@ -35,14 +31,17 @@ pub trait ParamWidget { /// /// Undefined behavior of the `ParamPtr` does not point to a valid parameter. unsafe fn into_widget_element_raw<'a>( - param: &ParamPtr, - state: &'a mut Self::State, - ) -> Element<'a, ParamMessage> { + param: &'a ParamPtr, + ) -> Element<'a, ParamMessage, Theme, Renderer> + where + Theme: 'a, + Renderer: 'a, + { match param { - ParamPtr::FloatParam(p) => Self::into_widget_element(&**p, state), - ParamPtr::IntParam(p) => Self::into_widget_element(&**p, state), - ParamPtr::BoolParam(p) => Self::into_widget_element(&**p, state), - ParamPtr::EnumParam(p) => Self::into_widget_element(&**p, state), + ParamPtr::FloatParam(p) => Self::into_widget_element(&**p), + ParamPtr::IntParam(p) => Self::into_widget_element(&**p), + ParamPtr::BoolParam(p) => Self::into_widget_element(&**p), + ParamPtr::EnumParam(p) => Self::into_widget_element(&**p), } } } @@ -55,72 +54,72 @@ pub struct GenericSlider; /// 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: Arc, +pub struct GenericUi { + // Hacky work around so we can borrow &ParamPtr and ensure references + // stay alive for the lifetime of this object. + params: Vec, + id: Option, width: Length, height: Length, - max_width: u32, - max_height: u32, + max_width: u16, + max_height: u16, + pad_scrollbar: bool, /// 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>, + _phantom: PhantomData<(W, Theme, Renderer)>, } -impl<'a, W> GenericUi<'a, W> -where - W: ParamWidget, -{ +impl GenericUi { /// Creates a new [`GenericUi`] for all provided parameters. - pub fn new(state: &'a mut State, params: Arc) -> Self { + pub fn new(params: Arc) -> Self { + let params = params + .param_map() + .into_iter() + .map(|(_, ptr, _)| ptr) + .collect(); Self { - state, - + id: None, params, width: Length::Fill, height: Length::Fill, - max_width: u32::MAX, - max_height: u32::MAX, + max_width: u16::MAX, + max_height: u16::MAX, pad_scrollbar: false, _phantom: PhantomData, } } + /// Sets the [`Id`] of the [`Container`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Sets the width of the [`GenericUi`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); self } /// Sets the height of the [`GenericUi`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); self } /// Sets the maximum width of the [`GenericUi`]. - pub fn max_width(mut self, width: u32) -> Self { + pub fn max_width(mut self, width: u16) -> Self { self.max_width = width; self } /// Sets the maximum height of the [`GenericUi`]. - pub fn max_height(mut self, height: u32) -> Self { + pub fn max_height(mut self, height: u16) -> Self { self.max_height = height; self } @@ -130,166 +129,203 @@ where self.pad_scrollbar = true; 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( +impl<'a, W, Theme, Renderer> GenericUi +where + W: ParamWidget, + Theme: scrollable::Catalog + widget::text::Catalog + 'a, + Renderer: text::Renderer + 'a, +{ + fn content( &'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); - - // Make sure we already have widget state for each widget - let param_map = self.params.param_map(); - for (_, param_ptr, _) in ¶m_map { - let flags = unsafe { param_ptr.flags() }; - if flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) { - continue; - } - - if !widget_state.contains_key(param_ptr) { - widget_state.insert(*param_ptr, Default::default()); - } - } - - for (_, param_ptr, _) in param_map { - let flags = unsafe { param_ptr.flags() }; - if flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) { - continue; - } - - // 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 - let mut row = 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, widget_state) }); - if self.pad_scrollbar { - // There's already spacing applied, so this element doesn't actually need to hae any - // size of its own - row = row.push(Space::with_width(Length::Units(0))); - } - - scrollable = scrollable.push(row); - } - - f(scrollable, renderer) + renderer: Option<&Renderer>, + ) -> Scrollable<'a, ParamMessage, Theme, Renderer> { + let (spacing, padding) = match renderer { + Some(renderer) => ( + (renderer.default_size() * 0.2).0.round(), + (renderer.default_size() * 0.5).0.round(), + ), + None => (0.0, 0.0), + }; + + let content = Column::with_children( + self.params + .iter() + .filter(|param| is_hidden(*param)) + .map(|param| { + let row = row![ + widget::text(unsafe { param.name() }) + .height(20) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right) + .align_y(alignment::Vertical::Center), + unsafe { W::into_widget_element_raw(param) } + ] + .width(Length::Fill) + .align_y(alignment::Vertical::Center) + .spacing(spacing * 2.0); + + if self.pad_scrollbar { + row.push(Space::with_width(0)) + } else { + row + } + }) + .map(Element::from), + ) + .align_x(alignment::Horizontal::Center) + .spacing(spacing) + .padding(padding) + .width(self.width) + .height(self.height) + .max_width(self.max_width); + + scrollable(content).spacing(spacing) } } -impl<'a, W> Widget for GenericUi<'a, W> +impl<'a, W, Theme, Renderer> Widget for GenericUi where - W: ParamWidget, + W: ParamWidget, + Theme: scrollable::Catalog + widget::text::Catalog + 'a, + Renderer: text::Renderer + 'a, { - fn width(&self) -> Length { - self.width + fn size(&self) -> iced_baseview::Size { + Size { + width: self.width, + height: self.height, + } } - fn height(&self) -> Length { - self.height + fn children(&self) -> Vec { + let content = self.content(None); + + vec![Tree::new( + &content as &dyn Widget, + )] } - 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 layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content(Some(renderer)) + .layout(&mut tree.children[0], renderer, limits) } fn draw( &self, + tree: &iced_baseview::core::widget::Tree, renderer: &mut Renderer, + theme: &Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: iced_baseview::core::mouse::Cursor, 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, + self.content(Some(renderer)).draw( + &tree.children[0], renderer, - |scrollable, renderer| { - scrollable.draw(renderer, style, layout, cursor_position, viewport) - }, + theme, + style, + layout, + cursor, + viewport, ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(self.id.as_ref(), layout.bounds(), &mut |operation| { + self.content(Some(renderer)).operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ) + }); + } + fn on_event( &mut self, - event: Event, + tree: &mut iced_baseview::core::widget::Tree, + event: event::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: iced_baseview::core::mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, ParamMessage>, + viewport: &Rectangle, ) -> 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, + self.content(Some(renderer)).on_event( + &mut tree.children[0], + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: iced_baseview::core::mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> iced_baseview::core::mouse::Interaction { + self.content(Some(renderer)).mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor, + viewport, renderer, - |mut scrollable, _| { - scrollable.on_event(event, layout, cursor_position, renderer, clipboard, shell) - }, ) } } -impl ParamWidget for GenericSlider { - type State = super::param_slider::State; +fn is_hidden(param_ptr: &ParamPtr) -> bool { + let flags = unsafe { param_ptr.flags() }; + flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) +} - fn into_widget_element<'a, P: Param>( - param: &'a P, - state: &'a mut Self::State, - ) -> Element<'a, ParamMessage> { - ParamSlider::new(state, param).into() +impl ParamWidget for GenericSlider +where + Theme: widget::text_input::Catalog, + Renderer: text::Renderer, + Renderer::Font: From, +{ + fn into_widget_element<'a, P: Param>(param: &'a P) -> Element<'a, ParamMessage, Theme, Renderer> + where + Theme: 'a, + Renderer: 'a, + { + ParamSlider::new(param).into() } } -impl<'a, W: ParamWidget> GenericUi<'a, W> { +impl<'a, W, Theme, Renderer> GenericUi +where + W: ParamWidget + 'a, + Theme: scrollable::Catalog + widget::text::Catalog + 'a, + Renderer: text::Renderer + 'a, +{ /// 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> + pub fn map(self, f: F) -> Element<'a, Message, Theme, Renderer> where Message: 'static, F: Fn(ParamMessage) -> Message + 'static, @@ -298,11 +334,14 @@ impl<'a, W: ParamWidget> GenericUi<'a, W> { } } -impl<'a, W> From> for Element<'a, ParamMessage> +impl<'a, W, Theme, Renderer> From> + for Element<'a, ParamMessage, Theme, Renderer> where - W: ParamWidget, + W: ParamWidget + 'a, + Theme: scrollable::Catalog + widget::text::Catalog + 'a, + Renderer: text::Renderer + 'a, { - fn from(widget: GenericUi<'a, W>) -> Self { + fn from(widget: GenericUi) -> 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 fb941562a..a89730a64 100644 --- a/nih_plug_iced/src/widgets/param_slider.rs +++ b/nih_plug_iced/src/widgets/param_slider.rs @@ -1,20 +1,16 @@ -//! A slider that integrates with NIH-plug's [`Param`] types. - -use atomic_refcell::AtomicRefCell; use nih_plug::prelude::Param; use std::borrow::Borrow; -use crate::backend::widget; -use crate::backend::Renderer; -use crate::renderer::Renderer as GraphicsRenderer; -use crate::text::Renderer as TextRenderer; -use crate::{ - alignment, event, keyboard, layout, mouse, renderer, text, touch, Background, Clipboard, Color, - Element, Event, Font, Layout, Length, Point, Rectangle, Shell, Size, TextInput, Vector, Widget, +use crate::core::text::{Paragraph, Renderer as TextRenderer, Text}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + alignment, event, keyboard, layout, mouse, renderer, text, touch, Border, Clipboard, Color, + Element, Event, Font, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, }; +use crate::widget::text_input; +use crate::widget::text_input::{Id, TextInput}; -use super::util; -use super::ParamMessage; +use super::{util, ParamMessage}; /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the /// noramlized parameter. @@ -28,19 +24,17 @@ const BORDER_WIDTH: f32 = 1.0; /// TODO: There are currently no styling options at all /// TODO: Handle scrolling for steps (and shift+scroll for smaller steps?) pub struct ParamSlider<'a, P: Param> { - state: &'a mut State, - param: &'a P, - height: Length, width: Length, - text_size: Option, - font: Font, + height: Length, + text_size: Option, + font: Option, } /// State for a [`ParamSlider`]. -#[derive(Debug, Default)] -pub struct State { +#[derive(Debug)] +struct State { keyboard_modifiers: keyboard::Modifiers, /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a /// text value should not initiate a drag. @@ -51,14 +45,26 @@ pub struct State { /// Track clicks for double clicks. last_click: Option, - /// State for the text input overlay that will be shown when this widget is alt+clicked. - text_input_state: AtomicRefCell, /// The text that's currently in the text input. If this is set to `None`, then the text input /// is not visible. text_input_value: Option, + text_input_id: Id, } -/// An internal message for intercep- I mean handling output from the embedded [`TextInpu`] widget. +impl Default for State { + fn default() -> Self { + Self { + text_input_id: Id::unique(), + keyboard_modifiers: Default::default(), + drag_active: Default::default(), + granular_drag_start_x_value: Default::default(), + last_click: Default::default(), + text_input_value: Default::default(), + } + } +} + +/// An internal message for intercep- I mean handling output from the embedded [`TextInput`] widget. #[derive(Debug, Clone)] enum TextInputMessage { /// A new value was entered in the text input dialog. @@ -67,48 +73,19 @@ enum TextInputMessage { Submit, } -/// The default text input style with the border removed. -struct TextInputStyle; - -impl widget::text_input::StyleSheet for TextInputStyle { - fn active(&self) -> widget::text_input::Style { - widget::text_input::Style { - background: Background::Color(Color::TRANSPARENT), - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - - fn focused(&self) -> widget::text_input::Style { - self.active() - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.7, 0.7, 0.7) - } - - fn value_color(&self) -> Color { - Color::from_rgb(0.3, 0.3, 0.3) - } - - fn selection_color(&self) -> Color { - Color::from_rgb(0.8, 0.8, 1.0) - } -} - impl<'a, P: Param> ParamSlider<'a, P> { + pub const DEFAULT_WIDTH: Length = Length::Fixed(180.0); + pub const DEFAULT_HEIGHT: Length = Length::Fixed(30.0); + /// Creates a new [`ParamSlider`] for the given parameter. - pub fn new(state: &'a mut State, param: &'a P) -> Self { + pub fn new(param: &'a P) -> Self { Self { - state, - param, - width: Length::Units(180), - height: Length::Units(30), + width: Self::DEFAULT_WIDTH, + height: Self::DEFAULT_HEIGHT, text_size: None, - font: ::Font::default(), + font: None, } } @@ -125,50 +102,68 @@ impl<'a, P: Param> ParamSlider<'a, P> { } /// Sets the text size of the [`ParamSlider`]. - pub fn text_size(mut self, size: u16) -> Self { + pub fn text_size(mut self, size: Pixels) -> Self { self.text_size = Some(size); self } /// Sets the font of the [`ParamSlider`]. pub fn font(mut self, font: Font) -> Self { - self.font = font; + self.font = Some(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 + fn with_text_input( + &self, + layout: Layout, + renderer: BorrowedRenderer, + current_value: &str, + state: &State, + f: F, + ) -> T where - F: FnOnce(TextInput<'_, TextInputMessage>, Layout, R) -> T, - R: Borrow, + F: FnOnce(TextInput<'_, TextInputMessage, Theme, Renderer>, Layout, BorrowedRenderer) -> T, + Theme: text_input::Catalog, + Renderer: TextRenderer, + Renderer::Font: From, + BorrowedRenderer: Borrow, { - let mut text_input_state = self.state.text_input_state.borrow_mut(); - text_input_state.focus(); + let font = self + .font + .map(Renderer::Font::from) + .unwrap_or_else(|| renderer.borrow().default_font()); 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); + let text_width = Renderer::Paragraph::with_text(Text { + content: current_value, + bounds: layout.bounds().size(), + size: text_size, + font, + line_height: Default::default(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: Default::default(), + wrapping: Default::default(), + }) + .min_width(); + + let text_input = text_input("", current_value) + .id(state.text_input_id.clone()) + .font(font) + .size(text_size) + .width(text_width) + .on_input(TextInputMessage::Value) + .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), + height: layout.bounds().shrink(BORDER_WIDTH).size().height, }, vec![layout::Node::new(layout.bounds().size())], ); @@ -204,66 +199,246 @@ impl<'a, P: Param> ParamSlider<'a, P> { } } -impl<'a, P: Param> Widget for ParamSlider<'a, P> { - fn width(&self) -> Length { - self.width +impl<'a, P, Theme, Renderer> Widget for ParamSlider<'a, P> +where + P: Param, + Theme: text_input::Catalog, + Renderer: TextRenderer, + Renderer::Font: From, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn height(&self) -> Length { - self.height + fn state(&self) -> tree::State { + tree::State::new(State::default()) } - fn layout(&self, _renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + fn children(&self) -> Vec { + let input = text_input::("", ""); + + // One child to store text input state. + vec![Tree { + tag: input.tag(), + state: input.state(), + children: input.children(), + }] + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.width, self.height) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + + let background_color = + if cursor.is_over(bounds) || state.drag_active || state.text_input_value.is_some() { + Color::new(0.5, 0.5, 0.5, 0.1) + } else { + Color::TRANSPARENT + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + color: Color::BLACK, + width: BORDER_WIDTH, + radius: 0.0.into(), + }, + ..Default::default() + }, + background_color, + ); + + // Shrink bounds to inside of the border + let bounds = bounds.shrink(BORDER_WIDTH); + + if let Some(current_value) = &state.text_input_value { + self.with_text_input( + layout, + renderer, + current_value, + state, + |text_input, layout, renderer| { + text_input.draw( + &tree.children[0], + renderer, + theme, + layout, + cursor, + None, + viewport, + ); + }, + ); + } else { + // 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. + let current_value = self.param.modulated_normalized_value(); + let default_value = self.param.default_normalized_value(); + + let fill_start_x = util::remap_rect_x_t( + &bounds, + if self.param.step_count().is_none() && (0.45..=0.55).contains(&default_value) { + default_value + } else { + 0.0 + }, + ); - layout::Node::new(size) + 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 { + x: fill_start_x.min(fill_end_x), + width: (fill_end_x - fill_start_x).abs(), + ..bounds + }; + + renderer.fill_quad( + renderer::Quad { + bounds: fill_rect, + ..Default::default() + }, + fill_color, + ); + + // To make it more readable (and because it looks cool), the parts that overlap with the + // fill rect will be rendered in white while the rest will be rendered in black. + let display_value = self.param.to_string(); + + let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); + let font = self + .font + .map(Renderer::Font::from) + .unwrap_or_else(|| renderer.default_font()); + + let text_bounds = Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }; + renderer.fill_text( + text::Text { + content: display_value.clone(), + font: font, + size: text_size, + bounds: text_bounds.size(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::Relative(1.0), + shaping: Default::default(), + wrapping: Default::default(), + }, + text_bounds.position(), + style.text_color, + *viewport, + ); + + // This will clip to the filled area + renderer.with_layer(fill_rect, |renderer| { + let filled_text_color = Color::from_rgb8(80, 80, 80); + renderer.fill_text( + text::Text { + content: display_value, + font: font, + size: text_size, + bounds: text_bounds.size(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::Relative(1.0), + shaping: Default::default(), + wrapping: Default::default(), + }, + text_bounds.position(), + filled_text_color, + *viewport, + ); + }); + } } fn on_event( &mut self, - event: Event, + tree: &mut Tree, + event: event::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, ParamMessage>, + viewport: &Rectangle, ) -> event::Status { + let state = tree.state.downcast_mut::(); + // The pressence of a value in `self.state.text_input_value` indicates that the field should // be focussed. The field handles defocussing by itself // FIMXE: This is super hacky, I have no idea how you can reuse the text input widget // otherwise. Widgets are not supposed to handle messages from other widgets, but // we'll do so anyways by using a special `TextInputMessage` type and our own // `Shell`. - let text_input_status = if let Some(current_value) = &self.state.text_input_value { + let text_input_status = if let Some(current_value) = &state.text_input_value { let event = event.clone(); let mut messages = Vec::new(); let mut text_input_shell = Shell::new(&mut messages); + let status = self.with_text_input( layout, renderer, current_value, - |mut text_input, layout, renderer| { + &state, + |mut text_input: TextInput, layout, renderer| { text_input.on_event( + &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, &mut text_input_shell, + viewport, ) }, ); + // Check if text input is focused. + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + // Pressing escape will unfocus the text field, so we should propagate that change in // our own model - if self.state.text_input_state.borrow().is_focused() { + if text_input_state.is_focused() { for message in messages { match message { - TextInputMessage::Value(s) => self.state.text_input_value = Some(s), + TextInputMessage::Value(s) => state.text_input_value = Some(s), TextInputMessage::Submit => { - if let Some(normalized_value) = self - .state + if let Some(normalized_value) = state .text_input_value .as_ref() .and_then(|s| self.param.string_to_normalized_value(s)) @@ -274,12 +449,12 @@ impl<'a, P: Param> Widget for ParamSlider<'a, P> { } // And defocus the text input widget again - self.state.text_input_value = None; + state.text_input_value = None; } } } } else { - self.state.text_input_value = None; + state.text_input_value = None; } status @@ -290,83 +465,88 @@ impl<'a, P: Param> Widget for ParamSlider<'a, P> { return event::Status::Captured; } - // Compensate for the border when handling these events - let bounds = layout.bounds(); - let bounds = Rectangle { - x: bounds.x + BORDER_WIDTH, - y: bounds.y + BORDER_WIDTH, - width: bounds.width - (BORDER_WIDTH * 2.0), - height: bounds.height - (BORDER_WIDTH * 2.0), - }; - match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if bounds.contains(cursor_position) { - let click = mouse::Click::new(cursor_position, self.state.last_click); - self.state.last_click = Some(click); - if self.state.keyboard_modifiers.alt() { - // Alt+click should not start a drag, instead it should show the text entry - // widget - self.state.drag_active = false; - - // Changing the parameter happens in the TextInput event handler above - let mut text_input_state = self.state.text_input_state.borrow_mut(); - self.state.text_input_value = Some(self.param.to_string()); - text_input_state.move_cursor_to_end(); - text_input_state.select_all(); - } else if self.state.keyboard_modifiers.command() - || matches!(click.kind(), mouse::click::Kind::Double) - { - // Likewise resetting a parameter should not let you immediately drag it to a new value - self.state.drag_active = false; - - shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); - self.set_normalized_value(shell, self.param.default_normalized_value()); - shell.publish(ParamMessage::EndSetParameter(self.param.as_ptr())); - } else if self.state.keyboard_modifiers.shift() { - shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); - self.state.drag_active = true; - - // When holding down shift while clicking on a parameter we want to - // granuarly edit the parameter without jumping to a new value - self.state.granular_drag_start_x_value = - Some((cursor_position.x, self.param.modulated_normalized_value())); - } else { - shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); - self.state.drag_active = true; + let bounds = layout.bounds(); + + let Some(cursor_position) = cursor.position_over(bounds) else { + return event::Status::Ignored; + }; + + let click = + mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); + state.last_click = Some(click); + + if state.keyboard_modifiers.alt() { + // Alt+click should not start a drag, instead it should show the text entry + // widget + state.drag_active = false; + + let current_value = self.param.to_string(); + state.text_input_value = Some(current_value.clone()); + + let text_input_state = tree.children[0] + .state + .downcast_mut::>(); + text_input_state.select_all(); + text_input_state.move_cursor_to_end(); + text_input_state.focus(); + } else if state.keyboard_modifiers.command() + || matches!(click.kind(), mouse::click::Kind::Double) + { + // Likewise resetting a parameter should not let you immediately drag it to a new value + state.drag_active = false; - self.set_normalized_value( - shell, - util::remap_rect_x_coordinate(&bounds, cursor_position.x), - ); - self.state.granular_drag_start_x_value = None; - } + shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); + self.set_normalized_value(shell, self.param.default_normalized_value()); + shell.publish(ParamMessage::EndSetParameter(self.param.as_ptr())); + } else if state.keyboard_modifiers.shift() { + shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); + state.drag_active = true; + + // When holding down shift while clicking on a parameter we want to + // granuarly edit the parameter without jumping to a new value + state.granular_drag_start_x_value = + Some((cursor_position.x, self.param.modulated_normalized_value())); + } else { + shell.publish(ParamMessage::BeginSetParameter(self.param.as_ptr())); + state.drag_active = true; - return event::Status::Captured; + self.set_normalized_value( + shell, + util::remap_rect_x_coordinate(&bounds, cursor_position.x), + ); + state.granular_drag_start_x_value = None; } + + event::Status::Captured } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { - if self.state.drag_active { - shell.publish(ParamMessage::EndSetParameter(self.param.as_ptr())); - - self.state.drag_active = false; - - return event::Status::Captured; + if !state.drag_active { + return event::Status::Ignored; } + + shell.publish(ParamMessage::EndSetParameter(self.param.as_ptr())); + state.drag_active = false; + event::Status::Captured } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { // Don't do anything when we just reset the parameter because that would be weird - if self.state.drag_active { - // If shift is being held then the drag should be more granular instead of - // absolute - if self.state.keyboard_modifiers.shift() { - let (drag_start_x, drag_start_value) = *self - .state - .granular_drag_start_x_value - .get_or_insert_with(|| { + if !state.drag_active { + return event::Status::Ignored; + } + + let bounds = layout.bounds(); + + // If shift is being held then the drag should be more granular instead of + // absolute + if let Some(cursor_position) = cursor.position() { + if state.keyboard_modifiers.shift() { + let (drag_start_x, drag_start_value) = + *state.granular_drag_start_x_value.get_or_insert_with(|| { (cursor_position.x, self.param.modulated_normalized_value()) }); @@ -379,190 +559,86 @@ impl<'a, P: Param> Widget for ParamSlider<'a, P> { ), ); } else { - self.state.granular_drag_start_x_value = None; + state.granular_drag_start_x_value = None; self.set_normalized_value( shell, util::remap_rect_x_coordinate(&bounds, cursor_position.x), ); } - - return event::Status::Captured; } + + event::Status::Captured } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - self.state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = modifiers; + let bounds = layout.bounds(); // If this happens while dragging, snap back to reality uh I mean the current screen // position - if self.state.drag_active - && self.state.granular_drag_start_x_value.is_some() + if state.drag_active + && state.granular_drag_start_x_value.is_some() && !modifiers.shift() { - self.state.granular_drag_start_x_value = None; + state.granular_drag_start_x_value = None; - self.set_normalized_value( - shell, - util::remap_rect_x_coordinate(&bounds, cursor_position.x), - ); + if let Some(cursor_position) = cursor.position() { + self.set_normalized_value( + shell, + util::remap_rect_x_coordinate(&bounds, cursor_position.x), + ); + } } - return event::Status::Captured; + event::Status::Captured } - _ => {} + _ => event::Status::Ignored, } - - event::Status::Ignored } fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() } } - - fn draw( - &self, - renderer: &mut Renderer, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - // I'm sure there's some philosophical meaning behind this - let bounds_without_borders = Rectangle { - x: bounds.x + BORDER_WIDTH, - y: bounds.y + BORDER_WIDTH, - width: bounds.width - (BORDER_WIDTH * 2.0), - height: bounds.height - (BORDER_WIDTH * 2.0), - }; - let is_mouse_over = bounds.contains(cursor_position); - - // The bar itself, show a different background color when the value is being edited or when - // the mouse is hovering over it to indicate that it's interactive - let background_color = - if is_mouse_over || self.state.drag_active || self.state.text_input_value.is_some() { - Color::new(0.5, 0.5, 0.5, 0.1) - } else { - Color::TRANSPARENT - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: Color::BLACK, - border_width: BORDER_WIDTH, - border_radius: 0.0, - }, - background_color, - ); - - // Only draw the text input widget when it gets focussed. Otherwise, overlay the label with - // the slider. - if let Some(current_value) = &self.state.text_input_value { - self.with_text_input( - layout, - renderer, - current_value, - |text_input, layout, renderer| { - text_input.draw(renderer, layout, cursor_position, None) - }, - ) - } else { - // 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. - let current_value = self.param.modulated_normalized_value(); - let default_value = self.param.default_normalized_value(); - let fill_start_x = util::remap_rect_x_t( - &bounds_without_borders, - if self.param.step_count().is_none() && (0.45..=0.55).contains(&default_value) { - default_value - } else { - 0.0 - }, - ); - let fill_end_x = util::remap_rect_x_t(&bounds_without_borders, current_value); - - let fill_color = Color::from_rgb8(196, 196, 196); - let fill_rect = Rectangle { - x: fill_start_x.min(fill_end_x), - width: (fill_end_x - fill_start_x).abs(), - ..bounds_without_borders - }; - renderer.fill_quad( - renderer::Quad { - bounds: fill_rect, - border_color: Color::TRANSPARENT, - border_width: 0.0, - border_radius: 0.0, - }, - fill_color, - ); - - // To make it more readable (and because it looks cool), the parts that overlap with the - // fill rect will be rendered in white while the rest will be rendered in black. - let display_value = self.param.to_string(); - let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()) as f32; - let text_bounds = Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }; - renderer.fill_text(text::Text { - content: &display_value, - font: self.font, - size: text_size, - bounds: text_bounds, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - }); - - // This will clip to the filled area - renderer.with_layer(fill_rect, |renderer| { - let filled_text_color = Color::from_rgb8(80, 80, 80); - renderer.fill_text(text::Text { - content: &display_value, - font: self.font, - size: text_size, - bounds: text_bounds, - color: filled_text_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - }); - }); - } - } } -impl<'a, P: Param> ParamSlider<'a, P> { +impl<'a, P> ParamSlider<'a, P> +where + P: Param + 'a, +{ /// 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 /// messages to /// [`IcedEditor::handle_param_message()`][crate::IcedEditor::handle_param_message()]. - pub fn map(self, f: F) -> Element<'a, Message> + pub fn map(self, f: F) -> Element<'a, Message, Theme, Renderer> where Message: 'static, F: Fn(ParamMessage) -> Message + 'static, + Theme: text_input::Catalog + 'a, + Renderer: TextRenderer + 'a, + Renderer::Font: From, { Element::from(self).map(f) } } -impl<'a, P: Param> From> for Element<'a, ParamMessage> { +impl<'a, P, Theme, Renderer> From> for Element<'a, ParamMessage, Theme, Renderer> +where + P: Param + 'a, + Theme: text_input::Catalog + 'a, + Renderer: TextRenderer + 'a, + Renderer::Font: From, +{ fn from(widget: ParamSlider<'a, P>) -> Self { Element::new(widget) } diff --git a/nih_plug_iced/src/widgets/peak_meter.rs b/nih_plug_iced/src/widgets/peak_meter.rs index 688d67d76..bce0204be 100644 --- a/nih_plug_iced/src/widgets/peak_meter.rs +++ b/nih_plug_iced/src/widgets/peak_meter.rs @@ -5,12 +5,11 @@ use std::marker::PhantomData; use std::time::Duration; use std::time::Instant; -use crate::backend::Renderer; -use crate::renderer::Renderer as GraphicsRenderer; -use crate::text::Renderer as TextRenderer; -use crate::{ - alignment, layout, renderer, text, Background, Color, Element, Font, Layout, Length, Point, - Rectangle, Size, Widget, +use crate::core::text::{self, Paragraph, Renderer as TextRenderer}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + alignment, layout, mouse, padding, renderer, Background, Border, Color, Element, Font, Layout, + Length, Pixels, Point, Rectangle, Size, Widget, }; /// The thickness of this widget's borders. @@ -22,9 +21,7 @@ const TICK_WIDTH: f32 = 1.0; /// /// TODO: There are currently no styling options at all /// TODO: Vertical peak meter, this is just a proof of concept to fit the gain GUI example. -pub struct PeakMeter<'a, Message> { - state: &'a mut State, - +pub struct PeakMeter { /// The current measured value in decibel. current_value_db: f32, @@ -33,8 +30,8 @@ pub struct PeakMeter<'a, Message> { height: Length, width: Length, - text_size: Option, - font: Font, + text_size: Option, + font: Option, /// We don't emit any messages, but iced requires us to define some message type anyways. _phantom: PhantomData, @@ -42,29 +39,27 @@ pub struct PeakMeter<'a, Message> { /// State for a [`PeakMeter`]. #[derive(Debug, Default)] -pub struct State { +struct State { /// The last peak value in decibel. held_peak_value_db: AtomicCell, /// When the last peak value was hit. last_held_peak_value: AtomicCell>, } -impl<'a, Message> PeakMeter<'a, Message> { +impl PeakMeter { /// Creates a new [`PeakMeter`] using the current measurement in decibel. This measurement can /// already have some form of smoothing applied to it. This peak slider widget can draw the last /// hold value for you. - pub fn new(state: &'a mut State, value_db: f32) -> Self { + pub fn new(value_db: f32) -> Self { Self { - state, - current_value_db: value_db, hold_time: None, - width: Length::Units(180), - height: Length::Units(30), + width: Length::Fixed(180.0), + height: Length::Fixed(30.0), text_size: None, - font: ::Font::default(), + font: None, _phantom: PhantomData, } @@ -77,71 +72,72 @@ impl<'a, Message> PeakMeter<'a, Message> { } /// Sets the width of the [`PeakMeter`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); self } /// Sets the height of the [`PeakMeter`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); self } /// Sets the text size of the [`PeakMeter`]'s ticks bar. - pub fn text_size(mut self, size: u16) -> Self { - self.text_size = Some(size); + pub fn text_size(mut self, size: impl Into) -> Self { + self.text_size = Some(size.into()); self } /// Sets the font of the [`PeakMeter`]'s ticks bar. pub fn font(mut self, font: Font) -> Self { - self.font = font; + self.font = Some(font); self } } -impl<'a, Message> Widget for PeakMeter<'a, Message> +impl Widget for PeakMeter where Message: Clone, + Renderer: TextRenderer, + Renderer::Font: From, { - fn width(&self) -> Length { - self.width + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn height(&self) -> Length { - self.height + fn state(&self) -> tree::State { + tree::State::new(State::default()) } - fn layout(&self, _renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + fn size(&self) -> Size { + (self.width, self.height).into() + } - layout::Node::new(size) + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.width, self.height) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + _theme: &Theme, style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, + _cursor: mouse::Cursor, + viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let bar_bounds = Rectangle { - height: bounds.height / 2.0, - ..bounds - }; - let ticks_bounds = Rectangle { - y: bounds.y + (bounds.height / 2.0), - height: bounds.height / 2.0, - ..bounds - }; + let state = tree.state.downcast_ref::(); - let text_size = self - .text_size - .unwrap_or_else(|| (renderer.default_size() as f32 * 0.7).round() as u16); + let bounds = layout.bounds(); + let bar_bounds = bounds.shrink(padding::bottom(bounds.height / 2.0)); + let ticks_bounds = bounds.shrink(padding::top(bounds.height / 2.0)); // We'll draw a simple horizontal for [-90, 20] dB where we'll treat -80 as -infinity, with // a label containing the tick markers below it. If `.hold_time()` was called then we'll @@ -181,9 +177,12 @@ where renderer.fill_quad( renderer::Quad { bounds: tick_bounds, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + ..Default::default() }, Background::Color(tick_color), ); @@ -192,14 +191,14 @@ where // Draw the hold peak value if the hold time option has been set if let Some(hold_time) = self.hold_time { let now = Instant::now(); - let mut held_peak_value_db = self.state.held_peak_value_db.load(); - let last_peak_value = self.state.last_held_peak_value.load(); + let mut held_peak_value_db = state.held_peak_value_db.load(); + let last_peak_value = state.last_held_peak_value.load(); if self.current_value_db >= held_peak_value_db || last_peak_value.is_none() || now > last_peak_value.unwrap() + hold_time { - self.state.held_peak_value_db.store(self.current_value_db); - self.state.last_held_peak_value.store(Some(now)); + state.held_peak_value_db.store(self.current_value_db); + state.last_held_peak_value.store(Some(now)); held_peak_value_db = self.current_value_db; } @@ -211,9 +210,12 @@ where width: TICK_WIDTH, height: bar_bounds.height - (BORDER_WIDTH * 2.0), }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + ..Default::default() }, Background::Color(Color::from_rgb(0.3, 0.3, 0.3)), ); @@ -223,13 +225,24 @@ where renderer.fill_quad( renderer::Quad { bounds: bar_bounds, - border_radius: 0.0, - border_width: BORDER_WIDTH, - border_color: Color::BLACK, + border: Border { + color: Color::BLACK, + width: BORDER_WIDTH, + radius: 0.0.into(), + }, + ..Default::default() }, Background::Color(Color::TRANSPARENT), ); + let text_size = self + .text_size + .unwrap_or_else(|| Pixels((renderer.default_size().0 * 0.7).round())); + let font = self + .font + .map(Renderer::Font::from) + .unwrap_or_else(|| renderer.default_font()); + // Beneath the bar we want to draw the names of the ticks for tick_db in text_ticks { let x_coordinate = db_to_x_coord(tick_db as f32); @@ -242,9 +255,12 @@ where width: TICK_WIDTH, height: ticks_bounds.height * 0.3, }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + ..Default::default() }, Background::Color(Color::from_rgb(0.3, 0.3, 0.3)), ); @@ -254,47 +270,75 @@ where } else { tick_db.to_string() }; - renderer.fill_text(text::Text { - content: &tick_text, - font: self.font, - size: text_size as f32, - bounds: Rectangle { + renderer.fill_text( + text::Text { + content: tick_text, + font, + size: text_size, + bounds: ticks_bounds.size(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Top, + line_height: Default::default(), + shaping: Default::default(), + wrapping: text::Wrapping::None, + }, + Point { x: x_coordinate, y: ticks_bounds.y + (ticks_bounds.height * 0.35), - ..ticks_bounds }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Top, - }); + style.text_color, + *viewport, + ); } // Every proper graph needs a unit label let zero_db_x_coordinate = db_to_x_coord(0.0); - let zero_db_text_width = renderer.measure_width("0", text_size, self.font); - renderer.fill_text(text::Text { - // The spacing looks a bit off if we start with a space here so we'll add a little - // offset to the x-coordinate instead - content: "dBFS", - font: self.font, - size: text_size as f32, - bounds: Rectangle { - x: zero_db_x_coordinate + (zero_db_text_width / 2.0) + (text_size as f32 * 0.2), + + let zero_db_text_width = Renderer::Paragraph::with_text(text::Text { + content: "0", + font, + size: text_size, + bounds: ticks_bounds.size(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Top, + line_height: Default::default(), + shaping: Default::default(), + wrapping: text::Wrapping::None, + }) + .min_width(); + + renderer.fill_text( + text::Text { + // The spacing looks a bit off if we start with a space here so we'll add a little + // offset to the x-coordinate instead + content: "dBFS".into(), + font, + size: text_size, + bounds: ticks_bounds.size(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height: Default::default(), + shaping: Default::default(), + wrapping: text::Wrapping::None, + }, + Point { + x: zero_db_x_coordinate + (zero_db_text_width / 2.0) + (text_size.0 * 0.2), y: ticks_bounds.y + (ticks_bounds.height * 0.35), - ..ticks_bounds }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - }); + style.text_color, + *viewport, + ); } } -impl<'a, Message> From> for Element<'a, Message> +impl<'a, Theme, Message, Renderer> From> + for Element<'a, Message, Theme, Renderer> where - Message: 'a + Clone, + Message: Clone + 'a, + Renderer: TextRenderer + 'a, + Renderer::Font: From, { - fn from(widget: PeakMeter<'a, Message>) -> Self { + fn from(widget: PeakMeter) -> Self { Element::new(widget) } } diff --git a/nih_plug_iced/src/widgets/util.rs b/nih_plug_iced/src/widgets/util.rs index e755635b2..cd61033ee 100644 --- a/nih_plug_iced/src/widgets/util.rs +++ b/nih_plug_iced/src/widgets/util.rs @@ -1,6 +1,6 @@ //! Utilities for creating these widgets. -use crate::Rectangle; +use crate::core::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. diff --git a/nih_plug_iced/src/wrapper.rs b/nih_plug_iced/src/wrapper.rs index c149489ed..f6deb90cd 100644 --- a/nih_plug_iced/src/wrapper.rs +++ b/nih_plug_iced/src/wrapper.rs @@ -2,14 +2,15 @@ //! `nih_plug_iced`. use crossbeam::channel; +use futures_util::FutureExt; +use iced_baseview::{ + baseview::WindowScalePolicy, core::Element, futures::Subscription, window::WindowSubs, + Renderer, Task, +}; use nih_plug::prelude::GuiContext; use std::sync::Arc; -use crate::futures::FutureExt; -use crate::{ - futures, subscription, Application, Color, Command, Element, IcedEditor, ParameterUpdate, - Subscription, WindowQueue, WindowScalePolicy, WindowSubs, -}; +use crate::{IcedEditor, ParameterUpdate}; /// Wraps an `iced_baseview` [`Application`] around [`IcedEditor`]. Needed to allow editors to /// always receive a copy of the GUI context. @@ -30,6 +31,16 @@ pub enum Message { ParameterUpdate, } +impl Message { + fn into_editor_message(self) -> Option { + if let Message::EditorMessage(message) = self { + Some(message) + } else { + None + } + } +} + impl std::fmt::Debug for Message { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -48,7 +59,7 @@ impl Clone for Message { } } -impl Application for IcedEditorWrapperApplication { +impl iced_baseview::Application for IcedEditorWrapperApplication { type Executor = E::Executor; type Message = Message; type Flags = ( @@ -56,34 +67,30 @@ impl Application for IcedEditorWrapperApplication { Arc>, E::InitializationFlags, ); + type Theme = E::Theme; fn new( (context, parameter_updates_receiver, flags): Self::Flags, - ) -> (Self, Command) { - let (editor, command) = E::new(flags, context); + ) -> (Self, Task) { + let (editor, task) = E::new(flags, context); ( Self { editor, parameter_updates_receiver, }, - command.map(Message::EditorMessage), + task.map(Message::EditorMessage), ) } #[inline] - fn update( - &mut self, - window: &mut WindowQueue, - message: Self::Message, - ) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { - Message::EditorMessage(message) => self - .editor - .update(window, message) - .map(Message::EditorMessage), + Message::EditorMessage(message) => { + self.editor.update(message).map(Message::EditorMessage) + } // This message only exists to force a redraw - Message::ParameterUpdate => Command::none(), + Message::ParameterUpdate => Task::none(), } } @@ -93,15 +100,17 @@ impl Application for IcedEditorWrapperApplication { window_subs: &mut WindowSubs, ) -> Subscription { // Since we're wrapping around `E::Message`, we need to do this transformation ourselves - let mut editor_window_subs = WindowSubs { - on_frame: match &window_subs.on_frame { - Some(Message::EditorMessage(message)) => Some(message.clone()), - _ => None, - }, - on_window_will_close: match &window_subs.on_window_will_close { - Some(Message::EditorMessage(message)) => Some(message.clone()), - _ => None, - }, + let on_frame = window_subs.on_frame.clone(); + let on_window_will_close = window_subs.on_window_will_close.clone(); + let mut editor_window_subs: WindowSubs = WindowSubs { + on_frame: Some(Arc::new(move || { + let cb = on_frame.clone(); + cb.and_then(|cb| cb().and_then(|m| m.into_editor_message())) + })), + on_window_will_close: Some(Arc::new(move || { + let cb = on_window_will_close.clone(); + cb.and_then(|cb| cb().and_then(|m| m.into_editor_message())) + })), }; let subscription = Subscription::batch([ @@ -109,50 +118,58 @@ impl Application for IcedEditorWrapperApplication { // into a stream that doesn't require consuming that receiver (which wouldn't work in // this case since the subscriptions function gets called repeatedly). So we'll just use // a crossbeam queue and this unfold instead. - subscription::unfold( + Subscription::run_with_id( "parameter updates", - self.parameter_updates_receiver.clone(), - |parameter_updates_receiver| match parameter_updates_receiver.try_recv() { - Ok(_) => futures::future::ready(( - Some(Message::ParameterUpdate), - parameter_updates_receiver, - )) - .boxed(), - Err(_) => futures::future::pending().boxed(), - }, + futures_util::stream::unfold( + self.parameter_updates_receiver.clone(), + |parameter_updates_receiver| match parameter_updates_receiver.try_recv() { + Ok(_) => futures_util::future::ready(Some(( + Message::ParameterUpdate, + parameter_updates_receiver, + ))) + .boxed(), + Err(channel::TryRecvError::Empty) => { + futures_util::future::pending().boxed() + } + Err(channel::TryRecvError::Disconnected) => { + futures_util::future::ready(None).boxed() + } + }, + ), ), self.editor .subscription(&mut editor_window_subs) - .map(Message::EditorMessage), + .map(|m| Message::EditorMessage(m)), ]); - if let Some(message) = editor_window_subs.on_frame { - window_subs.on_frame = Some(Message::EditorMessage(message)); + if let Some(message) = editor_window_subs.on_frame.as_ref() { + let message = Arc::clone(message); + window_subs.on_frame = Some(Arc::new(move || message().map(Message::EditorMessage))); } - if let Some(message) = editor_window_subs.on_window_will_close { - window_subs.on_window_will_close = Some(Message::EditorMessage(message)); + if let Some(message) = editor_window_subs.on_window_will_close.as_ref() { + let message = Arc::clone(message); + window_subs.on_window_will_close = + Some(Arc::new(move || message().map(Message::EditorMessage))); } subscription } #[inline] - fn view(&mut self) -> Element<'_, Self::Message> { + fn view(&self) -> Element<'_, Self::Message, Self::Theme, Renderer> { self.editor.view().map(Message::EditorMessage) } #[inline] - fn background_color(&self) -> Color { - self.editor.background_color() + fn scale_policy(&self) -> WindowScalePolicy { + WindowScalePolicy::SystemScaleFactor } - #[inline] - fn scale_policy(&self) -> WindowScalePolicy { - self.editor.scale_policy() + fn title(&self) -> String { + self.editor.title() } - #[inline] - fn renderer_settings() -> iced_baseview::backend::settings::Settings { - E::renderer_settings() + fn theme(&self) -> Self::Theme { + self.editor.theme() } } diff --git a/plugins/examples/gain_gui_iced/Cargo.toml b/plugins/examples/gain_gui_iced/Cargo.toml index ea4f8f3b1..be23f2240 100644 --- a/plugins/examples/gain_gui_iced/Cargo.toml +++ b/plugins/examples/gain_gui_iced/Cargo.toml @@ -8,10 +8,10 @@ license = "ISC" description = "A simple gain plugin with an iced GUI" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [dependencies] nih_plug = { path = "../../../", features = ["assert_process_allocs"] } -nih_plug_iced = { path = "../../../nih_plug_iced" } +nih_plug_iced = { path = "../../../nih_plug_iced", features = ["wgpu"] } atomic_float = "0.1" diff --git a/plugins/examples/gain_gui_iced/src/editor.rs b/plugins/examples/gain_gui_iced/src/editor.rs index db2c31b73..c2921425d 100644 --- a/plugins/examples/gain_gui_iced/src/editor.rs +++ b/plugins/examples/gain_gui_iced/src/editor.rs @@ -1,5 +1,7 @@ use atomic_float::AtomicF32; use nih_plug::prelude::{util, Editor, GuiContext}; +use nih_plug_iced::assets::noto_sans_fonts_data; +use nih_plug_iced::widget::{column, text, Space}; use nih_plug_iced::widgets as nih_widgets; use nih_plug_iced::*; use std::sync::Arc; @@ -17,7 +19,11 @@ pub(crate) fn create( peak_meter: Arc, editor_state: Arc, ) -> Option> { - create_iced_editor::(editor_state, (params, peak_meter)) + create_iced_editor::( + editor_state, + (params, peak_meter), + noto_sans_fonts_data().into(), + ) } struct GainEditor { @@ -25,9 +31,6 @@ struct GainEditor { context: Arc, peak_meter: Arc, - - gain_slider_state: nih_widgets::param_slider::State, - peak_meter_state: nih_widgets::peak_meter::State, } #[derive(Debug, Clone, Copy)] @@ -40,72 +43,57 @@ impl IcedEditor for GainEditor { type Executor = executor::Default; type Message = Message; type InitializationFlags = (Arc, Arc); + type Theme = Theme; fn new( (params, peak_meter): Self::InitializationFlags, context: Arc, - ) -> (Self, Command) { + ) -> (Self, Task) { let editor = GainEditor { params, context, peak_meter, - - gain_slider_state: Default::default(), - peak_meter_state: Default::default(), }; - (editor, Command::none()) + (editor, Task::none()) } fn context(&self) -> &dyn GuiContext { self.context.as_ref() } - fn update( - &mut self, - _window: &mut WindowQueue, - message: Self::Message, - ) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::ParamUpdate(message) => self.handle_param_message(message), } - Command::none() + Task::none() } - fn view(&mut self) -> Element<'_, Self::Message> { - Column::new() - .align_items(Alignment::Center) - .push( - Text::new("Gain GUI") - .font(assets::NOTO_SANS_LIGHT) - .size(40) - .height(50.into()) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Bottom), - ) - .push( - Text::new("Gain") - .height(20.into()) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), - ) - .push( - nih_widgets::ParamSlider::new(&mut self.gain_slider_state, &self.params.gain) - .map(Message::ParamUpdate), - ) - .push(Space::with_height(10.into())) - .push( - nih_widgets::PeakMeter::new( - &mut self.peak_meter_state, - util::gain_to_db(self.peak_meter.load(std::sync::atomic::Ordering::Relaxed)), - ) - .hold_time(Duration::from_millis(600)), - ) - .into() + fn view(&self) -> Element<'_, Self::Message> { + let title = text("Gain GUI") + .font(assets::NOTO_SANS_LIGHT) + .size(40) + .height(50) + .width(Length::Fill) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Bottom); + + let text = text("Gain").height(20).width(Length::Fill).center(); + + column![ + title, + text, + nih_widgets::ParamSlider::new(&self.params.gain).map(Message::ParamUpdate), + Space::with_height(10), + nih_widgets::PeakMeter::new(util::gain_to_db( + self.peak_meter.load(std::sync::atomic::Ordering::Relaxed), + )) + .hold_time(Duration::from_millis(600)) + ] + .align_x(alignment::Horizontal::Center) + .into() } fn background_color(&self) -> nih_plug_iced::Color { diff --git a/plugins/examples/gain_gui_iced/src/lib.rs b/plugins/examples/gain_gui_iced/src/lib.rs index 15a30ca55..006c7acaa 100644 --- a/plugins/examples/gain_gui_iced/src/lib.rs +++ b/plugins/examples/gain_gui_iced/src/lib.rs @@ -9,7 +9,7 @@ mod editor; const PEAK_METER_DECAY_MS: f64 = 150.0; /// This is mostly identical to the gain example, minus some fluff, and with a GUI. -struct Gain { +pub struct Gain { params: Arc, /// Needed to normalize the peak meter's response based on the sample rate. diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index 43476cc1f..bd4ff58f7 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -342,7 +342,7 @@ impl> Wrapper { height: height as f64, }, scale: scaling_policy, - gl_config: None, + // gl_config: None, }, move |window| { let parent_handle = match window.raw_window_handle() {