git.lucas.co / cce-color-editor
color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git

commit678f999e393a8d2167eada5aa58b5330ff72ea08
parent6898a67952
authorLucas Galante <[email protected]>
date2026-07-12 19:47
refactor: cce-colors on routed events (6bd shrink — direct dispatch retired)

The 6ab shape: one Event per handler through ui_context.propagate_event per
root (apply/cancel buttons gated on expecting_output, then the sliders); the
router owns press hit-gating, Enter/Leave, and the whole drag lifecycle —
the app-held `dragging: Option<usize>` index is deleted; value changes drain
through the sliders' just_changed flags exactly like the (already-routed)
wheel path. Routed drags gain the standard 3px start threshold the other
routed apps share.

Verified live A/B (same pointer sequence per build): static and
click-on-track states byte-identical (AE=0); 3x wheel after click identical
under the 8% mask. NOTE: a single wheel tick right after a re-park+click can
be dropped by the compositor focus-drop class in EITHER build — flake, not a
regression. Held thumb drag is not headlessly drivable — user spot-check.

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

 src/main.rs | 101 ++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 58 insertions(+), 43 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f89ceca..f67a3f5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -347,7 +347,6 @@ struct ColorApp {
 
     cursor_x: f32,
     cursor_y: f32,
-    dragging: Option<usize>,
     width: u32,
     height: u32,
     scale_factor: f64,
@@ -676,7 +675,6 @@ impl Application for ColorApp {
             expecting_output,
             cursor_x: 0.0,
             cursor_y: 0.0,
-            dragging: None,
             width: initial_w,
             height: initial_h,
             scale_factor: 1.0,
@@ -819,25 +817,35 @@ impl Application for ColorApp {
         self.cursor_y = pos.y;
         cce_ui::widget::hover_animation::set_cursor_pos(pos.x, pos.y);
 
-        if let Some(i) = self.dragging {
-            let slider = &mut self.sliders[i];
-            if slider.drag_update(pos.x, pos.y) {
-                let val = slider.value;
-                self.update_color_from_slider(i, val);
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        } else {
-            if self.expecting_output {
-                let _ = self.apply_btn.cursor_moved(pos.x, pos.y, &mut self.ui_context);
-                let _ = self.cancel_btn.cursor_moved(pos.x, pos.y, &mut self.ui_context);
-            }
-            for slider in &mut self.sliders {
-                let _ = slider.cursor_moved(pos.x, pos.y, &mut self.ui_context);
+        // Routed dispatch (6bd shrink): one PointerMove through the router per root —
+        // hover bookkeeping plus the router's drag forwarding (replaces the app-held
+        // dragging index; DragUpdate reaches the drag target even off-rect).
+        let ev = Event::PointerMove { x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
+        if self.expecting_output {
+            let apply = self.apply_btn.as_ptr_mut();
+            self.ui_context.propagate_event(&ev, apply);
+            let cancel = self.cancel_btn.as_ptr_mut();
+            self.ui_context.propagate_event(&ev, cancel);
+        }
+        let slider_ptrs: Vec<_> = self.sliders.iter_mut().map(|s| s.as_ptr_mut()).collect();
+        for ptr in slider_ptrs {
+            self.ui_context.propagate_event(&ev, ptr);
+        }
+        // Drain the drag's value change like the wheel path does.
+        let mut changed_slider = None;
+        for (i, slider) in self.sliders.iter_mut().enumerate() {
+            if slider.just_changed {
+                slider.just_changed = false;
+                changed_slider = Some((i, slider.value));
+                break;
             }
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
         }
+        if let Some((i, val)) = changed_slider {
+            self.update_color_from_slider(i, val);
+        }
+        // Legacy parity: every pointer move rebuilt (hover fades ride the rebuild).
+        *needs_rebuild = true;
+        self.needs_rebuild = true;
     }
 
     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
@@ -845,18 +853,22 @@ impl Application for ColorApp {
             return None;
         }
 
-        let px = pos.x;
-        let py = pos.y;
-
+        // Routed dispatch (6bd shrink): the router hit-gates presses, records the drag
+        // target, and delivers DragEnd on release; the app keeps the take_click /
+        // just_changed drains.
+        let ev = Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
+        let was_dragging = self.ui_context.is_dragging;
         let mut handled = false;
 
         if self.expecting_output {
-            if self.apply_btn.mouse_input(button, state, px, py, &mut self.ui_context) {
+            let apply = self.apply_btn.as_ptr_mut();
+            if self.ui_context.propagate_event(&ev, apply) {
                 handled = true;
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
-            if self.cancel_btn.mouse_input(button, state, px, py, &mut self.ui_context) {
+            let cancel = self.cancel_btn.as_ptr_mut();
+            if self.ui_context.propagate_event(&ev, cancel) {
                 handled = true;
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -870,30 +882,33 @@ impl Application for ColorApp {
             }
         }
 
-        if state == ElementState::Released {
-            if let Some(i) = self.dragging {
-                self.sliders[i].drag_end();
-                self.dragging = None;
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        }
-
         if !handled {
+            let slider_ptrs: Vec<_> = self.sliders.iter_mut().map(|s| s.as_ptr_mut()).collect();
+            for ptr in slider_ptrs {
+                if self.ui_context.propagate_event(&ev, ptr) {
+                    break;
+                }
+            }
+            let mut changed_slider = None;
             for (i, slider) in self.sliders.iter_mut().enumerate() {
-                if slider.mouse_input(button, state, px, py, &mut self.ui_context) {
-                    if slider.dragging {
-                        self.dragging = Some(i);
-                    } else {
-                        self.dragging = None;
-                    }
-                    let val = slider.value;
-                    self.update_color_from_slider(i, val);
-                    *needs_rebuild = true;
-                    self.needs_rebuild = true;
+                if slider.just_changed {
+                    slider.just_changed = false;
+                    changed_slider = Some((i, slider.value));
                     break;
                 }
             }
+            if let Some((i, val)) = changed_slider {
+                self.update_color_from_slider(i, val);
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+
+        // The router delivered DragEnd on the first propagate call of a release; rebuild
+        // so the thumb sheds its dragging state, as the legacy drag_end path did.
+        if state == ElementState::Released && was_dragging {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
         }
 
         None