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 ) )
	}
}