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