Improve screen scaling behavior

This commit is contained in:
Ebu
2025-12-03 12:37:38 +01:00
parent d8e9e12515
commit 3cf22b7189
8 changed files with 451 additions and 199 deletions

7
Cargo.lock generated
View File

@@ -324,6 +324,8 @@ dependencies = [
name = "ebu-dsp" name = "ebu-dsp"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"femtovg",
"image",
"nih_plug", "nih_plug",
] ]
@@ -368,9 +370,9 @@ dependencies = [
[[package]] [[package]]
name = "femtovg" name = "femtovg"
version = "0.18.1" version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0530af3119be5658d8c1f7e69248d46e2c59e500dc2ef373cf25b355158ef101" checksum = "be5d925785ad33d7b0ae2b445d9f157c3ab42ff3c515fff0b46d53d4a86c43c5"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
@@ -384,6 +386,7 @@ dependencies = [
"rgb", "rgb",
"rustybuzz", "rustybuzz",
"slotmap", "slotmap",
"ttf-parser",
"unicode-bidi", "unicode-bidi",
"unicode-segmentation", "unicode-segmentation",
"wasm-bindgen", "wasm-bindgen",

View File

@@ -12,7 +12,7 @@ nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0",
parking_lot = "0.12.5" parking_lot = "0.12.5"
baseview = { git = "https://github.com/RustAudio/baseview", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3", features = ["opengl"]} baseview = { git = "https://github.com/RustAudio/baseview", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3", features = ["opengl"]}
crossbeam = "0.8.4" crossbeam = "0.8.4"
femtovg = "0.18.1" femtovg = "0.19.3"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
image = { version = "0.25.9", default-features = false, features = ["png"] } image = { version = "0.25.9", default-features = false, features = ["png"] }

View File

@@ -4,4 +4,6 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
femtovg = "0.19.3"
image = { version = "0.25.9", default-features = false, features = ["png"] }
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false }

187
ebu-dsp/src/gui/mod.rs Normal file
View File

@@ -0,0 +1,187 @@
use std::sync::{Arc, atomic::Ordering};
use femtovg::{Canvas, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::AtomicF32;
use crate::{Rect, ScaledRect};
pub struct SpriteSheet {
scale_factor: Arc<AtomicF32>,
image: Result<ImageId, String>,
width: usize,
height: usize,
frame_width: usize,
frame_height: usize,
frames_x: usize,
}
impl SpriteSheet {
pub fn empty() -> Self {
Self {
scale_factor: Arc::new(AtomicF32::new(1.0)),
image: Err("No image loaded".to_owned()),
width: 0,
height: 0,
frame_width: 0,
frame_height: 0,
frames_x: 0,
}
}
pub fn new(
canvas: &mut Canvas<OpenGl>,
data: &[u8],
scale_factor: Arc<AtomicF32>,
frame_width: usize,
frame_height: usize,
frames: usize,
) -> Self {
let image = canvas
.load_image_mem(data, ImageFlags::empty())
.map_err(|e| format!("{e:?}"));
if let Ok(image) = image {
let (width, height) = canvas.image_size(image).unwrap_or_default();
Self {
scale_factor,
image: Ok(image),
width,
height,
frame_width,
frame_height,
frames_x: width / frame_width,
}
} else {
Self {
scale_factor,
image,
width: 0,
height: 0,
frame_width,
frame_height,
frames_x: 0,
}
}
}
pub fn draw(&self, canvas: &mut Canvas<OpenGl>, x: f32, y: f32, frame: usize) {
let factor = self.scale_factor.load(Ordering::Relaxed);
let frame_x = frame % self.frames_x;
let frame_y = frame / self.frames_x;
let screen_rect = Rect {
x: x * factor,
y: y * factor,
width: self.frame_width as f32 * factor,
height: self.frame_height as f32 * factor,
};
let image_rect = Rect {
x: screen_rect.x - (frame_x * self.frame_width) as f32 * factor,
y: screen_rect.y - (frame_y * self.frame_height) as f32 * factor,
width: self.width as f32 * factor,
height: self.height as f32 * factor,
};
let mut screen_path = Path::new();
screen_path.rect(
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
);
if let Ok(image) = self.image {
canvas.fill_path(
&screen_path,
&Paint::image(
image,
image_rect.x,
image_rect.y,
image_rect.width,
image_rect.height,
0.0,
1.0,
),
);
}
}
pub fn screen_bounds(&self, x: f32, y: f32) -> ScaledRect {
ScaledRect::new_from(
self.scale_factor.clone(),
(x, y, self.frame_width as f32, self.frame_height as f32),
)
}
}
pub struct Sprite {
scale_factor: Arc<AtomicF32>,
image: Result<ImageId, String>,
width: usize,
height: usize,
}
impl Sprite {
pub fn empty() -> Self {
Self {
scale_factor: Arc::new(AtomicF32::new(1.0)),
image: Err("No image loaded".to_owned()),
width: 0,
height: 0,
}
}
pub fn new(canvas: &mut Canvas<OpenGl>, data: &[u8], scale_factor: Arc<AtomicF32>) -> Self {
let image = canvas
.load_image_mem(data, ImageFlags::empty())
.map_err(|e| format!("{e:?}"));
if let Ok(image) = image {
let (width, height) = canvas.image_size(image).unwrap_or_default();
Self {
scale_factor,
image: Ok(image),
width,
height,
}
} else {
Self {
scale_factor,
image,
width: 0,
height: 0,
}
}
}
pub fn draw(&self, canvas: &mut Canvas<OpenGl>, x: f32, y: f32, alpha: f32) {
let factor = self.scale_factor.load(Ordering::Relaxed);
let screen_rect = Rect {
x: x * factor,
y: y * factor,
width: self.width as f32 * factor,
height: self.height as f32 * factor,
};
let mut screen_path = Path::new();
screen_path.rect(
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
);
if let Ok(image) = self.image {
canvas.fill_path(
&screen_path,
&Paint::image(
image,
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
0.0,
alpha,
),
);
}
}
pub fn screen_bounds(&self, x: f32, y: f32) -> ScaledRect {
ScaledRect::new_from(
self.scale_factor.clone(),
(x, y, self.width as f32, self.height as f32),
)
}
}

View File

@@ -4,13 +4,16 @@ mod comp;
mod decibel; mod decibel;
mod eq; mod eq;
mod freq_split; mod freq_split;
mod gui;
mod meter; mod meter;
mod ring_buffer; mod ring_buffer;
mod smoother; mod smoother;
mod traits; mod traits;
use std::{ use std::{
fmt::Debug, ops::Add, time::Duration fmt::Debug,
sync::{Arc, atomic::Ordering},
time::Duration,
}; };
pub use amplitude::Amplitude; pub use amplitude::Amplitude;
@@ -19,42 +22,159 @@ pub use comp::{Compressor, CompressorState};
pub use decibel::Decibel; pub use decibel::Decibel;
pub use eq::{Equalizer, EqualizerState}; pub use eq::{Equalizer, EqualizerState};
pub use freq_split::{FreqSplitter, FreqSplitterState}; pub use freq_split::{FreqSplitter, FreqSplitterState};
pub use gui::{SpriteSheet, Sprite};
use nih_plug::prelude::AtomicF32;
pub use traits::{FloatFormatter, IntFormatter, Lerp, Processor}; pub use traits::{FloatFormatter, IntFormatter, Lerp, Processor};
pub struct Rect<T> { #[derive(Copy, Clone, Debug, Default, PartialEq)]
pub x: T, pub struct Point {
pub y: T, x: f32,
pub width: T, y: f32,
pub height: T,
} }
impl<T: Debug> Debug for Rect<T> { impl Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub const fn new(x: f32, y: f32) -> Self {
f.debug_struct("Rect") Self { x, y }
.field("x", &self.x)
.field("y", &self.y)
.field("width", &self.width)
.field("height", &self.height)
.finish()
} }
} }
impl<T: PartialOrd + Add<Output = T> + Copy> Rect<T> { impl From<(f32, f32)> for Point {
pub fn contains(&self, value: (T, T)) -> bool { fn from(value: (f32, f32)) -> Self {
value.0 >= self.x Point {
&& value.1 >= self.y x: value.0,
&& value.0 < self.x + self.width y: value.1,
&& value.1 < self.y + self.height }
} }
} }
impl<T> Default for Rect<T>
where #[derive(Clone, Debug, Default)]
T: Default, pub struct ScaledPoint {
{ pub factor: Arc<AtomicF32>,
fn default() -> Self { pub x: f32,
pub y: f32,
}
impl PartialEq for ScaledPoint {
fn eq(&self, other: &Self) -> bool {
self.factor.load(Ordering::Acquire) == other.factor.load(Ordering::Acquire)
&& self.x == other.x
&& self.y == other.y
}
}
impl ScaledPoint {
pub fn new(factor: Arc<AtomicF32>) -> Self {
Self { Self {
x: Default::default(), factor,
y: Default::default(), ..Default::default()
width: Default::default(), }
height: Default::default(), }
pub fn new_from<P>(factor: Arc<AtomicF32>, point: P) -> Self
where
P: Into<Point>,
{
let point = point.into();
Self {
factor,
x: point.x,
y: point.y,
}
}
pub fn set<P>(&mut self, point: P)
where
P: Into<Point>,
{
let point = point.into();
self.x = point.x;
self.y = point.y;
}
pub fn set_scaled<P>(&mut self, point: P)
where
P: Into<Point>,
{
let factor = self.factor.load(Ordering::Acquire);
let point = point.into();
self.x = point.x / factor;
self.y = point.y / factor;
}
pub fn as_point(&self) -> Point {
let factor = self.factor.load(Ordering::Acquire);
Point {
x: self.x * factor,
y: self.y * factor,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub fn contains<P>(&self, value: P) -> bool
where
P: Into<Point>,
{
let value = value.into();
value.x >= self.x
&& value.y >= self.y
&& value.x < self.x + self.width
&& value.y < self.y + self.height
}
}
impl From<(f32, f32, f32, f32)> for Rect {
fn from(value: (f32, f32, f32, f32)) -> Self {
Rect {
x: value.0,
y: value.1,
width: value.2,
height: value.3,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ScaledRect {
pub factor: Arc<AtomicF32>,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl PartialEq for ScaledRect {
fn eq(&self, other: &Self) -> bool {
self.factor.load(Ordering::Acquire) == other.factor.load(Ordering::Acquire)
&& self.x == other.x
&& self.y == other.y
&& self.width == other.width
&& self.height == other.height
}
}
impl ScaledRect {
pub fn new(factor: Arc<AtomicF32>) -> Self {
Self {
factor,
..Default::default()
}
}
pub fn new_from<R>(factor: Arc<AtomicF32>, rect: R) -> Self
where
R: Into<Rect>,
{
let rect = rect.into();
Self {
factor,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
}
}
pub fn as_rect(&self) -> Rect {
let factor = self.factor.load(Ordering::Acquire);
Rect {
x: self.x * factor,
y: self.y * factor,
width: self.width * factor,
height: self.height * factor,
} }
} }
} }
@@ -194,4 +314,3 @@ pub fn rms<const N: usize>(sample_buffer: &RingBuffer<f64, N>, last_rms: f64) ->
last_rms.powf(2.0) last_rms.powf(2.0)
+ (1.0 / N as f64) * (sample_buffer[0].powf(2.0) - sample_buffer[N - 1].powf(2.0)) + (1.0 / N as f64) * (sample_buffer[0].powf(2.0) - sample_buffer[N - 1].powf(2.0))
} }

View File

@@ -8,6 +8,8 @@ use crossbeam::atomic::AtomicCell;
use nih_plug::params::persist::PersistentField; use nih_plug::params::persist::PersistentField;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::window::EditorWindow;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct EditorState { pub struct EditorState {
/// The window's size in logical pixels before applying `scale_factor`. /// The window's size in logical pixels before applying `scale_factor`.
@@ -20,7 +22,10 @@ pub struct EditorState {
impl EditorState { impl EditorState {
pub fn from_size(size: (u32, u32)) -> Arc<Self> { pub fn from_size(size: (u32, u32)) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
size: AtomicCell::new(size), size: AtomicCell::new((
(size.0 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
(size.1 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
)),
open: AtomicBool::new(false), open: AtomicBool::new(false),
}) })
} }
@@ -47,7 +52,7 @@ impl<'a> PersistentField<'a, EditorState> for Arc<EditorState> {
pub struct EditorHandle { pub struct EditorHandle {
pub state: Arc<EditorState>, pub state: Arc<EditorState>,
pub window: WindowHandle pub window: WindowHandle,
} }
unsafe impl Send for EditorHandle {} unsafe impl Send for EditorHandle {}
impl Drop for EditorHandle { impl Drop for EditorHandle {
@@ -55,4 +60,4 @@ impl Drop for EditorHandle {
self.state.open.store(false, Ordering::Release); self.state.open.store(false, Ordering::Release);
self.window.close(); self.window.close();
} }
} }

View File

@@ -1,40 +1,38 @@
use crate::{parameters::PluginParams, window::EditorWindow}; use crate::parameters::PluginParams;
use baseview::{ use baseview::{
Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler, gl::GlContext, Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler, gl::GlContext,
}; };
use crossbeam::atomic::AtomicCell; use ebu_dsp::{ScaledPoint, ScaledRect, Sprite, SpriteSheet};
use ebu_dsp::Rect; use femtovg::{Canvas, Color, FontId, Paint, Path, renderer::OpenGl};
use femtovg::{Canvas, Color, FontId, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::*; use nih_plug::prelude::*;
use std::sync::Arc; use std::sync::{Arc, atomic::Ordering};
const DROID_SANS_FONT: &'static [u8] = include_bytes!("../assets/DroidSans.ttf"); const DROID_SANS_FONT: &'static [u8] = include_bytes!("../assets/DroidSans.ttf");
const FRESHENER_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/sheet.png"); const FRESHENER_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/sheet.png");
const NOT_SO_FRESH_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg0.png"); const NOT_SO_FRESH_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg0.png");
const FRESH_DUMBLEDORE_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg1.png"); const FRESH_DUMBLEDORE_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg1.png");
const FRESHENER_FRAMES: u32 = 256; const FRESHENER_FRAMES: usize = 256;
const FRESHENER_FRAMES_X: u32 = 20; const FRESHENER_FRAME_WIDTH: usize = 73;
const FRESHENER_FRAMES_Y: u32 = 13; const FRESHENER_FRAME_HEIGHT: usize = 144;
const FRESHENER_FRAME_WIDTH: f32 = 73.0; const FRESHENER_SCREEN_X: f32 = 120.0;
const FRESHENER_FRAME_HEIGHT: f32 = 144.0; const FRESHENER_SCREEN_Y: f32 = 20.0;
pub struct PluginGui { pub struct PluginGui {
font: Result<FontId, String>, font: Result<FontId, String>,
params: Arc<PluginParams>, params: Arc<PluginParams>,
canvas: Option<Canvas<OpenGl>>, canvas: Option<Canvas<OpenGl>>,
_gui_context: Arc<dyn GuiContext>, _gui_context: Arc<dyn GuiContext>,
scaling_factor: Arc<AtomicCell<Option<f32>>>, scaling_factor: Arc<AtomicF32>,
freshener_screen_bounds: Rect<f32>, freshener_image: SpriteSheet,
freshener_image_bounds: ScaledRect,
freshener_image: Result<ImageId, String>, not_so_fresh_image: Sprite,
not_so_fresh_image: Result<ImageId, String>, fresh_dumbledore_image: Sprite,
fresh_dumbledore_image: Result<ImageId, String>,
dirty: bool, dirty: bool,
mouse_position: (f32, f32), mouse_position: ScaledPoint,
drag_start_mouse_pos: (f32, f32), drag_start_mouse_pos: ScaledPoint,
drag_start_parameter_value: f32, drag_start_parameter_value: f32,
dragging: bool, dragging: bool,
} }
@@ -42,13 +40,13 @@ pub struct PluginGui {
fn create_canvas( fn create_canvas(
context: &GlContext, context: &GlContext,
params: &PluginParams, params: &PluginParams,
scaling_factor: &AtomicCell<Option<f32>>, scaling_factor: &AtomicF32,
) -> Result<Canvas<OpenGl>, &'static str> { ) -> Result<Canvas<OpenGl>, &'static str> {
let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) } let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }
.map_err(|_| "Failed to create OpenGL renderer")?; .map_err(|_| "Failed to create OpenGL renderer")?;
let mut canvas = Canvas::new(renderer).map_err(|_| "Failed to create femtovg canvas")?; let mut canvas = Canvas::new(renderer).map_err(|_| "Failed to create femtovg canvas")?;
let (width, height) = params.editor_state.size(); let (width, height) = params.editor_state.size();
canvas.set_size(width, height, scaling_factor.load().unwrap_or(1.0)); canvas.set_size(width, height, scaling_factor.load(Ordering::Relaxed));
Ok(canvas) Ok(canvas)
} }
@@ -57,7 +55,7 @@ impl PluginGui {
window: &mut baseview::Window<'_>, window: &mut baseview::Window<'_>,
gui_context: Arc<dyn GuiContext>, gui_context: Arc<dyn GuiContext>,
params: Arc<PluginParams>, params: Arc<PluginParams>,
scaling_factor: Arc<AtomicCell<Option<f32>>>, scaling_factor: Arc<AtomicF32>,
) -> Self { ) -> Self {
let mut this = Self { let mut this = Self {
font: Err("Not loaded".to_owned()), font: Err("Not loaded".to_owned()),
@@ -66,14 +64,14 @@ impl PluginGui {
_gui_context: gui_context, _gui_context: gui_context,
scaling_factor: scaling_factor.clone(), scaling_factor: scaling_factor.clone(),
dirty: true, dirty: true,
mouse_position: (0.0, 0.0), mouse_position: ScaledPoint::new(scaling_factor.clone()),
drag_start_mouse_pos: (0.0, 0.0), drag_start_mouse_pos: ScaledPoint::new(scaling_factor.clone()),
drag_start_parameter_value: 0.0, drag_start_parameter_value: 0.0,
dragging: false, dragging: false,
freshener_image: Err("Not loaded".to_owned()), freshener_image: SpriteSheet::empty(),
fresh_dumbledore_image: Err("Not loaded".to_owned()), freshener_image_bounds: ScaledRect::new(scaling_factor.clone()),
not_so_fresh_image: Err("Not loaded".to_owned()), fresh_dumbledore_image: Sprite::empty(),
freshener_screen_bounds: Rect::default(), not_so_fresh_image: Sprite::empty(),
}; };
if let Some(context) = window.gl_context() { if let Some(context) = window.gl_context() {
@@ -84,15 +82,24 @@ impl PluginGui {
this.font = canvas this.font = canvas
.add_font_mem(DROID_SANS_FONT) .add_font_mem(DROID_SANS_FONT)
.map_err(|err| format!("{:?}", err)); .map_err(|err| format!("{:?}", err));
this.freshener_image = canvas this.freshener_image = SpriteSheet::new(
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty()) &mut canvas,
.map_err(|err| format!("{:?}", err)); FRESHENER_IMAGE,
this.fresh_dumbledore_image = canvas scaling_factor.clone(),
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty()) FRESHENER_FRAME_WIDTH,
.map_err(|err| format!("{:?}", err)); FRESHENER_FRAME_HEIGHT,
this.not_so_fresh_image = canvas FRESHENER_FRAMES,
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty()) );
.map_err(|err| format!("{:?}", err)); this.freshener_image_bounds = this
.freshener_image
.screen_bounds(FRESHENER_SCREEN_X, FRESHENER_SCREEN_Y);
this.fresh_dumbledore_image = Sprite::new(
&mut canvas,
FRESH_DUMBLEDORE_BG_IMAGE,
scaling_factor.clone(),
);
this.not_so_fresh_image =
Sprite::new(&mut canvas, NOT_SO_FRESH_BG_IMAGE, scaling_factor.clone());
this.canvas = Some(canvas); this.canvas = Some(canvas);
} }
unsafe { unsafe {
@@ -115,15 +122,6 @@ impl PluginGui {
*/ */
this this
} }
fn scaling_factor(&self) -> f32 {
if let Some(factor) = self.scaling_factor.load() {
factor
} else if let Some(canvas) = &self.canvas {
canvas.width() as f32 / EditorWindow::WINDOW_SIZE.0 as f32
} else {
1.0
}
}
} }
impl WindowHandler for PluginGui { impl WindowHandler for PluginGui {
@@ -131,7 +129,7 @@ impl WindowHandler for PluginGui {
if self.canvas.is_none() { if self.canvas.is_none() {
return; return;
} }
let scaling_factor = self.scaling_factor(); let scaling_factor = self.scaling_factor.load(Ordering::Relaxed);
let canvas = self.canvas.as_mut().unwrap(); let canvas = self.canvas.as_mut().unwrap();
if !self.dirty { if !self.dirty {
@@ -139,12 +137,6 @@ impl WindowHandler for PluginGui {
} }
let font_size = 12.0 * scaling_factor; let font_size = 12.0 * scaling_factor;
self.freshener_screen_bounds = Rect {
x: 120.0 * scaling_factor,
y: 20.0 * scaling_factor,
width: FRESHENER_FRAME_WIDTH * scaling_factor,
height: FRESHENER_FRAME_HEIGHT * scaling_factor,
};
let context = match window.gl_context() { let context = match window.gl_context() {
None => { None => {
@@ -169,62 +161,11 @@ impl WindowHandler for PluginGui {
let mut full_window_path = Path::new(); let mut full_window_path = Path::new();
full_window_path.rect(0.0, 0.0, width, height); full_window_path.rect(0.0, 0.0, width, height);
let mut freshener_path = Path::new(); let freshness = self.params.freshness.unmodulated_normalized_value();
freshener_path.rect( let frame_index = (freshness * (FRESHENER_FRAMES - 1) as f32).floor() as usize;
self.freshener_screen_bounds.x, self.not_so_fresh_image.draw(canvas, 0.0, 0.0, 1.0);
self.freshener_screen_bounds.y, self.fresh_dumbledore_image.draw(canvas, 0.0, 0.0, freshness);
self.freshener_screen_bounds.width, self.freshener_image.draw(canvas, FRESHENER_SCREEN_X, FRESHENER_SCREEN_Y, frame_index);
self.freshener_screen_bounds.height,
);
let frame_index = (self.params.freshness.unmodulated_normalized_value()
* (FRESHENER_FRAMES - 1) as f32)
.floor();
let frame_x = (frame_index % FRESHENER_FRAMES_X as f32).floor();
let frame_y = (frame_index / FRESHENER_FRAMES_X as f32).floor();
let freshener_image_source_rect = Rect {
x: self.freshener_screen_bounds.x - frame_x * FRESHENER_FRAME_WIDTH,
y: self.freshener_screen_bounds.y - frame_y * FRESHENER_FRAME_HEIGHT,
width: FRESHENER_FRAME_WIDTH * FRESHENER_FRAMES_X as f32 * scaling_factor,
height: FRESHENER_FRAME_HEIGHT * FRESHENER_FRAMES_Y as f32 * scaling_factor,
};
if let Ok(not_so_fresh) = self.not_so_fresh_image {
canvas.fill_path(
&full_window_path,
&Paint::image(not_so_fresh, 0.0, 0.0, width, height, 0.0, 1.0),
);
}
if let Ok(fresh_dumbledore) = self.fresh_dumbledore_image {
canvas.fill_path(
&full_window_path,
&Paint::image(
fresh_dumbledore,
0.0,
0.0,
width,
height,
0.0,
self.params.freshness.unmodulated_normalized_value(),
),
);
}
if let Ok(freshener) = self.freshener_image {
canvas.fill_path(
&freshener_path,
&Paint::image(
freshener,
freshener_image_source_rect.x,
freshener_image_source_rect.y,
freshener_image_source_rect.width,
freshener_image_source_rect.height,
0.0,
1.0,
),
);
}
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@@ -250,21 +191,8 @@ impl WindowHandler for PluginGui {
print("Debug version"); print("Debug version");
print(&format!("scaling_factor {:?}", scaling_factor)); print(&format!("scaling_factor {:?}", scaling_factor));
print(&format!( print(&format!("mouse_pos {:?}", self.mouse_position));
"screen_bounds {:#?}", print(&format!("frame_index {:?}", frame_index));
self.freshener_screen_bounds
));
print(&format!("frame_bounds {:#?}", freshener_image_source_rect));
if let Err(e) = &self.freshener_image {
print(e);
}
if let Err(e) = &self.fresh_dumbledore_image {
print(e);
}
if let Err(e) = &self.not_so_fresh_image {
print(e);
}
} }
} }
@@ -282,23 +210,20 @@ impl WindowHandler for PluginGui {
event: baseview::Event, event: baseview::Event,
) -> baseview::EventStatus { ) -> baseview::EventStatus {
let setter = ParamSetter::new(self._gui_context.as_ref()); let setter = ParamSetter::new(self._gui_context.as_ref());
let scaling_factor = self.scaling_factor(); let scaling_factor = self.scaling_factor.load(Ordering::Acquire);
match event { match event {
Event::Window(WindowEvent::Resized(size)) => { Event::Window(WindowEvent::Resized(size)) => {
let phys_size = size.physical_size(); let phys_size = size.physical_size();
if let Some(canvas) = self.canvas.as_mut() { if let Some(canvas) = self.canvas.as_mut() {
canvas.set_size( canvas.set_size(phys_size.width, phys_size.height, scaling_factor);
phys_size.width,
phys_size.height,
self.scaling_factor.load().unwrap_or(1.0),
);
} }
self.dirty = true; self.dirty = true;
} }
Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { Event::Mouse(MouseEvent::CursorMoved { position, .. }) => {
self.mouse_position = (position.x as f32 / scaling_factor, position.y as f32 / scaling_factor); self.mouse_position
.set((position.x as f32, position.y as f32));
if self.dragging { if self.dragging {
let delta = self.mouse_position.1 - self.drag_start_mouse_pos.1; let delta = self.mouse_position.y - self.drag_start_mouse_pos.y;
let new_value = let new_value =
(self.drag_start_parameter_value - delta * 0.01).clamp(0.0, 1.0); (self.drag_start_parameter_value - delta * 0.01).clamp(0.0, 1.0);
setter.set_parameter_normalized(&self.params.freshness, new_value); setter.set_parameter_normalized(&self.params.freshness, new_value);
@@ -306,11 +231,14 @@ impl WindowHandler for PluginGui {
self.dirty = true; self.dirty = true;
} }
Event::Mouse(MouseEvent::ButtonPressed { button, .. }) => { Event::Mouse(MouseEvent::ButtonPressed { button, .. }) => {
self.dragging = self.freshener_screen_bounds.contains(self.mouse_position) self.dragging = self
.freshener_image_bounds
.as_rect()
.contains(self.mouse_position.as_point())
&& button == MouseButton::Left; && button == MouseButton::Left;
if self.dragging { if self.dragging {
setter.begin_set_parameter(&self.params.freshness); setter.begin_set_parameter(&self.params.freshness);
self.drag_start_mouse_pos = self.mouse_position; self.drag_start_mouse_pos = self.mouse_position.clone();
self.drag_start_parameter_value = self.drag_start_parameter_value =
self.params.freshness.unmodulated_normalized_value(); self.params.freshness.unmodulated_normalized_value();
} }

View File

@@ -1,23 +1,21 @@
use std::sync::{Arc, atomic::Ordering}; use crate::{AirFreshener, editor::EditorHandle, gui::PluginGui, parameters::PluginParams};
use baseview::{Window, WindowOpenOptions, WindowScalePolicy, gl::GlConfig}; use baseview::{Window, WindowOpenOptions, WindowScalePolicy, gl::GlConfig};
use crossbeam::atomic::AtomicCell; use nih_plug::prelude::AtomicF32;
use nih_plug::{editor::Editor, plugin::Plugin}; use nih_plug::{editor::Editor, plugin::Plugin};
use crate::{AirFreshener, editor::EditorHandle, parameters::PluginParams, gui::PluginGui}; use std::sync::{Arc, atomic::Ordering};
pub struct EditorWindow { pub struct EditorWindow {
params: Arc<PluginParams>, params: Arc<PluginParams>,
scaling_factor: Arc<AtomicCell<Option<f32>>>, scaling_factor: Arc<AtomicF32>,
} }
impl EditorWindow { impl EditorWindow {
pub const VIRTUAL_SCALE: f32 = 1.0;
pub const WINDOW_SIZE: (u32, u32) = (230, 320); pub const WINDOW_SIZE: (u32, u32) = (230, 320);
pub fn new(params: Arc<PluginParams>) -> Self { pub fn new(params: Arc<PluginParams>) -> Self {
Self { Self {
params, params,
#[cfg(target_os = "macos")] scaling_factor: Arc::new(AtomicF32::new(EditorWindow::VIRTUAL_SCALE)),
scaling_factor: Arc::new(AtomicCell::new(None)),
#[cfg(not(target_os = "macos"))]
scaling_factor: Arc::new(AtomicCell::new(Some(1.0))),
} }
} }
} }
@@ -29,33 +27,43 @@ impl Editor for EditorWindow {
context: Arc<dyn nih_plug::prelude::GuiContext>, context: Arc<dyn nih_plug::prelude::GuiContext>,
) -> Box<dyn std::any::Any + Send> { ) -> Box<dyn std::any::Any + Send> {
let (unscaled_width, unscaled_height) = self.params.editor_state.size(); let (unscaled_width, unscaled_height) = self.params.editor_state.size();
let scaling_factor = self.scaling_factor.load(); let scaling_factor = self.scaling_factor.load(Ordering::Acquire);
let move_scaling_factor = self.scaling_factor.clone(); let move_scaling_factor = self.scaling_factor.clone();
let gui_context = context.clone(); let gui_context = context.clone();
let params = self.params.clone(); let params = self.params.clone();
let window = Window::open_parented(&parent, WindowOpenOptions { let window = Window::open_parented(
title: AirFreshener::NAME.to_owned(), &parent,
size: baseview::Size { width: unscaled_width as f64, height: unscaled_height as f64 }, WindowOpenOptions {
scale: scaling_factor.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)).unwrap_or(WindowScalePolicy::SystemScaleFactor), title: AirFreshener::NAME.to_owned(),
gl_config: Some(GlConfig { size: baseview::Size {
version: (3, 2), width: unscaled_width as f64,
red_bits: 8, height: unscaled_height as f64,
green_bits: 8, },
blue_bits: 8, #[cfg(target_os = "macos")]
alpha_bits: 8, scale: WindowScalePolicy::SystemScaleFactor,
samples: None, #[cfg(not(target_os = "macos"))]
srgb: true, scale: WindowScalePolicy::ScaleFactor(scaling_factor as f64),
double_buffer: true, gl_config: Some(GlConfig {
vsync: false, version: (3, 2),
..Default::default() red_bits: 8,
}) green_bits: 8,
}, move |window: &mut baseview::Window<'_>| -> PluginGui { blue_bits: 8,
PluginGui::new(window, gui_context, params, move_scaling_factor) alpha_bits: 8,
}); samples: None,
srgb: true,
double_buffer: true,
vsync: false,
..Default::default()
}),
},
move |window: &mut baseview::Window<'_>| -> PluginGui {
PluginGui::new(window, gui_context, params, move_scaling_factor)
},
);
self.params.editor_state.open.store(true, Ordering::Release); self.params.editor_state.open.store(true, Ordering::Release);
Box::new(EditorHandle { Box::new(EditorHandle {
state: self.params.editor_state.clone(), state: self.params.editor_state.clone(),
window window,
}) })
} }
@@ -67,7 +75,7 @@ impl Editor for EditorWindow {
if self.params.editor_state.is_open() { if self.params.editor_state.is_open() {
return false; return false;
} }
self.scaling_factor.store(Some(factor)); self.scaling_factor.store(factor, Ordering::Release);
true true
} }