The signalfd-based source (calloop's `signals` feature) needs SIGTERM and SIGINT blocked in every thread of the process, since the kernel hands a process-directed signal to any thread that does not block it. calloop only blocks them in the thread that creates the source, so the runtime depended on `ltk::run` being entered before the app spawned any thread — a constraint apps do not honour: crustace-notifier builds its tokio runtime and its D-Bus thread first, and in that configuration a SIGTERM lands on a tokio worker and kills the process outright, skipping the clean exit (`save_state`, `mark_clean_exit`) entirely. The blocked mask had a second consequence: it is inherited across fork and exec, and Rust's std deliberately does not reset it in the child. Every process an ltk app spawns from a thread created after `ltk::run` therefore started with SIGTERM blocked. For the `gsettings monitor` child of the text-scale watcher this meant it never died on systemd's stop, so the unit's cgroup stayed populated until `TimeoutStopSec` (90 s) expired and systemd resorted to SIGKILL — visible as a stop job holding up every session shutdown. `install_signal_source` now installs a `sigaction` handler for SIGTERM and SIGINT that writes the signal number to a non-blocking `O_CLOEXEC` pipe (an atomic load and a `write`, errno preserved); the read end is a calloop `Generic` source that drains the pipe and sets `exit_requested`. A handler is process-wide, so the clean exit works no matter which thread receives the signal or when it was created, and no thread blocks anything, so children inherit a clean mask. `SA_RESTART` keeps the handler from injecting spurious EINTRs into the app's own threads; the pipe wakes the event loop regardless. `text_scale` additionally clears the child's mask in `pre_exec` before running `gsettings`, as a guard against a mask already dirty when the process was launched. The `save_state` docs drop the caveat about threads started before `run`, the ordering comment at the call site goes with the constraint it described, and the `signals` feature is dropped from calloop. `libc` becomes a direct dependency for `sigaction`, `pipe2` and `sigprocmask`. Tests: `child_starts_with_clear_signal_mask` blocks SIGTERM in the test thread, spawns a child through the helper and checks its `SigBlk` in `/proc`, with the inverse control asserting that std really does inherit the mask (the reason the helper exists); `handler_writes_signal_to_pipe` exercises the handler directly and through a real `sigaction` + `raise`, then restores `SIG_DFL`.
1164 lines
51 KiB
Rust
1164 lines
51 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
pub use smithay_client_toolkit::seat::keyboard::Keysym;
|
||
pub use calloop::channel::Sender as ChannelSender;
|
||
|
||
use crate::widget::Element;
|
||
|
||
/// Wayland `ext-foreign-toplevel-list-v1` event delivered to apps via
|
||
/// [`App::on_toplevel_event`]. `Opened` fires after the compositor
|
||
/// commits the first `done` for a new handle (so `app_id` is the value
|
||
/// in effect at that point — the protocol allows the compositor to
|
||
/// re-commit later, but most don't). `Closed` fires when the
|
||
/// compositor sends `closed` and the runtime is about to destroy the
|
||
/// handle proxy. Both carry the same `id`, the Wayland protocol id of
|
||
/// the handle, unique within the session and stable for the handle's
|
||
/// lifetime.
|
||
#[ derive( Debug, Clone ) ]
|
||
pub enum ToplevelEvent
|
||
{
|
||
Opened { id: u32, app_id: String },
|
||
Closed { id: u32 },
|
||
}
|
||
|
||
/// Wayland shell mode for the application surface.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
|
||
pub enum ShellMode
|
||
{
|
||
/// Normal application window using xdg-shell protocol.
|
||
/// This is the default for regular applications.
|
||
Window,
|
||
|
||
/// Layer-shell surface at the specified layer.
|
||
/// Used for shell components like panels, backgrounds, overlays.
|
||
Layer( Layer ),
|
||
|
||
/// `ext-session-lock-v1` lock surface. The compositor blanks the outputs
|
||
/// and shows only this surface until the app requests exit (see
|
||
/// [`App::requested_exit`]). Used by the screen locker.
|
||
SessionLock,
|
||
}
|
||
|
||
/// Layer-shell layer position.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
|
||
pub enum Layer
|
||
{
|
||
/// Below normal windows (wallpapers, desktop backgrounds).
|
||
Background,
|
||
/// Below normal windows but above background.
|
||
Bottom,
|
||
/// Not a layer-shell surface at all: an ordinary `xdg_toplevel`
|
||
/// window stacked among the applications, decorated and listed by
|
||
/// the compositor like any other. [`OverlaySpec::size`] is its fixed
|
||
/// size (`0` falls back to a default, it cannot "fill"); anchor,
|
||
/// exclusive zone and keyboard exclusivity are ignored. The
|
||
/// compositor's close button delivers [`OverlaySpec::on_dismiss`].
|
||
Window,
|
||
/// Above normal windows (panels, docks).
|
||
Top,
|
||
/// Above everything (notifications, on-screen displays).
|
||
Overlay,
|
||
}
|
||
|
||
impl Layer
|
||
{
|
||
/// Convert to smithay's wlr-layer Layer type.
|
||
pub( crate ) fn to_wlr_layer( self ) -> smithay_client_toolkit::shell::wlr_layer::Layer
|
||
{
|
||
use smithay_client_toolkit::shell::wlr_layer::Layer as WlrLayer;
|
||
match self
|
||
{
|
||
Layer::Background => WlrLayer::Background,
|
||
Layer::Bottom => WlrLayer::Bottom,
|
||
Layer::Top => WlrLayer::Top,
|
||
Layer::Overlay => WlrLayer::Overlay,
|
||
// Never reaches layer-shell; the reconciler creates a window first.
|
||
Layer::Window => WlrLayer::Top,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Layer-shell anchor edges.
|
||
/// Determines which screen edges the surface is attached to.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
|
||
pub struct Anchor
|
||
{
|
||
pub top: bool,
|
||
pub bottom: bool,
|
||
pub left: bool,
|
||
pub right: bool,
|
||
}
|
||
|
||
impl Anchor
|
||
{
|
||
/// Anchor to all edges (fill screen).
|
||
pub const ALL: Self = Self { top: true, bottom: true, left: true, right: true };
|
||
|
||
/// Anchor to top edge only (horizontal bar at top).
|
||
pub const TOP: Self = Self { top: true, bottom: false, left: true, right: true };
|
||
|
||
/// Anchor to bottom edge only (horizontal bar at bottom).
|
||
pub const BOTTOM: Self = Self { top: false, bottom: true, left: true, right: true };
|
||
|
||
/// Anchor to left edge only (vertical bar at left).
|
||
pub const LEFT: Self = Self { top: true, bottom: true, left: true, right: false };
|
||
|
||
/// Anchor to right edge only (vertical bar at right).
|
||
pub const RIGHT: Self = Self { top: true, bottom: true, left: false, right: true };
|
||
|
||
/// Convert to smithay's Anchor bitflags.
|
||
pub( crate ) fn to_wlr_anchor( self ) -> smithay_client_toolkit::shell::wlr_layer::Anchor
|
||
{
|
||
use smithay_client_toolkit::shell::wlr_layer::Anchor as WlrAnchor;
|
||
let mut anchor = WlrAnchor::empty();
|
||
if self.top { anchor |= WlrAnchor::TOP; }
|
||
if self.bottom { anchor |= WlrAnchor::BOTTOM; }
|
||
if self.left { anchor |= WlrAnchor::LEFT; }
|
||
if self.right { anchor |= WlrAnchor::RIGHT; }
|
||
anchor
|
||
}
|
||
}
|
||
|
||
/// Stable identifier for an overlay surface. The runtime uses it to diff the
|
||
/// overlay list between frames: if the same `OverlayId` is returned from
|
||
/// [`App::overlays`] on consecutive frames the underlying Wayland surface
|
||
/// and its internal state are kept; if it disappears the surface is destroyed.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord ) ]
|
||
pub struct OverlayId( pub u32 );
|
||
|
||
/// Stable identifier for a subsurface, used to diff the list returned by
|
||
/// [`App::subsurfaces`] between frames the same way [`OverlayId`] diffs
|
||
/// overlays.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord ) ]
|
||
pub struct SubsurfaceId( pub u32 );
|
||
|
||
/// Which surface a [`SubsurfaceSpec`] is composited as a child of. `Main`
|
||
/// (the default position) parents to the app's main surface; `Overlay(id)`
|
||
/// parents to one of the [`App::overlays`] surfaces, so a slide can ride
|
||
/// above app windows the way an overlay panel does. An `Overlay` parent that
|
||
/// is absent or not yet configured is skipped for that frame.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
|
||
pub enum SubsurfaceParent
|
||
{
|
||
Main,
|
||
Overlay( OverlayId ),
|
||
}
|
||
|
||
/// One of the surfaces an [`App`] can target with an invalidation. Used inside
|
||
/// [`InvalidationScope::Only`] to name the affected surfaces.
|
||
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ]
|
||
pub enum SurfaceTarget
|
||
{
|
||
/// The application's main surface (the one returned by [`App::view`]).
|
||
Main,
|
||
/// The overlay with the given stable [`OverlayId`].
|
||
Overlay( OverlayId ),
|
||
}
|
||
|
||
/// Which surfaces a given [`App::update`] mutation can affect.
|
||
///
|
||
/// Returned by [`App::invalidate_after`] (default [`InvalidationScope::All`])
|
||
/// to let the runtime skip redraws on surfaces whose contents could not
|
||
/// possibly have changed by the message in question. On a shell with many
|
||
/// overlays most messages only touch one of them, so the savings are large.
|
||
#[ derive( Debug, Clone ) ]
|
||
pub enum InvalidationScope
|
||
{
|
||
/// Treat every surface as potentially affected (safe default).
|
||
All,
|
||
/// Only the listed surfaces may have changed; others can be skipped this
|
||
/// iteration. An empty list means "nothing visible to redraw" — useful
|
||
/// for messages that only touch internal bookkeeping.
|
||
Only( Vec<SurfaceTarget> ),
|
||
}
|
||
|
||
impl InvalidationScope
|
||
{
|
||
/// Combine two scopes. `All` absorbs any other scope; two `Only` scopes
|
||
/// merge into a deduped list of targets. Used by the run loop to fold the
|
||
/// per-message scopes from a batch of pending messages into a single
|
||
/// decision before applying it.
|
||
pub fn union( self, other: Self ) -> Self
|
||
{
|
||
match ( self, other )
|
||
{
|
||
( Self::All, _ ) | ( _, Self::All ) => Self::All,
|
||
( Self::Only( mut a ), Self::Only( b ) ) =>
|
||
{
|
||
for t in b
|
||
{
|
||
if !a.contains( &t ) { a.push( t ); }
|
||
}
|
||
Self::Only( a )
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Description of an additional layer-shell surface rendered on top of the
|
||
/// main application surface.
|
||
///
|
||
/// Apps return a list of these from [`App::overlays`] each frame. The runtime
|
||
/// creates one Wayland layer-shell surface per active overlay, renders its
|
||
/// [`view`](Self::view) to its own canvas, and dispatches input to it
|
||
/// independently of the main surface. Overlays share the same message type as
|
||
/// the main app — messages emitted by widgets inside an overlay are delivered
|
||
/// to [`App::update`] exactly like messages from the main view.
|
||
///
|
||
/// Overlays are useful for building shells whose main surface sits behind
|
||
/// regular app windows (e.g. a homescreen on [`Layer::Background`]) while
|
||
/// still exposing on-demand panels above everything (launcher, quick
|
||
/// settings, power menu…).
|
||
pub struct OverlaySpec<Message: Clone>
|
||
{
|
||
/// Stable identifier, used to diff overlays between frames.
|
||
pub id: OverlayId,
|
||
|
||
/// Wayland layer for this overlay. Typically [`Layer::Overlay`] or
|
||
/// [`Layer::Top`].
|
||
pub layer: Layer,
|
||
|
||
/// Screen edges to anchor to.
|
||
pub anchor: Anchor,
|
||
|
||
/// Desired size `( width, height )`, resolved to logical pixels against
|
||
/// the main surface (output) when the overlay is materialized. A
|
||
/// [`Length::px`](crate::Length::px) keeps a fixed size; a
|
||
/// [`Length::widget`](crate::Length::widget) / `vmin` / `dp` makes the
|
||
/// overlay surface scale with the display like any other sized element.
|
||
/// A component that resolves to `0` (e.g. `Length::px( 0.0 )`) means
|
||
/// "fill available space in that dimension".
|
||
pub size: ( crate::types::Length, crate::types::Length ),
|
||
|
||
/// Exclusive zone in pixels reserved from the anchored edge.
|
||
/// `-1` requests focus without reserving space, `0` is the default for
|
||
/// transient overlays that should not push other surfaces around.
|
||
pub exclusive_zone: i32,
|
||
|
||
/// When `true`, the compositor sends keyboard events to this overlay
|
||
/// without requiring a click first.
|
||
pub keyboard_exclusive: bool,
|
||
|
||
/// Interactive input region as a list of rects (logical pixels). Only
|
||
/// these areas receive pointer/touch input; the rest passes through.
|
||
/// `None` means the full surface receives input.
|
||
pub input_region: Option<Vec<crate::types::Rect>>,
|
||
|
||
/// Widget tree for this overlay.
|
||
pub view: Element<Message>,
|
||
|
||
/// Message sent when the overlay should be dismissed. The runtime
|
||
/// fires it in three situations:
|
||
///
|
||
/// 1. The compositor sends `xdg_popup.popup_done` (xdg-popup mode)
|
||
/// — typically when the user clicks outside the popup with the
|
||
/// grab fully active.
|
||
/// 2. The user presses pointer / touch on the main surface while
|
||
/// the overlay is still mapped, and the press does not fall on
|
||
/// the trigger rect identified by [`Self::anchor_widget_id`].
|
||
/// This covers compositors (notably Mutter) that route the
|
||
/// button to the parent surface instead of breaking the popup
|
||
/// grab when the cursor was already over the parent.
|
||
/// 3. The user presses Escape while at least one xdg-popup overlay
|
||
/// is open.
|
||
///
|
||
/// The application is expected to flip its `is_open` flag (or
|
||
/// equivalent) to `false` in `update()` so the next frame stops
|
||
/// returning the spec from [`App::overlays`]. The runtime is
|
||
/// idempotent if the message arrives more than once for the same
|
||
/// open / close cycle.
|
||
pub on_dismiss: Option<Message>,
|
||
|
||
/// When set, the overlay is rendered as a Wayland **xdg-popup**
|
||
/// anchored to the rect of the widget tagged with this
|
||
/// [`crate::types::WidgetId`] in the previous frame's layout — the
|
||
/// standard mechanism for combo / context-menu / tooltip popups in
|
||
/// `xdg-shell` applications. The compositor positions the popup
|
||
/// adjacent to the anchor and is allowed to flip it (drop-up
|
||
/// instead of drop-down) when there is not enough room.
|
||
///
|
||
/// When this field is `None`, the overlay is rendered as a
|
||
/// [`wlr-layer-shell`](Layer) surface — the path used by shells,
|
||
/// panels, lock screens and overlays that need full-surface
|
||
/// coverage. In that mode `layer` / `anchor` / `exclusive_zone` /
|
||
/// `keyboard_exclusive` carry the placement; for the popup mode
|
||
/// they are ignored.
|
||
pub anchor_widget_id: Option<crate::types::WidgetId>,
|
||
}
|
||
|
||
/// An input-transparent child surface composited over the main surface and
|
||
/// moved by the compositor.
|
||
///
|
||
/// Returned from [`App::subsurfaces`]. The runtime creates one `wl_subsurface`
|
||
/// per active spec, full-size over the main surface, with an **empty input
|
||
/// region** — all pointer/touch falls through to the main surface, so the host
|
||
/// keeps a single input/gesture model. The win is in the move: the content
|
||
/// buffer is rasterised only when [`content_version`](Self::content_version)
|
||
/// (or the surface size) changes, while [`x`](Self::x) / [`y`](Self::y)
|
||
/// changes only emit `wl_subsurface.set_position` + a parent commit — no
|
||
/// re-raster, no buffer re-upload. This makes an animated reveal/slide track
|
||
/// the finger at the compositor's compositing cost rather than the client's
|
||
/// raster cost.
|
||
pub struct SubsurfaceSpec<Message: Clone>
|
||
{
|
||
/// Stable identifier, used to diff subsurfaces between frames.
|
||
pub id: SubsurfaceId,
|
||
|
||
/// Surface this subsurface is composited as a child of. Sized to that
|
||
/// parent; [`x`](Self::x) / [`y`](Self::y) are relative to its top-left.
|
||
pub parent: SubsurfaceParent,
|
||
|
||
/// Widget tree for this subsurface. Should paint its own opaque
|
||
/// background if it must cover the main surface beneath it.
|
||
pub view: Element<Message>,
|
||
|
||
/// Position of the subsurface relative to the parent's top-left, in
|
||
/// layout (physical) pixels — the same space as [`App::on_resize`] and
|
||
/// widget rects. The runtime divides by the surface scale to obtain the
|
||
/// logical position it hands to the compositor. Animate this for a cheap
|
||
/// compositor-side move.
|
||
pub x: i32,
|
||
pub y: i32,
|
||
|
||
/// Content revision. Bump it whenever [`view`](Self::view) would paint
|
||
/// differently; leave it unchanged for position-only updates so the
|
||
/// runtime skips the re-raster and only repositions.
|
||
pub content_version: u64,
|
||
|
||
/// Render through GLES (so `backdrop-filter` glass works) rather than the
|
||
/// software rasteriser. GLES is only worth it for content that actually
|
||
/// uses the blur: it pays a per-surface EGL-surface creation (and a
|
||
/// one-time shader compile) the cheap software path avoids, so a panel
|
||
/// without glass should leave this `false`.
|
||
pub gpu: bool,
|
||
}
|
||
|
||
/// Trait that application types must implement to integrate with ltk.
|
||
pub trait App: 'static
|
||
{
|
||
/// The message type produced by this application.
|
||
type Message: Clone + 'static;
|
||
|
||
/// Build the widget tree for the current frame.
|
||
fn view( &self ) -> Element<Self::Message>;
|
||
|
||
/// Apply a message to the application state.
|
||
fn update( &mut self, msg: Self::Message );
|
||
|
||
/// Stable application identifier, reverse-DNS style
|
||
/// (`"net.liberux.settings"`, `"io.eydos.Calls"`), constant for the
|
||
/// life of the process.
|
||
///
|
||
/// The runtime uses it in three places:
|
||
///
|
||
/// * `xdg_toplevel.set_app_id` on the main window, so the compositor,
|
||
/// the dock and the task switcher can match the window to its
|
||
/// `.desktop` entry (use the desktop file's name minus `.desktop`,
|
||
/// or its `StartupWMClass`);
|
||
/// * the accessibility tree — AccessKit / AT-SPI2 report it as the
|
||
/// application name;
|
||
/// * the on-disk session directory `$XDG_STATE_HOME/<app_id>/`
|
||
/// where [`save_state`](Self::save_state) is persisted.
|
||
///
|
||
/// It must not be empty and must not contain `/`; the runtime then
|
||
/// disables session persistence and logs once. Layer-shell and
|
||
/// session-lock components still return one (it names them in the
|
||
/// a11y tree), even though they never get a toplevel. The `app_id`
|
||
/// element of the deprecated [`window_config`](Self::window_config)
|
||
/// tuple is ignored in favour of this method — keep both pointing at
|
||
/// the same `const`.
|
||
///
|
||
/// ```rust
|
||
/// # use ltk::{ App, Element, text };
|
||
/// # #[ derive( Clone ) ] enum Msg {}
|
||
/// # struct Notes;
|
||
/// const APP_ID: &str = "net.example.Notes";
|
||
///
|
||
/// impl App for Notes
|
||
/// {
|
||
/// # type Message = Msg;
|
||
/// # fn view( &self ) -> Element<Msg> { text( "" ).into() }
|
||
/// # fn update( &mut self, _: Msg ) {}
|
||
/// # fn save_state( &self ) -> Option<Vec<u8>> { None }
|
||
/// # fn restore_state( &mut self, _: Vec<u8> ) {}
|
||
/// fn app_id( &self ) -> &str { APP_ID }
|
||
/// }
|
||
/// ```
|
||
fn app_id( &self ) -> &str;
|
||
|
||
/// Serialize the part of the state worth surviving a restart.
|
||
///
|
||
/// Return the bytes the runtime should persist, or `None` when there
|
||
/// is nothing to bring back (a shell panel, a lock screen, a
|
||
/// stateless tool). The encoding is the app's own — JSON, CBOR, a
|
||
/// hand-rolled line format — ltk never inspects it and puts no serde
|
||
/// bound on the trait. Keep it small and cheap: the runtime calls
|
||
/// this
|
||
///
|
||
/// * every ~30 s while the app runs, touching the disk only when
|
||
/// the bytes differ from the last save (a UI-only change that
|
||
/// round-trips to identical bytes costs one comparison);
|
||
/// * when the window closes — after
|
||
/// [`on_close_requested`](Self::on_close_requested) allowed it or
|
||
/// [`requested_exit`](Self::requested_exit) returned `true`;
|
||
/// * on `SIGTERM` / `SIGINT`, which the runtime turns into a clean
|
||
/// exit of the event loop instead of letting the process die.
|
||
///
|
||
/// The bytes are written atomically (temp file + rename, mode
|
||
/// `0600`) to `$XDG_STATE_HOME/<app_id>/state.bin`
|
||
/// (`~/.local/state/<app_id>/state.bin` when the variable is unset)
|
||
/// next to a `session.json` holding the compositor's
|
||
/// `xdg-session-management-v1` session id and a clean-exit marker.
|
||
/// Returning `None` removes a stale `state.bin`. Only
|
||
/// [`ShellMode::Window`] apps (and the deprecated
|
||
/// [`window_config`](Self::window_config) path) are persisted;
|
||
/// layer and session-lock surfaces are skipped silently, so shell
|
||
/// components simply return `None`.
|
||
///
|
||
/// The file is plain on disk: never put secrets in it (passwords
|
||
/// belong to the secure-mode `text_edit` and its wiped buffers),
|
||
/// and keep large blobs (images, caches) elsewhere. The signal
|
||
/// handler is installed process-wide when [`run`](crate::run) is
|
||
/// entered, so the clean exit holds no matter which thread receives
|
||
/// the signal or when it was started.
|
||
///
|
||
/// [`restore_state`](Self::restore_state) explains when — and when
|
||
/// not — the bytes come back.
|
||
///
|
||
/// ```rust
|
||
/// use ltk::{ App, Element, text };
|
||
/// use serde::{ Deserialize, Serialize };
|
||
///
|
||
/// #[ derive( Clone ) ]
|
||
/// enum Msg { Tab( usize ) }
|
||
///
|
||
/// #[ derive( Serialize, Deserialize ) ]
|
||
/// struct Persisted { version: u32, tab: usize, draft: String }
|
||
///
|
||
/// struct Editor { tab: usize, draft: String, cursor_blink: bool }
|
||
///
|
||
/// impl App for Editor
|
||
/// {
|
||
/// type Message = Msg;
|
||
///
|
||
/// fn app_id( &self ) -> &str { "net.example.Editor" }
|
||
///
|
||
/// fn save_state( &self ) -> Option<Vec<u8>>
|
||
/// {
|
||
/// // Only what a relaunch needs — the blink phase stays out.
|
||
/// let p = Persisted { version: 1, tab: self.tab, draft: self.draft.clone() };
|
||
/// serde_json::to_vec( &p ).ok()
|
||
/// }
|
||
///
|
||
/// fn restore_state( &mut self, state: Vec<u8> )
|
||
/// {
|
||
/// // Tolerate an older or corrupt file: keep the defaults.
|
||
/// if let Ok( p ) = serde_json::from_slice::<Persisted>( &state )
|
||
/// {
|
||
/// self.tab = p.tab;
|
||
/// self.draft = p.draft;
|
||
/// }
|
||
/// }
|
||
/// # fn view( &self ) -> Element<Msg> { text( self.draft.clone() ).into() }
|
||
/// # fn update( &mut self, msg: Msg ) { let Msg::Tab( t ) = msg; self.tab = t; }
|
||
/// }
|
||
/// ```
|
||
fn save_state( &self ) -> Option<Vec<u8>>;
|
||
|
||
/// Re-apply bytes previously returned by [`save_state`](Self::save_state).
|
||
///
|
||
/// Called at most once per process, synchronously inside
|
||
/// [`run`](crate::run) / [`try_run`](crate::try_run), before the
|
||
/// window is created and before the first [`view`](Self::view) — the
|
||
/// first frame already shows the restored state. It is **not**
|
||
/// called on an ordinary launch. The runtime restores app state only
|
||
/// when
|
||
///
|
||
/// * the process is relaunched as part of a session restore: the
|
||
/// shell sets `LTK_SESSION_RESTORE=1` in the environment (the
|
||
/// runtime removes the variable before the app can spawn
|
||
/// children); or
|
||
/// * the previous run of this `app_id` did not exit cleanly (crash,
|
||
/// `SIGKILL`, power loss): its `session.json` still says
|
||
/// `clean_exit: false`.
|
||
///
|
||
/// This mirrors Android's saved-instance-state contract: opening the
|
||
/// app from the launcher gives a fresh instance, coming back after
|
||
/// the system killed it lands where the user left off. Window
|
||
/// geometry (size, position, workspace) is separate — the compositor
|
||
/// restores it through `xdg-session-management-v1` on *every*
|
||
/// launch, plain ones included, using the session id ltk stores.
|
||
///
|
||
/// `state` is exactly what `save_state` produced, possibly by an
|
||
/// older build: validate, version, and fall back to defaults instead
|
||
/// of panicking. Nothing is called when no state file exists or the
|
||
/// app returned `None` last time. Shell components never receive
|
||
/// this call and implement it as a no-op.
|
||
///
|
||
/// See [`save_state`](Self::save_state) for the file layout and a
|
||
/// worked example.
|
||
fn restore_state( &mut self, state: Vec<u8> );
|
||
|
||
/// Tell the runtime which surfaces *could* have changed visibly as a
|
||
/// result of [`update`](Self::update) being called with this message.
|
||
///
|
||
/// Default is [`InvalidationScope::All`] — every surface is redrawn.
|
||
/// A shell that knows, for example, that a `SetVolume` message only
|
||
/// affects its quick-settings overlay can return
|
||
/// `InvalidationScope::Only( vec![ SurfaceTarget::Overlay( quick_id ) ] )`
|
||
/// to skip pointless work on other surfaces.
|
||
///
|
||
/// Called *before* [`update`](Self::update) (so it sees the message but
|
||
/// not the post-update state). Side-effect free: must not mutate `self`.
|
||
fn invalidate_after( &self, _msg: &Self::Message ) -> InvalidationScope
|
||
{
|
||
InvalidationScope::All
|
||
}
|
||
|
||
/// Describe the auxiliary overlay surfaces that should be active this
|
||
/// frame. Each entry becomes its own Wayland layer-shell surface
|
||
/// independent from the main surface returned by [`view`](Self::view).
|
||
///
|
||
/// Return an empty `Vec` (the default) to disable multi-surface support —
|
||
/// the app then renders only onto its main surface.
|
||
///
|
||
/// The runtime diffs the list across frames using [`OverlaySpec::id`]:
|
||
/// stable IDs are kept alive, new IDs cause a surface to be created, and
|
||
/// IDs that disappear cause the surface to be destroyed.
|
||
fn overlays( &self ) -> Vec<OverlaySpec<Self::Message>> { Vec::new() }
|
||
|
||
/// Title of a [`Layer::Window`] overlay, shown in its decoration and
|
||
/// in the compositor's window lists. Default: the [`app_id`](Self::app_id).
|
||
fn overlay_title( &self, _id: OverlayId ) -> Option<String> { None }
|
||
|
||
/// Describe the input-transparent child surfaces composited over the main
|
||
/// surface this frame. Each becomes a `wl_subsurface` the compositor moves
|
||
/// by position; see [`SubsurfaceSpec`]. Diffed across frames by
|
||
/// [`SubsurfaceSpec::id`]. Default empty.
|
||
fn subsurfaces( &self ) -> Vec<SubsurfaceSpec<Self::Message>> { Vec::new() }
|
||
|
||
/// Return any pending messages from external sources (timers, async, etc.).
|
||
fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] }
|
||
|
||
/// Tags for which the app wants a fresh `xdg-activation-v1` token this
|
||
/// iteration. Drained once per loop; each tag is echoed back verbatim
|
||
/// through [`App::on_activation_token`] once the compositor issues the
|
||
/// token, so the app can hand it to a child it is about to spawn via the
|
||
/// `$XDG_ACTIVATION_TOKEN` launch convention. Default none.
|
||
fn take_activation_requests( &mut self ) -> Vec<String> { Vec::new() }
|
||
|
||
/// Deliver a token issued for an earlier [`App::take_activation_requests`]
|
||
/// tag. `tag` is the string the app handed out; `token` is the opaque
|
||
/// activation token to place in the launched child's environment.
|
||
fn on_activation_token( &mut self, _tag: String, _token: String ) -> Option<Self::Message> { None }
|
||
|
||
/// Called once, immediately after the first buffer for the main surface
|
||
/// has been rendered and committed to the compositor. Note: this is the
|
||
/// client-side commit, not the compositor's present — under VT switching
|
||
/// the actual present can be deferred (no DRM master yet), so a handoff
|
||
/// signal that fires here is enough to tell a parent "ready to be
|
||
/// presented as soon as my VT becomes active".
|
||
fn on_first_frame_committed( &mut self ) {}
|
||
|
||
/// Called when the surface is asked to close (compositor request, titlebar button,
|
||
/// layer-shell closed event). Return `true` to allow the application to exit,
|
||
/// or `false` to cancel.
|
||
///
|
||
/// Default: `true` (always close).
|
||
fn on_close_requested( &mut self ) -> bool { true }
|
||
|
||
/// Called when tapping/clicking outside any widget.
|
||
fn on_tap( &mut self ) -> Option<Self::Message> { None }
|
||
|
||
/// Called on key press when no text input is focused.
|
||
fn on_key( &mut self, _keysym: Keysym ) -> Option<Self::Message> { None }
|
||
|
||
/// Called on key press with modifier state. Override this instead of
|
||
/// `on_key` when you need Ctrl/Shift/Alt awareness.
|
||
fn on_key_with_modifiers( &mut self, keysym: Keysym, _ctrl: bool, _shift: bool ) -> Option<Self::Message>
|
||
{
|
||
self.on_key( keysym )
|
||
}
|
||
|
||
/// Called when a sufficient upward swipe gesture is detected.
|
||
///
|
||
/// Return a message to handle the swipe (e.g., opening an overview).
|
||
/// The gesture is recognized when the user drags upward by at least
|
||
/// `swipe_threshold()` × screen height and then releases.
|
||
fn on_swipe_up( &mut self ) -> Option<Self::Message> { None }
|
||
|
||
/// Called during an upward swipe gesture with progress ≥ 0.0.
|
||
///
|
||
/// Use this to create follow-the-finger animations. `progress` starts at 0.0
|
||
/// when the drag begins and increases as the finger moves upward, reaching 1.0
|
||
/// when the drag distance equals `swipe_threshold()` × screen height. It is
|
||
/// **not** clamped at 1.0 — the value keeps growing if the finger drags further
|
||
/// so that follow-the-finger panels can continue tracking the finger all the way
|
||
/// up the screen. Clamp inside the handler if you need a bounded `[0, 1]` range
|
||
/// for a visual indicator.
|
||
///
|
||
/// When the user releases without completing the gesture, this method is called
|
||
/// once more with `progress = 0.0` to signal cancellation.
|
||
fn on_swipe_progress( &mut self, _progress: f32 ) {}
|
||
|
||
/// Called when a sufficient downward swipe gesture is detected.
|
||
///
|
||
/// Return a message to handle the swipe (e.g., toggling quick settings).
|
||
/// The gesture is recognized when the user drags downward by at least
|
||
/// `swipe_down_threshold()` × screen height and then releases.
|
||
fn on_swipe_down( &mut self ) -> Option<Self::Message> { None }
|
||
|
||
/// Called during a downward swipe gesture with progress ≥ 0.0.
|
||
///
|
||
/// Use this to create follow-the-finger animations. `progress` starts at 0.0
|
||
/// when the drag begins and increases as the finger moves downward, reaching
|
||
/// 1.0 when the drag distance equals `swipe_down_threshold()` × screen height.
|
||
/// It is **not** clamped at 1.0 — the value keeps growing if the finger drags
|
||
/// further so that follow-the-finger panels can continue tracking the finger
|
||
/// all the way down the screen. Clamp inside the handler if you need a
|
||
/// bounded `[0, 1]` range for a visual indicator.
|
||
///
|
||
/// When the user releases without completing the gesture, this method is called
|
||
/// once more with `progress = 0.0` to signal cancellation.
|
||
fn on_swipe_down_progress( &mut self, _progress: f32 ) {}
|
||
|
||
/// Fraction of screen height required to trigger [`on_swipe_up`](Self::on_swipe_up).
|
||
///
|
||
/// Default: `0.6` (60% of screen height). Lower values make the gesture easier
|
||
/// to trigger; higher values require more vertical travel.
|
||
fn swipe_threshold( &self ) -> f32 { 0.6 }
|
||
|
||
/// Fraction of screen height required to trigger [`on_swipe_down`](Self::on_swipe_down).
|
||
///
|
||
/// Default: `0.15` (15% of screen height). Typically lower than `swipe_threshold()`
|
||
/// because downward swipes are often initiated from a small UI element like a top bar.
|
||
fn swipe_down_threshold( &self ) -> f32 { 0.15 }
|
||
|
||
/// Top-edge band where a downward swipe may originate, as a fraction of
|
||
/// screen height. Presses below this band never fire
|
||
/// [`on_swipe_down`](Self::on_swipe_down) nor
|
||
/// [`on_swipe_down_progress`](Self::on_swipe_down_progress).
|
||
///
|
||
/// Default: `1.0` (entire screen — any starting point is accepted).
|
||
/// Set to a small value like `0.05` to restrict the gesture to the top
|
||
/// edge, matching the usual system-panel pull-down UX.
|
||
fn swipe_down_edge( &self ) -> f32 { 1.0 }
|
||
|
||
/// Called while a vertical `scroll` is pinned at
|
||
/// its top and the finger keeps pulling down. `progress` is the
|
||
/// accumulated overscroll as a fraction of the surface height (≥ 0.0,
|
||
/// unbounded). Use it to drive a pull-from-top-to-dismiss panel that
|
||
/// tracks the finger.
|
||
fn on_overscroll_down_progress( &mut self, _progress: f32 ) {}
|
||
|
||
/// Called when the finger releases after a top overscroll pull-down.
|
||
/// `committed` is `true` when the pull passed
|
||
/// [`overscroll_dismiss_threshold`](Self::overscroll_dismiss_threshold).
|
||
fn on_overscroll_down( &mut self, _committed: bool ) -> Option<Self::Message> { None }
|
||
|
||
/// Fraction of surface height a top overscroll pull-down must exceed to
|
||
/// commit a dismiss on release. Default: `0.25`.
|
||
fn overscroll_dismiss_threshold( &self ) -> f32 { 0.25 }
|
||
|
||
/// Called when a sufficient leftward swipe gesture is detected.
|
||
///
|
||
/// Return a message to handle the swipe (e.g., paging to the next
|
||
/// homescreen). Fires on release when the user dragged left by at least
|
||
/// `swipe_horizontal_threshold()` × screen width. Unlike vertical swipes,
|
||
/// horizontal swipes survive starting the press on an interactive widget:
|
||
/// once the drag distance crosses an 8 px threshold the press is promoted
|
||
/// to a horizontal drag and no tap will fire at release.
|
||
fn on_swipe_left( &mut self ) -> Option<Self::Message> { None }
|
||
|
||
/// Called when a sufficient rightward swipe gesture is detected.
|
||
/// Mirror of [`on_swipe_left`](Self::on_swipe_left) for the other direction.
|
||
fn on_swipe_right( &mut self ) -> Option<Self::Message> { None }
|
||
|
||
/// Called during a horizontal swipe with signed progress. Negative values
|
||
/// mean the finger has moved left, positive values mean right. The value
|
||
/// is `dx / (threshold * width)`, so ±1.0 marks the commit threshold;
|
||
/// it is **not** clamped so follow-the-finger panels can keep tracking
|
||
/// past the threshold. When the user releases without completing the
|
||
/// gesture this method is called once more with `0.0` to signal
|
||
/// cancellation.
|
||
fn on_swipe_horizontal_progress( &mut self, _progress: f32 ) {}
|
||
|
||
/// Fraction of screen width required to commit [`App::on_swipe_left`] /
|
||
/// [`App::on_swipe_right`]. Default: `0.5` (half the screen). A shell that
|
||
/// wants easier paging can lower this; one that wants to avoid
|
||
/// accidental pages can raise it.
|
||
fn swipe_horizontal_threshold( &self ) -> f32 { 0.5 }
|
||
|
||
/// Duration a press must remain stationary (within tolerance) before the
|
||
/// widget's long-press message fires and the gesture transitions into
|
||
/// drag mode. Default: `500 ms`.
|
||
fn long_press_duration( &self ) -> std::time::Duration
|
||
{
|
||
std::time::Duration::from_millis( 500 )
|
||
}
|
||
|
||
/// Called on every pointer motion once a long-press has fired and the
|
||
/// gesture has entered drag mode. `x`, `y` are in physical pixels of the
|
||
/// surface that owned the original press.
|
||
fn on_drag_move( &mut self, _x: f32, _y: f32 ) {}
|
||
|
||
/// Called on release once a long-press has fired. Return a message to
|
||
/// handle the drop (e.g., commit the drop target). `x`, `y` are in
|
||
/// physical pixels of the surface that owned the original press. The
|
||
/// regular tap / swipe handling is suppressed for this gesture.
|
||
fn on_drop( &mut self, _x: f32, _y: f32 ) -> Option<Self::Message> { None }
|
||
|
||
/// Called when a text-input widget gains or loses focus.
|
||
fn on_text_input_focused( &mut self, _active: bool ) {}
|
||
|
||
/// Translate a Wayland `ext-foreign-toplevel-list-v1` event into an
|
||
/// app message. The runtime binds the protocol globally and forwards
|
||
/// every open / close it sees here; apps that ignore window state
|
||
/// leave the default (return `None`) and pay nothing.
|
||
///
|
||
/// The `id` field is the Wayland object id of the toplevel handle —
|
||
/// unique within the session, stable for the handle's lifetime, the
|
||
/// same value coming in on `Closed` as the matching `Opened`.
|
||
/// `app_id` on `Opened` is the toplevel's `app_id` event (may be
|
||
/// empty if the compositor never sent one before the first `done`).
|
||
fn on_toplevel_event( &self, _event: ToplevelEvent ) -> Option<Self::Message>
|
||
{
|
||
None
|
||
}
|
||
|
||
/// Return `Some(id)` once to focus the widget with that [`WidgetId`](crate::types::WidgetId).
|
||
/// The app must return `None` after the first call.
|
||
fn take_focus_request( &mut self ) -> Option<crate::types::WidgetId> { None }
|
||
|
||
/// Called on every pointer motion (mouse move, button press, button
|
||
/// release). `x`, `y` are in physical pixels of the main surface,
|
||
/// the same coordinate space as widget rects. Default: no-op.
|
||
///
|
||
/// Useful for embedding components that need to know the live
|
||
/// pointer position without going through a full
|
||
/// [`crate::widget::external::ExternalSource`] / `WidgetHandlers`
|
||
/// dispatch path — for example forwarding clicks into an embedded
|
||
/// WPE WebView.
|
||
fn on_pointer_move( &mut self, _x: f32, _y: f32 ) {}
|
||
|
||
/// Called on a wheel / touchpad scroll event whose position does
|
||
/// not fall inside any LTK [`scroll`](crate::scroll) viewport. `x`,
|
||
/// `y` are in physical pixels (same coordinate space as
|
||
/// [`Self::on_pointer_move`]); `dx`, `dy` are scroll deltas in the
|
||
/// raw axis units the compositor delivered (`AxisSource::Wheel`
|
||
/// gives ~10 px ticks, touchpads give continuous values). Default:
|
||
/// no-op.
|
||
///
|
||
/// Useful for embeddings that take over scrolling for their own
|
||
/// content — for example forwarding the event to a WPE view that
|
||
/// owns a scrollable web page — or for wheel-driven view state such
|
||
/// as a stepped carousel. The runtime rebuilds and repaints after
|
||
/// the hook returns, so state mutated here shows immediately.
|
||
fn on_pointer_axis( &mut self, _x: f32, _y: f32, _dx: f32, _dy: f32 ) {}
|
||
|
||
/// Raw multi-touch callbacks. Default: no-op.
|
||
///
|
||
/// The first finger to land becomes the *primary slot* and is fed
|
||
/// through the regular gesture machine — `on_pointer_*`, swipe,
|
||
/// scroll, long-press, drag-and-drop all run from that slot. Every
|
||
/// additional finger fires `on_touch_down` / `on_touch_move` /
|
||
/// `on_touch_up` instead, with `id` matching the `wl_touch.id`
|
||
/// the compositor delivered.
|
||
///
|
||
/// Use this for app-defined multi-finger gestures (pinch-zoom,
|
||
/// two-finger pan in a canvas, parallel keyboard keys) that the
|
||
/// built-in single-slot machine cannot model. `(x, y)` are in
|
||
/// physical pixels, matching the coordinate space of
|
||
/// [`Self::on_pointer_move`].
|
||
///
|
||
/// When [`Self::claims_raw_touch`] returns `true` the primary
|
||
/// finger arrives here as well, instead of driving the gesture
|
||
/// machine.
|
||
fn on_touch_down( &mut self, _id: i64, _x: f32, _y: f32 ) {}
|
||
/// See [`Self::on_touch_down`].
|
||
fn on_touch_move( &mut self, _id: i64, _x: f32, _y: f32 ) {}
|
||
/// See [`Self::on_touch_down`]. `x`, `y` are the last known
|
||
/// position of the finger (`wl_touch.up` does not carry one).
|
||
fn on_touch_up( &mut self, _id: i64, _x: f32, _y: f32 ) {}
|
||
|
||
/// Return `true` to receive the *primary* finger through the raw
|
||
/// [`Self::on_touch_down`] / move / up stream as well, bypassing
|
||
/// the built-in single-slot gesture machine entirely — widget
|
||
/// presses, taps and swipes stop working on touch. For apps whose
|
||
/// surface is one self-contained input consumer (an embedded
|
||
/// WebView, a game canvas) that needs every finger verbatim.
|
||
fn claims_raw_touch( &self ) -> bool { false }
|
||
|
||
/// Pointer of a cross-application drag-and-drop entered or moved
|
||
/// inside the main surface. `(x, y)` is in logical pixels.
|
||
fn on_drop_motion( &mut self, _x: f32, _y: f32 ) {}
|
||
/// The drag left without dropping.
|
||
fn on_drop_leave( &mut self ) {}
|
||
/// External drop payload landed on the surface. `mime` is the
|
||
/// best mime the source offered (text/uri-list, text/plain, …)
|
||
/// and `text` is the decoded payload (UTF-8). `(x, y)` is the
|
||
/// release position in logical pixels.
|
||
fn on_drop_received( &mut self, _x: f32, _y: f32, _mime: &str, _text: &str ) {}
|
||
|
||
/// Called on every left-button mouse press / release before the
|
||
/// regular gesture machine fires. `pressed = true` for press,
|
||
/// `false` for release. `(x, y)` are in physical pixels (same
|
||
/// coordinate space as [`Self::on_pointer_move`]). Default: no-op.
|
||
///
|
||
/// Lets embeddings see the raw button transitions without going
|
||
/// through LTK's tap/long-press/drag classification — needed for
|
||
/// forwarding clicks AND drags to an embedded view, since the tap
|
||
/// classifier collapses press+release into a single event.
|
||
fn on_pointer_button( &mut self, _x: f32, _y: f32, _pressed: bool ) {}
|
||
|
||
/// Called on every keyboard event (press *and* release) before the
|
||
/// regular focus-aware dispatch ([`Self::on_key`] /
|
||
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
||
///
|
||
/// `keycode` is the raw hardware scancode straight from the
|
||
/// compositor (`wl_keyboard.key`'s `key` argument); `keysym` is
|
||
/// the xkb-translated keysym the user effectively pressed. Most
|
||
/// consumers only care about `keysym`, but embeddings that
|
||
/// forward into an inner window system (a WPE web view, an X11
|
||
/// emulator, …) typically need both — the inner system does its
|
||
/// own keycode → keysym translation for layout-aware shortcuts.
|
||
///
|
||
/// This bypass exists for those embeddings: it fires *in addition*
|
||
/// to the normal callbacks; consume or ignore at will.
|
||
fn on_raw_key( &mut self, _keysym: Keysym, _keycode: u32, _pressed: bool, _ctrl: bool, _shift: bool ) {}
|
||
|
||
/// Called when the surface is (re-)configured with a new size.
|
||
///
|
||
/// `width` and `height` are in **physical pixels** (the Wayland surface
|
||
/// size multiplied by the current buffer scale). They therefore match the
|
||
/// coordinate space used for layout and the pointer/touch callbacks
|
||
/// (`on_drag_move`, `on_drop`, widget hit-testing). Also fired when the
|
||
/// buffer scale changes, so apps can refresh any state keyed off the old
|
||
/// physical dimensions.
|
||
fn on_resize( &mut self, _width: u32, _height: u32 ) {}
|
||
|
||
/// Called when the compositor reports a new integer buffer scale for the
|
||
/// main surface. `scale` is the integer buffer scale (1 = standard DPI,
|
||
/// 2 = HiDPI ×2); physical pixels equal logical pixels × scale. The default
|
||
/// is inert so apps that only care about physical pixels can keep using
|
||
/// [`on_resize`](Self::on_resize).
|
||
fn on_scale_changed( &mut self, _scale: u32 ) {}
|
||
|
||
/// Called once at startup with a channel sender that can be used from any
|
||
/// thread to deliver messages into the event loop. Sending a message
|
||
/// immediately wakes the event loop — no polling delay.
|
||
fn set_channel_sender( &mut self, _sender: ChannelSender<Self::Message> ) {}
|
||
|
||
/// How often the event loop should call [`poll_external`](Self::poll_external) for
|
||
/// periodic work such as clock ticks. Return `None` (the default) to disable the
|
||
/// timer — `poll_external` will still be called after every Wayland event.
|
||
fn poll_interval( &self ) -> Option<std::time::Duration> { None }
|
||
|
||
/// Delay before the runtime starts repeating a held-down key.
|
||
///
|
||
/// `Some(d)` overrides whatever the compositor advertises through
|
||
/// `wl_keyboard.repeat_info`; `None` (the default) means "use the
|
||
/// compositor's setting, or fall back to 500 ms when the compositor
|
||
/// did not provide one".
|
||
///
|
||
/// Returning `Some(Duration::ZERO)` effectively disables the wait
|
||
/// before the first repeat — useful only for diagnostic builds.
|
||
fn key_repeat_delay( &self ) -> Option<std::time::Duration> { None }
|
||
|
||
/// Interval between successive synthetic key events while a key is
|
||
/// held down past the initial [`Self::key_repeat_delay`].
|
||
///
|
||
/// `Some(d)` overrides the compositor's `wl_keyboard.repeat_info`
|
||
/// rate; `None` (the default) means "use the compositor's setting,
|
||
/// or fall back to 33 ms (~30 Hz) when the compositor did not
|
||
/// provide one". Pass `Some(Duration::ZERO)` to disable repeats
|
||
/// entirely (or override the per-key gate via
|
||
/// [`Self::key_repeats`]).
|
||
fn key_repeat_interval( &self ) -> Option<std::time::Duration> { None }
|
||
|
||
/// Override the pointer cursor shape globally. When `Some(shape)`,
|
||
/// the runtime sends that shape to the compositor regardless of
|
||
/// which widget the pointer is over — useful for "the app is busy"
|
||
/// states (return [`crate::CursorShape::Wait`]) or while the app
|
||
/// is doing a long synchronous operation. Returning `None` (the
|
||
/// default) lets per-widget defaults take effect.
|
||
///
|
||
/// The runtime calls this every frame, so flipping a `loading`
|
||
/// boolean in [`Self::update`] propagates to the cursor on the
|
||
/// next iteration without any extra wiring.
|
||
fn cursor_override( &self ) -> Option<crate::types::CursorShape> { None }
|
||
|
||
/// Decide whether a given key participates in held-key repeat.
|
||
///
|
||
/// Default: every non-modifier key repeats *except* `Escape`, `Tab`
|
||
/// and `ISO_Left_Tab` — those drive one-shot UI semantics
|
||
/// (dismiss / focus cycle) where a held key would multiply the
|
||
/// effect in surprising ways. Override to widen or narrow the gate
|
||
/// (a chess clock app, for example, might want even Tab to
|
||
/// repeat).
|
||
fn key_repeats( &self, keysym: Keysym ) -> bool
|
||
{
|
||
!matches!( keysym,
|
||
Keysym::Escape | Keysym::Tab | Keysym::ISO_Left_Tab,
|
||
)
|
||
}
|
||
|
||
/// Return `true` while a frame-by-frame animation is running.
|
||
/// The event loop will keep requesting redraws at ~60 fps until this returns `false`.
|
||
fn is_animating( &self ) -> bool { false }
|
||
|
||
/// Cap [`Self::is_animating`] redraws to ~30 Hz when the **software**
|
||
/// renderer is active. Default `true`: 60 fps software rasterization is a
|
||
/// common battery sink on mobile (no GPU offload), and halving it is
|
||
/// usually imperceptible for the animations software fallback can afford.
|
||
/// The GLES backend is never capped. Return `false` to keep the full
|
||
/// display rate on software.
|
||
fn cap_software_animation( &self ) -> bool { true }
|
||
|
||
/// Return `true` when a finger-tracked swipe or an [`Self::is_animating`]
|
||
/// slide only repositions the app's input-transparent subsurfaces
|
||
/// ([`Self::subsurfaces`]) while the main surface buffer stays unchanged.
|
||
/// The runtime then skips the per-frame main re-raster that
|
||
/// [`Self::on_swipe_progress`] and `is_animating` would otherwise force,
|
||
/// keeping the animation pumped (motion events during the drag, a bare
|
||
/// frame callback while `is_animating`) and letting the per-frame
|
||
/// subsurface reconcile carry the motion. Use it for a slide-to-reveal
|
||
/// panel over a static main surface.
|
||
fn subsurface_motion_only( &self ) -> bool { false }
|
||
|
||
/// Background color for the canvas. Override to make the surface
|
||
/// transparent or to deviate from the theme. Default: the active
|
||
/// theme's `bg` token, so the window matches the rest of the
|
||
/// shell out of the box.
|
||
fn background_color( &self ) -> crate::types::Color
|
||
{
|
||
crate::theme::palette().bg
|
||
}
|
||
|
||
/// Return the interactive input region as a list of rects (logical pixels).
|
||
/// Only these areas receive pointer/touch input; the rest passes through.
|
||
/// Return `None` (default) to receive input everywhere.
|
||
fn input_region( &self ) -> Option<Vec<crate::types::Rect>> { None }
|
||
|
||
/// Return `Some(( title, app_id ))` to force an XDG toplevel window instead of
|
||
/// layer-shell overlay. The compositor will display the title in the title bar
|
||
/// and use the app_id for taskbar/icon matching. Return `None` (default) to
|
||
/// use layer-shell when available. The `app_id` element is ignored:
|
||
/// [`app_id`](Self::app_id) names the window.
|
||
///
|
||
/// **Deprecated**: Use [`shell_mode`](Self::shell_mode) instead.
|
||
fn window_config( &self ) -> Option<( &str, &str )> { None }
|
||
|
||
/// Specify the Wayland shell mode for this application.
|
||
///
|
||
/// - [`ShellMode::Window`]: Normal application window (xdg-shell). **Default.**
|
||
/// - [`ShellMode::Layer`]: System component at a specific layer (layer-shell).
|
||
/// - [`ShellMode::SessionLock`]: `ext-session-lock-v1` lock surface (screen locker).
|
||
///
|
||
/// For regular applications, use the default `Window` mode.
|
||
/// For shell components (panels, backgrounds, overlays), use `Layer`.
|
||
/// For a screen locker, use `SessionLock` and request the unlock by
|
||
/// returning `true` from [`requested_exit`](Self::requested_exit).
|
||
fn shell_mode( &self ) -> ShellMode { ShellMode::Window }
|
||
|
||
/// Content for the surfaces the runtime creates on every output beyond
|
||
/// the first. In [`ShellMode::SessionLock`] these are lock surfaces —
|
||
/// the compositor blanks any output without one, so each is covered. In
|
||
/// [`ShellMode::Layer`] on [`Layer::Background`] they are background
|
||
/// layer surfaces, so a shell's wallpaper reaches extended outputs
|
||
/// (typically without the grid or any chrome). They take no input.
|
||
/// `width` / `height` are that output's surface size in physical pixels.
|
||
/// Default `None`: the surface is filled with
|
||
/// [`background_color`](Self::background_color).
|
||
fn secondary_view( &self, _width: u32, _height: u32 ) -> Option<Element<Self::Message>>
|
||
{
|
||
None
|
||
}
|
||
|
||
/// Return `true` to tear the surface down and exit the event loop. For a
|
||
/// [`ShellMode::SessionLock`] surface the runtime calls `unlock` first, so
|
||
/// the compositor lifts the lock instead of leaving the outputs blanked.
|
||
/// Polled after every batch of `update`s.
|
||
fn requested_exit( &self ) -> bool { false }
|
||
|
||
/// Suggest an initial size for an xdg-shell window in logical pixels.
|
||
///
|
||
/// Returning `Some(( w, h ))` makes ltk call both
|
||
/// `xdg_toplevel.set_min_size( w, h )` and
|
||
/// `xdg_toplevel.set_max_size( w, h )` before the first commit, which
|
||
/// most compositors honour as an exact size for the configure they
|
||
/// send back. The window is still resizable from the application's
|
||
/// point of view — if the compositor sends a different configure, the
|
||
/// runtime adopts that size on the next frame.
|
||
///
|
||
/// Only applies to [`ShellMode::Window`] surfaces and to the
|
||
/// xdg-shell fallback path used when `wlr-layer-shell` is missing.
|
||
/// Ignored for layer-shell surfaces, which use [`Self::layer_size`]
|
||
/// instead.
|
||
///
|
||
/// **Default**: `None` (let the compositor pick).
|
||
fn window_size_hint( &self ) -> Option<( u32, u32 )> { None }
|
||
|
||
/// Whether the user may resize an xdg-shell window.
|
||
///
|
||
/// When [`Self::window_size_hint`] is set, ltk pins
|
||
/// `min_size == max_size` before the first commit so the compositor's
|
||
/// first configure honours the requested size, and then releases
|
||
/// `max_size` from the first configure so the surface becomes
|
||
/// user-resizable. Returning `false` skips that release: the pin stays
|
||
/// in place for the lifetime of the toplevel, declaring a fixed-size
|
||
/// window that compositors must not resize or restore a remembered
|
||
/// geometry over.
|
||
///
|
||
/// Ignored when [`Self::window_size_hint`] returns `None`.
|
||
///
|
||
/// **Default**: `true`.
|
||
fn window_resizable( &self ) -> bool { true }
|
||
|
||
/// Request fullscreen on the toplevel before its first commit.
|
||
///
|
||
/// When `true`, ltk calls `xdg_toplevel.set_fullscreen( None )` so
|
||
/// the compositor picks an output and maps the surface fullscreen
|
||
/// from the start (no flicker through a windowed configure first).
|
||
/// The compositor's own decorations are suppressed for fullscreen
|
||
/// surfaces; the built-in titlebar from [`Self::window_config`] is
|
||
/// still painted by ltk on top of the surface unless the app opts
|
||
/// out.
|
||
///
|
||
/// Only applies to [`ShellMode::Window`] surfaces and to the
|
||
/// xdg-shell fallback path used when `wlr-layer-shell` is missing.
|
||
///
|
||
/// **Default**: `false`.
|
||
fn start_fullscreen( &self ) -> bool { false }
|
||
|
||
/// Specify the exclusive zone for layer-shell surfaces.
|
||
///
|
||
/// The exclusive zone reserves screen space for this surface.
|
||
/// For example, a top panel with height 50 should return `50` to prevent
|
||
/// other windows from overlapping it.
|
||
///
|
||
/// - `> 0`: Reserve this many pixels from the anchored edge
|
||
/// - `0`: No exclusive zone (default for overlays)
|
||
/// - `-1`: Don't reserve space but request focus
|
||
///
|
||
/// Only applies to [`ShellMode::Layer`] surfaces. Ignored for windows.
|
||
fn exclusive_zone( &self ) -> i32 { -1 }
|
||
|
||
/// Specify which screen edges the layer-shell surface is anchored to.
|
||
///
|
||
/// Anchoring determines the position and size of the surface:
|
||
/// - [`Anchor::TOP`]: Top bar (anchored to top, left, right)
|
||
/// - [`Anchor::BOTTOM`]: Bottom bar (anchored to bottom, left, right)
|
||
/// - [`Anchor::LEFT`]: Left sidebar (anchored to left, top, bottom)
|
||
/// - [`Anchor::RIGHT`]: Right sidebar (anchored to right, top, bottom)
|
||
/// - [`Anchor::ALL`]: Full screen (anchored to all edges)
|
||
///
|
||
/// **Default**: `Anchor::ALL` (full screen)
|
||
///
|
||
/// Only applies to [`ShellMode::Layer`] surfaces. Ignored for windows.
|
||
fn layer_anchor( &self ) -> Anchor { Anchor::ALL }
|
||
|
||
/// Specify the desired size for layer-shell surfaces.
|
||
///
|
||
/// Returns `(width, height)` where:
|
||
/// - `0` means "fill available space in that dimension"
|
||
/// - `> 0` means "use this exact size in pixels"
|
||
///
|
||
/// **Examples:**
|
||
/// - Top bar: `(0, 50)` - full width, 50px height
|
||
/// - Bottom bar: `(0, 40)` - full width, 40px height
|
||
/// - Side panel: `(300, 0)` - 300px width, full height
|
||
/// - Full screen: `(0, 0)` - fill everything (default)
|
||
///
|
||
/// **Default**: `(0, 0)` (fill all available space)
|
||
///
|
||
/// Only applies to [`ShellMode::Layer`] surfaces. Ignored for windows.
|
||
fn layer_size( &self ) -> ( u32, u32 ) { ( 0, 0 ) }
|
||
|
||
/// Request exclusive keyboard focus for this layer-shell surface.
|
||
///
|
||
/// When `true`, the compositor sends keyboard events to this surface
|
||
/// without requiring a pointer click first. Useful for greeters,
|
||
/// lock screens, and other surfaces that must capture input immediately.
|
||
///
|
||
/// **Default**: `false` (on-demand keyboard interactivity)
|
||
///
|
||
/// Only applies to [`ShellMode::Layer`] surfaces. Ignored for windows.
|
||
fn keyboard_exclusive( &self ) -> bool { false }
|
||
}
|
||
|
||
/// Run the application. Blocks until the window is closed.
|
||
///
|
||
/// Panics on init failure (no Wayland compositor, missing protocol, etc.).
|
||
/// Embedders that need to recover gracefully — e.g. fall back to a
|
||
/// software TTY UI when no compositor is reachable — should call
|
||
/// [`try_run`] instead and match on the returned [`RunError`].
|
||
pub fn run<A: App>( app: A )
|
||
{
|
||
crate::event_loop::run( app );
|
||
}
|
||
|
||
/// Run the application, returning a typed error on init failure.
|
||
///
|
||
/// Same as [`run`] but recoverable: every fatal init step (Wayland
|
||
/// connection, registry, calloop event loop, `wl_compositor` / `wl_shm`
|
||
/// / `xdg_wm_base` bindings) is converted into a [`RunError`] variant
|
||
/// the caller can match on. Once init succeeds the function blocks
|
||
/// until the surface is closed and returns `Ok(())`. Errors from the
|
||
/// dispatch loop itself (already on screen) still panic — they are
|
||
/// non-recoverable since the surface state machine cannot be unwound
|
||
/// cleanly from this entry point.
|
||
///
|
||
/// Use this when:
|
||
///
|
||
/// * the application needs a graceful fallback path on systems without
|
||
/// a Wayland session (CI runners, minimal containers, X11-only
|
||
/// environments where ltk has no backend),
|
||
/// * an embedder wants to log the specific protocol that's missing
|
||
/// instead of panicking with a generic stack trace,
|
||
/// * the caller wants to retry / wait for a compositor to come up
|
||
/// instead of aborting.
|
||
///
|
||
/// The standard `run( app )` remains the simpler entry point for
|
||
/// applications that always run on a known-good Wayland session.
|
||
///
|
||
/// ```rust,no_run
|
||
/// # use ltk::{ button, App, Element, RunError };
|
||
/// # #[ derive( Clone ) ] enum Msg {}
|
||
/// # struct MyApp;
|
||
/// # impl App for MyApp {
|
||
/// # type Message = Msg;
|
||
/// # fn app_id( &self ) -> &str { "net.example.MyApp" }
|
||
/// # fn save_state( &self ) -> Option<Vec<u8>> { None }
|
||
/// # fn restore_state( &mut self, _: Vec<u8> ) {}
|
||
/// # fn view( &self ) -> Element<Msg> { button( "x" ).into() }
|
||
/// # fn update( &mut self, _: Msg ) {}
|
||
/// # }
|
||
/// match ltk::try_run( MyApp )
|
||
/// {
|
||
/// Ok( () ) => {}
|
||
/// Err( RunError::NoWaylandConnection( _ ) ) =>
|
||
/// {
|
||
/// eprintln!( "no compositor — falling back to stdio" );
|
||
/// // run a CLI fallback…
|
||
/// }
|
||
/// Err( RunError::MissingProtocol { name, .. } ) =>
|
||
/// {
|
||
/// eprintln!( "compositor lacks `{name}` — refusing to start" );
|
||
/// std::process::exit( 1 );
|
||
/// }
|
||
/// Err( e ) =>
|
||
/// {
|
||
/// eprintln!( "ltk init failed: {e}" );
|
||
/// std::process::exit( 1 );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
pub fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||
{
|
||
crate::event_loop::try_run( app )
|
||
}
|
||
|
||
pub use crate::event_loop::RunError;
|