Coalesce develop renders instead of rendering per input event

Every slider, curve-point and crop-handle drag ran a full render
straight from its `moved` handler. A render ends in a blocking GPU
readback, so that stall sat directly on the input path: touch events
arrive far faster than a render completes, the queue backed up, and
positions reached the handlers several samples stale — the jumpy
dragging.

The worse consequence was gestures being lost outright. When events go
unconsumed for long enough Android reclaims the stream and hands it to
the view underneath, so a drag stopped mid-gesture never received `up`,
only `cancel` — which every handler here treats as "abort the drag".

`redraw` no longer renders. It marks the canvas dirty and posts one
render onto the event loop, coalescing any further requests that arrive
while it is pending, and re-checks the flag afterwards so a value that
moved during the render is not left on a stale frame. Handlers now
return immediately, which is what keeps the gesture consumed.

A zero-delay `Timer` rather than `invoke_from_event_loop`: the latter
requires `Send` and this state is deliberately `Rc` on the UI thread.

All existing `redraw` call sites are unchanged — the signature is the
same and coalescing is internal.
This commit is contained in:
2026-08-09 21:49:43 +02:00
parent b4c1645c7a
commit 31dd86d8c0
+53 -1
View File
@@ -479,7 +479,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
// //
// Called on every slider change, so it must do no more than run the // Called on every slider change, so it must do no more than run the
// adjust pass — the demosaic is not repeated. // adjust pass — the demosaic is not repeated.
let redraw: Rc<dyn Fn(&AppWindow)> = { let render_now: Rc<dyn Fn(&AppWindow)> = {
let session = session.clone(); let session = session.clone();
let viewport = viewport.clone(); let viewport = viewport.clone();
Rc::new(move |window: &AppWindow| { Rc::new(move |window: &AppWindow| {
@@ -514,6 +514,58 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
}) })
}; };
// **Rendering is decoupled from input, and this is why.**
//
// A render is a blocking GPU round-trip (see `AdjustPass::read_output`).
// Running one straight from a `moved` handler put that stall *inside* the
// gesture: touch events arrive far faster than a render completes, so the
// input queue backed up, positions arrived stale, and Android — seeing the
// events go unconsumed — reclaimed the gesture and delivered `cancel`
// instead of `up`. That is the dropped-drag bug, and no amount of tuning
// inside the Slint handlers fixes it while the stall is on the input path.
//
// So `redraw` no longer renders. It marks the canvas dirty and posts a
// single render onto the event loop; every further request while one is
// already pending just sets the flag again. A drag emitting forty events
// therefore renders a handful of times instead of forty, and — the part
// that actually fixes the drop — each event handler returns immediately,
// so the gesture is always consumed promptly.
//
// The flag is re-checked *after* the render because parameters may have
// moved again while it ran; that repost is what keeps the image converging
// 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));
let redraw: Rc<dyn Fn(&AppWindow)> = {
let render_now = render_now.clone();
let render_pending = render_pending.clone();
let render_dirty = render_dirty.clone();
Rc::new(move |window: &AppWindow| {
render_dirty.set(true);
if render_pending.get() {
return;
}
render_pending.set(true);
let weak = window.as_weak();
let render_now = render_now.clone();
let render_pending = render_pending.clone();
let render_dirty = render_dirty.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);
let Some(window) = weak.upgrade() else { return };
if !render_dirty.replace(false) {
return;
}
render_now(&window);
});
})
};
let show = { let show = {
let entries = entries.clone(); let entries = entries.clone();
let index = index.clone(); let index = index.clone();