Let the crop be held to a ratio while it is dragged

A photographer cropping for a print, a phone wallpaper or a 16:9 frame is
not choosing four edges — they are choosing one edge and a known shape.
Free-dragging every corner made them do that arithmetic by eye on every
drag, and get it slightly wrong.

The panel now offers Free, Original, 1:1, 3:2, 4:3 and 16:9, with a
Portrait switch for the ones that have two orientations. Original follows
the frame rather than naming a number, so it stays right on the next
photograph from another body and after a quarter turn.

**The ratio is of output pixels, and the rect is not.** `CropRect` is
stored in fractions of a frame that is not itself square, so holding a
shape needs the frame's size — `ratio * height / width` of the frame.
Skipping that gives a "1:1" crop that is square only on a square
photograph, which is the one case nobody would test on, so the conversion
lives in `CropRect::with_aspect` where it is explained and pinned by a
test that asserts the fractions are *not* equal.

Two decisions worth recording:

The reshaped rect **grows** onto the ratio rather than shrinking onto it,
then scales down only as far as the frame's edge demands. Fitting inside
instead makes a one-axis drag do nothing at all — the other axis clamps
the first straight back, and the handle simply refuses to move.

The overlay now reports **which corner the drag is holding**, because
reshaping onto a ratio has to know which corner is nailed down and only
the handle that took the press knows that. A move reports no corner and
keeps its shape: reshaping about a centre would pull an over-moved rect
smaller instead of sliding it along the edge.

The lock lives with the window rather than the session. A `DevelopSession`
is per image, and cropping a set of frames to one shape is exactly when
the lock earns its place. It is not an edit and reaches no sidecar — what
is saved is the rectangle it produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:48:51 +02:00
co-authored by Claude Opus 5
parent dbaf5358d1
commit e6ad906bc1
7 changed files with 644 additions and 56 deletions
+39 -3
View File
@@ -115,10 +115,31 @@ export component AppWindow inherits Window {
in-out property <float> crop-w: 1.0;
in-out property <float> crop-h: 1.0;
/// TRACES: FR-DEV-3
/// The ratios the crop may be locked to, and which one is chosen.
///
/// Named by Rust rather than listed here: the set is `CropAspect::CHOICES`
/// and the labels come off it, so a ratio added there appears without this
/// file changing.
in property <[string]> crop-aspects;
in property <int> crop-aspect: 0;
/// Whether the chosen ratio is standing on its short edge.
in property <bool> crop-portrait: false;
/// Whether the chosen ratio has a portrait form at all — `Free` and the
/// square do not, and the switch says so rather than vanishing.
in property <bool> crop-aspect-turnable: false;
/// Enter a mode, or `photo` to leave whichever one is current.
callback mode-picked(ViewMode);
/// A dragged crop rect, in fractions of the frame.
callback crop-changed(float, float, float, float);
/// A dragged crop rect in fractions of the frame, then the corner the drag
/// is holding as a `0`/`1` pair — or `-1, -1` for a move, which keeps the
/// shape it has. See `CropOverlay` in crop.slint for why the corner has to
/// travel with the rect.
callback crop-changed(float, float, float, float, float, float);
/// Lock the crop to one of `crop-aspects`, by index.
callback crop-aspect-picked(int);
/// Stand the chosen ratio on its other edge.
callback crop-portrait-toggled();
// --- rotation, flips and straightening (FR-DEV-3) ---
//
@@ -1758,7 +1779,9 @@ in property <bool> panel-visible: true;
crop-w: root.crop-w;
crop-h: root.crop-h;
crop-changed(x, y, w, h) => { root.crop-changed(x, y, w, h); }
crop-changed(x, y, w, h, hx, hy) => {
root.crop-changed(x, y, w, h, hx, hy);
}
}
// --- gradient handles ---------------------------------------
@@ -2118,6 +2141,19 @@ in property <bool> panel-visible: true;
flip-v: root.flip-v;
modified: root.framing-modified;
// The ratio chips belong to the crop tool, so
// they appear with it. Shown at all times they
// would be a control for a gesture that is not
// on screen; hidden behind a lid they would be
// a feature nobody finds.
cropping: root.cropping;
aspects: root.crop-aspects;
aspect: root.crop-aspect;
portrait: root.crop-portrait;
aspect-turnable: root.crop-aspect-turnable;
aspect-picked(i) => { root.crop-aspect-picked(i); }
portrait-toggled => { root.crop-portrait-toggled(); }
rotate(turns) => { root.rotate-quarters(turns); }
flip-h-toggled => { root.flip-h-toggled(); }
flip-v-toggled => { root.flip-v-toggled(); }