git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

commita9abae0026ddefccc730161ef6050bd4a55b1956
parent5a1227cceb
authorLucas Galante <[email protected]>
date2026-07-12 20:16
refactor: cce-graph on routed events (6bd shrink — direct dispatch retired)

The three menubar dropdowns, the Graph canvas, and the wheel all dispatch
one Event through ui_context.propagate_event; the app-owned drags stay app
logic (the dissolved-Plate panel drag and the loaded-image grid drag are not
widgets). The node-drag lifecycle moves onto the router: the press arms the
grab as before, the router's DragStart replaces the immediate drag_begin
call (standard 3px threshold), DragUpdate streams the node, and DragEnd
commits — Graph's commit takes the drag index, so the release the router
then delivers lands in the inert arm (no double-commit; legacy suppressed
that release by hand). Rebuilds ride ui_context.is_dragging during drags
(the router drops drag_update's changed flag).

Depends on the routed-drag fix (cce-ui ac218aa). Verified live A/B
(identical pointer sequences): File-menu popover, canvas click, node select,
and 3x wheel pan — all empty 8% masks. Held node drag: user spot-check.

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

 src/main.rs | 74 ++++++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 46 insertions(+), 28 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index ec13477..58c9255 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use wayland_client::QueueHandle;
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::widget::{Adapted, MouseButton, ElementState, MouseScrollDelta, KeyEvent, WidgetHost, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Label};
+use cce_ui::widget::{Adapted, MouseButton, ElementState, MouseScrollDelta, KeyEvent, WidgetHost, Event, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Label};
 use image::GenericImageView;
 
 #[derive(Debug, Clone)]
@@ -1334,10 +1334,14 @@ impl Application for GraphApp {
             || self.dropdown_view.hit_test(pos.x, pos.y, &self.ui_context)
             || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context);
 
+        // Routed dispatch (6bd shrink): one Event through the router per root; the panel
+        // and loaded-image drags stay app-owned (they are not widgets).
+        let mv = Event::PointerMove { x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
         if over_menu {
-            if self.dropdown_file.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
-            if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
-            if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            let dd_ptrs = [self.dropdown_file.as_ptr_mut(), self.dropdown_edit.as_ptr_mut(), self.dropdown_view.as_ptr_mut()];
+            for ptr in dd_ptrs {
+                if self.ui_context.propagate_event(&mv, ptr) { changed = true; }
+            }
         } else {
             let mut handled_by_panel = false;
             if self.show_control_panel {
@@ -1361,11 +1365,7 @@ impl Application for GraphApp {
 
             if !handled_by_panel {
                 // Otherwise feed it to Graph
-                if self.graph.is_dragging() {
-                    if self.graph.drag_update(pos.x, pos.y) {
-                        changed = true;
-                    }
-                } else if let Some(img_idx) = self.dragging_image_idx {
+                if let Some(img_idx) = self.dragging_image_idx {
                     let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
                     let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
                     let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
@@ -1390,18 +1390,28 @@ impl Application for GraphApp {
                         }
                     }
                 } else {
-                    if self.graph.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) {
+                    // The router forwards DragUpdate to a mid-drag node grab; a plain move
+                    // runs the hover recompute. Node positions change without the propagate
+                    // reporting it — rebuild every move while a drag is live.
+                    let g = self.graph.as_ptr_mut();
+                    if self.ui_context.propagate_event(&mv, g) {
+                        changed = true;
+                    }
+                    if self.ui_context.is_dragging {
                         changed = true;
                     }
                 }
             } else {
-                if self.graph.on_cursor_moved(-1000.0, -1000.0, &mut self.ui_context) { changed = true; }
+                let clear = Event::PointerMove { x: -1000.0, y: -1000.0, local_x: -1000.0, local_y: -1000.0 };
+                let g = self.graph.as_ptr_mut();
+                if self.ui_context.propagate_event(&clear, g) { changed = true; }
             }
-            
+
             // Clear hover states if cursor moved away
-            if self.dropdown_file.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
-            if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
-            if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            let dd_ptrs = [self.dropdown_file.as_ptr_mut(), self.dropdown_edit.as_ptr_mut(), self.dropdown_view.as_ptr_mut()];
+            for ptr in dd_ptrs {
+                if self.ui_context.propagate_event(&mv, ptr) { changed = true; }
+            }
         }
 
         if changed {
@@ -1420,8 +1430,10 @@ impl Application for GraphApp {
             || self.dropdown_view.hit_test(pos.x, pos.y, &self.ui_context)
             || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context);
 
+        let ev = Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
         if over_menu {
-            if self.dropdown_file.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+            let dd_file = self.dropdown_file.as_ptr_mut();
+            if self.ui_context.propagate_event(&ev, dd_file) {
                 changed = true;
                 if self.dropdown_file.take_change() {
                     let selected_idx = self.dropdown_file.selected;
@@ -1443,7 +1455,8 @@ impl Application for GraphApp {
                     self.dropdown_file.selected = 999;
                 }
             }
-            if self.dropdown_edit.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+            let dd_edit = self.dropdown_edit.as_ptr_mut();
+            if self.ui_context.propagate_event(&ev, dd_edit) {
                 changed = true;
                 if self.dropdown_edit.take_change() {
                     let idx = self.dropdown_edit.selected;
@@ -1455,7 +1468,8 @@ impl Application for GraphApp {
                     self.dropdown_edit.selected = 999;
                 }
             }
-            if self.dropdown_view.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+            let dd_view = self.dropdown_view.as_ptr_mut();
+            if self.ui_context.propagate_event(&ev, dd_view) {
                 changed = true;
                 if self.dropdown_view.take_change() {
                     let idx = self.dropdown_view.selected;
@@ -1509,10 +1523,11 @@ impl Application for GraphApp {
                 // Otherwise route to Graph
                 if button == MouseButton::Left {
                     if state == ElementState::Pressed {
-                        if self.graph.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
-                            if self.graph.is_dragging() {
-                                self.graph.drag_begin(pos.x, pos.y);
-                            }
+                        // Routed press: a node grab records the drag target; the router
+                        // synthesizes DragStart past its threshold (the old immediate
+                        // drag_begin call).
+                        let g = self.graph.as_ptr_mut();
+                        if self.ui_context.propagate_event(&ev, g) {
                             self.selected_image_idx = None;
                             changed = true;
                         } else if let Some(img_idx) = self.hit_test_image(pos.x, pos.y) {
@@ -1538,14 +1553,15 @@ impl Application for GraphApp {
                             changed = true;
                         }
                     } else if state == ElementState::Released {
-                        if self.graph.is_dragging() {
-                            self.graph.drag_end();
-                            changed = true;
-                        } else if self.dragging_image_idx.is_some() {
+                        if self.dragging_image_idx.is_some() {
                             self.dragging_image_idx = None;
                             changed = true;
                         } else {
-                            if self.graph.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+                            // The router delivers DragEnd (commit) before the release
+                            // reaches Graph; a committed drag leaves the release arm inert.
+                            let was_dragging = self.ui_context.is_dragging;
+                            let g = self.graph.as_ptr_mut();
+                            if self.ui_context.propagate_event(&ev, g) || was_dragging {
                                 changed = true;
                             }
                         }
@@ -1563,7 +1579,9 @@ impl Application for GraphApp {
     }
 
     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
-        if self.graph.mouse_wheel(delta, pos.x as f32, pos.y as f32, &mut self.ui_context) {
+        let ev = Event::MouseWheel { delta: *delta, x: pos.x as f32, y: pos.y as f32, local_x: pos.x as f32, local_y: pos.y as f32 };
+        let g = self.graph.as_ptr_mut();
+        if self.ui_context.propagate_event(&ev, g) {
             *needs_rebuild = true;
             self.needs_rebuild = true;
         }