ltk/platform/wayland/surface.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
// 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 as _,
wlr_layer::{ KeyboardInteractivity, Layer, LayerShell, LayerSurface },
xdg::{ popup::Popup, window::Window },
},
shm::{ Shm, slot::SlotPool },
};
use smithay_client_toolkit::reexports::client::
{
protocol::{ wl_output::WlOutput, wl_surface::WlSurface },
QueueHandle,
};
use smithay_client_toolkit::session_lock::SessionLockSurface;
use std::sync::Arc;
use crate::app::App;
use crate::egl_context::{ EglContext, EglSurface };
use crate::event_loop::{ AppData, SurfaceFocus, SurfaceState };
use crate::event_loop::perf::RasterTrace;
use crate::types::Rect;
/// 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 ),
/// `ext-session-lock-v1` lock surface, created in `SessionLockHandler::locked`.
Lock( SessionLockSurface ),
/// Session lock requested; waiting for the compositor's `locked` event
/// before the lock surface exists.
PendingLock,
}
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(),
SurfaceKind::Lock( s ) => s.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( .. ) | SurfaceKind::PendingLock => 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::Lock( s ) => Some( s.wl_surface() ),
SurfaceKind::Pending( .. ) | SurfaceKind::PendingLock => 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 App>>,
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
}
}
}
/// The Wayland half of a surface: its role object and what the compositor was last told about it.
pub( crate ) struct WaylandSurfaceState
{
pub surface: SurfaceKind,
/// SHM buffers for the software path; mutually exclusive with `egl_surface`, and the path is locked after the first configure.
pub pool: Option<SlotPool>,
/// EGL window surface for the GPU path, created on the first configure when an [`EglContext`] is available.
pub egl_surface: Option<EglSurface>,
/// Last size sent with `layer_surface.set_size`, compared each reconcile against the overlay's size so overlays can resize after creation. `(0, 0)` for other surfaces and before the first configure.
pub last_requested_size: ( u32, u32 ),
/// Logical exclusive zone last sent with `set_exclusive_zone`, kept in step with the size so the compositor never reserves a band the overlay no longer paints.
pub last_requested_zone: i32,
/// Anchor rect the xdg-popup positioner was last configured with; `None` for other surfaces.
pub last_popup_anchor: Option<Rect>,
/// Monotonic counter for `xdg_popup.reposition` tokens.
pub popup_reposition_token: u32,
/// Opaque rects (physical) last applied to the surface.
pub opaque_full: Option<Vec<Rect>>,
/// `LTK_PERF_RASTER` timing of the GPU frame being drawn, from preparing the canvas to presenting it.
pub raster_trace: Option<RasterTrace>,
/// Title bar height before decoration negotiation; restored when the compositor hands decorations back to the client.
pub titlebar_base: f32,
}
impl From<SurfaceKind> for WaylandSurfaceState
{
fn from( surface: SurfaceKind ) -> Self
{
Self
{
surface,
pool: None,
egl_surface: None,
last_requested_size: ( 0, 0 ),
last_requested_zone: 0,
last_popup_anchor: None,
popup_reposition_token: 0,
opaque_full: None,
raster_trace: None,
titlebar_base: 0.0,
}
}
}
impl<Msg: Clone> SurfaceState<Msg>
{
/// 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,
)
{
let scale = self.buffer_scale() as u32;
let pw = w.saturating_mul( scale );
let ph = h.saturating_mul( scale );
let pool_bytes = (pw as u64)
.saturating_mul( ph as u64 )
.saturating_mul( 4 );
const MAX_POOL: u64 = 256 * 1024 * 1024;
if pool_bytes > MAX_POOL || pw == 0 || ph == 0
{
eprintln!( "[ltk] on_configure: size {w}×{h} (physical {pw}×{ph}) out of range — ignoring" );
return;
}
let pool_bytes = pool_bytes as usize;
let wl = &mut self.platform;
if wl.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 ) = wl.egl_surface
{
es.resize( pw as i32, ph as i32 );
}
} else if wl.pool.is_some() {
// SHM path: reallocate the pool to fit the new buffer size.
match SlotPool::new( pool_bytes, shm )
{
Ok( pool ) => wl.pool = Some( pool ),
Err( e ) =>
{
eprintln!( "[ltk] SlotPool resize failed: {e} — keeping previous allocation" );
return;
}
}
} 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( wl.surface.wl_surface(), pw as i32, ph as i32 )
{
Ok( es ) =>
{
wl.egl_surface = Some( es );
chose_gpu = true;
}
Err( e ) =>
{
eprintln!( "[ltk] eglCreateWindowSurface failed: {e} — using SHM" );
}
}
}
if !chose_gpu
{
match SlotPool::new( pool_bytes, shm )
{
Ok( pool ) => wl.pool = Some( pool ),
Err( e ) =>
{
eprintln!( "[ltk] SlotPool init failed: {e}" );
return;
}
}
}
}
wl.surface.wl_surface().set_buffer_scale( scale as i32 );
self.set_size( w, h );
}
}
impl<A: App> AppData<A>
{
/// 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.platform.egl_context` is available.
pub( crate ) fn on_configure( &mut self, w: u32, h: u32 )
{
self.main.on_configure( &self.platform.shm, self.platform.egl_context.as_ref(), w, h );
// 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.platform.activation_token_pending.take()
{
if let ( Some( activation ), Some( wl ) ) =
( self.platform.activation_state.as_ref(), self.main.platform.surface.try_wl_surface() )
{
activation.activate::<Self>( wl, token );
}
}
self.on_main_resized( w, h );
}
/// 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<SurfaceFocus>
{
if self.main.platform.surface.try_wl_surface() == Some( wl )
{
return Some( SurfaceFocus::Main );
}
for ( id, ss ) in self.overlays.iter()
{
if ss.platform.surface.try_wl_surface() == Some( wl )
{
return Some( SurfaceFocus::Overlay( *id ) );
}
}
None
}
/// Whether `wl` is one of the secondary per-output surfaces. Pointer and
/// touch events landing on them must be dropped rather than falling back
/// to `Main` — their coordinates would hit-test the main surface's
/// widgets. Keyboard keeps the `Main` fallback so typing works wherever
/// the compositor puts the focus.
pub( crate ) fn is_secondary_surface( &self, wl: &WlSurface ) -> bool
{
self.secondaries.iter().any( |( _, ss )| ss.platform.surface.try_wl_surface() == Some( wl ) )
}
}