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>
240 lines
8.5 KiB
Plaintext
240 lines
8.5 KiB
Plaintext
// TRACES: FR-DEV-4
|
|
// The crop rectangle drawn on the photograph.
|
|
//
|
|
// Four dimmed, desaturated panels around the crop, then the rect itself with
|
|
// handles. Panels rather than one shape with a hole: Slint has no cut-out, and
|
|
// four rectangles are exact.
|
|
//
|
|
// Sized by the develop view to the fitted image rect, so the fractions below
|
|
// are fractions of the picture and not of the canvas box it is letterboxed
|
|
// 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";
|
|
|
|
export component CropOverlay inherits Rectangle {
|
|
/// The crop rect, in fractions of the frame.
|
|
in property <float> crop-x: 0.0;
|
|
in property <float> crop-y: 0.0;
|
|
in property <float> crop-w: 1.0;
|
|
in property <float> crop-h: 1.0;
|
|
|
|
/// A dragged crop rect, in the same fractions.
|
|
callback crop-changed(float, float, float, float);
|
|
|
|
|
|
property <length> rx: root.crop-x * self.width;
|
|
property <length> ry: root.crop-y * self.height;
|
|
property <length> rw: root.crop-w * self.width;
|
|
property <length> rh: root.crop-h * self.height;
|
|
|
|
// The surround: dimmed and drained of colour, so the crop
|
|
// reads as the photograph and everything else as context
|
|
// being discarded. Slint cannot desaturate a live image,
|
|
// so this is a heavy neutral wash over it — the dimming
|
|
// carries the separation and the neutrality kills the
|
|
// colour cue.
|
|
property <brush> veil: #20242aE0;
|
|
|
|
Rectangle {
|
|
x: 0;
|
|
y: 0;
|
|
width: parent.width;
|
|
height: root.ry;
|
|
background: root.veil;
|
|
}
|
|
Rectangle {
|
|
x: 0;
|
|
y: root.ry + root.rh;
|
|
width: parent.width;
|
|
height: parent.height - root.ry - root.rh;
|
|
background: root.veil;
|
|
}
|
|
Rectangle {
|
|
x: 0;
|
|
y: root.ry;
|
|
width: root.rx;
|
|
height: root.rh;
|
|
background: root.veil;
|
|
}
|
|
Rectangle {
|
|
x: root.rx + root.rw;
|
|
y: root.ry;
|
|
width: parent.width - root.rx - root.rw;
|
|
height: root.rh;
|
|
background: root.veil;
|
|
}
|
|
|
|
// The rect, its thirds, and the handles.
|
|
Rectangle {
|
|
x: parent.rx;
|
|
y: parent.ry;
|
|
width: parent.rw;
|
|
height: parent.rh;
|
|
border-width: 1px;
|
|
border-color: #ffffffCC;
|
|
|
|
// Rule of thirds, the reason a crop overlay is worth
|
|
// drawing at all rather than typing numbers.
|
|
Rectangle {
|
|
x: parent.width / 3;
|
|
y: 0; width: 1px; height: parent.height;
|
|
background: #ffffff44;
|
|
}
|
|
Rectangle {
|
|
x: parent.width * 2 / 3;
|
|
y: 0; width: 1px; height: parent.height;
|
|
background: #ffffff44;
|
|
}
|
|
Rectangle {
|
|
x: 0; y: parent.height / 3;
|
|
width: parent.width; height: 1px;
|
|
background: #ffffff44;
|
|
}
|
|
Rectangle {
|
|
x: 0; y: parent.height * 2 / 3;
|
|
width: parent.width; height: 1px;
|
|
background: #ffffff44;
|
|
}
|
|
|
|
// 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%;
|
|
mouse-cursor: MouseCursor.move;
|
|
|
|
property <float> start-x;
|
|
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) => {
|
|
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.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,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Corner handles. Each drags one corner while the opposite
|
|
// stays put, which is the only behaviour that lets a crop
|
|
// be shaped rather than merely moved.
|
|
for corner in [
|
|
{ hx: 0.0, hy: 0.0 },
|
|
{ hx: 1.0, hy: 0.0 },
|
|
{ hx: 0.0, hy: 1.0 },
|
|
{ hx: 1.0, hy: 1.0 },
|
|
]: Rectangle {
|
|
property <length> size: 18px;
|
|
x: parent.rx + corner.hx * parent.rw - self.size / 2;
|
|
y: parent.ry + corner.hy * parent.rh - self.size / 2;
|
|
width: self.size;
|
|
height: self.size;
|
|
|
|
Rectangle {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #ffffff;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
TouchArea {
|
|
width: 100%;
|
|
height: 100%;
|
|
mouse-cursor: (corner.hx == corner.hy)
|
|
? MouseCursor.nwse-resize
|
|
: MouseCursor.nesw-resize;
|
|
|
|
property <float> ox;
|
|
property <float> oy;
|
|
property <float> ow;
|
|
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) => {
|
|
if (ev.kind == PointerEventKind.down) {
|
|
self.ox = root.crop-x;
|
|
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 <float> dx: self.fraction-x(self.mouse-x) - self.from-x;
|
|
property <float> dy: self.fraction-y(self.mouse-y) - self.from-y;
|
|
|
|
moved => {
|
|
if (!self.pressed) {
|
|
return;
|
|
}
|
|
// Dragging a left/top handle moves the origin
|
|
// and shrinks the extent by the same amount;
|
|
// a right/bottom handle moves only the extent.
|
|
// Rust clamps the result, so an over-drag
|
|
// slides rather than inverting.
|
|
root.crop-changed(
|
|
corner.hx == 0 ? self.ox + self.dx : self.ox,
|
|
corner.hy == 0 ? self.oy + self.dy : self.oy,
|
|
corner.hx == 0 ? self.ow - self.dx : self.ow + self.dx,
|
|
corner.hy == 0 ? self.oh - self.dy : self.oh + self.dy,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|