Add a third Wayland surface type to the runtime so an ltk `App` can be a screen locker, alongside the existing xdg-shell window and wlr-layer-shell surfaces. A new `ShellMode::SessionLock` makes `run()` bind `ext_session_lock_manager_v1` and request the lock at startup; the lock surface itself is created in the new `SessionLockHandler::locked` callback (one surface on the first advertised output) and replaces the `SurfaceKind::PendingLock` placeholder the main surface holds until the compositor grants the lock. The `configure` event routes through the same `on_configure` path as layer and xdg surfaces, so sizing and rendering are unchanged, and `finished` (the compositor denied or ended the lock) tears the loop down. The whole thing is additive and opt-in: the `Window` and `Layer` paths are untouched and nothing enters lock mode unless an `App` returns `ShellMode::SessionLock`, so existing apps are unaffected — the only non-additive edits are the two exhaustive `match`es on `SurfaceKind` (`wl_surface` / `try_wl_surface`), which gain arms for the two new variants. Doing the locker as a first-class surface rather than compositing a static texture into an offscreen `UiSurface` is the whole point: the compositor gives the lock surface keyboard focus, so ltk's existing text-input, editing, focus and IME machinery works inside the lock exactly as on any other surface — cursor, click-to-focus, Tab, character input. A locker built on top of this is just a normal interactive ltk app that happens to be presented on the lock layer, with no special input plumbing on the compositor or the app side. `App::requested_exit()` is the new way an app asks the runtime to tear the surface down and leave the loop; it is polled after every batch of `update`s. It exists because of the one hard invariant of `ext-session-lock-v1`: a locker that disconnects without sending `unlock` leaves the compositor's outputs blanked forever — that is the protocol's deliberate anti-bypass guarantee. So when `requested_exit()` returns true and the surface is a session lock, the loop calls `session_lock.unlock()` and round-trips the connection before setting `exit_requested`, lifting the lock cleanly; for a `Window` or `Layer` surface there is no lock and it simply exits. The consequence for lock apps is that they must stop calling `process::exit` from the lock path and instead flip a flag they return from `requested_exit()`. `text_edit` gains a `read_only( bool )` builder. A read-only field still renders its box and value in the normal field style but takes no keyboard focus and accepts no input: `Element::is_focusable` and `Element::is_text_input` now return false for a read-only `TextEdit`, which keeps it out of the Tab cycle, off the keyboard-edit path, and stops the cursor from ever being drawn on it. The flag is carried through `map_msg` so it survives `Element::map`. This is for presenting a known, non-editable value in the same visual idiom as the editable fields beside it — for example the already-known user shown on a session lock, where letting that field take focus or blink a cursor would be wrong. The `shell_mode()` doc comment and the README now list the `SessionLock` surface type and point at `requested_exit()` for the unlock. Two warnings are cleared along the way: the runtime no longer stores the `SessionLockState` after requesting the lock — it has no `Drop`, so the manager object outlives the dropped handle inside the connection and the lock lifecycle runs entirely off the returned `SessionLock`, which removes a never-read field — and a pre-existing rustdoc `private_intra_doc_links` warning in `list_item` (a public doc comment linking to the private `theme::ICON_SIZE`) is downgraded to plain code formatting.
445 lines
18 KiB
Rust
445 lines
18 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use smithay_client_toolkit::
|
|
{
|
|
compositor::CompositorState,
|
|
output::OutputState,
|
|
registry::RegistryState,
|
|
seat::SeatState,
|
|
shell::{ wlr_layer::LayerShell, xdg::XdgShell },
|
|
shm::Shm,
|
|
session_lock::SessionLock,
|
|
};
|
|
use smithay_client_toolkit::reexports::client::
|
|
{
|
|
protocol::
|
|
{
|
|
wl_keyboard::WlKeyboard,
|
|
wl_pointer::WlPointer,
|
|
wl_surface::WlSurface,
|
|
wl_touch::WlTouch,
|
|
},
|
|
QueueHandle,
|
|
};
|
|
use wayland_protocols::wp::text_input::zv3::client::
|
|
{
|
|
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
|
|
zwp_text_input_v3::ZwpTextInputV3,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::app::{ App, OverlayId };
|
|
use crate::egl_context::EglContext;
|
|
use crate::types::Point;
|
|
|
|
use super::repeat::{ ButtonRepeatState, KeyRepeatState };
|
|
use super::surface::{ SurfaceFocus, SurfaceState };
|
|
use super::tooltip::{ TooltipPending, TooltipVisible };
|
|
|
|
pub struct AppData<A: App>
|
|
{
|
|
pub app: A,
|
|
pub registry_state: RegistryState,
|
|
pub seat_state: SeatState,
|
|
pub output_state: OutputState,
|
|
pub compositor_state: CompositorState,
|
|
pub shm: Shm,
|
|
pub session_lock: Option<SessionLock>,
|
|
/// Process-wide EGL context (display + GLES context). `None` when EGL
|
|
/// failed to initialise or `LTK_FORCE_SOFTWARE=1` — every surface then
|
|
/// falls back to the SHM path.
|
|
pub egl_context: Option<Arc<EglContext>>,
|
|
#[allow(dead_code)]
|
|
pub xdg_shell: Option<XdgShell>,
|
|
/// Shared layer-shell binding used for the main surface and every
|
|
/// overlay. `None` when the compositor does not advertise the protocol.
|
|
pub layer_shell: Option<LayerShell>,
|
|
pub keyboard: Option<WlKeyboard>,
|
|
pub pointer: Option<WlPointer>,
|
|
pub touch: Option<WlTouch>,
|
|
pub pointer_pos: Point,
|
|
/// Process-wide cursor-shape manager (`wp_cursor_shape_v1`).
|
|
/// `None` when the compositor does not advertise the protocol —
|
|
/// the runtime then leaves cursor shape to the compositor's
|
|
/// defaults.
|
|
pub cursor_shape_manager: Option<smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager>,
|
|
/// Per-pointer cursor-shape device, populated when the compositor
|
|
/// supports cursor-shape AND the seat has a pointer capability.
|
|
pub cursor_shape_device: Option<smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
|
/// Last `Enter` serial seen on a pointer event. Required by
|
|
/// `wp_cursor_shape_device_v1::set_shape` per spec — the
|
|
/// compositor tags every cursor change with the entry serial.
|
|
pub last_pointer_enter_serial: u32,
|
|
/// The cursor shape currently active on the pointer. `None`
|
|
/// means "we have not pushed any shape since the last
|
|
/// `wl_pointer.enter`" — the compositor is showing whatever the
|
|
/// previous client requested, so the next `dispatch_cursor_shape`
|
|
/// must send unconditionally to claim the cursor for our
|
|
/// surface. `Some(s)` lets the dispatch short-circuit when the
|
|
/// target equals what we already sent.
|
|
pub current_cursor_shape: Option<crate::types::CursorShape>,
|
|
pub text_input_manager: Option<ZwpTextInputManagerV3>,
|
|
pub text_input: Option<ZwpTextInputV3>,
|
|
/// `xdg-activation-v1`. Present when the compositor advertises the
|
|
/// global. Used on first configure to honour an incoming
|
|
/// `XDG_ACTIVATION_TOKEN` (a launcher that spawned us wants the
|
|
/// main surface raised to focus) and exposed via
|
|
/// [`App::request_activation_token`] for outbound requests.
|
|
pub activation_state:
|
|
Option<smithay_client_toolkit::activation::ActivationState>,
|
|
/// Honour the activation token exactly once — after the first
|
|
/// successful configure of the main surface. Subsequent configures
|
|
/// (resizes, scale changes) must not re-activate.
|
|
pub activation_token_pending: Option<String>,
|
|
|
|
/// `wl_data_device_manager` binding. `None` when the compositor
|
|
/// does not advertise the global, in which case copy / paste stays
|
|
/// process-local and inbound selections from other clients are
|
|
/// invisible.
|
|
pub data_device_manager:
|
|
Option<smithay_client_toolkit::data_device_manager::DataDeviceManagerState>,
|
|
/// Per-seat `wl_data_device` handle, created the first time a seat
|
|
/// with a keyboard or pointer capability appears. Required for
|
|
/// `set_selection` and to receive inbound `selection` events.
|
|
pub data_device:
|
|
Option<smithay_client_toolkit::data_device_manager::data_device::DataDevice>,
|
|
/// Currently-published outbound selection source. Held alive
|
|
/// across [`DataSourceHandler::send_request`] so the cached
|
|
/// clipboard text can be re-served on each paste from the peer.
|
|
pub clipboard_source:
|
|
Option<smithay_client_toolkit::data_device_manager::data_source::CopyPasteSource>,
|
|
/// Sender half of the cross-thread channel used to ferry inbound
|
|
/// selection bytes (a worker thread drains the read pipe).
|
|
pub clipboard_inbox_tx: std::sync::mpsc::Sender<String>,
|
|
/// Receiver half — drained once per run loop iteration.
|
|
pub clipboard_inbox_rx: std::sync::mpsc::Receiver<String>,
|
|
|
|
/// Cross-application drag-and-drop: best mime negotiated on
|
|
/// `enter` and the last pointer position (in logical surface
|
|
/// coords). Cleared on `leave` and after `drop_performed`.
|
|
pub drop_position: Option<( f64, f64 )>,
|
|
pub drop_mime: Option<String>,
|
|
pub drop_inbox_tx: std::sync::mpsc::Sender<super::data_device::DropPayload>,
|
|
pub drop_inbox_rx: std::sync::mpsc::Receiver<super::data_device::DropPayload>,
|
|
|
|
/// AccessKit / AT-SPI2 adapter. `None` when the platform adapter
|
|
/// could not be created (no AT-SPI2 daemon on the session bus,
|
|
/// missing system libraries at runtime, headless CI). The
|
|
/// runtime carries on without an accessibility tree in that
|
|
/// case; nothing else in the pipeline reads this field except
|
|
/// the per-frame tree update and the per-iteration action
|
|
/// inbox drain.
|
|
pub a11y: Option<crate::a11y::A11yState>,
|
|
/// Client-side handle to `ext-foreign-toplevel-list-v1`. Holds the
|
|
/// global proxy plus the live list of open toplevels; SCTK fans
|
|
/// events through this object into our `ForeignToplevelListHandler`
|
|
/// impl in `handlers.rs`, which then routes them to
|
|
/// `App::on_toplevel_event`. Present on every session, even when
|
|
/// the compositor does not advertise the global — internally the
|
|
/// list holds a `GlobalProxy` that simply yields no toplevels in
|
|
/// that case.
|
|
pub foreign_toplevel_list:
|
|
smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList,
|
|
pub shift_pressed: bool,
|
|
pub ctrl_pressed: bool,
|
|
/// Calloop handle for inserting timers / channels. Used by the
|
|
/// key-repeat machinery and any future feature that needs to
|
|
/// schedule work on the run loop without going through the message
|
|
/// queue.
|
|
pub loop_handle: calloop::LoopHandle<'static, Self>,
|
|
/// Repeat rate (events per second) advertised by the compositor in
|
|
/// `wl_keyboard.repeat_info`. `0` means "the compositor disabled
|
|
/// repeat" — we honour that and never start a repeat timer.
|
|
pub compositor_repeat_rate: u32,
|
|
/// Initial delay (ms) before the first repeat fires, advertised by
|
|
/// the compositor. `0` means the compositor disabled repeat.
|
|
pub compositor_repeat_delay: u32,
|
|
/// Currently-active key repeat. `Some` between the press of a
|
|
/// repeating key and either its release, the keyboard losing focus
|
|
/// or another key being pressed.
|
|
pub key_repeat: Option<KeyRepeatState>,
|
|
/// Currently-active button-press repeat. `Some` between the
|
|
/// press of a repeating-button and either its release, the
|
|
/// pointer leaving the surface, a gesture cancel, focus loss or
|
|
/// long-press promotion.
|
|
pub button_repeat: Option<ButtonRepeatState>,
|
|
/// Clipboard buffer. Copy / Cut store the selected text here;
|
|
/// Paste retrieves from here. Synchronised with the Wayland
|
|
/// selection through `wl_data_device_manager` when the compositor
|
|
/// advertises the global: outbound copies publish a
|
|
/// `CopyPasteSource`, and inbound selections from other clients
|
|
/// land here through a worker thread (see
|
|
/// [`super::data_device`]).
|
|
pub clipboard: String,
|
|
/// Timestamp of the previous press (pointer or touch). Combined
|
|
/// with [`Self::last_press_pos`] it lets the press handler
|
|
/// detect a double-click on a TextEdit and turn it into a
|
|
/// word-select.
|
|
pub last_press_time: Option<std::time::Instant>,
|
|
/// Position of the previous press. See [`Self::last_press_time`].
|
|
pub last_press_pos: Option<Point>,
|
|
pub debug_layout: bool,
|
|
pub pending_msgs: Vec<A::Message>,
|
|
/// Initial cursor positions queued when a long-press fires, to be flushed
|
|
/// to [`App::on_drag_move`] right after the paired long-press message is
|
|
/// processed. This gives the drag ghost a valid starting position even
|
|
/// before the user's finger / cursor moves — useful on mouse where the
|
|
/// pointer might sit perfectly still between press and drag.
|
|
pub pending_drag_inits: Vec<Point>,
|
|
pub tooltip_pending: Option<TooltipPending>,
|
|
pub tooltip_visible: Option<TooltipVisible>,
|
|
pub qh: QueueHandle<Self>,
|
|
/// Last pointer serial (needed for interactive move).
|
|
pub last_pointer_serial: u32,
|
|
/// Last serial from any input event. Required by `xdg_popup.grab`,
|
|
/// which only honours serials from recent input events on the same
|
|
/// seat.
|
|
pub last_input_serial: u32,
|
|
/// Set to true when the app has accepted a close request.
|
|
pub exit_requested: bool,
|
|
/// First-configure latch for `App::start_fullscreen`.
|
|
pub pending_fullscreen: bool,
|
|
/// First-configure latch: clears `max_size` after the compositor
|
|
/// has honoured the pinned initial size.
|
|
pub pending_size_hint_unpin: bool,
|
|
/// Main application surface.
|
|
pub main: SurfaceState<A::Message>,
|
|
/// Auxiliary layer-shell surfaces keyed by their stable [`OverlayId`].
|
|
/// Populated and reconciled by the run loop from [`App::overlays`]. Always
|
|
/// empty until overlay diffing is wired up.
|
|
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
|
|
/// Surface currently receiving pointer events (updated on each pointer
|
|
/// event via its `surface` field).
|
|
pub pointer_focus: SurfaceFocus,
|
|
/// Surface currently holding keyboard focus (updated on keyboard
|
|
/// enter/leave).
|
|
pub keyboard_focus: SurfaceFocus,
|
|
/// Per-touch-id tracking of which surface each active touch belongs to
|
|
/// (populated on `down`, cleared on `up`/`cancel`).
|
|
pub touch_focus: HashMap<i32, SurfaceFocus>,
|
|
/// Cached widget tree returned by [`App::view`]. Populated lazily by the
|
|
/// run loop just before drawing when `view_dirty` is set, then re-used by
|
|
/// every subsequent partial / interaction-only redraw until invalidated.
|
|
/// Dropping the rebuild on idle frames is a big win for shells that sit
|
|
/// idle most of the time.
|
|
pub cached_view: Option<crate::widget::Element<A::Message>>,
|
|
/// Cached overlay-spec list returned by [`App::overlays`]. Same lifecycle
|
|
/// as `cached_view` but driven by `overlays_dirty`.
|
|
pub cached_overlays: Option<Vec<crate::app::OverlaySpec<A::Message>>>,
|
|
/// `true` when `cached_view` is stale and must be rebuilt before the next
|
|
/// draw. Set on startup, by every `InvalidationScope` touching `Main`, and
|
|
/// every frame `App::is_animating` returns `true`.
|
|
pub view_dirty: bool,
|
|
/// Counterpart of `view_dirty` for `cached_overlays`.
|
|
pub overlays_dirty: bool,
|
|
/// Set after the first commit of a rendered buffer on the main surface.
|
|
/// Used to drive `App::on_first_frame_committed` exactly once.
|
|
pub first_frame_committed: bool,
|
|
}
|
|
|
|
impl<A: App> AppData<A>
|
|
{
|
|
/// Find which [`SurfaceFocus`] owns the given `WlSurface`, or `None` if it
|
|
/// does not correspond to any tracked surface. Safe even when the main
|
|
/// surface is still `Pending` (returns `None`).
|
|
pub( crate ) fn focus_for_surface(
|
|
&self,
|
|
wl: &WlSurface,
|
|
) -> Option<SurfaceFocus>
|
|
{
|
|
if self.main.surface.try_wl_surface() == Some( wl )
|
|
{
|
|
return Some( SurfaceFocus::Main );
|
|
}
|
|
for ( id, ss ) in self.overlays.iter()
|
|
{
|
|
if ss.surface.try_wl_surface() == Some( wl )
|
|
{
|
|
return Some( SurfaceFocus::Overlay( *id ) );
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`
|
|
/// refers to an overlay that is not currently registered — callers must
|
|
/// only pass focus values obtained from [`focus_for_surface`] or from the
|
|
/// per-device focus fields, which the run loop keeps in sync.
|
|
#[allow( dead_code )]
|
|
pub( crate ) fn surface( &self, focus: SurfaceFocus ) -> &SurfaceState<A::Message>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => &self.main,
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get( &id )
|
|
.expect( "surface(): overlay not registered" ),
|
|
}
|
|
}
|
|
|
|
/// Non-panicking variant of [`surface`]. Returns `None` when `focus`
|
|
/// refers to an overlay that has already been removed — callers on
|
|
/// async dispatch paths (IME Done, tooltip arm) must use this.
|
|
pub( crate ) fn try_surface( &self, focus: SurfaceFocus ) -> Option<&SurfaceState<A::Message>>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => Some( &self.main ),
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get( &id ),
|
|
}
|
|
}
|
|
|
|
/// Mutable counterpart of [`surface`].
|
|
#[allow( dead_code )]
|
|
pub( crate ) fn surface_mut( &mut self, focus: SurfaceFocus ) -> &mut SurfaceState<A::Message>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => &mut self.main,
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id )
|
|
.expect( "surface_mut(): overlay not registered" ),
|
|
}
|
|
}
|
|
|
|
/// Non-panicking variant of [`surface_mut`].
|
|
pub( crate ) fn try_surface_mut( &mut self, focus: SurfaceFocus ) -> Option<&mut SurfaceState<A::Message>>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => Some( &mut self.main ),
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id ),
|
|
}
|
|
}
|
|
|
|
/// Synchronous overlay teardown: removes the overlay from the map
|
|
/// and rewrites every per-device focus that pointed at it so the
|
|
/// next event in the same dispatch can no longer land on a freed
|
|
/// surface. Used by the compositor-driven destruction paths
|
|
/// (`PopupHandler::done`, `LayerShellHandler::closed`) where
|
|
/// waiting for the next `reconcile_overlays` would leave a window
|
|
/// in which `surface()` / `surface_mut()` panic. Migrates an
|
|
/// in-flight long-press drag to the main surface for the same
|
|
/// reason `reconcile_overlays` does.
|
|
pub( crate ) fn discard_overlay( &mut self, id: crate::app::OverlayId )
|
|
{
|
|
if let Some( ss ) = self.overlays.remove( &id )
|
|
{
|
|
if ss.gesture.long_press_fired
|
|
{
|
|
self.main.gesture.long_press_fired = true;
|
|
self.main.gesture.long_press_origin = ss.gesture.long_press_origin;
|
|
}
|
|
}
|
|
if let SurfaceFocus::Overlay( fid ) = self.pointer_focus
|
|
{
|
|
if fid == id { self.pointer_focus = SurfaceFocus::Main; }
|
|
}
|
|
if let SurfaceFocus::Overlay( fid ) = self.keyboard_focus
|
|
{
|
|
if fid == id { self.keyboard_focus = SurfaceFocus::Main; }
|
|
}
|
|
for f in self.touch_focus.values_mut()
|
|
{
|
|
if let SurfaceFocus::Overlay( fid ) = *f
|
|
{
|
|
if fid == id { *f = SurfaceFocus::Main; }
|
|
}
|
|
}
|
|
if let Some( pending ) = self.tooltip_pending.as_ref()
|
|
{
|
|
if pending.focus == SurfaceFocus::Overlay( id ) { self.tooltip_pending = None; }
|
|
}
|
|
if let Some( visible ) = self.tooltip_visible.as_ref()
|
|
{
|
|
if visible.focus == SurfaceFocus::Overlay( id )
|
|
{
|
|
self.tooltip_visible = None;
|
|
self.overlays_dirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Configure the main surface size, (re)allocate its rendering target, and
|
|
// request a redraw. Routes to the GPU or SHM path inside `SurfaceState`
|
|
// according to whether `self.egl_context` is available.
|
|
pub( crate ) fn on_configure( &mut self, w: u32, h: u32 )
|
|
{
|
|
self.main.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
|
|
if let Some( ref mut a ) = self.a11y
|
|
{
|
|
let sf = self.main.scale_factor.max( 1 ) as f64;
|
|
a.set_window_bounds( ( w as f64 ) * sf, ( h as f64 ) * sf );
|
|
a.set_window_focus( true );
|
|
}
|
|
// First-configure latch: honour an incoming
|
|
// `XDG_ACTIVATION_TOKEN` exactly once, after the surface has
|
|
// been mapped (`xdg_activation_v1.activate` is only meaningful
|
|
// against a configured surface). Compositors that don't
|
|
// advertise the global leave `activation_state` as `None` and
|
|
// the call drops the token silently.
|
|
if let Some( token ) = self.activation_token_pending.take()
|
|
{
|
|
if let ( Some( ref activation ), Some( wl ) ) =
|
|
( self.activation_state.as_ref(), self.main.surface.try_wl_surface() )
|
|
{
|
|
activation.activate::<Self>( &wl, token );
|
|
}
|
|
}
|
|
// `on_resize` is documented to deliver **physical** pixels, matching the
|
|
// coordinate space that the layout passes and the pointer/touch
|
|
// callbacks (`on_drag_move`, `on_drop`) work in. Wayland's
|
|
// configure.new_size is surface-local (logical), so multiply by the
|
|
// current buffer scale before handing the dimensions to the app.
|
|
let sf = self.main.scale_factor.max( 1 ) as u32;
|
|
self.app.on_resize( w * sf, h * sf );
|
|
// `on_resize` may flip app-state that the view depends on (apps that
|
|
// branch on the new dimensions, layout caches keyed by size, …), so
|
|
// drop the cached tree to force a rebuild on the next draw.
|
|
self.dirty_caches();
|
|
}
|
|
|
|
/// Mark both view caches stale after a direct app-state mutation that
|
|
/// doesn't go through [`App::update`] — swipe progress callbacks, text-
|
|
/// input focus changes, configure-driven resize. Per-message updates use
|
|
/// the run loop's `apply_invalidation` path instead so that
|
|
/// [`App::invalidate_after`] can scope the rebuild.
|
|
pub( crate ) fn dirty_caches( &mut self )
|
|
{
|
|
self.view_dirty = true;
|
|
self.overlays_dirty = true;
|
|
}
|
|
|
|
/// Smallest remaining time until a pending long-press deadline fires.
|
|
///
|
|
/// Used by the run loop to bound `event_loop.dispatch()` so that a
|
|
/// stationary press wakes us up at the right moment even when no
|
|
/// Wayland events arrive. `Some(Duration::ZERO)` means the deadline
|
|
/// has already elapsed; `None` means nothing is pending.
|
|
pub( crate ) fn next_long_press_wakeup( &self ) -> Option<std::time::Duration>
|
|
{
|
|
let dur = self.app.long_press_duration();
|
|
let now = std::time::Instant::now();
|
|
let mut soonest: Option<std::time::Duration> = None;
|
|
let mut consider = |start: Option<std::time::Instant>|
|
|
{
|
|
if let Some( s ) = start
|
|
{
|
|
let deadline = s + dur;
|
|
let remaining = deadline.saturating_duration_since( now );
|
|
soonest = Some( match soonest
|
|
{
|
|
Some( cur ) => cur.min( remaining ),
|
|
None => remaining,
|
|
} );
|
|
}
|
|
};
|
|
consider( self.main.gesture.long_press_start );
|
|
for ss in self.overlays.values()
|
|
{
|
|
consider( ss.gesture.long_press_start );
|
|
}
|
|
soonest
|
|
}
|
|
}
|