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>
This commit is contained in:
@@ -486,3 +486,107 @@ export component MaskPanel inherits Rectangle {
|
||||
// the answer on the thing that changed rather than above it.
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3 | FR-UI-3
|
||||
/// The mask's handles, drawn on the photograph.
|
||||
///
|
||||
/// A linear or radial mask could be made and then never moved: there was
|
||||
/// nothing to grab, so a radial sat at the centre of the frame at its default
|
||||
/// size for ever.
|
||||
///
|
||||
/// **Placed over the fitted image, not the canvas box.** The photograph is
|
||||
/// letterboxed inside the canvas and Slint does not report the fitted rect, so
|
||||
/// the develop view derives it and gives this component exactly that rect —
|
||||
/// which is why everything in here is a plain fraction of `root.width` and
|
||||
/// `root.height` rather than an offset from someone else's origin. Rust
|
||||
/// supplies the positions already mapped through the framing — the same map
|
||||
/// the shader applies — so a handle follows the mask through a zoom, a pan, a
|
||||
/// crop and a straightening rather than sitting where the mask used to be.
|
||||
///
|
||||
/// **Drawn small, grabbed large.** The visible dot is 14px because a bigger
|
||||
/// one would hide the edge it is placed on, and the `TouchArea` is a full
|
||||
/// touch target, centred on it — `Button` establishes the same split. These
|
||||
/// are the first controls in the application meant to be dragged on the
|
||||
/// photograph, and a 12-inch tablet has no hover to reveal them with and no
|
||||
/// modifier to qualify them by, so what is drawn is all there is (FR-UI-7).
|
||||
export component GradientHandles inherits Rectangle {
|
||||
in property <[GradientHandle]> handles;
|
||||
|
||||
/// role, from-x, from-y, to-x, to-y — all fractions of this rect.
|
||||
callback dragged(HandleRole, float, float, float, float);
|
||||
/// One history step per gesture, not per frame.
|
||||
callback released();
|
||||
|
||||
background: transparent;
|
||||
|
||||
for handle in root.handles: Rectangle {
|
||||
x: handle.x * root.width - self.width / 2;
|
||||
y: handle.y * root.height - self.height / 2;
|
||||
width: Theme.touch-target;
|
||||
height: Theme.touch-target;
|
||||
|
||||
// The centre moves the whole mask, so it is filled; the others shape
|
||||
// it and are rings. Shape rather than colour, because the handles sit
|
||||
// on a photograph and any colour they carried would be read as part
|
||||
// of it.
|
||||
property <bool> solid: handle.role == HandleRole.centre;
|
||||
|
||||
Rectangle {
|
||||
width: handle.role == HandleRole.rotate ? 12px : 14px;
|
||||
height: self.width;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
border-radius: self.width / 2;
|
||||
border-width: 2px;
|
||||
// White with a dark ring, so the handle is visible against a blown
|
||||
// sky and against a black frame — one treatment, both extremes, no
|
||||
// theme token because this is drawn over the image and not over
|
||||
// the interface.
|
||||
border-color: #00000099;
|
||||
background: parent.solid ? #ffffff : #ffffff44;
|
||||
}
|
||||
|
||||
drag := TouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mouse-cursor: parent.solid
|
||||
? MouseCursor.move
|
||||
: MouseCursor.crosshair;
|
||||
|
||||
// Where the press landed, in the same fractions the callback
|
||||
// reports in. Captured on the way down so the whole gesture is
|
||||
// measured from one origin.
|
||||
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.from-x = self.fraction-x(self.pressed-x);
|
||||
self.from-y = self.fraction-y(self.pressed-y);
|
||||
}
|
||||
if (ev.kind == PointerEventKind.up) {
|
||||
root.released();
|
||||
}
|
||||
}
|
||||
|
||||
moved => {
|
||||
if (self.pressed) {
|
||||
root.dragged(
|
||||
handle.role,
|
||||
self.from-x,
|
||||
self.from-y,
|
||||
self.fraction-x(self.mouse-x),
|
||||
self.fraction-y(self.mouse-y),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user