Files
ebu-plugs/src/gui.rs
2025-12-02 14:11:40 +01:00

322 lines
12 KiB
Rust

use crate::{parameters::PluginParams, window::EditorWindow};
use baseview::{
Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler, gl::GlContext,
};
use crossbeam::atomic::AtomicCell;
use ebu_dsp::Rect;
use femtovg::{Canvas, Color, FontId, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::*;
use std::sync::Arc;
const DROID_SANS_FONT: &'static [u8] = include_bytes!("../assets/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: Result<FontId, String>,
params: Arc<PluginParams>,
canvas: Option<Canvas<OpenGl>>,
_gui_context: Arc<dyn GuiContext>,
scaling_factor: Arc<AtomicCell<Option<f32>>>,
freshener_screen_bounds: Rect<f32>,
freshener_image: Result<ImageId, String>,
not_so_fresh_image: Result<ImageId, String>,
fresh_dumbledore_image: Result<ImageId, String>,
dirty: bool,
mouse_position: (f32, f32),
drag_start_mouse_pos: (f32, f32),
drag_start_parameter_value: f32,
dragging: bool,
}
fn create_canvas(
context: &GlContext,
params: &PluginParams,
scaling_factor: &AtomicCell<Option<f32>>,
) -> Result<Canvas<OpenGl>, &'static str> {
let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }
.map_err(|_| "Failed to create OpenGL renderer")?;
let mut canvas = Canvas::new(renderer).map_err(|_| "Failed to create femtovg canvas")?;
let (width, height) = params.editor_state.size();
canvas.set_size(width, height, scaling_factor.load().unwrap_or(1.0));
Ok(canvas)
}
impl PluginGui {
pub fn new(
window: &mut baseview::Window<'_>,
gui_context: Arc<dyn GuiContext>,
params: Arc<PluginParams>,
scaling_factor: Arc<AtomicCell<Option<f32>>>,
) -> Self {
let mut this = Self {
font: Err("Not loaded".to_owned()),
params: params.clone(),
canvas: None,
_gui_context: gui_context,
scaling_factor: scaling_factor.clone(),
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: Err("Not loaded".to_owned()),
fresh_dumbledore_image: Err("Not loaded".to_owned()),
not_so_fresh_image: Err("Not loaded".to_owned()),
freshener_screen_bounds: Rect::default(),
};
if let Some(context) = window.gl_context() {
unsafe {
context.make_current();
}
if let Ok(mut canvas) = create_canvas(context, &params, &scaling_factor) {
this.font = canvas
.add_font_mem(DROID_SANS_FONT)
.map_err(|err| format!("{:?}", err));
this.freshener_image = canvas
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty())
.map_err(|err| format!("{:?}", err));
this.fresh_dumbledore_image = canvas
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty())
.map_err(|err| format!("{:?}", err));
this.not_so_fresh_image = canvas
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty())
.map_err(|err| format!("{:?}", err));
this.canvas = Some(canvas);
}
unsafe {
context.make_not_current();
}
};
/*
if let Some(canvas) = canvas {
this.freshener_image = canvas
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load le fresh")?;
let not_so_fresh_image = canvas
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load not so fresh")?;
let fresh_dumbledore_image = canvas
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load fresh dumbledore")?;
}
*/
this
}
}
impl WindowHandler for PluginGui {
fn on_frame(&mut self, window: &mut baseview::Window) {
if self.canvas.is_none() {
return;
}
let canvas = self.canvas.as_mut().unwrap();
let scaling_factor = self.scaling_factor.load().unwrap_or(1.0);
if !self.dirty {
//return;
}
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() {
None => {
nih_error!("No OpenGL context");
return;
}
Some(ctx) => ctx,
};
unsafe {
context.make_current();
}
let (width, height) = (canvas.width() as f32, canvas.height() as f32);
canvas.reset();
canvas.clear_rect(
0,
0,
width as u32,
height as u32,
Color::rgbaf(0.5, 0.5, 0.6, 1.0),
);
let mut full_window_path = Path::new();
full_window_path.rect(0.0, 0.0, width, height);
let mut freshener_path = Path::new();
freshener_path.rect(
self.freshener_screen_bounds.x,
self.freshener_screen_bounds.y,
self.freshener_screen_bounds.width,
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,
height: FRESHENER_FRAME_HEIGHT * FRESHENER_FRAMES_Y as f32,
};
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)]
{
use femtovg::Baseline;
if let Ok(font) = self.font {
let mut y = 5.0;
let mut print = |str: &str| {
for s in str.lines() {
canvas
.fill_text(
5.0,
y,
s,
&Paint::color(Color::white())
.with_font(&[font])
.with_font_size(font_size)
.with_text_baseline(Baseline::Top),
)
.ok();
y += font_size;
}
};
let scaling_factor = self.scaling_factor.load();
print("Debug version");
print(&format!("scaling_factor {:?}", scaling_factor));
print(&format!(
"screen_bounds {:#?}",
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);
}
}
}
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();
if let Some(canvas) = self.canvas.as_mut() {
canvas.set_size(
phys_size.width,
phys_size.height,
self.scaling_factor.load().unwrap_or(1.0),
);
}
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_screen_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
}
}