diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 1c7f080..af36cbf 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -53,6 +53,13 @@ const MAX_DISPLAY_DIM: u32 = 2048; /// compact layout exactly as a tablet in portrait would. const EXPANDED_MIN_WIDTH: f32 = 820.0; +/// How long after the last change a draft frame is replaced by a sharp one. +/// +/// Above the interval between events in a drag, so an ordinary gesture never +/// reaches it and never renders full resolution mid-motion; well below the +/// point where a photographer would notice waiting for the sharp frame. +const SETTLE_DELAY: std::time::Duration = std::time::Duration::from_millis(120); + /// Everything loaded for the currently displayed image. struct Loaded { /// A develop session. `None` only where the file could not be opened for @@ -479,13 +486,27 @@ pub fn run(paths: Vec) -> Result<()> { // // Called on every slider change, so it must do no more than run the // adjust pass — the demosaic is not repeated. - let render_now: Rc = { + let render_now: Rc = { let session = session.clone(); let viewport = viewport.clone(); - Rc::new(move |window: &AppWindow| { + Rc::new(move |window: &AppWindow, draft: bool| { let mut slot = session.borrow_mut(); let Some(s) = slot.as_mut() else { return }; - let (w, h) = *viewport.borrow(); + let (mut w, mut h) = *viewport.borrow(); + + // **Half resolution while the gesture is still moving.** + // + // The adjust pass and the readback both scale with pixel count, so + // halving each edge is roughly a quarter of the work — the + // difference between keeping up with a drag and lagging behind it. + // A draft frame is visible for one gesture and is replaced by a + // full-resolution one the moment motion stops, so the cost is a + // little softness exactly while the image is moving too fast to + // study anyway. + if draft { + w = (w / 2).max(1); + h = (h / 2).max(1); + } // Crop mode shows the whole frame, or the area being cropped away // would not be on screen for the handles to drag across. The @@ -536,13 +557,22 @@ pub fn run(paths: Vec) -> Result<()> { // on the finger rather than settling on a stale frame. let render_pending = Rc::new(std::cell::Cell::new(false)); let render_dirty = Rc::new(std::cell::Cell::new(false)); + // Set while a settle render is already queued, so a burst of draft frames + // schedules exactly one of them rather than one apiece. + let settle_pending = Rc::new(std::cell::Cell::new(false)); + // Whether a request had to be coalesced into one already queued — see the + // gesture note below, where this stands in for a drag boundary. + let was_coalesced = Rc::new(std::cell::Cell::new(false)); let redraw: Rc = { let render_now = render_now.clone(); let render_pending = render_pending.clone(); let render_dirty = render_dirty.clone(); + let settle_pending = settle_pending.clone(); + let was_coalesced = was_coalesced.clone(); Rc::new(move |window: &AppWindow| { render_dirty.set(true); if render_pending.get() { + was_coalesced.set(true); return; } render_pending.set(true); @@ -551,6 +581,8 @@ pub fn run(paths: Vec) -> Result<()> { let render_now = render_now.clone(); let render_pending = render_pending.clone(); let render_dirty = render_dirty.clone(); + let settle_pending = settle_pending.clone(); + let was_coalesced = was_coalesced.clone(); // A zero-delay `Timer` rather than `invoke_from_event_loop`: the // latter demands `Send`, and every piece of state here is `Rc` on // the UI thread by design. The delay being zero is the point — this @@ -561,7 +593,36 @@ pub fn run(paths: Vec) -> Result<()> { if !render_dirty.replace(false) { return; } - render_now(&window); + + // **What counts as "still dragging".** + // + // No control reports a gesture boundary, and threading one out + // of every slider, curve point and crop handle would be a lot + // of surface for a rendering concern. `was_coalesced` answers + // it instead: it is set only when a request arrived while a + // render was already queued, which can only mean a control + // moved again — that is a drag. One-off changes — a click, a + // reset, a resize — never coalesce, so they render sharp the + // first time and never draw a draft frame at all. + let dragging = was_coalesced.replace(false); + render_now(&window, dragging); + if !dragging { + return; + } + + if settle_pending.replace(true) { + return; + } + let weak = window.as_weak(); + let render_now = render_now.clone(); + let settle_pending = settle_pending.clone(); + // Long enough that an ordinary drag never reaches it, short + // enough that the sharp frame feels immediate on release. + slint::Timer::single_shot(SETTLE_DELAY, move || { + settle_pending.set(false); + let Some(window) = weak.upgrade() else { return }; + render_now(&window, false); + }); }); }) };