From 91a0ff745e7c86c5a25a1afaa8279a52a3d0dcdd Mon Sep 17 00:00:00 2001 From: Jakub Turowski Date: Sun, 14 Jul 2024 22:09:36 +0200 Subject: [PATCH] Introduce egui window resizability --- Cargo.lock | 37 ++++++++- nih_plug_egui/Cargo.toml | 2 +- nih_plug_egui/src/editor.rs | 35 ++++++++- nih_plug_egui/src/lib.rs | 12 +++ nih_plug_egui/src/resizable_window.rs | 80 ++++++++++++++++++++ plugins/examples/gain_gui_egui/src/lib.rs | 92 ++++++++++++----------- 6 files changed, 208 insertions(+), 50 deletions(-) create mode 100644 nih_plug_egui/src/resizable_window.rs diff --git a/Cargo.lock b/Cargo.lock index 1a22bfaf..e81e8a8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1439,7 +1439,7 @@ dependencies = [ "atomic_float", "nih_plug", "nih_plug_vizia", - "open", + "open 3.2.0", "realfft", "semver", "triple_buffer", @@ -1548,7 +1548,7 @@ dependencies = [ [[package]] name = "egui-baseview" version = "0.2.0" -source = "git+https://github.com/BillyDM/egui-baseview.git?rev=68c4d0e8e5c1c702a888a245f4ac50eddfdfcaed#68c4d0e8e5c1c702a888a245f4ac50eddfdfcaed" +source = "git+https://github.com/BillyDM/egui-baseview.git?rev=87a6cbead6cf89ca27c2f0448e480e901cc2754d#87a6cbead6cf89ca27c2f0448e480e901cc2754d" dependencies = [ "baseview 0.1.0 (git+https://github.com/RustAudio/baseview.git?rev=45465c5f46abed6c6ce370fffde5edc8e4cd5aa3)", "copypasta 0.10.1", @@ -1556,6 +1556,7 @@ dependencies = [ "egui_glow", "keyboard-types", "log", + "open 5.1.4", "raw-window-handle 0.5.2", ] @@ -2544,6 +2545,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3420,6 +3440,17 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "open" +version = "5.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "orbclient" version = "0.3.47" @@ -4506,7 +4537,7 @@ dependencies = [ "crossbeam", "nih_plug", "nih_plug_vizia", - "open", + "open 3.2.0", "realfft", "serde", "triple_buffer", diff --git a/nih_plug_egui/Cargo.toml b/nih_plug_egui/Cargo.toml index e14bd37e..0518f4b8 100644 --- a/nih_plug_egui/Cargo.toml +++ b/nih_plug_egui/Cargo.toml @@ -20,7 +20,7 @@ nih_plug = { path = "..", default-features = false } raw-window-handle = "0.5" baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" } crossbeam = "0.8" -egui-baseview = { git = "https://github.com/BillyDM/egui-baseview.git", rev = "68c4d0e8e5c1c702a888a245f4ac50eddfdfcaed", default-features = false } +egui-baseview = { git = "https://github.com/BillyDM/egui-baseview.git", rev = "87a6cbead6cf89ca27c2f0448e480e901cc2754d", default-features = false } parking_lot = "0.12" # To make the state persistable serde = { version = "1.0", features = ["derive"] } diff --git a/nih_plug_egui/src/editor.rs b/nih_plug_egui/src/editor.rs index 46eb4391..edea9eb6 100644 --- a/nih_plug_egui/src/editor.rs +++ b/nih_plug_egui/src/editor.rs @@ -1,6 +1,10 @@ //! An [`Editor`] implementation for egui. +use crate::egui::Vec2; +use crate::egui::ViewportCommand; +use crate::EguiState; use baseview::gl::GlConfig; +use baseview::PhySize; use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy}; use crossbeam::atomic::AtomicCell; use egui_baseview::egui::Context; @@ -11,8 +15,6 @@ use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; use std::sync::atomic::Ordering; use std::sync::Arc; -use crate::EguiState; - /// An [`Editor`] implementation that calls an egui draw loop. pub(crate) struct EguiEditor { pub(crate) egui_state: Arc, @@ -67,6 +69,7 @@ where let build = self.build.clone(); let update = self.update.clone(); let state = self.user_state.clone(); + let egui_state = self.egui_state.clone(); let (unscaled_width, unscaled_height) = self.egui_state.size(); let scaling_factor = self.scaling_factor.load(); @@ -100,9 +103,25 @@ where }, state, move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()), - move |egui_ctx, _queue, state| { + move |egui_ctx, queue, state| { let setter = ParamSetter::new(context.as_ref()); + // If the window was requested to resize + if let Some(new_size) = egui_state.requested_size.swap(None) { + // Ask the plugin host to resize to self.size() + if context.request_resize() { + // Resize the content of egui window + queue.resize(PhySize::new(new_size.0, new_size.1)); + egui_ctx.send_viewport_cmd(ViewportCommand::InnerSize(Vec2::new( + new_size.0 as f32, + new_size.1 as f32, + ))); + + // Update the state + egui_state.size.store(new_size); + } + } + // For now, just always redraw. Most plugin GUIs have meters, and those almost always // need a redraw. Later we can try to be a bit more sophisticated about this. Without // this we would also have a blank GUI when it gets first opened because most DAWs open @@ -119,8 +138,16 @@ where }) } + /// Size of the editor window fn size(&self) -> (u32, u32) { - self.egui_state.size() + let new_size = self.egui_state.requested_size.load(); + // This method will be used to ask the host for new size. + // If the editor is currently being resized and new size hasn't been consumed and set yet, return new requested size. + if let Some(new_size) = new_size { + new_size + } else { + self.egui_state.size() + } } fn set_scale_factor(&self, factor: f32) -> bool { diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index a98b5ef1..f268e670 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -21,6 +21,7 @@ compile_error!("There's currently no software rendering support for egui"); pub use egui_baseview::egui; mod editor; +pub mod resizable_window; pub mod widgets; /// Create an [`Editor`] instance using an [`egui`][::egui] GUI. Using the user state parameter is @@ -66,6 +67,11 @@ pub struct EguiState { /// The window's size in logical pixels before applying `scale_factor`. #[serde(with = "nih_plug::params::persist::serialize_atomic_cell")] size: AtomicCell<(u32, u32)>, + + /// The new size of the window, if it was requested to resize by the GUI. + #[serde(skip)] + requested_size: AtomicCell>, + /// Whether the editor's window is currently open. #[serde(skip)] open: AtomicBool, @@ -90,6 +96,7 @@ impl EguiState { pub fn from_size(width: u32, height: u32) -> Arc { Arc::new(EguiState { size: AtomicCell::new((width, height)), + requested_size: Default::default(), open: AtomicBool::new(false), }) } @@ -104,4 +111,9 @@ impl EguiState { pub fn is_open(&self) -> bool { self.open.load(Ordering::Acquire) } + + /// Set the new size that will be used to resize the window if the host allows. + fn set_requested_size(&self, new_size: (u32, u32)) { + self.requested_size.store(Some(new_size)); + } } diff --git a/nih_plug_egui/src/resizable_window.rs b/nih_plug_egui/src/resizable_window.rs new file mode 100644 index 00000000..d8f0ec7b --- /dev/null +++ b/nih_plug_egui/src/resizable_window.rs @@ -0,0 +1,80 @@ +//! Resizable window wrapper for Egui editor. +//! +//! + +use crate::egui::{pos2, CentralPanel, Context, Id, Rect, Response, Sense, Ui, Vec2}; +use crate::EguiState; +use egui_baseview::egui::InnerResponse; + +/// Adds a corner to the plugin window that can be dragged in order to resize it. +/// Resizing happens through plugin API, hence a custom implementation is needed. +pub struct ResizableWindow { + id: Id, + min_size: Vec2, +} + +impl ResizableWindow { + pub fn new(id_source: impl std::hash::Hash) -> Self { + Self { + id: Id::new(id_source), + min_size: Vec2::splat(16.0), + } + } + + /// Won't shrink to smaller than this + #[inline] + pub fn min_size(mut self, min_size: impl Into) -> Self { + self.min_size = min_size.into(); + self + } + + pub fn show( + self, + context: &Context, + egui_state: &EguiState, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + CentralPanel::default().show(context, move |ui| { + let ui_rect = ui.clip_rect(); + let mut content_ui = ui.child_ui(ui_rect, *ui.layout()); + + let ret = add_contents(&mut content_ui); + + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); + let corner_rect = Rect::from_min_size(ui_rect.max - corner_size, corner_size); + + let corner_response = ui.interact(corner_rect, self.id.with("corner"), Sense::drag()); + + if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + let desired_size = (pointer_pos - ui_rect.min + 0.5 * corner_response.rect.size()) + .max(self.min_size); + + if corner_response.dragged() { + egui_state.set_requested_size(( + desired_size.x.round() as u32, + desired_size.y.round() as u32, + )); + } + } + + paint_resize_corner(&content_ui, &corner_response); + + ret + }) + } +} + +pub fn paint_resize_corner(ui: &Ui, response: &Response) { + let stroke = ui.style().interact(response).fg_stroke; + + let painter = ui.painter(); + let rect = response.rect.translate(-Vec2::splat(2.0)); // move away from the corner + let cp = painter.round_pos_to_pixels(rect.max); + + let mut w = 2.0; + + while w <= rect.width() && w <= rect.height() { + painter.line_segment([pos2(cp.x - w, cp.y), pos2(cp.x, cp.y - w)], stroke); + w += 4.0; + } +} diff --git a/plugins/examples/gain_gui_egui/src/lib.rs b/plugins/examples/gain_gui_egui/src/lib.rs index 0cddb773..a89d55f6 100644 --- a/plugins/examples/gain_gui_egui/src/lib.rs +++ b/plugins/examples/gain_gui_egui/src/lib.rs @@ -1,5 +1,10 @@ use nih_plug::prelude::*; -use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState}; +use nih_plug_egui::{ + create_egui_editor, + egui::{self, Vec2}, + resizable_window::ResizableWindow, + widgets, EguiState, +}; use std::sync::Arc; /// The time it takes for the peak meter to decay by 12 dB after switching to complete silence. @@ -102,62 +107,65 @@ impl Plugin for Gain { fn editor(&mut self, _async_executor: AsyncExecutor) -> Option> { let params = self.params.clone(); let peak_meter = self.peak_meter.clone(); + let egui_state = params.editor_state.clone(); create_egui_editor( self.params.editor_state.clone(), (), |_, _| {}, move |egui_ctx, setter, _state| { - egui::CentralPanel::default().show(egui_ctx, |ui| { - // NOTE: See `plugins/diopser/src/editor.rs` for an example using the generic UI widget + ResizableWindow::new("res-wind") + .min_size(Vec2::new(128.0, 128.0)) + .show(egui_ctx, egui_state.as_ref(), |ui| { + // NOTE: See `plugins/diopser/src/editor.rs` for an example using the generic UI widget - // This is a fancy widget that can get all the information it needs to properly - // display and modify the parameter from the parametr itself - // It's not yet fully implemented, as the text is missing. - ui.label("Some random integer"); - ui.add(widgets::ParamSlider::for_param(¶ms.some_int, setter)); + // This is a fancy widget that can get all the information it needs to properly + // display and modify the parameter from the parametr itself + // It's not yet fully implemented, as the text is missing. + ui.label("Some random integer"); + ui.add(widgets::ParamSlider::for_param(¶ms.some_int, setter)); - ui.label("Gain"); - ui.add(widgets::ParamSlider::for_param(¶ms.gain, setter)); + ui.label("Gain"); + ui.add(widgets::ParamSlider::for_param(¶ms.gain, setter)); - ui.label( + ui.label( "Also gain, but with a lame widget. Can't even render the value correctly!", ); - // This is a simple naieve version of a parameter slider that's not aware of how - // the parameters work - ui.add( - egui::widgets::Slider::from_get_set(-30.0..=30.0, |new_value| { - match new_value { - Some(new_value_db) => { - let new_value = util::gain_to_db(new_value_db as f32); + // This is a simple naieve version of a parameter slider that's not aware of how + // the parameters work + ui.add( + egui::widgets::Slider::from_get_set(-30.0..=30.0, |new_value| { + match new_value { + Some(new_value_db) => { + let new_value = util::gain_to_db(new_value_db as f32); - setter.begin_set_parameter(¶ms.gain); - setter.set_parameter(¶ms.gain, new_value); - setter.end_set_parameter(¶ms.gain); + setter.begin_set_parameter(¶ms.gain); + setter.set_parameter(¶ms.gain, new_value); + setter.end_set_parameter(¶ms.gain); - new_value_db + new_value_db + } + None => util::gain_to_db(params.gain.value()) as f64, } - None => util::gain_to_db(params.gain.value()) as f64, - } - }) - .suffix(" dB"), - ); + }) + .suffix(" dB"), + ); - // TODO: Add a proper custom widget instead of reusing a progress bar - let peak_meter = - util::gain_to_db(peak_meter.load(std::sync::atomic::Ordering::Relaxed)); - let peak_meter_text = if peak_meter > util::MINUS_INFINITY_DB { - format!("{peak_meter:.1} dBFS") - } else { - String::from("-inf dBFS") - }; + // TODO: Add a proper custom widget instead of reusing a progress bar + let peak_meter = + util::gain_to_db(peak_meter.load(std::sync::atomic::Ordering::Relaxed)); + let peak_meter_text = if peak_meter > util::MINUS_INFINITY_DB { + format!("{peak_meter:.1} dBFS") + } else { + String::from("-inf dBFS") + }; - let peak_meter_normalized = (peak_meter + 60.0) / 60.0; - ui.allocate_space(egui::Vec2::splat(2.0)); - ui.add( - egui::widgets::ProgressBar::new(peak_meter_normalized) - .text(peak_meter_text), - ); - }); + let peak_meter_normalized = (peak_meter + 60.0) / 60.0; + ui.allocate_space(egui::Vec2::splat(2.0)); + ui.add( + egui::widgets::ProgressBar::new(peak_meter_normalized) + .text(peak_meter_text), + ); + }); }, ) }