diff --git a/nih_plug_vizia/assets/widgets.css b/nih_plug_vizia/assets/widgets.css index 6499e994..42c0c1df 100644 --- a/nih_plug_vizia/assets/widgets.css +++ b/nih_plug_vizia/assets/widgets.css @@ -87,3 +87,21 @@ peak-meter .ticks__label { top: 4px; /* In pixels in an attempt to get this to better align to the grid */ font-size: 11; /* 14.667px */ } + +resize-handle { + bottom: 0; + color: #696969; + height: 20px; + left: 1s; + opacity: 0.5; + position-type: self-directed; + right: 0; + top: 1s; + transition: opacity 0.1 0; + width: 20px; + z-index: 1337; +} +resize-handle:hover { + opacity: 0.8; + transition: opacity 0.1 0; +} diff --git a/nih_plug_vizia/src/widgets.rs b/nih_plug_vizia/src/widgets.rs index 213b3ef6..48bf5411 100644 --- a/nih_plug_vizia/src/widgets.rs +++ b/nih_plug_vizia/src/widgets.rs @@ -8,18 +8,20 @@ use nih_plug::prelude::{GuiContext, Param, ParamPtr}; use std::sync::Arc; -use vizia::{Context, Model, WindowEvent}; +use vizia::{Context, Lens, Model, WindowEvent}; use super::ViziaState; mod generic_ui; mod param_slider; mod peak_meter; +mod resize_handle; pub mod util; pub use generic_ui::GenericUi; pub use param_slider::{ParamSlider, ParamSliderExt, ParamSliderStyle}; pub use peak_meter::PeakMeter; +pub use resize_handle::ResizeHandle; /// Register the default theme for the widgets exported by this module. This is automatically called /// for you when using [`create_vizia_editor()`][super::create_vizia_editor()]. @@ -67,6 +69,7 @@ pub(crate) struct ParamModel { /// Handles interactions through `WindowEvent` for VIZIA GUIs by updating the `ViziaState`. /// Registered in [`ViziaEditor::spawn()`][super::ViziaEditor::spawn()]. +#[derive(Lens)] pub(crate) struct WindowModel { pub context: Arc, pub vizia_state: Arc, diff --git a/nih_plug_vizia/src/widgets/generic_ui.rs b/nih_plug_vizia/src/widgets/generic_ui.rs index 6b2c8f7a..ebc6a15c 100644 --- a/nih_plug_vizia/src/widgets/generic_ui.rs +++ b/nih_plug_vizia/src/widgets/generic_ui.rs @@ -74,7 +74,7 @@ impl GenericUi { cx: &mut Context, params: L, mut make_widget: impl FnMut(&mut Context, ParamPtr), - ) -> Handle<'_, GenericUi> + ) -> Handle where L: Lens> + Copy, PsPtr: 'static + Deref, diff --git a/nih_plug_vizia/src/widgets/param_slider.rs b/nih_plug_vizia/src/widgets/param_slider.rs index 3e6187fc..4051f605 100644 --- a/nih_plug_vizia/src/widgets/param_slider.rs +++ b/nih_plug_vizia/src/widgets/param_slider.rs @@ -100,11 +100,7 @@ impl ParamSlider { /// the VIZIA wrapper. /// /// See [`ParamSliderExt`] for additonal options. - pub fn new( - cx: &mut Context, - params: L, - params_to_param: F, - ) -> Handle<'_, ParamSlider> + pub fn new(cx: &mut Context, params: L, params_to_param: F) -> Handle where L: Lens + Copy, F: 'static + Fn(&Params) -> &P + Copy, diff --git a/nih_plug_vizia/src/widgets/peak_meter.rs b/nih_plug_vizia/src/widgets/peak_meter.rs index 78b02d95..db8b2059 100644 --- a/nih_plug_vizia/src/widgets/peak_meter.rs +++ b/nih_plug_vizia/src/widgets/peak_meter.rs @@ -39,11 +39,7 @@ where impl PeakMeter { /// Creates a new [`PeakMeter`] for the given value in decibel, optionally holding the peak /// value for a certain amount of time. - pub fn new( - cx: &mut Context, - level_dbfs: L, - hold_time: Option, - ) -> Handle<'_, PeakMeter> + pub fn new(cx: &mut Context, level_dbfs: L, hold_time: Option) -> Handle where L: Lens, { diff --git a/nih_plug_vizia/src/widgets/resize_handle.rs b/nih_plug_vizia/src/widgets/resize_handle.rs new file mode 100644 index 00000000..629b3150 --- /dev/null +++ b/nih_plug_vizia/src/widgets/resize_handle.rs @@ -0,0 +1,193 @@ +//! A resize handle for uniformly scaling a plugin GUI. + +use femtovg::{Paint, Path}; +use vizia::*; + +use super::WindowModel; + +/// A resize handle placed at the bottom right of the window that lets you resize the window. +pub struct ResizeHandle { + /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a + /// text value should not initiate a drag. + drag_active: bool, + + /// The scale factor when we started dragging. This is kept track of separately to avoid + /// accumulating rounding errors. + start_scale_factor: f64, + /// The cursor position in physical screen pixels when the drag started. + start_physical_coordinates: (f32, f32), +} + +impl ResizeHandle { + /// Create a resize handle at the bottom right of the window. This should be created at the top + /// level. Dragging this handle around will cause the window to be resized. + pub fn new(cx: &mut Context) -> Handle { + // Styling is done in the style sheet + ResizeHandle { + drag_active: false, + start_scale_factor: 1.0, + start_physical_coordinates: (0.0, 0.0), + } + .build(cx) + } +} + +impl View for ResizeHandle { + fn element(&self) -> Option { + Some(String::from("resize-handle")) + } + + fn event(&mut self, cx: &mut Context, event: &mut Event) { + if let Some(window_event) = event.message.downcast() { + match *window_event { + WindowEvent::MouseDown(MouseButton::Left) => { + cx.capture(); + cx.current.set_active(cx, true); + + let vizia_state = WindowModel::vizia_state.get(cx); + self.drag_active = true; + self.start_scale_factor = vizia_state.user_scale_factor(); + self.start_physical_coordinates = ( + cx.mouse.cursorx * cx.style.dpi_factor as f32, + cx.mouse.cursory * cx.style.dpi_factor as f32, + ); + } + WindowEvent::MouseUp(MouseButton::Left) => { + if self.drag_active { + cx.release(); + cx.current.set_active(cx, false); + + self.drag_active = false; + } + } + WindowEvent::MouseMove(x, y) => { + // TODO: Filter the hover color and dragging to the actual triangle + if self.drag_active { + let vizia_state = WindowModel::vizia_state.get(cx); + + // We need to convert our measurements into physical pixels relative to the + // initial drag to be able to keep a consistent ratio. This 'relative to the + // start' bit is important because otherwise we would be comparing the + // position to the same absoltue screen spotion. + // TODO: This may start doing fun things when the window grows so large that + // it gets pushed upwards or leftwards + let (compensated_physical_x, compensated_physical_y) = ( + x * self.start_scale_factor as f32, + y * self.start_scale_factor as f32, + ); + let (start_physical_x, start_physical_y) = self.start_physical_coordinates; + let new_scale_factor = self.start_scale_factor + * (compensated_physical_x / start_physical_x) + .max(compensated_physical_y / start_physical_y) + as f64; + if new_scale_factor != vizia_state.user_scale_factor() { + cx.emit(WindowEvent::SetScale(new_scale_factor)); + } + } + } + _ => {} + } + } + } + + fn draw(&self, cx: &mut Context, canvas: &mut Canvas) { + // We'll draw the handle directly as styling elements for this is going to be a bit tricky + + // These basics are taken directly from the default implementation of this function + let entity = cx.current; + let bounds = cx.cache.get_bounds(entity); + if bounds.w == 0.0 || bounds.h == 0.0 { + return; + } + + let background_color = cx + .style + .background_color + .get(entity) + .cloned() + .unwrap_or_default(); + let border_color = cx + .style + .border_color + .get(entity) + .cloned() + .unwrap_or_default(); + let opacity = cx.cache.get_opacity(entity); + let mut background_color: femtovg::Color = background_color.into(); + background_color.set_alphaf(background_color.a * opacity); + let mut border_color: femtovg::Color = border_color.into(); + border_color.set_alphaf(border_color.a * opacity); + + let border_width = match cx + .style + .border_width + .get(entity) + .cloned() + .unwrap_or_default() + { + Units::Pixels(val) => val, + Units::Percentage(val) => bounds.w.min(bounds.h) * (val / 100.0), + _ => 0.0, + }; + + let mut path = Path::new(); + let x = bounds.x + border_width / 2.0; + let y = bounds.y + border_width / 2.0; + let w = bounds.w - border_width; + let h = bounds.h - border_width; + path.move_to(x, y); + path.line_to(x, y + h); + path.line_to(x + w, y + h); + path.line_to(x + w, y); + path.line_to(x, y); + path.close(); + + // Fill with background color + let paint = Paint::color(background_color); + canvas.fill_path(&mut path, paint); + + // Borders are only supported to make debugging easier + let mut paint = Paint::color(border_color); + paint.set_line_width(border_width); + canvas.stroke_path(&mut path, paint); + + // We'll draw a simple triangle, since we're going flat everywhere anyways and that style + // tends to not look too tacky + let mut path = Path::new(); + let x = bounds.x + border_width / 2.0; + let y = bounds.y + border_width / 2.0; + let w = bounds.w - border_width; + let h = bounds.h - border_width; + path.move_to(x, y + h); + path.line_to(x + w, y + h); + path.line_to(x + w, y); + path.move_to(x, y + h); + path.close(); + + // Yeah this looks nowhere as good + // path.move_to(x, y + h); + // path.line_to(x + (w / 3.0), y + h); + // path.line_to(x + w, y + h / 3.0); + // path.line_to(x + w, y); + // path.move_to(x, y + h); + // path.close(); + + // path.move_to(x + (w / 3.0 * 1.5), y + h); + // path.line_to(x + (w / 3.0 * 2.5), y + h); + // path.line_to(x + w, y + (h / 3.0 * 2.5)); + // path.line_to(x + w, y + (h / 3.0 * 1.5)); + // path.move_to(x + (w / 3.0 * 1.5), y + h); + // path.close(); + + let mut color: femtovg::Color = cx + .style + .font_color + .get(entity) + .cloned() + .unwrap_or(crate::Color::white()) + .into(); + color.set_alphaf(color.a * opacity); + let paint = Paint::color(color); + canvas.fill_path(&mut path, paint); + } +}