From 57facee3ae53a7371944a87a3fbae34bad0b740a Mon Sep 17 00:00:00 2001 From: esca111 Date: Tue, 1 Sep 2026 10:18:48 +1200 Subject: [PATCH] init --- Cargo.toml | 22 +++ src/lib.rs | 101 +++++++++++++ src/param_dial.rs | 371 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 494 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/param_dial.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6e247c8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "custom_egui_plug_widgets" +version = "0.1.0" +edition = "2024" +author = "Euphoria Audio" +crate-type = "lib" + +[dependencies] + +egui = { version = "0.36.1", features = ["default_fonts"] } +egui_extras = { version = "0.36.1", features = [ + "all_loaders", + "svg", + "svg_text", +] } +image = { version = "0.25.10", features = ["png"] } +nice-plug = { version = "0.3.0", features = [ + "standalone", + "assert_process_allocs", +] } +nice-plug-egui = { version = "0.4.0" } +rtrb = "0.4.0" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d57fcf7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,101 @@ +use egui::{Pos2, Rect, Vec2}; + +pub mod param_dial; + +pub struct ScaleableImage<'a> { + image: Option>, + scale_factor: f32, //cannot be >1 +} + +impl<'a> ScaleableImage<'a> { + pub fn new(image: Option>, scale_factor: f32) -> Self { + Self { + image, + scale_factor, + } + } + + pub fn empty() -> Self { + Self { + image: None, + scale_factor: 1.0, + } + } + + pub fn get_image_source(&self) -> egui::ImageSource<'a> { + self.image + .clone() + .expect("get_image() called on ScaleableImage without a loaded image") + } + + pub fn is_loaded(&self) -> bool { + self.image.is_some() + } + + pub fn load(&self) -> egui::Image<'a> { + egui::Image::new(self.get_image_source()) + .shrink_to_fit() + .fit_to_fraction(Vec2::new(self.scale_factor, self.scale_factor)) + } +} + +pub enum UiDirection { + UP, + DOWN, + LEFT, + RIGHT, +} + +pub struct WindowRectWithAspectRatio { + rect: Rect, + aspect_ratio: Vec2, +} + +impl WindowRectWithAspectRatio { + pub fn new(initial_rect: Rect) -> Self { + Self { + rect: initial_rect, + aspect_ratio: Vec2::new(initial_rect.width(), initial_rect.height()), + } + } + + pub fn update_rect(&mut self, window_rect: Rect) { + let ratio = self.aspect_ratio.x / self.aspect_ratio.y; + let window_ratio = window_rect.width() / window_rect.height(); + let (x, y): (f32, f32); + if ratio <= window_ratio { + y = window_rect.height(); + x = y * ratio; + //window is wider than the target aspect ratio + } else { + x = window_rect.width(); + y = x / ratio; + } + + self.rect = Rect::from_two_pos(Pos2::new(0.0, 0.0), Pos2::new(x, y)); + } + + pub fn get_window_rect(&self) -> Rect { + self.rect + } + + pub fn convert_to_pixels(&self, percent: f32, direction: UiDirection) -> f32 { + match direction { + UiDirection::UP => -percent * (self.rect.height() / 100.0), + UiDirection::DOWN => percent * (self.rect.height() / 100.0), + UiDirection::LEFT => -percent * (self.rect.width() / 100.0), + UiDirection::RIGHT => percent * (self.rect.width() / 100.0), + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[test] +// fn it_works() { +// let result = add(2, 2); +// assert_eq!(result, 4); +// } +// } diff --git a/src/param_dial.rs b/src/param_dial.rs new file mode 100644 index 0000000..d15dfa3 --- /dev/null +++ b/src/param_dial.rs @@ -0,0 +1,371 @@ +use super::ScaleableImage; +use std::sync::{Arc, LazyLock}; + +use egui::{ + Color32, Image, ImageSource, Painter, Pos2, Response, Sense, Stroke, Ui, Vec2, Widget, emath, + mutex::Mutex, vec2, +}; +use nice_plug::{context::gui::ParamSetter, nice_warn, params::Param}; + +const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015; + +static DRAG_NORMALIZED_START_VALUE_MEMORY_ID: LazyLock = + LazyLock::new(|| egui::Id::new((file!(), 0))); +static DRAG_AMOUNT_MEMORY_ID: LazyLock = LazyLock::new(|| egui::Id::new((file!(), 1))); +static IS_DRAGGING_MEMORY_ID: LazyLock = LazyLock::new(|| egui::Id::new((file!(), 2))); +static VALUE_ENTRY_MEMORY_ID: LazyLock = LazyLock::new(|| egui::Id::new((file!(), 3))); + +static DIAL_LOWER_TURN_LIMIT: f32 = 135.0; +static DIAL_UPPER_TURN_LIMIT: f32 = 405.0; + +#[derive(Clone)] +pub struct DialDrawParams { + stroke_width: f32, + background_colour: Color32, + arc_colour: Color32, + arc_background_colour: Color32, + head_colour: Color32, + outline_colour: Color32, +} + +impl DialDrawParams { + pub fn new( + stroke_width: f32, + background_colour: Color32, + arc_colour: Color32, + arc_background_colour: Color32, + head_colour: Color32, + outline_colour: Color32, + ) -> Self { + Self { + stroke_width, + background_colour, + arc_colour, + arc_background_colour, + head_colour, + outline_colour, + } + } +} + +#[must_use = "You should put this widget in a UI with `ui.add(widget);`"] +pub struct ParamDial<'a, P: Param> { + param: &'a P, + setter: &'a ParamSetter<'a>, + + draw_value: bool, + + keyboard_focus_id: Option, + + radius: f32, + + head_image: ScaleableImage<'a>, + background_image: ScaleableImage<'a>, + overlay_image: ScaleableImage<'a>, + + draw_params: Option, +} + +impl<'a, P: Param> ParamDial<'a, P> { + pub fn for_param( + param: &'a P, + setter: &'a ParamSetter<'a>, + radius: f32, + head_image: ScaleableImage<'a>, + background_image: ScaleableImage<'a>, + overlay_image: ScaleableImage<'a>, + draw_params: Option, + ) -> Self { + Self { + param, + setter, + + draw_value: true, + keyboard_focus_id: None, + head_image, + background_image, + overlay_image, + + draw_params, + radius, + } + } + + pub fn without_value(mut self) -> Self { + self.draw_value = false; + self + } + + fn plain_value(&self) -> P::Plain { + self.param.modulated_plain_value() + } + + fn normalized_value(&self) -> f32 { + self.param.modulated_normalized_value() + } + + fn string_value(&self) -> String { + self.param.to_string() + } + + fn begin_keyboard_entry(&self, ui: &Ui) { + ui.memory_mut(|mem| mem.request_focus(self.keyboard_focus_id.unwrap())); + + let value_entry_mutex = ui.memory_mut(|mem| { + mem.data + .get_temp_mut_or_default::>>(*VALUE_ENTRY_MEMORY_ID) + .clone() + }); + *value_entry_mutex.lock() = self.string_value(); + } + + fn keyboard_entry_active(&self, ui: &Ui) -> bool { + ui.memory(|mem| mem.has_focus(self.keyboard_focus_id.unwrap())) + } + + fn begin_drag(&self, ui: &Ui) { + self.setter.begin_set_parameter(self.param); + Self::set_is_dragging_memory(ui, true); + } + + fn set_normalized_value(&self, normalized: f32) { + let value = self.param.preview_plain(normalized); + if value != self.plain_value() { + self.setter.set_parameter(self.param, value); + } + } + + fn set_from_string(&self, string: &str) -> bool { + match self.param.string_to_normalized_value(string) { + Some(normalized_value) => { + self.set_normalized_value(normalized_value); + true + } + None => false, + } + } + + fn reset_param(&self) { + self.setter + .set_parameter(self.param, self.param.default_plain_value()); + } + + fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) { + let start_value = if Self::get_drag_amount_memory(ui) == 0.0 { + Self::set_drag_normalized_start_value_memory(ui, self.normalized_value()); + self.normalized_value() + } else { + Self::get_drag_normalized_start_value_memory(ui) + }; + + let total_drag_distance = -drag_delta.y + Self::get_drag_amount_memory(ui); + Self::set_drag_amount_memory(ui, total_drag_distance); + + self.set_normalized_value( + (start_value + (total_drag_distance * GRANULAR_DRAG_MULTIPLIER)).clamp(0.0, 1.0), + ); + } + + fn end_drag(&self, ui: &Ui) { + self.setter.end_set_parameter(self.param); + Self::set_is_dragging_memory(ui, false); + } + + fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 { + ui.memory(|mem| { + mem.data + .get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID) + .unwrap_or(0.5) + }) + } + + fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) { + ui.memory_mut(|mem| { + mem.data + .insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount) + }); + } + + fn get_drag_amount_memory(ui: &Ui) -> f32 { + ui.memory(|mem| mem.data.get_temp(*DRAG_AMOUNT_MEMORY_ID).unwrap_or(0.0)) + } + + fn set_drag_amount_memory(ui: &Ui, amount: f32) { + ui.memory_mut(|mem| mem.data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount)); + } + + fn get_is_dragging_memory(ui: &Ui) -> bool { + ui.memory(|mem| mem.data.get_temp(*IS_DRAGGING_MEMORY_ID).unwrap_or(false)) + } + + fn set_is_dragging_memory(ui: &Ui, is_dragging: bool) { + ui.memory_mut(|mem| mem.data.insert_temp(*IS_DRAGGING_MEMORY_ID, is_dragging)); + } + + fn dial_ui(&mut self, ui: &mut Ui, response: &mut Response) { + if response.is_pointer_button_down_on() { + let is_dragging = Self::get_is_dragging_memory(ui); + if !is_dragging { + self.begin_drag(ui); + Self::set_drag_amount_memory(ui, 0.0); + } + } + + if let Some(click_pos) = response.interact_pointer_pos() { + if ui.input(|i| i.modifiers.command) { + self.reset_param(); + response.mark_changed(); + } else if ui.input(|i| i.modifiers.shift) { + self.granular_drag(ui, response.drag_delta()); + response.mark_changed(); + } else { + let proportion = + emath::remap_clamp(click_pos.y, response.rect.y_range(), 0.0..=1.0); + self.set_normalized_value(1.0 - proportion); + response.mark_changed(); + Self::set_drag_amount_memory(ui, 0.0); + } + } + + if response.double_clicked() { + self.reset_param(); + response.mark_changed(); + } + + if response.drag_stopped() { + self.end_drag(ui); + } + + //DRAW CODE + if ui.is_rect_visible(response.rect) { + let progress = self.normalized_value(); //should be a value between 0 and 1 + let angle: f32 = + DIAL_LOWER_TURN_LIMIT + (DIAL_UPPER_TURN_LIMIT - DIAL_LOWER_TURN_LIMIT) * progress; + if self.draw_params.is_some() { + self.draw_dial( + ui.painter(), + response.rect.center(), + angle, + self.draw_params.clone().unwrap(), + ); + } + + if self.background_image.is_loaded() { + self.background_image.load().paint_at( + ui, + response + .rect + .scale_from_center(self.background_image.scale_factor), + ); + } + + if self.head_image.is_loaded() { + self.draw_image_head(ui, response.rect, angle); + } + + if self.overlay_image.is_loaded() { + self.overlay_image.load().paint_at(ui, response.rect); + } + } + } + + fn draw_dial(&self, painter: &Painter, center: Pos2, angle: f32, draw_params: DialDrawParams) { + //draw background colour + painter.circle_filled( + center, + self.radius - draw_params.stroke_width / 2.0, + draw_params.background_colour, + ); + + //draw outline + painter.circle_stroke( + center, + self.radius, + Stroke::new(draw_params.stroke_width, draw_params.outline_colour), + ); + let pointer = center + Vec2::angled(angle.to_radians()) * self.radius * 0.8; + + //draw the head + painter.circle_filled( + center, + draw_params.stroke_width / 2.0, + draw_params.head_colour, + ); + painter.line_segment( + [center, pointer], + Stroke::new(draw_params.stroke_width * 1.2, draw_params.head_colour), + ); + painter.circle_filled( + pointer, + draw_params.stroke_width / 2.0, + draw_params.head_colour, + ); + + //draw the arc background + let mut background_points = Vec::with_capacity(129); + for i in 0..128 { + let t = i as f32 / 128.0; + let pos = center + + Vec2::angled( + (DIAL_LOWER_TURN_LIMIT + (DIAL_UPPER_TURN_LIMIT - DIAL_LOWER_TURN_LIMIT) * t) + .to_radians(), + ) * self.radius; + background_points.push(pos); + } + + painter.add(egui::Shape::line( + background_points, + Stroke::new(draw_params.stroke_width, draw_params.arc_background_colour), + )); + + // draw the arc + let mut points = Vec::with_capacity(129); + for i in 0..128 { + let t = i as f32 / 128.0; + let a = DIAL_LOWER_TURN_LIMIT + (angle - DIAL_LOWER_TURN_LIMIT) * t; + let pos = center + Vec2::angled(a.to_radians()) * self.radius; + points.push(pos); + } + + painter.add(egui::Shape::line( + points, + Stroke::new(draw_params.stroke_width, draw_params.arc_colour), + )); + } + + fn draw_image_head(&self, ui: &mut Ui, rect: egui::Rect, angle: f32) { + self.head_image + .load() + .rotate((angle + 90.0).to_radians(), Vec2::splat(0.5)) + .paint_at(ui, rect.scale_from_center(self.head_image.scale_factor)); + // ui.painter().rect_filled(rect, 0.5, Color32::WHITE); // .rotate(angle, vec2(rect.center().x, rect.center().y)) + } + + fn draw_background_image(&self, ui: &mut Ui, rect: egui::Rect) { + self.background_image.load().paint_at(ui, rect); + } + + fn draw_overlay_image(&self, ui: &mut Ui, rect: egui::Rect) {} +} + +impl Widget for ParamDial<'_, P> { + fn ui(mut self, ui: &mut Ui) -> Response { + ui.vertical_centered(|ui| { + let mut response = ui + .vertical_centered(|ui| { + // ui.allocate_space(vec2(self.radius * 2.0, self.radius * 2.0)); + let response = ui.allocate_response( + vec2(self.radius * 2.0, self.radius * 2.0), + Sense::click_and_drag(), + ); + let (kb_edit_id, _) = ui.allocate_space(vec2(1.0, 1.0)); + self.keyboard_focus_id = Some(kb_edit_id); + response + }) + .inner; + self.dial_ui(ui, &mut response); + response + }) + .inner + } +}