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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
|
//! Compositor state shared by both backends.
//!
//! [`CompState`] is the `D` type parameter of smithay's `Display<D>`: every
//! protocol handler in [`crate::protocols`] is implemented on it, and every
//! calloop callback the udev backend registers receives `&mut CompState`.
//! That makes it the one place backend-agnostic state has to live - hence
//! the `udev` field, which is `Some` only for the DRM backend.
//!
//! The inherent methods here are srdwm's own window bookkeeping (mapping
//! toplevels to `srdwm_core::WindowId`s, pushing geometry back out, keeping
//! titlebar buffers current). Protocol *reactions* live in
//! [`crate::protocols`]; input routing lives in [`crate::input`].
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::time::Instant;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::desktop::{layer_map_for_output, Space, Window as DWindow, WindowSurfaceType};
use smithay::input::{Seat, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{DisplayHandle, Resource};
use smithay::utils::{Logical, Point, Rectangle, Size, Transform, SERIAL_COUNTER};
use smithay::wayland::compositor::{with_states, CompositorClientState, CompositorState};
use smithay::wayland::selection::data_device::{set_data_device_focus, DataDeviceState};
use smithay::wayland::selection::primary_selection::{set_primary_focus, PrimarySelectionState};
use smithay::wayland::selection::wlr_data_control::DataControlState;
use smithay::wayland::session_lock::SessionLockManagerState;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, LayerSurfaceData, WlrLayerShellState};
use smithay::wayland::shell::xdg::{ToplevelSurface, XdgShellState, XdgToplevelSurfaceData};
use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
use smithay::wayland::shm::ShmState;
use srdwm_core::{Event as CoreEvent, Window as CoreWindow, WindowId, WindowManager, TITLEBAR_HEIGHT};
use crate::lock::SessionLock;
use crate::{decoration, screencopy, udev, xwayland};
#[derive(Default)]
pub(crate) struct ClientState {
pub(crate) compositor_state: CompositorClientState,
}
impl ClientData for ClientState {
fn initialized(&self, _client_id: ClientId) {}
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}
/// One output srdwm drives, and where it sits in the global coordinate
/// space.
///
/// The smithay [`Output`] is the protocol object: it carries the
/// `wl_output` global and owns that output's `LayerMap`.
///
/// A entry's **index in `CompState::outputs` is its
/// [`srdwm_core::Monitor`] id** - the udev backend builds both lists in
/// the same connector order, so core's already-multi-monitor-aware layout
/// code (`WindowManager::arrange_workspace` groups windows by monitor)
/// lines up with what is actually on screen without a separate mapping.
pub(crate) struct OutputEntry {
pub(crate) output: Output,
/// Origin of this output in the global space. Outputs are laid out
/// left-to-right, so this is `(sum of widths to the left, 0)`.
pub(crate) location: Point<i32, Logical>,
}
impl OutputEntry {
/// Size in logical coordinates, or `(0, 0)` if no mode is set yet.
pub(crate) fn size(&self) -> Size<i32, Logical> {
self.output
.current_mode()
.map(|m| (m.size.w, m.size.h).into())
.unwrap_or_default()
}
/// This output's rectangle in the global space.
pub(crate) fn geometry(&self) -> Rectangle<i32, Logical> {
Rectangle::new(self.location, self.size())
}
}
/// Everything smithay's protocol handlers need `&mut` access to. This is the
/// `D` type parameter of `Display<D>` - every `delegate_*!` macro below
/// requires the corresponding `*Handler` trait to be implemented on it.
pub(crate) struct CompState {
pub(crate) compositor_state: CompositorState,
pub(crate) xdg_shell_state: XdgShellState,
pub(crate) _xdg_decoration_state: XdgDecorationState,
pub(crate) shm_state: ShmState,
pub(crate) seat_state: SeatState<CompState>,
pub(crate) seat: Seat<CompState>,
pub(crate) space: Space<DWindow>,
/// Every output srdwm drives, left-to-right in the global coordinate
/// space. The winit backend always has exactly one (its nested window);
/// the udev backend has one per connected connector.
///
/// Nothing outside this module should index this directly - go through
/// [`CompState::primary_output`], [`CompState::output_at`] or
/// [`CompState::output_for_wl`], so the single- and multi-output cases
/// stay the same code path.
pub(crate) outputs: Vec<OutputEntry>,
pub(crate) layer_shell_state: WlrLayerShellState,
/// Needed by the selection (clipboard) protocols: `set_data_device_focus`
/// and `set_primary_focus` both take a `DisplayHandle`, and focus has to
/// be re-pointed on every focus change (see `set_keyboard_focus`).
pub(crate) dh: DisplayHandle,
pub(crate) data_device_state: DataDeviceState,
pub(crate) primary_selection_state: PrimarySelectionState,
pub(crate) data_control_state: DataControlState,
pub(crate) session_lock_state: SessionLockManagerState,
pub(crate) _screencopy_state: screencopy::ScreencopyState,
/// Captures requested via `wlr-screencopy` but not yet serviced; drained
/// inside the render pass (see `screencopy::service_pending`).
pub(crate) screencopy_pending: Vec<screencopy::PendingCapture>,
/// Session lock (`ext-session-lock-v1`). While `locked` is set, client
/// content is never rendered and input never reaches normal clients --
/// see `SessionLockHandler` below.
pub(crate) lock: SessionLock,
/// What the pointer should look like, as set by the focused client (or
/// the default when no client has said). See `cursor.rs` for why this
/// has to be drawn by us on the DRM backend.
pub(crate) cursor_status: smithay::input::pointer::CursorImageStatus,
/// Bitmap for the built-in arrow, built once at startup rather than
/// per frame.
pub(crate) cursor_buffer: MemoryRenderBuffer,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
pub(crate) decorations: HashMap<WindowId, MemoryRenderBuffer>,
pub(crate) pending: Rc<RefCell<Vec<CoreEvent>>>,
pub(crate) bound_keys: Rc<HashSet<String>>,
pub(crate) start_time: Instant,
/// `Some` only for the udev/DRM backend; see `udev.rs` module docs for
/// why its runtime state lives here rather than on a separate struct.
pub(crate) udev: Option<udev::UdevState>,
/// XWayland support; see `xwayland.rs` module docs. `xwm` is `None`
/// until `XWaylandEvent::Ready` fires.
pub(crate) xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState,
pub(crate) xwm: Option<smithay::xwayland::X11Wm>,
pub(crate) xwayland_windows: HashMap<xwayland::X11Window, WindowId>,
/// Mapped X11 windows still waiting for XWayland to associate a
/// `wl_surface` - see `xwayland.rs` and `commit()` above.
pub(crate) xwayland_pending: Vec<smithay::xwayland::X11Surface>,
}
/// Titlebar background is the same regardless of focus (matching the X11
/// backend); only the title text color changes.
const TITLEBAR_BG: (u8, u8, u8) = (0x2e, 0x34, 0x40);
const TITLEBAR_FG_FOCUSED: (u8, u8, u8) = (0x88, 0xc0, 0xd0);
const TITLEBAR_FG_UNFOCUSED: (u8, u8, u8) = (0x4c, 0x56, 0x6a);
/// Output lookup. Everything that used to reach for a single
/// `CompState::output` goes through one of these, so adding outputs did not
/// require every call site to learn about multiple ones.
impl CompState {
/// The output new surfaces land on when nothing else determines it.
/// First in the list, matching the udev backend's connector order.
pub(crate) fn primary_output(&self) -> Option<&Output> {
self.outputs.first().map(|e| &e.output)
}
/// The output containing a point in the global space - pointer
/// hit-testing, and deciding which output a window belongs to. Falls
/// back to the primary output if the point is outside every output
/// (possible between mismatched-height monitors).
pub(crate) fn output_at(&self, pos: Point<f64, Logical>) -> Option<&OutputEntry> {
let point = pos.to_i32_round();
self.outputs
.iter()
.find(|e| e.geometry().contains(point))
.or_else(|| self.outputs.first())
}
/// Resolves a client-supplied `wl_output` to one of ours. Clients name
/// outputs in layer-shell, session-lock and screencopy requests.
pub(crate) fn output_for_wl(&self, wl: &WlOutput) -> Option<&OutputEntry> {
let output = Output::from_resource(wl)?;
self.outputs.iter().find(|e| e.output == output)
}
/// Iterator over the smithay outputs, for render loops.
pub(crate) fn outputs(&self) -> impl Iterator<Item = &Output> {
self.outputs.iter().map(|e| &e.output)
}
}
impl CompState {
pub(crate) fn new_managed_window(&mut self, toplevel: ToplevelSurface) {
let surface = toplevel.wl_surface().clone();
let id = {
let mut wm = self.wm.borrow_mut();
let id = wm.alloc_window_id();
let title = with_toplevel_title(&toplevel).unwrap_or_default();
let mut w = CoreWindow::new(id, title);
w.geometry = srdwm_core::Rect::new(0, 0, 800, 600 + TITLEBAR_HEIGHT as i32 as u32);
wm.add_window(w);
id
};
let geom = self.wm.borrow().window(id).map(|w| w.geometry).unwrap_or_default();
let dwindow = DWindow::new_wayland_window(toplevel.clone());
toplevel.with_pending_state(|state| {
state.size = Some((geom.width as i32, (geom.height - TITLEBAR_HEIGHT) as i32).into());
});
toplevel.send_configure();
self.space.map_element(dwindow.clone(), (geom.x, geom.y + TITLEBAR_HEIGHT as i32), true);
self.surface_to_id.insert(surface.clone(), id);
self.id_to_window.insert(id, dwindow);
self.redraw_decoration_buffer(id);
// `WindowManager::add_window` already made this the focused window in
// srdwm's own state, but that alone is purely internal bookkeeping --
// without this, a freshly-opened window receives no keystrokes and
// can't copy/paste until it's clicked, because nothing ever gave it
// real Wayland keyboard/selection focus. (Same class of bug as the
// click-to-focus one fixed earlier; this is the creation path.)
self.set_keyboard_focus(Some(surface));
self.pending.borrow_mut().push(CoreEvent::WindowCreated(id));
}
/// (Re)renders the titlebar band for `id` - background plus title text
/// via `decoration::render_titlebar` - and replaces the buffer in
/// `self.decorations`. Called on creation, geometry change (width
/// affects layout), and focus change (text color).
pub(crate) fn redraw_decoration_buffer(&mut self, id: WindowId) {
let Some(w) = self.wm.borrow().window(id).cloned() else { return };
if !w.decorated {
self.decorations.remove(&id);
return;
}
let focused = self.wm.borrow().focused_id() == Some(id);
let fg = if focused { TITLEBAR_FG_FOCUSED } else { TITLEBAR_FG_UNFOCUSED };
let width = w.geometry.width.max(1);
let data = decoration::render_titlebar(width, TITLEBAR_HEIGHT, &w.title, TITLEBAR_BG, fg);
let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (width as i32, TITLEBAR_HEIGHT as i32), 1, Transform::Normal, None);
self.decorations.insert(id, buffer);
}
pub(crate) fn remove_window(&mut self, surface: &WlSurface) {
let Some(id) = self.surface_to_id.remove(surface) else { return };
if let Some(w) = self.id_to_window.remove(&id) {
self.space.unmap_elem(&w);
}
self.decorations.remove(&id);
self.wm.borrow_mut().remove_window(id);
self.pending.borrow_mut().push(CoreEvent::WindowDestroyed(id));
}
/// Layer surfaces need a configure sent in direct response to their
/// first commit (sending it any earlier violates the protocol - see
/// `smithay::desktop::LayerMap::arrange`'s doc comment on why `arrange`
/// itself deliberately won't send one). Also the point at which an
/// `Exclusive`-interactivity layer (e.g. a lock screen, or a launcher
/// configured to grab all keyboard input) claims keyboard focus, since
/// its `keyboard_interactivity` isn't reliably known until the client's
/// state has actually committed.
pub(crate) fn ensure_layer_initial_configure(&mut self, surface: &WlSurface) {
// A layer surface lives in exactly one output's `LayerMap` (whichever
// one `new_layer_surface` mapped it into), so find that output rather
// than assuming a single global one.
let found = self.outputs().find_map(|output| {
let layer = layer_map_for_output(output).layer_for_surface(surface, WindowSurfaceType::TOPLEVEL).cloned();
layer.map(|l| (output.clone(), l))
});
let Some((output, layer)) = found else { return };
// Recompute geometry from whatever the client just committed
// (`set_size`/`set_anchor`/`set_margin`/`set_exclusive_zone` are all
// double-buffered, applied on this commit) *before* looking at
// `initial_configure_sent` - `map_layer`'s own `arrange()` call ran
// before the client had sent any of that, so without this, the
// first configure would carry stale, pre-request-processed geometry
// (verified live: wofi's `set_size(420, 550)` was otherwise ignored
// and it got stuck at the half-output fallback size instead). Every
// later commit needs the same treatment for live resizes/anchor
// changes; `arrange()` only actually sends a configure when
// something changed, so this is a no-op on a commit that didn't
// touch layer-shell state.
layer_map_for_output(&output).arrange();
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.map(|d| d.lock().unwrap().initial_configure_sent)
.unwrap_or(false)
});
if !initial_configure_sent {
layer.layer_surface().send_configure();
}
// Checked on every commit, not just the first: a client can flip
// `keyboard_interactivity` to `Exclusive` after already being
// mapped (and this is also, in practice, where a freshly-mapped
// `Exclusive` surface - e.g. wofi, which requests it from the very
// first commit - actually gets focus, since `set_keyboard_focus`
// is idempotent against a surface that's already focused).
if layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive {
self.set_keyboard_focus(Some(surface.clone()));
}
}
/// Sets keyboard focus *and* selection (clipboard/primary) focus to the
/// same surface's client. These have to move together: the data-device
/// protocols only ever offer the current selection to the client that
/// holds selection focus, and only accept `set_selection` from it, so a
/// window that has keyboard focus but not data-device focus can neither
/// paste nor copy.
pub(crate) fn set_keyboard_focus(&mut self, surface: Option<WlSurface>) {
// While the session is locked, only the lock surface may hold focus.
// This is the single chokepoint that enforces it: without the guard,
// any path that focuses a window - notably `new_managed_window`,
// i.e. *a client simply opening a window* - would hand keyboard
// focus to a normal client at a locked screen. (Caught by an A/B
// test that counted `wl_keyboard.enter` events delivered to a client
// launched while locked; it was 1 before this guard, 0 after.)
if self.lock.locked {
// With multiple outputs there is a lock surface per output, and
// any of them is a legitimate focus target.
let is_lock_surface = surface
.as_ref()
.is_some_and(|s| self.lock.surfaces.values().any(|lock| lock.wl_surface() == s));
if surface.is_some() && !is_lock_surface {
return;
}
}
let Some(keyboard) = self.seat.get_keyboard() else { return };
if keyboard.current_focus() == surface {
return;
}
let client = surface.as_ref().and_then(|s| self.dh.get_client(s.id()).ok());
set_data_device_focus(&self.dh.clone(), &self.seat.clone(), client.clone());
set_primary_focus(&self.dh.clone(), &self.seat.clone(), client);
let serial = SERIAL_COUNTER.next_serial();
keyboard.set_focus(self, surface, serial);
}
pub(crate) fn sync_geometry(&mut self, id: WindowId) {
let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { return };
if let Some(w) = self.id_to_window.get(&id) {
self.space.map_element(w.clone(), (geom.x, geom.y + TITLEBAR_HEIGHT as i32), false);
if let Some(top) = w.toplevel() {
top.with_pending_state(|state| {
state.size = Some((geom.width as i32, (geom.height - TITLEBAR_HEIGHT) as i32).into());
});
top.send_configure();
}
}
if self.decorations.contains_key(&id) {
self.redraw_decoration_buffer(id);
}
}
}
pub(crate) fn with_toplevel_title(toplevel: &ToplevelSurface) -> Option<String> {
smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<XdgToplevelSurfaceData>().map(|d| d.lock().unwrap().title.clone().unwrap_or_default())
})
}
|