Give a mis-drag a way back
Develop edits now save themselves to a sidecar the moment you leave the image, so until this there was no way to undo one — the mistake was persisted and the only recourse was to remember the old number. The history is a stack of snapshots, because the edit graph is already plain data: `Preset::capture` reduces it to what differs from default and `Preset::apply` puts it back, so undo is those two calls and nothing else. A command object per action, with an inverse beside it, would have been a second thing every operation had to register — and operations are declared in YAML precisely so that a new one needs no code written for it. A snapshot cannot fall behind them. The interesting part is coalescing. A slider drag emits an event per frame and must be one step, not forty. Nothing in the interface reports a gesture boundary — the same wall the render coalescing hit, and it is answered the same way rather than by threading a "finger is down" out of every slider, curve point and crop handle. What stands in for the boundary is the control plus recency: changes to the same control within 700 ms amend one step. Which control is "the same" is asked of the graph, not listed: an operation whose declared presentation claims a parameter is one where a single gesture moves several — a curve point carries an x and a y — so those coalesce as one widget. Nothing in the history names the tone curve. The compromise, and it is a real one: a control let go of and picked up again within the window is one step rather than two. Buying the other answer costs a gesture-boundary signal on every control, which is more surface than the difference is worth. The stack is bounded at 64 states for NFR-RES-1 — a develop session stays open for hours. Sixty-four rather than a byte cap: what is being bounded is steps a photographer would want back, and a byte cap would give the elaborate edit the shallowest history, which is exactly backwards. The session owns its history and every mutator records into it, so the callbacks in `lib.rs` cannot change the edit and forget to — with a dozen generic callbacks that would have been one press of undo away from wrong every time a control was added. Opening a photograph makes its stored edit the floor rather than a step: it is not work done in this sitting, and an undo reaching behind it would discard a previous session's edit and then save that on the way out. Not yet done, from FR-DEV-5: history is per-session and in memory, and there are no named snapshots. What mattered was that a saved mis-drag had no way back at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,11 +33,16 @@ component StatusBar inherits Rectangle {
|
||||
/// dialogue: an export that succeeded needs no acknowledging, and one
|
||||
/// that failed needs its reason where the retry is.
|
||||
in property <string> export-status;
|
||||
/// Whether the edit can be stepped either way (FR-DEV-5).
|
||||
in property <bool> can-undo: false;
|
||||
in property <bool> can-redo: false;
|
||||
|
||||
callback back-to-library();
|
||||
callback open-settings();
|
||||
callback toggle-panel();
|
||||
callback export-image();
|
||||
callback undo();
|
||||
callback redo();
|
||||
|
||||
// 44px and `surface`, the same bar the library and settings draw.
|
||||
//
|
||||
@@ -86,6 +91,25 @@ component StatusBar inherits Rectangle {
|
||||
|
||||
Caption { text: root.layout-class; }
|
||||
|
||||
// Undo and redo, in the strip rather than in the develop column: the
|
||||
// panel can be put away, and the one control that takes back a
|
||||
// mis-drag must not go away with it. Disabled rather than hidden, so
|
||||
// the pair keeps its place and the keyboard shortcut has something
|
||||
// visible to correspond to.
|
||||
Button {
|
||||
text: "Undo";
|
||||
enabled: root.can-undo;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.undo(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Redo";
|
||||
enabled: root.can-redo;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.redo(); }
|
||||
}
|
||||
|
||||
// Show or hide the develop column. On a tablet the panel is 280px of a
|
||||
// screen that is mostly photograph, and the whole point of opening an
|
||||
// image is to look at it — so being able to put the instruments away
|
||||
@@ -253,6 +277,16 @@ export component AppWindow inherits Window {
|
||||
callback straighten-changed(float);
|
||||
/// Crop, angle, rotation and flips back to neutral, leaving colour alone.
|
||||
callback framing-reset();
|
||||
|
||||
// --- undo and redo (FR-DEV-5) ---
|
||||
//
|
||||
// Rust owns whether there is anywhere to step, because "anywhere" is a
|
||||
// position in a stack of snapshots this side never sees. A whole drag is
|
||||
// one step; the coalescing that makes it so lives in `dr-pipeline`.
|
||||
in property <bool> can-undo: false;
|
||||
in property <bool> can-redo: false;
|
||||
callback undo();
|
||||
callback redo();
|
||||
/// Scroll-to-zoom: factor, and the anchor in fractions of the visible area.
|
||||
callback zoom-at(float, float, float);
|
||||
callback pan-by(float, float);
|
||||
@@ -1015,10 +1049,14 @@ in property <bool> panel-visible: true;
|
||||
export-label: root.export-label;
|
||||
export-busy: root.export-busy;
|
||||
export-status: root.export-status;
|
||||
can-undo: root.can-undo;
|
||||
can-redo: root.can-redo;
|
||||
back-to-library() => { root.back-to-library(); }
|
||||
open-settings() => { root.settings-open(); }
|
||||
toggle-panel() => { root.toggle-panel(); }
|
||||
export-image() => { root.export-image(); }
|
||||
undo() => { root.undo(); }
|
||||
redo() => { root.redo(); }
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
@@ -1371,6 +1409,19 @@ in property <bool> panel-visible: true;
|
||||
init => { self.focus(); }
|
||||
|
||||
key-pressed(event) => {
|
||||
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5). Both cases
|
||||
// of the letter, because the logical key that
|
||||
// reaches us carries the shift: holding it for
|
||||
// redo turns "z" into "Z".
|
||||
if (event.modifiers.control
|
||||
&& (event.text == "z" || event.text == "Z")) {
|
||||
if (event.modifiers.shift) {
|
||||
root.redo();
|
||||
} else {
|
||||
root.undo();
|
||||
}
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.RightArrow || event.text == " ") {
|
||||
root.next-image();
|
||||
return accept;
|
||||
|
||||
Reference in New Issue
Block a user