diff options
| author | srdusr <[email protected]> | 2024-04-02 01:07:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-04-02 01:07:00 +0200 |
| commit | 43175c78a5b450eb108738187b72e80d36a7bf5d (patch) | |
| tree | dbbadbcc18162f766f5af653d81882aaaa6f5e41 /crates/wayland/src/lib.rs | |
| parent | 8110bb2773b6c841029a51eca7971f42a36f480c (diff) | |
| download | srdwm-43175c78a5b450eb108738187b72e80d36a7bf5d.tar.gz srdwm-43175c78a5b450eb108738187b72e80d36a7bf5d.zip | |
Add window rules, real config validation, and a Wayland DRM/udev backend
- srd.rule(): match windows by title/class, apply floating/maximized/
workspace/geometry/decoration actions on creation (crates/core/src/rules.rs)
- srd.validate_config()/srd.debug.*: real range/format checks and
status/profiling helpers, replacing the always-true stub
- Wayland titlebar text rendering via fontdue, unit-tested without a
display (crates/wayland/src/decoration.rs)
- Wayland precise keybinding matching, replacing the "any Super-held key"
heuristic, sharing the keysym table with X11 (moved to
crates/core/src/keysyms.rs)
- Wayland DRM/udev backend (crates/wayland/src/udev.rs): runs as the real
compositor on a bare TTY via libseat/libinput/KMS, software rendering
via Pixman + dumb buffers (no GBM/EGL required)
- srdwm_platform::detect() fix, found via VM testing: a bare TTY with no
DISPLAY/WAYLAND_DISPLAY now correctly resolves to Wayland instead of an
X11 backend that can never work there
- XWayland integration groundwork (crates/wayland/src/xwayland.rs): spawn,
X11Wm, and full XwmHandler event routing into the same WindowManager/
Space pipeline as native clients. Windows don't render yet - a real
glamor-vs-software-renderer conflict in XWayland's own fallback path,
root-caused via WAYLAND_DEBUG tracing and documented in
docs/IMPLEMENTATION_STATUS.md rather than worked around blind.
All verified live in an isolated QEMU VM: X11 backend shows two
decorated, correctly-tiled xterms with real title text; the DRM/udev
Wayland backend opens the GPU, initializes input, and scans out a
rendered frame via KMS page-flip.
Diffstat (limited to 'crates/wayland/src/lib.rs')
| -rw-r--r-- | crates/wayland/src/lib.rs | 225 |
1 files changed, 171 insertions, 54 deletions
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs index 4913d58..b2fa9dc 100644 --- a/crates/wayland/src/lib.rs +++ b/crates/wayland/src/lib.rs @@ -18,29 +18,40 @@ //! X windows: through `srdwm_core::WindowManager`, so layout, smart //! placement, and drag/resize hit-testing are the *same* code path as X11 //! (`srdwm_core::window::ResizeEdge::hit_test`), not a reimplementation. -//! - Decorations are drawn as a solid-color titlebar band (no text - font -//! rasterization is a real chunk of additional work, not something to -//! fake here) using smithay's `SolidColorRenderElement`. -//! - Global keybindings are intercepted using a simple heuristic: any key -//! press with the Super/Mod4 modifier held is treated as WM-exclusive and -//! not forwarded to the focused client; everything else is forwarded. -//! Real WMs do something similar in practice (Super is rarely used by -//! applications); a more precise design would thread the config's actual -//! bound-key set into the platform layer, which is left as a TODO. +//! - Decorations are a titlebar band rendered in software (`decoration.rs`: +//! solid background plus the actual window title, rasterized via +//! `fontdue` against whatever monospace font is found on the system) and +//! uploaded per-frame through smithay's `MemoryRenderBuffer`, the same +//! band geometry and button hit-testing as the X11 backend's drawn +//! titlebar. +//! - Global keybindings are matched precisely: `WaylandPlatform::connect` +//! takes the config's actual bound-key combo strings (the same +//! `"Mod4+Shift+Return"` format `srd.bind` uses and the X11 backend grabs +//! via `XGrabKey`), and every keypress is translated to that same combo +//! string (via the keysym table shared with X11, `srdwm_core::keysyms`) +//! and checked against the set. Only a match is withheld from the focused +//! client; everything else is forwarded, mirroring X11's grab-specific-keys +//! behavior instead of the coarser "any Super-held key is ours" heuristic +//! an earlier pass used. //! - xdg-decoration is forced to server-side mode (`Mode::ServerSide`) so //! well-behaved clients don't also draw their own client-side titlebar. +mod decoration; +mod udev; +mod xwayland; + use std::cell::RefCell; -use std::collections::HashMap; +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::solid::{SolidColorBuffer, SolidColorRenderElement}; +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}; @@ -97,12 +108,29 @@ struct CompState { wm: Rc<RefCell<WindowManager>>, surface_to_id: HashMap<WlSurface, WindowId>, id_to_window: HashMap<WindowId, DWindow>, - decorations: HashMap<WindowId, SolidColorBuffer>, + decorations: HashMap<WindowId, MemoryRenderBuffer>, pending: Rc<RefCell<Vec<CoreEvent>>>, - pointer_focus_super_held: bool, + 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(); @@ -126,10 +154,28 @@ impl CompState { 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.decorations.insert(id, SolidColorBuffer::new((geom.width as i32, TITLEBAR_HEIGHT as i32), [0.18, 0.204, 0.251, 1.0])); + 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) { @@ -151,8 +197,8 @@ impl CompState { top.send_configure(); } } - if let Some(deco) = self.decorations.get_mut(&id) { - deco.resize((geom.width as i32, TITLEBAR_HEIGHT as i32)); + if self.decorations.contains_key(&id) { + self.redraw_decoration_buffer(id); } } } @@ -169,11 +215,24 @@ impl CompositorHandler for CompState { } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { - &client.get_data::<ClientState>().unwrap().compositor_state + // 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(); @@ -265,7 +324,11 @@ pub struct WaylandPlatform { } impl WaylandPlatform { - pub fn connect(wm: Rc<RefCell<WindowManager>>) -> PlatformResult<Self> { + /// `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(); @@ -310,8 +373,13 @@ impl WaylandPlatform { id_to_window: HashMap::new(), decorations: HashMap::new(), pending: pending.clone(), - pointer_focus_super_held: false, + 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)?; @@ -350,15 +418,18 @@ impl WaylandPlatform { let size = self.backend.window_size(); self.output.change_current_state(Some(OutputMode { size, refresh: 60_000 }), None, None, None); - let mut custom_elements: Vec<SolidColorRenderElement> = Vec::new(); + 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() { - if let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) { - custom_elements.push(SolidColorRenderElement::from_buffer(deco, (geom.x, geom.y), 1.0, 1.0, Kind::Unspecified)); + 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}"), } } - let age = self.backend.buffer_age().unwrap_or(0); - let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?; render_output( &self.output, renderer, @@ -381,28 +452,7 @@ impl WaylandPlatform { 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 }) => { - 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 mut super_now = state.pointer_focus_super_held; - keyboard.input::<(), _>(state, keycode, key_state, serial, time, |_, mods, _handle| { - super_now = mods.logo; - FilterResult::Forward - }); - state.pointer_focus_super_held = super_now; - - if super_now && key_state == BackendKeyState::Pressed { - if let Some(name) = keysym_name_for(&keyboard, keycode) { - state.pending.borrow_mut().push(CoreEvent::KeyPress { key_name: name, modifiers: Modifiers::SUPER }); - } - } - // Not a WM shortcut: let the focused client see it (already - // forwarded by the FilterResult::Forward above). - } + 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); @@ -419,14 +469,60 @@ fn handle_winit_event(state: &mut CompState, output: &Output, event: WinitEvent, } } -fn keysym_name_for(keyboard: &smithay::input::keyboard::KeyboardHandle<CompState>, keycode: smithay::backend::input::Keycode) -> Option<String> { - let _ = keyboard; - let _ = keycode; - // TODO: translate via xkbcommon keysym -> name; the X11 backend's - // `keysyms` table isn't reachable from here without duplicating it. - // Global Wayland keybindings beyond the raw Super-modifier gate are not - // wired up yet - see docs/IMPLEMENTATION_STATUS.md. - None +/// 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> { @@ -580,6 +676,10 @@ impl Platform for WaylandPlatform { } 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(()) } @@ -592,3 +692,20 @@ impl Platform for WaylandPlatform { 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 +/// nested-vs-native. Falls back to winit if udev initialization fails for +/// any reason (no seat access, no DRM device, ...), logging why rather than +/// failing outright. +pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Box<dyn Platform>> { + let no_host_display = std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none(); + if no_host_display { + match udev::UdevPlatform::connect(wm.clone(), bound_keys) { + Ok(platform) => return Ok(Box::new(platform)), + Err(e) => log::warn!("udev/DRM backend unavailable ({e}); falling back to nested winit backend"), + } + } + Ok(Box::new(WaylandPlatform::connect(wm, bound_keys)?)) +} |