Fit the photograph clear of the roll and the floating row while composing

A crop handle straddles the picture's edge. With the photograph fitted
to the whole canvas, the bottom handles sat in the photo roll's band,
whose swipe handler takes every press inside it, and the bottom-left
one under the "Done Composing" row, which is declared after the overlay
and whose buttons press 44px tall. Both were drawn and could not be
grabbed: on a laptop-shaped window, one or the other for nearly any
photograph.

While composing, the canvas box is inset 16px from its clip and stops
above the band, the row's margin and the row's hit height. The band is
measured once, on the canvas, and the row reads it from there.
This commit is contained in:
2026-09-29 21:31:27 -04:00
parent 9558b77759
commit 4da2ec39b3
3 changed files with 107 additions and 69 deletions
+55 -17
View File
@@ -1823,8 +1823,10 @@ in property <bool> panel-visible: true;
clip: true;
canvas-image := Image {
width: 100%;
height: 100%;
x: parent.fit-x;
y: parent.fit-y;
width: parent.fit-w;
height: parent.fit-h;
source: root.canvas;
image-fit: contain;
// TRACES: FR-UI-4
@@ -1872,8 +1874,10 @@ in property <bool> panel-visible: true;
// original straight away, and the last draft of the edit
// fades out over it rather than vanishing.
previous-frame := Image {
width: 100%;
height: 100%;
x: canvas-image.x;
y: canvas-image.y;
width: canvas-image.width;
height: canvas-image.height;
source: root.canvas-previous;
image-fit: contain;
image-rendering: canvas-image.image-rendering;
@@ -1941,15 +1945,54 @@ in property <bool> panel-visible: true;
// The overlay is placed against *this*, not against the whole
// area — otherwise the crop rect drifts off the picture on any
// window whose shape differs from the image's.
//
// **While composing, the box is inset and stops above the
// photo roll.** A crop handle straddles the picture's edge,
// half of it outside, and a photograph fitted to the full
// box puts that half under this area's clip — and, along
// the bottom, under the roll's band, whose swipe handler
// takes every press inside it (see `PhotoRoll`). The bottom
// corners were drawn there, lit up, and could not be
// grabbed. Fitting the picture clear of both is the only
// fix: the handler has to be where it is, and moving the
// handles inward would leave them off the corner they
// move.
property <length> roll-band:
Library.library-total > 0
? (root.roll-open
? Theme.roll-strip + Theme.roll-reach
: Theme.roll-reach)
: 0px;
// The roll's own travel, so what stands on it rises with
// the strip rather than jumping ahead of it.
animate roll-band { duration: 180ms; easing: ease-out; }
//
// The floating "Done Composing" row stands on that band,
// and is declared after the crop overlay so it takes a
// press first: a photograph filling the width put the
// bottom-left handle under it. So the reserve is the band,
// the row's 12px margin, and the row's hit height — a
// `Button`'s press area is the touch target, not its ink.
// Every other side keeps half a handle and a finger's
// slop clear of this area's clip.
property <length> fit-inset: Develop.cropping ? 16px : 0px;
property <length> fit-below: Develop.cropping
? self.roll-band + 12px + Theme.touch-target
: 0px;
property <length> fit-x: self.fit-inset;
property <length> fit-y: self.fit-inset;
property <length> fit-w: max(1px, self.width - 2 * self.fit-inset);
property <length> fit-h: max(1px, self.height - 2 * self.fit-inset - self.fit-below);
property <float> src-w: root.canvas.width > 0 ? root.canvas.width : 1;
property <float> src-h: root.canvas.height > 0 ? root.canvas.height : 1;
property <float> box-w: self.width / 1px;
property <float> box-h: self.height / 1px;
property <float> box-w: self.fit-w / 1px;
property <float> box-h: self.fit-h / 1px;
property <float> fit-scale: min(self.box-w / self.src-w, self.box-h / self.src-h);
property <length> shown-w: self.src-w * self.fit-scale * 1px;
property <length> shown-h: self.src-h * self.fit-scale * 1px;
property <length> shown-x: (self.width - self.shown-w) / 2;
property <length> shown-y: (self.height - self.shown-h) / 2;
property <length> shown-x: self.fit-x + (self.fit-w - self.shown-w) / 2;
property <length> shown-y: self.fit-y + (self.fit-h - self.shown-h) / 2;
// Empty and error states say what happened rather than
// showing a blank canvas.
@@ -2904,15 +2947,10 @@ in property <bool> panel-visible: true;
// the thing that could not be reached. They stack
// instead — the controls ride on top of the roll's
// band, at whatever height it currently stands.
property <length> roll-clear:
Library.library-total > 0
? (root.roll-open
? Theme.roll-strip + Theme.roll-reach
: Theme.roll-reach)
: 0px;
// The roll's own travel, so they rise with the strip
// rather than jumping ahead of it.
animate roll-clear { duration: 180ms; easing: ease-out; }
// The canvas's measure of the band, animated there
// with the roll's own travel so these rise with the
// strip rather than jumping ahead of it.
property <length> roll-clear: canvas-area.roll-band;
x: 12px;
y: parent.height - self.preferred-height - 12px