ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there. The platform layer. Input reaches the runtime as platform-neutral events: `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). The runtime is generic over the platform, `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time. The backend-neutral half of a loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type. Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering. The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it. Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero. Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora. APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry. The built-in keyboard for an app's own text input. While `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose`, so a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows, comes back when the serial changes and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method. Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes. `README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend. Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings.
341 lines
14 KiB
Rust
341 lines
14 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use crate::app::{ App, OverlayId };
|
||
use crate::platform::{ NativePlatform, Platform };
|
||
use crate::types::Point;
|
||
|
||
use super::repeat::{ ButtonRepeatState, KeyRepeatState };
|
||
use super::surface::{ SurfaceFocus, SurfaceState };
|
||
use super::tooltip::{ TooltipPending, TooltipVisible };
|
||
|
||
/// The runtime's state for one app, on the platform `P`: the build's backend unless a test supplies another.
|
||
pub struct AppData<A: App, P: Platform = NativePlatform<A>>
|
||
{
|
||
pub app: A,
|
||
pub platform: P,
|
||
pub pointer_pos: Point,
|
||
/// 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>,
|
||
/// No input method to ask for: the runtime draws its own keyboard under a focused text field.
|
||
pub builtin_osk: bool,
|
||
/// Whether the currently focused text field is `secure` (password).
|
||
/// Drives the text-input-v3 content type.
|
||
pub text_input_secure: bool,
|
||
pub text_input_purpose: crate::osk::Purpose,
|
||
/// Serial of the [`App::external_text_input`] request the text-input is held for.
|
||
pub external_text_input: Option<u64>,
|
||
|
||
/// The selection the middle button pastes, ours or another application's.
|
||
pub primary_selection: String,
|
||
/// The selection of the focused text field when it was last looked at.
|
||
pub primary_field: Option<String>,
|
||
|
||
/// 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>,
|
||
pub shift_pressed: bool,
|
||
pub ctrl_pressed: bool,
|
||
pub alt_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. Outbound copies are offered to other
|
||
/// applications through [`Platform::publish_clipboard`]; the backend
|
||
/// writes their selections here.
|
||
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>,
|
||
/// Set to true when the app has accepted a close request.
|
||
pub exit_requested: bool,
|
||
/// Last value of `App::fullscreen` sent to the compositor.
|
||
pub applied_fullscreen: Option<bool>,
|
||
/// Main application surface.
|
||
pub main: SurfaceState<A::Message, P::Surface>,
|
||
/// 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, P::Surface>>,
|
||
/// Surfaces covering every output beyond the first, paired with the
|
||
/// output each one belongs to: session-lock surfaces while locked (the
|
||
/// compositor blanks any output without one), background layer surfaces
|
||
/// for a [`crate::Layer::Background`] shell (the wallpaper reaches
|
||
/// extended outputs). These render [`App::secondary_view`] and take no
|
||
/// input (the input handlers find no focus for them, and keyboard falls
|
||
/// through to `Main`).
|
||
pub secondaries: Vec<( P::Output, SurfaceState<A::Message, P::Surface> )>,
|
||
/// 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,
|
||
/// The current press landed on the widget that already held the focus, not the press that gives it.
|
||
pub press_on_focused: bool,
|
||
/// 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,
|
||
/// Focus request deferred because the target widget was absent from
|
||
/// `widget_rects` (not yet rebuilt, or clipped by a scroll viewport),
|
||
/// with the number of drawn frames it may still wait for.
|
||
pub focus_retry: Option<( crate::types::WidgetId, u8 )>,
|
||
/// Runtime performance guardrails (stuck-animation / poll-interval
|
||
/// diagnostics and the software animation-rate cap). See [`super::perf`].
|
||
pub perf: super::perf::PerfState,
|
||
}
|
||
|
||
impl<A: App, P: Platform> AppData<A, P>
|
||
{
|
||
/// Whether the main surface is an application window the user can move, resize and minimise.
|
||
pub( crate ) fn main_is_toplevel( &self ) -> bool
|
||
{
|
||
self.platform.is_toplevel( &self.main.platform )
|
||
}
|
||
|
||
/// Forward a change of [`App::fullscreen`] to the main window.
|
||
pub( crate ) fn sync_fullscreen( &mut self )
|
||
{
|
||
let Some( wanted ) = self.app.fullscreen() else { return };
|
||
if self.applied_fullscreen == Some( wanted ) { return; }
|
||
if self.platform.set_fullscreen( &self.main.platform, wanted )
|
||
{
|
||
self.applied_fullscreen = Some( wanted );
|
||
}
|
||
}
|
||
|
||
/// 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 the backend's surface lookup 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, P::Surface>
|
||
{
|
||
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, P::Surface>>
|
||
{
|
||
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, P::Surface>
|
||
{
|
||
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, P::Surface>>
|
||
{
|
||
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;
|
||
// Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine.
|
||
if self.main.primary_touch_id.is_none()
|
||
{
|
||
self.main.primary_touch_id = ss.primary_touch_id;
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Tell the app and the accessibility tree the main surface is now `w × h` logical pixels at its scale, once the backend has resized it.
|
||
pub( crate ) fn on_main_resized( &mut self, w: u32, h: u32 )
|
||
{
|
||
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 );
|
||
}
|
||
// `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. A configure's size is
|
||
// 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;
|
||
crate::types::set_viewport_size( w * sf, h * sf );
|
||
crate::types::set_density( sf as f32 );
|
||
self.app.on_scale_changed( sf );
|
||
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
|
||
}
|
||
}
|