git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commite8e7bac8cb28abc4024faa423f567e1ea17e1ea0
parent44b66a4ed8
authorLucas Galante <[email protected]>
date2026-07-12 20:23
refactor: designer wheel loop on routed events; press/move cascade deferred (6bd)

The roster wheel loop and viewport fallback dispatch through
ui_context.propagate_event, whose preamble replaces the scroll-gesture
bookkeeping this loop hand-rolled (verbatim duplicate). Per-index post-
processing (CONTENT pan/velocity sync, grid sync) is unchanged.

The press/move/keyboard cascade stays on direct dispatch DELIBERATELY,
recorded in an inline deferral note: `drag_widget` doubles as widget-drag
and app-mode-drag (floating-pane edge resizes, cross-widget panel drags
started from menubar/breadcrumb presses, circular-border hits) — policy the
router cannot own, and running its drag tracker alongside would deliver
DragUpdates at the move loop's substituted (-9999) coords. Converting is a
designer event-layer redesign, not a call-form swap.

Verified via the HTTP API: /state before the wheel identical across builds
(modulo launch-timestamped node ids); after 3 wheel ticks the camera pan
matches to 0.01 (the pan-velocity EMA is wall-clock-dependent — run noise);
full-window pixel A/B empty 8% mask.

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

 src/app.rs | 40 ++++++++++++++++++++++------------------
 1 file changed, 22 insertions(+), 18 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 1530785..289415e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4155,32 +4155,28 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 let mut handled = false;
                 let mut needs_sync_grid = false;
                 if !dialog_open && !self.modifiers.control_key() {
-                    let ctx = &mut self.ui_context;
-                    let now = std::time::Instant::now();
-                    let elapsed_ms = match ctx.last_scroll_time {
-                        None => 999999,
-                        Some(last) => now.duration_since(last).as_millis(),
+                    // Routed (6bd shrink): propagate_event owns the scroll-gesture
+                    // bookkeeping this loop used to hand-roll. The wheel is the only
+                    // designer surface routed so far — the press/move cascade stays
+                    // app-sovereign (see the routed-events deferral note below).
+                    let wheel_ev = cce_ui::widget::Event::MouseWheel {
+                        delta: *delta,
+                        x: self.cursor_x,
+                        y: self.cursor_y,
+                        local_x: self.cursor_x,
+                        local_y: self.cursor_y,
                     };
-                    if elapsed_ms >= 5 {
-                        let is_new_gesture = elapsed_ms > 250;
-                        if is_new_gesture {
-                            ctx.scroll_initiate_widget_id = None;
-                            ctx.scroll_gesture_new = true;
-                        } else {
-                            ctx.scroll_gesture_new = false;
-                        }
-                        ctx.last_scroll_time = Some(now);
-                    }
                     for i in 0..WIDGET_COUNT {
                         if i == VIEWPORT_IDX {
                             continue;
                         }
-                        let w = self.slots.get_dyn_mut(i);
-                        if w.mouse_wheel(delta, self.cursor_x, self.cursor_y, ctx) {
+                        let ptr = self.slots.get_dyn_mut(i).as_ptr_mut();
+                        if self.ui_context.propagate_event(&wheel_ev, ptr) {
                             handled = true;
                             if i == CONTENT_IDX {
                                 let active_node_area_y = self.positions[CONTENT_IDX].1;
                                 let active_node_area_x = self.positions[CONTENT_IDX].0;
+                                let w = self.slots.get_dyn(i);
                                 let (gx, gy) = cce_ui::widget::GraphController::grid_origin(
                                     w.as_any().downcast_ref::<Graph>().expect("CONTENT_IDX must be a Graph"),
                                 );
@@ -4216,7 +4212,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         }
                     }
                     if !handled {
-                        if self.slots.viewport.mouse_wheel(delta, self.cursor_x, self.cursor_y, ctx) {
+                        let vp = self.slots.viewport.as_ptr_mut();
+                        if self.ui_context.propagate_event(&wheel_ev, vp) {
                             handled = true;
                         }
                     }
@@ -4422,6 +4419,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 }
                 changed
             }
+            // ROUTED-EVENTS DEFERRAL (6bd): the press/move/keyboard cascade below stays on
+            // direct dispatch deliberately. `drag_widget` doubles as widget-drag AND
+            // app-mode-drag (floating-pane edge resizes, cross-widget panel drags started
+            // from menubar/breadcrumb presses, circular-border hits) — policy the router
+            // cannot own; running its drag tracker alongside would deliver DragUpdates at
+            // this loop's substituted (-9999) coords. Converting means redesigning the
+            // designer's event layer (its own z-ordered windowing), not a call-form swap.
             WindowEvent::MouseInput { state: btn_state, button, .. } => {
                 println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
                 if *btn_state == ElementState::Pressed {