Step to a library photograph without flashing frames in between

A step along the roll showed up to four pictures: the grid thumbnail,
the previous photograph again (the thumbnail was dropped when the bytes
landed, while the canvas still held the last texture through the decode
and first render), the new one at its defaults when a cached original
beat the sidecar, and then its edit.

The thumbnail now stays up until render_now draws the new photograph's
first frame, and that first frame waits up to 400 ms for the stored edit
before drawing at the defaults. A later arrival still redraws.
This commit is contained in:
2026-10-02 20:46:05 -04:00
parent 379dd1afcc
commit 7839b1b056
2 changed files with 95 additions and 57 deletions
File diff suppressed because one or more lines are too long
+58 -20
View File
@@ -794,17 +794,17 @@ pub(crate) fn refresh_export_label(window: &AppWindow) {
/// dragged, because each move event destroys the thing that would deliver
/// the next one.
/// TRACES: FR-CAT-8
/// Apply a fetched sidecar to the open session, now or as soon as it arrives.
/// Apply a fetched sidecar to the open session, then draw its first frame.
///
/// The sidecar fetch is started beside the image fetch and is three orders of
/// magnitude smaller, so it has almost always landed by the time there is a
/// session to apply it to — and this takes it straight from the channel. The
/// timer covers the case where it has not, which is why this is not simply a
/// blocking receive: a slow or stalled sidecar request must not freeze the
/// window with the photograph already decoded and on screen.
///
/// A late arrival redraws, so the image is correct either way; the only
/// difference is whether it was ever briefly shown at its defaults.
/// magnitude smaller, so it has usually landed by the time there is a session
/// to apply it to — and this takes it straight from the channel. An original
/// read from the cache can still beat it, and drawing then showed the
/// photograph at its defaults and changed it a moment later. So the first
/// frame waits up to `SIDECAR_GRACE` for the edit, with the grid's thumbnail
/// still standing in; past that it is drawn at its defaults, and a later
/// arrival redraws. Not a blocking receive: a stalled request must not freeze
/// the window.
///
/// `still_current` is false once the view has moved to another photograph,
/// whose session this sidecar must not be applied to.
@@ -816,11 +816,14 @@ fn apply_when_ready(
redraw: &Rc<dyn Fn(&AppWindow)>,
still_current: impl Fn() -> bool + 'static,
) {
// Already here — the overwhelmingly common case.
const SIDECAR_GRACE: std::time::Duration = std::time::Duration::from_millis(400);
// Already here — the common case.
if let Ok(got) = rx.try_recv() {
if let Some(sidecar) = got {
presets::apply_stored_edit(window, &sidecar, session, rows);
}
redraw(window);
return;
}
@@ -828,21 +831,37 @@ fn apply_when_ready(
let session = session.clone();
let rows = rows.clone();
let redraw = redraw.clone();
let deadline = std::time::Instant::now() + SIDECAR_GRACE;
let drawn = Cell::new(false);
let timer = Rc::new(slint::Timer::default());
let held = timer.clone();
timer.start(
slint::TimerMode::Repeated,
std::time::Duration::from_millis(50),
move || {
let Some(w) = weak.upgrade() else {
held.stop();
return;
};
if !still_current() {
held.stop();
return;
}
let Ok(got) = rx.try_recv() else { return };
let Ok(got) = rx.try_recv() else {
// Waited long enough: draw at the defaults and keep
// listening, so a late edit still lands.
if !drawn.get() && std::time::Instant::now() >= deadline {
drawn.set(true);
redraw(&w);
}
return;
};
held.stop();
let Some(w) = weak.upgrade() else { return };
let Some(sidecar) = got else { return };
if presets::apply_stored_edit(&w, &sidecar, &session, &rows) {
let applied = match got {
Some(sidecar) => presets::apply_stored_edit(&w, &sidecar, &session, &rows),
None => false,
};
if applied || !drawn.get() {
redraw(&w);
}
},
@@ -2252,6 +2271,13 @@ fn build_render_now(
window.set_canvas_draft(draft);
window.global::<Levels>().set_provisional(draft);
window.set_load_error("".into());
// TRACES: FR-NC-6a
// The library path keeps the grid's thumbnail up until
// here, the first frame of the new photograph, because
// the canvas still holds the last photograph's texture
// until this line replaces it.
window.set_load_pending(false);
window.set_has_load_preview(false);
// The readout and the "Fit" button follow the session
// rather than the gesture, so a clamped zoom shows the
// value that was actually applied.
@@ -2787,13 +2813,18 @@ fn wire_remote_open(
log::debug!("{name}: landed after the view moved on");
return;
}
w.set_load_pending(false);
// The bar goes, and the thumbnail goes back to full
// strength; the thumbnail itself stays until the first
// frame of this photograph replaces it in `render_now`.
// Dropping it here showed the last photograph's texture,
// still in the canvas, for the length of the decode.
w.set_load_waiting("".into());
w.set_has_load_preview(false);
let bytes = match got {
Ok(b) => b,
Err(e) => {
w.set_load_pending(false);
w.set_has_load_preview(false);
job.fail(e.message.clone());
log::warn!("{name}: {e}");
// Offline needs its own words. "network error:
@@ -2862,9 +2893,10 @@ fn wire_remote_open(
// TRACES: FR-CAT-8
// The stored edit, if it has landed. It
// was started before the download of a
// file thousands of times its size, so in
// practice it has; `apply_when_ready`
// covers the case where it has not rather
// file thousands of times its size, but
// an original read from the cache can
// beat it; `apply_when_ready` holds the
// first frame back a moment for it rather
// than blocking the UI thread on a socket.
let still = current.clone();
apply_when_ready(
@@ -2886,12 +2918,16 @@ fn wire_remote_open(
// moves the point rather than losing it.
resume_inspection(&session, &viewport, &inspection);
sync_rows(&w, &rows, &session);
redraw(&w);
// No redraw here: `apply_when_ready` makes
// the first one, once the edit is applied
// or has been waited for long enough.
}
None => {
*session.borrow_mut() = None;
rows.set_vec(Vec::<ParamRow>::new());
w.global::<Develop>().set_enabled(false);
w.set_load_pending(false);
w.set_has_load_preview(false);
if let Some(image) = l.fallback {
w.set_canvas(image);
}
@@ -2903,6 +2939,8 @@ fn wire_remote_open(
log::warn!("{name}: {e}");
*session.borrow_mut() = None;
w.global::<Develop>().set_enabled(false);
w.set_load_pending(false);
w.set_has_load_preview(false);
w.set_load_error(e.into());
}
}