From dbaf5358d1eaf4e2f64ed675943c5c1d6f3ab684 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 29 Aug 2026 12:11:28 +0200 Subject: [PATCH] Measure a crop drag against the frame, not against the rect it is moving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragging the crop rectangle tracked the pointer at half speed: the rect slid out from under the cursor, and the handle being held stopped being the one under the finger. On a photograph where the whole point is to place an edge by eye, that made the tool close to unusable. The cause is that a `TouchArea` reports `mouse-x` relative to itself, and every area in the overlay is positioned *by the very rect the drag is editing*. Rust echoes the applied rect back on each step, so the area moves under the pointer and the reported position falls by exactly the amount it had just risen. `mouse-x - pressed-x` therefore subtracts the drag from itself, and the fixed point of that feedback is a rect that moves half as far as the pointer does — which is why it looked like sluggish tracking rather than like a coordinate bug. Both terms are now taken in the frame's own coordinates: `parent.x + mouse-x`, where `parent.x` tracks precisely the movement `mouse-x` lost, with the press captured in those same coordinates on the way down. The two movements cancel and the rect follows the pointer exactly. `GradientHandles` in masks.slint was already written this way, and for the same reason — its handles are placed by the mask they drag. The header note here now says why the shape matters, since the wrong version compiles, looks plausible, and is only wrong once the rect starts moving. Co-Authored-By: Claude Opus 5 (1M context) --- ui/dr-ui/ui/crop.slint | 57 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/ui/dr-ui/ui/crop.slint b/ui/dr-ui/ui/crop.slint index 3508c4a..d7df264 100644 --- a/ui/dr-ui/ui/crop.slint +++ b/ui/dr-ui/ui/crop.slint @@ -10,6 +10,23 @@ // inside. The rect itself is mirrored from Rust rather than owned here, for // the reason `AppWindow` gives: Rust clamps and squares it, so the overlay // must draw what was actually applied and not what the drag asked for. +// +// **Every drag is measured in the frame's coordinates, never in the dragged +// area's own.** This is not a stylistic preference. A `TouchArea` reports +// `mouse-x` relative to itself, and each of these areas is placed *by the very +// rect the drag is editing* — so the moment Rust echoes a new rect back, the +// area moves under the pointer and the reported position drops by the same +// amount it just rose. Taking `mouse-x - pressed-x` as the movement therefore +// subtracts the drag from itself: the rect converges on tracking the pointer +// at half speed, which on screen reads as the crop sliding out from under the +// cursor and the handle you are holding no longer being the one beneath your +// finger. +// +// The fix is the one `GradientHandles` in masks.slint already uses: convert +// the pointer to a fraction of the *frame* — `parent.x + mouse-x`, where +// `parent.x` tracks exactly the movement `mouse-x` lost — and capture the +// press in those same coordinates on the way down. Both terms then move +// together and cancel, so the rect follows the pointer exactly. import { Theme } from "theme.slint"; @@ -99,6 +116,10 @@ export component CropOverlay inherits Rectangle { } // Drag the whole rect. + // + // Measured in the frame's coordinates, not this area's — see the + // header. This `TouchArea` *is* the rect being dragged, so it is the + // worst case for the drift: it moves by the whole of every step. move-area := TouchArea { width: 100%; height: 100%; @@ -106,23 +127,30 @@ export component CropOverlay inherits Rectangle { property start-x; property start-y; + property from-x; + property from-y; + + function fraction-x(local-x: length) -> float { + return (parent.x + local-x) / max(root.width, 1px); + } + function fraction-y(local-y: length) -> float { + return (parent.y + local-y) / max(root.height, 1px); + } pointer-event(ev) => { if (ev.kind == PointerEventKind.down) { self.start-x = root.crop-x; self.start-y = root.crop-y; + self.from-x = self.fraction-x(self.pressed-x); + self.from-y = self.fraction-y(self.pressed-y); } } moved => { if (self.pressed) { root.crop-changed( - self.start-x - + (self.mouse-x - self.pressed-x) - / max(root.width, 1px), - self.start-y - + (self.mouse-y - self.pressed-y) - / max(root.height, 1px), + self.start-x + self.fraction-x(self.mouse-x) - self.from-x, + self.start-y + self.fraction-y(self.mouse-y) - self.from-y, root.crop-w, root.crop-h, ); @@ -164,6 +192,15 @@ export component CropOverlay inherits Rectangle { property oy; property ow; property oh; + property from-x; + property from-y; + + function fraction-x(local-x: length) -> float { + return (parent.x + local-x) / max(root.width, 1px); + } + function fraction-y(local-y: length) -> float { + return (parent.y + local-y) / max(root.height, 1px); + } pointer-event(ev) => { if (ev.kind == PointerEventKind.down) { @@ -171,15 +208,15 @@ export component CropOverlay inherits Rectangle { self.oy = root.crop-y; self.ow = root.crop-w; self.oh = root.crop-h; + self.from-x = self.fraction-x(self.pressed-x); + self.from-y = self.fraction-y(self.pressed-y); } } // Movement as a fraction of the frame, live while // the handle is held. - property dx: - (self.mouse-x - self.pressed-x) / max(root.width, 1px); - property dy: - (self.mouse-y - self.pressed-y) / max(root.height, 1px); + property dx: self.fraction-x(self.mouse-x) - self.from-x; + property dy: self.fraction-y(self.mouse-y) - self.from-y; moved => { if (!self.pressed) {