First commit. Version 0.1.0
This commit is contained in:
737
src/app.rs
Normal file
737
src/app.rs
Normal file
@@ -0,0 +1,737 @@
|
||||
// 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 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 ) {}
|
||||
|
||||
/// 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;
|
||||
Reference in New Issue
Block a user