Measure a crop drag against the frame, not against the rect it is moving

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:48:44 +02:00
co-authored by Claude Opus 5
parent 480c46c41a
commit dbaf5358d1
+47 -10
View File
@@ -10,6 +10,23 @@
// inside. The rect itself is mirrored from Rust rather than owned here, for // 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 // 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. // 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"; import { Theme } from "theme.slint";
@@ -99,6 +116,10 @@ export component CropOverlay inherits Rectangle {
} }
// Drag the whole rect. // 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 { move-area := TouchArea {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -106,23 +127,30 @@ export component CropOverlay inherits Rectangle {
property <float> start-x; property <float> start-x;
property <float> start-y; property <float> start-y;
property <float> from-x;
property <float> 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) => { pointer-event(ev) => {
if (ev.kind == PointerEventKind.down) { if (ev.kind == PointerEventKind.down) {
self.start-x = root.crop-x; self.start-x = root.crop-x;
self.start-y = root.crop-y; 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 => { moved => {
if (self.pressed) { if (self.pressed) {
root.crop-changed( root.crop-changed(
self.start-x self.start-x + self.fraction-x(self.mouse-x) - self.from-x,
+ (self.mouse-x - self.pressed-x) self.start-y + self.fraction-y(self.mouse-y) - self.from-y,
/ max(root.width, 1px),
self.start-y
+ (self.mouse-y - self.pressed-y)
/ max(root.height, 1px),
root.crop-w, root.crop-w,
root.crop-h, root.crop-h,
); );
@@ -164,6 +192,15 @@ export component CropOverlay inherits Rectangle {
property <float> oy; property <float> oy;
property <float> ow; property <float> ow;
property <float> oh; property <float> oh;
property <float> from-x;
property <float> 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) => { pointer-event(ev) => {
if (ev.kind == PointerEventKind.down) { if (ev.kind == PointerEventKind.down) {
@@ -171,15 +208,15 @@ export component CropOverlay inherits Rectangle {
self.oy = root.crop-y; self.oy = root.crop-y;
self.ow = root.crop-w; self.ow = root.crop-w;
self.oh = root.crop-h; 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 // Movement as a fraction of the frame, live while
// the handle is held. // the handle is held.
property <float> dx: property <float> dx: self.fraction-x(self.mouse-x) - self.from-x;
(self.mouse-x - self.pressed-x) / max(root.width, 1px); property <float> dy: self.fraction-y(self.mouse-y) - self.from-y;
property <float> dy:
(self.mouse-y - self.pressed-y) / max(root.height, 1px);
moved => { moved => {
if (!self.pressed) { if (!self.pressed) {