This commit is contained in:
esca111
2026-09-01 10:18:48 +12:00
commit 57facee3ae
3 changed files with 494 additions and 0 deletions

22
Cargo.toml Normal file
View File

@@ -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"

101
src/lib.rs Normal file
View File

@@ -0,0 +1,101 @@
use egui::{Pos2, Rect, Vec2};
pub mod param_dial;
pub struct ScaleableImage<'a> {
image: Option<egui::ImageSource<'a>>,
scale_factor: f32, //cannot be >1
}
impl<'a> ScaleableImage<'a> {
pub fn new(image: Option<egui::ImageSource<'a>>, 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);
// }
// }

371
src/param_dial.rs Normal file
View File

@@ -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<egui::Id> =
LazyLock::new(|| egui::Id::new((file!(), 0)));
static DRAG_AMOUNT_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 1)));
static IS_DRAGGING_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 2)));
static VALUE_ENTRY_MEMORY_ID: LazyLock<egui::Id> = 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<egui::Id>,
radius: f32,
head_image: ScaleableImage<'a>,
background_image: ScaleableImage<'a>,
overlay_image: ScaleableImage<'a>,
draw_params: Option<DialDrawParams>,
}
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<DialDrawParams>,
) -> 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::<Arc<Mutex<String>>>(*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<P: Param> 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
}
}