Build and test / Desktop (Linux) (push) Failing after 1h14m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 16m30s
Build and test / android-image (push) Successful in 16m31s
Traceability / Requirement traces (push) Successful in 1m47s
Build and test / Android (aarch64) (push) Successful in 1h0m21s
Crop, Local and Repair were chips at the head of the develop column, sharing a row with the adjustment groups and told apart from them by the shape of their highlight. Three things followed from that, and only the last is cosmetic: the column closes, so the way out of a mode went away with the way in — hence the duplicate "Done Cropping" over the canvas; the chips are generated from the operation set, so the widest thing in the sidebar was a row nobody had chosen the contents of; and a mode and a filter are different kinds of state wearing one control. They are a fixed 60px rail down the left now, generated from a single table in toolrail.slint. A tool is one row of it plus a drawing plus a ViewMode variant; nothing in app.slint is touched to add one. What is left of the strip is the group filters, so it is GroupStrip. The column stops measuring itself. Every panel published a content-width and declared it as min-width, and the column took the largest — which spent the photograph's pixels on whatever happened to be widest, and moved the image sideways when switching tools swapped one set of panels for another. It is panel-width now, one number in style.yaml. That number is 360 and it is measured, not picked: the contents report a minimum of 344 in every mode, and they do not compress below it because a Text that does not elide reports the same minimum as preferred. 320 was tried and sliced Paste down the middle. The Flickable's viewport is floored at the layout's minimum rather than its preferred width for the same reason — content that is never told how much room it has cannot adapt to having less. Removing the eight content-width declarations repairs three comments an earlier edit had spliced sentences into. The raw histogram's note on keeping its hint short is rewritten rather than dropped: an over-long hint no longer widens the column, it pushes the column's minimum past the width it has and clips the panel, which makes that constraint sharper rather than obsolete. 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.
|
|
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); }
|
|
}
|
|
}
|
|
}
|