// 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 crop-x: 0.0; in property crop-y: 0.0; in property crop-w: 1.0; in property crop-h: 1.0; /// A dragged crop rect — `x`, `y`, `width`, `height`, then the corner the /// drag is holding as a `0`/`1` pair, or `-1, -1` where the whole rect is /// being moved and its shape must not be touched. /// /// The corner is what an aspect lock needs and cannot infer: reshaping a /// rect onto a ratio has to know which of its corners is nailed down, and /// only the handle that took the press knows that. It is reported as the /// held corner rather than as the fixed one because that is what is /// written here — Rust takes the opposite corner. callback crop-changed(float, float, float, float, float, float); /// TRACES: FR-DEV-17 /// A drag of the rect or a handle was let go. Carries the rect as it was /// when the press began, so Rust can measure what the whole gesture did. /// /// The one boundary a crop gesture reports. `crop-changed` fires on /// every pointer move and a handle passing over a mask on its way /// somewhere else is not an event; only the release is a decision. callback crop-released(float, float, float, float); property rx: root.crop-x * self.width; property ry: root.crop-y * self.height; property rw: root.crop-w * self.width; property 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 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 start-x; property start-y; property start-w; property start-h; 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.start-w = root.crop-w; self.start-h = root.crop-h; self.from-x = self.fraction-x(self.pressed-x); self.from-y = self.fraction-y(self.pressed-y); } if (ev.kind == PointerEventKind.up || ev.kind == PointerEventKind.cancel) { // The extent too, not only the origin: Rust clamps a rect // moved past an edge, and that can narrow it. root.crop-released(self.start-x, self.start-y, self.start-w, self.start-h); } } 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, // No corner: a move keeps the shape it already has, // and an over-drag should slide along the edge rather // than be reshaped back inside. -1, -1, ); } } } } // 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 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 ox; 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) { 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); } if (ev.kind == PointerEventKind.up || ev.kind == PointerEventKind.cancel) { root.crop-released(self.ox, self.oy, self.ow, self.oh); } } // Movement as a fraction of the frame, live while // the handle is held. 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) { 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, corner.hx, corner.hy, ); } } } }