Build and test / Desktop (Linux) (push) Successful in 19m30s
Build and test / Layer separation (push) Successful in 25s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 23s
Build and test / Android (aarch64) (push) Failing after 33m5s
Undo answers "take back the last thing", which is the question asked about a mistake just noticed. It is the wrong instrument for one noticed six adjustments later: eight presses, each changing the picture, with no way to see how far back the mistake is without passing through it. A step is a whole state, so arriving from six away costs what arriving from one does — which is what makes a row worth making clickable rather than decorative. `Edit::Discrete` had to go for the list to be worth drawing. Seventeen call sites recorded the same anonymous step, which is fine for deciding whether two changes are one gesture and useless for a panel: seventeen rows reading "Discrete" is not a history. Every variant now carries enough to name itself, and the compiler enumerated the sites that had to start saying so. A step that moved a parameter is still named out of the descriptor, so an operation added as a YAML declaration appears in the history correctly named with nothing written for it (FR-DEV-3c). Choosing a film stock was not undoable at all. The pick went straight to `choose_film`, which nothing on the history's path ever sees. `pick_film` records it, and is separate because the same call is also how a *restored* edit gets its tables back — recording that would push a step for the undo the photographer had just asked for. The list is rebuilt off a revision rather than off every redraw. A drag ends in a redraw per frame while folding into one step, so the unconditional version would tear down and recreate every row sixty times a second to arrive back at the list already on screen. The counter is process-wide: a per-instance one starts every photograph at the same number, so a frontend holding "the revision I last drew" would keep the previous image's steps on screen — invisible while every image opens with one identical row, and a wrong-photograph bug the moment persisted history means it does not. The step names that no descriptor can supply are constants with a roll, and a test walks the roll rather than a second copy of it. `resolve` splits so that "is this catalogued?" can be asked: `derive` turns `history.mask_toggled` into "Mask Toggled", which names a field rather than an act and, being perfectly readable, is a mistake nobody would look at twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
193 lines
7.8 KiB
Plaintext
193 lines
7.8 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
|
|
// thing that has to keep up with a repeater.
|
|
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); }
|
|
}
|
|
}
|
|
}
|