refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9.
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use smithay_client_toolkit::
|
||||
{
|
||||
compositor::CompositorState,
|
||||
shell::
|
||||
{
|
||||
WaylandSurface,
|
||||
wlr_layer::{ KeyboardInteractivity, Layer, LayerShell, LayerSurface },
|
||||
xdg::{ popup::Popup, window::Window },
|
||||
},
|
||||
shm::{ Shm, slot::SlotPool },
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::
|
||||
{
|
||||
protocol::wl_surface::WlSurface,
|
||||
QueueHandle,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::app_data::AppData;
|
||||
use crate::app::OverlayId;
|
||||
use crate::egl_context::{ EglContext, EglSurface };
|
||||
use crate::input::GestureState;
|
||||
use crate::render::Canvas;
|
||||
use crate::types::{ Point, Rect, WidgetId };
|
||||
use crate::widget::LaidOutWidget;
|
||||
|
||||
/// Identifies which surface an event, focus, or draw call belongs to.
|
||||
///
|
||||
/// `Main` refers to the application's main surface (xdg window or layer
|
||||
/// shell). `Overlay( id )` refers to an auxiliary layer-shell surface created
|
||||
/// from an entry in [`crate::app::App::overlays`].
|
||||
#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash )]
|
||||
pub( crate ) enum SurfaceFocus
|
||||
{
|
||||
Main,
|
||||
#[allow( dead_code )]
|
||||
Overlay( OverlayId ),
|
||||
}
|
||||
|
||||
/// Configuration for a layer-shell surface, used both for the main surface
|
||||
/// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay
|
||||
/// returned by [`crate::app::App::overlays`].
|
||||
#[derive( Clone )]
|
||||
pub( crate ) struct LayerConfig
|
||||
{
|
||||
pub layer: Layer,
|
||||
pub exclusive_zone: i32,
|
||||
pub anchor: crate::app::Anchor,
|
||||
pub size: ( u32, u32 ),
|
||||
pub keyboard_exclusive: bool,
|
||||
/// Wayland surface role namespace, sent to the compositor for debugging.
|
||||
pub namespace: &'static str,
|
||||
}
|
||||
|
||||
pub( crate ) enum SurfaceKind
|
||||
{
|
||||
/// Layer shell surface, fully created and committed.
|
||||
Layer( LayerSurface ),
|
||||
/// XDG window fallback (no layer shell compositor support).
|
||||
Window( Window ),
|
||||
/// Layer shell is available but we are waiting for an output to be
|
||||
/// advertised before creating the Wayland surface.
|
||||
Pending( LayerConfig ),
|
||||
/// XDG popup, child of the main window. Used for combo dropdowns,
|
||||
/// context menus, tooltips. The compositor positions it relative
|
||||
/// to an anchor rect specified at creation time and may flip it
|
||||
/// (drop-up vs drop-down) when constrained.
|
||||
Popup( Popup ),
|
||||
}
|
||||
|
||||
impl SurfaceKind
|
||||
{
|
||||
pub( crate ) fn wl_surface( &self ) -> &WlSurface
|
||||
{
|
||||
match self
|
||||
{
|
||||
SurfaceKind::Layer( l ) => l.wl_surface(),
|
||||
SurfaceKind::Window( w ) => w.wl_surface(),
|
||||
SurfaceKind::Popup( p ) => p.wl_surface(),
|
||||
// Unreachable: draw_frame is only called when configured == true,
|
||||
// which only becomes true after on_configure, which requires a real surface.
|
||||
SurfaceKind::Pending( .. ) => unreachable!( "surface not yet created" ),
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`wl_surface`] but returns `None` for a [`SurfaceKind::Pending`]
|
||||
/// surface instead of panicking. Used by event dispatch to look up which
|
||||
/// [`SurfaceState`] an incoming Wayland event belongs to without risk of
|
||||
/// crashing during startup.
|
||||
pub( crate ) fn try_wl_surface( &self ) -> Option<&WlSurface>
|
||||
{
|
||||
match self
|
||||
{
|
||||
SurfaceKind::Layer( l ) => Some( l.wl_surface() ),
|
||||
SurfaceKind::Window( w ) => Some( w.wl_surface() ),
|
||||
SurfaceKind::Popup( p ) => Some( p.wl_surface() ),
|
||||
SurfaceKind::Pending( .. ) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the actual layer surface on `output` using `layer_shell`. Returns
|
||||
/// true if the surface was created (i.e., was `Pending` before).
|
||||
pub( crate ) fn materialize(
|
||||
&mut self,
|
||||
compositor: &CompositorState,
|
||||
layer_shell: &LayerShell,
|
||||
qh: &QueueHandle<AppData<impl crate::app::App>>,
|
||||
output: &smithay_client_toolkit::reexports::client::protocol::wl_output::WlOutput,
|
||||
) -> bool
|
||||
{
|
||||
if let SurfaceKind::Pending( ref cfg ) = *self
|
||||
{
|
||||
let surface = compositor.create_surface( qh );
|
||||
let layer_surface = layer_shell.create_layer_surface(
|
||||
qh,
|
||||
surface,
|
||||
cfg.layer,
|
||||
Some( cfg.namespace ),
|
||||
Some( output ),
|
||||
);
|
||||
layer_surface.set_exclusive_zone( cfg.exclusive_zone );
|
||||
layer_surface.set_anchor( cfg.anchor.to_wlr_anchor() );
|
||||
let interactivity = if cfg.keyboard_exclusive
|
||||
{
|
||||
KeyboardInteractivity::Exclusive
|
||||
} else {
|
||||
KeyboardInteractivity::OnDemand
|
||||
};
|
||||
layer_surface.set_keyboard_interactivity( interactivity );
|
||||
layer_surface.set_size( cfg.size.0, cfg.size.1 );
|
||||
layer_surface.commit();
|
||||
*self = SurfaceKind::Layer( layer_surface );
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-surface render + interaction state.
|
||||
///
|
||||
/// Each Wayland surface managed by the event loop (main surface and any
|
||||
/// active overlays) owns an instance of this struct, so multiple surfaces
|
||||
/// can coexist without interfering.
|
||||
pub( crate ) struct SurfaceState<Msg: Clone>
|
||||
{
|
||||
pub pool: Option<SlotPool>,
|
||||
/// EGL window surface for the GPU path. Populated on the first configure
|
||||
/// when an [`EglContext`] is available; mutually exclusive with `pool`
|
||||
/// (the path is locked after the first configure).
|
||||
pub egl_surface: Option<EglSurface>,
|
||||
pub surface: SurfaceKind,
|
||||
pub canvas: Option<Canvas>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub configured: bool,
|
||||
pub needs_redraw: bool,
|
||||
/// Set to `true` when the app's view may have changed in a way that rects
|
||||
/// don't capture (text content, progress value, slider thumb position, etc).
|
||||
/// Interaction-only transitions (hover in/out, pressed in/out) leave this
|
||||
/// `false` so the draw path can take the partial-redraw fast path.
|
||||
/// Always reset to `false` at the end of a successful draw.
|
||||
pub content_dirty: bool,
|
||||
pub last_draw: std::time::Instant,
|
||||
pub focused_idx: Option<usize>,
|
||||
pub focused_id: Option<WidgetId>,
|
||||
pub hovered_idx: Option<usize>,
|
||||
pub widget_rects: Vec<LaidOutWidget<Msg>>,
|
||||
pub cursor_state: HashMap<usize, usize>,
|
||||
/// Selection anchor (byte offset). When equal to the cursor, there
|
||||
/// is no active selection. Mutated jointly with `cursor_state`:
|
||||
/// plain arrow keys collapse the selection by setting anchor =
|
||||
/// cursor; Shift+arrow extends by leaving anchor put while moving
|
||||
/// cursor; pointer drags set both ends.
|
||||
pub selection_anchor: HashMap<usize, usize>,
|
||||
pub pending_text_values: HashMap<usize, String>,
|
||||
pub scroll_offsets: HashMap<usize, f32>,
|
||||
pub scroll_rects: Vec<( Rect, usize )>,
|
||||
pub scroll_canvases: HashMap<usize, Canvas>,
|
||||
/// Per-scroll list of `(flat_idx, content_y, height)` entries for
|
||||
/// every interactive item the scroll's child laid out, in document
|
||||
/// order. Includes items currently scrolled off-screen so keyboard
|
||||
/// arrow handlers can step `hovered_idx` item-by-item without
|
||||
/// regenerating the layout pass. Y is in pre-translation,
|
||||
/// pre-offset content coordinates.
|
||||
pub scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
|
||||
/// Built-in Copy / Cut / Paste context menu when shown on this
|
||||
/// surface. `None` means no menu is up.
|
||||
pub context_menu: Option<super::context_menu::ContextMenu>,
|
||||
/// Per-surface gesture state machine. Owns the press →
|
||||
/// (long-press? / drag? / scroll? / swipe?) → release lifecycle,
|
||||
/// driven by both pointer and touch handlers. The input layer
|
||||
/// (`src/input/`) calls the lifecycle methods; the long-press
|
||||
/// deadline poller reaches into `gesture.long_press_*` directly.
|
||||
pub gesture: GestureState<Msg>,
|
||||
/// Previous frame interaction state for damage tracking
|
||||
pub prev_focused: Option<usize>,
|
||||
pub prev_hovered: Option<usize>,
|
||||
pub prev_pressed: Option<usize>,
|
||||
/// Height of the client-side title bar (0 for layer-shell surfaces).
|
||||
pub titlebar_height: f32,
|
||||
/// Title text shown in the client-side title bar.
|
||||
pub titlebar_title: String,
|
||||
/// Rect of the close button in the title bar (for hit testing).
|
||||
pub titlebar_close_rect: Rect,
|
||||
/// Compositor scale factor (integer, e.g. 1 or 2). Used to create
|
||||
/// high-DPI buffers and set `dpi_scale` on the canvas.
|
||||
pub scale_factor: i32,
|
||||
/// Last size requested from the compositor via `layer_surface.set_size`.
|
||||
/// Compared against the app's current [`crate::app::OverlaySpec::size`]
|
||||
/// each reconcile pass so that overlays can resize after creation (used
|
||||
/// for slide-down / grow animations). `(0, 0)` for non-layer surfaces
|
||||
/// and for layer surfaces before their first configure.
|
||||
pub last_requested_size: ( u32, u32 ),
|
||||
pub layer_anchor: Option<crate::app::Anchor>,
|
||||
/// Anchor rect the xdg-popup positioner was last configured with.
|
||||
/// `None` for non-popup surfaces.
|
||||
pub last_popup_anchor: Option<Rect>,
|
||||
/// Monotonic counter for `xdg_popup.reposition` tokens.
|
||||
pub popup_reposition_token: u32,
|
||||
/// `true` after the surface has been committed and we are waiting for the
|
||||
/// matching `wl_surface.frame` callback to fire. Drawing is gated on this
|
||||
/// being `false`: even if `needs_redraw` flips back to `true` we sit on
|
||||
/// the next frame until the compositor signals it's time to commit again,
|
||||
/// so the per-surface pacing follows the compositor (and the display
|
||||
/// refresh / VRR / "screen is off") instead of a fixed-period timer.
|
||||
pub frame_pending: bool,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> SurfaceState<Msg>
|
||||
{
|
||||
pub( crate ) fn new( surface: SurfaceKind, titlebar_height: f32, titlebar_title: String ) -> Self
|
||||
{
|
||||
Self {
|
||||
pool: None,
|
||||
egl_surface: None,
|
||||
surface,
|
||||
canvas: None,
|
||||
width: 0,
|
||||
height: 0,
|
||||
configured: false,
|
||||
needs_redraw: false,
|
||||
content_dirty: true,
|
||||
last_draw: std::time::Instant::now() - std::time::Duration::from_millis( 100 ),
|
||||
focused_idx: None,
|
||||
focused_id: None,
|
||||
hovered_idx: None,
|
||||
widget_rects: Vec::new(),
|
||||
cursor_state: HashMap::new(),
|
||||
selection_anchor: HashMap::new(),
|
||||
pending_text_values: HashMap::new(),
|
||||
scroll_offsets: HashMap::new(),
|
||||
scroll_rects: Vec::new(),
|
||||
scroll_navigable_items: HashMap::new(),
|
||||
context_menu: None,
|
||||
scroll_canvases: HashMap::new(),
|
||||
gesture: GestureState::new(),
|
||||
prev_focused: None,
|
||||
prev_hovered: None,
|
||||
prev_pressed: None,
|
||||
titlebar_height,
|
||||
titlebar_title,
|
||||
titlebar_close_rect: Rect::default(),
|
||||
scale_factor: 1,
|
||||
last_requested_size: ( 0, 0 ),
|
||||
layer_anchor: None,
|
||||
last_popup_anchor: None,
|
||||
popup_reposition_token: 0,
|
||||
frame_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a logical (surface-local) position to physical pixel coordinates.
|
||||
pub( crate ) fn to_physical( &self, x: f64, y: f64 ) -> Point
|
||||
{
|
||||
let sf = self.scale_factor.max( 1 ) as f32;
|
||||
Point { x: x as f32 * sf, y: y as f32 * sf }
|
||||
}
|
||||
|
||||
/// Surface width in physical pixels (logical × buffer scale). Matches the
|
||||
/// coordinate space used by pointer/touch callbacks and layout, so swipe
|
||||
/// thresholds can be compared against physical `dx` / `start.x` without
|
||||
/// unit mixing.
|
||||
pub( crate ) fn physical_width( &self ) -> u32
|
||||
{
|
||||
self.width * self.scale_factor.max( 1 ) as u32
|
||||
}
|
||||
|
||||
/// Surface height in physical pixels. See [`Self::physical_width`].
|
||||
pub( crate ) fn physical_height( &self ) -> u32
|
||||
{
|
||||
self.height * self.scale_factor.max( 1 ) as u32
|
||||
}
|
||||
|
||||
/// Configure the surface with a new logical size. Picks the rendering path
|
||||
/// on the very first configure (GPU when `egl_context` is `Some` and the
|
||||
/// EGL window can be created, SHM otherwise) and just resizes the existing
|
||||
/// path on subsequent configures. The two paths are mutually exclusive
|
||||
/// (`pool.is_some()` ⇔ SHM, `egl_surface.is_some()` ⇔ GPU) and locked
|
||||
/// after the first configure — switching backends mid-life would require
|
||||
/// tearing down the canvas as well.
|
||||
pub( crate ) fn on_configure(
|
||||
&mut self,
|
||||
shm: &Shm,
|
||||
egl_context: Option<&Arc<EglContext>>,
|
||||
w: u32,
|
||||
h: u32,
|
||||
)
|
||||
{
|
||||
self.width = w;
|
||||
self.height = h;
|
||||
let scale = self.scale_factor.max( 1 ) as u32;
|
||||
let pw = w * scale;
|
||||
let ph = h * scale;
|
||||
|
||||
if self.egl_surface.is_some()
|
||||
{
|
||||
// GPU path: resize the wl_egl_window so the next eglSwapBuffers
|
||||
// publishes a buffer of the new size. The canvas FBO is reallocated
|
||||
// by `Canvas::resize`, but only after we eglMakeCurrent — the draw
|
||||
// path will do that, so we just record the pending size here.
|
||||
if let Some( ref es ) = self.egl_surface
|
||||
{
|
||||
es.resize( pw as i32, ph as i32 );
|
||||
}
|
||||
} else if self.pool.is_some() {
|
||||
// SHM path: reallocate the pool to fit the new buffer size.
|
||||
self.pool = Some(
|
||||
SlotPool::new( ( pw * ph * 4 ) as usize, shm ).expect( "pool" ),
|
||||
);
|
||||
} else {
|
||||
// First configure — pick the path. Try GPU when the bootstrap
|
||||
// succeeded; fall back to SHM if surface creation fails.
|
||||
let mut chose_gpu = false;
|
||||
if let Some( ctx ) = egl_context
|
||||
{
|
||||
match ctx.create_surface( self.surface.wl_surface(), pw as i32, ph as i32 )
|
||||
{
|
||||
Ok( es ) =>
|
||||
{
|
||||
self.egl_surface = Some( es );
|
||||
chose_gpu = true;
|
||||
}
|
||||
Err( e ) =>
|
||||
{
|
||||
eprintln!( "[ltk] eglCreateWindowSurface failed: {e} — using SHM" );
|
||||
}
|
||||
}
|
||||
}
|
||||
if !chose_gpu
|
||||
{
|
||||
self.pool = Some(
|
||||
SlotPool::new( ( pw * ph * 4 ) as usize, shm ).expect( "pool" ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.surface.wl_surface().set_buffer_scale( self.scale_factor.max( 1 ) );
|
||||
self.configured = true;
|
||||
self.needs_redraw = true;
|
||||
self.content_dirty = true;
|
||||
}
|
||||
|
||||
/// Request a redraw and mark the surface's content as dirty.
|
||||
///
|
||||
/// Use this from every site that potentially changes what the app's view
|
||||
/// returns (text edits, scroll offset, slider value, app messages, animation
|
||||
/// tick, configure). Pure hover/pressed transitions should leave
|
||||
/// `content_dirty` alone and only set `needs_redraw` directly so the draw
|
||||
/// pass can take the cheap partial-redraw path.
|
||||
pub( crate ) fn request_redraw( &mut self )
|
||||
{
|
||||
self.needs_redraw = true;
|
||||
self.content_dirty = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user