Let a photographer name the state they liked, and go back to it or look at it
FR-DEV-5 asked for named snapshots of an edit state and FR-DEV-7 for a comparison against a chosen one, and neither existed. The history stack is per sitting and forgotten with it, on purpose — the gap that mattered was an automatically saved mis-drag with no way back, and that was closed first. What was left was the other half: a state the photographer wants to keep *because* it is worth keeping, which is a different thing from a step and is not served by making the steps last longer. A snapshot is an edit state, and an edit state is exactly what a sidecar version stores, so it is stored as one: a `[version]` block carrying `snapshot-of = <uuid>`. The parameters, the masks and their parts, the repairs and the film all arrive through the blocks that already carry them, a merge keys on the uuid as it does for any version, and a build that predates the key reads the block as a named version and keeps it — the right failure. Only the pointer is new. The one reader that has to know is `default_version`, which must never answer with a snapshot: a file whose edit is missing is not a file whose edit is one of its saved moments. The snapshots of an edit are listed by that pointer, oldest first, the same on every device. Writing them back removes what this sitting deleted and puts in what it holds, and leaves standing whatever it never saw — a snapshot the other device took since the photograph was opened here is not this device's to remove by not knowing about it. That is the rule the version merge already keeps, applied one level down, and it is why the save carries the deleted ids rather than replacing the list wholesale as the masks are. Each is re-pointed at the uuid the save settled on, because the default may have been fused onto its canonical identity since the snapshot was taken. Restoring is one history step, so undo takes it back whole, as a paste is. Taking and deleting are not steps: they change nothing about the photograph, and an undo that removed a snapshot would be undoing a decision to remember. Holding the eye beside one renders the snapshot and hands the edit straight back — the same suspension "Before" uses, against a point the photographer chose rather than the file. Two sessions on the same photograph get ids that cannot collide, stamped with the second and a random word, because the merge folds equal ids into one.
This commit is contained in:
+152
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Develop } from "session.slint";
|
||||
import { PanelHeading, Caption, Label, Value, Button } from "widgets.slint";
|
||||
import { PanelHeading, Caption, Label, Value, Button, Field } from "widgets.slint";
|
||||
|
||||
// One step, flattened for Slint's model system.
|
||||
export struct HistoryRow {
|
||||
@@ -40,6 +40,105 @@ export struct HistoryRow {
|
||||
undone: bool,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-5
|
||||
/// One named snapshot of the open edit.
|
||||
export struct SnapshotRow {
|
||||
/// Routing back to the core. Opaque to this file.
|
||||
id: string,
|
||||
name: string,
|
||||
/// The canvas is showing this one instead of the edit, while held.
|
||||
comparing: bool,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-5 | FR-DEV-7
|
||||
/// A snapshot: press it to go back to it, hold the eye beside it to look.
|
||||
component SnapshotItem inherits Rectangle {
|
||||
in property <SnapshotRow> data;
|
||||
in property <bool> enabled: true;
|
||||
callback restored();
|
||||
/// Both edges, like "Before": down shows the snapshot, up shows the edit.
|
||||
callback compared(bool);
|
||||
callback removed();
|
||||
|
||||
height: Theme.row-height;
|
||||
background: root.data.comparing
|
||||
? Theme.selected
|
||||
: (touch.has-hover ? Theme.hover : transparent);
|
||||
|
||||
// Behind the two buttons, so a press on either does its own job and a
|
||||
// press anywhere else on the row is the restore.
|
||||
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.restored(); }
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap-sm;
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Label {
|
||||
text: root.data.name;
|
||||
emphasised: root.data.comparing || touch.has-hover;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
// GESTURE: See the photograph as a snapshot had it
|
||||
// where: Develop
|
||||
// touch: Press and hold the eye beside the snapshot
|
||||
// pointer: Press and hold the eye beside the snapshot
|
||||
// why: The same hold as "Before", against a point the
|
||||
// photographer chose rather than the file: "the version
|
||||
// I liked twenty minutes ago" is how a choice between two
|
||||
// treatments is actually made. It takes no history step
|
||||
// and changes nothing; letting go puts the edit back.
|
||||
//
|
||||
// TRACES: FR-DEV-7
|
||||
eye := TouchArea {
|
||||
width: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
enabled: root.enabled;
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down
|
||||
&& event.button == PointerEventButton.left {
|
||||
root.compared(true);
|
||||
}
|
||||
if event.kind == PointerEventKind.up
|
||||
|| event.kind == PointerEventKind.cancel {
|
||||
root.compared(false);
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "◐";
|
||||
emphasised: eye.has-hover || root.data.comparing;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
cut := TouchArea {
|
||||
width: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.removed(); }
|
||||
|
||||
Label {
|
||||
text: "×";
|
||||
emphasised: cut.has-hover;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StepRow inherits Rectangle {
|
||||
in property <HistoryRow> data;
|
||||
in property <bool> enabled: true;
|
||||
@@ -115,6 +214,18 @@ export global Steps {
|
||||
callback redo();
|
||||
/// A row's own `index`, not its position in `rows`.
|
||||
callback picked(int);
|
||||
|
||||
/// TRACES: FR-DEV-5
|
||||
/// The named snapshots of the open edit, oldest first. Rust owns the
|
||||
/// list; the panel only names, restores, deletes and holds them.
|
||||
in property <[SnapshotRow]> snapshots;
|
||||
/// The name typed for the next one. Empty is allowed: Rust numbers it.
|
||||
callback snapshot-taken(string);
|
||||
callback snapshot-restored(string);
|
||||
callback snapshot-removed(string);
|
||||
/// TRACES: FR-DEV-7
|
||||
/// `true` on the way down, `false` on the way up.
|
||||
callback snapshot-compared(string, bool);
|
||||
}
|
||||
|
||||
export component HistoryPanel inherits Rectangle {
|
||||
@@ -189,6 +300,46 @@ export component HistoryPanel inherits Rectangle {
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-5
|
||||
// Snapshots above the steps: a snapshot is the state worth keeping
|
||||
// out of a run of steps, so it sits where the steps lead to.
|
||||
//
|
||||
// GESTURE: Keep the photograph as it is now, under a name
|
||||
// where: Develop
|
||||
// touch: Type a name in the History panel and press Snapshot
|
||||
// pointer: Type a name in the History panel and press Snapshot
|
||||
// why: The history is forgotten with the sitting, on purpose;
|
||||
// a snapshot is the photographer saying this one should
|
||||
// not be. It is written into the sidecar as a version
|
||||
// of the edit, so it survives a restart and reaches the
|
||||
// other device. Pressing a snapshot puts the photograph
|
||||
// back to it, as one step that undo takes back whole.
|
||||
if Develop.enabled: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
name := Field {
|
||||
label: "Snapshot name";
|
||||
placeholder: "Name this state";
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Snapshot";
|
||||
clicked => {
|
||||
Steps.snapshot-taken(name.text);
|
||||
name.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for snapshot in Steps.snapshots: SnapshotItem {
|
||||
data: snapshot;
|
||||
enabled: Develop.enabled;
|
||||
restored => { Steps.snapshot-restored(snapshot.id); }
|
||||
compared(down) => { Steps.snapshot-compared(snapshot.id, down); }
|
||||
removed => { Steps.snapshot-removed(snapshot.id); }
|
||||
}
|
||||
|
||||
if Develop.enabled && Steps.rows.length > 0: Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
|
||||
Reference in New Issue
Block a user