Files
ltk/src/input/dispatch/outcomes.rs
Pedro M. de Echanove Pasquin d221d5a0cd
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
app: add App::subsurface_motion_only so a slide-to-reveal panel animates without re-rastering the main surface
An app whose gesture/animation only moves an input-transparent subsurface (a slide-to-reveal panel over a static main surface) still paid a full main-surface re-raster on every frame of the slide, because the runtime force-dirties the main view on two paths it cannot tell apart from a real content change: `MoveOutcome::Swipe` calls `dirty_caches()` + `request_redraw()` per motion sample, and the `WlCallback` Main handler sets `view_dirty` + `request_redraw()` on every frame callback while `is_animating`. The subsurface reconcile already repositions the panel with a cheap `set_position` + bare parent commit, so the main re-raster is wasted work — and on slow targets (Librem5) it competes with the reposition and makes the slide stutter.
New opt-in `App::subsurface_motion_only` (default `false`, so existing apps are unaffected). When it returns `true`:
- the swipe dispatch (`input/dispatch/outcomes.rs`) still calls the `on_swipe_progress` family but skips `dirty_caches()` / `request_redraw()`; incoming motion events pump the loop and the per-iteration `reconcile_subsurfaces` carries the move.
- the frame-callback animation pump (`event_loop/handlers.rs`) keeps the vsync cadence without a re-raster by requesting a bare `wl.frame()` + `commit()` on the main surface (no buffer attach) instead of dirtying the view; `poll_external` advances the animation and the reconcile repositions each frame.
The main surface still redraws for genuine content changes (messages, resize, the one-shot redraw on swipe release) — only the per-frame slide raster is dropped.
2026-06-01 21:53:40 +02:00

211 lines
5.2 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
use crate::input::gesture::{ MoveOutcome, ReleaseEvent };
impl<A: App> AppData<A>
{
/// Apply the outcome of a motion event. Non-blocking side-effects
/// only: push messages, call app callbacks, set redraw flags.
pub( crate ) fn apply_move_outcome
(
&mut self,
focus: SurfaceFocus,
outcome: MoveOutcome<A::Message>,
)
{
match outcome
{
MoveOutcome::Idle => {}
MoveOutcome::Drag { pos } =>
{
let ( ox, oy ) = self.surface_offset_for( focus );
self.app.on_drag_move( pos.x + ox, pos.y + oy );
self.dirty_caches();
self.surface_mut( focus ).request_redraw();
self.main.request_redraw();
for ss in self.overlays.values_mut()
{
ss.request_redraw();
}
}
MoveOutcome::Slider { msg } =>
{
if let Some( m ) = msg
{
self.pending_msgs.push( m );
}
self.surface_mut( focus ).request_redraw();
}
MoveOutcome::Scroll =>
{
self.surface_mut( focus ).request_redraw();
}
MoveOutcome::Swipe { up, down, horizontal } =>
{
if let Some( v ) = up { self.app.on_swipe_progress( v ); }
if let Some( v ) = down { self.app.on_swipe_down_progress( v ); }
if let Some( v ) = horizontal { self.app.on_swipe_horizontal_progress( v ); }
// Swipe-progress callbacks mutate app state outside
// `update`, so the cached view tree is stale — unless the
// motion only slides a subsurface over a static main
// surface, in which case the per-frame subsurface reconcile
// carries the move and a main re-raster is wasted work.
if !self.app.subsurface_motion_only()
{
self.dirty_caches();
self.surface_mut( focus ).request_redraw();
for ss in self.overlays.values_mut()
{
ss.request_redraw();
}
}
}
}
}
/// Apply the ordered list of events from a release. A single
/// release can emit more than one event (e.g. horizontal
/// fall-through followed by a vertical commit or fall-through), so
/// we walk the list and run each event's side-effects in order.
pub( crate ) fn apply_release_events
(
&mut self,
focus: SurfaceFocus,
events: Vec<ReleaseEvent<A::Message>>,
)
{
for event in events
{
self.apply_release_event( focus, event );
}
}
fn apply_release_event
(
&mut self,
focus: SurfaceFocus,
event: ReleaseEvent<A::Message>,
)
{
match event
{
ReleaseEvent::Drop { pos } =>
{
let ( ox, oy ) = self.surface_offset_for( focus );
if let Some( msg ) = self.app.on_drop( pos.x + ox, pos.y + oy )
{
self.pending_msgs.push( msg );
}
self.clear_long_press_drag();
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
for ss in self.overlays.values_mut()
{
ss.request_redraw();
}
}
ReleaseEvent::SwipeLeft =>
{
if let Some( msg ) = self.app.on_swipe_left()
{
self.pending_msgs.push( msg );
}
// When the app handles the release internally (settle
// animation, state-only mutation) it returns no
// Message, so the update-driven redraw never fires.
// Kick the main surface here so `is_animating()` has a
// frame to latch onto.
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::SwipeRight =>
{
if let Some( msg ) = self.app.on_swipe_right()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::SwipeUp =>
{
if let Some( msg ) = self.app.on_swipe_up()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
for ss in self.overlays.values_mut()
{
ss.request_redraw();
ss.frame_pending = false;
}
}
ReleaseEvent::SwipeDown =>
{
if let Some( msg ) = self.app.on_swipe_down()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
for ss in self.overlays.values_mut()
{
ss.request_redraw();
ss.frame_pending = false;
}
}
ReleaseEvent::HorizontalFellThrough =>
{
// Below threshold: pulse horizontal 0 so a
// vertical-dominant gesture that drifted laterally
// does not leave the app stuck on a stale horizontal
// progress.
self.app.on_swipe_horizontal_progress( 0.0 );
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::VerticalFellThrough =>
{
self.app.on_swipe_progress( 0.0 );
self.app.on_swipe_down_progress( 0.0 );
self.dirty_caches();
}
ReleaseEvent::PushMsg( msg ) =>
{
self.pending_msgs.push( msg );
}
ReleaseEvent::EmptyRelease =>
{
match focus
{
SurfaceFocus::Main =>
{
if let Some( msg ) = self.app.on_tap()
{
self.pending_msgs.push( msg );
}
}
SurfaceFocus::Overlay( id ) =>
{
if let Some( msg ) = self.overlay_dismiss_msg( id )
{
self.pending_msgs.push( msg );
}
}
}
}
}
}
}