1use std::sync::{Arc, LazyLock};
2
3use egui::emath::GuiRounding;
4use egui::{
5 self, Key, Response, Sense, Stroke, TextEdit, TextStyle, Ui, Vec2, Widget, WidgetText, emath,
6 vec2,
7};
8use nice_plug_core::context::gui::ParamSetter;
9use nice_plug_core::params::Param;
10use parking_lot::Mutex;
11
12use super::util;
13
14const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015;
17
18static DRAG_NORMALIZED_START_VALUE_MEMORY_ID: LazyLock<egui::Id> =
19 LazyLock::new(|| egui::Id::new((file!(), 0)));
20static DRAG_AMOUNT_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 1)));
21static IS_DRAGGING_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 2)));
22static VALUE_ENTRY_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 3)));
23
24#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
34pub struct ParamSlider<'a, P: Param> {
35 param: &'a P,
36 setter: &'a ParamSetter<'a>,
37
38 draw_value: bool,
39 slider_width: Option<f32>,
40
41 keyboard_focus_id: Option<egui::Id>,
43}
44
45impl<'a, P: Param> ParamSlider<'a, P> {
46 pub fn for_param(param: &'a P, setter: &'a ParamSetter<'a>) -> Self {
49 Self {
50 param,
51 setter,
52
53 draw_value: true,
54 slider_width: None,
55
56 keyboard_focus_id: None,
57 }
58 }
59
60 pub fn without_value(mut self) -> Self {
62 self.draw_value = false;
63 self
64 }
65
66 pub fn with_width(mut self, width: f32) -> Self {
68 self.slider_width = Some(width);
69 self
70 }
71
72 fn plain_value(&self) -> P::Plain {
73 self.param.modulated_plain_value()
74 }
75
76 fn normalized_value(&self) -> f32 {
77 self.param.modulated_normalized_value()
78 }
79
80 fn string_value(&self) -> String {
81 self.param.to_string()
82 }
83
84 fn begin_keyboard_entry(&self, ui: &Ui) {
86 ui.memory_mut(|mem| mem.request_focus(self.keyboard_focus_id.unwrap()));
87
88 let value_entry_mutex = ui.memory_mut(|mem| {
91 mem.data
92 .get_temp_mut_or_default::<Arc<Mutex<String>>>(*VALUE_ENTRY_MEMORY_ID)
93 .clone()
94 });
95 *value_entry_mutex.lock() = self.string_value();
96 }
97
98 fn keyboard_entry_active(&self, ui: &Ui) -> bool {
99 ui.memory(|mem| mem.has_focus(self.keyboard_focus_id.unwrap()))
100 }
101
102 fn begin_drag(&self, ui: &Ui) {
103 self.setter.begin_set_parameter(self.param);
104 Self::set_is_dragging_memory(ui, true);
105 }
106
107 fn set_normalized_value(&self, normalized: f32) {
108 let value = self.param.preview_plain(normalized);
113 if value != self.plain_value() {
114 self.setter.set_parameter(self.param, value);
115 }
116 }
117
118 fn set_from_string(&self, string: &str) -> bool {
121 match self.param.string_to_normalized_value(string) {
122 Some(normalized_value) => {
123 self.set_normalized_value(normalized_value);
124 true
125 }
126 None => false,
127 }
128 }
129
130 fn reset_param(&self) {
132 self.setter
133 .set_parameter(self.param, self.param.default_plain_value());
134 }
135
136 fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) {
137 let start_value = if Self::get_drag_amount_memory(ui) == 0.0 {
140 Self::set_drag_normalized_start_value_memory(ui, self.normalized_value());
141 self.normalized_value()
142 } else {
143 Self::get_drag_normalized_start_value_memory(ui)
144 };
145
146 let total_drag_distance = drag_delta.x + Self::get_drag_amount_memory(ui);
147 Self::set_drag_amount_memory(ui, total_drag_distance);
148
149 self.set_normalized_value(
150 (start_value + (total_drag_distance * GRANULAR_DRAG_MULTIPLIER)).clamp(0.0, 1.0),
151 );
152 }
153
154 fn end_drag(&self, ui: &Ui) {
155 self.setter.end_set_parameter(self.param);
156 Self::set_is_dragging_memory(ui, false);
157 }
158
159 fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 {
160 ui.memory(|mem| {
161 mem.data
162 .get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID)
163 .unwrap_or(0.5)
164 })
165 }
166
167 fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) {
168 ui.memory_mut(|mem| {
169 mem.data
170 .insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount)
171 });
172 }
173
174 fn get_drag_amount_memory(ui: &Ui) -> f32 {
175 ui.memory(|mem| mem.data.get_temp(*DRAG_AMOUNT_MEMORY_ID).unwrap_or(0.0))
176 }
177
178 fn set_drag_amount_memory(ui: &Ui, amount: f32) {
179 ui.memory_mut(|mem| mem.data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount));
180 }
181
182 fn get_is_dragging_memory(ui: &Ui) -> bool {
183 ui.memory(|mem| mem.data.get_temp(*IS_DRAGGING_MEMORY_ID).unwrap_or(false))
184 }
185
186 fn set_is_dragging_memory(ui: &Ui, is_dragging: bool) {
187 ui.memory_mut(|mem| mem.data.insert_temp(*IS_DRAGGING_MEMORY_ID, is_dragging));
188 }
189
190 fn slider_ui(&mut self, ui: &Ui, response: &mut Response) {
191 if response.is_pointer_button_down_on() {
194 let is_dragging = Self::get_is_dragging_memory(ui);
195 if !is_dragging {
196 self.begin_drag(ui);
199 Self::set_drag_amount_memory(ui, 0.0);
200 }
201 }
202 if let Some(click_pos) = response.interact_pointer_pos() {
203 if ui.input(|i| i.modifiers.command) {
204 self.reset_param();
206 response.mark_changed();
207 } else if ui.input(|i| i.modifiers.shift) {
214 self.granular_drag(ui, response.drag_delta());
216 response.mark_changed();
217 } else {
218 let proportion =
219 emath::remap_clamp(click_pos.x, response.rect.x_range(), 0.0..=1.0) as f64;
220 self.set_normalized_value(proportion as f32);
221 response.mark_changed();
222 Self::set_drag_amount_memory(ui, 0.0);
223 }
224 }
225 if response.double_clicked() {
226 self.reset_param();
227 response.mark_changed();
228 }
229 if response.drag_stopped() {
230 self.end_drag(ui);
231 }
232
233 if ui.is_rect_visible(response.rect) {
235 ui.painter()
237 .rect_filled(response.rect, 0.0, ui.visuals().widgets.inactive.bg_fill);
238
239 let filled_proportion = self.normalized_value();
240 if filled_proportion > 0.0 {
241 let mut filled_rect = response.rect;
242 filled_rect.set_width(response.rect.width() * filled_proportion);
243 let filled_bg = if response.hovered() {
244 util::add_hsv(ui.visuals().selection.bg_fill, 0.0, -0.1, 0.1)
245 } else {
246 ui.visuals().selection.bg_fill
247 };
248 ui.painter().rect_filled(filled_rect, 0.0, filled_bg);
249 }
250
251 ui.painter().rect_stroke(
252 response.rect,
253 0.0,
254 Stroke::new(1.0f32, ui.visuals().widgets.active.bg_fill),
255 egui::StrokeKind::Middle,
256 );
257 }
258 }
259
260 fn value_ui(&mut self, ui: &mut Ui) {
261 let visuals = ui.visuals().widgets.inactive;
262 let should_draw_frame = ui.visuals().button_frame;
263 let padding = ui.spacing().button_padding;
264
265 let keyboard_focus_id = self.keyboard_focus_id.unwrap();
268 if self.keyboard_entry_active(ui) {
269 let value_entry_mutex = ui.memory_mut(|mem| {
270 mem.data
271 .get_temp_mut_or_default::<Arc<Mutex<String>>>(*VALUE_ENTRY_MEMORY_ID)
272 .clone()
273 });
274 let mut value_entry = value_entry_mutex.lock();
275
276 ui.add(
277 TextEdit::singleline(&mut *value_entry)
278 .id(keyboard_focus_id)
279 .font(TextStyle::Monospace),
280 );
281 if ui.input(|i| i.key_pressed(Key::Escape)) {
282 ui.memory_mut(|mem| mem.surrender_focus(keyboard_focus_id));
284 } else if ui.input(|i| i.key_pressed(Key::Enter)) {
285 self.begin_drag(ui);
287 self.set_from_string(&value_entry);
288 self.end_drag(ui);
289
290 ui.memory_mut(|mem| mem.surrender_focus(keyboard_focus_id));
291 }
292 } else {
293 let text = WidgetText::from(self.string_value()).into_galley(
294 ui,
295 None,
296 ui.available_width() - (padding.x * 2.0),
297 TextStyle::Button,
298 );
299
300 let response = ui.allocate_response(text.size() + (padding * 2.0), Sense::click());
301 if response.clicked() {
302 self.begin_keyboard_entry(ui);
303 }
304
305 if ui.is_rect_visible(response.rect) {
306 if should_draw_frame {
307 let fill = visuals.bg_fill;
308 let stroke = if response.hovered() {
309 ui.set_cursor_icon(egui::CursorIcon::Text);
310 visuals.fg_stroke
311 } else {
312 visuals.bg_stroke
313 };
314 ui.painter().rect(
315 response.rect.expand(visuals.expansion),
316 visuals.corner_radius,
317 fill,
318 stroke,
319 egui::StrokeKind::Middle,
320 );
321 }
322
323 let text_pos = ui
324 .layout()
325 .align_size_within_rect(text.size(), response.rect.shrink2(padding))
326 .min;
327
328 ui.painter().add(egui::epaint::TextShape::new(
329 text_pos,
330 text,
331 visuals.fg_stroke.color,
332 ));
333 }
334 }
335 }
336}
337
338impl<P: Param> Widget for ParamSlider<'_, P> {
339 fn ui(mut self, ui: &mut Ui) -> Response {
340 let slider_width = self
341 .slider_width
342 .unwrap_or_else(|| ui.spacing().slider_width);
343
344 ui.horizontal(|ui| {
345 let height = ui
347 .text_style_height(&TextStyle::Body)
348 .max(ui.spacing().interact_size.y * 0.8);
349 let slider_height = (height * 0.8).round_to_pixels(ui.painter().pixels_per_point());
350 let mut response = ui
351 .vertical(|ui| {
352 ui.allocate_space(vec2(slider_width, (height - slider_height) / 2.0));
353 let response = ui.allocate_response(
354 vec2(slider_width, slider_height),
355 Sense::click_and_drag(),
356 );
357 let (kb_edit_id, _) =
358 ui.allocate_space(vec2(slider_width, (height - slider_height) / 2.0));
359 self.keyboard_focus_id = Some(kb_edit_id);
363
364 response
365 })
366 .inner;
367
368 self.slider_ui(ui, &mut response);
369 if self.draw_value {
370 self.value_ui(ui);
371 }
372
373 response
374 })
375 .inner
376 }
377}