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) {