Keep a drag in draft until it stops, then render sharp once
During any drag longer than 120 ms the canvas rendered a full-resolution frame every 128 ms under the finger. The settle timer was armed by the first draft of a burst and not re-armed by later ones, so it counted from the start of the gesture rather than from its last movement, fired mid-drag, and the next coalesced event armed it again. Each of those frames is the most expensive one the canvas draws, landing where the frame budget is tightest. The draft/sharp decision now lives in `refine::Refine`, apart from the timers that carry it out. Every draft frame arms a settle timer carrying a generation token and only the newest token is honoured, so the sharp frame lands SETTLE_DELAY after the last movement. A request arriving while a settle is still owed also counts as part of the gesture, so a slow stretch of a drag (one event per frame, nothing to coalesce) no longer renders sharp between drafts. A timer that fires with a render already posted defers to it. The tests drive the state machine through simulated timelines; the long-drag case reproduced the four mid-drag sharp frames before the fix.
This commit is contained in:
+32
-64
@@ -58,6 +58,7 @@ mod place;
|
||||
mod preset_store;
|
||||
mod presets;
|
||||
mod recovery_ui;
|
||||
mod refine;
|
||||
mod remote;
|
||||
pub mod repairs;
|
||||
mod segmentation;
|
||||
@@ -2428,77 +2429,44 @@ fn build_render_now(
|
||||
/// on the finger rather than settling on a stale frame.
|
||||
fn build_redraw(render_now: &Render) -> Rc<dyn Fn(&AppWindow)> {
|
||||
let render_now = render_now.clone();
|
||||
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));
|
||||
{
|
||||
// Which frame to draw and when to settle, decided apart from the timers
|
||||
// that carry it out — see `refine` for the rules and their tests.
|
||||
let refine = Rc::new(RefCell::new(refine::Refine::default()));
|
||||
Rc::new(move |window: &AppWindow| {
|
||||
if !refine.borrow_mut().request() {
|
||||
return;
|
||||
}
|
||||
|
||||
let weak = window.as_weak();
|
||||
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);
|
||||
let refine = refine.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
|
||||
// is "after the queued input has drained", not a throttle.
|
||||
slint::Timer::single_shot(std::time::Duration::ZERO, move || {
|
||||
let Some(window) = weak.upgrade() else { return };
|
||||
let Some((frame, settle)) = refine.borrow_mut().tick() else {
|
||||
return;
|
||||
}
|
||||
render_pending.set(true);
|
||||
};
|
||||
render_now(&window, frame == refine::Frame::Draft);
|
||||
let Some(token) = settle else { return };
|
||||
|
||||
// TRACES: FR-DSP-4
|
||||
// One timer per draft frame, and only the newest is honoured, so
|
||||
// the sharp frame waits for the gesture to stop rather than for
|
||||
// the first draft to age. A superseded timer wakes, finds its
|
||||
// token stale, and does nothing — cheaper than holding a `Timer`
|
||||
// to restart, and it keeps the rule where the tests can see it.
|
||||
let weak = window.as_weak();
|
||||
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
|
||||
// is "after the queued input has drained", not a throttle.
|
||||
slint::Timer::single_shot(std::time::Duration::ZERO, move || {
|
||||
render_pending.set(false);
|
||||
slint::Timer::single_shot(SETTLE_DELAY, move || {
|
||||
let Some(window) = weak.upgrade() else { return };
|
||||
if !render_dirty.replace(false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// **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 };
|
||||
if refine.borrow_mut().settle(token).is_some() {
|
||||
render_now(&window, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// Build `show`: load the entry at `index` and put it on screen.
|
||||
|
||||
Reference in New Issue
Block a user