Files
DarkRoom/ui/dr-ui/ui/crop.slint
T
dtourolleandClaude Opus 5 4fa914cbb1 Send each canvas overlay home to its own domain
The crop rectangle, the gradient handles and the repair discs were 480
lines inside `canvas-area` in app.slint, while the panels that drive them
already lived in adjust.slint, masks.slint and spots.slint. spots.slint
even opens by describing "what is drawn over the photograph, and what a
finger can take hold of" -- which was not in it.

They stayed behind because all three are positioned against `shown-*`,
the fitted image rect the develop view derives because Slint does not
report it. That is now the interface rather than the obstacle: each
overlay is *given* that rect as its own bounds, so every position inside
is a plain fraction of `root.width`, and none of them reaches out to
`canvas-area` for an origin any more.

GradientHandles joins MaskPanel in masks.slint and SpotHandles joins
SpotPanel in spots.slint, each file now holding one domain's panel and
its canvas overlay together, matching masks_ui.rs and spots_ui.rs.
CropOverlay gets crop.slint of its own rather than growing adjust.slint.

Arithmetic is unchanged: the old fraction-x subtracted shown-x from a
coordinate measured relative to canvas-area, and the new one measures
from an origin that already is shown-x. The handles stay unconditional
rather than gaining an emptiness guard, so the repeater identity that
spots_ui::sync_handles warns about is untouched.

app.slint: 2834 -> 2490 lines. Verified by running the desktop app on a
photograph, with the crop overlay's guard temporarily forced open so all
three instantiate -- no binding loop, no panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 21:35:08 +02:00

203 lines
6.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.
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.
move-area := TouchArea {
width: 100%;
height: 100%;
mouse-cursor: MouseCursor.move;
property <float> start-x;
property <float> start-y;
pointer-event(ev) => {
if (ev.kind == PointerEventKind.down) {
self.start-x = root.crop-x;
self.start-y = root.crop-y;
}
}
moved => {
if (self.pressed) {
root.crop-changed(
self.start-x
+ (self.mouse-x - self.pressed-x)
/ max(root.width, 1px),
self.start-y
+ (self.mouse-y - self.pressed-y)
/ max(root.height, 1px),
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;
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;
}
}
// Movement as a fraction of the frame, live while
// the handle is held.
property <float> dx:
(self.mouse-x - self.pressed-x) / max(root.width, 1px);
property <float> dy:
(self.mouse-y - self.pressed-y) / max(root.height, 1px);
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,
);
}
}
}
}