GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/input/ramp.rs (73.3K)
1 use crate::colors;
2 use crate::scene::layout::{Rect, Size};
3 use crate::scene::paint::{Cap, PaintCtx};
4 use crate::widget::model::{EventCtx, Input, Layout, Paint};
5 use crate::widget::*;
6 use crate::widget::input::{Slider, Slider2D, Button};
7
8 // ==========================================
9 // 1. Color Ramp (renamed from Ramp)
10 // ==========================================
11
12 #[derive(Debug, Clone)]
13 pub struct ColorRampKey {
14 pub pos: f32,
15 pub color: [f32; 3],
16 }
17
18 pub struct ColorRamp {
19 pub base: Widget,
20 pub keys: Vec<ColorRampKey>,
21 pub selected_key_idx: Option<usize>,
22 pub is_dragging_key: bool,
23 pub just_changed: bool,
24
25 // Child controls for color editing & deletion
26 pub r_slider: Adapted<Slider>,
27 pub g_slider: Adapted<Slider>,
28 pub b_slider: Adapted<Slider>,
29 pub del_button: Adapted<Button>,
30
31 }
32
33 impl ColorRamp {
34 pub fn new() -> Adapted<ColorRamp> {
35 let keys = vec![
36 ColorRampKey { pos: 0.0, color: [0.0, 0.0, 0.0] },
37 ColorRampKey { pos: 1.0, color: [1.0, 1.0, 1.0] },
38 ];
39
40 let r_slider = Slider::new().with_label("Red");
41 let g_slider = Slider::new().with_label("Green");
42 let b_slider = Slider::new().with_label("Blue");
43 let del_button = Button::new(0.0, 0.0, 70.0, 28.0).with_label("Delete Key");
44
45 Adapted::new(ColorRamp {
46 base: Widget::new(),
47 keys,
48 selected_key_idx: None,
49 is_dragging_key: false,
50 just_changed: false,
51 r_slider,
52 g_slider,
53 b_slider,
54 del_button,
55 })
56 }
57
58 pub fn get_interpolated_color(&self, t: f32) -> [f32; 3] {
59 if self.keys.is_empty() {
60 return [0.0, 0.0, 0.0];
61 }
62 if t <= self.keys[0].pos {
63 return self.keys[0].color;
64 }
65 if t >= self.keys[self.keys.len() - 1].pos {
66 return self.keys[self.keys.len() - 1].color;
67 }
68
69 for i in 0..self.keys.len() - 1 {
70 let k1 = &self.keys[i];
71 let k2 = &self.keys[i+1];
72 if t >= k1.pos && t <= k2.pos {
73 let range = k2.pos - k1.pos;
74 if range.abs() < 0.0001 {
75 return k1.color;
76 }
77 let w = (t - k1.pos) / range;
78 return [
79 k1.color[0] * (1.0 - w) + k2.color[0] * w,
80 k1.color[1] * (1.0 - w) + k2.color[1] * w,
81 k1.color[2] * (1.0 - w) + k2.color[2] * w,
82 ];
83 }
84 }
85 self.keys[0].color
86 }
87
88 fn sort_keys(&mut self) {
89 let prev_selected_id = self.selected_key_idx.map(|idx| self.keys[idx].pos);
90 self.keys.sort_by(|a, b| a.pos.partial_cmp(&b.pos).unwrap());
91 if let Some(pos) = prev_selected_id {
92 if let Some(new_idx) = self.keys.iter().position(|k| (k.pos - pos).abs() < 0.0001) {
93 self.selected_key_idx = Some(new_idx);
94 }
95 }
96 }
97 }
98
99
100
101
102 // ==========================================
103 // 2. Houdini-Style Float Ramp
104 // ==========================================
105
106 #[derive(Debug, Clone)]
107 pub struct RampKey {
108 pub pos: f32,
109 pub value: f32,
110 }
111
112 pub struct Ramp {
113 pub base: Widget,
114 pub keys: Vec<RampKey>,
115 pub selected_key_idx: Option<usize>,
116 pub is_dragging_key: bool,
117 pub just_changed: bool,
118 /// The key latched by the current hover-scroll gesture: a trackpad
119 /// scroll starting over a key steers that key until the fingers lift
120 /// (a >250ms pause reads as a new gesture and re-latches by hover).
121 scroll_key_idx: Option<usize>,
122 /// Context-menu toggle: hide the bottom control strip and let the graph
123 /// claim its space.
124 pub controls_collapsed: bool,
125 /// Hover-scroll glide velocity (plot units/sec, applied-delta signs) and
126 /// the last scroll-event instant: when the event stream stops, the tick
127 /// keeps the latched key coasting with exponential decay.
128 scroll_vel: (f32, f32),
129 last_key_scroll: Option<std::time::Instant>,
130
131 // Child controls for key editing & deletion. The key pad is a 2-axis
132 // slider driving the selected key's position (x) and value (y).
133 pub key_pad: Adapted<Slider2D>,
134 pub del_button: Adapted<Button>,
135 pub preset_dropdown: Adapted<Dropdown>,
136 pub line_type_dropdown: Adapted<Dropdown>,
137
138 }
139
140 impl Ramp {
141 pub fn new() -> Adapted<Ramp> {
142 let keys = vec![
143 RampKey { pos: 0.0, value: 0.5 },
144 RampKey { pos: 0.2, value: 1.0 },
145 RampKey { pos: 0.8, value: 1.0 },
146 RampKey { pos: 1.0, value: 0.5 },
147 ];
148
149 // The key pad: a 2-axis slider driving the selected key's position
150 // (x) and value (y), labeled like the dropdowns.
151 let key_pad = Slider2D::new().with_label("Key");
152 // A square x-icon button (cce-icons); label fallback if the icon set
153 // is missing on this machine. By NAME, not by a captured id: an id
154 // does not survive the renderer rebuild a reconnect performs, and the
155 // widget outlives the renderer (see `Button::icon_name`).
156 let del_button =
157 Button::new(0.0, 0.0, 22.0, 22.0).with_icon_name("x", "Delete");
158 // Short names on purpose: the strip's columns are narrow, and these
159 // render inside param rows too ("Bevel (Raised)" used to clip).
160 // Labeled: the dropdowns draw their own detached labels, sitting on
161 // the expanded top wall of their inset (the labeled-relief style).
162 let preset_dropdown = Dropdown::new(
163 vec![
164 "Custom".to_string(),
165 "Linear".to_string(),
166 "Raised".to_string(),
167 "Sunken".to_string(),
168 "Peak".to_string(),
169 "Valley".to_string(),
170 ],
171 2,
172 ).with_open_upward(true).with_label("Preset");
173 let line_type_dropdown = Dropdown::new(
174 vec![
175 "Linear".to_string(),
176 "Bezier".to_string(),
177 ],
178 0,
179 ).with_open_upward(true).with_label("Line");
180
181 Adapted::new(Ramp {
182 base: Widget::new(),
183 keys,
184 selected_key_idx: None,
185 is_dragging_key: false,
186 just_changed: false,
187 scroll_key_idx: None,
188 controls_collapsed: false,
189 scroll_vel: (0.0, 0.0),
190 last_key_scroll: None,
191 key_pad,
192 del_button,
193 preset_dropdown,
194 line_type_dropdown,
195 })
196 }
197
198 pub fn apply_preset(&mut self, idx: usize) {
199 match idx {
200 1 => { // Linear
201 self.keys = vec![
202 RampKey { pos: 0.0, value: 0.0 },
203 RampKey { pos: 1.0, value: 1.0 },
204 ];
205 }
206 2 => { // Bevel (Raised)
207 self.keys = vec![
208 RampKey { pos: 0.0, value: 0.5 },
209 RampKey { pos: 0.2, value: 1.0 },
210 RampKey { pos: 0.8, value: 1.0 },
211 RampKey { pos: 1.0, value: 0.5 },
212 ];
213 }
214 3 => { // Bevel (Sunken)
215 self.keys = vec![
216 RampKey { pos: 0.0, value: 0.5 },
217 RampKey { pos: 0.2, value: 0.0 },
218 RampKey { pos: 0.8, value: 0.0 },
219 RampKey { pos: 1.0, value: 0.5 },
220 ];
221 }
222 4 => { // Peak
223 self.keys = vec![
224 RampKey { pos: 0.0, value: 0.0 },
225 RampKey { pos: 0.5, value: 1.0 },
226 RampKey { pos: 1.0, value: 0.0 },
227 ];
228 }
229 5 => { // Valley
230 self.keys = vec![
231 RampKey { pos: 0.0, value: 1.0 },
232 RampKey { pos: 0.5, value: 0.0 },
233 RampKey { pos: 1.0, value: 1.0 },
234 ];
235 }
236 _ => {}
237 }
238 self.selected_key_idx = None;
239 self.just_changed = true;
240 }
241
242 /// The curve's value at `t` — [`crate::layout::sample_ramp_keys`], the
243 /// DE's one ramp interpolation, so what this widget draws is exactly
244 /// what every consumer of its spec string evaluates.
245 pub fn get_interpolated_value(&self, t: f32) -> f32 {
246 let keys: Vec<(f32, f32)> = self.keys.iter().map(|k| (k.pos, k.value)).collect();
247 crate::layout::sample_ramp_keys(&keys, self.smooth(), t)
248 }
249
250 /// Whether the curve is the smooth (monotone cubic) line type vs straight
251 /// segments — see [`crate::layout::sample_ramp_keys`].
252 pub fn smooth(&self) -> bool {
253 self.line_type_dropdown.selected == 1
254 }
255
256 /// This ramp's state as the DE's ramp spec string ([`format_ramp_spec`]).
257 pub fn spec_string(&self) -> String {
258 let keys: Vec<(f32, f32)> = self.keys.iter().map(|k| (k.pos, k.value)).collect();
259 format_ramp_spec(&keys, self.smooth())
260 }
261
262 /// Apply a spec string ([`parse_ramp_spec`]); returns whether anything changed.
263 /// Unparsable specs are ignored (keeps the current curve).
264 pub fn set_spec(&mut self, spec: &str) -> bool {
265 let Some((keys, smooth)) = parse_ramp_spec(spec) else {
266 return false;
267 };
268 let new_keys: Vec<RampKey> =
269 keys.into_iter().map(|(pos, value)| RampKey { pos, value }).collect();
270 let new_line = if smooth { 1 } else { 0 };
271 let changed = self.line_type_dropdown.selected != new_line
272 || self.keys.len() != new_keys.len()
273 || self
274 .keys
275 .iter()
276 .zip(new_keys.iter())
277 .any(|(a, b)| (a.pos - b.pos).abs() > 0.0005 || (a.value - b.value).abs() > 0.0005);
278 if changed {
279 self.keys = new_keys;
280 self.line_type_dropdown.selected = new_line;
281 self.selected_key_idx = None;
282 self.preset_dropdown.selected = 0; // Custom
283 self.arrange_fields();
284 }
285 changed
286 }
287 }
288
289 /// Serialize ramp keys + line type as the DE's ramp spec string:
290 /// `"smooth;0.000:0.500,0.200:1.000,…"` (`"linear;…"` for straight segments) —
291 /// the format ramp-valued params travel in (`ParametersBg` "ramp" rows,
292 /// project files, `cce_ui::layout::set_bevel_profile_keys` consumers).
293 pub fn format_ramp_spec(keys: &[(f32, f32)], smooth: bool) -> String {
294 let body: Vec<String> =
295 keys.iter().map(|(p, v)| format!("{:.3}:{:.3}", p, v)).collect();
296 format!("{};{}", if smooth { "smooth" } else { "linear" }, body.join(","))
297 }
298
299 /// Parse a ramp spec string ([`format_ramp_spec`]) into `(keys, smooth)`.
300 /// `None` for anything that doesn't yield at least two keys.
301 pub fn parse_ramp_spec(spec: &str) -> Option<(Vec<(f32, f32)>, bool)> {
302 let (head, body) = spec.split_once(';')?;
303 let smooth = head.trim() == "smooth";
304 let mut keys = Vec::new();
305 for part in body.split(',') {
306 let (p, v) = part.split_once(':')?;
307 keys.push((
308 p.trim().parse::<f32>().ok()?.clamp(0.0, 1.0),
309 v.trim().parse::<f32>().ok()?.clamp(0.0, 1.0),
310 ));
311 }
312 if keys.len() < 2 {
313 return None;
314 }
315 keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
316 Some((keys, smooth))
317 }
318
319
320
321 impl ColorRamp {
322 fn arrange_fields(&mut self) {
323 let (x, y, w, h) = (self.base.x, self.base.y, self.base.w, self.base.h);
324 self.base.x = x;
325 self.base.y = y;
326 self.base.w = w;
327 self.base.h = h;
328
329
330 let th = crate::layout::ramp_height();
331 let sy = y + th + 55.0;
332 let slider_w = w - 100.0;
333
334 if self.selected_key_idx.is_some() {
335 self.r_slider.set_rect(x + 10.0, sy, slider_w, 20.0);
336 self.g_slider.set_rect(x + 10.0, sy + 25.0, slider_w, 20.0);
337 self.b_slider.set_rect(x + 10.0, sy + 50.0, slider_w, 20.0);
338 self.del_button.set_rect(x + w - 80.0, sy + 20.0, 70.0, 28.0);
339 } else {
340 self.r_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
341 self.g_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
342 self.b_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
343 self.del_button.set_rect(-1000.0, -1000.0, 0.0, 0.0);
344 }
345
346 }
347 }
348
349 impl Layout for ColorRamp {
350 fn rect_assigned(&mut self, rect: Rect) {
351 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
352 self.base.x = x;
353 self.base.y = y;
354 self.base.w = w;
355 self.base.h = h;
356
357
358 let th = crate::layout::ramp_height();
359 let sy = y + th + 55.0;
360 let slider_w = w - 100.0;
361
362 if self.selected_key_idx.is_some() {
363 self.r_slider.set_rect(x + 10.0, sy, slider_w, 20.0);
364 self.g_slider.set_rect(x + 10.0, sy + 25.0, slider_w, 20.0);
365 self.b_slider.set_rect(x + 10.0, sy + 50.0, slider_w, 20.0);
366 self.del_button.set_rect(x + w - 80.0, sy + 20.0, 70.0, 28.0);
367 } else {
368 self.r_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
369 self.g_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
370 self.b_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
371 self.del_button.set_rect(-1000.0, -1000.0, 0.0, 0.0);
372 }
373
374 }
375
376 }
377
378 impl Paint for ColorRamp {
379 fn color(&self) -> [f32; 4] {
380 colors::ramp_background_color()
381 }
382
383 // Field children are ctx-linked for event propagation but painted here (gated on a
384 // key being selected) — the walk must not also descend.
385 fn paints_own_subtree(&self) -> bool {
386 true
387 }
388
389 fn paint(&self, _rect: Rect, pc: &mut PaintCtx) {
390 let quads: Vec<(f32, f32, f32, f32, [f32; 4])> = {
391 let mut quads = Vec::new();
392 let th = crate::layout::ramp_height();
393 let track_x = self.base.x + 10.0;
394 let track_w = self.base.w - 20.0;
395
396 // Draw outer container border
397 let bx = self.base.x;
398 let by = self.base.y;
399 let bw = self.base.w;
400 let bh = self.base.h;
401 let border_color = colors::ramp_border_color();
402 quads.push((bx, by, bw, 1.0, border_color)); // Top
403 quads.push((bx, by + bh - 1.0, bw, 1.0, border_color)); // Bottom
404 quads.push((bx, by, 1.0, bh, border_color)); // Left
405 quads.push((bx + bw - 1.0, by, 1.0, bh, border_color)); // Right
406
407 // Draw track border
408 quads.push((track_x - 1.0, self.base.y + 10.0 - 1.0, track_w + 2.0, th + 2.0, border_color));
409
410 // Draw interpolated track slices (e.g. 100 slices)
411 let slices = 100;
412 let slice_w = track_w / slices as f32;
413 for i in 0..slices {
414 let t1 = i as f32 / slices as f32;
415 let t2 = (i + 1) as f32 / slices as f32;
416 let center_t = (t1 + t2) / 2.0;
417 let col = self.get_interpolated_color(center_t);
418 let sx = track_x + t1 * track_w;
419 quads.push((sx, self.base.y + 10.0, slice_w, th, [col[0], col[1], col[2], 1.0]));
420 }
421
422 if self.selected_key_idx.is_some() {
423 let ctx_dummy = crate::context::UiContext::new();
424 quads.extend(self.r_slider.all_quads(&ctx_dummy));
425 quads.extend(self.g_slider.all_quads(&ctx_dummy));
426 quads.extend(self.b_slider.all_quads(&ctx_dummy));
427 quads.extend(self.del_button.all_quads(&ctx_dummy));
428 }
429
430 quads
431
432 };
433 for (qx, qy, qw, qh, qc) in quads {
434 pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
435 }
436 let circles: Vec<(f32, f32, f32, [f32; 4])> = {
437 let mut circles = Vec::new();
438 let th = crate::layout::ramp_height();
439 let track_x = self.base.x + 10.0;
440 let track_w = self.base.w - 20.0;
441 let py = self.base.y + 10.0 + th + 15.0;
442
443 for (idx, key) in self.keys.iter().enumerate() {
444 let cx = track_x + key.pos * track_w;
445 circles.push((cx, py, 7.0, [0.0, 0.0, 0.0, 0.8]));
446 circles.push((cx, py, 6.0, [key.color[0], key.color[1], key.color[2], 1.0]));
447 if Some(idx) == self.selected_key_idx {
448 circles.push((cx, py, 8.0, [0.49, 1.0, 1.0, 0.5]));
449 }
450 }
451
452 circles
453
454 };
455 for (cx, cy, r, c) in circles {
456 pc.circle(cx, cy, r, c);
457 }
458 if self.selected_key_idx.is_some() {
459 let dummy = UiContext::new();
460 self.r_slider.paint_self(&dummy, pc);
461 self.g_slider.paint_self(&dummy, pc);
462 self.b_slider.paint_self(&dummy, pc);
463 self.del_button.paint_self(&dummy, pc);
464 }
465 }
466 }
467
468 impl Input for ColorRamp {
469 fn wants_tick(&self) -> bool {
470 true
471 }
472
473 fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
474 // (The per-tick field-widget re-parenting is gone, 6bd: it was a dummy-ctx
475 // `set_parent` whose every effect was discarded — legacy behaved the same.)
476 let Some(ui) = ectx.ui.as_deref_mut() else {
477 return false;
478 };
479 let mut changed = self.just_changed;
480 self.just_changed = false;
481
482 if self.selected_key_idx.is_some() {
483 if self.r_slider.tick(dt, ui) {
484 if let Some(idx) = self.selected_key_idx {
485 self.keys[idx].color[0] = self.r_slider.inner().value();
486 }
487 changed = true;
488 }
489 if self.g_slider.tick(dt, ui) {
490 if let Some(idx) = self.selected_key_idx {
491 self.keys[idx].color[1] = self.g_slider.inner().value();
492 }
493 changed = true;
494 }
495 if self.b_slider.tick(dt, ui) {
496 if let Some(idx) = self.selected_key_idx {
497 self.keys[idx].color[2] = self.b_slider.inner().value();
498 }
499 changed = true;
500 }
501 if self.del_button.tick(dt, ui) {
502 changed = true;
503 }
504 }
505 changed
506
507 }
508
509 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
510 match event {
511 Event::MouseButton { button, state, x, y, .. } => {
512 let (button, state, px, py_event) = (*button, *state, *x, *y);
513 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
514 if button != MouseButton::Left { return false; }
515
516 let th = crate::layout::ramp_height();
517 let track_x = self.base.x + 10.0;
518 let track_w = self.base.w - 20.0;
519 let py_peg = self.base.y + 10.0 + th + 15.0;
520
521 if state == ElementState::Pressed {
522 for (idx, key) in self.keys.iter().enumerate() {
523 let cx = track_x + key.pos * track_w;
524 let dx = px - cx;
525 let dy = py_event - py_peg;
526 if (dx*dx + dy*dy) <= 64.0 {
527 self.selected_key_idx = Some(idx);
528 self.is_dragging_key = true;
529 self.r_slider.set_value(key.color[0]);
530 self.g_slider.set_value(key.color[1]);
531 self.b_slider.set_value(key.color[2]);
532 self.arrange_fields();
533 return true;
534 }
535 }
536
537 if px >= track_x && px <= track_x + track_w && py_event >= self.base.y + 10.0 && py_event <= self.base.y + 10.0 + th {
538 let t = (px - track_x) / track_w;
539 let col = self.get_interpolated_color(t);
540 let new_key = ColorRampKey { pos: t, color: col };
541 self.keys.push(new_key);
542 self.sort_keys();
543 self.just_changed = true;
544
545 if let Some(new_idx) = self.keys.iter().position(|k| (k.pos - t).abs() < 0.0001) {
546 self.selected_key_idx = Some(new_idx);
547 self.r_slider.set_value(col[0]);
548 self.g_slider.set_value(col[1]);
549 self.b_slider.set_value(col[2]);
550 }
551 self.arrange_fields();
552 return true;
553 }
554
555 if self.selected_key_idx.is_some() {
556 if self.r_slider.mouse_input(button, state, px, py_event, ui) { return true; }
557 if self.g_slider.mouse_input(button, state, px, py_event, ui) { return true; }
558 if self.b_slider.mouse_input(button, state, px, py_event, ui) { return true; }
559 if self.del_button.mouse_input(button, state, px, py_event, ui) {
560 if self.del_button.take_click() {
561 if let Some(idx) = self.selected_key_idx {
562 if self.keys.len() > 2 {
563 self.keys.remove(idx);
564 self.selected_key_idx = None;
565 self.just_changed = true;
566 self.arrange_fields();
567 }
568 }
569 }
570 return true;
571 }
572 }
573 } else {
574 self.is_dragging_key = false;
575 if self.selected_key_idx.is_some() {
576 self.r_slider.mouse_input(button, state, px, py_event, ui);
577 self.g_slider.mouse_input(button, state, px, py_event, ui);
578 self.b_slider.mouse_input(button, state, px, py_event, ui);
579 if self.del_button.mouse_input(button, state, px, py_event, ui) {
580 if self.del_button.take_click() {
581 if let Some(idx) = self.selected_key_idx {
582 if self.keys.len() > 2 {
583 self.keys.remove(idx);
584 self.selected_key_idx = None;
585 self.just_changed = true;
586 self.arrange_fields();
587 }
588 }
589 }
590 }
591 return true;
592 }
593 }
594 false
595
596 }
597 Event::PointerMove { x, y, .. } => {
598 let (px, py_event) = (*x, *y);
599 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
600 let mut changed = false;
601 let track_x = self.base.x + 10.0;
602 let track_w = self.base.w - 20.0;
603
604 if self.is_dragging_key {
605 if let Some(idx) = self.selected_key_idx {
606 let t = ((px - track_x) / track_w).clamp(0.0, 1.0);
607 self.keys[idx].pos = t;
608 self.sort_keys();
609 changed = true;
610 }
611 }
612
613 if self.selected_key_idx.is_some() {
614 if self.r_slider.cursor_moved(px, py_event, ui) {
615 if let Some(idx) = self.selected_key_idx {
616 self.keys[idx].color[0] = self.r_slider.inner().value();
617 changed = true;
618 }
619 }
620 if self.g_slider.cursor_moved(px, py_event, ui) {
621 if let Some(idx) = self.selected_key_idx {
622 self.keys[idx].color[1] = self.g_slider.inner().value();
623 changed = true;
624 }
625 }
626 if self.b_slider.cursor_moved(px, py_event, ui) {
627 if let Some(idx) = self.selected_key_idx {
628 self.keys[idx].color[2] = self.b_slider.inner().value();
629 changed = true;
630 }
631 }
632 if self.del_button.cursor_moved(px, py_event, ui) {
633 changed = true;
634 }
635 }
636 if changed {
637 self.just_changed = true;
638 }
639 changed
640
641 }
642 Event::MouseWheel { delta, x, y, .. } => {
643 // Wheel forwarding (6bd self-routing): with the field widgets no longer
644 // tree-linked, the sliders' wheel rides this arm — and the key color syncs
645 // immediately (the old descent path left it stale until the next hover flip).
646 let (delta, px, py) = (delta.clone(), *x, *y);
647 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
648 if self.selected_key_idx.is_none() {
649 return false;
650 }
651 let mut changed = false;
652 if self.r_slider.mouse_wheel(&delta, px, py, ui) {
653 if let Some(idx) = self.selected_key_idx {
654 self.keys[idx].color[0] = self.r_slider.inner().value();
655 }
656 changed = true;
657 }
658 if self.g_slider.mouse_wheel(&delta, px, py, ui) {
659 if let Some(idx) = self.selected_key_idx {
660 self.keys[idx].color[1] = self.g_slider.inner().value();
661 }
662 changed = true;
663 }
664 if self.b_slider.mouse_wheel(&delta, px, py, ui) {
665 if let Some(idx) = self.selected_key_idx {
666 self.keys[idx].color[2] = self.b_slider.inner().value();
667 }
668 changed = true;
669 }
670 if changed {
671 self.just_changed = true;
672 }
673 changed
674 }
675 Event::KeyInput(event) => {
676 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
677 if ui.is_focused(&self.r_slider) {
678 return self.r_slider.keyboard_input(event, ui);
679 }
680 if ui.is_focused(&self.g_slider) {
681 return self.g_slider.keyboard_input(event, ui);
682 }
683 if ui.is_focused(&self.b_slider) {
684 return self.b_slider.keyboard_input(event, ui);
685 }
686 if ui.is_focused(&self.del_button) {
687 return self.del_button.keyboard_input(event, ui);
688 }
689 false
690
691 }
692 _ => false,
693 }
694 }
695
696 // Field-slider drags forward through the composite (6bd self-routing): the router
697 // records THIS widget as the drag target once a press is handled here, so the hooks
698 // hand DragUpdate to whichever slider armed itself — and sync the key color, which
699 // the old descent path never did mid-drag.
700 fn draggable(&self, _rect: Rect) -> bool {
701 self.is_dragging_key
702 || self.r_slider.is_dragging()
703 || self.g_slider.is_dragging()
704 || self.b_slider.is_dragging()
705 }
706 fn is_dragging(&self) -> bool {
707 self.is_dragging_key
708 || self.r_slider.is_dragging()
709 || self.g_slider.is_dragging()
710 || self.b_slider.is_dragging()
711 }
712 fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
713 let mut changed = false;
714 if self.r_slider.is_dragging() && self.r_slider.drag_update(px, py) {
715 if let Some(idx) = self.selected_key_idx {
716 self.keys[idx].color[0] = self.r_slider.inner().value();
717 }
718 changed = true;
719 }
720 if self.g_slider.is_dragging() && self.g_slider.drag_update(px, py) {
721 if let Some(idx) = self.selected_key_idx {
722 self.keys[idx].color[1] = self.g_slider.inner().value();
723 }
724 changed = true;
725 }
726 if self.b_slider.is_dragging() && self.b_slider.drag_update(px, py) {
727 if let Some(idx) = self.selected_key_idx {
728 self.keys[idx].color[2] = self.b_slider.inner().value();
729 }
730 changed = true;
731 }
732 if changed {
733 self.just_changed = true;
734 }
735 changed
736 }
737 fn drag_end(&mut self) {
738 self.r_slider.drag_end();
739 self.g_slider.drag_end();
740 self.b_slider.drag_end();
741 self.is_dragging_key = false;
742 }
743 }
744
745 impl Ramp {
746 /// The one spacing value the whole control strip uses — matching the
747 /// visible gap between the graph opening and the window's top edge (the
748 /// widget's 10px graph inset plus the host plate's padding).
749 const STRIP_GAP: f32 = 18.0;
750
751 /// The key pad's square well side.
752 const PAD_SIDE: f32 = 64.0;
753
754 /// Vertical reserve under the curve area — the strip stack at the
755 /// uniform STRIP_GAP rhythm (labeled dropdown row, labeled pad row),
756 /// closed by a bottom margin sized so the VISIBLE bottom gap (widget
757 /// margin + host plate padding, ~8) lands on STRIP_GAP as well.
758 fn strip_reserve() -> f32 {
759 let strip = Self::label_strip();
760 10.0 + Self::STRIP_GAP + strip + 22.0
761 + Self::STRIP_GAP + strip + Self::PAD_SIDE
762 + 10.0
763 }
764
765 /// Key peg ring stroke centerline radius (the 2px stroke spans ±1px).
766 /// Paint and the grab hit-test share it: a press anywhere inside a ring
767 /// lands on that key.
768 const KEY_RING_R: f32 = 26.0;
769
770 /// The key ring radius on THIS plot: the editor's full ring, shrunk so a
771 /// peg never outgrows the plot it sits in (an inline ramp a control high
772 /// draws pegs a few px across, not 26px discs swallowing the curve).
773 fn key_ring_r(&self) -> f32 {
774 let plot = self.plot_rect();
775 Self::KEY_RING_R.min((plot.height * 0.45).max(4.0))
776 }
777
778 /// Inner margin between the graph opening's walls and the plotted 0..1
779 /// domain, so the 0 and 1 gridlines (and their axis numbers) sit visibly
780 /// inside the opening instead of on the walls.
781 const PLOT_INSET: f32 = 22.0;
782
783 /// The plot rect: where the ramp's 0..1 × 0..1 domain maps on screen —
784 /// the graph opening inset by [`PLOT_INSET`](Self::PLOT_INSET). Every
785 /// t/value ↔ pixel mapping (paint and input alike) goes through this.
786 fn plot_rect(&self) -> Rect {
787 let gh = self.graph_h();
788 Rect {
789 x: self.base.x + 10.0 + Self::PLOT_INSET,
790 y: self.base.y + 10.0 + Self::PLOT_INSET,
791 width: (self.base.w - 20.0 - 2.0 * Self::PLOT_INSET).max(1.0),
792 height: (gh - 2.0 * Self::PLOT_INSET).max(1.0),
793 }
794 }
795
796 /// Neighbor resistance (drag), in track units: the soft wall starts
797 /// RESIST_ZONE before a neighbor's position, and pushing the cursor
798 /// RESIST_BREAK past the neighbor breaks through.
799 const RESIST_ZONE: f32 = 0.10;
800 const RESIST_BREAK: f32 = 0.16;
801
802 /// Where a drag whose cursor sits at `t_raw` actually puts key `idx`:
803 /// 1:1 tracking until the cursor enters a neighbor's resistance zone,
804 /// then the key compresses toward the neighbor with growing resistance
805 /// (slope 1 at the zone edge, flattening at the wall), and once the
806 /// cursor overshoots the neighbor by RESIST_BREAK the key pops through —
807 /// the crossing completes and tracking is free again.
808 fn resisted_pos(&self, idx: usize, t_raw: f32) -> f32 {
809 let cur = self.keys[idx].pos;
810 if t_raw > cur {
811 if let Some(next) = self.keys.get(idx + 1) {
812 return Self::soft_wall(t_raw, next.pos, 1.0);
813 }
814 } else if idx > 0 {
815 return Self::soft_wall(t_raw, self.keys[idx - 1].pos, -1.0);
816 }
817 t_raw
818 }
819
820 /// Restore sort order after `keys[i]` changed position, by adjacent
821 /// swaps, and return the key's new index. Exact identity tracking —
822 /// `sort_keys`' float-pos re-match misidentifies the selection when the
823 /// dragged key sits within ε of the key it is passing (leftward
824 /// crossings flipped the selection onto the passed key).
825 fn resettle_key(&mut self, mut i: usize) -> usize {
826 while i + 1 < self.keys.len() && self.keys[i].pos > self.keys[i + 1].pos {
827 self.keys.swap(i, i + 1);
828 i += 1;
829 }
830 while i > 0 && self.keys[i].pos < self.keys[i - 1].pos {
831 self.keys.swap(i, i - 1);
832 i -= 1;
833 }
834 i
835 }
836
837 /// A key's rolled edge: the disc's own surface curving away at the
838 /// perimeter — NOT a separate border. Each sub-arc blends radially from
839 /// the surface color at the band's inner edge (continuing the flat top
840 /// seamlessly), through a half-rolled tint, to the silhouette — which
841 /// leans toward the light on the lit side and falls into shadow opposite,
842 /// and runs denser than the top the way a glass edge reads. `r` is the
843 /// outer-edge radius; `base`/`top_alpha` are the disc's surface color.
844 #[allow(clippy::too_many_arguments)]
845 fn rolled_rim_arc(
846 pc: &mut PaintCtx,
847 cx: f32,
848 cy: f32,
849 r: f32,
850 thickness: f32,
851 start: f32,
852 end: f32,
853 az: f32,
854 base: [f32; 3],
855 top_alpha: f32,
856 ) {
857 let sweep = end - start;
858 let steps = ((sweep.abs() / 0.18).ceil() as usize).max(1);
859 let tint = |sv: f32, k: f32| -> [f32; 3] {
860 [
861 (base[0] + k * sv).clamp(0.0, 1.0),
862 (base[1] + k * sv).clamp(0.0, 1.0),
863 (base[2] + k * sv).clamp(0.0, 1.0),
864 ]
865 };
866 for i in 0..steps {
867 let a0 = start + sweep * i as f32 / steps as f32;
868 let a1 = start + sweep * (i + 1) as f32 / steps as f32;
869 let sv = ((a0 + a1) / 2.0 + az).cos();
870 let mid = tint(sv, 0.20);
871 let edge = tint(sv, 0.38);
872 let mid_a = (top_alpha + 0.78) / 2.0;
873 pc.arc_shaded(
874 cx,
875 cy,
876 r,
877 thickness,
878 a0,
879 a1,
880 [base[0], base[1], base[2], top_alpha],
881 [mid[0], mid[1], mid[2], mid_a],
882 [edge[0], edge[1], edge[2], 0.78],
883 );
884 }
885 }
886
887 /// Apply the key pad's two axes to the selected key: x is the key's
888 /// track position (order restored by adjacent swaps), y its value.
889 fn apply_pad_to_selected(&mut self) {
890 let Some(idx) = self.selected_key_idx else { return };
891 self.keys[idx].pos = self.key_pad.inner().value_x();
892 self.keys[idx].value = self.key_pad.inner().value_y();
893 let settled = self.resettle_key(idx);
894 self.selected_key_idx = Some(settled);
895 self.preset_dropdown.selected = 0; // Custom
896 self.just_changed = true;
897 }
898
899 /// One soft wall at `wall`, approached along direction `s` (±1). Maps the
900 /// cursor's depth into the zone onto the zone's width with an ease that
901 /// reaches the wall exactly at breakthrough depth — continuous at the
902 /// zone edge, asymptotically stiff at the wall, then a `RESIST_BREAK`
903 /// pop as the mapping hands back to 1:1 tracking.
904 fn soft_wall(t_raw: f32, wall: f32, s: f32) -> f32 {
905 let entry = wall - s * Self::RESIST_ZONE;
906 let depth = s * (t_raw - entry);
907 let full = Self::RESIST_ZONE + Self::RESIST_BREAK;
908 if depth <= 0.0 || depth >= full {
909 return t_raw; // outside the zone, or broken through
910 }
911 let k = full / Self::RESIST_ZONE;
912 let g = 1.0 - (1.0 - depth / full).powf(k);
913 entry + s * Self::RESIST_ZONE * g
914 }
915
916 /// The curve area's height: the widget minus the control strip — or,
917 /// with the controls collapsed (context-menu toggle), minus just the
918 /// top/bottom insets, the graph claiming the strip's space.
919 fn graph_h(&self) -> f32 {
920 if self.controls_collapsed {
921 (self.base.h - 20.0).max(30.0)
922 } else {
923 (self.base.h - Self::strip_reserve()).max(30.0)
924 }
925 }
926
927 /// The detached-label strip height the labeled dropdowns carry
928 /// (`Widget::label_offset`'s formula).
929 pub fn label_strip() -> f32 {
930 crate::layout::control_label_strip()
931 }
932
933 /// Lay out the control strip under the curve area. One rhythm: the label
934 /// tabs sit STRIP_GAP under the graph and every other gap shares the
935 /// same rhythm, all columns one shared height on one shared baseline. The labeled dropdowns
936 /// get rects that INCLUDE their label strip (the adapter carves it off the
937 /// content); the unlabeled columns get the content band only. The preset
938 /// column takes the wider share — its options are the strip's longest
939 /// strings and used to clip.
940 fn arrange_fields(&mut self) {
941 let (x, y, w, h) = (self.base.x, self.base.y, self.base.w, self.base.h);
942 if self.controls_collapsed {
943 self.preset_dropdown.set_rect(-1000.0, -1000.0, 0.0, 0.0);
944 self.line_type_dropdown.set_rect(-1000.0, -1000.0, 0.0, 0.0);
945 self.key_pad.set_rect(-1000.0, -1000.0, 0.0, 0.0);
946 self.del_button.set_rect(-1000.0, -1000.0, 0.0, 0.0);
947 let _ = (x, y, w, h);
948 return;
949 }
950 let gh = self.graph_h();
951 let graph_bottom = y + 10.0 + gh;
952 let ctrl_h = 22.0;
953 let strip = Self::label_strip();
954 let gap = Self::STRIP_GAP;
955 // One rhythm: every gap in the strip — graph to label tab, row to
956 // row, columns, pad to button — is STRIP_GAP.
957 let ctrl_y = graph_bottom + gap + strip;
958 let (dd_y, dd_h) = (ctrl_y - strip, ctrl_h + strip);
959 let track_x = x + 10.0;
960 let track_w = w - 20.0;
961
962 if self.selected_key_idx.is_some() {
963 // Selected: the dropdowns keep their full-width row, and a second
964 // row below carries the square key pad (pos × value) with the
965 // delete button beside it, centered on the pad's well.
966 let pad_side = Self::PAD_SIDE;
967 let del_w: f32 = if self.del_button.inner().has_icon() { ctrl_h } else { 64.0 };
968 let pre_w = ((track_w - gap) * 0.58).max(40.0);
969 let line_w = (track_w - gap - pre_w).max(40.0);
970 self.preset_dropdown.set_rect(track_x, dd_y, pre_w, dd_h);
971 self.line_type_dropdown.set_rect(track_x + pre_w + gap, dd_y, line_w, dd_h);
972 let row2_y = ctrl_y + ctrl_h + gap;
973 self.key_pad.set_rect(track_x, row2_y, pad_side, pad_side + strip);
974 self.del_button.set_rect(
975 track_x + pad_side + gap,
976 row2_y + strip + (pad_side - ctrl_h) / 2.0,
977 del_w,
978 ctrl_h,
979 );
980 } else {
981 // Two columns, preset the wider share.
982 let pre_w = ((track_w - gap) * 0.58).max(40.0);
983 let line_w = (track_w - gap - pre_w).max(40.0);
984 self.preset_dropdown.set_rect(track_x, dd_y, pre_w, dd_h);
985 self.line_type_dropdown.set_rect(track_x + pre_w + gap, dd_y, line_w, dd_h);
986 self.key_pad.set_rect(-1000.0, -1000.0, 0.0, 0.0);
987 self.del_button.set_rect(-1000.0, -1000.0, 0.0, 0.0);
988 }
989 }
990 }
991
992 impl Layout for Ramp {
993 fn intrinsic_size(&self) -> Option<Size> {
994 Some(Size::new(0.0, 150.0))
995 }
996
997 fn rect_assigned(&mut self, rect: Rect) {
998 self.base.x = rect.x;
999 self.base.y = rect.y;
1000 self.base.w = rect.width;
1001 self.base.h = rect.height;
1002 self.arrange_fields();
1003 }
1004
1005 // register_embedded_children: gone entirely (6bd self-routing): the fields need no
1006 // eager registry presence — focus setters self-register on demand (6bc), the composite
1007 // itself covers the spatial grid, and an eagerly-registered child DROPDOWN's open
1008 // popover made `is_coordinate_covered` occlude the composite's own hit gate (the
1009 // exclusion is exact-id only), which is why preset-item clicks never landed.
1010 }
1011
1012 impl Paint for Ramp {
1013 fn color(&self) -> [f32; 4] {
1014 [0.15, 0.15, 0.18, 1.0]
1015 }
1016
1017 fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
1018 self.preset_dropdown.popover_rect()
1019 .or_else(|| self.line_type_dropdown.popover_rect())
1020
1021 }
1022
1023 fn draw_popover(&self, _rect: Rect, pc: &mut dyn crate::layout::RenderTarget) {
1024 self.preset_dropdown.render_popover(pc);
1025 self.line_type_dropdown.render_popover(pc);
1026
1027 }
1028
1029 // Field children are ctx-linked for event propagation but painted here — the walk
1030 // must not also descend (the legacy own-labels rule, now with the children too).
1031 fn paints_own_subtree(&self) -> bool {
1032 true
1033 }
1034
1035 fn paint(&self, _rect: Rect, pc: &mut PaintCtx) {
1036 // No container box: the controls sit directly on the host's plate, and
1037 // the graph area reads as an OPENING cut through it — a dark floor
1038 // behind the plate, with the recess wall (drawn after the content, so
1039 // its shading falls across the graph's edges) as the cut's bevel.
1040 let graph = {
1041 let gh = self.graph_h();
1042 Rect { x: self.base.x + 10.0, y: self.base.y + 10.0, width: self.base.w - 20.0, height: gh }
1043 };
1044 let graph_radius = 6.0f32;
1045 pc.rounded_rect(
1046 graph,
1047 graph_radius,
1048 (true, true, true, true),
1049 [0.08, 0.08, 0.10, 1.0],
1050 );
1051
1052 let quads: Vec<(f32, f32, f32, f32, [f32; 4])> = {
1053 let mut quads = Vec::new();
1054 let plot = self.plot_rect();
1055
1056 // Grid lines over the plotted 0..1 domain — 0 and 1 included, sitting
1057 // inside the opening (the plot is inset from the walls).
1058 for ratio in [0.0, 0.25, 0.5, 0.75, 1.0] {
1059 let gy = plot.y + plot.height * (1.0 - ratio);
1060 quads.push((plot.x, gy, plot.width, 1.0, [0.25, 0.25, 0.28, 0.5]));
1061 let gx = plot.x + plot.width * ratio;
1062 quads.push((gx, plot.y, 1.0, plot.height, [0.25, 0.25, 0.28, 0.5]));
1063 }
1064
1065 // Curve area fill: translucent columns under the curve. The outline is
1066 // a real vector polyline below — these only tint the area. Columns
1067 // share exact edges (overlap double-blends a translucent fill into
1068 // visible banding; found the hard way).
1069 let slices = 200;
1070 for i in 0..slices {
1071 let t1 = i as f32 / slices as f32;
1072 let x0 = plot.x + t1 * plot.width;
1073 let x1 = plot.x + (i + 1) as f32 / slices as f32 * plot.width;
1074 let v1 = self.get_interpolated_value(t1);
1075
1076 let slice_h = v1 * plot.height;
1077 let sy = plot.y + plot.height - slice_h;
1078 // Faint on purpose: the graph reads as a dark opening behind the
1079 // plate — a strong fill floods the floor and flattens the depth.
1080 quads.push((x0, sy, x1 - x0, slice_h, [0.25, 0.40, 0.55, 0.10]));
1081 }
1082
1083 quads
1084
1085 };
1086 for (qx, qy, qw, qh, qc) in quads {
1087 pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
1088 }
1089
1090 // Axis numbers on the gridlines — small, dim, part of the graph
1091 // floor (under the curve and keys, inside the opening). They sit in
1092 // the wall-side gutters the plot inset leaves free.
1093 let plot = self.plot_rect();
1094 let num_color = [0x84u8, 0x84, 0x92];
1095 for ratio in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
1096 let gy = plot.y + plot.height * (1.0 - ratio);
1097 pc.text_with(
1098 format!("{ratio:.2}"),
1099 graph.x + 5.0,
1100 gy - 11.0,
1101 9.0,
1102 num_color,
1103 Some("monospace".to_string()),
1104 None,
1105 );
1106 let gx = plot.x + plot.width * ratio;
1107 pc.text_with(
1108 format!("{ratio:.2}"),
1109 gx - 11.0,
1110 graph.y + graph.height - 13.0,
1111 9.0,
1112 num_color,
1113 Some("monospace".to_string()),
1114 None,
1115 );
1116 }
1117
1118 // The curve itself: one anti-aliased round-capped polyline — exact
1119 // key-to-key segments in linear mode, dense samples under smoothstep
1120 // blending. Constant-value extensions reach the plot's 0/1 edges.
1121 let curve_color = [0.5, 0.75, 1.0, 1.0];
1122 let px_of = |t: f32, v: f32| {
1123 (plot.x + t * plot.width, plot.y + plot.height * (1.0 - v))
1124 };
1125 let mut pts: Vec<(f32, f32)> = Vec::new();
1126 if self.line_type_dropdown.selected == 1 {
1127 let n = 64;
1128 for i in 0..=n {
1129 let t = i as f32 / n as f32;
1130 pts.push(px_of(t, self.get_interpolated_value(t)));
1131 }
1132 } else {
1133 if let Some(first) = self.keys.first() {
1134 if first.pos > 0.0 {
1135 pts.push(px_of(0.0, first.value));
1136 }
1137 }
1138 for k in &self.keys {
1139 pts.push(px_of(k.pos, k.value));
1140 }
1141 if let Some(last) = self.keys.last() {
1142 if last.pos < 1.0 {
1143 pts.push(px_of(1.0, last.value));
1144 }
1145 }
1146 }
1147 for pair in pts.windows(2) {
1148 pc.vector(pair[0].0, pair[0].1, pair[1].0, pair[1].1, 2.0, curve_color, Cap::Round);
1149 }
1150 // Key pegs: glassy translucent fills (solid when selected) in thin
1151 // white rings. Overlapping pegs render as foam cells: each pair's
1152 // shared wall is the chord through the two points where the ring
1153 // circles cross (equal radii, so it lies on the perpendicular
1154 // bisector of the centers); rings are cut at the wall, the wall is
1155 // stroked once, and each fill keeps to its own side.
1156 {
1157 let plot = self.plot_rect();
1158 let ring_r = self.key_ring_r(); // roll-band centerline
1159 // The disc surface: flat top out to the roll band's inner edge,
1160 // then the rolled perimeter out to ring_r + 2.5. Band and rim
1161 // shrink with the ring so a small peg keeps a flat top.
1162 let base = [0.5f32, 0.75, 1.0];
1163 let fill_r = (ring_r - 3.0).max(ring_r * 0.5);
1164 let rim_t = 6.0f32.min(ring_r * 0.25).max(1.0);
1165 // Bevel light: the DE light azimuth the plate shading uses.
1166 let az = crate::layout::light_source_position();
1167 let tau = std::f32::consts::TAU;
1168
1169 let centers: Vec<(f32, f32)> = self
1170 .keys
1171 .iter()
1172 .map(|k| (plot.x + k.pos * plot.width, plot.y + plot.height * (1.0 - k.value)))
1173 .collect();
1174
1175 // Every intersecting pair: wall midpoint M + unit normal n toward
1176 // the neighbor per key, and the chord endpoints once per pair.
1177 let mut cuts: Vec<Vec<((f32, f32), (f32, f32))>> = vec![Vec::new(); centers.len()];
1178 let mut walls: Vec<((f32, f32), (f32, f32), (f32, f32))> = Vec::new();
1179 for i in 0..centers.len() {
1180 for j in (i + 1)..centers.len() {
1181 let (dx, dy) = (centers[j].0 - centers[i].0, centers[j].1 - centers[i].1);
1182 let d = (dx * dx + dy * dy).sqrt();
1183 if d < 1e-3 || d >= 2.0 * ring_r {
1184 continue;
1185 }
1186 let n = (dx / d, dy / d);
1187 let m =
1188 ((centers[i].0 + centers[j].0) / 2.0, (centers[i].1 + centers[j].1) / 2.0);
1189 cuts[i].push((m, n));
1190 cuts[j].push((m, (-n.0, -n.1)));
1191 let h = (ring_r * ring_r - (d / 2.0) * (d / 2.0)).sqrt();
1192 walls.push((
1193 (m.0 - h * n.1, m.1 + h * n.0),
1194 (m.0 + h * n.1, m.1 - h * n.0),
1195 n,
1196 ));
1197 }
1198 }
1199
1200 // Fills. Uncut: one disc. Cut: the cell — vertical strips bounded
1201 // by the wall half-planes, the round edge from the circle clip.
1202 for (idx, &(cx, cy)) in centers.iter().enumerate() {
1203 let selected = Some(idx) == self.selected_key_idx;
1204 let fill = [base[0], base[1], base[2], if selected { 0.85 } else { 0.22 }];
1205 if cuts[idx].is_empty() {
1206 pc.circle(cx, cy, fill_r, fill);
1207 continue;
1208 }
1209 pc.push_clip_circle([cx, cy, fill_r]);
1210 let step = 1.5f32;
1211 let mut x = cx - fill_r;
1212 while x < cx + fill_r {
1213 let mid = x + step / 2.0;
1214 let (mut ylo, mut yhi) = (cy - fill_r, cy + fill_r);
1215 let mut visible = true;
1216 for &((mx, my), (nx, ny)) in &cuts[idx] {
1217 // Keep (p − M)·n ≤ 0 — this key's side of the wall.
1218 let c = nx * (mid - mx);
1219 if ny.abs() < 1e-4 {
1220 if c > 0.0 {
1221 visible = false;
1222 break;
1223 }
1224 } else {
1225 let yb = my - c / ny;
1226 if ny > 0.0 {
1227 yhi = yhi.min(yb);
1228 } else {
1229 ylo = ylo.max(yb);
1230 }
1231 }
1232 }
1233 if visible && ylo < yhi {
1234 pc.quad(Rect { x, y: ylo, width: step, height: yhi - ylo }, fill);
1235 }
1236 x += step;
1237 }
1238 pc.pop_clip_circle();
1239 }
1240
1241 // Walls: the shared boundary as the surface rolling into the
1242 // seam and back out — surface-tinted slopes (lit side leans to
1243 // the light, far side into shadow) around a slightly lifted
1244 // crest, in the discs\' own color like the rims.
1245 let (lx, ly) = (az.cos(), -az.sin());
1246 let wall_tint = |sv: f32, k: f32| -> [f32; 3] {
1247 [
1248 (base[0] + k * sv).clamp(0.0, 1.0),
1249 (base[1] + k * sv).clamp(0.0, 1.0),
1250 (base[2] + k * sv).clamp(0.0, 1.0),
1251 ]
1252 };
1253 for &((x1, y1), (x2, y2), (nx, ny)) in &walls {
1254 let facing = nx * lx + ny * ly;
1255 let cp = wall_tint(facing, 0.38);
1256 let cm = wall_tint(-facing, 0.38);
1257 let cc = wall_tint(facing, 0.12);
1258 pc.vector(
1259 x1 + nx * 1.6, y1 + ny * 1.6, x2 + nx * 1.6, y2 + ny * 1.6,
1260 1.6, [cp[0], cp[1], cp[2], 0.78], Cap::Round,
1261 );
1262 pc.vector(
1263 x1 - nx * 1.6, y1 - ny * 1.6, x2 - nx * 1.6, y2 - ny * 1.6,
1264 1.6, [cm[0], cm[1], cm[2], 0.78], Cap::Round,
1265 );
1266 pc.vector(x1, y1, x2, y2, 1.8, [cc[0], cc[1], cc[2], 0.85], Cap::Round);
1267 }
1268
1269 // Rims: beveled circles minus the angular span facing each wall
1270 // (no drawn border — the shaded edge IS the ring).
1271 for (idx, &(cx, cy)) in centers.iter().enumerate() {
1272 let top_a = if Some(idx) == self.selected_key_idx { 0.85 } else { 0.22 };
1273 if cuts[idx].is_empty() {
1274 Self::rolled_rim_arc(pc, cx, cy, ring_r + 2.5, rim_t, 0.0, tau, az, base, top_a);
1275 continue;
1276 }
1277 // Excluded spans [θ−α, θ+α] toward each neighbor, normalized
1278 // into [0, τ) (wrapping spans split), then merged.
1279 let mut segs: Vec<(f32, f32)> = Vec::new();
1280 for &((mx, my), (nx, ny)) in &cuts[idx] {
1281 let theta = ny.atan2(nx);
1282 let half = (mx - cx) * nx + (my - cy) * ny;
1283 let alpha = (half / ring_r).clamp(-1.0, 1.0).acos();
1284 let (a, b) = ((theta - alpha).rem_euclid(tau), (theta + alpha).rem_euclid(tau));
1285 if a <= b {
1286 segs.push((a, b));
1287 } else {
1288 segs.push((a, tau));
1289 segs.push((0.0, b));
1290 }
1291 }
1292 segs.sort_by(|p, q| p.0.partial_cmp(&q.0).unwrap());
1293 let mut merged: Vec<(f32, f32)> = Vec::new();
1294 for s in segs {
1295 match merged.last_mut() {
1296 Some(last) if s.0 <= last.1 => last.1 = last.1.max(s.1),
1297 _ => merged.push(s),
1298 }
1299 }
1300 // Stroke the complement (the two pieces meeting at θ=0 join
1301 // seamlessly when no span covers 0).
1302 let mut prev = 0.0f32;
1303 for &(a, b) in &merged {
1304 if a > prev + 1e-3 {
1305 Self::rolled_rim_arc(pc, cx, cy, ring_r + 2.5, rim_t, prev, a, az, base, top_a);
1306 }
1307 prev = prev.max(b);
1308 }
1309 if prev < tau - 1e-3 {
1310 Self::rolled_rim_arc(pc, cx, cy, ring_r + 2.5, rim_t, prev, tau, az, base, top_a);
1311 }
1312 }
1313 }
1314 // The opening's cut edge: drawn after the graph content so the wall's
1315 // shading falls across the curve and keys where they pass behind the
1316 // plate's rim. Nested translucent border rings first — the contact
1317 // shadow the plate casts down into the opening — then the recess wall
1318 // itself as the cut's bevel.
1319 let radii = (graph_radius, graph_radius, graph_radius, graph_radius);
1320 for (t, a) in [(7.0, 0.08), (4.0, 0.10), (2.0, 0.14)] {
1321 pc.border(graph, radii, [0.0; 4], [0.0, 0.0, 0.0, a], t);
1322 }
1323 let depth = crate::layout::bevel_width().min(graph.height * 0.2);
1324 let (well, radii) = crate::layout::carve_inside(graph, radii, depth);
1325 pc.recess(well, radii, depth);
1326 if !self.controls_collapsed {
1327 let dummy = UiContext::new();
1328 self.preset_dropdown.paint_self(&dummy, pc);
1329 self.line_type_dropdown.paint_self(&dummy, pc);
1330 if self.selected_key_idx.is_some() {
1331 self.key_pad.paint_self(&dummy, pc);
1332 self.del_button.paint_self(&dummy, pc);
1333 }
1334 }
1335 }
1336 }
1337
1338 impl Input for Ramp {
1339 fn wants_tick(&self) -> bool {
1340 true
1341 }
1342
1343 /// The graph context menu's actions. Overriding loses the trait-default
1344 /// clipboard arms, so Copy/Paste (the spec string) are restated here.
1345 fn context_action(&mut self, action: ContextAction) -> bool {
1346 match action {
1347 ContextAction::ToggleRampControls => {
1348 self.controls_collapsed = !self.controls_collapsed;
1349 self.just_changed = true;
1350 self.arrange_fields();
1351 true
1352 }
1353 ContextAction::Copy => {
1354 crate::widget::clipboard::copy_to_clipboard(&self.spec_string());
1355 true
1356 }
1357 ContextAction::Paste => {
1358 if let Some(text) = crate::widget::clipboard::read_from_clipboard() {
1359 let changed = self.set_spec(&text);
1360 if changed {
1361 self.just_changed = true;
1362 }
1363 changed
1364 } else {
1365 false
1366 }
1367 }
1368 _ => false,
1369 }
1370 }
1371
1372 /// The curve as a ramp spec string ([`format_ramp_spec`]) — the value hosts
1373 /// poll and persist for ramp-valued params.
1374 fn value_string(&self) -> Option<String> {
1375 Some(self.spec_string())
1376 }
1377
1378 fn set_value_string(&mut self, val: &str) -> bool {
1379 self.set_spec(val)
1380 }
1381
1382 /// The open dropdown popover extends the hit area (the 5p Dropdown pattern).
1383 fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
1384 if let Some((px, py, pw, ph)) = {
1385 self.preset_dropdown.popover_rect()
1386 .or_else(|| self.line_type_dropdown.popover_rect())
1387
1388 } {
1389 if x >= px && x <= px + pw && y >= py && y <= py + ph {
1390 return true;
1391 }
1392 }
1393 x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
1394 }
1395
1396 fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
1397 // (The per-tick field-widget re-parenting is gone, 6bd: it was a dummy-ctx
1398 // `set_parent` whose every effect was discarded — legacy behaved the same.)
1399 let Some(ui) = ectx.ui.as_deref_mut() else {
1400 return false;
1401 };
1402 let mut changed = self.just_changed;
1403 self.just_changed = false;
1404
1405 // Hover-scroll inertia: once the finger stream stops (>60ms without
1406 // an event), the latched key coasts on the estimated velocity with
1407 // exponential decay, still resettling and syncing like live scrolls.
1408 if let (Some(idx), Some(last)) = (self.scroll_key_idx, self.last_key_scroll) {
1409 if last.elapsed().as_secs_f32() > 0.06 && idx < self.keys.len() {
1410 let (vx, vy) = self.scroll_vel;
1411 // Animations off: the key stops where the scroll left it.
1412 if (vx.abs() > 0.02 || vy.abs() > 0.02) && crate::motion::enabled() {
1413 self.keys[idx].pos = (self.keys[idx].pos + vx * dt).clamp(0.0, 1.0);
1414 self.keys[idx].value = (self.keys[idx].value + vy * dt).clamp(0.0, 1.0);
1415 let settled = self.resettle_key(idx);
1416 self.scroll_key_idx = Some(settled);
1417 self.selected_key_idx = Some(settled);
1418 self.key_pad
1419 .set_values(self.keys[settled].pos, self.keys[settled].value);
1420 self.preset_dropdown.selected = 0; // Custom
1421 let f = (-5.0 * dt).exp();
1422 self.scroll_vel = (vx * f, vy * f);
1423 changed = true;
1424 } else {
1425 self.scroll_vel = (0.0, 0.0);
1426 self.last_key_scroll = None;
1427 }
1428 }
1429 }
1430
1431 if self.preset_dropdown.tick(dt, ui) {
1432 let idx = self.preset_dropdown.selected;
1433 self.apply_preset(idx);
1434 changed = true;
1435 }
1436
1437 if self.line_type_dropdown.tick(dt, ui) {
1438 changed = true;
1439 }
1440
1441 if self.selected_key_idx.is_some() {
1442 if self.key_pad.tick(dt, ui) {
1443 self.apply_pad_to_selected();
1444 changed = true;
1445 }
1446 if self.del_button.tick(dt, ui) {
1447 self.preset_dropdown.selected = 0; // Custom
1448 changed = true;
1449 }
1450 }
1451 changed
1452
1453 }
1454
1455 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
1456 match event {
1457 Event::MouseButton { button, state, x, y, .. } => {
1458 let (button, state, px, py_event) = (*button, *state, *x, *y);
1459 // Right-press in the graph opening → the shared context menu
1460 // (the key-crossing toggle lives there). Before the ui borrow:
1461 // open_context_menu needs the whole EventCtx.
1462 if button == MouseButton::Right {
1463 if state == ElementState::Pressed {
1464 let gh = self.graph_h();
1465 let gx = self.base.x + 10.0;
1466 let gw = self.base.w - 20.0;
1467 let gy = self.base.y + 10.0;
1468 if px >= gx && px <= gx + gw && py_event >= gy && py_event <= gy + gh {
1469 ectx.open_context_menu(px, py_event);
1470 return true;
1471 }
1472 }
1473 return false;
1474 }
1475 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1476 if button != MouseButton::Left { return false; }
1477
1478 if self.preset_dropdown.mouse_input(button, state, px, py_event, ui) {
1479 if self.preset_dropdown.take_change() {
1480 let idx = self.preset_dropdown.selected;
1481 self.apply_preset(idx);
1482 }
1483 return true;
1484 }
1485
1486 if self.line_type_dropdown.mouse_input(button, state, px, py_event, ui) {
1487 return true;
1488 }
1489
1490 let gh = self.graph_h();
1491 let plot = self.plot_rect();
1492
1493 if state == ElementState::Pressed {
1494 // Any press cancels a hover-scroll glide in progress.
1495 self.scroll_vel = (0.0, 0.0);
1496 self.scroll_key_idx = None;
1497 self.last_key_scroll = None;
1498 // Grab the NEAREST key whose ring contains the press — the rings
1499 // are the pegs' visual extent, and nearest-center also matches the
1500 // foam walls (perpendicular bisectors) where rings overlap.
1501 let hit_r = self.key_ring_r() + 2.5;
1502 let mut best: Option<(usize, f32)> = None;
1503 for (idx, key) in self.keys.iter().enumerate() {
1504 let cx = plot.x + key.pos * plot.width;
1505 let cy = plot.y + plot.height * (1.0 - key.value);
1506 let dx = px - cx;
1507 let dy = py_event - cy;
1508 let d2 = dx * dx + dy * dy;
1509 if d2 <= hit_r * hit_r && best.is_none_or(|(_, bd)| d2 < bd) {
1510 best = Some((idx, d2));
1511 }
1512 }
1513 if let Some((idx, _)) = best {
1514 self.selected_key_idx = Some(idx);
1515 self.is_dragging_key = true;
1516 self.key_pad.set_values(self.keys[idx].pos, self.keys[idx].value);
1517 self.arrange_fields();
1518 return true;
1519 }
1520
1521 // Creation accepts the whole opening (the inset gutters included);
1522 // the domain mapping clamps to the plot's 0..1.
1523 if px >= self.base.x + 10.0 && px <= self.base.x + self.base.w - 10.0 && py_event >= self.base.y + 10.0 && py_event <= self.base.y + 10.0 + gh {
1524 let t = ((px - plot.x) / plot.width).clamp(0.0, 1.0);
1525 let val = (1.0 - (py_event - plot.y) / plot.height).clamp(0.0, 1.0);
1526 let new_key = RampKey { pos: t, value: val };
1527 self.keys.push(new_key);
1528 let new_idx = self.resettle_key(self.keys.len() - 1);
1529 self.preset_dropdown.selected = 0; // Custom
1530 self.just_changed = true;
1531 self.selected_key_idx = Some(new_idx);
1532 self.key_pad.set_values(t, val);
1533 // Arm the drag: a fresh key follows the pointer until release,
1534 // so create-and-place is one gesture (the grab-branch behavior).
1535 self.is_dragging_key = true;
1536 self.arrange_fields();
1537 return true;
1538 }
1539
1540 if self.selected_key_idx.is_some() {
1541 if self.key_pad.mouse_input(button, state, px, py_event, ui) {
1542 self.apply_pad_to_selected();
1543 return true;
1544 }
1545 if self.del_button.mouse_input(button, state, px, py_event, ui) {
1546 if self.del_button.take_click() {
1547 if let Some(idx) = self.selected_key_idx {
1548 if self.keys.len() > 2 {
1549 self.keys.remove(idx);
1550 self.selected_key_idx = None;
1551 self.preset_dropdown.selected = 0; // Custom
1552 self.just_changed = true;
1553 self.arrange_fields();
1554 }
1555 }
1556 }
1557 return true;
1558 }
1559 }
1560 } else {
1561 self.is_dragging_key = false;
1562 if self.selected_key_idx.is_some() {
1563 self.key_pad.mouse_input(button, state, px, py_event, ui);
1564 if self.del_button.mouse_input(button, state, px, py_event, ui) {
1565 if self.del_button.take_click() {
1566 if let Some(idx) = self.selected_key_idx {
1567 if self.keys.len() > 2 {
1568 self.keys.remove(idx);
1569 self.selected_key_idx = None;
1570 self.preset_dropdown.selected = 0; // Custom
1571 self.just_changed = true;
1572 self.arrange_fields();
1573 }
1574 }
1575 }
1576 }
1577 return true;
1578 }
1579 }
1580 false
1581
1582 }
1583 Event::PointerMove { x, y, .. } => {
1584 let (px, py_event) = (*x, *y);
1585 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1586 if self.preset_dropdown.cursor_moved(px, py_event, ui) {
1587 return true;
1588 }
1589 if self.line_type_dropdown.cursor_moved(px, py_event, ui) {
1590 return true;
1591 }
1592
1593 let mut changed = false;
1594 let plot = self.plot_rect();
1595
1596 if self.is_dragging_key {
1597 if let Some(idx) = self.selected_key_idx {
1598 let t_raw = ((px - plot.x) / plot.width).clamp(0.0, 1.0);
1599 let t = self.resisted_pos(idx, t_raw);
1600 let val = (1.0 - (py_event - plot.y) / plot.height).clamp(0.0, 1.0);
1601 self.keys[idx].pos = t;
1602 self.keys[idx].value = val;
1603 self.key_pad.set_values(t, val);
1604 let settled = self.resettle_key(idx);
1605 self.selected_key_idx = Some(settled);
1606 self.preset_dropdown.selected = 0; // Custom
1607 changed = true;
1608 }
1609 }
1610
1611 if self.selected_key_idx.is_some() {
1612 if self.key_pad.cursor_moved(px, py_event, ui) {
1613 self.apply_pad_to_selected();
1614 changed = true;
1615 }
1616 if self.del_button.cursor_moved(px, py_event, ui) {
1617 changed = true;
1618 }
1619 }
1620 if changed {
1621 self.just_changed = true;
1622 }
1623 changed
1624
1625 }
1626 Event::MouseWheel { delta, x, y, .. } => {
1627 // Wheel forwarding (6bd self-routing): dropdowns first (mirroring the press
1628 // order, incl. the preset drain), then the value slider with the key sync.
1629 let (delta, px, py) = (delta.clone(), *x, *y);
1630 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1631 if self.preset_dropdown.mouse_wheel(&delta, px, py, ui) {
1632 if self.preset_dropdown.take_change() {
1633 let idx = self.preset_dropdown.selected;
1634 self.apply_preset(idx);
1635 }
1636 return true;
1637 }
1638 if self.line_type_dropdown.mouse_wheel(&delta, px, py, ui) {
1639 return true;
1640 }
1641 // Hover-scroll: a gesture STARTING over a key latches it and
1642 // steers it on both axes — following the fingers like a drag
1643 // — until the stream pauses (fingers lifted). Mid-gesture the
1644 // latch holds even if the key slides out from under the
1645 // cursor. Latching also selects the key, so the pad tracks.
1646 let plot = self.plot_rect();
1647 if ui.scroll_gesture_new {
1648 let hit_r = self.key_ring_r() + 2.5;
1649 let mut best: Option<(usize, f32)> = None;
1650 for (idx, key) in self.keys.iter().enumerate() {
1651 let cx = plot.x + key.pos * plot.width;
1652 let cy = plot.y + plot.height * (1.0 - key.value);
1653 let dx = px - cx;
1654 let dy = py - cy;
1655 let d2 = dx * dx + dy * dy;
1656 if d2 <= hit_r * hit_r && best.is_none_or(|(_, bd)| d2 < bd) {
1657 best = Some((idx, d2));
1658 }
1659 }
1660 self.scroll_key_idx = best.map(|(i, _)| i);
1661 self.scroll_vel = (0.0, 0.0);
1662 }
1663 if let Some(idx) = self.scroll_key_idx {
1664 if idx < self.keys.len() {
1665 ui.scroll_initiate_widget_id = Some(ectx.id);
1666 // Damped well below 1:1 — hover-scroll is for fine
1667 // adjustment; the drag paths cover coarse moves.
1668 let (dx, dy) = match &delta {
1669 MouseScrollDelta::LineDelta(x, y) => (*x * 0.005, *y * 0.005),
1670 MouseScrollDelta::PixelDelta(pos) => (
1671 0.2 * pos.x as f32 / plot.width,
1672 0.2 * pos.y as f32 / plot.height,
1673 ),
1674 };
1675 // Direct manipulation: the key moves WITH the scroll
1676 // (runner deltas are content-motion negated, so both
1677 // axes flip): scroll right → key right, down → down.
1678 self.keys[idx].pos = (self.keys[idx].pos - dx).clamp(0.0, 1.0);
1679 self.keys[idx].value = (self.keys[idx].value + dy).clamp(0.0, 1.0);
1680 // Velocity estimate for the release glide: EMA of
1681 // applied delta over inter-event time. A leisurely
1682 // wheel produces negligible velocity (big gaps clamp
1683 // to 0.1s); fast trackpad streams build real speed.
1684 let now = std::time::Instant::now();
1685 let dt_ev = self
1686 .last_key_scroll
1687 .map(|t| now.duration_since(t).as_secs_f32())
1688 .unwrap_or(0.016)
1689 .clamp(0.004, 0.1);
1690 self.last_key_scroll = Some(now);
1691 let (ivx, ivy) = (-dx / dt_ev, dy / dt_ev);
1692 self.scroll_vel = (
1693 self.scroll_vel.0 * 0.65 + ivx * 0.35,
1694 self.scroll_vel.1 * 0.65 + ivy * 0.35,
1695 );
1696 let settled = self.resettle_key(idx);
1697 self.scroll_key_idx = Some(settled);
1698 self.selected_key_idx = Some(settled);
1699 self.key_pad
1700 .set_values(self.keys[settled].pos, self.keys[settled].value);
1701 self.preset_dropdown.selected = 0; // Custom
1702 self.just_changed = true;
1703 self.arrange_fields();
1704 return true;
1705 }
1706 self.scroll_key_idx = None;
1707 }
1708 if self.selected_key_idx.is_some() && self.key_pad.mouse_wheel(&delta, px, py, ui) {
1709 self.apply_pad_to_selected();
1710 return true;
1711 }
1712 false
1713 }
1714 Event::KeyInput(event) => {
1715 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1716 if event.state != ElementState::Pressed { return false; }
1717
1718 if event.logical_key == Key::Named(NamedKey::Tab) {
1719 let is_shift = event.shift;
1720 let self_ptr = self as *mut Self;
1721 let mut children = unsafe {
1722 let mut list = vec![
1723 (*self_ptr).preset_dropdown.as_ptr_mut(),
1724 (*self_ptr).line_type_dropdown.as_ptr_mut(),
1725 ];
1726 if (*self_ptr).selected_key_idx.is_some() {
1727 list.push((*self_ptr).key_pad.as_ptr_mut());
1728 list.push((*self_ptr).del_button.as_ptr_mut());
1729 }
1730 list
1731 };
1732
1733 let mut focused_idx = None;
1734 for (idx, child) in children.iter().enumerate() {
1735 if unsafe { ui.is_focused(&**child) } {
1736 focused_idx = Some(idx);
1737 break;
1738 }
1739 }
1740
1741 if let Some(curr) = focused_idx {
1742 let next_idx = if is_shift {
1743 if curr == 0 { children.len() - 1 } else { curr - 1 }
1744 } else {
1745 (curr + 1) % children.len()
1746 };
1747 unsafe {
1748 ui.set_focused(&mut *children[next_idx]);
1749 }
1750 } else {
1751 unsafe {
1752 ui.set_focused(&mut *children[0]);
1753 }
1754 }
1755 return true;
1756 }
1757
1758 if ui.is_focused(&self.preset_dropdown) {
1759 return self.preset_dropdown.keyboard_input(event, ui);
1760 }
1761 if ui.is_focused(&self.line_type_dropdown) {
1762 return self.line_type_dropdown.keyboard_input(event, ui);
1763 }
1764 if ui.is_focused(&self.key_pad) {
1765 return self.key_pad.keyboard_input(event, ui);
1766 }
1767 if ui.is_focused(&self.del_button) {
1768 return self.del_button.keyboard_input(event, ui);
1769 }
1770 false
1771
1772 }
1773 Event::FocusIn => {
1774 if let Some(ui) = ectx.ui.as_deref_mut() {
1775 ui.set_focused(&mut self.preset_dropdown);
1776 }
1777 false
1778 }
1779 Event::FocusOut => {
1780 self.base.focused = false;
1781 self.preset_dropdown.unfocus();
1782 self.line_type_dropdown.unfocus();
1783 self.key_pad.unfocus();
1784 self.del_button.unfocus();
1785
1786 false
1787 }
1788 _ => false,
1789 }
1790 }
1791
1792 // Field-slider drags forward through the composite (6bd self-routing), with the key
1793 // value sync the old descent path never ran mid-drag.
1794 fn draggable(&self, _rect: Rect) -> bool {
1795 self.is_dragging_key || self.key_pad.is_dragging()
1796 }
1797 fn is_dragging(&self) -> bool {
1798 self.is_dragging_key || self.key_pad.is_dragging()
1799 }
1800 fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
1801 if self.key_pad.is_dragging() && self.key_pad.drag_update(px, py) {
1802 self.apply_pad_to_selected();
1803 return true;
1804 }
1805 false
1806 }
1807 fn drag_end(&mut self) {
1808 self.key_pad.drag_end();
1809 self.is_dragging_key = false;
1810 }
1811 }