Four bar handles at the midpoints of the sides, each moving only its own side along the axis across it. The overlay reports an edge as 0.5 on the axis it does not move, so the anchor Rust takes is the middle of the far side. Under a ratio lock the dragged axis leads. with_aspect grew the short axis onto the ratio, which for an edge pulled inward made the untouched axis the leader and pushed the edge straight back out.
300 lines
12 KiB
Plaintext
300 lines
12 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 — `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 <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> start-w;
|
|
property <float> start-h;
|
|
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.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 <bool> across: handle.hx == 0.5;
|
|
property <bool> 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 <length> grip: 18px;
|
|
property <length> 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 <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);
|
|
}
|
|
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 <float> dx: parent.across ? 0 : self.fraction-x(self.mouse-x) - self.from-x;
|
|
property <float> 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,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|