// 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 handle the /// drag is holding as a point of the rect: a `0`/`1` pair for a corner, /// `0.5` on the axis an edge does not move, or `-1, -1` where the whole /// rect is being moved and its shape must not be touched. /// /// The handle is what an aspect lock needs and cannot infer: reshaping a /// rect onto a ratio has to know which point of it is nailed down, and /// only the handle that took the press knows that. It is reported as the /// held point rather than as the fixed one because that is what is /// written here — Rust takes the opposite one, which for an edge is the /// middle of the far side. 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, ); } } } } // Handles: the four edges, then the four corners. // // A corner drags one corner while the opposite stays put, which is the // only behaviour that lets a crop be shaped rather than merely moved. An // edge drags that one side along the axis across it and leaves the other // three where they are — the way to trim a strip off one side without // disturbing the corner you had already placed. // // Each is a point of the rect in its own `0..1` coordinates; `0.5` on an // axis is an edge's midpoint and means "this axis does not move". The // edges come first because among overlapping siblings the last declared // takes the press, and on a rect dragged small the corners are the ones // that must stay reachable. for handle in [ { hx: 0.5, hy: 0.0 }, { hx: 0.5, hy: 1.0 }, { hx: 0.0, hy: 0.5 }, { hx: 1.0, hy: 0.5 }, { 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 across: handle.hx == 0.5; property along: handle.hy == 0.5; // A corner is a square; an edge is a bar lying along its side, with // a hit area as long as the bar and as deep as a corner's. property grip: 18px; property reach: 36px; width: self.across ? self.reach : self.grip; height: self.along ? self.reach : self.grip; x: parent.rx + handle.hx * parent.rw - self.width / 2; y: parent.ry + handle.hy * parent.rh - self.height / 2; Rectangle { width: parent.across ? 24px : (parent.along ? 5px : 12px); height: parent.along ? 24px : (parent.across ? 5px : 12px); background: #ffffff; border-radius: 2px; } TouchArea { width: 100%; height: 100%; mouse-cursor: parent.across ? MouseCursor.ns-resize : parent.along ? MouseCursor.ew-resize : (handle.hx == handle.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 — and nothing along an axis the handle does not move, so // an edge dragged at a slant still only moves its own side. property dx: parent.across ? 0 : self.fraction-x(self.mouse-x) - self.from-x; property dy: parent.along ? 0 : 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( handle.hx == 0 ? self.ox + self.dx : self.ox, handle.hy == 0 ? self.oy + self.dy : self.oy, handle.hx == 0 ? self.ow - self.dx : self.ow + self.dx, handle.hy == 0 ? self.oh - self.dy : self.oh + self.dy, handle.hx, handle.hy, ); } } } }