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

commit2a334dd0a2bf2b55596a1a8ea04dba3935ad732f
parent752c738ae2
authorLucas Galante <[email protected]>
date2026-09-21 14:43
fix: a press on the spreadsheet's column header sorts the column, not the camera

The toolkit's Spreadsheet has sorted on a header click since cce-ui@a693b97,
but in this app the click never reached it: `cursor_in_viewport` counted the
whole centre column as scene, floating spreadsheet and playbar included, and
the orbit arm at the top of the left-press path returns before any widget is
asked. Clicking a header hovered it, tinted it, and did nothing.

The non-overlay branch now subtracts the floating panes, as the overlay
branch always did; the right-click menu and the pinch zoom gate on the same
test and stop claiming the spreadsheet with it. The wheel's pane-focus
routing asked the spreadsheet's rect as a refinement of the old answer, so
it now asks it first.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/app.rs  | 26 ++++++++++++++++++++------
 src/main.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 6 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 63e69b0..ba1dbcf 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1807,8 +1807,16 @@ impl State {
                 && self.cursor_y >= HEADER_H
                 && self.cursor_y < self.height - STATUS_H;
         }
+        // Minus the floating panes here too. The spreadsheet and the playbar
+        // sit INSIDE the centre column, over the full-bleed scene, and this
+        // test used to count them as viewport — so a left press on a column
+        // header armed the camera orbit and returned before any widget was
+        // asked, and the spreadsheet's own header-click sort never fired.
+        // Same for the right-click menu and the pinch zoom, which gate on
+        // this test as well.
         let node_area_y = self.positions[CONTENT_IDX].1;
-        self.cursor_x >= self.content_right_x()
+        !self.over_floating_pane()
+            && self.cursor_x >= self.content_right_x()
             && self.cursor_x < self.splitter_layout.splitter2_x
             && self.cursor_y >= node_area_y
             && self.cursor_y < self.height - STATUS_H
@@ -5924,12 +5932,18 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 {
                     if in_network_pane {
                         new_pane = Some(LEFT_MENUBAR_IDX);
+                    } else if self.show_spreadsheet && {
+                        let (sx, sy, sw, sh) = self.positions[SPREADSHEET_IDX];
+                        sw > 0.0 && sh > 0.0
+                            && self.cursor_x >= sx && self.cursor_x < sx + sw
+                            && self.cursor_y >= sy && self.cursor_y < sy + sh
+                    } {
+                        // Asked before the viewport: `cursor_in_viewport` no
+                        // longer counts the spreadsheet as scene, so this can
+                        // no longer be a refinement of that answer.
+                        new_pane = Some(SPREADSHEET_MENUBAR_IDX);
                     } else if in_viewport {
-                        if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_IDX].1 {
-                            new_pane = Some(SPREADSHEET_MENUBAR_IDX);
-                        } else {
-                            new_pane = Some(RIGHT_MENUBAR_IDX);
-                        }
+                        new_pane = Some(RIGHT_MENUBAR_IDX);
                     } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W && self.cursor_y >= node_area_y && self.cursor_y < self.height - STATUS_H {
                         new_pane = Some(PARAM_MENUBAR_IDX);
                     }
diff --git a/src/main.rs b/src/main.rs
index 9f8b24f..d201c12 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -422,6 +422,53 @@ mod tests {
     /// Dock swap: dragging a plate's dot to another region swaps occupants,
     /// and the dock-owned dimensions stay put — the network lands in the
     /// bottom strip's rect, the spreadsheet in the left column's.
+    /// A left press on the spreadsheet's column header reaches the widget,
+    /// which sorts the column — it does not orbit the camera.
+    ///
+    /// `cursor_in_viewport` used to be the whole centre column, spreadsheet
+    /// included, and the orbit arm at the top of the left-press path returns
+    /// before any widget is asked. So the toolkit's header-click sort
+    /// (tested in cce-ui) never fired in this app: clicking a header
+    /// hovered it, tinted it, and did nothing.
+    #[test]
+    fn spreadsheet_header_press_reaches_the_widget_not_the_camera() {
+        use crate::slots::{SPREADSHEET_IDX, SPREADSHEET_MENUBAR_IDX, VIEWPORT_IDX};
+        use crate::window::{LocalPosition, WindowEvent};
+        use cce_ui::widget::{ElementState, MouseButton};
+        let mut state = State::new(false);
+        state.resize(1600.0, 900.0, 1.0);
+        state.show_spreadsheet = true;
+        state.rebuild_positions();
+        state.apply_layout();
+        state.spreadsheet_mut().set_spreadsheet_data(
+            vec!["Point".into(), "Pos.x".into()],
+            vec![vec!["0".into(), "9".into()], vec!["1".into(), "10".into()]],
+        );
+
+        // The middle of the second column's header cell.
+        let (sx, sy, sw, sh) = state.positions[SPREADSHEET_IDX];
+        assert!(sw > 0.0 && sh > 0.0, "the spreadsheet is laid out");
+        let (hx, hy) = (sx + sw * 0.75, sy + 12.0);
+        state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: hx as f64, y: hy as f64 } });
+        assert!(!state.cursor_in_viewport(), "a floating pane is not the scene");
+
+        state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
+        assert!(state.orbit_drag.is_none(), "the press must not arm the camera orbit");
+        assert_eq!(state.focused_widget, Some(SPREADSHEET_IDX), "the press reached the spreadsheet");
+        assert_eq!(state.focused_pane, SPREADSHEET_MENUBAR_IDX);
+        state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
+
+        // And the scene beside it is still the scene: a press in the
+        // viewport's own rect, clear of every floating pane, orbits.
+        let (vx, vy, vw, vh) = state.positions[VIEWPORT_IDX];
+        let (cx, cy) = (vx + vw * 0.5, vy + vh * 0.3);
+        assert!(!state.over_floating_pane_at(cx, cy), "pick a point clear of the panes");
+        state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: cx as f64, y: cy as f64 } });
+        assert!(state.cursor_in_viewport());
+        state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
+        assert_eq!(state.orbit_drag, Some((cx, cy)), "a press on the scene still orbits");
+    }
+
     #[test]
     fn test_dock_swap_repositions_plates() {
         use crate::app::Dock;