init
This commit is contained in:
22
Cargo.toml
Normal file
22
Cargo.toml
Normal 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
101
src/lib.rs
Normal 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
371
src/param_dial.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user