git.lucas.co / cce-grid
desktop grid client
git clone https://git.lucas.co/cce-grid.git

commitf7fb7bd9cd322ae3d6e2d1dd40fa49afd9830baa
parent6ad8697235
authorLucas Galante <[email protected]>
date2026-09-01 14:49
fix: pointer and region math in surface px — the grid surface has no ui scale

Input regions, item hit-tests, drag deltas, and drop positions all divided
by the process-wide output scale (logical_per_virtual). But this surface is
pinned at buffer_scale 1 — cce-ui ignores scale events for grid apps, the
patch is the sole resolution authority — so its surface-local coordinates
are BUFFER px at every output scale, and patch.scale alone is the right
conversion (surface_per_virtual now).

The /ui came from 7e027a5, calibrated against the compositor's old
hit-test, which handed out raw layout offsets instead of surface
coordinates. Those two errors cancelled only at camera zoom 1 on the pow2
patch quantization. Anywhere else the input region sat displaced from the
drawn items — on a scale-2 output at half their offset and size, putting it
entirely off the visible canvas, so every press fell through to the desktop
(and in overview, exited it) — and when a press did land, its position
disagreed with the drag-delta space by the display ratio times the surface
position, which is what kept flinging the dragged image thousands of
virtual units. The compositor speaks true surface coordinates since
cce-compositor@feab593, so the compensation now inverts into exactly these
bugs.

Verified in shadow sessions at output scale 1 and 2: regions land exactly
under the drawn items, presses hit, and drags track the pointer 1:1 in
normal mode, in overview, and across a camera flight held mid-grab.

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

 CLAUDE.md   | 21 ++++++++++++++-------
 src/main.rs | 39 +++++++++++++++++++--------------------
 2 files changed, 33 insertions(+), 27 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9e3a3a2..8edb853 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -72,13 +72,20 @@ popup at `ccectl pointer-location`, whose reply comes back as
 that menu blocks on its own thread, and the list can be reordered by a drag or
 grown by a drop while it is open.
 
-One trap, spelled out on `Patch::logical_per_virtual`: `Patch::scale` is
-BUFFER px per virtual unit — it already folds in the output scale, which is
-why the paint path uses it directly — while pointer events and input regions
-are surface-local LOGICAL px. On a scale-2 display the two differ by exactly
-the output scale, which put every input region at twice its size and offset
-(clicks missed the image entirely) and landed every drop at half its intended
-position.
+One trap, spelled out on `Patch::surface_per_virtual`: pointer events and
+input regions are surface-local px, and for THIS surface that means BUFFER
+px at every output scale — the grid surface is pinned at buffer_scale 1
+(cce-ui ignores scale events for grid apps; patch.scale is the sole
+resolution authority), so `Patch::scale` is the one conversion for paint,
+regions, and pointer math alike. This replaced a `/ui` division that had
+been calibrated against the compositor's old hit-test, which handed out raw
+layout offsets: numerically buffer/ui only at camera zoom 1 on the pow2
+patch quantization, and at any other camera state it displaced the input
+region off the items (presses read as background — in overview they EXITED
+it) and tore the press position apart from the drag deltas, flinging the
+grabbed item thousands of virtual units. The compositor's hit-test speaks
+true surface coordinates since cce-compositor@feab593; do not reintroduce
+output-scale terms here.
 
 This directory is its own git repository (gitsite-published, fetch-only
 origin; committing locally is publishing). `cce-grid.service` autostarts it
diff --git a/src/main.rs b/src/main.rs
index 4dcc9c8..0b18e54 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -70,22 +70,21 @@ struct GridApp {
 }
 
 impl Patch {
-    /// Surface-local logical px per virtual unit.
+    /// Surface-local px per virtual unit — `Patch::scale` itself.
     ///
-    /// `Patch::scale` is BUFFER px per virtual unit — it already folds in the
-    /// output scale, which is why the paint path uses it directly. Pointer
-    /// events and input regions are not in that space: both are surface-local
-    /// LOGICAL px. On a scale-2 display the two differ by exactly the output
-    /// scale, which put every input region at twice its size and offset (so
-    /// clicks missed the image entirely) and made every drop land at half its
-    /// distance from the patch origin.
-    fn logical_per_virtual(&self) -> f64 {
-        let ui = cce_ui::scale::scale_factor() as f64;
-        if ui > 0.0 {
-            self.scale / ui
-        } else {
-            self.scale
-        }
+    /// The grid surface is PINNED at buffer_scale 1 (cce-ui ignores scale
+    /// events for grid apps; patch.scale is the sole resolution authority),
+    /// so surface-local coordinates ARE buffer px at every output scale:
+    /// pointer events arrive in that space and input regions are interpreted
+    /// in it. The /ui division that used to live here calibrated against the
+    /// compositor's old hit-test, which handed out raw layout offsets —
+    /// numerically buffer/ui only at zoom 1 on the pow2 patch quantization —
+    /// and at any other camera state it displaced the input region off the
+    /// items (presses read as background) and tore the press position apart
+    /// from the drag deltas (the flung-item bug). The compositor now speaks
+    /// true surface coordinates, so the patch scale is used unmodified.
+    fn surface_per_virtual(&self) -> f64 {
+        self.scale
     }
 }
 
@@ -500,7 +499,7 @@ impl Application for GridApp {
         // The drop point in world coordinates — the inverse of the mapping
         // `paint` uses to place cells, so the image lands under the cursor
         // whatever the camera is doing.
-        let s = patch.logical_per_virtual();
+        let s = patch.surface_per_virtual();
         let vx = patch.x + pos.x as f64 / s;
         let vy = patch.y + pos.y as f64 / s;
 
@@ -560,7 +559,7 @@ impl Application for GridApp {
         if p.scale <= 0.0 {
             return Some(Vec::new());
         }
-        let s = p.logical_per_virtual();
+        let s = p.surface_per_virtual();
         let out: Vec<(i32, i32, i32, i32)> = self
             .items
             .iter()
@@ -591,7 +590,7 @@ impl Application for GridApp {
             drag.last_origin = origin;
             return;
         }
-        let s = p.logical_per_virtual();
+        let s = p.surface_per_virtual();
         let dx = (pos.x - last_pos.0) as f64 / s;
         let dy = (pos.y - last_pos.1) as f64 / s;
         if dx == 0.0 && dy == 0.0 {
@@ -621,7 +620,7 @@ impl Application for GridApp {
             if state != ElementState::Pressed {
                 return None;
             }
-            let s = p.logical_per_virtual();
+            let s = p.surface_per_virtual();
             let vx = p.x + pos.x as f64 / s;
             let vy = p.y + pos.y as f64 / s;
             let hit = self.items.iter().rposition(|(i, _)| {
@@ -647,7 +646,7 @@ impl Application for GridApp {
         }
         match state {
             ElementState::Pressed => {
-                let s = p.logical_per_virtual();
+                let s = p.surface_per_virtual();
                 let vx = p.x + pos.x as f64 / s;
                 let vy = p.y + pos.y as f64 / s;
                 // Last drawn is on top, so search backwards and take the