Build and test / Desktop (Linux) (push) Successful in 21m53s
Build and test / Layer separation (push) Successful in 28s
Traceability / Requirement traces (push) Successful in 32s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 53m45s
The column was 280px, a number chosen for a tablet, with 380px bolted on later for a desktop. Both were guesses at how much room the widest row inside needs, and a guess is what cannot work here: the mode strip is one chip per attribute the *operation set declares*, so the row is generated and no constant in app.slint can track it. When the guess came up short the failure was not a tidy clip. The Flickable inside the column never had its `viewport-width` set, so the viewport took its content's preferred width, and a viewport wider than its Flickable is *centred* in it — the same rule the note on the seam's `x: 0` already records a few lines below. So the column lost half of each edge rather than one of them: "HISTOGRAM" read "ISTOGRAM", "Straighten" read "aighten", Copy sat centred while Paste ran off the far side. It looked like a rendering fault and it was an alignment one. So the column asks instead of guessing. Every panel that can appear in it — image, histogram, geometry, settings transfer, masks, repairs, adjust, history — now publishes a `content-width`: how wide it has to be before it starts clipping itself, read off its own layout rather than asserted. Each declares that as its `min-width` too, and that is what makes the aggregation automatic: `column` is a layout, so it already reports the largest minimum among its children, and it does so for the panels that come and go with the mode as well, which live inside `if`s and cannot be named from outside. Grep `content-width` in ui/dr-ui/ui to see every panel with a say in the answer. The mode strip is named explicitly only because it is pinned outside that layout, so nothing else measures it. There is no floor left. A floor is one more guess and the panels state their own minimums now. The only thing still above the measurement is `panel-max-width`, which is not a size but a policy — a column may not take the window from the photograph it exists to serve — and it comes from Rust beside `layout-class` because a width read from `root.width` inside the layout that `root.width` depends on is a binding loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
201 lines
8.2 KiB
Plaintext
201 lines
8.2 KiB
Plaintext
// TRACES: FR-DEV-5 | FR-DEV-7
|
|
// The steps this photograph has been through, and the way back to any of them.
|
|
//
|
|
// **Nothing here names an operation, and nothing here names a step.** The rows
|
|
// arrive already resolved — Rust turns each step's localisation key into a
|
|
// display string against the UI's catalogue — so an operation added to the
|
|
// pipeline as a YAML declaration appears in this list under a sensible name
|
|
// with no change to this file, on the same terms it appears in the panel
|
|
// (FR-DEV-3c).
|
|
//
|
|
// **Why a list and not just the two buttons.** Undo answers "take back the
|
|
// last thing", which is the question a photographer asks about the mistake
|
|
// they have just noticed. It is the wrong instrument for the one they notice
|
|
// six adjustments later: eight presses, each changing the picture, with no way
|
|
// to see how far back the mistake was without passing through it. A step is a
|
|
// whole state in `dr-pipeline`, so arriving at one from six away costs what
|
|
// arriving from one does — which is what makes a row worth making clickable
|
|
// rather than decorative.
|
|
|
|
import { Theme } from "theme.slint";
|
|
import { PanelHeading, Caption, Label, Value, Button } from "widgets.slint";
|
|
|
|
// One step, flattened for Slint's model system.
|
|
export struct HistoryRow {
|
|
// Routing back to the core: this step's position in the stack. **Not** its
|
|
// position in this list, which runs the other way — see `history-rows` in
|
|
// `develop.rs` for why the two are deliberately different numbers.
|
|
index: int,
|
|
|
|
// Resolved in Rust against the UI's catalogue; the core deals in keys.
|
|
label: string,
|
|
|
|
// The state the photograph is in right now. Exactly one row carries it.
|
|
current: bool,
|
|
|
|
// A step the photographer has stepped back *out* of, still reachable by
|
|
// redo. Listed rather than hidden — redo would otherwise arrive somewhere
|
|
// this panel never mentioned — but drawn as the branch it is.
|
|
undone: bool,
|
|
}
|
|
|
|
component StepRow inherits Rectangle {
|
|
in property <HistoryRow> data;
|
|
in property <bool> enabled: true;
|
|
callback picked();
|
|
|
|
height: Theme.row-height;
|
|
background: root.data.current
|
|
? Theme.selected
|
|
: (touch.has-hover ? Theme.hover : transparent);
|
|
|
|
touch := TouchArea {
|
|
width: 100%;
|
|
height: max(parent.height, Theme.touch-target);
|
|
y: (parent.height - self.height) / 2;
|
|
enabled: root.enabled;
|
|
mouse-cursor: root.enabled ? MouseCursor.pointer : MouseCursor.default;
|
|
clicked => { root.picked(); }
|
|
}
|
|
|
|
HorizontalLayout {
|
|
// Small, because the panel around this already pads. The row's
|
|
// background is the highlight for the current step, so it wants to be
|
|
// a band the width of the column rather than a chip inset from it.
|
|
padding-left: Theme.gap-sm;
|
|
padding-right: Theme.gap-sm;
|
|
spacing: Theme.gap-sm;
|
|
|
|
// The mark for where the photograph stands. A filled bar against the
|
|
// leading edge rather than a tick beside the name: the eye finds one
|
|
// edge down a column of forty rows, and a glyph in the text column
|
|
// would have to be read.
|
|
Rectangle {
|
|
width: 2px;
|
|
height: parent.height;
|
|
background: root.data.current ? Theme.active : transparent;
|
|
}
|
|
|
|
Label {
|
|
text: root.data.label;
|
|
emphasised: root.data.current || touch.has-hover;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
// Dimmed rather than removed: this step is a future the
|
|
// photographer stepped out of, and it is still where redo goes.
|
|
opacity: root.data.undone ? 0.45 : 1.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
export component HistoryPanel inherits Rectangle {
|
|
in property <[HistoryRow]> rows;
|
|
in property <bool> enabled: true;
|
|
in property <bool> can-undo: false;
|
|
in property <bool> can-redo: false;
|
|
/// What undo would take back, already resolved. Empty when there is
|
|
/// nowhere to go.
|
|
in property <string> undo-label: "";
|
|
|
|
callback undo();
|
|
callback redo();
|
|
/// A row's own `index`, not its position in `rows`.
|
|
callback picked(int);
|
|
|
|
background: Theme.surface;
|
|
|
|
// **Flat, deliberately** — `MaskPanel` carries the long version of this
|
|
// and it applies here unchanged: a nested layout under-reports its height,
|
|
// so what follows it gets drawn on top of what came before, which is
|
|
// invisible in the source and obvious the moment anyone opens the panel.
|
|
// Every element is a direct child of the one layout that measures them,
|
|
// and the ones that come and go carry their own condition rather than
|
|
// being grouped inside a wrapper.
|
|
//
|
|
// No explicit height either, for the same reason `AdjustPanel` and
|
|
// `MaskPanel` declare none: the row count changes as the photographer
|
|
// works, and a height pinned to a layout's preferred size is one more
|
|
/// TRACES: FR-UI-2
|
|
/// How wide this panel has to be before it starts clipping itself. The
|
|
/// develop column is the largest of these and nothing else; publishing it
|
|
/// as `min-width` too is what makes the enclosing layout aggregate them
|
|
/// without this panel having to be referenced by name.
|
|
out property <length> content-width: layout.preferred-width;
|
|
min-width: root.content-width;
|
|
|
|
// thing that has to keep up with a repeater.
|
|
layout := VerticalLayout {
|
|
padding: Theme.gap;
|
|
spacing: Theme.gap-sm;
|
|
alignment: start;
|
|
|
|
// A heading, not a collapsible. `GeometryPanel` carries the argument
|
|
// and it holds here: the list is last in the column, so what a lid
|
|
// would save is scrolling past nothing.
|
|
HorizontalLayout {
|
|
PanelHeading { text: "HISTORY"; }
|
|
Rectangle { horizontal-stretch: 1; }
|
|
if root.enabled && root.rows.length > 1: Value {
|
|
text: root.rows.length - 1 + (root.rows.length == 2 ? " step" : " steps");
|
|
}
|
|
}
|
|
|
|
if !root.enabled: Caption { text: "No image"; }
|
|
|
|
// The pair, here as well as in the status strip. The strip's copy is
|
|
// the one that survives the column being put away; this one sits
|
|
// against the list that says what it will do, which is the pairing
|
|
// that makes either of them legible.
|
|
if root.enabled: HorizontalLayout {
|
|
spacing: Theme.gap-sm;
|
|
|
|
Button {
|
|
text: "Undo";
|
|
enabled: root.can-undo;
|
|
horizontal-stretch: 1;
|
|
clicked => { root.undo(); }
|
|
}
|
|
|
|
Button {
|
|
text: "Redo";
|
|
enabled: root.can-redo;
|
|
horizontal-stretch: 1;
|
|
clicked => { root.redo(); }
|
|
}
|
|
}
|
|
|
|
// What the left-hand button would take back, spelled out.
|
|
//
|
|
// On a caption rather than on the button, because `Button` sizes to
|
|
// its label and does not elide: "Undo Highlights & Shadows" is most of
|
|
// a 280px column on its own, and two of those would lever the column
|
|
// open — `MaskPanel` has the same note about an unwrapped sentence.
|
|
//
|
|
// Undo only. Redo's destination is the row directly above the mark in
|
|
// the list below, where it can be seen; the step undo takes back is
|
|
// the one the photographer is standing on and stopped tracking after a
|
|
// run of small adjustments, which is the moment they are least willing
|
|
// to press a button to find out.
|
|
if root.enabled && root.can-undo: Caption {
|
|
text: "Undo: " + root.undo-label;
|
|
overflow: elide;
|
|
}
|
|
|
|
if root.enabled && root.rows.length > 0: Rectangle {
|
|
height: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
|
|
// Newest first. The list is consulted to take back something just
|
|
// done rather than browsed from the beginning — the order
|
|
// `dr_catalog::trash` settled on for the same question — and it keeps
|
|
// the end being worked at against the heading rather than sixty-four
|
|
// rows below it.
|
|
for row in root.rows: StepRow {
|
|
data: row;
|
|
enabled: root.enabled;
|
|
picked => { root.picked(row.index); }
|
|
}
|
|
}
|
|
}
|