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:
+53
-1
@@ -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
|
||||
// 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 viewport = viewport.clone();
|
||||
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 entries = entries.clone();
|
||||
let index = index.clone();
|
||||
|
||||
Reference in New Issue
Block a user