diff options
| author | srdusr <[email protected]> | 2024-04-08 23:54:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-04-08 23:54:00 +0200 |
| commit | 709333908d5b7d3157165d1811c8bc1795ab0028 (patch) | |
| tree | 92d5616519f63c1e4743add0294ce552e239681c /crates/wayland/src/lib.rs | |
| parent | 592bd3716c0aa5a47a5eb43f45f55844de9db382 (diff) | |
| download | srdwm-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/wayland/src/lib.rs')
| -rw-r--r-- | crates/wayland/src/lib.rs | 702 |
1 files changed, 12 insertions, 690 deletions
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 |