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.
211 lines
5.2 KiB
Rust
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 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|