Adds an `App` callback that delivers the live list of open toplevels from the compositor — the data source a shell needs for dock running-app indicators, taskbar tiles, alt-tab and any other "what is currently running" UI. Hand-wiring the protocol binding from every shell that wants it is the kind of boilerplate ltk should absorb once: this is that move.
`Cargo.toml` adds `"staging"` to `wayland-protocols`' feature list. SCTK 0.20 already pulls staging in transitively (it carries `foreign_toplevel_list.rs` and its own dispatch helper), so this is belt-and-braces against a future ltk tree that swaps SCTK for a different client toolkit — it keeps the protocol available crate-wide even then. `src/app.rs` introduces `ToplevelEvent { Opened { id: u32, app_id: String }, Closed { id: u32 } }` and `App::on_toplevel_event( &self, ToplevelEvent ) -> Option<Self::Message>` with the default returning `None` so apps that do not care pay nothing (no allocation, no dispatch). `id` is the Wayland protocol id of the handle proxy — unique per session, stable for the handle's lifetime, the same value paired across `Opened` and `Closed`. `src/lib.rs` re-exports `ToplevelEvent` from the public prelude.
`src/event_loop/app_data.rs` grows a `pub foreign_toplevel_list: ForeignToplevelList` field. `src/event_loop/mod.rs` constructs it via `ForeignToplevelList::new( &globals, &qh )` and stores it on `AppData`. If the compositor does not advertise the global the inner `GlobalProxy` just resolves to "absent" and the list yields no toplevels — no error path needed at construction. `src/event_loop/handlers.rs` adds the `Proxy` import, `delegate_foreign_toplevel_list!( @<A: App> AppData<A> )` next to the rest, and implements `ForeignToplevelListHandler` for `AppData<A>`. The three SCTK callbacks (`new_toplevel`, `update_toplevel`, `toplevel_closed`) each pull the handle's protocol id and its currently-cached `app_id` from the list's info cache, call `self.app.on_toplevel_event( … )`, and push the returned message onto `pending_msgs` so it flows through the normal `update` cycle with the regular `invalidate_after` scoping path. `update_toplevel` re-emits `Opened` with the latest info — compositors fire this on title changes too, not just `app_id` changes, but apps whose state is keyed on `(id, app_id)` can absorb the repeat idempotently and apps that need title-change granularity can scope via `invalidate_after`.
The wire-up is generic: a shell that wants finer behaviour (focus follow, per-title indicators, multi-window grouping) can layer on top by translating the event into more specific app messages. The default app pays zero, the shell that opts in gets a real event stream without touching `smithay-client-toolkit` directly.
769 lines
32 KiB
Rust
769 lines
32 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 ),
|
||
}
|
||
|
||
/// 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,
|
||
/// 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,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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 );
|
||
|
||
/// 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 )` in logical pixels. `0` in either
|
||
/// component means "fill available space in that dimension".
|
||
pub size: ( u32, u32 ),
|
||
|
||
/// 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>,
|
||
}
|
||
|
||
/// 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 );
|
||
|
||
/// 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() }
|
||
|
||
/// Return any pending messages from external sources (timers, async, etc.).
|
||
fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] }
|
||
|
||
/// 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..=1.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.
|
||
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 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.
|
||
fn on_pointer_axis( &mut self, _x: f32, _y: f32, _dx: f32, _dy: f32 ) {}
|
||
|
||
/// 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 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 }
|
||
|
||
/// Return `true` while the next frame should swap the expensive
|
||
/// Glass passes for cheap fallbacks — currently the
|
||
/// `backdrop-filter` blur drops from a 41-tap kernel to a 9-tap
|
||
/// kernel, with the snapshot region shrunk to match. The event
|
||
/// loop sets this on the renderer right before drawing through an
|
||
/// internal low-quality-paint flag.
|
||
///
|
||
/// Default: matches [`Self::is_animating`], so a settle animation
|
||
/// automatically downgrades. Override to include other "in motion"
|
||
/// states the runtime cannot observe — e.g. a finger-tracked
|
||
|
||
/// 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.
|
||
///
|
||
/// **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).
|
||
///
|
||
/// For regular applications, use the default `Window` mode.
|
||
/// For shell components (panels, backgrounds, overlays), use `Layer`.
|
||
fn shell_mode( &self ) -> ShellMode { ShellMode::Window }
|
||
|
||
/// 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 }
|
||
|
||
/// 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 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;
|