`Attribute::Geometry` becomes `Attribute::Compose`, and `film_sim` moves from `[tone, colour]` to `[effect]`. Two categories were doing the wrong job. "Geometry" describes what crop, straighten and the quarter turns do to coordinates — but it describes lens distortion correction exactly as well, and that is not a compositional choice at all. Naming the attribute for the photographer's decision is what separates it from `Optics`: one is what the lens did, the other is what they chose. The maths the two have in common is not the thing worth filing them under. A film stock declared both `tone` and `colour`, so "Kodachrome" appeared in the Light group beside exposure and again in Colour beside white balance — two places, neither of which is where anyone looks for it. It is neither: `Effect` is defined in this same file as "applied rather than corrected — a look, not a fix", which is what a stock is. That it moves tone and colour is true of every look, and is not what the attribute is for. `from_name` still accepts "geometry" on the way in. That string is persisted in `develop.copy_attributes`, and an entry it fails to parse is not an error — `presets::scope_for` logs it and drops it — so without the alias an existing settings file would have quietly narrowed what a paste carries. `name` writes the current spelling, so the file migrates itself the first time it is saved. 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. `ComposePanel` 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); }
|
|
}
|
|
}
|
|
}
|