// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use std::collections::HashMap; use crate::app::{ App, OverlayId }; use crate::platform::{ NativePlatform, Platform }; use crate::types::Point; use super::repeat::{ ButtonRepeatState, KeyRepeatState }; use super::surface::{ SurfaceFocus, SurfaceState }; use super::tooltip::{ TooltipPending, TooltipVisible }; /// The runtime's state for one app, on the platform `P`: the build's backend unless a test supplies another. pub struct AppData> { pub app: A, pub platform: P, pub pointer_pos: Point, /// The cursor shape currently active on the pointer. `None` /// means "we have not pushed any shape since the last /// `wl_pointer.enter`" — the compositor is showing whatever the /// previous client requested, so the next `dispatch_cursor_shape` /// must send unconditionally to claim the cursor for our /// surface. `Some(s)` lets the dispatch short-circuit when the /// target equals what we already sent. pub current_cursor_shape: Option, /// No input method to ask for: the runtime draws its own keyboard under a focused text field. pub builtin_osk: bool, /// Whether the currently focused text field is `secure` (password). /// Drives the text-input-v3 content type. pub text_input_secure: bool, pub text_input_purpose: crate::osk::Purpose, /// Serial of the [`App::external_text_input`] request the text-input is held for. pub external_text_input: Option, /// The selection the middle button pastes, ours or another application's. pub primary_selection: String, /// The selection of the focused text field when it was last looked at. pub primary_field: Option, /// 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, pub shift_pressed: bool, pub ctrl_pressed: bool, pub alt_pressed: bool, /// Calloop handle for inserting timers / channels. Used by the /// key-repeat machinery and any future feature that needs to /// schedule work on the run loop without going through the message /// queue. pub loop_handle: calloop::LoopHandle<'static, Self>, /// Repeat rate (events per second) advertised by the compositor in /// `wl_keyboard.repeat_info`. `0` means "the compositor disabled /// repeat" — we honour that and never start a repeat timer. pub compositor_repeat_rate: u32, /// Initial delay (ms) before the first repeat fires, advertised by /// the compositor. `0` means the compositor disabled repeat. pub compositor_repeat_delay: u32, /// Currently-active key repeat. `Some` between the press of a /// repeating key and either its release, the keyboard losing focus /// or another key being pressed. pub key_repeat: Option, /// 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. Outbound copies are offered to other /// applications through [`Platform::publish_clipboard`]; the backend /// writes their selections here. pub clipboard: String, /// Timestamp of the previous press (pointer or touch). Combined /// with [`Self::last_press_pos`] it lets the press handler /// detect a double-click on a TextEdit and turn it into a /// word-select. pub last_press_time: Option, /// 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, /// Set to true when the app has accepted a close request. pub exit_requested: bool, /// Last value of `App::fullscreen` sent to the compositor. pub applied_fullscreen: Option, /// 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>, /// Surfaces covering every output beyond the first, paired with the /// output each one belongs to: session-lock surfaces while locked (the /// compositor blanks any output without one), background layer surfaces /// for a [`crate::Layer::Background`] shell (the wallpaper reaches /// extended outputs). These render [`App::secondary_view`] and take no /// input (the input handlers find no focus for them, and keyboard falls /// through to `Main`). pub secondaries: Vec<( P::Output, SurfaceState )>, /// Surface currently receiving pointer events (updated on each pointer /// event via its `surface` field). pub pointer_focus: SurfaceFocus, /// Surface currently holding keyboard focus (updated on keyboard /// enter/leave). pub keyboard_focus: SurfaceFocus, /// The current press landed on the widget that already held the focus, not the press that gives it. pub press_on_focused: bool, /// Per-touch-id tracking of which surface each active touch belongs to /// (populated on `down`, cleared on `up`/`cancel`). pub touch_focus: HashMap, /// 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, /// Focus request deferred because the target widget was absent from /// `widget_rects` (not yet rebuilt, or clipped by a scroll viewport), /// with the number of drawn frames it may still wait for. pub focus_retry: Option<( crate::types::WidgetId, u8 )>, /// Runtime performance guardrails (stuck-animation / poll-interval /// diagnostics and the software animation-rate cap). See [`super::perf`]. pub perf: super::perf::PerfState, } impl AppData { /// Whether the main surface is an application window the user can move, resize and minimise. pub( crate ) fn main_is_toplevel( &self ) -> bool { self.platform.is_toplevel( &self.main.platform ) } /// Forward a change of [`App::fullscreen`] to the main window. pub( crate ) fn sync_fullscreen( &mut self ) { let Some( wanted ) = self.app.fullscreen() else { return }; if self.applied_fullscreen == Some( wanted ) { return; } if self.platform.set_fullscreen( &self.main.platform, wanted ) { self.applied_fullscreen = Some( wanted ); } } /// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus` /// refers to an overlay that is not currently registered — callers must /// only pass focus values obtained from the backend's surface lookup or from the /// per-device focus fields, which the run loop keeps in sync. #[ allow( dead_code ) ] pub( crate ) fn surface( &self, focus: SurfaceFocus ) -> &SurfaceState { 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; // Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine. if self.main.primary_touch_id.is_none() { self.main.primary_touch_id = ss.primary_touch_id; } } } if let SurfaceFocus::Overlay( fid ) = self.pointer_focus { if fid == id { self.pointer_focus = SurfaceFocus::Main; } } if let SurfaceFocus::Overlay( fid ) = self.keyboard_focus { if fid == id { self.keyboard_focus = SurfaceFocus::Main; } } for f in self.touch_focus.values_mut() { if let SurfaceFocus::Overlay( fid ) = *f { if fid == id { *f = SurfaceFocus::Main; } } } if let Some( pending ) = self.tooltip_pending.as_ref() { if pending.focus == SurfaceFocus::Overlay( id ) { self.tooltip_pending = None; } } if let Some( visible ) = self.tooltip_visible.as_ref() { if visible.focus == SurfaceFocus::Overlay( id ) { self.tooltip_visible = None; self.overlays_dirty = true; } } } /// Tell the app and the accessibility tree the main surface is now `w × h` logical pixels at its scale, once the backend has resized it. pub( crate ) fn on_main_resized( &mut self, w: u32, h: u32 ) { if let Some( ref mut a ) = self.a11y { let sf = self.main.scale_factor.max( 1 ) as f64; a.set_window_bounds( ( w as f64 ) * sf, ( h as f64 ) * sf ); a.set_window_focus( true ); } // `on_resize` is documented to deliver **physical** pixels, matching the // coordinate space that the layout passes and the pointer/touch // callbacks (`on_drag_move`, `on_drop`) work in. A configure's size is // logical, so multiply by the current buffer scale before handing the // dimensions to the app. let sf = self.main.scale_factor.max( 1 ) as u32; crate::types::set_viewport_size( w * sf, h * sf ); crate::types::set_density( sf as f32 ); self.app.on_scale_changed( sf ); self.app.on_resize( w * sf, h * sf ); // `on_resize` may flip app-state that the view depends on (apps that // branch on the new dimensions, layout caches keyed by size, …), so // drop the cached tree to force a rebuild on the next draw. self.dirty_caches(); } /// Mark both view caches stale after a direct app-state mutation that /// doesn't go through [`App::update`] — swipe progress callbacks, text- /// input focus changes, configure-driven resize. Per-message updates use /// the run loop's `apply_invalidation` path instead so that /// [`App::invalidate_after`] can scope the rebuild. pub( crate ) fn dirty_caches( &mut self ) { self.view_dirty = true; self.overlays_dirty = true; } /// Smallest remaining time until a pending long-press deadline fires. /// /// Used by the run loop to bound `event_loop.dispatch()` so that a /// stationary press wakes us up at the right moment even when no /// Wayland events arrive. `Some(Duration::ZERO)` means the deadline /// has already elapsed; `None` means nothing is pending. pub( crate ) fn next_long_press_wakeup( &self ) -> Option { 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 } }