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

commit7e93c8fc72c79c4fd0dc5081a867842fb1108576
parent58be1862f1
authorLucas Galante <[email protected]>
date2026-07-13 10:27
refactor(events)!: the event redesign — AppDrag splits app-mode drags out of drag_widget

Closes the 6bd routed-events deferral as a design decision: the designer's
event layer is its own z-ordered windowing system (custom hit shapes, pane
focus, unfocus rituals) delivering through handle_event directly — the
router cannot own that policy. The conflation the deferral named is gone:
AppDrag { NetworkResize, ParamResize, SpreadsheetResize } carries each
edge-resize gesture whole (dir + start state); the three is_resizing_*
flags and four drag_start_* scratch fields are deleted; drag_widget only
ever names a widget drag; exactly one of the two is armed per press. The
hot-path MouseInput debug println died as a rider.

Verified: A/B vs stashed baseline — canvas-click and menu-open frames
byte-identical (AE=0), id-stripped /state identical, launch frame cursor
sprites only (same-binary control AE=0). Held gestures: user spot-check.

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

 src/app.rs     | 231 ++++++++++++++++++++++++++++++++-------------------------
 src/project.rs |   3 +
 src/render.rs  |   2 +-
 3 files changed, 132 insertions(+), 104 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 376b840..e96b35a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -887,6 +887,20 @@ pub struct ResizeDirection {
     pub bottom: bool,
 }
 
+/// An app-mode drag: one of the designer's own windowing gestures (floating-pane edge
+/// resizes), carrying its whole gesture state. The event-layer redesign splits this out
+/// of `drag_widget`, which previously doubled as widget-drag index AND app-mode-drag
+/// marker (via three `is_resizing_*` flags keyed against pane indices) — `drag_widget`
+/// now ONLY ever names a widget drag (a slot whose `Input` drag hooks are driving:
+/// panel move, graph node drag, param slider, spreadsheet scroll). Exactly one of
+/// `app_drag`/`drag_widget` is armed per press.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum AppDrag {
+    NetworkResize { dir: ResizeDirection, start_rect: (f32, f32, f32, f32), start_mouse: (f32, f32) },
+    ParamResize { start_w: f32, start_mouse_x: f32 },
+    SpreadsheetResize { start_h: f32, start_mouse_y: f32 },
+}
+
 #[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct ViewportUniforms {
@@ -1041,15 +1055,10 @@ pub struct State {
     pub last_design_mod_time: Option<std::time::SystemTime>,
     pub last_config_mod_time: Option<std::time::SystemTime>,
     pub floating_network_layout: (f32, f32, f32, f32),
-    pub is_resizing_network: Option<ResizeDirection>,
-    pub drag_start_rect: (f32, f32, f32, f32),
-    pub drag_start_mouse: (f32, f32),
+    /// The active app-mode drag (pane edge resize), if any. See [`AppDrag`].
+    pub app_drag: Option<AppDrag>,
     pub floating_param_width: f32,
-    pub is_resizing_param: bool,
-    pub drag_start_param_w: f32,
     pub floating_spreadsheet_height: f32,
-    pub is_resizing_spreadsheet: bool,
-    pub drag_start_spreadsheet_h: f32,
     pub loaded_project_path: Option<std::path::PathBuf>,
     pub last_saved_root_json: String,
     pub recent_files: Vec<std::path::PathBuf>,
@@ -2193,6 +2202,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
     pub fn on_path_changed(&mut self) {
         self.graph_mut().set_selected_node(None);
         self.drag_widget = None;
+        self.app_drag = None;
         self.focused_widget = None;
         self.pan_x = 0.0;
         self.pan_y = 0.0;
@@ -3139,15 +3149,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 mod_time
             },
             floating_network_layout: (18.0, 44.0, 400.0, 710.0),
-            is_resizing_network: None,
-            drag_start_rect: (0.0, 0.0, 0.0, 0.0),
-            drag_start_mouse: (0.0, 0.0),
+            app_drag: None,
             floating_param_width: 300.0,
-            is_resizing_param: false,
-            drag_start_param_w: 0.0,
             floating_spreadsheet_height: 250.0,
-            is_resizing_spreadsheet: false,
-            drag_start_spreadsheet_h: 0.0,
             loaded_project_path: None,
             last_saved_root_json: serde_json::to_string(&fs_root).unwrap_or_default(),
             recent_files,
@@ -4334,74 +4338,76 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.update_panel_bounds();
                     changed = true;
                 } else {
-                    if let Some(idx) = self.drag_widget {
-                        if idx == NETWORK_PANEL_IDX && self.is_resizing_network.is_some() {
-                            let dir = self.is_resizing_network.unwrap();
-                            let dx = self.cursor_x - self.drag_start_mouse.0;
-                            let dy = self.cursor_y - self.drag_start_mouse.1;
-                            let (sx, sy, sw, sh) = self.drag_start_rect;
-
-                            let mut fx = sx;
-                            let mut fy = sy;
-                            let mut fw = sw;
-                            let mut fh = sh;
-
-                            if dir.left {
-                                let new_w = (sw - dx).max(150.0);
-                                fx = sx + sw - new_w;
-                                fw = new_w;
-                            } else if dir.right {
-                                fw = (sw + dx).max(150.0);
-                            }
+                    if let Some(drag) = self.app_drag {
+                        match drag {
+                            AppDrag::NetworkResize { dir, start_rect, start_mouse } => {
+                                let dx = self.cursor_x - start_mouse.0;
+                                let dy = self.cursor_y - start_mouse.1;
+                                let (sx, sy, sw, sh) = start_rect;
+
+                                let mut fx = sx;
+                                let mut fy = sy;
+                                let mut fw = sw;
+                                let mut fh = sh;
+
+                                if dir.left {
+                                    let new_w = (sw - dx).max(150.0);
+                                    fx = sx + sw - new_w;
+                                    fw = new_w;
+                                } else if dir.right {
+                                    fw = (sw + dx).max(150.0);
+                                }
 
-                            if dir.top {
-                                let new_h = (sh - dy).max(100.0);
-                                fy = sy + sh - new_h;
-                                fh = new_h;
-                            } else if dir.bottom {
-                                fh = (sh + dy).max(100.0);
-                            }
+                                if dir.top {
+                                    let new_h = (sh - dy).max(100.0);
+                                    fy = sy + sh - new_h;
+                                    fh = new_h;
+                                } else if dir.bottom {
+                                    fh = (sh + dy).max(100.0);
+                                }
 
-                            self.floating_network_layout = (fx, fy, fw, fh);
-                            self.rebuild_positions();
-                            self.apply_layout();
-                            self.sync_grid_settings();
-                            changed = true;
-                        } else if idx == PARAM_IDX && self.is_resizing_param {
-                            let dx = self.cursor_x - self.drag_start_mouse.0;
-                            let sw = self.drag_start_param_w;
-                            let new_w = (sw - dx).max(150.0);
-                            self.floating_param_width = new_w;
-                            self.rebuild_positions();
-                            self.apply_layout();
-                            changed = true;
-                        } else if idx == SPREADSHEET_IDX && self.is_resizing_spreadsheet {
-                            let dy = self.cursor_y - self.drag_start_mouse.1;
-                            let sh = self.drag_start_spreadsheet_h;
-                            let new_h = (sh - dy).max(100.0);
-                            self.floating_spreadsheet_height = new_h;
-                            self.rebuild_positions();
-                            self.apply_layout();
-                            changed = true;
-                        } else {
-                            self.slots.get_dyn_mut(idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
-                            if {
-                                let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
-                                let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
-                                unsafe { (*ptr).handle_event(&ev, &mut self.ui_context) }
-                            } {
+                                self.floating_network_layout = (fx, fy, fw, fh);
+                                self.rebuild_positions();
+                                self.apply_layout();
+                                self.sync_grid_settings();
                                 changed = true;
-                                if idx == NETWORK_PANEL_IDX {
-                                    self.sync_grid_settings();
-                                }
-                                if idx == PARAM_IDX {
-                                    self.sync_parameters_to_project();
-                                }
+                            }
+                            AppDrag::ParamResize { start_w, start_mouse_x } => {
+                                let dx = self.cursor_x - start_mouse_x;
+                                let new_w = (start_w - dx).max(150.0);
+                                self.floating_param_width = new_w;
+                                self.rebuild_positions();
+                                self.apply_layout();
+                                changed = true;
+                            }
+                            AppDrag::SpreadsheetResize { start_h, start_mouse_y } => {
+                                let dy = self.cursor_y - start_mouse_y;
+                                let new_h = (start_h - dy).max(100.0);
+                                self.floating_spreadsheet_height = new_h;
+                                self.rebuild_positions();
+                                self.apply_layout();
+                                changed = true;
+                            }
+                        }
+                    } else if let Some(idx) = self.drag_widget {
+                        // A widget drag: the slot's own Input drag hooks are driving.
+                        self.slots.get_dyn_mut(idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                        if {
+                            let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
+                            let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                            unsafe { (*ptr).handle_event(&ev, &mut self.ui_context) }
+                        } {
+                            changed = true;
+                            if idx == NETWORK_PANEL_IDX {
+                                self.sync_grid_settings();
+                            }
+                            if idx == PARAM_IDX {
+                                self.sync_parameters_to_project();
                             }
                         }
                     }
 
-                    if self.drag_widget.is_none() {
+                    if self.drag_widget.is_none() && self.app_drag.is_none() {
                         for i in 0..WIDGET_COUNT {
                             let (cx, cy) = (self.cursor_x, self.cursor_y);
                             let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
@@ -4431,15 +4437,17 @@ 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.
+            // THE DESIGNER'S EVENT LAYER IS ITS OWN WINDOWING SYSTEM (the event redesign,
+            // closing the 6bd deferral): presses resolve a z-ordered click target through
+            // app-owned hit shapes (circular-pane overrides, hardcoded pane z), derive pane
+            // focus, run the unfocus rituals, and deliver through `handle_event` directly —
+            // the UiContext router's hit-gating/descent/drag tracker cannot own that policy,
+            // so it is deliberately not used here (mixing the two would double-run drag
+            // state machines). Drags are split: `app_drag` is the app-mode gesture state
+            // machine (floating-pane edge resizes, each variant carrying its whole gesture),
+            // `drag_widget` only ever names a widget drag driven through the slot's Input
+            // drag hooks. Exactly one of the two is armed per press.
             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 {
                     self.pan_velocity_x = 0.0;
                     self.pan_velocity_y = 0.0;
@@ -4590,15 +4598,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             let on_bottom = false;
 
                             if on_left || on_right || on_top || on_bottom {
-                                self.is_resizing_network = Some(ResizeDirection {
-                                    left: on_left,
-                                    right: on_right,
-                                    top: on_top,
-                                    bottom: on_bottom,
+                                self.app_drag = Some(AppDrag::NetworkResize {
+                                    dir: ResizeDirection {
+                                        left: on_left,
+                                        right: on_right,
+                                        top: on_top,
+                                        bottom: on_bottom,
+                                    },
+                                    start_rect: self.floating_network_layout,
+                                    start_mouse: (cx, cy),
                                 });
-                                self.drag_widget = Some(NETWORK_PANEL_IDX);
-                                self.drag_start_rect = self.floating_network_layout;
-                                self.drag_start_mouse = (cx, cy);
                                 self.focused_pane = LEFT_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
                                     self.slots.get_dyn_mut(old).unfocus();
@@ -4611,7 +4620,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                 if self.slots.breadcrumb.mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context) {
                                     return true;
                                 }
-                                self.is_resizing_network = None;
                                 self.drag_widget = Some(NETWORK_PANEL_IDX);
                                 self.slots.network_panel.set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
                                 self.slots.network_panel.drag_begin(cx, cy);
@@ -4639,10 +4647,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             let on_left = cx >= param_x - margin && cx <= param_x + margin && cy >= param_y - margin && cy <= param_y + param_h + margin;
 
                             if on_left {
-                                self.is_resizing_param = true;
-                                self.drag_widget = Some(PARAM_IDX);
-                                self.drag_start_param_w = param_w;
-                                self.drag_start_mouse = (cx, cy);
+                                self.app_drag = Some(AppDrag::ParamResize {
+                                    start_w: param_w,
+                                    start_mouse_x: cx,
+                                });
                                 self.focused_pane = PARAM_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
                                     self.slots.get_dyn_mut(old).unfocus();
@@ -4675,10 +4683,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             let on_top = cx >= ss_x && cx <= ss_x + ss_w && cy >= ss_y - margin && cy <= ss_y + margin;
                             
                             if on_top {
-                                self.is_resizing_spreadsheet = true;
-                                self.drag_widget = Some(SPREADSHEET_IDX);
-                                self.drag_start_spreadsheet_h = ss_h;
-                                self.drag_start_mouse = (cx, cy);
+                                self.app_drag = Some(AppDrag::SpreadsheetResize {
+                                    start_h: ss_h,
+                                    start_mouse_y: cy,
+                                });
                                 self.focused_pane = SPREADSHEET_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
                                     self.slots.get_dyn_mut(old).unfocus();
@@ -4856,6 +4864,26 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         self.sync_pane_focus();
                     }
                     ElementState::Released => {
+                        if let Some(drag) = self.app_drag.take() {
+                            // App-mode drag teardown. The DragEnd send is kept from the old
+                            // shared teardown for faithfulness — the pane widget never began
+                            // a drag in resize mode, so its commit/cancel hook is a no-op.
+                            let idx = match drag {
+                                AppDrag::NetworkResize { .. } => NETWORK_PANEL_IDX,
+                                AppDrag::ParamResize { .. } => PARAM_IDX,
+                                AppDrag::SpreadsheetResize { .. } => SPREADSHEET_IDX,
+                            };
+                            {
+                                let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
+                            }
+                            self.sync_layout();
+                            if matches!(drag, AppDrag::NetworkResize { .. }) {
+                                self.read_panel_offsets();
+                            }
+                            self.upload_vertices();
+                            changed = true;
+                        }
                         if self.drag_widget.is_some() {
                             let idx = self.drag_widget.unwrap();
                             if idx == NETWORK_PANEL_IDX {
@@ -4863,7 +4891,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
-                                self.is_resizing_network = None;
                                 self.sync_layout();
                                 self.read_panel_offsets();
                                 self.upload_vertices();
@@ -4872,7 +4899,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
-                                self.is_resizing_param = false;
                                 self.sync_layout();
                                 self.upload_vertices();
                             } else if idx == SPREADSHEET_IDX {
@@ -4880,7 +4906,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
-                                self.is_resizing_spreadsheet = false;
                                 self.sync_layout();
                                 self.upload_vertices();
                             } else if idx == CONTENT_IDX {
diff --git a/src/project.rs b/src/project.rs
index 68d0ab6..2b6b473 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -137,6 +137,7 @@ impl State {
                 self.focused_widget = None;
             }
             self.drag_widget = None;
+            self.app_drag = None;
             self.last_click = None;
 
             self.sync_grid_settings();
@@ -191,6 +192,7 @@ impl State {
             self.focused_widget = None;
         }
         self.drag_widget = None;
+        self.app_drag = None;
         self.last_click = None;
 
         self.sync_grid_settings();
@@ -242,6 +244,7 @@ impl State {
 
         self.focused_widget = None;
         self.drag_widget = None;
+        self.app_drag = None;
         self.last_click = None;
 
         self.sync_grid_settings();
diff --git a/src/render.rs b/src/render.rs
index 70d75e6..c0a4610 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -62,7 +62,7 @@ impl State {
         let sh = self.height;
 
         let node_area_y = self.positions[CONTENT_IDX].1;
-        let show_cursor = self.drag_widget.is_none();
+        let show_cursor = self.drag_widget.is_none() && self.app_drag.is_none();
 
         let clip = if self.circular_network_pane {
             (