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:
@@ -256,6 +256,11 @@ fn reset_view_state(window: &AppWindow) {
|
||||
window.set_flip_h(false);
|
||||
window.set_flip_v(false);
|
||||
window.set_framing_modified(false);
|
||||
// A photograph that failed to decode has no session, so nothing below
|
||||
// will speak for it — and the buttons would otherwise keep offering the
|
||||
// previous image's history.
|
||||
window.set_can_undo(false);
|
||||
window.set_can_redo(false);
|
||||
}
|
||||
|
||||
/// Push the framing back to the geometry panel.
|
||||
@@ -961,6 +966,15 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
Rc::new(move |window: &AppWindow, draft: bool| {
|
||||
let mut slot = session.borrow_mut();
|
||||
let Some(s) = slot.as_mut() else { return };
|
||||
|
||||
// TRACES: FR-DEV-5
|
||||
// Whether undo has anywhere to go, pushed from here because every
|
||||
// edit ends in a redraw and nothing else is on all of their paths:
|
||||
// the parameter callbacks sync rows, the framing ones sync the
|
||||
// geometry panel, and a paste arrives through neither.
|
||||
window.set_can_undo(s.can_undo());
|
||||
window.set_can_redo(s.can_redo());
|
||||
|
||||
let (mut w, mut h) = *viewport.borrow();
|
||||
|
||||
// **Half resolution while the gesture is still moving.**
|
||||
@@ -1505,6 +1519,44 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- undo and redo (FR-DEV-5) ---------------------------------------
|
||||
//
|
||||
// Thin, because the history lives in the session and every mutator there
|
||||
// records into it — see `DevelopSession::history`. What is left for the
|
||||
// interface is the refresh a paste also needs: the controls are showing
|
||||
// values that have just moved underneath them.
|
||||
//
|
||||
// `can-undo` and `can-redo` are not set here; `render_now` pushes them on
|
||||
// every redraw, which is every path that can change them.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.on_undo(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let stepped = session.borrow_mut().as_mut().is_some_and(|s| s.undo());
|
||||
if stepped {
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.on_redo(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let stepped = session.borrow_mut().as_mut().is_some_and(|s| s.redo());
|
||||
if stepped {
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- zoom, pan and crop ---------------------------------------------
|
||||
//
|
||||
// Zoom and pan are viewing state and touch no parameter, so unlike the
|
||||
|
||||
Reference in New Issue
Block a user