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:
@@ -1838,6 +1838,11 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
// the list is rebuilt when it would read differently, not when the picture
|
||||
// is redrawn, and those are very different rates.
|
||||
let drawn_history: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
|
||||
// TRACES: FR-DEV-5
|
||||
// And the snapshot list, compared by value rather than by a revision: it
|
||||
// is a handful of rows, and a held comparison changes one of them
|
||||
// without a step being taken.
|
||||
let drawn_snapshots: Rc<RefCell<Option<Vec<SnapshotRow>>>> = Rc::new(RefCell::new(None));
|
||||
|
||||
// TRACES: FR-CULL-3
|
||||
// How the photographer wants focus peaking drawn, or `None` for off.
|
||||
@@ -1874,6 +1879,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let session = session.clone();
|
||||
let viewport = viewport.clone();
|
||||
let drawn_history = drawn_history.clone();
|
||||
let drawn_snapshots = drawn_snapshots.clone();
|
||||
let display = display.clone();
|
||||
let chosen_peaking = chosen_peaking.clone();
|
||||
Rc::new(move |window: &AppWindow, draft: bool| {
|
||||
@@ -1916,6 +1922,16 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
steps.set_rows(slint::ModelRc::new(slint::VecModel::from(s.history_rows())));
|
||||
steps.set_undo_label(s.undo_label().into());
|
||||
}
|
||||
// TRACES: FR-DEV-5
|
||||
// The snapshots, on the same path. Rebuilt only when the list
|
||||
// would read differently, for the reason the steps are: a held
|
||||
// comparison redraws, and it is the one thing here that changes
|
||||
// a row without changing the history.
|
||||
let snapshots = s.snapshot_rows();
|
||||
if drawn_snapshots.borrow().as_ref() != Some(&snapshots) {
|
||||
*drawn_snapshots.borrow_mut() = Some(snapshots.clone());
|
||||
steps.set_snapshots(slint::ModelRc::new(slint::VecModel::from(snapshots)));
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-3
|
||||
// Which part of the region overlay the view is showing. Here
|
||||
@@ -1979,6 +1995,10 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
s.render_uncropped(w, h).map(|(image, _, _)| image)
|
||||
} else if window.get_showing_original() {
|
||||
s.render_original(w, h)
|
||||
} else if s.compared_snapshot().is_some() {
|
||||
// TRACES: FR-DEV-7
|
||||
// The same hold, against a snapshot rather than the file.
|
||||
s.render_compared(w, h)
|
||||
} else {
|
||||
s.render(w, h)
|
||||
};
|
||||
@@ -3043,6 +3063,80 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
}
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-5
|
||||
// Named snapshots. Taking and deleting one change the list and nothing
|
||||
// else — no rows, no history — so they redraw only to push the list,
|
||||
// through the same path everything else pushes it on. Restoring one is
|
||||
// the paste's shape: the whole edit moves, so the rows resync and the
|
||||
// picture is redrawn.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.global::<Steps>().on_snapshot_taken(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.take_snapshot(&name);
|
||||
}
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.global::<Steps>().on_snapshot_restored(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let restored = session
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.is_some_and(|s| s.restore_snapshot(&id));
|
||||
if restored {
|
||||
sync_rows(&w, &rows, &session);
|
||||
// The mask panel too: a snapshot carries its layers, and
|
||||
// the redraw syncs the repairs and the overlay but not the
|
||||
// rows the layers are listed in.
|
||||
masks_ui::sync(&w, &session);
|
||||
redraw(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.global::<Steps>().on_snapshot_removed(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.delete_snapshot(&id);
|
||||
}
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
// TRACES: FR-DEV-7
|
||||
// Holding a snapshot against the edit, and letting it go — the
|
||||
// Before button's shape, and its rule: no rows are synced and no
|
||||
// history is touched. A full frame rather than a draft, for the
|
||||
// reason the original is: a soft comparison shows a difference the
|
||||
// edit does not have.
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let render_now = render_now.clone();
|
||||
window
|
||||
.global::<Steps>()
|
||||
.on_snapshot_compared(move |id, down| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let changed = session
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.is_some_and(|s| s.compare_snapshot(down.then_some(id.as_str())));
|
||||
if changed {
|
||||
render_now(&w, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- zoom, pan and crop ---------------------------------------------
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user