Cropping tighter past a mask layer made it invisible without a word: the layer stayed in the panel and the sidecar, and its adjustment went on landing on pixels nobody would see again. When a crop is let go, develop now measures what the gesture did to the mask stack (dr_pipeline::orphan) and, if any layer is now entirely or mostly outside the frame, shows a notice over the photograph: how many layers, their names, "Undo crop" and "Keep crop". The crop is already applied and nothing waits on the answer. The crop overlay gains a release callback carrying the rect the press began from, so the measurement runs once per gesture and never on the drag's per-frame changes. Choosing a ratio is measured the same way, being a crop committed in one click. "Undo crop" is the ordinary undo, and the notice is tied to the history revision it was raised at: the redraw that follows any history move clears it, so the crop and its warning go back as one step. A second drag folded into the same step is measured from where that step began. A crop that strands nothing shows nothing.
275 lines
10 KiB
Plaintext
275 lines
10 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 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 <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,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
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 <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,
|
|
corner.hx,
|
|
corner.hy,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|