Initial Air Freshener plugin implementation

This commit is contained in:
Ebu
2025-12-02 00:08:44 +01:00
commit 853b60e126
29 changed files with 3170 additions and 0 deletions

250
src/gui.rs Normal file
View File

@@ -0,0 +1,250 @@
use crate::{parameters::PluginParams, window::EditorWindow};
use baseview::{Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler};
use ebu_dsp::Rect;
use femtovg::{Canvas, Color, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::*;
use std::sync::Arc;
//const DROID_SANS_FONT: &'static [u8] = include_bytes!("../resources/fonts/DroidSans.ttf");
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 FRESH_DUMBLEDORE_BG_IMAGE: &'static [u8] =
include_bytes!("../assets/AirFreshener/bg1.png");
const FRESHENER_FRAMES: u32 = 256;
const FRESHENER_FRAMES_X: u32 = 20;
const FRESHENER_FRAMES_Y: u32 = 13;
const FRESHENER_FRAME_WIDTH: f32 = 73.0;
const FRESHENER_FRAME_HEIGHT: f32 = 144.0;
pub struct PluginGui {
// font: FontId,
params: Arc<PluginParams>,
canvas: Canvas<OpenGl>,
_gui_context: Arc<dyn GuiContext>,
scaling_factor: f32,
freshener_image: ImageId,
not_so_fresh_image: ImageId,
fresh_dumbledore_image: ImageId,
freshener_bounds: Rect<f32>,
dirty: bool,
mouse_position: (f32, f32),
drag_start_mouse_pos: (f32, f32),
drag_start_parameter_value: f32,
dragging: bool,
}
impl PluginGui {
pub fn new(
window: &mut baseview::Window<'_>,
gui_context: Arc<dyn GuiContext>,
params: Arc<PluginParams>,
scaling_factor: f32,
) -> Self {
let context = window
.gl_context()
.expect("Failed to get window OpenGL context");
unsafe {
context.make_current();
}
let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }
.expect("Failed to create femtovg renderer");
let mut canvas = Canvas::new(renderer).expect("Failed to create femtovg canvas");
let (width, height) = params.editor_state.size();
canvas.set_size(width, height, scaling_factor);
let le_fresh = canvas
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty())
.expect("Failed to load le fresh");
let not_so_fresh_image = canvas
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty())
.expect("Failed to load not so fresh");
let fresh_dumbledore_image = canvas
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty())
.expect("Failed to load fresh dumbledore");
//let font = canvas
// .add_font_mem(DROID_SANS_FONT)
// .expect("Failed to load font");
unsafe {
context.make_not_current();
}
Self {
//font,
params,
canvas,
_gui_context: gui_context,
scaling_factor,
dirty: true,
mouse_position: (0.0, 0.0),
drag_start_mouse_pos: (0.0, 0.0),
drag_start_parameter_value: 0.0,
dragging: false,
freshener_image: le_fresh,
fresh_dumbledore_image,
not_so_fresh_image,
freshener_bounds: Rect {
x: 120.0,
y: 20.0,
width: FRESHENER_FRAME_WIDTH,
height: FRESHENER_FRAME_HEIGHT,
},
}
}
}
impl WindowHandler for PluginGui {
fn on_frame(&mut self, window: &mut baseview::Window) {
const WINDOW_WIDTH: f32 = EditorWindow::WINDOW_SIZE.0 as f32;
const WINDOW_HEIGHT: f32 = EditorWindow::WINDOW_SIZE.1 as f32;
if !self.dirty {
//return;
}
let context = window
.gl_context()
.expect("Failed to get window OpenGL context");
unsafe {
context.make_current();
}
let (width, height) = (self.canvas.width(), self.canvas.height());
self.canvas.reset();
self.canvas
.clear_rect(0, 0, width, height, Color::rgbaf(0.0, 0.0, 0.0, 1.0));
let mut full_window_path = Path::new();
full_window_path.rect(
0.0,
0.0,
EditorWindow::WINDOW_SIZE.0 as f32,
EditorWindow::WINDOW_SIZE.1 as f32,
);
let mut freshener_path = Path::new();
freshener_path.rect(
self.freshener_bounds.x,
self.freshener_bounds.y,
self.freshener_bounds.width,
self.freshener_bounds.height,
);
let bbox = self.canvas.path_bbox(&freshener_path);
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();
self.canvas.fill_path(
&full_window_path,
&Paint::image(
self.not_so_fresh_image,
0.0,
0.0,
WINDOW_WIDTH,
WINDOW_HEIGHT,
0.0,
1.0,
),
);
self.canvas.fill_path(
&full_window_path,
&Paint::image(
self.fresh_dumbledore_image,
0.0,
0.0,
WINDOW_WIDTH,
WINDOW_HEIGHT,
0.0,
self.params.freshness.unmodulated_normalized_value(),
),
);
self.canvas.fill_path(
&freshener_path,
&Paint::image(
self.freshener_image,
bbox.minx - frame_x * FRESHENER_FRAME_WIDTH,
bbox.miny - frame_y * FRESHENER_FRAME_HEIGHT,
self.freshener_bounds.width * FRESHENER_FRAMES_X as f32,
self.freshener_bounds.height * FRESHENER_FRAMES_Y as f32,
0.0,
1.0,
),
);
/*
let debug = format!("{:#?}", self.params.comp_state.load());
for (i, line) in debug.split('\n').enumerate() {
self.canvas
.fill_text(
10.0,
10.0 + i as f32 * 12.5,
line,
&Paint::color(Color::rgbf(1.0, 1.0, 1.0))
.with_font(&[self.font])
.with_font_size(12.5)
.with_text_baseline(femtovg::Baseline::Top),
)
.expect("Failed to render font");
}
*/
self.canvas.flush();
context.swap_buffers();
unsafe {
context.make_not_current();
}
self.dirty = false;
}
fn on_event(
&mut self,
_window: &mut baseview::Window,
event: baseview::Event,
) -> baseview::EventStatus {
let setter = ParamSetter::new(self._gui_context.as_ref());
match event {
Event::Window(WindowEvent::Resized(size)) => {
let phys_size = size.physical_size();
self.canvas
.set_size(phys_size.width, phys_size.height, self.scaling_factor);
self.dirty = true;
}
Event::Mouse(MouseEvent::CursorMoved { position, .. }) => {
self.mouse_position = (position.x as f32, position.y as f32);
if self.dragging {
let delta = self.mouse_position.1 - self.drag_start_mouse_pos.1;
let new_value =
(self.drag_start_parameter_value - delta * 0.01).clamp(0.0, 1.0);
setter.set_parameter_normalized(&self.params.freshness, new_value);
}
self.dirty = true;
}
Event::Mouse(MouseEvent::ButtonPressed { button, .. }) => {
self.dragging = self.freshener_bounds.contains(self.mouse_position)
&& button == MouseButton::Left;
if self.dragging {
setter.begin_set_parameter(&self.params.freshness);
self.drag_start_mouse_pos = self.mouse_position;
self.drag_start_parameter_value =
self.params.freshness.unmodulated_normalized_value();
}
self.dirty = true;
}
Event::Mouse(MouseEvent::ButtonReleased { .. }) => {
if self.dragging {
setter.end_set_parameter(&self.params.freshness);
}
self.dragging = false;
self.dirty = true;
}
_ => return EventStatus::Ignored,
}
EventStatus::Captured
}
}