mirror of
https://github.com/robbert-vdh/nih-plug.git
synced 2026-07-01 02:36:54 +00:00
Introduce egui window resizability
This commit is contained in:
committed by
Robbert van der Helm
parent
bba7064441
commit
91a0ff745e
37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -1439,7 +1439,7 @@ dependencies = [
|
|||||||
"atomic_float",
|
"atomic_float",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_vizia",
|
"nih_plug_vizia",
|
||||||
"open",
|
"open 3.2.0",
|
||||||
"realfft",
|
"realfft",
|
||||||
"semver",
|
"semver",
|
||||||
"triple_buffer",
|
"triple_buffer",
|
||||||
@@ -1548,7 +1548,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-baseview"
|
name = "egui-baseview"
|
||||||
version = "0.2.0"
|
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 = [
|
dependencies = [
|
||||||
"baseview 0.1.0 (git+https://github.com/RustAudio/baseview.git?rev=45465c5f46abed6c6ce370fffde5edc8e4cd5aa3)",
|
"baseview 0.1.0 (git+https://github.com/RustAudio/baseview.git?rev=45465c5f46abed6c6ce370fffde5edc8e4cd5aa3)",
|
||||||
"copypasta 0.10.1",
|
"copypasta 0.10.1",
|
||||||
@@ -1556,6 +1556,7 @@ dependencies = [
|
|||||||
"egui_glow",
|
"egui_glow",
|
||||||
"keyboard-types",
|
"keyboard-types",
|
||||||
"log",
|
"log",
|
||||||
|
"open 5.1.4",
|
||||||
"raw-window-handle 0.5.2",
|
"raw-window-handle 0.5.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2544,6 +2545,25 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
@@ -3420,6 +3440,17 @@ dependencies = [
|
|||||||
"windows-sys 0.42.0",
|
"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]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
@@ -4506,7 +4537,7 @@ dependencies = [
|
|||||||
"crossbeam",
|
"crossbeam",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_vizia",
|
"nih_plug_vizia",
|
||||||
"open",
|
"open 3.2.0",
|
||||||
"realfft",
|
"realfft",
|
||||||
"serde",
|
"serde",
|
||||||
"triple_buffer",
|
"triple_buffer",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ nih_plug = { path = "..", default-features = false }
|
|||||||
raw-window-handle = "0.5"
|
raw-window-handle = "0.5"
|
||||||
baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" }
|
baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" }
|
||||||
crossbeam = "0.8"
|
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"
|
parking_lot = "0.12"
|
||||||
# To make the state persistable
|
# To make the state persistable
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
//! An [`Editor`] implementation for egui.
|
//! An [`Editor`] implementation for egui.
|
||||||
|
|
||||||
|
use crate::egui::Vec2;
|
||||||
|
use crate::egui::ViewportCommand;
|
||||||
|
use crate::EguiState;
|
||||||
use baseview::gl::GlConfig;
|
use baseview::gl::GlConfig;
|
||||||
|
use baseview::PhySize;
|
||||||
use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy};
|
use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use egui_baseview::egui::Context;
|
use egui_baseview::egui::Context;
|
||||||
@@ -11,8 +15,6 @@ use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::EguiState;
|
|
||||||
|
|
||||||
/// An [`Editor`] implementation that calls an egui draw loop.
|
/// An [`Editor`] implementation that calls an egui draw loop.
|
||||||
pub(crate) struct EguiEditor<T> {
|
pub(crate) struct EguiEditor<T> {
|
||||||
pub(crate) egui_state: Arc<EguiState>,
|
pub(crate) egui_state: Arc<EguiState>,
|
||||||
@@ -67,6 +69,7 @@ where
|
|||||||
let build = self.build.clone();
|
let build = self.build.clone();
|
||||||
let update = self.update.clone();
|
let update = self.update.clone();
|
||||||
let state = self.user_state.clone();
|
let state = self.user_state.clone();
|
||||||
|
let egui_state = self.egui_state.clone();
|
||||||
|
|
||||||
let (unscaled_width, unscaled_height) = self.egui_state.size();
|
let (unscaled_width, unscaled_height) = self.egui_state.size();
|
||||||
let scaling_factor = self.scaling_factor.load();
|
let scaling_factor = self.scaling_factor.load();
|
||||||
@@ -100,9 +103,25 @@ where
|
|||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()),
|
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());
|
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
|
// 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
|
// 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
|
// this we would also have a blank GUI when it gets first opened because most DAWs open
|
||||||
@@ -119,9 +138,17 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Size of the editor window
|
||||||
fn size(&self) -> (u32, u32) {
|
fn size(&self) -> (u32, u32) {
|
||||||
|
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()
|
self.egui_state.size()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn set_scale_factor(&self, factor: f32) -> bool {
|
fn set_scale_factor(&self, factor: f32) -> bool {
|
||||||
// If the editor is currently open then the host must not change the current HiDPI scale as
|
// If the editor is currently open then the host must not change the current HiDPI scale as
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ compile_error!("There's currently no software rendering support for egui");
|
|||||||
pub use egui_baseview::egui;
|
pub use egui_baseview::egui;
|
||||||
|
|
||||||
mod editor;
|
mod editor;
|
||||||
|
pub mod resizable_window;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
/// Create an [`Editor`] instance using an [`egui`][::egui] GUI. Using the user state parameter is
|
/// 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`.
|
/// The window's size in logical pixels before applying `scale_factor`.
|
||||||
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
|
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
|
||||||
size: AtomicCell<(u32, u32)>,
|
size: AtomicCell<(u32, u32)>,
|
||||||
|
|
||||||
|
/// The new size of the window, if it was requested to resize by the GUI.
|
||||||
|
#[serde(skip)]
|
||||||
|
requested_size: AtomicCell<Option<(u32, u32)>>,
|
||||||
|
|
||||||
/// Whether the editor's window is currently open.
|
/// Whether the editor's window is currently open.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
open: AtomicBool,
|
open: AtomicBool,
|
||||||
@@ -90,6 +96,7 @@ impl EguiState {
|
|||||||
pub fn from_size(width: u32, height: u32) -> Arc<EguiState> {
|
pub fn from_size(width: u32, height: u32) -> Arc<EguiState> {
|
||||||
Arc::new(EguiState {
|
Arc::new(EguiState {
|
||||||
size: AtomicCell::new((width, height)),
|
size: AtomicCell::new((width, height)),
|
||||||
|
requested_size: Default::default(),
|
||||||
open: AtomicBool::new(false),
|
open: AtomicBool::new(false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -104,4 +111,9 @@ impl EguiState {
|
|||||||
pub fn is_open(&self) -> bool {
|
pub fn is_open(&self) -> bool {
|
||||||
self.open.load(Ordering::Acquire)
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
nih_plug_egui/src/resizable_window.rs
Normal file
80
nih_plug_egui/src/resizable_window.rs
Normal file
@@ -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<Vec2>) -> Self {
|
||||||
|
self.min_size = min_size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show<R>(
|
||||||
|
self,
|
||||||
|
context: &Context,
|
||||||
|
egui_state: &EguiState,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> InnerResponse<R> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
use nih_plug::prelude::*;
|
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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// The time it takes for the peak meter to decay by 12 dB after switching to complete silence.
|
/// The time it takes for the peak meter to decay by 12 dB after switching to complete silence.
|
||||||
@@ -102,12 +107,15 @@ impl Plugin for Gain {
|
|||||||
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||||
let params = self.params.clone();
|
let params = self.params.clone();
|
||||||
let peak_meter = self.peak_meter.clone();
|
let peak_meter = self.peak_meter.clone();
|
||||||
|
let egui_state = params.editor_state.clone();
|
||||||
create_egui_editor(
|
create_egui_editor(
|
||||||
self.params.editor_state.clone(),
|
self.params.editor_state.clone(),
|
||||||
(),
|
(),
|
||||||
|_, _| {},
|
|_, _| {},
|
||||||
move |egui_ctx, setter, _state| {
|
move |egui_ctx, setter, _state| {
|
||||||
egui::CentralPanel::default().show(egui_ctx, |ui| {
|
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
|
// 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
|
// This is a fancy widget that can get all the information it needs to properly
|
||||||
|
|||||||
Reference in New Issue
Block a user