2022-02-27 16:49:18 +01:00
|
|
|
use egui::{vec2, Response, Sense, Stroke, TextStyle, Ui, Vec2, Widget, WidgetText};
|
2022-02-09 12:24:01 +01:00
|
|
|
use lazy_static::lazy_static;
|
2022-02-09 01:07:57 +01:00
|
|
|
|
2022-02-09 11:32:19 +01:00
|
|
|
use super::util;
|
2022-02-09 11:13:51 +01:00
|
|
|
use nih_plug::{Param, ParamSetter};
|
2022-02-09 01:07:57 +01:00
|
|
|
|
2022-02-09 12:24:01 +01:00
|
|
|
/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the
|
|
|
|
/// noramlized parameter.
|
|
|
|
const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015;
|
|
|
|
|
|
|
|
lazy_static! {
|
|
|
|
static ref DRAG_NORMALIZED_START_VALUE_MEMORY_ID: egui::Id = egui::Id::new((file!(), 0));
|
|
|
|
static ref DRAG_AMOUNT_MEMORY_ID: egui::Id = egui::Id::new((file!(), 1));
|
|
|
|
}
|
|
|
|
|
2022-03-03 23:05:01 +01:00
|
|
|
/// A slider widget similar to [`egui::widgets::Slider`] that knows about NIH-plug parameters ranges
|
2022-02-09 01:07:57 +01:00
|
|
|
/// and can get values for it.
|
|
|
|
///
|
|
|
|
/// TODO: Vertical orientation
|
2022-02-09 11:51:42 +01:00
|
|
|
/// TODO: Check below for more input methods that should be added
|
2022-02-09 12:24:01 +01:00
|
|
|
/// TODO: Decouple the logic from the drawing so we can also do things like nobs without having to
|
|
|
|
/// repeat everything
|
2022-02-09 19:45:54 +01:00
|
|
|
/// TODO: Add WidgetInfo annotations for accessibility
|
|
|
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
2022-02-09 11:13:51 +01:00
|
|
|
pub struct ParamSlider<'a, P: Param> {
|
|
|
|
param: &'a P,
|
2022-02-09 01:07:57 +01:00
|
|
|
setter: &'a ParamSetter<'a>,
|
2022-02-09 19:45:54 +01:00
|
|
|
|
|
|
|
draw_value: bool,
|
2022-02-09 01:07:57 +01:00
|
|
|
}
|
|
|
|
|
2022-02-09 11:13:51 +01:00
|
|
|
impl<'a, P: Param> ParamSlider<'a, P> {
|
2022-02-09 01:07:57 +01:00
|
|
|
/// Create a new slider for a parameter. Use the other methods to modify the slider before
|
2022-03-03 23:05:01 +01:00
|
|
|
/// passing it to [`Ui::add()`].
|
2022-02-09 11:13:51 +01:00
|
|
|
pub fn for_param(param: &'a P, setter: &'a ParamSetter<'a>) -> Self {
|
2022-02-09 19:45:54 +01:00
|
|
|
Self {
|
|
|
|
param,
|
|
|
|
setter,
|
|
|
|
draw_value: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Don't draw the text slider's current value after the slider.
|
|
|
|
pub fn without_value(mut self) -> Self {
|
|
|
|
self.draw_value = false;
|
|
|
|
self
|
2022-02-09 01:07:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn normalized_value(&self) -> f32 {
|
|
|
|
self.param.normalized_value()
|
|
|
|
}
|
|
|
|
|
2022-02-09 11:34:58 +01:00
|
|
|
fn begin_drag(&self) {
|
2022-02-09 01:07:57 +01:00
|
|
|
self.setter.begin_set_parameter(self.param);
|
2022-02-09 11:34:58 +01:00
|
|
|
}
|
2022-02-09 11:18:38 +01:00
|
|
|
|
2022-02-09 11:34:58 +01:00
|
|
|
fn set_normalized_value(&self, normalized: f32) {
|
2022-02-09 11:48:05 +01:00
|
|
|
if normalized != self.normalized_value() {
|
|
|
|
// This snaps to the nearest plain value if the parameter is stepped in some wayA
|
|
|
|
// TODO: As an optimization, we could add a `const CONTINUOUS: bool` to the parameter to
|
|
|
|
// avoid this normalized->plain->normalized conversion for parameters that don't need
|
|
|
|
// it
|
|
|
|
let value = self.param.preview_plain(normalized);
|
|
|
|
self.setter.set_parameter(self.param, value);
|
|
|
|
}
|
2022-02-09 11:34:58 +01:00
|
|
|
}
|
2022-02-09 11:18:38 +01:00
|
|
|
|
2022-02-09 11:42:47 +01:00
|
|
|
// This still needs to be part of a drag gestur
|
|
|
|
fn reset_param(&self) {
|
|
|
|
let normalized_default = self.setter.default_normalized_param_value(self.param);
|
|
|
|
self.setter
|
|
|
|
.set_parameter_normalized(self.param, normalized_default);
|
|
|
|
}
|
|
|
|
|
2022-02-09 12:24:01 +01:00
|
|
|
fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) {
|
|
|
|
// Remember the intial position when we started with the granular drag. This value gets
|
|
|
|
// reset whenever we have a normal itneraction with the slider.
|
|
|
|
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.x + 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),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-02-09 11:34:58 +01:00
|
|
|
fn end_drag(&self) {
|
2022-02-09 01:07:57 +01:00
|
|
|
self.setter.end_set_parameter(self.param);
|
|
|
|
}
|
2022-02-09 12:24:01 +01:00
|
|
|
|
|
|
|
fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 {
|
|
|
|
ui.memory()
|
|
|
|
.data
|
|
|
|
.get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID)
|
2022-02-09 19:17:46 +01:00
|
|
|
.unwrap_or(0.5)
|
2022-02-09 12:24:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) {
|
|
|
|
ui.memory()
|
|
|
|
.data
|
|
|
|
.insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_drag_amount_memory(ui: &Ui) -> f32 {
|
|
|
|
ui.memory()
|
|
|
|
.data
|
|
|
|
.get_temp(*DRAG_AMOUNT_MEMORY_ID)
|
|
|
|
.unwrap_or(0.0)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_drag_amount_memory(ui: &Ui, amount: f32) {
|
|
|
|
ui.memory().data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount);
|
|
|
|
}
|
2022-02-09 01:07:57 +01:00
|
|
|
|
2022-02-09 19:45:54 +01:00
|
|
|
fn slider_ui(&self, ui: &mut Ui, response: &Response) {
|
2022-02-09 01:07:57 +01:00
|
|
|
// Handle user input
|
2022-02-09 11:51:42 +01:00
|
|
|
// TODO: Optionally (since it can be annoying) add scrolling behind a builder option
|
|
|
|
// TODO: Optionally add alt+click for value entry?
|
2022-02-09 11:34:58 +01:00
|
|
|
if response.drag_started() {
|
2022-02-09 12:24:01 +01:00
|
|
|
// When beginning a drag or dragging normally, reset the memory used to keep track of
|
|
|
|
// our granular drag
|
2022-02-09 11:34:58 +01:00
|
|
|
self.begin_drag();
|
2022-02-09 12:24:01 +01:00
|
|
|
Self::set_drag_amount_memory(ui, 0.0);
|
2022-02-09 11:34:58 +01:00
|
|
|
}
|
2022-02-09 01:07:57 +01:00
|
|
|
if let Some(click_pos) = response.interact_pointer_pos() {
|
2022-02-09 11:51:42 +01:00
|
|
|
if ui.input().modifiers.command {
|
2022-02-09 12:24:01 +01:00
|
|
|
// Like double clicking, Ctrl+Click should reset the parameter
|
2022-02-09 11:51:42 +01:00
|
|
|
self.reset_param();
|
2022-02-09 12:24:01 +01:00
|
|
|
} else if ui.input().modifiers.shift {
|
|
|
|
// And shift dragging should switch to a more granulra input method
|
|
|
|
self.granular_drag(ui, response.drag_delta());
|
2022-02-09 11:51:42 +01:00
|
|
|
} else {
|
|
|
|
let proportion =
|
|
|
|
egui::emath::remap_clamp(click_pos.x, response.rect.x_range(), 0.0..=1.0)
|
|
|
|
as f64;
|
|
|
|
self.set_normalized_value(proportion as f32);
|
2022-02-09 12:24:01 +01:00
|
|
|
Self::set_drag_amount_memory(ui, 0.0);
|
2022-02-09 11:51:42 +01:00
|
|
|
}
|
2022-02-09 01:07:57 +01:00
|
|
|
}
|
2022-02-09 11:42:47 +01:00
|
|
|
if response.double_clicked() {
|
|
|
|
self.reset_param();
|
|
|
|
}
|
2022-02-09 11:34:58 +01:00
|
|
|
if response.drag_released() {
|
|
|
|
self.end_drag();
|
|
|
|
}
|
2022-02-09 01:07:57 +01:00
|
|
|
|
|
|
|
// And finally draw the thing
|
|
|
|
if ui.is_rect_visible(response.rect) {
|
|
|
|
// We'll do a flat widget with background -> filled foreground -> slight border
|
|
|
|
ui.painter()
|
|
|
|
.rect_filled(response.rect, 0.0, ui.visuals().widgets.inactive.bg_fill);
|
|
|
|
|
|
|
|
let filled_proportion = self.normalized_value();
|
2022-02-09 11:19:49 +01:00
|
|
|
if filled_proportion > 0.0 {
|
|
|
|
let mut filled_rect = response.rect;
|
|
|
|
filled_rect.set_width(response.rect.width() * filled_proportion);
|
|
|
|
let filled_bg = if response.dragged() {
|
2022-02-09 11:32:19 +01:00
|
|
|
util::add_hsv(ui.visuals().selection.bg_fill, 0.0, -0.1, 0.1)
|
2022-02-09 11:19:49 +01:00
|
|
|
} else {
|
|
|
|
ui.visuals().selection.bg_fill
|
|
|
|
};
|
|
|
|
ui.painter().rect_filled(filled_rect, 0.0, filled_bg);
|
|
|
|
}
|
2022-02-09 01:07:57 +01:00
|
|
|
|
|
|
|
ui.painter().rect_stroke(
|
|
|
|
response.rect,
|
|
|
|
0.0,
|
|
|
|
Stroke::new(1.0, ui.visuals().widgets.active.bg_fill),
|
|
|
|
);
|
|
|
|
}
|
2022-02-09 19:17:46 +01:00
|
|
|
}
|
2022-02-09 19:45:54 +01:00
|
|
|
|
|
|
|
fn value_ui(&self, ui: &mut Ui) {
|
|
|
|
let visuals = ui.visuals().widgets.inactive;
|
|
|
|
let should_draw_frame = ui.visuals().button_frame;
|
|
|
|
let padding = ui.spacing().button_padding;
|
|
|
|
|
2022-02-27 16:49:18 +01:00
|
|
|
let text = WidgetText::from(format!("{}", self.param)).into_galley(
|
|
|
|
ui,
|
|
|
|
None,
|
2022-02-09 19:45:54 +01:00
|
|
|
ui.available_width() - (padding.x * 2.0),
|
2022-02-27 16:49:18 +01:00
|
|
|
TextStyle::Button,
|
2022-02-09 19:45:54 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
let (_, rect) = ui.allocate_space(text.size() + (padding * 2.0));
|
|
|
|
if ui.is_rect_visible(rect) {
|
|
|
|
if should_draw_frame {
|
|
|
|
let fill = visuals.bg_fill;
|
|
|
|
let stroke = visuals.bg_stroke;
|
|
|
|
ui.painter().rect(
|
|
|
|
rect.expand(visuals.expansion),
|
2022-02-27 16:49:18 +01:00
|
|
|
visuals.rounding,
|
2022-02-09 19:45:54 +01:00
|
|
|
fill,
|
|
|
|
stroke,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let text_pos = ui
|
|
|
|
.layout()
|
|
|
|
.align_size_within_rect(text.size(), rect.shrink2(padding))
|
|
|
|
.min;
|
2022-02-27 16:49:18 +01:00
|
|
|
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
|
2022-02-09 19:45:54 +01:00
|
|
|
}
|
|
|
|
}
|
2022-02-09 19:17:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<P: Param> Widget for ParamSlider<'_, P> {
|
|
|
|
fn ui(self, ui: &mut Ui) -> Response {
|
2022-02-09 19:45:54 +01:00
|
|
|
ui.horizontal(|ui| {
|
|
|
|
// Allocate space, but add some padding on the top and bottom to make it look a bit slimmer.
|
|
|
|
let height = ui
|
2022-02-27 16:49:18 +01:00
|
|
|
.text_style_height(&TextStyle::Body)
|
2022-02-09 19:45:54 +01:00
|
|
|
.max(ui.spacing().interact_size.y);
|
|
|
|
let slider_height = ui.painter().round_to_pixel(height * 0.65);
|
|
|
|
let response = ui
|
|
|
|
.vertical(|ui| {
|
|
|
|
ui.allocate_space(vec2(
|
|
|
|
ui.spacing().slider_width,
|
|
|
|
(height - slider_height) / 2.0,
|
|
|
|
));
|
|
|
|
let response = ui.allocate_response(
|
|
|
|
vec2(ui.spacing().slider_width, slider_height),
|
|
|
|
Sense::click_and_drag(),
|
|
|
|
);
|
|
|
|
ui.allocate_space(vec2(
|
|
|
|
ui.spacing().slider_width,
|
|
|
|
(height - slider_height) / 2.0,
|
|
|
|
));
|
|
|
|
response
|
|
|
|
})
|
|
|
|
.inner;
|
|
|
|
|
|
|
|
self.slider_ui(ui, &response);
|
|
|
|
if self.draw_value {
|
|
|
|
self.value_ui(ui);
|
|
|
|
}
|
|
|
|
|
|
|
|
response
|
|
|
|
})
|
|
|
|
.inner
|
2022-02-09 01:07:57 +01:00
|
|
|
}
|
|
|
|
}
|