git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit3747891049d1cd72c75e62cabd06082b85598be7
parentb51792e625
authorLucas Galante <[email protected]>
date2026-07-12 22:21
fix(widget): ramps complete their internal forwarding and unlink (6bd
self-routing, the TreeList rule)

Ramp and ColorRamp gain the two pieces their self-routing was missing —
a MouseWheel arm (dropdowns first with the preset drain, then the sliders
with the key color/value sync the old descent path never ran) and the Input
drag hooks (forwarding DragUpdate to whichever field slider armed itself,
with the same sync; drag_end releases all fields and the key-peg drag) —
then register_embedded_children drops link_ids like TreeList: no tree
children, no children-first descent, no starved composite-level drains.

Verified live A/B (TI --type Ramp, identical sequences): static and
post-preset-click states match (cursor sprites + an 808px hover-ring
cadence delta on the dropdown button — single vs double hover recompute);
popover opens and dismisses; key-peg click selects and re-arranges the
panel through the routed composite press. PRE-EXISTING, baseline-reproduced:
clicking a preset ITEM closes the popover without changing the selection
(both builds — the apply drain never had a working pointer path); recorded,
not chased here. In-situ slider wheel/drag on a selected key: user
spot-check (the val_slider is only laid out post-selection).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/widget/input/ramp.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 139 insertions(+), 8 deletions(-)

diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index ce7b076..2a10a4c 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -374,22 +374,22 @@ impl Layout for ColorRamp {
     }
 
     fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
+        // Registered but NOT tree-linked (6bd self-routing, the TreeList rule): on_event
+        // forwards every event class internally — descent double-delivered and starved
+        // the composite-level sync/drains.
+        let _ = host_id;
         let p = self.r_slider.as_ptr_mut();
         let id = self.r_slider.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.g_slider.as_ptr_mut();
         let id = self.g_slider.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.b_slider.as_ptr_mut();
         let id = self.b_slider.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.del_button.as_ptr_mut();
         let id = self.del_button.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
     }
 }
 
@@ -657,6 +657,39 @@ impl Input for ColorRamp {
         changed
     
             }
+            Event::MouseWheel { delta, x, y, .. } => {
+                // Wheel forwarding (6bd self-routing): with the field widgets no longer
+                // tree-linked, the sliders' wheel rides this arm — and the key color syncs
+                // immediately (the old descent path left it stale until the next hover flip).
+                let (delta, px, py) = (delta.clone(), *x, *y);
+                let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+                if self.selected_key_idx.is_none() {
+                    return false;
+                }
+                let mut changed = false;
+                if self.r_slider.mouse_wheel(&delta, px, py, ui) {
+                    if let Some(idx) = self.selected_key_idx {
+                        self.keys[idx].color[0] = self.r_slider.inner().value();
+                    }
+                    changed = true;
+                }
+                if self.g_slider.mouse_wheel(&delta, px, py, ui) {
+                    if let Some(idx) = self.selected_key_idx {
+                        self.keys[idx].color[1] = self.g_slider.inner().value();
+                    }
+                    changed = true;
+                }
+                if self.b_slider.mouse_wheel(&delta, px, py, ui) {
+                    if let Some(idx) = self.selected_key_idx {
+                        self.keys[idx].color[2] = self.b_slider.inner().value();
+                    }
+                    changed = true;
+                }
+                if changed {
+                    self.just_changed = true;
+                }
+                changed
+            }
             Event::KeyInput(event) => {
                 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
         if ui.is_focused(&self.r_slider) {
@@ -677,6 +710,54 @@ impl Input for ColorRamp {
             _ => false,
         }
     }
+
+    // Field-slider drags forward through the composite (6bd self-routing): the router
+    // records THIS widget as the drag target once a press is handled here, so the hooks
+    // hand DragUpdate to whichever slider armed itself — and sync the key color, which
+    // the old descent path never did mid-drag.
+    fn draggable(&self, _rect: Rect) -> bool {
+        self.is_dragging_key
+            || self.r_slider.is_dragging()
+            || self.g_slider.is_dragging()
+            || self.b_slider.is_dragging()
+    }
+    fn is_dragging(&self) -> bool {
+        self.is_dragging_key
+            || self.r_slider.is_dragging()
+            || self.g_slider.is_dragging()
+            || self.b_slider.is_dragging()
+    }
+    fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
+        let mut changed = false;
+        if self.r_slider.is_dragging() && self.r_slider.drag_update(px, py) {
+            if let Some(idx) = self.selected_key_idx {
+                self.keys[idx].color[0] = self.r_slider.inner().value();
+            }
+            changed = true;
+        }
+        if self.g_slider.is_dragging() && self.g_slider.drag_update(px, py) {
+            if let Some(idx) = self.selected_key_idx {
+                self.keys[idx].color[1] = self.g_slider.inner().value();
+            }
+            changed = true;
+        }
+        if self.b_slider.is_dragging() && self.b_slider.drag_update(px, py) {
+            if let Some(idx) = self.selected_key_idx {
+                self.keys[idx].color[2] = self.b_slider.inner().value();
+            }
+            changed = true;
+        }
+        if changed {
+            self.just_changed = true;
+        }
+        changed
+    }
+    fn drag_end(&mut self) {
+        self.r_slider.drag_end();
+        self.g_slider.drag_end();
+        self.b_slider.drag_end();
+        self.is_dragging_key = false;
+    }
 }
 
 impl Ramp {
@@ -757,22 +838,20 @@ impl Layout for Ramp {
     }
 
     fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
+        // Registered but NOT tree-linked (6bd self-routing, the TreeList rule).
+        let _ = host_id;
         let p = self.preset_dropdown.as_ptr_mut();
         let id = self.preset_dropdown.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.line_type_dropdown.as_ptr_mut();
         let id = self.line_type_dropdown.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.val_slider.as_ptr_mut();
         let id = self.val_slider.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
         let p = self.del_button.as_ptr_mut();
         let id = self.del_button.base().id();
         ctx.register_widget(id, p);
-        ctx.link_ids(host_id, id);
     }
 }
 
@@ -1088,6 +1167,31 @@ impl Input for Ramp {
         changed
     
             }
+            Event::MouseWheel { delta, x, y, .. } => {
+                // Wheel forwarding (6bd self-routing): dropdowns first (mirroring the press
+                // order, incl. the preset drain), then the value slider with the key sync.
+                let (delta, px, py) = (delta.clone(), *x, *y);
+                let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+                if self.preset_dropdown.mouse_wheel(&delta, px, py, ui) {
+                    if self.preset_dropdown.take_change() {
+                        let idx = self.preset_dropdown.selected;
+                        self.apply_preset(idx);
+                    }
+                    return true;
+                }
+                if self.line_type_dropdown.mouse_wheel(&delta, px, py, ui) {
+                    return true;
+                }
+                if self.selected_key_idx.is_some() && self.val_slider.mouse_wheel(&delta, px, py, ui) {
+                    if let Some(idx) = self.selected_key_idx {
+                        self.keys[idx].value = self.val_slider.inner().value();
+                        self.preset_dropdown.selected = 0; // Custom
+                    }
+                    self.just_changed = true;
+                    return true;
+                }
+                false
+            }
             Event::KeyInput(event) => {
                 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
         if event.state != ElementState::Pressed { return false; }
@@ -1165,4 +1269,31 @@ impl Input for Ramp {
             _ => false,
         }
     }
+
+    // Field-slider drags forward through the composite (6bd self-routing), with the key
+    // value sync the old descent path never ran mid-drag.
+    fn draggable(&self, _rect: Rect) -> bool {
+        self.is_dragging_key || self.val_slider.is_dragging()
+    }
+    fn is_dragging(&self) -> bool {
+        self.is_dragging_key || self.val_slider.is_dragging()
+    }
+    fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
+        let mut changed = false;
+        if self.val_slider.is_dragging() && self.val_slider.drag_update(px, py) {
+            if let Some(idx) = self.selected_key_idx {
+                self.keys[idx].value = self.val_slider.inner().value();
+                self.preset_dropdown.selected = 0; // Custom
+            }
+            changed = true;
+        }
+        if changed {
+            self.just_changed = true;
+        }
+        changed
+    }
+    fn drag_end(&mut self) {
+        self.val_slider.drag_end();
+        self.is_dragging_key = false;
+    }
 }