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:
@@ -9,7 +9,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { PanelHeading, Label, Value, Caption, Button, IconButton, Swatch } from "widgets.slint";
|
||||
import { SliderTrack, ControlRow, CurveEditor, Segmented } from "controls.slint";
|
||||
import { SliderTrack, ControlRow, CurveEditor, Segmented, ChoiceChip } from "controls.slint";
|
||||
|
||||
// One parameter, flattened for Slint's model system.
|
||||
//
|
||||
@@ -496,11 +496,25 @@ export component GeometryPanel inherits Rectangle {
|
||||
/// Any of crop, angle, rotation or flips differs from neutral.
|
||||
in property <bool> modified: false;
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The crop overlay is up, so the ratio lock is a control for a gesture
|
||||
/// that is currently on screen.
|
||||
in property <bool> cropping: false;
|
||||
/// The ratios on offer, named by Rust — see `CropAspect::CHOICES`.
|
||||
in property <[string]> aspects;
|
||||
in property <int> aspect: 0;
|
||||
in property <bool> portrait: false;
|
||||
/// Whether the chosen ratio has a second orientation. `Free` and the
|
||||
/// square do not.
|
||||
in property <bool> aspect-turnable: false;
|
||||
|
||||
callback rotate(int);
|
||||
callback flip-h-toggled();
|
||||
callback flip-v-toggled();
|
||||
callback angle-changed(float);
|
||||
callback angle-reset();
|
||||
callback aspect-picked(int);
|
||||
callback portrait-toggled();
|
||||
callback reset();
|
||||
|
||||
/// TRACES: FR-UI-2
|
||||
@@ -592,6 +606,54 @@ export component GeometryPanel inherits Rectangle {
|
||||
changed(v) => { root.angle-changed(v); }
|
||||
reset => { root.angle-reset(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-3
|
||||
// The ratio the crop is held to.
|
||||
//
|
||||
// **A grid rather than a row.** Six chips laid side by side need
|
||||
// over 400px, and this column's width is the largest any panel
|
||||
// declares — so a single row would widen every other panel in the
|
||||
// application to fit a control that is on screen only while
|
||||
// cropping. Three columns is what the column already has room for.
|
||||
//
|
||||
// The chips are drawn from the model rather than written out, so
|
||||
// the offered set lives in one place: `CropAspect::CHOICES`. Their
|
||||
// positions are computed from the index for the same reason — a
|
||||
// seventh ratio wraps onto a third row by itself.
|
||||
if root.cropping: VerticalLayout {
|
||||
spacing: 4px;
|
||||
padding-top: Theme.gap-sm;
|
||||
|
||||
Label { text: "Ratio"; body: true; }
|
||||
|
||||
GridLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
for name[i] in root.aspects: ChoiceChip {
|
||||
row: floor(i / 3);
|
||||
col: mod(i, 3);
|
||||
label: name;
|
||||
selected: i == root.aspect;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.aspect-picked(i); }
|
||||
}
|
||||
}
|
||||
|
||||
// One chip, lit when the ratio is standing on its short edge,
|
||||
// rather than a Landscape/Portrait pair: the second chip in
|
||||
// such a pair says nothing the first does not, and this row is
|
||||
// already the widest thing in the panel.
|
||||
//
|
||||
// Disabled rather than hidden where the ratio has no second
|
||||
// orientation, so the panel does not change height as the
|
||||
// chips above it are tried.
|
||||
ChoiceChip {
|
||||
label: "Portrait";
|
||||
selected: root.portrait;
|
||||
enabled: root.enabled && root.aspect-turnable;
|
||||
clicked => { root.portrait-toggled(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user