srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-04-08 23:54:00 +0200
committersrdusr <[email protected]>2024-04-08 23:54:00 +0200
commit709333908d5b7d3157165d1811c8bc1795ab0028 (patch)
tree92d5616519f63c1e4743add0294ce552e239681c /crates
parent592bd3716c0aa5a47a5eb43f45f55844de9db382 (diff)
downloadsrdwm-709333908d5b7d3157165d1811c8bc1795ab0028.tar.gz
srdwm-709333908d5b7d3157165d1811c8bc1795ab0028.zip
Wayland: clipboard, session lock, screencopy, multi-monitor; modularize
Implements the protocols that were blocking srdwm-wayland from being a real session, plus multi-monitor, and splits the backend into modules. Everything here was verified by running it against real clients, not just compiled. Protocols - wlr-layer-shell + xdg-output: bars/launchers/notifications. xdg-output is not optional in practice - without it wofi segfaults rather than degrading, since it calls get_xdg_output without null-checking. - Clipboard: wl_data_device_manager, primary selection, and wlr-data-control. Data-control is what `wl-paste --watch cliphist store` needs, as it reads the selection without holding focus. Selection focus now follows keyboard focus, without which a focused window can neither copy nor paste. - ext-session-lock: `locked` gates rendering and input. No key is treated as a WM binding while locked - the config binds Mod4+Return to spawn a terminal, so honouring bindings at a locked screen would defeat the lock. The lock is confirmed only after a client-content-free frame has actually been presented, never at request time. - wlr-screencopy (hand-written; smithay ships no helper) for grim/slurp. Multi-monitor Every connected connector becomes a head with its own buffers, damage tracker and page-flip state, matched by CRTC so differing refresh rates don't gate each other. Outputs are reached through primary_output/output_at/ output_for_wl rather than a single field, which kept the change to ~11 call sites. Session lock creates one lock surface per output and waits for all of them, so a second monitor can't still show the desktop when the locker is told the session is safe. Modes are picked by the PREFERRED flag, not list order, and CRTCs are never double-assigned. Bugs found by testing, not review - Nothing gave a newly-created window Wayland focus: a freshly-opened app received no keystrokes and could not paste until clicked. - Opening a window at a locked screen stole keyboard focus. Caught by counting wl_keyboard.enter delivered to a client launched while locked: 1 before the guard, 0 after. A killed locker correctly leaves it locked. - Reading back the winit EGL window surface destroyed the GL context on the first screencopy capture, taking the compositor down. Root-caused by A/B-ing the same build with only the readback removed; capture now renders an offscreen pass. - Output mode was resent every frame at 60Hz, flooding any client bound to wl_output with duplicate mode/done events. - general.default_layout was defaulted and validated but never read, so setting it did nothing. srdwm is dynamic-first (Windows/macOS style, with drag-to-edge snapping); tiling is one opt-in layout, and that stays true. - .gitignore's unanchored `srdwm` matched any path component of that name, silently excluding the whole crates/srdwm source crate - the binary crate the workspace lists as a member, so a fresh clone could not build. Modularization lib.rs went from ~1260 lines to 78: state, protocols, input, lock, screencopy, winit and udev now each own one responsibility. lock is grouped by feature rather than kind on purpose, since its security invariant spans state, protocol handling and rendering at once. Multi-monitor was verified in the QEMU VM with a two-output virtio-gpu: both heads screendumped at their own resolution showing srdwm's clear colour, and a window forced to global x=1500 landed on head 1 at head-local x=220 while head 0 stayed empty. Known limitation, now documented: the nested winit backend stalls while its window is occluded, because the host stops scheduling frames and eglSwapBuffers blocks.
Diffstat (limited to 'crates')
-rw-r--r--crates/srdwm/Cargo.toml27
-rw-r--r--crates/srdwm/src/main.rs149
-rw-r--r--crates/wayland/Cargo.toml4
-rw-r--r--crates/wayland/src/input.rs277
-rw-r--r--crates/wayland/src/lib.rs702
-rw-r--r--crates/wayland/src/lock.rs171
-rw-r--r--crates/wayland/src/protocols.rs237
-rw-r--r--crates/wayland/src/screencopy.rs302
-rw-r--r--crates/wayland/src/state.rs356
-rw-r--r--crates/wayland/src/udev.rs508
-rw-r--r--crates/wayland/src/winit.rs412
-rw-r--r--crates/wayland/src/xwayland.rs2
12 files changed, 2312 insertions, 835 deletions
diff --git a/crates/srdwm/Cargo.toml b/crates/srdwm/Cargo.toml
new file mode 100644
index 0000000..d7af2ee
--- /dev/null
+++ b/crates/srdwm/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "srdwm"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+description = "SRDWM - a cross-platform, Lua-configured window manager"
+
+[[bin]]
+name = "srdwm"
+path = "src/main.rs"
+
+[dependencies]
+srdwm-core.workspace = true
+srdwm-platform.workspace = true
+srdwm-config.workspace = true
+log.workspace = true
+env_logger.workspace = true
+
+[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
+srdwm-x11.workspace = true
+srdwm-wayland.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+srdwm-windows.workspace = true
+
+[target.'cfg(target_os = "macos")'.dependencies]
+srdwm-macos.workspace = true
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
new file mode 100644
index 0000000..cc7755e
--- /dev/null
+++ b/crates/srdwm/src/main.rs
@@ -0,0 +1,149 @@
+use srdwm_config::Engine;
+use srdwm_core::{Event, WindowManager};
+use srdwm_platform::{Platform, PlatformKind};
+use std::cell::RefCell;
+use std::path::PathBuf;
+use std::rc::Rc;
+
+fn config_dir() -> PathBuf {
+ if let Ok(p) = std::env::var("SRDWM_CONFIG_PATH") {
+ return PathBuf::from(p);
+ }
+ if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
+ return PathBuf::from(xdg).join("srdwm/srd");
+ }
+ if let Ok(home) = std::env::var("HOME") {
+ return PathBuf::from(home).join(".config/srdwm/srd");
+ }
+ PathBuf::from("config/srd")
+}
+
+/// Applies `general.default_layout` to the workspaces that exist at
+/// startup.
+///
+/// srdwm is dynamic-first, not a tiling WM: the built-in default is
+/// `"dynamic"`, where windows keep whatever geometry they have and only
+/// *new* ones are positioned (by `SmartPlacement`, with Windows-style
+/// drag-to-edge snapping). Tiling is one opt-in layout among several.
+///
+/// Run *after* `load_init`, so a config that sets the key takes effect;
+/// `srd.layout.set()` is the separate runtime switch (the shipped config
+/// only calls it from key bindings), so this does not fight with it.
+fn apply_default_layout(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
+ let name = engine.get_string("general.default_layout", "dynamic");
+ let mut wm = wm.borrow_mut();
+ if !wm.available_layouts().iter().any(|l| *l == name) {
+ log::warn!("general.default_layout = '{name}' is not a registered layout; keeping built-in default");
+ return;
+ }
+ let ids: Vec<_> = wm.workspaces().iter().map(|w| w.id).collect();
+ for id in ids {
+ wm.set_layout(id, name.clone());
+ }
+ log::info!("default layout: {name}");
+}
+
+/// Applies the WindowManager's current layout decisions to the real
+/// platform: re-tiles the active workspace if needed, then pushes geometry
+/// and decoration state for every visible window.
+fn sync(wm: &Rc<RefCell<WindowManager>>, platform: &mut dyn Platform) {
+ let ws = wm.borrow().current_workspace();
+ wm.borrow_mut().arrange_workspace(ws);
+
+ let focused = wm.borrow().focused_id();
+ let snapshot: Vec<_> = wm.borrow().visible_windows().cloned().collect();
+ for w in snapshot {
+ if let Err(e) = platform.apply_geometry(w.id, w.geometry) {
+ log::warn!("apply_geometry({}) failed: {e}", w.id);
+ }
+ if let Err(e) = platform.redraw_decoration(w.id, &w, focused == Some(w.id)) {
+ log::warn!("redraw_decoration({}) failed: {e}", w.id);
+ }
+ }
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ env_logger::init();
+ log::info!("srdwm starting");
+
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ let dir = config_dir();
+ let engine = Engine::new(wm.clone(), &dir)?;
+ match engine.load_init() {
+ Ok(()) => log::info!("loaded config from {}", dir.display()),
+ Err(e) => log::warn!("no usable config at {} ({e}); running with built-in defaults", dir.display()),
+ }
+ apply_default_layout(&engine, &wm);
+ let running = engine.running_flag();
+
+ let kind = srdwm_platform::detect();
+ log::info!("selected platform backend: {}", kind.name());
+
+ let mut platform: Box<dyn Platform> = match kind {
+ #[cfg(all(unix, not(target_os = "macos")))]
+ PlatformKind::X11 => {
+ let mut p = srdwm_x11::X11Platform::connect(wm.clone())?;
+ let combos = engine.bound_keys();
+ p.grab_keybindings(&combos)?;
+ log::info!("grabbed {} keybinding(s)", combos.len());
+ Box::new(p)
+ }
+ #[cfg(all(unix, not(target_os = "macos")))]
+ PlatformKind::Wayland => {
+ let combos = engine.bound_keys();
+ log::info!("{} keybinding(s) will be intercepted from clients", combos.len());
+ srdwm_wayland::connect(wm.clone(), &combos)?
+ }
+ #[cfg(windows)]
+ PlatformKind::Windows => Box::new(srdwm_windows::WindowsPlatform::new()?),
+ #[cfg(target_os = "macos")]
+ PlatformKind::MacOS => Box::new(srdwm_macos::MacOsPlatform::new()?),
+ #[allow(unreachable_patterns)]
+ other => return Err(format!("platform backend '{}' is not available on this build", other.name()).into()),
+ };
+
+ let monitors = platform.monitors()?;
+ log::info!("detected {} monitor(s)", monitors.len());
+ wm.borrow_mut().set_monitors(monitors);
+
+ sync(&wm, platform.as_mut());
+
+ while running.get() {
+ let events = match platform.poll_events() {
+ Ok(events) => events,
+ Err(e) => {
+ log::error!("poll_events failed: {e}");
+ break;
+ }
+ };
+
+ let mut dirty = false;
+ for event in events {
+ match event {
+ Event::KeyPress { key_name, modifiers } => {
+ let combo = format!("{modifiers}{key_name}");
+ if !engine.dispatch_keybinding(&combo) {
+ log::debug!("no binding for '{combo}'");
+ }
+ dirty = true;
+ }
+ Event::WindowCreated(id) => {
+ log::info!("window {id} created");
+ dirty = true;
+ }
+ Event::WindowDestroyed(id) => {
+ log::info!("window {id} destroyed");
+ dirty = true;
+ }
+ Event::WindowMoved { .. } | Event::WindowResized { .. } => dirty = true,
+ _ => {}
+ }
+ }
+ if dirty {
+ sync(&wm, platform.as_mut());
+ }
+ }
+
+ log::info!("srdwm shutting down");
+ Ok(())
+}
diff --git a/crates/wayland/Cargo.toml b/crates/wayland/Cargo.toml
index 4602608..fb86dd8 100644
--- a/crates/wayland/Cargo.toml
+++ b/crates/wayland/Cargo.toml
@@ -11,6 +11,10 @@ srdwm-platform.workspace = true
log.workspace = true
thiserror.workspace = true
fontdue = "0.9"
+# smithay 0.7 ships no wlr-screencopy helper, so `src/screencopy.rs`
+# implements that protocol directly against these server bindings. Pinned to
+# the same version smithay itself depends on, so both see one set of types.
+wayland-protocols-wlr = { version = "0.3.12", features = ["server"] }
[dependencies.smithay]
version = "0.7"
diff --git a/crates/wayland/src/input.rs b/crates/wayland/src/input.rs
new file mode 100644
index 0000000..76607ee
--- /dev/null
+++ b/crates/wayland/src/input.rs
@@ -0,0 +1,277 @@
+//! Input routing: keyboard, pointer, and what "focus" means.
+//!
+//! Shared by both backends - smithay delivers keyboard/pointer events
+//! through generic `InputBackend` traits, so the precise keybinding matching
+//! and titlebar hit-testing exist once here and are called from the winit
+//! backend ([`crate::winit`]) and the libinput/udev one ([`crate::udev`])
+//! alike.
+//!
+//! Every function that routes an event checks the session lock first: while
+//! locked, input goes to the lock surface and nowhere else. See
+//! [`crate::lock`].
+
+use smithay::backend::input::{ButtonState as BackendButtonState, KeyState as BackendKeyState, KeyboardKeyEvent};
+use smithay::desktop::{layer_map_for_output, Window as DWindow, WindowSurfaceType};
+use smithay::input::keyboard::FilterResult;
+use smithay::input::pointer::{ButtonEvent, MotionEvent};
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::utils::{Logical, Point, SERIAL_COUNTER};
+use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
+
+use srdwm_core::{Event as CoreEvent, Modifiers, TitlebarHit, WindowId};
+
+use crate::state::CompState;
+
+pub(crate) fn last_pointer_pos(state: &CompState) -> Point<f64, Logical> {
+ state.seat.get_pointer().map(|p| p.current_location()).unwrap_or_default()
+}
+
+/// Topmost layer-shell surface (if any) under `pos`, checked in the same
+/// above-everything-else stacking order `space_render_elements` renders
+/// `Overlay`/`Top` layers in (bars, launchers, notifications, lock UIs).
+/// `Background`/`Bottom` layers (wallpapers) deliberately aren't checked
+/// here: nothing in scope for the daily-driver gate needs pointer input
+/// routed to them, and space windows should stay clickable over a
+/// wallpaper.
+/// `pos` is in the global space; layer geometry is relative to its own
+/// output, so the pointer is translated into output-local coordinates
+/// before hit-testing and the result translated back out.
+pub(crate) fn layer_surface_under(state: &CompState, pos: Point<f64, Logical>) -> Option<(WlSurface, Point<i32, Logical>)> {
+ let entry = state.output_at(pos)?;
+ let origin = entry.location;
+ let local = pos - origin.to_f64();
+ let map = layer_map_for_output(&entry.output);
+ for layer_kind in [Layer::Overlay, Layer::Top] {
+ let Some(layer) = map.layer_under(layer_kind, local) else { continue };
+ let Some(geo) = map.layer_geometry(layer) else { continue };
+ if let Some((surface, surface_loc)) = layer.surface_under(local - geo.loc.to_f64(), WindowSurfaceType::ALL) {
+ return Some((surface, origin + geo.loc + surface_loc));
+ }
+ }
+ None
+}
+
+pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Logical>, time: u32) {
+ // Locked: pointer motion goes to the lock surface only. No hit-testing
+ // against windows/decorations, so no hover, no drag, no resize.
+ if state.lock.locked {
+ let surface = state.any_lock_surface().cloned();
+ if let Some(pointer) = state.seat.get_pointer() {
+ let focus = surface.map(|s| (s, Point::from((0, 0)).to_f64()));
+ pointer.motion(state, focus, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time });
+ }
+ return;
+ }
+
+ let layer_hit = layer_surface_under(state, pos);
+ let hit = state.wm.borrow().hit_test(pos.x as i32, pos.y as i32);
+ let under = state.space.element_under(pos).map(|(w, loc)| (w.clone(), loc));
+
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ if let Some((surface, loc)) = layer_hit {
+ let surface_loc = pos - loc.to_f64();
+ pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time });
+ } else if hit.is_some() {
+ // Over our own decoration - no client focus.
+ pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time });
+ } else if let Some((window, loc)) = under {
+ if let Some(surface) = window.toplevel().map(|t| t.wl_surface().clone()) {
+ let surface_loc = pos - loc.to_f64();
+ pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time });
+ }
+ } else {
+ pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time });
+ }
+
+ let mut wm = state.wm.borrow_mut();
+ let dragging_or_resizing = wm.is_dragging() || wm.is_resizing();
+ if wm.is_dragging() {
+ wm.update_drag(pos.x as i32, pos.y as i32);
+ } else if wm.is_resizing() {
+ wm.update_resize(pos.x as i32, pos.y as i32);
+ }
+ let focused = wm.focused_id();
+ drop(wm);
+ if dragging_or_resizing {
+ if let Some(id) = focused {
+ state.sync_geometry(id);
+ }
+ }
+}
+
+/// The underlying `wl_surface` for a mapped window, regardless of whether
+/// it's a native `xdg-shell` toplevel or an XWayland `X11Surface` --
+/// `desktop::Window` exposes these as two separate accessors with no
+/// shared one.
+pub(crate) fn dwindow_wl_surface(w: &DWindow) -> Option<WlSurface> {
+ if let Some(top) = w.toplevel() {
+ return Some(top.wl_surface().clone());
+ }
+ w.x11_surface().and_then(|x| x.wl_surface())
+}
+
+/// Requests a client close its window, whichever kind it is.
+pub(crate) fn close_dwindow(w: &DWindow) {
+ if let Some(top) = w.toplevel() {
+ top.send_close();
+ } else if let Some(x11) = w.x11_surface() {
+ let _ = x11.close();
+ }
+}
+
+/// Focuses `id` in our own `WindowManager` *and* gives its surface real
+/// Wayland/X11 keyboard focus - without this, a window can be raised and
+/// tiled correctly yet never receive a single keystroke.
+pub(crate) fn focus_window(state: &mut CompState, id: WindowId) {
+ state.wm.borrow_mut().focus_window(id);
+ state.pending.borrow_mut().push(CoreEvent::WindowFocused(id));
+ let surface = state.id_to_window.get(&id).and_then(dwindow_wl_surface);
+ // Routed through `set_keyboard_focus` (rather than calling
+ // `KeyboardHandle::set_focus` directly) so clipboard/primary-selection
+ // focus follows window focus too - see that method's doc comment.
+ state.set_keyboard_focus(surface);
+}
+
+pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logical>, button: u32, pressed: bool, time: u32) {
+ const BTN_LEFT: u32 = 0x110;
+ let serial = SERIAL_COUNTER.next_serial();
+
+ // Locked: forward the click to the lock surface (it may have a button or
+ // a text field) but never let it focus, raise, drag, or close a window.
+ if state.lock.locked {
+ if let Some(pointer) = state.seat.get_pointer() {
+ let button_state = if pressed { BackendButtonState::Pressed } else { BackendButtonState::Released };
+ pointer.button(state, &ButtonEvent { serial, time, button, state: button_state });
+ }
+ return;
+ }
+
+ if pressed && button == BTN_LEFT {
+ let layer_hit = layer_surface_under(state, pos);
+ if let Some((surface, _)) = &layer_hit {
+ // Look the surface up on whichever output actually holds it.
+ let on_demand = state
+ .outputs()
+ .find_map(|output| {
+ layer_map_for_output(output)
+ .layer_for_surface(surface, WindowSurfaceType::ALL)
+ .map(|l| {
+ l.can_receive_keyboard_focus()
+ && l.cached_state().keyboard_interactivity != KeyboardInteractivity::Exclusive
+ })
+ })
+ .unwrap_or(false);
+ // `Exclusive` layers (lock screens, exclusive launchers) already
+ // hold focus from `ensure_layer_initial_configure` and keep it
+ // regardless of where else is clicked; only `OnDemand` layers
+ // (e.g. a bar's search field) claim it on click.
+ if on_demand {
+ state.set_keyboard_focus(Some(surface.clone()));
+ }
+ }
+ let hit = if layer_hit.is_some() { None } else { state.wm.borrow().hit_test(pos.x as i32, pos.y as i32) };
+ if let Some((id, hit)) = hit {
+ focus_window(state, id);
+ match hit {
+ TitlebarHit::Drag => state.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32),
+ TitlebarHit::Close => {
+ if let Some(w) = state.id_to_window.get(&id) {
+ close_dwindow(w);
+ }
+ }
+ TitlebarHit::Maximize => {
+ state.wm.borrow_mut().toggle_maximize(id);
+ state.sync_geometry(id);
+ }
+ TitlebarHit::Minimize => state.wm.borrow_mut().minimize_window(id),
+ TitlebarHit::Resize(edge) => state.wm.borrow_mut().start_resize(id, edge, pos.x as i32, pos.y as i32),
+ }
+ } else if layer_hit.is_none() {
+ if let Some((window, _loc)) = state.space.element_under(pos) {
+ let window = window.clone();
+ state.space.raise_element(&window, true);
+ if let Some(&id) = dwindow_wl_surface(&window).and_then(|s| state.surface_to_id.get(&s)) {
+ focus_window(state, id);
+ }
+ }
+ }
+ } else if !pressed {
+ let mut wm = state.wm.borrow_mut();
+ if wm.is_dragging() {
+ wm.end_drag();
+ } else if wm.is_resizing() {
+ wm.end_resize();
+ }
+ }
+
+ if let Some(pointer) = state.seat.get_pointer() {
+ let button_state = if pressed { BackendButtonState::Pressed } else { BackendButtonState::Released };
+ pointer.button(state, &ButtonEvent { serial, time, button, state: button_state });
+ }
+}
+
+/// Shared between the winit (nested) and udev (bare-TTY) backends: both
+/// deliver keyboard events through smithay's generic `KeyboardKeyEvent`
+/// trait, so the precise-keybinding-matching logic (see the module docs)
+/// only needs to exist once.
+pub(crate) fn handle_keyboard_key_event<B: smithay::backend::input::InputBackend, E: KeyboardKeyEvent<B>>(state: &mut CompState, event: &E) {
+ let keycode = event.key_code();
+ let key_state = event.state();
+ let time = event.time_msec();
+ let serial = SERIAL_COUNTER.next_serial();
+ let Some(keyboard) = state.seat.get_keyboard() else { return };
+
+ // While the session is locked, every key goes to the lock surface and
+ // *nothing* is treated as a WM keybinding. Skipping this would leave the
+ // lock trivially bypassable - the config binds spawn commands
+ // (`Mod4+Return` opens a terminal), so honouring bindings here would let
+ // anyone at a locked screen run arbitrary programs.
+ if state.lock.locked {
+ keyboard.input::<(), _>(state, keycode, key_state, serial, time, |_, _, _| FilterResult::Forward);
+ return;
+ }
+
+ let bound_keys = state.bound_keys.clone();
+ let matched: Option<(String, Modifiers)> =
+ keyboard.input(state, keycode, key_state, serial, time, move |_, mods, handle| {
+ let modifiers = core_modifiers_from_xkb(mods);
+ match keysym_name_for(handle) {
+ Some(name) if bound_keys.contains(&srdwm_core::key_combo_string(modifiers, &name)) => {
+ FilterResult::Intercept((name, modifiers))
+ }
+ _ => FilterResult::Forward,
+ }
+ });
+
+ if key_state == BackendKeyState::Pressed {
+ if let Some((key_name, modifiers)) = matched {
+ state.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers });
+ }
+ }
+ // Unmatched keys were already forwarded to the focused client by
+ // `FilterResult::Forward` inside the closure above.
+}
+
+/// Translates the effective xkb keysym for this keypress into the same
+/// `"Return"`/`"a"`/`"F5"`-style name `srdwm_core::keysyms` uses, so a
+/// binding written once in Lua resolves identically on X11 and Wayland.
+pub(crate) fn keysym_name_for(handle: smithay::input::keyboard::KeysymHandle<'_>) -> Option<String> {
+ srdwm_core::keysyms::keysym_to_name(handle.modified_sym().raw())
+}
+
+pub(crate) fn core_modifiers_from_xkb(mods: &smithay::input::keyboard::ModifiersState) -> Modifiers {
+ let mut m = Modifiers::empty();
+ if mods.shift {
+ m |= Modifiers::SHIFT;
+ }
+ if mods.ctrl {
+ m |= Modifiers::CTRL;
+ }
+ if mods.alt {
+ m |= Modifiers::ALT;
+ }
+ if mods.logo {
+ m |= Modifiers::SUPER;
+ }
+ m
+}
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs
index fa56517..053e6ad 100644
--- a/crates/wayland/src/lib.rs
+++ b/crates/wayland/src/lib.rs
@@ -37,707 +37,29 @@
//! well-behaved clients don't also draw their own client-side titlebar.
mod decoration;
+mod input;
+mod lock;
+mod protocols;
+mod screencopy;
+mod state;
mod udev;
+mod winit;
mod xwayland;
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::input::{
- AbsolutePositionEvent, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent,
- KeyState as BackendKeyState, KeyboardKeyEvent, PointerButtonEvent,
-};
-use smithay::backend::renderer::damage::OutputDamageTracker;
-use smithay::backend::renderer::element::memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement};
-use smithay::backend::renderer::element::Kind;
-use smithay::backend::renderer::gles::GlesRenderer;
-use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
-use smithay::desktop::space::render_output;
-use smithay::desktop::{Space, Window as DWindow};
-use smithay::input::keyboard::FilterResult;
-use smithay::input::pointer::{ButtonEvent, CursorImageStatus, MotionEvent};
-use smithay::input::{Seat, SeatHandler, SeatState};
-use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
-use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
-use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
-use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
-use smithay::reexports::wayland_server::protocol::wl_seat;
-use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
-use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
-use smithay::reexports::winit::platform::pump_events::PumpStatus;
-use smithay::utils::{Logical, Point, Serial, Transform, SERIAL_COUNTER};
-use smithay::wayland::buffer::BufferHandler;
-use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
-use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
-use smithay::wayland::shell::xdg::{
- PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData,
-};
-use smithay::wayland::shm::{ShmHandler, ShmState};
-use smithay::{delegate_compositor, delegate_output, delegate_seat, delegate_shm, delegate_xdg_decoration, delegate_xdg_shell};
+use srdwm_core::WindowManager;
+use srdwm_platform::{Platform, PlatformError, Result as PlatformResult};
-use srdwm_core::{Event as CoreEvent, Modifiers, TitlebarHit, Window as CoreWindow, WindowId, WindowManager, TITLEBAR_HEIGHT};
-use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
+pub use winit::WaylandPlatform;
-fn err(e: impl std::fmt::Display) -> PlatformError {
+/// Shared error shim: every backend turns foreign errors into
+/// `PlatformError::Other` the same way.
+pub(crate) fn err(e: impl std::fmt::Display) -> PlatformError {
PlatformError::Other(e.to_string())
}
-#[derive(Default)]
-struct ClientState {
- compositor_state: CompositorClientState,
-}
-impl ClientData for ClientState {
- fn initialized(&self, _client_id: ClientId) {}
- fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
-}
-
-/// 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.
-struct CompState {
- compositor_state: CompositorState,
- xdg_shell_state: XdgShellState,
- _xdg_decoration_state: XdgDecorationState,
- shm_state: ShmState,
- seat_state: SeatState<CompState>,
- seat: Seat<CompState>,
- space: Space<DWindow>,
- wm: Rc<RefCell<WindowManager>>,
- surface_to_id: HashMap<WlSurface, WindowId>,
- id_to_window: HashMap<WindowId, DWindow>,
- decorations: HashMap<WindowId, MemoryRenderBuffer>,
- pending: Rc<RefCell<Vec<CoreEvent>>>,
- bound_keys: Rc<HashSet<String>>,
- 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.
- udev: Option<udev::UdevOutput>,
- /// XWayland support; see `xwayland.rs` module docs. `xwm` is `None`
- /// until `XWaylandEvent::Ready` fires.
- xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState,
- xwm: Option<smithay::xwayland::X11Wm>,
- xwayland_windows: HashMap<xwayland::X11Window, WindowId>,
- /// Mapped X11 windows still waiting for XWayland to associate a
- /// `wl_surface` - see `xwayland.rs` and `commit()` above.
- 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);
-
-impl CompState {
- 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, id);
- self.id_to_window.insert(id, dwindow);
- self.redraw_decoration_buffer(id);
- 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).
- 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);
- }
-
- 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));
- }
-
- 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);
- }
- }
-}
-
-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())
- })
-}
-
-impl CompositorHandler for CompState {
- fn compositor_state(&mut self) -> &mut CompositorState {
- &mut self.compositor_state
- }
-
- fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
- // Two possible client kinds now: our own `ClientState` for regular
- // Wayland clients, or smithay's `XWaylandClientData` for the single
- // XWayland client (see `xwayland.rs`) - both carry a
- // `CompositorClientState`, just under different wrapper types.
- if let Some(state) = client.get_data::<ClientState>() {
- return &state.compositor_state;
- }
- &client.get_data::<smithay::xwayland::XWaylandClientData>().expect("client is neither ours nor XWayland's").compositor_state
- }
-
- fn commit(&mut self, surface: &WlSurface) {
- smithay::backend::renderer::utils::on_commit_buffer_handler::<CompState>(surface);
- // XWayland's association of an X11 window with this wl_surface can
- // arrive at any point relative to the map request (see
- // `xwayland.rs`'s module docs); `surface_associated` handles the
- // common ordering, this retries the surfaces still waiting on a
- // commit to actually make that association queryable.
- self.retry_pending_x11_windows();
- if let Some(&id) = self.surface_to_id.get(surface) {
- if let Some(w) = self.id_to_window.get(&id) {
- w.on_commit();
- }
- }
- }
-}
-
-impl XdgShellHandler for CompState {
- fn xdg_shell_state(&mut self) -> &mut XdgShellState {
- &mut self.xdg_shell_state
- }
-
- fn new_toplevel(&mut self, surface: ToplevelSurface) {
- self.new_managed_window(surface);
- }
-
- fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {}
-
- fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
-
- fn reposition_request(&mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32) {}
-
- fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
- self.remove_window(surface.wl_surface());
- }
-}
-
-impl XdgDecorationHandler for CompState {
- fn new_decoration(&mut self, toplevel: ToplevelSurface) {
- toplevel.with_pending_state(|state| {
- state.decoration_mode = Some(DecorationMode::ServerSide);
- });
- }
-
- fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
- toplevel.with_pending_state(|state| {
- state.decoration_mode = Some(DecorationMode::ServerSide);
- });
- toplevel.send_configure();
- }
-
- fn unset_mode(&mut self, _toplevel: ToplevelSurface) {}
-}
-
-impl ShmHandler for CompState {
- fn shm_state(&self) -> &ShmState {
- &self.shm_state
- }
-}
-
-impl BufferHandler for CompState {
- fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
-}
-
-impl smithay::wayland::output::OutputHandler for CompState {}
-
-impl SeatHandler for CompState {
- type KeyboardFocus = WlSurface;
- type PointerFocus = WlSurface;
- type TouchFocus = WlSurface;
-
- fn seat_state(&mut self) -> &mut SeatState<Self> {
- &mut self.seat_state
- }
-
- fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
- fn cursor_image(&mut self, _seat: &Seat<Self>, _image: CursorImageStatus) {}
-}
-
-delegate_compositor!(CompState);
-delegate_xdg_shell!(CompState);
-delegate_xdg_decoration!(CompState);
-delegate_shm!(CompState);
-delegate_seat!(CompState);
-delegate_output!(CompState);
-// TODO: delegate_layer_shell! (wlr-layer-shell-unstable-v1) - bars,
-// launchers, notification daemons, and lock-screen UIs are layer
-// surfaces, not xdg_toplevels; none of them can run under srdwm without
-// this. See docs/IMPLEMENTATION_STATUS.md's "Not implemented anywhere
-// yet" section.
-// TODO: delegate_data_device! (wl_data_device_manager) - no clipboard/
-// drag-and-drop between Wayland clients without it.
-// TODO: session-lock (ext-session-lock-v1) - no way for a lock-screen
-// client to actually lock input/display without it.
-
-pub struct WaylandPlatform {
- display: Display<CompState>,
- state: CompState,
- backend: WinitGraphicsBackend<GlesRenderer>,
- winit_events: WinitEventLoop,
- damage_tracker: OutputDamageTracker,
- output: Output,
- listener: ListeningSocket,
- clients: Vec<Client>,
- pending: Rc<RefCell<Vec<CoreEvent>>>,
- wm: Rc<RefCell<WindowManager>>,
-}
-
-impl WaylandPlatform {
- /// `bound_keys` are the config's `"Mod4+Shift+Return"`-style combo
- /// strings (see `srdwm_core::key_combo_string`) - the same set the X11
- /// backend grabs individually via `XGrabKey`. Only a keypress matching
- /// one of these is withheld from the focused client.
- pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Self> {
- let display: Display<CompState> = Display::new().map_err(err)?;
- let dh = display.handle();
-
- let (backend, winit_events) = winit::init::<GlesRenderer>().map_err(err)?;
- let size = backend.window_size();
-
- let output = Output::new(
- "srdwm-wayland".to_string(),
- PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "winit".into() },
- );
- output.change_current_state(
- Some(OutputMode { size, refresh: 60_000 }),
- Some(Transform::Normal),
- None,
- Some((0, 0).into()),
- );
- output.create_global::<CompState>(&dh);
-
- let compositor_state = CompositorState::new::<CompState>(&dh);
- let xdg_shell_state = XdgShellState::new::<CompState>(&dh);
- let xdg_decoration_state = XdgDecorationState::new::<CompState>(&dh);
- let shm_state = ShmState::new::<CompState>(&dh, Vec::new());
- let mut seat_state = SeatState::new();
- let mut seat = seat_state.new_wl_seat(&dh, "seat0");
- seat.add_keyboard(Default::default(), 200, 25).map_err(err)?;
- seat.add_pointer();
-
- let mut space = Space::default();
- space.map_output(&output, (0, 0));
-
- let pending = Rc::new(RefCell::new(Vec::new()));
- let state = CompState {
- compositor_state,
- xdg_shell_state,
- _xdg_decoration_state: xdg_decoration_state,
- shm_state,
- seat_state,
- seat,
- space,
- wm: wm.clone(),
- surface_to_id: HashMap::new(),
- id_to_window: HashMap::new(),
- decorations: HashMap::new(),
- pending: pending.clone(),
- bound_keys: Rc::new(bound_keys.iter().cloned().collect()),
- start_time: Instant::now(),
- udev: None,
- xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&dh),
- xwm: None,
- xwayland_windows: HashMap::new(),
- xwayland_pending: Vec::new(),
- };
-
- let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
- if let Some(name) = listener.socket_name() {
- std::env::set_var("WAYLAND_DISPLAY", name);
- log::info!("wayland socket: {}", name.to_string_lossy());
- }
-
- let damage_tracker = OutputDamageTracker::from_output(&output);
-
- Ok(Self { display, state, backend, winit_events, damage_tracker, output, listener, clients: Vec::new(), pending, wm })
- }
-
- fn accept_clients(&mut self) -> PlatformResult<()> {
- if let Some(stream) = self.listener.accept().map_err(err)? {
- let client = self.display.handle().insert_client(stream, std::sync::Arc::new(ClientState::default())).map_err(err)?;
- self.clients.push(client);
- }
- Ok(())
- }
-
- fn pump_winit(&mut self) -> PlatformResult<bool> {
- let mut closed = false;
- let state = &mut self.state;
- let output = &self.output;
- let pump = self.winit_events.dispatch_new_events(|event| {
- handle_winit_event(state, output, event, &mut closed);
- });
- if matches!(pump, PumpStatus::Exit(_)) {
- closed = true;
- }
- Ok(closed)
- }
-
- fn render_frame(&mut self) -> PlatformResult<()> {
- let size = self.backend.window_size();
- self.output.change_current_state(Some(OutputMode { size, refresh: 60_000 }), None, None, None);
-
- let age = self.backend.buffer_age().unwrap_or(0);
- let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?;
-
- let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new();
- for (&id, deco) in self.state.decorations.iter() {
- let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
- match MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(elem),
- Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
- }
- }
-
- render_output(
- &self.output,
- renderer,
- &mut framebuffer,
- 1.0,
- age,
- [&self.state.space],
- &custom_elements,
- &mut self.damage_tracker,
- [0.05, 0.05, 0.08, 1.0],
- )
- .map_err(err)?;
- drop(framebuffer);
- self.backend.submit(None).map_err(err)?;
- self.state.space.elements().for_each(|w| w.send_frame(&self.output, self.state.start_time.elapsed(), None, |_, _| Some(self.output.clone())));
- Ok(())
- }
-}
-
-fn handle_winit_event(state: &mut CompState, output: &Output, event: WinitEvent, closed: &mut bool) {
- match event {
- WinitEvent::CloseRequested => *closed = true,
- WinitEvent::Input(InputEvent::Keyboard { event }) => handle_keyboard_key_event(state, &event),
- WinitEvent::Input(InputEvent::PointerMotionAbsolute { event }) => {
- let size = output.current_mode().map(|m| m.size).unwrap_or_default().to_logical(1);
- let pos = event.position_transformed(size);
- handle_pointer_position(state, pos, event.time_msec());
- }
- WinitEvent::Input(InputEvent::PointerButton { event }) => {
- let pos = last_pointer_pos(state);
- let button = event.button_code();
- let pressed = event.state() == BackendButtonState::Pressed;
- handle_pointer_button(state, pos, button, pressed, event.time_msec());
- }
- WinitEvent::Resized { .. } => {}
- _ => {}
- }
-}
-
-/// Shared between the winit (nested) and udev (bare-TTY) backends: both
-/// deliver keyboard events through smithay's generic `KeyboardKeyEvent`
-/// trait, so the precise-keybinding-matching logic (see the module docs)
-/// only needs to exist once.
-fn handle_keyboard_key_event<B: smithay::backend::input::InputBackend, E: KeyboardKeyEvent<B>>(state: &mut CompState, event: &E) {
- let keycode = event.key_code();
- let key_state = event.state();
- let time = event.time_msec();
- let serial = SERIAL_COUNTER.next_serial();
- let Some(keyboard) = state.seat.get_keyboard() else { return };
-
- let bound_keys = state.bound_keys.clone();
- let matched: Option<(String, Modifiers)> =
- keyboard.input(state, keycode, key_state, serial, time, move |_, mods, handle| {
- let modifiers = core_modifiers_from_xkb(mods);
- match keysym_name_for(handle) {
- Some(name) if bound_keys.contains(&srdwm_core::key_combo_string(modifiers, &name)) => {
- FilterResult::Intercept((name, modifiers))
- }
- _ => FilterResult::Forward,
- }
- });
-
- if key_state == BackendKeyState::Pressed {
- if let Some((key_name, modifiers)) = matched {
- state.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers });
- }
- }
- // Unmatched keys were already forwarded to the focused client by
- // `FilterResult::Forward` inside the closure above.
-}
-
-/// Translates the effective xkb keysym for this keypress into the same
-/// `"Return"`/`"a"`/`"F5"`-style name `srdwm_core::keysyms` uses, so a
-/// binding written once in Lua resolves identically on X11 and Wayland.
-fn keysym_name_for(handle: smithay::input::keyboard::KeysymHandle<'_>) -> Option<String> {
- srdwm_core::keysyms::keysym_to_name(handle.modified_sym().raw())
-}
-
-fn core_modifiers_from_xkb(mods: &smithay::input::keyboard::ModifiersState) -> Modifiers {
- let mut m = Modifiers::empty();
- if mods.shift {
- m |= Modifiers::SHIFT;
- }
- if mods.ctrl {
- m |= Modifiers::CTRL;
- }
- if mods.alt {
- m |= Modifiers::ALT;
- }
- if mods.logo {
- m |= Modifiers::SUPER;
- }
- m
-}
-
-fn last_pointer_pos(state: &CompState) -> Point<f64, Logical> {
- state.seat.get_pointer().map(|p| p.current_location()).unwrap_or_default()
-}
-
-fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Logical>, time: u32) {
- let hit = state.wm.borrow().hit_test(pos.x as i32, pos.y as i32);
- let under = state.space.element_under(pos).map(|(w, loc)| (w.clone(), loc));
-
- let Some(pointer) = state.seat.get_pointer() else { return };
- if hit.is_some() {
- // Over our own decoration - no client focus.
- pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time });
- } else if let Some((window, loc)) = under {
- if let Some(surface) = window.toplevel().map(|t| t.wl_surface().clone()) {
- let surface_loc = pos - loc.to_f64();
- pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time });
- }
- } else {
- pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time });
- }
-
- let mut wm = state.wm.borrow_mut();
- let dragging_or_resizing = wm.is_dragging() || wm.is_resizing();
- if wm.is_dragging() {
- wm.update_drag(pos.x as i32, pos.y as i32);
- } else if wm.is_resizing() {
- wm.update_resize(pos.x as i32, pos.y as i32);
- }
- let focused = wm.focused_id();
- drop(wm);
- if dragging_or_resizing {
- if let Some(id) = focused {
- state.sync_geometry(id);
- }
- }
-}
-
-/// The underlying `wl_surface` for a mapped window, regardless of whether
-/// it's a native `xdg-shell` toplevel or an XWayland `X11Surface` --
-/// `desktop::Window` exposes these as two separate accessors with no
-/// shared one.
-fn dwindow_wl_surface(w: &DWindow) -> Option<WlSurface> {
- if let Some(top) = w.toplevel() {
- return Some(top.wl_surface().clone());
- }
- w.x11_surface().and_then(|x| x.wl_surface())
-}
-
-/// Requests a client close its window, whichever kind it is.
-fn close_dwindow(w: &DWindow) {
- if let Some(top) = w.toplevel() {
- top.send_close();
- } else if let Some(x11) = w.x11_surface() {
- let _ = x11.close();
- }
-}
-
-/// Focuses `id` in our own `WindowManager` *and* gives its surface real
-/// Wayland/X11 keyboard focus - without this, a window can be raised and
-/// tiled correctly yet never receive a single keystroke.
-fn focus_window(state: &mut CompState, id: WindowId) {
- state.wm.borrow_mut().focus_window(id);
- state.pending.borrow_mut().push(CoreEvent::WindowFocused(id));
- let surface = state.id_to_window.get(&id).and_then(dwindow_wl_surface);
- if let Some(keyboard) = state.seat.get_keyboard() {
- let serial = SERIAL_COUNTER.next_serial();
- keyboard.set_focus(state, surface, serial);
- }
-}
-
-fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logical>, button: u32, pressed: bool, time: u32) {
- const BTN_LEFT: u32 = 0x110;
- let serial = SERIAL_COUNTER.next_serial();
-
- if pressed && button == BTN_LEFT {
- let hit = state.wm.borrow().hit_test(pos.x as i32, pos.y as i32);
- if let Some((id, hit)) = hit {
- focus_window(state, id);
- match hit {
- TitlebarHit::Drag => state.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32),
- TitlebarHit::Close => {
- if let Some(w) = state.id_to_window.get(&id) {
- close_dwindow(w);
- }
- }
- TitlebarHit::Maximize => {
- state.wm.borrow_mut().toggle_maximize(id);
- state.sync_geometry(id);
- }
- TitlebarHit::Minimize => state.wm.borrow_mut().minimize_window(id),
- TitlebarHit::Resize(edge) => state.wm.borrow_mut().start_resize(id, edge, pos.x as i32, pos.y as i32),
- }
- } else if let Some((window, _loc)) = state.space.element_under(pos) {
- let window = window.clone();
- state.space.raise_element(&window, true);
- if let Some(&id) = dwindow_wl_surface(&window).and_then(|s| state.surface_to_id.get(&s)) {
- focus_window(state, id);
- }
- }
- } else if !pressed {
- let mut wm = state.wm.borrow_mut();
- if wm.is_dragging() {
- wm.end_drag();
- } else if wm.is_resizing() {
- wm.end_resize();
- }
- }
-
- if let Some(pointer) = state.seat.get_pointer() {
- let button_state = if pressed { BackendButtonState::Pressed } else { BackendButtonState::Released };
- pointer.button(state, &ButtonEvent { serial, time, button, state: button_state });
- }
-}
-
-impl Platform for WaylandPlatform {
- fn kind(&self) -> PlatformKind {
- PlatformKind::Wayland
- }
-
- fn poll_events(&mut self) -> PlatformResult<Vec<CoreEvent>> {
- self.accept_clients()?;
- let closed = self.pump_winit()?;
- if closed {
- return Err(PlatformError::Other("compositor window closed".into()));
- }
- self.display.dispatch_clients(&mut self.state).map_err(err)?;
- self.display.flush_clients().map_err(err)?;
- self.render_frame()?;
- Ok(self.pending.borrow_mut().drain(..).collect())
- }
-
- fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
- let size = self.backend.window_size();
- Ok(vec![{
- let mut m = srdwm_core::Monitor::new(0, "winit", srdwm_core::Rect::new(0, 0, size.w as u32, size.h as u32));
- m.primary = true;
- m
- }])
- }
-
- fn apply_geometry(&mut self, window: WindowId, geometry: srdwm_core::Rect) -> PlatformResult<()> {
- let _ = geometry;
- self.state.sync_geometry(window);
- Ok(())
- }
-
- fn set_title(&mut self, _window: WindowId, _title: &str) -> PlatformResult<()> {
- Ok(())
- }
-
- fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
- self.state.wm.borrow_mut().focus_window(window);
- Ok(())
- }
-
- fn minimize(&mut self, window: WindowId) -> PlatformResult<()> {
- if let Some(w) = self.state.id_to_window.get(&window) {
- self.state.space.unmap_elem(w);
- }
- Ok(())
- }
-
- fn restore(&mut self, window: WindowId) -> PlatformResult<()> {
- self.state.sync_geometry(window);
- Ok(())
- }
-
- fn close(&mut self, window: WindowId) -> PlatformResult<()> {
- if let Some(w) = self.state.id_to_window.get(&window).and_then(|w| w.toplevel()) {
- w.send_close();
- }
- Ok(())
- }
-
- fn set_decorated(&mut self, _window: WindowId, _decorated: bool) -> PlatformResult<()> {
- Ok(())
- }
-
- fn set_border_color(&mut self, _window: WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> {
- Ok(())
- }
-
- fn set_border_width(&mut self, _window: WindowId, _width: u32) -> PlatformResult<()> {
- Ok(())
- }
-
- fn redraw_decoration(&mut self, window: WindowId, _win: &CoreWindow, _focused: bool) -> PlatformResult<()> {
- // Re-renders the title/focus-color band and re-syncs geometry;
- // `sync_geometry` re-renders the decoration too, but only if one
- // already exists, so this also covers first paint.
- self.state.redraw_decoration_buffer(window);
- self.state.sync_geometry(window);
- Ok(())
- }
-
- fn grab_keyboard(&mut self) -> PlatformResult<()> {
- Ok(())
- }
-
- fn ungrab_keyboard(&mut self) -> PlatformResult<()> {
- Ok(())
- }
-}
-
/// Connects to Wayland, choosing between the udev/DRM backend (bare TTY, no
/// host compositor to nest under - see `udev.rs`) and this module's winit
/// backend (nested window), the same way real compositors decide
diff --git a/crates/wayland/src/lock.rs b/crates/wayland/src/lock.rs
new file mode 100644
index 0000000..51b6158
--- /dev/null
+++ b/crates/wayland/src/lock.rs
@@ -0,0 +1,171 @@
+//! Session lock (`ext-session-lock-v1`).
+//!
+//! Kept as its own module because the security-relevant invariant spans
+//! state, protocol handling *and* rendering: while [`SessionLock::locked`]
+//! is set, no client surface other than the lock surface may be drawn or
+//! receive input. The render helpers here are the "drawn" half; the input
+//! half is the locked-session branches in [`crate::input`], and the focus
+//! half is the guard in [`crate::state::CompState::set_keyboard_focus`].
+
+use smithay::backend::renderer::element::surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement};
+use smithay::backend::renderer::element::Kind;
+use smithay::output::Output;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::wayland::session_lock::{LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker};
+
+use crate::input::dwindow_wl_surface;
+use std::collections::{HashMap, HashSet};
+
+use crate::state::CompState;
+
+/// Session-lock runtime state (`ext-session-lock-v1`).
+///
+/// The security-relevant invariant is that `locked` gates *both* rendering
+/// and input: while it is set, no client surface other than a lock surface
+/// is drawn or receives events. `pending_confirm` exists because the
+/// protocol requires the compositor to confirm the lock only *after* a frame
+/// with no client content has actually been presented - confirming earlier
+/// would tell the locker "the screen is safe" while the user's windows were
+/// still on screen. So `lock()` only stashes the confirmation here, and the
+/// render paths call `confirm_lock_if_presented()` once such a frame is out.
+///
+/// With multiple monitors the locker creates **one lock surface per
+/// output**, and the confirmation must wait for *every* output to have both
+/// a surface and a presented frame - otherwise a second monitor could still
+/// be showing the desktop at the moment the locker is told the session is
+/// locked.
+#[derive(Default)]
+pub(crate) struct SessionLock {
+ pub(crate) locked: bool,
+ /// Lock surface per output, keyed by `Output::name()`.
+ pub(crate) surfaces: HashMap<String, LockSurface>,
+ pub(crate) pending_confirm: Option<SessionLocker>,
+ /// Outputs that have presented a client-content-free frame since the
+ /// lock request, keyed the same way.
+ pub(crate) presented: HashSet<String>,
+}
+
+impl CompState {
+ /// Records that `output` has presented a lock frame, and confirms the
+ /// lock once *every* output has done so. Called from both backends right
+ /// after a frame is presented. See `SessionLock::pending_confirm`.
+ pub(crate) fn confirm_lock_if_presented(&mut self, output: &Output) {
+ if !self.lock.locked || self.lock.pending_confirm.is_none() {
+ return;
+ }
+ self.lock.presented.insert(output.name());
+
+ // Every output must be both covered by a lock surface and have shown
+ // a frame of it. An output still missing its surface means the locker
+ // hasn't got to it yet - keep waiting rather than confirm early.
+ let all_covered = self
+ .outputs()
+ .all(|o| self.lock.surfaces.contains_key(&o.name()) && self.lock.presented.contains(&o.name()));
+ if !all_covered {
+ return;
+ }
+ if let Some(confirm) = self.lock.pending_confirm.take() {
+ log::info!("session lock: all {} output(s) presented a cleared frame, confirming lock", self.outputs.len());
+ confirm.lock();
+ }
+ }
+
+ /// The lock surface covering `output`, if we are locked and the locker
+ /// has created one for it. Used by the render paths and input routing.
+ pub(crate) fn lock_surface_for(&self, output: &Output) -> Option<&WlSurface> {
+ if !self.lock.locked {
+ return None;
+ }
+ self.lock.surfaces.get(&output.name()).map(|s| s.wl_surface())
+ }
+
+ /// Any lock surface at all - used by input routing, which needs a
+ /// keyboard-focus target rather than a per-output one.
+ pub(crate) fn any_lock_surface(&self) -> Option<&WlSurface> {
+ if !self.lock.locked {
+ return None;
+ }
+ self.lock.surfaces.values().next().map(|s| s.wl_surface())
+ }
+}
+
+impl SessionLockHandler for CompState {
+ fn lock_state(&mut self) -> &mut SessionLockManagerState {
+ &mut self.session_lock_state
+ }
+
+ fn lock(&mut self, confirmation: SessionLocker) {
+ log::info!("session lock: locking");
+ self.lock.locked = true;
+ self.lock.pending_confirm = Some(confirmation);
+ // Drop keyboard focus off whatever client had it immediately, so no
+ // keystroke can reach a normal client in the window between the lock
+ // request and the lock surface being mapped. Focus moves to the lock
+ // surface in `new_surface` once it exists.
+ self.set_keyboard_focus(None);
+ }
+
+ fn unlock(&mut self) {
+ log::info!("session lock: unlocking");
+ self.lock.locked = false;
+ self.lock.surfaces.clear();
+ self.lock.presented.clear();
+ self.lock.pending_confirm = None;
+ // Hand focus back to whatever srdwm considers the focused window.
+ let surface = self
+ .wm
+ .borrow()
+ .focused_id()
+ .and_then(|id| self.id_to_window.get(&id).cloned())
+ .and_then(|w| dwindow_wl_surface(&w));
+ self.set_keyboard_focus(surface);
+ }
+
+ fn new_surface(&mut self, surface: LockSurface, wl_output: WlOutput) {
+ // The locker creates one surface per output and names which; size it
+ // to that output specifically, since monitors differ in resolution.
+ let Some(entry) = self.output_for_wl(&wl_output) else {
+ log::warn!("session lock: lock surface for an output we don't drive; ignoring");
+ return;
+ };
+ let name = entry.output.name();
+ let size = entry.size();
+ surface.with_pending_state(|state| {
+ state.size = Some((size.w as u32, size.h as u32).into());
+ });
+ surface.send_configure();
+ // Store before focusing: `set_keyboard_focus`'s locked-session guard
+ // checks the focus target *against* the stored lock surfaces, so
+ // setting it afterwards would make the guard reject this very
+ // surface.
+ let wl_surface = surface.wl_surface().clone();
+ self.lock.surfaces.insert(name, surface);
+ self.set_keyboard_focus(Some(wl_surface));
+ }
+}
+
+/// Render elements for a locked session: the lock surface alone, at the
+/// output origin. Deliberately *not* built from the `Space` or the
+/// `LayerMap` - while locked, nothing else may reach the screen, so the
+/// caller pairs this with an opaque black clear color. Shared by both
+/// backends (winit and udev), which differ only in their renderer type.
+/// Takes the surface rather than `&CompState` so the udev backend can call
+/// it while already holding a `&mut` borrow of its own `UdevOutput`.
+pub(crate) fn lock_render_elements<R>(lock_surface: Option<&WlSurface>, renderer: &mut R) -> Vec<WaylandSurfaceRenderElement<R>>
+where
+ R: smithay::backend::renderer::Renderer + smithay::backend::renderer::ImportAll,
+ R::TextureId: Clone + 'static,
+{
+ let Some(surface) = lock_surface else { return Vec::new() };
+ render_elements_from_surface_tree(renderer, surface, (0, 0), 1.0, 1.0, Kind::Unspecified)
+}
+
+/// Frame callbacks for the lock surface. Without these the locker never
+/// gets to draw a second frame - no cursor blink, no password-dot
+/// feedback, no failed-attempt shake.
+pub(crate) fn send_lock_frame(lock_surface: Option<&WlSurface>, output: &Output, time: std::time::Duration) {
+ if let Some(surface) = lock_surface {
+ smithay::desktop::utils::send_frames_surface_tree(surface, output, time, None, |_, _| Some(output.clone()));
+ }
+}
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs
new file mode 100644
index 0000000..582a622
--- /dev/null
+++ b/crates/wayland/src/protocols.rs
@@ -0,0 +1,237 @@
+//! smithay protocol-handler implementations for [`CompState`], plus the
+//! `delegate_*!` macros that route each protocol's dispatch to them.
+//!
+//! Deliberately thin: these methods translate a protocol event into a call
+//! on [`crate::state`] (window bookkeeping) or [`crate::input`] (focus), and
+//! hold no logic of their own beyond what the protocol itself dictates. The
+//! session-lock handler is the one exception, living in [`crate::lock`]
+//! alongside the rest of that feature.
+
+use smithay::desktop::{layer_map_for_output, LayerSurface as DesktopLayerSurface};
+use smithay::input::pointer::CursorImageStatus;
+use smithay::input::{Seat, SeatHandler, SeatState};
+use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_seat;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::reexports::wayland_server::Client;
+use smithay::utils::Serial;
+use smithay::wayland::buffer::BufferHandler;
+use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
+use smithay::wayland::selection::data_device::{
+ ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
+};
+use smithay::wayland::selection::primary_selection::{PrimarySelectionHandler, PrimarySelectionState};
+use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
+use smithay::wayland::selection::SelectionHandler;
+use smithay::wayland::shell::wlr_layer::{
+ Layer, LayerSurface as WlrLayerSurface, WlrLayerShellHandler, WlrLayerShellState,
+};
+use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
+use smithay::wayland::shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState};
+use smithay::wayland::shm::{ShmHandler, ShmState};
+use smithay::{
+ delegate_compositor, delegate_data_control, delegate_data_device, delegate_layer_shell, delegate_output,
+ delegate_primary_selection, delegate_seat, delegate_session_lock, delegate_shm, delegate_xdg_decoration,
+ delegate_xdg_shell,
+};
+
+use crate::state::{ClientState, CompState};
+
+impl CompositorHandler for CompState {
+ fn compositor_state(&mut self) -> &mut CompositorState {
+ &mut self.compositor_state
+ }
+
+ fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
+ // Two possible client kinds now: our own `ClientState` for regular
+ // Wayland clients, or smithay's `XWaylandClientData` for the single
+ // XWayland client (see `xwayland.rs`) - both carry a
+ // `CompositorClientState`, just under different wrapper types.
+ if let Some(state) = client.get_data::<ClientState>() {
+ return &state.compositor_state;
+ }
+ &client.get_data::<smithay::xwayland::XWaylandClientData>().expect("client is neither ours nor XWayland's").compositor_state
+ }
+
+ fn commit(&mut self, surface: &WlSurface) {
+ smithay::backend::renderer::utils::on_commit_buffer_handler::<CompState>(surface);
+ // XWayland's association of an X11 window with this wl_surface can
+ // arrive at any point relative to the map request (see
+ // `xwayland.rs`'s module docs); `surface_associated` handles the
+ // common ordering, this retries the surfaces still waiting on a
+ // commit to actually make that association queryable.
+ self.retry_pending_x11_windows();
+ if let Some(&id) = self.surface_to_id.get(surface) {
+ if let Some(w) = self.id_to_window.get(&id) {
+ w.on_commit();
+ }
+ }
+ self.ensure_layer_initial_configure(surface);
+ }
+}
+
+impl XdgShellHandler for CompState {
+ fn xdg_shell_state(&mut self) -> &mut XdgShellState {
+ &mut self.xdg_shell_state
+ }
+
+ fn new_toplevel(&mut self, surface: ToplevelSurface) {
+ self.new_managed_window(surface);
+ }
+
+ fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {}
+
+ fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
+
+ fn reposition_request(&mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32) {}
+
+ fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
+ self.remove_window(surface.wl_surface());
+ }
+}
+
+impl XdgDecorationHandler for CompState {
+ fn new_decoration(&mut self, toplevel: ToplevelSurface) {
+ toplevel.with_pending_state(|state| {
+ state.decoration_mode = Some(DecorationMode::ServerSide);
+ });
+ }
+
+ fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
+ toplevel.with_pending_state(|state| {
+ state.decoration_mode = Some(DecorationMode::ServerSide);
+ });
+ toplevel.send_configure();
+ }
+
+ fn unset_mode(&mut self, _toplevel: ToplevelSurface) {}
+}
+
+impl ShmHandler for CompState {
+ fn shm_state(&self) -> &ShmState {
+ &self.shm_state
+ }
+}
+
+impl BufferHandler for CompState {
+ fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
+}
+
+impl smithay::wayland::output::OutputHandler for CompState {}
+
+impl SeatHandler for CompState {
+ type KeyboardFocus = WlSurface;
+ type PointerFocus = WlSurface;
+ type TouchFocus = WlSurface;
+
+ fn seat_state(&mut self) -> &mut SeatState<Self> {
+ &mut self.seat_state
+ }
+
+ fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
+ fn cursor_image(&mut self, _seat: &Seat<Self>, _image: CursorImageStatus) {}
+}
+
+impl WlrLayerShellHandler for CompState {
+ fn shell_state(&mut self) -> &mut WlrLayerShellState {
+ &mut self.layer_shell_state
+ }
+
+ fn new_layer_surface(&mut self, surface: WlrLayerSurface, wl_output: Option<WlOutput>, _layer: Layer, namespace: String) {
+ // A client may name the output it wants (a bar on a specific
+ // monitor); if it doesn't, or names one we don't drive, it lands on
+ // the primary output.
+ let output = wl_output
+ .as_ref()
+ .and_then(|wl| self.output_for_wl(wl))
+ .map(|e| e.output.clone())
+ .or_else(|| self.primary_output().cloned());
+ let Some(output) = output else {
+ log::warn!("wayland: layer surface requested but no output exists yet");
+ return;
+ };
+ let layer_surface = DesktopLayerSurface::new(surface, namespace);
+ let result = layer_map_for_output(&output).map_layer(&layer_surface);
+ if let Err(e) = result {
+ log::warn!("wayland: failed to map layer surface: {e}");
+ }
+ }
+
+ fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
+ // The surface belongs to exactly one output's map, but which one is
+ // the client's choice, so unmap from whichever holds it.
+ for output in self.outputs().cloned().collect::<Vec<_>>() {
+ let mut map = layer_map_for_output(&output);
+ let found = map.layers().find(|l| l.layer_surface() == &surface).cloned();
+ if let Some(layer) = found {
+ map.unmap_layer(&layer);
+ break;
+ }
+ }
+ // A lock/launcher surface holding exclusive keyboard focus just
+ // vanished (crash, or a normal close) - don't leave focus dangling
+ // on a dead surface.
+ if self.seat.get_keyboard().and_then(|k| k.current_focus()).as_ref() == Some(surface.wl_surface()) {
+ self.set_keyboard_focus(None);
+ }
+ }
+}
+
+/// Clipboard/primary-selection/drag-and-drop.
+///
+/// All three selection protocols below (`wl_data_device_manager`,
+/// `zwp_primary_selection_v1`, `zwlr_data_control_manager_v1`) share
+/// smithay's single `SelectionHandler`. Every transfer here is
+/// *client-to-client*: one client owns the selection and writes the bytes
+/// itself, and smithay wires the two ends together without the data passing
+/// through us. `send_selection` is only ever called for a
+/// **compositor-provided** selection (one this WM set itself via
+/// `set_data_device_selection`), which srdwm never does - so it is
+/// deliberately left unimplemented rather than faked.
+impl SelectionHandler for CompState {
+ type SelectionUserData = ();
+}
+
+impl DataDeviceHandler for CompState {
+ fn data_device_state(&self) -> &DataDeviceState {
+ &self.data_device_state
+ }
+}
+
+// Drag-and-drop: the default trait methods already do the right thing for a
+// compositor that doesn't draw its own drag icon or offer server-side drag
+// sources - smithay runs the pointer grab and the offer/accept negotiation
+// internally. Both are implemented empty (rather than skipped) because
+// `DataDeviceHandler` requires them as supertraits.
+impl ClientDndGrabHandler for CompState {}
+impl ServerDndGrabHandler for CompState {}
+
+impl PrimarySelectionHandler for CompState {
+ fn primary_selection_state(&self) -> &PrimarySelectionState {
+ &self.primary_selection_state
+ }
+}
+
+/// `zwlr_data_control_manager_v1`: lets a client read/watch the selection
+/// without ever holding keyboard focus. This is what `wl-paste --watch`
+/// (and thus `cliphist store`, which the user's session autostarts) needs
+/// - a focus-following clipboard manager is impossible without it.
+impl DataControlHandler for CompState {
+ fn data_control_state(&self) -> &DataControlState {
+ &self.data_control_state
+ }
+}
+
+delegate_compositor!(CompState);
+delegate_xdg_shell!(CompState);
+delegate_xdg_decoration!(CompState);
+delegate_shm!(CompState);
+delegate_seat!(CompState);
+delegate_output!(CompState);
+delegate_layer_shell!(CompState);
+delegate_data_device!(CompState);
+delegate_primary_selection!(CompState);
+delegate_data_control!(CompState);
+delegate_session_lock!(CompState);
diff --git a/crates/wayland/src/screencopy.rs b/crates/wayland/src/screencopy.rs
new file mode 100644
index 0000000..324669c
--- /dev/null
+++ b/crates/wayland/src/screencopy.rs
@@ -0,0 +1,302 @@
+//! `wlr-screencopy-unstable-v1`: lets a client ask the compositor for the
+//! contents of an output (or a region of one). This is what `grim` uses, and
+//! therefore what the user's `Print`/`Alt+Print` screenshot binds
+//! (`grim`, `slurp | grim -g -`) and `wf-recorder` need.
+//!
+//! Unlike every other protocol this backend speaks, smithay 0.7 ships no
+//! helper for this one - there is no `ScreencopyState`/`ScreencopyHandler`
+//! to delegate to - so the `GlobalDispatch`/`Dispatch` plumbing below is
+//! written out by hand against the raw `wayland-protocols-wlr` server
+//! bindings.
+//!
+//! Capture is deferred, not immediate: a `copy` request only *queues* the
+//! frame (`CompState::screencopy_pending`), and the actual pixels are read
+//! back inside the render pass, from the framebuffer that was just drawn
+//! (see `service_pending`). Doing it at request time would either capture
+//! the previous frame or require an extra off-screen render of the whole
+//! scene; reading back the real framebuffer is both cheaper and matches what
+//! is actually on screen.
+
+use std::time::UNIX_EPOCH;
+
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::renderer::{ExportMem, Renderer};
+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_shm;
+use smithay::reexports::wayland_server::{
+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
+};
+use smithay::utils::{Buffer as BufferCoord, Physical, Rectangle, Size};
+use smithay::wayland::shm::with_buffer_contents_mut;
+use wayland_protocols_wlr::screencopy::v1::server::{
+ zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
+ zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
+};
+
+use crate::state::CompState;
+
+/// Every pixel we hand out is 4 bytes; the protocol needs a stride and the
+/// shm format that matches what `copy_framebuffer` is asked to produce.
+const BYTES_PER_PIXEL: u32 = 4;
+const CAPTURE_FOURCC: Fourcc = Fourcc::Xrgb8888;
+const CAPTURE_SHM_FORMAT: wl_shm::Format = wl_shm::Format::Xrgb8888;
+
+/// The screencopy manager global. Held by `CompState` purely to keep the
+/// global alive for the compositor's lifetime.
+#[derive(Debug)]
+pub struct ScreencopyState {
+ _global: smithay::reexports::wayland_server::backend::GlobalId,
+}
+
+impl ScreencopyState {
+ pub fn new<D>(dh: &DisplayHandle) -> Self
+ where
+ D: GlobalDispatch<ZwlrScreencopyManagerV1, ()> + 'static,
+ {
+ // Version 3 advertises `linux_dmabuf`/`buffer_done`, which this
+ // software-readback implementation does not support, so cap at 2 --
+ // that still covers `copy_with_damage`, which `wf-recorder` uses.
+ Self { _global: dh.create_global::<D, ZwlrScreencopyManagerV1, _>(2, ()) }
+ }
+}
+
+/// State attached to each `zwlr_screencopy_frame_v1`.
+#[derive(Debug, Clone)]
+pub struct FrameData {
+ /// Region of the output to capture, in physical pixels.
+ pub region: Rectangle<i32, Physical>,
+ /// Set once `copy`/`copy_with_damage` has been handled, so a second one
+ /// can be rejected with the protocol's `already_used` error.
+ pub used: bool,
+}
+
+/// A capture that has been requested but not yet serviced. Drained by
+/// `service_pending` during the next render pass.
+#[derive(Debug)]
+pub struct PendingCapture {
+ pub frame: ZwlrScreencopyFrameV1,
+ pub buffer: WlBuffer,
+ pub region: Rectangle<i32, Physical>,
+ /// `copy_with_damage` clients expect a `damage` event before `ready`.
+ pub with_damage: bool,
+}
+
+impl GlobalDispatch<ZwlrScreencopyManagerV1, ()> for CompState {
+ fn bind(
+ _state: &mut Self,
+ _dh: &DisplayHandle,
+ _client: &Client,
+ manager: New<ZwlrScreencopyManagerV1>,
+ _data: &(),
+ data_init: &mut DataInit<'_, Self>,
+ ) {
+ data_init.init(manager, ());
+ }
+}
+
+impl Dispatch<ZwlrScreencopyManagerV1, ()> for CompState {
+ fn request(
+ state: &mut Self,
+ _client: &Client,
+ _manager: &ZwlrScreencopyManagerV1,
+ request: zwlr_screencopy_manager_v1::Request,
+ _data: &(),
+ _dh: &DisplayHandle,
+ data_init: &mut DataInit<'_, Self>,
+ ) {
+ use zwlr_screencopy_manager_v1::Request;
+ let (frame, region) = match request {
+ Request::CaptureOutput { frame, overlay_cursor: _, output } => {
+ (frame, state.output_capture_region(&output))
+ }
+ Request::CaptureOutputRegion { frame, overlay_cursor: _, output, x, y, width, height } => {
+ // Clamp to the output: a client is free to ask for a region
+ // hanging off the edge (slurp will, at a screen border), and
+ // `copy_framebuffer` errors out on out-of-bounds reads.
+ let full = state.output_capture_region(&output);
+ let requested = Rectangle::new((x, y).into(), (width.max(0), height.max(0)).into());
+ (frame, full.intersection(requested).unwrap_or_default())
+ }
+ Request::Destroy => return,
+ _ => return,
+ };
+
+ let frame = data_init.init(frame, FrameData { region, used: false });
+ if region.size.w <= 0 || region.size.h <= 0 {
+ // Nothing to capture (empty or fully off-screen region).
+ frame.failed();
+ return;
+ }
+ frame.buffer(
+ CAPTURE_SHM_FORMAT,
+ region.size.w as u32,
+ region.size.h as u32,
+ region.size.w as u32 * BYTES_PER_PIXEL,
+ );
+ }
+}
+
+impl Dispatch<ZwlrScreencopyFrameV1, FrameData> for CompState {
+ fn request(
+ state: &mut Self,
+ _client: &Client,
+ frame: &ZwlrScreencopyFrameV1,
+ request: zwlr_screencopy_frame_v1::Request,
+ data: &FrameData,
+ _dh: &DisplayHandle,
+ _data_init: &mut DataInit<'_, Self>,
+ ) {
+ use zwlr_screencopy_frame_v1::Request;
+ let (buffer, with_damage) = match request {
+ Request::Copy { buffer } => (buffer, false),
+ Request::CopyWithDamage { buffer } => (buffer, true),
+ Request::Destroy => {
+ state.screencopy_pending.retain(|p| &p.frame != frame);
+ return;
+ }
+ _ => return,
+ };
+
+ if data.used {
+ frame.post_error(zwlr_screencopy_frame_v1::Error::AlreadyUsed, "frame was already copied");
+ return;
+ }
+
+ // Validate the buffer really can hold the frame before promising a
+ // capture - a mismatch here would otherwise be a silent short write.
+ let expected_stride = data.region.size.w as u32 * BYTES_PER_PIXEL;
+ let ok = with_buffer_contents_mut(&buffer, |_ptr, len, spec| {
+ spec.format == CAPTURE_SHM_FORMAT
+ && spec.width == data.region.size.w
+ && spec.height == data.region.size.h
+ && spec.stride as u32 == expected_stride
+ && len >= (expected_stride * data.region.size.h as u32) as usize
+ })
+ .unwrap_or(false);
+ if !ok {
+ frame.post_error(
+ zwlr_screencopy_frame_v1::Error::InvalidBuffer,
+ "buffer does not match the advertised format/size/stride",
+ );
+ return;
+ }
+
+ state.screencopy_pending.push(PendingCapture {
+ frame: frame.clone(),
+ buffer,
+ region: data.region,
+ with_damage,
+ });
+ }
+
+ fn destroyed(state: &mut Self, _client: smithay::reexports::wayland_server::backend::ClientId, frame: &ZwlrScreencopyFrameV1, _data: &FrameData) {
+ state.screencopy_pending.retain(|p| &p.frame != frame);
+ }
+}
+
+impl CompState {
+ /// Full-output capture region for the output the client named, in that
+ /// output's own physical pixels (captures read back a single output's
+ /// framebuffer, so coordinates are output-local, not global).
+ fn output_capture_region(&self, output: &WlOutput) -> Rectangle<i32, Physical> {
+ let size: Size<i32, Physical> = self
+ .output_for_wl(output)
+ .and_then(|e| e.output.current_mode())
+ .map(|m| m.size)
+ .unwrap_or_default();
+ Rectangle::from_size(size)
+ }
+}
+
+/// Services every queued capture against the framebuffer that was just
+/// rendered, then answers each client with `flags` + `ready` (or `failed`).
+///
+/// Called from inside both backends' render passes, while `framebuffer` is
+/// still bound and holds the current frame.
+/// Rejects queued captures outright. Used while the session is locked: the
+/// render pass draws only the lock surface, so there is no frame a capture
+/// could legitimately be served from. Failing immediately is both correct
+/// (the client gets an answer instead of hanging) and safer than leaving
+/// requests queued, which would otherwise all fire against the first
+/// *unlocked* frame after the screen is unlocked.
+pub fn fail_pending(pending: Vec<PendingCapture>) {
+ for capture in pending {
+ if capture.frame.is_alive() {
+ capture.frame.failed();
+ }
+ }
+}
+
+/// Takes the queue by value rather than `&mut CompState` so the udev
+/// backend can call it while already holding a `&mut` borrow of the
+/// `UdevOutput` that owns its renderer. Callers drain
+/// `CompState::screencopy_pending` before binding the renderer.
+pub fn service_pending<R>(pending: Vec<PendingCapture>, renderer: &mut R, framebuffer: &R::Framebuffer<'_>)
+where
+ R: Renderer + ExportMem,
+{
+ for pending in pending {
+ if !pending.frame.is_alive() {
+ continue;
+ }
+ match copy_region(renderer, framebuffer, pending.region, &pending.buffer) {
+ Ok(()) => {
+ // No `y_invert`: `copy_framebuffer` hands back rows already
+ // in top-down order for both renderers used here (verified
+ // against a real `grim` capture - an inverted image is the
+ // immediately visible symptom if this is ever wrong).
+ pending.frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
+ if pending.with_damage {
+ pending.frame.damage(0, 0, pending.region.size.w as u32, pending.region.size.h as u32);
+ }
+ let now = UNIX_EPOCH.elapsed().unwrap_or_default();
+ let secs = now.as_secs();
+ pending.frame.ready((secs >> 32) as u32, secs as u32, now.subsec_nanos());
+ }
+ Err(e) => {
+ log::warn!("screencopy: capture failed: {e}");
+ pending.frame.failed();
+ }
+ }
+ }
+}
+
+fn copy_region<R>(
+ renderer: &mut R,
+ framebuffer: &R::Framebuffer<'_>,
+ region: Rectangle<i32, Physical>,
+ buffer: &WlBuffer,
+) -> Result<(), String>
+where
+ R: Renderer + ExportMem,
+{
+ // `copy_framebuffer` works in buffer coordinates; with no output
+ // transform or fractional scale in play (see this backend's single
+ // `Output`), those are the same numbers as the physical ones.
+ let src: Rectangle<i32, BufferCoord> = Rectangle::new((region.loc.x, region.loc.y).into(), (region.size.w, region.size.h).into());
+ let mapping = renderer
+ .copy_framebuffer(framebuffer, src, CAPTURE_FOURCC)
+ .map_err(|e| format!("copy_framebuffer: {e}"))?;
+ let pixels = renderer.map_texture(&mapping).map_err(|e| format!("map_texture: {e}"))?;
+
+ let stride = region.size.w as usize * BYTES_PER_PIXEL as usize;
+ let needed = stride * region.size.h as usize;
+ if pixels.len() < needed {
+ return Err(format!("readback produced {} bytes, need {}", pixels.len(), needed));
+ }
+
+ with_buffer_contents_mut(buffer, |ptr, len, _spec| {
+ if len < needed {
+ return Err(format!("client buffer holds {len} bytes, need {needed}"));
+ }
+ // SAFETY: `with_buffer_contents_mut` guarantees `ptr` is valid for
+ // `len` bytes for the duration of this closure, and `needed <= len`
+ // was just checked. Source and destination are distinct mappings.
+ unsafe {
+ std::ptr::copy_nonoverlapping(pixels.as_ptr(), ptr, needed);
+ }
+ Ok(())
+ })
+ .map_err(|e| format!("client buffer not accessible: {e}"))?
+}
diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs
new file mode 100644
index 0000000..6bc6f1a
--- /dev/null
+++ b/crates/wayland/src/state.rs
@@ -0,0 +1,356 @@
+//! 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,
+ 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())
+ })
+}
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index dcd5f76..2b491e7 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -2,10 +2,12 @@
//! host session to nest under), unlike the `backend_winit`-based path in
//! `lib.rs`.
//!
-//! Scope, kept deliberately narrow for a first real (not faked) pass:
-//! - Single primary GPU, first connected connector, its preferred (first
-//! listed) mode, positioned at `(0, 0)` - no hotplug of connectors or
-//! GPUs after startup, no multi-monitor layout.
+//! Scope:
+//! - Single primary GPU, but **every** connected connector on it: each
+//! becomes a [`UdevHead`] with its own scanout buffers, damage tracker
+//! and page-flip state, laid out left-to-right in the global coordinate
+//! space. Connectors are probed once at startup - no hotplug, and no
+//! second GPU.
//! - Rendering is **software**, via smithay's `PixmanRenderer` compositing
//! into plain KMS "dumb buffers" through the legacy (non-atomic) mode-set
//! API (`set_crtc`/`page_flip`). This deliberately avoids the
@@ -36,7 +38,7 @@ use smithay::backend::input::{
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
-use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::pixman::PixmanRenderer;
use smithay::backend::renderer::Bind;
@@ -51,7 +53,8 @@ use smithay::reexports::calloop::generic::{FdWrapper, Generic};
use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode as CalloopMode, PostAction};
use smithay::reexports::drm::buffer::DrmFourcc;
use smithay::reexports::drm::control::{
- connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Event as DrmEvent, Mode as DrmMode, PageFlipFlags,
+ connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Event as DrmEvent, Mode as DrmMode,
+ ModeTypeFlags, PageFlipFlags,
};
use smithay::reexports::drm::Device as BasicDevice;
use smithay::reexports::input::Libinput;
@@ -60,6 +63,9 @@ use smithay::reexports::rustix;
use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
use smithay::utils::{Logical, Point, Transform};
use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::selection::data_device::DataDeviceState;
+use smithay::wayland::selection::primary_selection::PrimarySelectionState;
+use smithay::wayland::selection::wlr_data_control::DataControlState;
use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
use smithay::wayland::shell::xdg::XdgShellState;
use smithay::wayland::shm::ShmState;
@@ -67,13 +73,13 @@ use smithay::wayland::shm::ShmState;
use srdwm_core::{Event as CoreEvent, WindowManager};
use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
-use crate::{
- err, handle_keyboard_key_event, handle_pointer_button, handle_pointer_position, ClientState, CompState,
-};
+use crate::err;
+use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position};
+use crate::state::{ClientState, CompState};
/// A DRM device node, opened through the session (not a raw `File::open`)
/// so access is properly gated by logind/seatd and revoked on VT switch.
-struct Card(OwnedFd);
+pub(crate) struct Card(OwnedFd);
impl AsFd for Card {
fn as_fd(&self) -> BorrowedFd<'_> {
@@ -83,98 +89,198 @@ impl AsFd for Card {
impl BasicDevice for Card {}
impl ControlDevice for Card {}
-struct DrmBuffer {
+pub(crate) struct DrmBuffer {
dumb: DumbBuffer,
fb: framebuffer::Handle,
image: Image<'static, 'static>,
}
+/// One connector+CRTC pair srdwm scans out to - i.e. one physical monitor.
+///
+/// Each head owns its own scanout buffers, damage tracker and flip state,
+/// because monitors have independent resolutions and refresh cycles: a flip
+/// completing on one says nothing about the others. The *renderer* is not
+/// here but on [`UdevState`], since all heads on one GPU share it.
+pub(crate) struct UdevHead {
+ pub(crate) crtc: crtc::Handle,
+ pub(crate) output: Output,
+ pub(crate) damage_tracker: OutputDamageTracker,
+ pub(crate) buffers: [DrmBuffer; 2],
+ pub(crate) front: usize,
+ /// A flip is in flight; the next frame for this head waits for the DRM
+ /// page-flip event (matched by `crtc`) before starting.
+ pub(crate) flip_pending: bool,
+ /// Origin of this head in the global coordinate space.
+ pub(crate) location: Point<i32, Logical>,
+ pub(crate) size: (i32, i32),
+}
+
/// Everything the DRM/udev backend needs that the nested winit backend
/// doesn't. Lives as a field on `CompState` (rather than a separate struct)
/// because calloop callbacks registered against the event loop only ever
/// get `&mut CompState` - see the module docs in `lib.rs` for why the
/// protocol-handler state itself has to be backend-agnostic.
-pub(crate) struct UdevOutput {
- card: Rc<Card>,
- crtc: crtc::Handle,
- renderer: PixmanRenderer,
- damage_tracker: OutputDamageTracker,
- output: Output,
- buffers: [DrmBuffer; 2],
- front: usize,
- flip_pending: bool,
- active: bool,
- pointer_pos: Point<f64, Logical>,
- size: (i32, i32),
+pub(crate) struct UdevState {
+ pub(crate) card: Rc<Card>,
+ /// Shared by every head: one GPU, one software renderer.
+ pub(crate) renderer: PixmanRenderer,
+ pub(crate) heads: Vec<UdevHead>,
+ pub(crate) active: bool,
+ /// Pointer position in the *global* space, so it can cross between
+ /// monitors; clamped to the union of all head rectangles.
+ pub(crate) pointer_pos: Point<f64, Logical>,
+}
+
+impl UdevState {
+ /// Bounding box of every head, used to clamp pointer motion.
+ fn bounds(&self) -> (f64, f64) {
+ let w = self.heads.iter().map(|h| h.location.x + h.size.0).max().unwrap_or(0);
+ let h = self.heads.iter().map(|h| h.location.y + h.size.1).max().unwrap_or(0);
+ (w as f64, h as f64)
+ }
}
impl CompState {
- /// Renders and (if there was damage) page-flips a new frame. No-op if
- /// a flip is already in flight (we wait for the DRM page-flip event
- /// before starting the next frame) or the session is paused.
+ /// Renders and (if there was damage) page-flips a new frame on every
+ /// head that is ready for one. A head with a flip still in flight is
+ /// skipped this pass and picked up when its page-flip event arrives, so
+ /// monitors on different refresh rates each run at their own pace
+ /// instead of the slowest one gating the rest.
pub(crate) fn render_udev_frame(&mut self) {
- let Some(udev) = self.udev.as_mut() else { return };
- if udev.flip_pending || !udev.active {
+ let locked = self.lock.locked;
+ let elapsed = self.start_time.elapsed();
+ // Drained before the `&mut self.udev` borrow below, so screencopy can
+ // be serviced with the renderer that borrow owns.
+ let mut captures = std::mem::take(&mut self.screencopy_pending);
+
+ // Which heads are eligible, and what each needs, gathered before the
+ // mutable borrow of `self.udev`.
+ let Some(udev) = self.udev.as_ref() else { return };
+ if !udev.active {
return;
}
-
- let back = 1 - udev.front;
-
- let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new();
- for (&id, deco) in self.decorations.iter() {
- let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
- match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(elem),
- Err(e) => log::warn!("udev: failed to import titlebar buffer for window {id}: {e}"),
+ let ready: Vec<(usize, Output)> = udev
+ .heads
+ .iter()
+ .enumerate()
+ .filter(|(_, h)| !h.flip_pending)
+ .map(|(i, h)| (i, h.output.clone()))
+ .collect();
+
+ let mut presented: Vec<Output> = Vec::new();
+ for (index, output) in ready {
+ let lock_surface = self.lock_surface_for(&output).cloned();
+
+ // Decoration elements are built per head: `from_buffer` needs the
+ // renderer, and geometry is translated into head-local space.
+ let origin = self.udev.as_ref().map(|u| u.heads[index].location).unwrap_or_default();
+ let decorations: Vec<(srdwm_core::Rect, MemoryRenderBuffer)> = if locked {
+ Vec::new()
+ } else {
+ self.decorations
+ .iter()
+ .filter_map(|(&id, deco)| self.wm.borrow().window(id).map(|w| (w.geometry, deco.clone())))
+ .collect()
+ };
+
+ let Some(udev) = self.udev.as_mut() else { return };
+ let head = &mut udev.heads[index];
+ let back = 1 - head.front;
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new();
+ if !locked {
+ for (geom, deco) in &decorations {
+ let pos = ((geom.x - origin.x) as f64, (geom.y - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, deco, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(elem),
+ Err(e) => log::warn!("udev: failed to import titlebar buffer: {e}"),
+ }
+ }
}
- }
-
- let mut framebuffer = match udev.renderer.bind(&mut udev.buffers[back].image) {
- Ok(fb) => fb,
- Err(e) => {
- log::error!("udev: pixman bind failed: {e}");
- return;
+ let lock_elements = if locked {
+ crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer)
+ } else {
+ Vec::new()
+ };
+
+ let head = &mut udev.heads[index];
+ let mut framebuffer = match udev.renderer.bind(&mut head.buffers[back].image) {
+ Ok(fb) => fb,
+ Err(e) => {
+ log::error!("udev: pixman bind failed: {e}");
+ continue;
+ }
+ };
+
+ // Locked heads draw the lock surface over opaque black and
+ // nothing else; unlocked heads draw the normal scene.
+ let result = if locked {
+ head.damage_tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &lock_elements, [0.0, 0.0, 0.0, 1.0])
+ .map(|r| r.damage.is_some())
+ .map_err(|e| e.to_string())
+ } else {
+ render_output(
+ &head.output,
+ &mut udev.renderer,
+ &mut framebuffer,
+ 1.0,
+ 0, // always a full redraw: buffer "age" tracking isn't worth the complexity for a software-only backend
+ [&self.space],
+ &custom_elements,
+ &mut head.damage_tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map(|r| r.damage.is_some())
+ // Both arms reduce to "was there damage"; the two error types
+ // differ, so they are flattened to a message here.
+ .map_err(|e| e.to_string())
+ };
+ if !locked {
+ crate::screencopy::service_pending(std::mem::take(&mut captures), &mut udev.renderer, &framebuffer);
}
- };
+ drop(framebuffer);
- let result = render_output(
- &udev.output,
- &mut udev.renderer,
- &mut framebuffer,
- 1.0,
- 0, // always a full redraw: buffer "age" tracking isn't worth the complexity for a software-only, single-output backend
- [&self.space],
- &custom_elements,
- &mut udev.damage_tracker,
- [0.05, 0.05, 0.08, 1.0],
- );
- drop(framebuffer);
-
- let has_damage = match result {
- Ok(res) => res.damage.is_some(),
- Err(e) => {
- log::error!("udev: render_output failed: {e}");
- false
+ let has_damage = match result {
+ Ok(d) => d,
+ Err(e) => {
+ log::error!("udev: render_output failed: {e}");
+ continue;
+ }
+ };
+ if has_damage {
+ let head = &mut udev.heads[index];
+ if let Err(e) = head.copy_and_flip(&udev.card, back) {
+ log::error!("udev: page flip failed: {e}");
+ continue;
+ }
}
- };
- if !has_damage {
- return;
+ presented.push(output);
}
- if let Err(e) = udev.copy_and_flip(back) {
- log::error!("udev: page flip failed: {e}");
- return;
+ // Frame callbacks + lock confirmation, once the `udev` borrow is done.
+ for output in presented {
+ if locked {
+ let surface = self.lock_surface_for(&output).cloned();
+ crate::lock::send_lock_frame(surface.as_ref(), &output, elapsed);
+ self.confirm_lock_if_presented(&output);
+ } else {
+ let out = output.clone();
+ self.space.elements().for_each(|w| w.send_frame(&out, elapsed, None, |_, _| Some(out.clone())));
+ }
+ }
+ if locked {
+ crate::screencopy::fail_pending(captures);
}
- self.space.elements().for_each(|w| w.send_frame(&udev.output, self.start_time.elapsed(), None, |_, _| Some(udev.output.clone())));
}
}
-impl UdevOutput {
+impl UdevHead {
/// Copies the just-rendered pixman image into buffer `back`'s dumb
/// buffer (software rendering writes into its own owned image, not the
/// scanout memory directly, to avoid tying that image's lifetime to an
- /// mmap - seem `crates/wayland/src/udev.rs` module docs) and flips to it.
- fn copy_and_flip(&mut self, back: usize) -> std::io::Result<()> {
+ /// mmap - see this module's docs) and flips to it.
+ fn copy_and_flip(&mut self, card: &Card, back: usize) -> std::io::Result<()> {
let (stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
let byte_len = stride * height;
// SAFETY: `image` owns this memory and outlives the byte slice we
@@ -182,12 +288,12 @@ impl UdevOutput {
// of this call.
let src: &[u8] = unsafe { std::slice::from_raw_parts(self.buffers[back].image.data() as *const u8, byte_len) };
{
- let mut mapping = self.card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
+ let mut mapping = card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
let dst = mapping.as_mut();
let len = byte_len.min(dst.len());
dst[..len].copy_from_slice(&src[..len]);
}
- self.card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
+ card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
self.flip_pending = true;
Ok(())
}
@@ -221,57 +327,93 @@ impl UdevPlatform {
.map_err(err)?;
let card = Rc::new(Card(fd));
- let (crtc, connector, mode) = find_connected_output(&card)?;
- let (width, height) = mode.size();
- let (width, height) = (width as i32, height as i32);
-
- let buffers = [make_drm_buffer(&card, width, height)?, make_drm_buffer(&card, width, height)?];
- card.set_crtc(crtc, Some(buffers[0].fb), (0, 0), &[connector], Some(mode)).map_err(err)?;
-
- let output = Output::new(
- "srdwm-udev".to_string(),
- PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
- );
- let refresh = mode_refresh_mhz(&mode);
- output.change_current_state(
- Some(OutputMode { size: (width, height).into(), refresh }),
- Some(Transform::Normal),
- None,
- Some((0, 0).into()),
- );
+ // Every connected connector becomes a head, laid out left-to-right.
+ let connected = find_connected_outputs(&card)?;
+ log::info!("udev: {} connected output(s)", connected.len());
let renderer = PixmanRenderer::new().map_err(err)?;
- let damage_tracker = OutputDamageTracker::from_output(&output);
-
let dh = Display::<CompState>::new().map_err(err)?;
let display_handle = dh.handle();
- output.create_global::<CompState>(&display_handle);
+
+ let mut heads: Vec<UdevHead> = Vec::new();
+ let mut output_entries: Vec<crate::state::OutputEntry> = Vec::new();
+ let mut x_offset = 0;
+ for (index, probe) in connected.iter().enumerate() {
+ let (width, height) = probe.mode.size();
+ let (width, height) = (width as i32, height as i32);
+
+ let buffers = [make_drm_buffer(&card, width, height)?, make_drm_buffer(&card, width, height)?];
+ card.set_crtc(probe.crtc, Some(buffers[0].fb), (0, 0), &[probe.connector], Some(probe.mode))
+ .map_err(err)?;
+
+ // Named after the real connector (eDP-1, HDMI-A-1, ...) so
+ // clients and the user can tell monitors apart; `wl_output.name`
+ // is what a bar's per-monitor config keys off.
+ let output = Output::new(
+ probe.name.clone(),
+ PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
+ );
+ output.change_current_state(
+ Some(OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) }),
+ Some(Transform::Normal),
+ None,
+ Some((x_offset, 0).into()),
+ );
+ output.set_preferred(OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) });
+ output.create_global::<CompState>(&display_handle);
+
+ let location: Point<i32, Logical> = (x_offset, 0).into();
+ heads.push(UdevHead {
+ crtc: probe.crtc,
+ output: output.clone(),
+ damage_tracker: OutputDamageTracker::from_output(&output),
+ buffers,
+ front: 0,
+ flip_pending: false,
+ location,
+ size: (width, height),
+ });
+ output_entries.push(crate::state::OutputEntry { output, location });
+ log::info!("udev: head {index}: {} {width}x{height} at x={x_offset}", probe.name);
+ x_offset += width;
+ }
+ let Some(first) = heads.first() else {
+ return Err(PlatformError::Other("udev: no usable outputs".into()));
+ };
+ // Pointer starts centred on the first head.
+ let (width, height) = first.size;
+ // xdg-output - see the matching comment in `lib.rs`'s
+ // `WaylandPlatform::connect` for why this isn't optional.
+ smithay::wayland::output::OutputManagerState::new_with_xdg_output::<CompState>(&display_handle);
let compositor_state = CompositorState::new::<CompState>(&display_handle);
let xdg_shell_state = XdgShellState::new::<CompState>(&display_handle);
let xdg_decoration_state = XdgDecorationState::new::<CompState>(&display_handle);
let shm_state = ShmState::new::<CompState>(&display_handle, Vec::new());
+ // Selection (clipboard) protocols - see the matching block in
+ // `lib.rs`'s `WaylandPlatform::connect` for the ordering constraint.
+ let primary_selection_state = PrimarySelectionState::new::<CompState>(&display_handle);
+ let data_control_state =
+ DataControlState::new::<CompState, _>(&display_handle, Some(&primary_selection_state), |_| true);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "seat0");
seat.add_keyboard(Default::default(), 200, 25).map_err(err)?;
seat.add_pointer();
+ // Each output occupies its own slice of the global space, so a
+ // window's coordinates say which monitor it is on.
let mut space = Space::default();
- space.map_output(&output, (0, 0));
+ for entry in &output_entries {
+ space.map_output(&entry.output, (entry.location.x, entry.location.y));
+ }
let pending = Rc::new(RefCell::new(Vec::new()));
- let udev_output = UdevOutput {
+ let udev_state = UdevState {
card: card.clone(),
- crtc,
renderer,
- damage_tracker,
- output: output.clone(),
- buffers,
- front: 0,
- flip_pending: false,
+ heads,
active: true,
pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(),
- size: (width, height),
};
let state = CompState {
@@ -282,6 +424,19 @@ impl UdevPlatform {
seat_state,
seat,
space,
+ outputs: output_entries,
+ layer_shell_state: smithay::wayland::shell::wlr_layer::WlrLayerShellState::new::<CompState>(&display_handle),
+ dh: display_handle.clone(),
+ data_device_state: DataDeviceState::new::<CompState>(&display_handle),
+ primary_selection_state,
+ data_control_state,
+ session_lock_state: smithay::wayland::session_lock::SessionLockManagerState::new::<CompState, _>(
+ &display_handle,
+ |_| true,
+ ),
+ _screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
+ screencopy_pending: Vec::new(),
+ lock: Default::default(),
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
@@ -289,7 +444,7 @@ impl UdevPlatform {
pending: pending.clone(),
bound_keys: Rc::new(bound_keys.iter().cloned().collect::<HashSet<_>>()),
start_time: Instant::now(),
- udev: Some(udev_output),
+ udev: Some(udev_state),
xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&display_handle),
xwm: None,
xwayland_windows: HashMap::new(),
@@ -331,27 +486,70 @@ fn mode_refresh_mhz(mode: &DrmMode) -> i32 {
}
}
-// TODO: multi-monitor - returns only the first connected connector; no
-// hotplug, no independent per-output layout. See
-// docs/IMPLEMENTATION_STATUS.md's "Not implemented anywhere yet" section.
-fn find_connected_output(card: &Card) -> PlatformResult<(crtc::Handle, connector::Handle, DrmMode)> {
+/// A connector we intend to drive, paired with the CRTC that will scan it
+/// out. Produced once at startup by [`find_connected_outputs`].
+struct OutputProbe {
+ crtc: crtc::Handle,
+ connector: connector::Handle,
+ mode: DrmMode,
+ /// Connector name as the kernel reports it (`eDP-1`, `HDMI-A-1`, ...).
+ name: String,
+}
+
+/// Every connected connector, each assigned a distinct CRTC.
+///
+/// CRTCs are a finite hardware resource and cannot be shared, so a CRTC
+/// already claimed by an earlier connector is skipped - a machine with more
+/// connected monitors than CRTCs drives as many as the hardware allows and
+/// logs the rest rather than failing outright.
+///
+/// Still no hotplug: connectors are probed once at startup. Plugging a
+/// monitor in later needs a udev event handler, which this backend does not
+/// register yet (see `docs/IMPLEMENTATION_STATUS.md`).
+fn find_connected_outputs(card: &Card) -> PlatformResult<Vec<OutputProbe>> {
let res = card.resource_handles().map_err(err)?;
let connectors: Vec<connector::Info> = res.connectors().iter().flat_map(|&h| card.get_connector(h, true)).collect();
- let con = connectors
- .iter()
- .find(|c| c.state() == connector::State::Connected)
- .ok_or_else(|| PlatformError::Other("udev: no connected connector found".into()))?;
- let mode = *con.modes().first().ok_or_else(|| PlatformError::Other("udev: connected connector has no modes".into()))?;
-
- let crtcs: Vec<crtc::Handle> = res.crtcs().to_vec();
- let crtc = con
- .current_encoder()
- .and_then(|enc| card.get_encoder(enc).ok())
- .and_then(|enc| res.filter_crtcs(enc.possible_crtcs()).into_iter().next())
- .or_else(|| crtcs.first().copied())
- .ok_or_else(|| PlatformError::Other("udev: no usable crtc found".into()))?;
-
- Ok((crtc, con.handle(), mode))
+
+ let mut used: Vec<crtc::Handle> = Vec::new();
+ let mut probes = Vec::new();
+ for con in connectors.iter().filter(|c| c.state() == connector::State::Connected) {
+ let name = format!("{:?}-{}", con.interface(), con.interface_id());
+ // Prefer the mode the display advertises as PREFERRED (its native
+ // resolution) rather than whatever happens to be listed first --
+ // the list order is not guaranteed, and picking wrong means running
+ // a monitor at the wrong resolution. Falls back to the first mode
+ // for connectors that flag none.
+ let Some(&mode) = con
+ .modes()
+ .iter()
+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
+ .or_else(|| con.modes().first())
+ else {
+ log::warn!("udev: connector {name} is connected but reports no modes; skipping");
+ continue;
+ };
+ // Prefer the CRTC already driving this connector, else any free one
+ // the encoder can reach.
+ let candidates: Vec<crtc::Handle> = con
+ .current_encoder()
+ .and_then(|enc| card.get_encoder(enc).ok())
+ .map(|enc| res.filter_crtcs(enc.possible_crtcs()))
+ .unwrap_or_default()
+ .into_iter()
+ .chain(res.crtcs().iter().copied())
+ .collect();
+ let Some(crtc) = candidates.into_iter().find(|c| !used.contains(c)) else {
+ log::warn!("udev: no free CRTC left for connector {name}; not driving it");
+ continue;
+ };
+ used.push(crtc);
+ probes.push(OutputProbe { crtc, connector: con.handle(), mode, name });
+ }
+
+ if probes.is_empty() {
+ return Err(PlatformError::Other("udev: no connected connector found".into()));
+ }
+ Ok(probes)
}
fn make_drm_buffer(card: &Card, width: i32, height: i32) -> PlatformResult<DrmBuffer> {
@@ -374,17 +572,21 @@ fn register_drm_fd(handle: &LoopHandle<'static, CompState>, card: &Rc<Card>) ->
let card = udev.card.clone();
match card.receive_events() {
Ok(events) => {
+ // The event names the CRTC it came from, so with several
+ // monitors only that head advances - flipping all of
+ // them would desynchronise the others' buffers.
let mut flipped = false;
for event in events {
- if let DrmEvent::PageFlip(_) = event {
- flipped = true;
+ let DrmEvent::PageFlip(flip) = event else { continue };
+ if let Some(udev) = data.udev.as_mut() {
+ if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) {
+ head.front = 1 - head.front;
+ head.flip_pending = false;
+ flipped = true;
+ }
}
}
if flipped {
- if let Some(udev) = data.udev.as_mut() {
- udev.front = 1 - udev.front;
- udev.flip_pending = false;
- }
data.render_udev_frame();
}
}
@@ -422,10 +624,16 @@ fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier:
log::info!("udev: session resumed (VT switch back)");
udev.active = true;
// Some drivers reset mode-setting state across a VT
- // switch; reassert it before rendering again.
- let fb = udev.buffers[udev.front].fb;
- if let Err(e) = udev.card.set_crtc(udev.crtc, Some(fb), (0, 0), &[], None) {
- log::warn!("udev: failed to reassert crtc on resume: {e}");
+ // switch; reassert every head before rendering again.
+ let card = udev.card.clone();
+ for head in &mut udev.heads {
+ let fb = head.buffers[head.front].fb;
+ if let Err(e) = card.set_crtc(head.crtc, Some(fb), (0, 0), &[], None) {
+ log::warn!("udev: failed to reassert crtc on resume: {e}");
+ }
+ // Force a full repaint: contents are undefined after
+ // the VT switch.
+ head.flip_pending = false;
}
data.render_udev_frame();
}
@@ -441,9 +649,11 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
InputEvent::PointerMotion { event } => {
let Some(udev) = state.udev.as_mut() else { return };
let delta = event.delta();
- let (w, h) = (udev.size.0 as f64, udev.size.1 as f64);
- udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(0.0, w - 1.0);
- udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(0.0, h - 1.0);
+ // Clamped to the union of every head, so the pointer travels
+ // between monitors instead of stopping at the first one's edge.
+ let (w, h) = udev.bounds();
+ udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(0.0, (w - 1.0).max(0.0));
+ udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(0.0, (h - 1.0).max(0.0));
let pos = udev.pointer_pos;
handle_pointer_position(state, pos, event.time_msec());
}
@@ -486,14 +696,24 @@ impl Platform for UdevPlatform {
Ok(self.pending.borrow_mut().drain(..).collect())
}
+ /// One `srdwm_core::Monitor` per head, positioned in the global space.
+ /// This is what makes core's layout engine multi-monitor-aware in
+ /// practice: `arrange_workspace` groups windows by `monitor` and lays
+ /// each group out inside that monitor's rectangle.
fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
let Some(udev) = self.state.udev.as_ref() else { return Ok(Vec::new()) };
- let (w, h) = udev.size;
- Ok(vec![{
- let mut m = srdwm_core::Monitor::new(0, "drm", srdwm_core::Rect::new(0, 0, w as u32, h as u32));
- m.primary = true;
- m
- }])
+ Ok(udev
+ .heads
+ .iter()
+ .enumerate()
+ .map(|(i, head)| {
+ let (w, h) = head.size;
+ let rect = srdwm_core::Rect::new(head.location.x, head.location.y, w as u32, h as u32);
+ let mut m = srdwm_core::Monitor::new(i as u32, head.output.name(), rect);
+ m.primary = i == 0;
+ m
+ })
+ .collect())
}
fn apply_geometry(&mut self, window: srdwm_core::WindowId, _geometry: srdwm_core::Rect) -> PlatformResult<()> {
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
new file mode 100644
index 0000000..85be223
--- /dev/null
+++ b/crates/wayland/src/winit.rs
@@ -0,0 +1,412 @@
+//! Nested ("winit") backend: srdwm as a window on an existing
+//! compositor/X server, the Wayland analogue of running the X11 backend
+//! under Xephyr. Used for development and for the case where srdwm is
+//! started from inside another session; the real bare-TTY path is
+//! [`crate::udev`].
+//!
+//! Both backends share all protocol state ([`crate::state::CompState`]),
+//! input routing ([`crate::input`]) and lock behaviour ([`crate::lock`]);
+//! what differs is only how a frame reaches a screen.
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::time::Instant;
+
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::input::{AbsolutePositionEvent, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerButtonEvent};
+use smithay::backend::renderer::damage::OutputDamageTracker;
+use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
+use smithay::desktop::space::render_output;
+use smithay::desktop::{layer_map_for_output, Space};
+use smithay::input::SeatState;
+use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
+use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
+use smithay::reexports::winit::platform::pump_events::PumpStatus;
+use smithay::utils::Transform;
+use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::selection::data_device::DataDeviceState;
+use smithay::wayland::selection::primary_selection::PrimarySelectionState;
+use smithay::wayland::selection::wlr_data_control::DataControlState;
+use smithay::wayland::session_lock::SessionLockManagerState;
+use smithay::wayland::shell::wlr_layer::WlrLayerShellState;
+use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
+use smithay::wayland::shell::xdg::XdgShellState;
+use smithay::wayland::shm::ShmState;
+
+use srdwm_core::{Event as CoreEvent, Window as CoreWindow, WindowId, WindowManager};
+use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
+
+use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position, last_pointer_pos};
+use crate::lock::{lock_render_elements, send_lock_frame};
+use crate::lock::SessionLock;
+use crate::state::{ClientState, CompState, OutputEntry};
+use crate::{err, screencopy};
+
+pub struct WaylandPlatform {
+ display: Display<CompState>,
+ state: CompState,
+ backend: WinitGraphicsBackend<GlesRenderer>,
+ winit_events: WinitEventLoop,
+ damage_tracker: OutputDamageTracker,
+ output: Output,
+ listener: ListeningSocket,
+ clients: Vec<Client>,
+ pending: Rc<RefCell<Vec<CoreEvent>>>,
+ wm: Rc<RefCell<WindowManager>>,
+}
+
+impl WaylandPlatform {
+ /// `bound_keys` are the config's `"Mod4+Shift+Return"`-style combo
+ /// strings (see `srdwm_core::key_combo_string`) - the same set the X11
+ /// backend grabs individually via `XGrabKey`. Only a keypress matching
+ /// one of these is withheld from the focused client.
+ pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Self> {
+ let display: Display<CompState> = Display::new().map_err(err)?;
+ let dh = display.handle();
+
+ let (backend, winit_events) = winit::init::<GlesRenderer>().map_err(err)?;
+ let size = backend.window_size();
+
+ let output = Output::new(
+ "srdwm-wayland".to_string(),
+ PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "winit".into() },
+ );
+ output.change_current_state(
+ Some(OutputMode { size, refresh: 60_000 }),
+ Some(Transform::Normal),
+ None,
+ Some((0, 0).into()),
+ );
+ output.create_global::<CompState>(&dh);
+ // xdg-output (zxdg_output_manager_v1): several real layer-shell
+ // clients (confirmed live: wofi 1.5.3) unconditionally call
+ // `zxdg_output_manager_v1.get_xdg_output` while setting up a layer
+ // surface and don't null-check the manager proxy if the global was
+ // never advertised - so without this, those clients don't just
+ // fail gracefully, they segfault. Real wlroots compositors (e.g.
+ // Hyprland) always advertise it, which is why this only surfaced
+ // once a real Wayland-native client could connect at all (see
+ // `docs/IMPLEMENTATION_STATUS.md`).
+ smithay::wayland::output::OutputManagerState::new_with_xdg_output::<CompState>(&dh);
+
+ let compositor_state = CompositorState::new::<CompState>(&dh);
+ let xdg_shell_state = XdgShellState::new::<CompState>(&dh);
+ let xdg_decoration_state = XdgDecorationState::new::<CompState>(&dh);
+ let shm_state = ShmState::new::<CompState>(&dh, Vec::new());
+ // Order matters: data-control piggybacks on primary-selection's
+ // client filter, so it needs the already-built state by reference.
+ let primary_selection_state = PrimarySelectionState::new::<CompState>(&dh);
+ let data_control_state = DataControlState::new::<CompState, _>(&dh, Some(&primary_selection_state), |_| true);
+ let mut seat_state = SeatState::new();
+ let mut seat = seat_state.new_wl_seat(&dh, "seat0");
+ seat.add_keyboard(Default::default(), 200, 25).map_err(err)?;
+ seat.add_pointer();
+
+ let mut space = Space::default();
+ space.map_output(&output, (0, 0));
+
+ let pending = Rc::new(RefCell::new(Vec::new()));
+ let state = CompState {
+ compositor_state,
+ xdg_shell_state,
+ _xdg_decoration_state: xdg_decoration_state,
+ shm_state,
+ seat_state,
+ seat,
+ space,
+ // The nested backend is inherently one output: a single window on
+ // the host. Multi-output is a udev/DRM concern.
+ outputs: vec![OutputEntry { output: output.clone(), location: (0, 0).into() }],
+ layer_shell_state: WlrLayerShellState::new::<CompState>(&dh),
+ dh: dh.clone(),
+ data_device_state: DataDeviceState::new::<CompState>(&dh),
+ primary_selection_state,
+ data_control_state,
+ session_lock_state: SessionLockManagerState::new::<CompState, _>(&dh, |_| true),
+ _screencopy_state: screencopy::ScreencopyState::new::<CompState>(&dh),
+ screencopy_pending: Vec::new(),
+ lock: SessionLock::default(),
+ wm: wm.clone(),
+ surface_to_id: HashMap::new(),
+ id_to_window: HashMap::new(),
+ decorations: HashMap::new(),
+ pending: pending.clone(),
+ bound_keys: Rc::new(bound_keys.iter().cloned().collect()),
+ start_time: Instant::now(),
+ udev: None,
+ xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&dh),
+ xwm: None,
+ xwayland_windows: HashMap::new(),
+ xwayland_pending: Vec::new(),
+ };
+
+ let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
+ if let Some(name) = listener.socket_name() {
+ std::env::set_var("WAYLAND_DISPLAY", name);
+ log::info!("wayland socket: {}", name.to_string_lossy());
+ }
+
+ let damage_tracker = OutputDamageTracker::from_output(&output);
+
+ Ok(Self { display, state, backend, winit_events, damage_tracker, output, listener, clients: Vec::new(), pending, wm })
+ }
+
+ fn accept_clients(&mut self) -> PlatformResult<()> {
+ if let Some(stream) = self.listener.accept().map_err(err)? {
+ let client = self.display.handle().insert_client(stream, std::sync::Arc::new(ClientState::default())).map_err(err)?;
+ self.clients.push(client);
+ }
+ Ok(())
+ }
+
+ fn pump_winit(&mut self) -> PlatformResult<bool> {
+ let mut closed = false;
+ let state = &mut self.state;
+ let output = &self.output;
+ let pump = self.winit_events.dispatch_new_events(|event| {
+ handle_winit_event(state, output, event, &mut closed);
+ });
+ if matches!(pump, PumpStatus::Exit(_)) {
+ closed = true;
+ }
+ Ok(closed)
+ }
+
+ fn render_frame(&mut self) -> PlatformResult<()> {
+ let size = self.backend.window_size();
+ let resized = self.output.current_mode().map(|m| m.size) != Some(size);
+ if resized {
+ // Only push a new output mode - and thus emit `wl_output.mode`/
+ // `done` - when the size actually changed. This used to run
+ // unconditionally every frame; harmless with no Wayland-native
+ // client connected (the only way this was ever exercised before
+ // real layer-shell clients existed), but a real client bound to
+ // `wl_output` would otherwise be flooded with duplicate
+ // mode/done events at the render loop's full frame rate.
+ self.output.change_current_state(Some(OutputMode { size, refresh: 60_000 }), None, None, None);
+ layer_map_for_output(&self.output).arrange();
+ }
+
+ let age = self.backend.buffer_age().unwrap_or(0);
+ let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?;
+
+ // Locked: the lock surface over an opaque black clear, and nothing
+ // else - no windows, no decorations, no layer surfaces.
+ if self.state.lock.locked {
+ let lock_surface = self.state.lock_surface_for(&self.output).cloned();
+ let elements = lock_render_elements(lock_surface.as_ref(), renderer);
+ self.damage_tracker
+ .render_output(renderer, &mut framebuffer, age, &elements, [0.0, 0.0, 0.0, 1.0])
+ .map_err(err)?;
+ drop(framebuffer);
+ self.backend.submit(None).map_err(err)?;
+ send_lock_frame(lock_surface.as_ref(), &self.output, self.state.start_time.elapsed());
+ self.state.confirm_lock_if_presented(&self.output);
+ screencopy::fail_pending(std::mem::take(&mut self.state.screencopy_pending));
+ return Ok(());
+ }
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new();
+ for (&id, deco) in self.state.decorations.iter() {
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(elem),
+ Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
+ }
+ }
+
+ render_output(
+ &self.output,
+ renderer,
+ &mut framebuffer,
+ 1.0,
+ age,
+ [&self.state.space],
+ &custom_elements,
+ &mut self.damage_tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map_err(err)?;
+ drop(framebuffer);
+ self.backend.submit(None).map_err(err)?;
+ self.state.space.elements().for_each(|w| w.send_frame(&self.output, self.state.start_time.elapsed(), None, |_, _| Some(self.output.clone())));
+
+ // Screencopy is serviced *after* the on-screen frame is submitted,
+ // into its own offscreen buffer - never by reading back the window
+ // surface. Reading the winit backend's EGL window surface (what an
+ // earlier version did) reliably killed the GL context: the first
+ // `grim` capture produced `eglSwapBuffers: BAD_SURFACE` followed by
+ // `BAD_ALLOC` and "context has been lost", confirmed by A/B-ing the
+ // same build with only the readback call removed. The offscreen
+ // detour costs a second scene render, but only on frames where a
+ // capture was actually requested.
+ //
+ // Deliberately placed after the locked-session early return above,
+ // so a capture requested while the screen is locked can never see
+ // client content.
+ let captures = std::mem::take(&mut self.state.screencopy_pending);
+ if !captures.is_empty() {
+ if let Err(e) = self.capture_offscreen(captures) {
+ log::warn!("screencopy: offscreen capture pass failed: {e}");
+ }
+ }
+ Ok(())
+ }
+
+ /// Re-renders the current scene into an offscreen GLES renderbuffer and
+ /// serves the queued screencopy captures from it. See the call site for
+ /// why capture cannot read the window surface directly.
+ fn capture_offscreen(&mut self, captures: Vec<screencopy::PendingCapture>) -> PlatformResult<()> {
+ use smithay::backend::renderer::{Bind, Offscreen};
+
+ let size = self.output.current_mode().map(|m| m.size).unwrap_or_default();
+ if size.w <= 0 || size.h <= 0 {
+ return Ok(());
+ }
+ let renderer = self.backend.renderer();
+ let mut target: smithay::backend::renderer::gles::GlesRenderbuffer =
+ renderer.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()).map_err(err)?;
+ let mut framebuffer = renderer.bind(&mut target).map_err(err)?;
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new();
+ for (&id, deco) in self.state.decorations.iter() {
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
+ custom_elements.push(elem);
+ }
+ }
+
+ // A throwaway damage tracker, so this pass always draws the whole
+ // scene (age 0) and never perturbs the on-screen tracker's history.
+ let mut tracker = OutputDamageTracker::from_output(&self.output);
+ render_output(
+ &self.output,
+ renderer,
+ &mut framebuffer,
+ 1.0,
+ 0,
+ [&self.state.space],
+ &custom_elements,
+ &mut tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map_err(err)?;
+
+ screencopy::service_pending(captures, renderer, &framebuffer);
+ Ok(())
+ }
+}
+
+fn handle_winit_event(state: &mut CompState, output: &Output, event: WinitEvent, closed: &mut bool) {
+ match event {
+ WinitEvent::CloseRequested => *closed = true,
+ WinitEvent::Input(InputEvent::Keyboard { event }) => handle_keyboard_key_event(state, &event),
+ WinitEvent::Input(InputEvent::PointerMotionAbsolute { event }) => {
+ let size = output.current_mode().map(|m| m.size).unwrap_or_default().to_logical(1);
+ let pos = event.position_transformed(size);
+ handle_pointer_position(state, pos, event.time_msec());
+ }
+ WinitEvent::Input(InputEvent::PointerButton { event }) => {
+ let pos = last_pointer_pos(state);
+ let button = event.button_code();
+ let pressed = event.state() == BackendButtonState::Pressed;
+ handle_pointer_button(state, pos, button, pressed, event.time_msec());
+ }
+ WinitEvent::Resized { .. } => {}
+ _ => {}
+ }
+}
+
+impl Platform for WaylandPlatform {
+ fn kind(&self) -> PlatformKind {
+ PlatformKind::Wayland
+ }
+
+ fn poll_events(&mut self) -> PlatformResult<Vec<CoreEvent>> {
+ self.accept_clients()?;
+ let closed = self.pump_winit()?;
+ if closed {
+ return Err(PlatformError::Other("compositor window closed".into()));
+ }
+ self.display.dispatch_clients(&mut self.state).map_err(err)?;
+ self.display.flush_clients().map_err(err)?;
+ self.render_frame()?;
+ Ok(self.pending.borrow_mut().drain(..).collect())
+ }
+
+ fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
+ let size = self.backend.window_size();
+ Ok(vec![{
+ let mut m = srdwm_core::Monitor::new(0, "winit", srdwm_core::Rect::new(0, 0, size.w as u32, size.h as u32));
+ m.primary = true;
+ m
+ }])
+ }
+
+ fn apply_geometry(&mut self, window: WindowId, geometry: srdwm_core::Rect) -> PlatformResult<()> {
+ let _ = geometry;
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn set_title(&mut self, _window: WindowId, _title: &str) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
+ self.state.wm.borrow_mut().focus_window(window);
+ Ok(())
+ }
+
+ fn minimize(&mut self, window: WindowId) -> PlatformResult<()> {
+ if let Some(w) = self.state.id_to_window.get(&window) {
+ self.state.space.unmap_elem(w);
+ }
+ Ok(())
+ }
+
+ fn restore(&mut self, window: WindowId) -> PlatformResult<()> {
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn close(&mut self, window: WindowId) -> PlatformResult<()> {
+ if let Some(w) = self.state.id_to_window.get(&window).and_then(|w| w.toplevel()) {
+ w.send_close();
+ }
+ Ok(())
+ }
+
+ fn set_decorated(&mut self, _window: WindowId, _decorated: bool) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn set_border_color(&mut self, _window: WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn set_border_width(&mut self, _window: WindowId, _width: u32) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn redraw_decoration(&mut self, window: WindowId, _win: &CoreWindow, _focused: bool) -> PlatformResult<()> {
+ // Re-renders the title/focus-color band and re-syncs geometry;
+ // `sync_geometry` re-renders the decoration too, but only if one
+ // already exists, so this also covers first paint.
+ self.state.redraw_decoration_buffer(window);
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn grab_keyboard(&mut self) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn ungrab_keyboard(&mut self) -> PlatformResult<()> {
+ Ok(())
+ }
+}
diff --git a/crates/wayland/src/xwayland.rs b/crates/wayland/src/xwayland.rs
index ca54f87..ac71492 100644
--- a/crates/wayland/src/xwayland.rs
+++ b/crates/wayland/src/xwayland.rs
@@ -31,7 +31,7 @@ use smithay::{delegate_xwayland_shell, desktop::Window as DWindow};
use srdwm_core::{Event as CoreEvent, ResizeEdge, Window as CoreWindow, TITLEBAR_HEIGHT};
-use crate::CompState;
+use crate::state::CompState;
pub(crate) type X11Window = smithay::xwayland::xwm::X11Window;