diff options
| author | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
| commit | 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch) | |
| tree | 7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/protocols.rs | |
| parent | 413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff) | |
| download | srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip | |
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has
diverged with its own separate fixes today. Nothing here is reviewed
or curated yet - this exists purely so none of this work can be lost
to a git operation, disk issue, or worktree cleanup while that
reconciliation happens.
Diffstat (limited to 'crates/wayland/src/protocols.rs')
| -rw-r--r-- | crates/wayland/src/protocols.rs | 819 |
1 files changed, 24 insertions, 795 deletions
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs index 391f665..664e889 100644 --- a/crates/wayland/src/protocols.rs +++ b/crates/wayland/src/protocols.rs @@ -6,806 +6,34 @@ //! 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. +//! +//! Split one file per protocol handler, matching niri's own convention (see +//! docs/TODO.md's "module splits" entry) - [`buffer`] groups `ShmHandler`/ +//! `BufferHandler`/`DmabufHandler` together since none has more than a +//! handful of lines, and [`misc`] groups the three purely-default-impl stub +//! handlers (`OutputHandler`/`TabletSeatHandler`/`FractionalScaleHandler`) +//! for the same reason; every other module is exactly one handler. + +mod buffer; +mod compositor; +mod idle; +mod input_method; +mod layer_shell; +mod misc; +mod seat; +mod selection; +mod xdg_activation; +mod xdg_decoration; +mod xdg_shell; -use smithay::desktop::{find_popup_root_surface, layer_map_for_output, LayerSurface as DesktopLayerSurface, PopupKeyboardGrab, PopupKind, PopupPointerGrab}; -use smithay::input::pointer::{CursorImageStatus, Focus}; -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_protocols::xdg::shell::server::xdg_toplevel; -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::reexports::wayland_server::Resource; -use smithay::backend::allocator::dmabuf::Dmabuf; -use smithay::backend::renderer::ImportDma; -use smithay::utils::Serial; -use smithay::wayland::buffer::BufferHandler; -use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState}; -use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; -use smithay::wayland::xdg_activation::{XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData}; -use smithay::wayland::input_method::PopupSurface as ImePopupSurface; -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::compositor::{add_pre_commit_hook, with_states}; -use smithay::wayland::selection::SelectionHandler; -use smithay::wayland::shell::wlr_layer::{ - Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, 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::wayland::tablet_manager::TabletSeatHandler; use smithay::{ delegate_compositor, delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf, - delegate_layer_shell, delegate_output, delegate_primary_selection, delegate_seat, delegate_session_lock, - delegate_shm, delegate_xdg_activation, delegate_xdg_decoration, delegate_xdg_shell, - delegate_input_method_manager, delegate_text_input_manager, + delegate_input_method_manager, delegate_layer_shell, delegate_output, delegate_primary_selection, delegate_seat, + delegate_session_lock, delegate_shm, delegate_text_input_manager, delegate_virtual_keyboard_manager, + delegate_xdg_activation, 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 - } - - /// Workaround for a real smithay bug (see docs/PANEL_SUPPORT_TODO.md and - /// `layer_destroyed` below): destroying a `zwlr_layer_surface_v1` role - /// resets the surface's `LayerSurfaceCachedState` to - /// `Default::default()` (size 0x0, no anchor) rather than removing it, - /// but the pre-commit hook smithay itself registers at - /// `get_layer_surface` time keeps validating that state against every - /// future commit regardless of whether the role still exists -- - /// tripping its own `width/height 0 requested without ... anchors` - /// check and posting `invalid_size`, which kills the client's whole - /// connection over what is protocol-legal (committing a now-roleless - /// surface). - /// - /// Fixed by registering our own pre-commit hook here, in `new_surface` - /// - called at `wl_compositor.create_surface`, strictly before any - /// later `get_layer_surface` on the same surface could register - /// smithay's own hook. Hooks run in registration order (`tree.rs`: - /// `pre_commit_hooks` is a plain `Vec`, pushed and iterated in order), - /// so ours always runs first and can neutralize the stale reset state - /// before smithay's hook ever inspects it. This depends on that - /// ordering guarantee holding in future smithay versions - it isn't - /// documented as an API contract, just an implementation detail - /// confirmed against 0.7.0's source - so re-check this file against - /// whatever smithay version replaces it. - /// - /// Cost: one closure registered per `wl_surface` (not just layer - /// surfaces, since we don't know in advance which ones will become - /// one), each a no-op unless that exact surface is in - /// `dead_layer_surfaces`. - fn new_surface(&mut self, surface: &WlSurface) { - add_pre_commit_hook::<CompState, _>(surface, |state, _dh, surface| { - if !state.dead_layer_surfaces.contains(surface) { - return; - } - with_states(surface, |states| { - let mut cached = states.cached_state.get::<LayerSurfaceCachedState>(); - let pending = cached.pending(); - if pending.size.w == 0 && !pending.anchor.anchored_horizontally() { - pending.size.w = 1; - } - if pending.size.h == 0 && !pending.anchor.anchored_vertically() { - pending.size.h = 1; - } - }); - }); - } - - 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(); - } - // See `content_epoch`'s doc comment: this is the only per-commit - // signal the udev backend's rounded-corner mask cache has to - // invalidate itself, since content can change every frame, - // independent of the geometry-driven points `redraw_decoration_ - // buffer` already runs at. - *self.content_epoch.entry(id).or_insert(0) += 1; - crate::state::sync_toplevel_metadata(self, id, surface); - } - // Before `ensure_layer_initial_configure`: if this commit just - // hid or re-showed a layer surface, `sync_layer_visibility` needs - // to unmap/re-map it first, so the lookup that function does via - // `layer_for_surface` sees the corrected state rather than acting - // on stale membership in `LayerMap`'s own list. - self.sync_layer_visibility(surface); - self.ensure_layer_initial_configure(surface); - // Advances a just-created popup from unmapped to mapped (needed for - // `PopupManager::popups_for_surface`, which `popup_render_elements` - // reads at render time) and prunes dead ones. Cheap and only does - // real work on a popup-role surface, so doing it on every commit - // rather than throttling is not worth the extra bookkeeping. - self.popups.commit(surface); - self.popups.cleanup(); - } -} - -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); - } - - /// `move_request`/`resize_request` were also still smithay's default - /// no-op implementations - a much larger gap than the five below: - /// this is *how a client-side-decorated window gets dragged or resized - /// by its own titlebar/edges at all*. A window we draw our own - /// decoration for never needed this (`TitlebarHit::Drag`/`Resize` in - /// `input.rs` detect the click directly, since we own those pixels), - /// but a window that negotiated client-side decoration and draws its - /// own titlebar - Firefox, and most GTK4 apps by default - handles - /// the click itself and then asks the compositor to actually perform - /// the move/resize via exactly these two requests. Left unimplemented, - /// dragging or resizing any such window by its own chrome did - /// nothing at all - the only way to reposition it was the - /// modifier+drag-anywhere gesture (`bindm`), which most users have no - /// reason to know exists and doesn't work for resize-from-a-specific- - /// edge at all. Reuses the exact same `WindowManager::start_drag`/ - /// `start_resize` the pointer-driven titlebar handlers call -- - /// `handle_pointer_position`/`handle_pointer_button` already drive any - /// in-progress drag/resize to completion on subsequent motion/release - /// regardless of what started it, so no smithay pointer grab is - /// needed here at all, just the same start call from a different - /// trigger. - fn move_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial) { - // Temporary: added to trace a live report that dragging a CSD - // window (Firefox) by its own tab strip/header bar does nothing -- - // this is the only way to tell "the client never sent xdg_toplevel - // ::move at all" apart from "it sent it and something downstream - // of here didn't follow through." Remove once that's settled. - match self.surface_to_id.get(surface.wl_surface()) { - Some(&id) => { - let pos = crate::input::last_pointer_pos(self); - log::info!("move_request: window {id:?} at pointer {pos:?}"); - self.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32); - } - None => log::warn!("move_request: surface has no tracked window id"), - } - } - - fn resize_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial, edges: xdg_toplevel::ResizeEdge) { - let Some(edge) = (match edges { - xdg_toplevel::ResizeEdge::Top => Some(srdwm_core::ResizeEdge::Top), - xdg_toplevel::ResizeEdge::Bottom => Some(srdwm_core::ResizeEdge::Bottom), - xdg_toplevel::ResizeEdge::Left => Some(srdwm_core::ResizeEdge::Left), - xdg_toplevel::ResizeEdge::Right => Some(srdwm_core::ResizeEdge::Right), - xdg_toplevel::ResizeEdge::TopLeft => Some(srdwm_core::ResizeEdge::TopLeft), - xdg_toplevel::ResizeEdge::TopRight => Some(srdwm_core::ResizeEdge::TopRight), - xdg_toplevel::ResizeEdge::BottomLeft => Some(srdwm_core::ResizeEdge::BottomLeft), - xdg_toplevel::ResizeEdge::BottomRight => Some(srdwm_core::ResizeEdge::BottomRight), - // `None` is a valid protocol value (the client leaves the edge - // unspecified) but `WindowManager::start_resize` needs one -- - // there's nothing sensible to default it to that wouldn't be a - // guess, so this is a no-op rather than picking one. - _ => None, - }) else { - return; - }; - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - let pos = crate::input::last_pointer_pos(self); - self.wm.borrow_mut().start_resize(id, edge, pos.x as i32, pos.y as i32); - } - } - - /// `maximize_request`/`unmaximize_request`/`fullscreen_request`/ - /// `unfullscreen_request`/`minimize_request` were all still smithay's - /// default no-op (or configure-only) implementations - found - /// investigating the `toggle_fullscreen` decoration bug above, by - /// checking what else routes through the same `WindowManager` calls - /// the titlebar-button click handlers in `input.rs` already use. - /// These five are the *client-initiated* equivalent of those clicks: a - /// client's own window-menu "Maximize", pressing F11, an HTML5 video - /// going fullscreen, or (for a client that negotiated client-side - /// decoration and draws its own titlebar, like Firefox) that titlebar's - /// own maximize button - all ask the compositor to actually perform - /// the state change via these requests rather than the compositor - /// noticing on its own. Left unimplemented, every one of them was a - /// silent no-op: the client's button did nothing, with no error and - /// nothing to suggest why, from any app that relies on this instead of - /// (or in addition to) a compositor-side keybinding. - fn maximize_request(&mut self, surface: ToplevelSurface) { - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - if !self.wm.borrow().window(id).is_some_and(|w| w.maximized) { - self.wm.borrow_mut().toggle_maximize(id); - self.sync_geometry(id); - crate::foreign_toplevel::send_state(self, id); - } - } - surface.send_configure(); - } - - fn unmaximize_request(&mut self, surface: ToplevelSurface) { - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - if self.wm.borrow().window(id).is_some_and(|w| w.maximized) { - self.wm.borrow_mut().toggle_maximize(id); - self.sync_geometry(id); - crate::foreign_toplevel::send_state(self, id); - } - } - surface.send_configure(); - } - - /// `_output` (the client's requested target output) is ignored -- - /// single-seat, and every other fullscreen entry point (the titlebar - /// button, `srd.window.fullscreen()`) already fullscreens on whatever - /// monitor the window is already on, so this matches that instead of - /// introducing an output-aware fullscreen path only this one request - /// would use. - fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) { - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - if !self.wm.borrow().is_fullscreen(id) { - // `redraw_decoration_buffer` first, same reason - // `set_decorated_from_mode` calls it before `sync_geometry`: - // fullscreen also flips `Window.decorated`, and dropping - // the decoration needs the buffer actually removed, not - // just left stale for `sync_geometry`'s own resize-only - // redraw check to skip. - self.wm.borrow_mut().toggle_fullscreen(id); - self.redraw_decoration_buffer(id); - self.sync_geometry(id); - crate::foreign_toplevel::send_state(self, id); - } - } - surface.send_configure(); - } - - fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - if self.wm.borrow().is_fullscreen(id) { - self.wm.borrow_mut().toggle_fullscreen(id); - self.redraw_decoration_buffer(id); - self.sync_geometry(id); - crate::foreign_toplevel::send_state(self, id); - } - } - surface.send_configure(); - } - - /// No `send_configure` here, matching the pointer-driven - /// `TitlebarHit::Minimize` handler in `input.rs`: minimizing doesn't - /// change the window's own size, only whether it's currently shown, so - /// there's nothing new to tell the client about its own geometry. - fn minimize_request(&mut self, surface: ToplevelSurface) { - if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) { - self.wm.borrow_mut().minimize_window(id); - crate::foreign_toplevel::send_state(self, id); - } - } - - /// Was a bare no-op - no `send_configure` at all. Per xdg-shell, - /// `xdg_surface.configure` is required before a popup's first commit; - /// real toolkits (confirmed live: GTK4's Wayland backend) block that - /// commit in a synchronous roundtrip waiting for it, so every popup - /// hung its client forever. GTK4 implements tooltips *and* - /// `Gtk.Popover` as `xdg_popup`, so this fired on hovering almost any - /// widget with a tooltip - confirmed by a peer session's gdb backtrace - /// (blocked in `wl_display_dispatch_queue` under `gtk_widget_show`) - /// after AGS wedged. - /// - /// Geometry is `positioner.get_geometry()` un-constrained - no - /// on-screen clamping yet (`PositionerState::get_unconstrained_geometry` - /// needs a target rect in the parent's surface-local space, which is a - /// real follow-up, not this fix); an occasional popup placed near a - /// screen edge may render partly off it, which is cosmetic, not a hang. - fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) { - surface.with_pending_state(|state| { - state.geometry = positioner.get_geometry(); - state.positioner = positioner; - }); - if surface.send_configure().is_err() { - return; - } - let _ = self.popups.track_popup(smithay::desktop::PopupKind::Xdg(surface)); - } - - /// Implicit grab + dismiss-on-outside-click. Previously believed - /// blocked on `CompState`'s `SeatHandler` associated types not - /// satisfying `PopupManager::grab_popup`'s `WaylandFocus + - /// From<PopupKind>` bound - rechecked while implementing - /// `move_request`/`resize_request` (same trait, adjacent methods) and - /// it turns out they already do: `KeyboardFocus`/`PointerFocus` are - /// both plain `WlSurface`, smithay provides `impl From<PopupKind> for - /// WlSurface` itself, and `WlSurface: From<WlSurface>` trivially. No - /// blocker ever existed by the time of this pass; the bound just - /// hadn't been rechecked since being noted as unmet. - /// - /// `self.seat.clone()` rather than resolving `_seat` (the client's - /// `wl_seat` resource) via `Seat::from_resource` - this compositor - /// only ever has the one seat, matching how `move_request`/ - /// `resize_request` already ignore the same parameter. - fn grab(&mut self, surface: PopupSurface, _seat: wl_seat::WlSeat, serial: Serial) { - let popup = PopupKind::Xdg(surface); - let Ok(root) = find_popup_root_surface(&popup) else { - log::warn!("POPUP-GRAB-DIAG find_popup_root_surface failed"); - return; - }; - let seat = self.seat.clone(); - let grab = match self.popups.grab_popup(root, popup, &seat, serial) { - Ok(g) => g, - Err(e) => { - log::warn!("POPUP-GRAB-DIAG grab_popup failed: {e:?}"); - return; - } - }; - log::warn!("POPUP-GRAB-DIAG grab established, has_pointer={} has_keyboard={}", seat.get_pointer().is_some(), seat.get_keyboard().is_some()); - if let Some(keyboard) = seat.get_keyboard() { - keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); - } - if let Some(pointer) = seat.get_pointer() { - pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); - } - } - - fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { - surface.with_pending_state(|state| { - state.geometry = positioner.get_geometry(); - state.positioner = positioner; - }); - surface.send_repositioned(token); - } - - fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { - self.remove_window(surface.wl_surface()); - } -} - -impl XdgDecorationHandler for CompState { - /// Offers whichever mode `theme.decorations.default_mode`/`srd set - /// decoration_mode` currently prefers - a client with a real opinion - /// of its own still overrides this via `request_mode` below regardless - /// of what's offered here; this only decides what a client with *no* - /// preference ends up with. See `srdwm_core::ThemeConfig:: - /// default_decorated`'s own doc comment for why this is configurable - /// rather than hardcoded to one mode. - fn new_decoration(&mut self, toplevel: ToplevelSurface) { - let offer = if self.wm.borrow().theme.default_decorated { DecorationMode::ServerSide } else { DecorationMode::ClientSide }; - toplevel.with_pending_state(|state| { - state.decoration_mode = Some(offer); - }); - } - - /// Honors whichever mode the client actually asked for, rather than - /// always forcing server-side - and mirrors the result into our own - /// `Window.decorated`, so a client drawing its own titlebar doesn't - /// *also* get one drawn on top of it by us. - /// - /// Always forcing `ServerSide` (what this used to do) is why some - /// clients ended up with two sets of window buttons: Firefox requests - /// client-side decoration when its own "use system titlebar" setting - /// is off, and draws its own close/minimize/maximize row regardless of - /// what the compositor grants - so forcing server-side just added - /// srdwm's row on top of the one Firefox was drawing anyway, instead - /// of preventing it. Respecting the request means srdwm steps out of - /// the way for exactly those clients, while everything that accepts - /// (or has no preference and gets offered) server-side still gets our - /// titlebar as before. - fn request_mode(&mut self, toplevel: ToplevelSurface, mode: DecorationMode) { - toplevel.with_pending_state(|state| { - state.decoration_mode = Some(mode); - }); - toplevel.send_configure(); - self.set_decorated_from_mode(toplevel.wl_surface(), mode == DecorationMode::ServerSide); - } - - /// The client dropped its decoration-mode preference. `new_decoration` - /// already offers the configured default as the mode the next - /// configure will carry, so mirror that same default here rather than - /// leaving whatever mode was negotiated before this - otherwise a - /// client that requests one mode, then later unsets it expecting the - /// default back, would stay stuck in that mode forever. - fn unset_mode(&mut self, toplevel: ToplevelSurface) { - let default_decorated = self.wm.borrow().theme.default_decorated; - let mode = if default_decorated { DecorationMode::ServerSide } else { DecorationMode::ClientSide }; - toplevel.with_pending_state(|state| { - state.decoration_mode = Some(mode); - }); - toplevel.send_configure(); - self.set_decorated_from_mode(toplevel.wl_surface(), default_decorated); - } -} - -impl ShmHandler for CompState { - fn shm_state(&self) -> &ShmState { - &self.shm_state - } -} - -impl BufferHandler for CompState { - fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {} -} - -impl DmabufHandler for CompState { - fn dmabuf_state(&mut self) -> &mut DmabufState { - &mut self.dmabuf_state - } - - /// Validates a client's dmabuf by actually importing it wherever a - /// renderer is reachable from here, so a genuinely bad buffer (wrong - /// modifier, format the renderer doesn't support) gets the protocol - /// error instead of silently rendering garbage later. - /// - /// That's only the udev backend: its `PixmanRenderer` lives inside - /// `self.udev` (`UdevState`), a field of this same struct. - /// `PixmanRenderer` supports dmabuf import despite being a pure - /// software renderer - `dmabuf_formats()` only advertises the Linear - /// modifier, which it imports by mmap'ing the buffer and reading it - /// directly as pixels, no GPU involved. This is what actually answers - /// `docs/PANEL_SUPPORT_TODO.md`'s P0.3: GTK4 allocates via its own - /// EGL/gbm path against the real DRM render node (untouched by this - /// compositor either way) and hands the result here as a Linear- - /// modifier dmabuf, which pixman can read straight off. - /// - /// The winit (nested/dev) backend's `GlesRenderer` lives on - /// `WaylandPlatform`, a sibling of `CompState`, not reachable from a - /// method on `CompState` itself. Accepted there without eager - /// validation - the buffer still gets imported the same way every - /// other buffer type already is, lazily, the first time it is actually - /// rendered via `render_elements_from_surface_tree`. Real hardware, - /// where P0.3 actually bites, always goes through the udev path. - fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { - match self.udev.as_mut() { - Some(udev) => match udev.renderer.import_dmabuf(&dmabuf, None) { - Ok(_) => { - let _ = notifier.successful::<CompState>(); - } - Err(e) => { - log::warn!("udev: rejecting dmabuf import: {e}"); - notifier.failed(); - } - }, - None => { - let _ = notifier.successful::<CompState>(); - } - } - } -} - -impl XdgActivationHandler for CompState { - fn activation_state(&mut self) -> &mut XdgActivationState { - &mut self.xdg_activation_state - } - - /// A launcher spawns an app after first getting a token - /// (`get_activation_token`) and handing it to the new process (usually - /// via `XDG_ACTIVATION_TOKEN`); the app's own first window then - /// presents that same token back here via `activate`, asking to be - /// raised. Without this, that request was silently ignored - the new - /// window opened and just sat there unfocused behind everything, - /// exactly the gap `docs/PANEL_SUPPORT_TODO.md`'s P1 flagged. - /// - /// No token bookkeeping of our own: `token_created`'s default already - /// accepts every token (fine for a single-user session with no - /// cross-client trust boundary to enforce), so all that's left is - /// mapping the activating `surface` to a `WindowId` and reusing the - /// exact same `focus_window` path a dock's "activate" request already - /// goes through (`foreign_toplevel.rs`). If the surface isn't tracked - /// yet - the activation raced ahead of this window's own mapping -- - /// there is nothing to focus yet, so this is a no-op rather than an - /// error; the protocol doesn't require honoring every activation. - fn request_activation(&mut self, _token: XdgActivationToken, _token_data: XdgActivationTokenData, surface: WlSurface) { - if let Some(&id) = self.surface_to_id.get(&surface) { - crate::input::focus_window(self, id); - } - } -} - -/// `zwp_text_input_manager_v3` + `zwp_input_method_manager_v2`: lets a real -/// input method (fcitx5, ibus, any CJK/dead-key/emoji-picker IME) attach to -/// whichever surface has keyboard focus and draw its own candidate/ -/// composition popup. Without these two globals a client that only speaks -/// text-input (most modern toolkits do, GTK4/Qt6 included) has no way to -/// tell the compositor "I have an editable text field, here is its cursor -/// rectangle" - every desktop app's search box, address bar, and chat -/// input silently loses IME support, not just an edge case. -/// -/// Focus tracking needs *no* wiring here at all: `CompState::KeyboardFocus` -/// is a plain `WlSurface`, and smithay's own blanket `impl KeyboardTarget -/// for WlSurface` already calls `seat.text_input().set_focus/.enter()/ -/// .leave()` and `seat.input_method().activate_input_method()/ -/// deactivate_input_method()` from inside `enter`/`leave` - which -/// `set_keyboard_focus`'s existing `keyboard.set_focus(...)` call already -/// triggers on every real focus change. The only things actually missing -/// were the two manager globals and this handler for the popup surface -/// lifecycle. -impl smithay::wayland::input_method::InputMethodHandler for CompState { - /// A candidate/composition window (an emoji picker, a CJK candidate - /// list) just opened. Tracked as a regular [`PopupKind::InputMethod`] - /// in the same [`PopupManager`](smithay::desktop::PopupManager) that - /// already owns every `xdg_popup` - `elements::popup_render_elements` - /// renders both kinds identically, so no separate render path is - /// needed for this to actually become visible. - fn new_popup(&mut self, surface: ImePopupSurface) { - if let Err(e) = self.popups.track_popup(PopupKind::from(surface)) { - log::warn!("input-method: failed to track popup: {e}"); - } - } - - fn dismiss_popup(&mut self, surface: ImePopupSurface) { - if let Some(parent) = surface.get_parent().map(|p| p.surface.clone()) { - let _ = smithay::desktop::PopupManager::dismiss_popup(&parent, &PopupKind::from(surface)); - } - } - - /// The IME moved its own popup (e.g. following the text cursor as the - /// user types) - `PopupSurface::location()` already reflects the new - /// position; nothing else needs updating on this side, matching every - /// other smithay-based compositor's own no-op here. - fn popup_repositioned(&mut self, _surface: ImePopupSurface) {} - - /// Where the IME should anchor its popup, in the parent surface's own - /// output-independent (logical, window-relative-origin) space - same - /// geometry `elements::popup_targets` already computes for xdg popups, - /// reused here rather than duplicated. A window not yet tracked (the - /// activation raced ahead of its own mapping) gets a default/zero rect, - /// same "no-op rather than an error" stance as `request_activation` - /// above. - fn parent_geometry(&self, parent: &WlSurface) -> smithay::utils::Rectangle<i32, smithay::utils::Logical> { - let Some(&id) = self.surface_to_id.get(parent) else { - return smithay::utils::Rectangle::default(); - }; - let wm = self.wm.borrow(); - let Some(w) = wm.window(id) else { - return smithay::utils::Rectangle::default(); - }; - let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 }; - smithay::utils::Rectangle::new((w.geometry.x, w.geometry.y + band).into(), (w.geometry.width as i32, w.geometry.height as i32).into()) - } -} - -impl smithay::wayland::output::OutputHandler for CompState {} - -/// `wp_cursor_shape_v1`: lets a client ask for a *named* cursor (text, -/// grab, resize edges, ...) instead of rendering and attaching its own -/// surface. Its requests route straight into `SeatHandler::cursor_image` -/// below, same as a client-drawn cursor surface does - no extra state on -/// our side. Without this global at all, a client that only speaks this -/// (increasingly the norm - recent GTK4/Firefox use it for most cursor -/// changes) has no way to tell us the pointer should look like anything -/// but whatever it last was, which reads as the cursor going stale, wrong, -/// or simply disappearing depending on what was showing when the client -/// gave up trying. -/// -/// `TabletSeatHandler` is a supertrait bound of this protocol's `Dispatch` -/// impl (cursor-shape covers tablet tools too); srdwm has no tablet -/// support to speak of, so every method is left at its no-op default. -impl TabletSeatHandler for CompState {} - -/// Fractional scaling. srdwm runs every output at scale 1, so there is -/// nothing to compute - but the global has to exist, because clients that -/// use it (notably wallpaper daemons) treat it as mandatory. -impl smithay::wayland::fractional_scale::FractionalScaleHandler 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>) {} - /// Clients set their own cursor (an I-beam over text, a hand over a - /// link). Recorded here and drawn by the render paths - on a bare TTY - /// nothing else would draw it. See `cursor.rs`. - fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) { - self.cursor_status = image; - } -} - -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) { - // Logged before anything else can early-return or panic: the - // question this answers (see docs/PANEL_SUPPORT_TODO.md) is - // whether this handler is reached AT ALL for a later - // `get_layer_surface` request in a create -> commit -> destroy -> - // commit-again -> create sequence, or whether the client's - // dispatch is already dead by then and this never runs. - log::debug!("layer-shell: new_layer_surface entered, surface={:?} namespace={namespace:?} output_named={}", surface.wl_surface().id(), wl_output.is_some()); - // 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; - }; - // Paired with the debug log in `ensure_layer_initial_configure`'s - // early return - see docs/PANEL_SUPPORT_TODO.md's P0. This is the - // other half of "did map_layer actually succeed, and on which - // output": logged unconditionally (not just on the error paths - // that already existed) so a real reproduction shows both sides of - // the handoff instead of just the failure. - let surface_id = surface.wl_surface().id(); - let layer_surface = DesktopLayerSurface::new(surface, namespace); - let result = layer_map_for_output(&output).map_layer(&layer_surface); - match &result { - Ok(()) => log::debug!("layer-shell: mapped surface {surface_id:?} onto output {}", output.name()), - Err(e) => log::warn!("wayland: failed to map layer surface {surface_id:?}: {e}"), - } - } - - fn layer_destroyed(&mut self, surface: WlrLayerSurface) { - // See the matching top-of-function log in `new_layer_surface`. - log::debug!("layer-shell: layer_destroyed entered, surface={:?}", surface.wl_surface().id()); - // Marks this surface for the pre-commit-hook workaround in - // `new_surface` - see that function's doc comment for the bug - // this exists to route around. - self.dead_layer_surfaces.insert(surface.wl_surface().clone()); - // GTK (confirmed live via an AGS peer session's WAYLAND_DEBUG trace) - // reuses the same `wl_surface` for the next `get_layer_surface` role - // rather than creating a fresh one - so without this, a "shown at - // least once" flag from *this* role would leak onto the next one - // and make `sync_layer_visibility` treat that new role's own - // ack-configure commit as eligible to hide again, the same bug - // `layer_surfaces_shown_once` exists to prevent, just reintroduced - // for exactly the reused-surface case that matters here. - self.layer_surfaces_shown_once.remove(surface.wl_surface()); - // 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 { - // Same zone-change recompute `ensure_layer_initial_configure` - // already does on every commit that changes a layer's - // exclusive zone (state/layers.rs) - but this is the *only* place - // that ever runs for a surface that goes away without one - // last commit. `unmap_layer` alone doesn't trigger it: - // reported live (by the AGS peer session) as a bar unmapping - // for fullscreen yet `srd monitors` still reporting the - // bar's old reserved_top for as long as fullscreen lasted -- - // harmless there only because fullscreen targets - // `full_geometry`, which ignores the reservation anyway, but - // wrong for anything that reads `usable`/`geometry` while a - // bar is unmapped without exiting cleanly (a crash, not just - // AGS's cooperative fullscreen hide). - let zone_before = map.non_exclusive_zone(); - map.unmap_layer(&layer); - let zone_after = map.non_exclusive_zone(); - if zone_after != zone_before { - self.pending.borrow_mut().push(srdwm_core::Event::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0)))); - } - 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. - // - // `sync_keyboard_focus`, not a bare `set_keyboard_focus(None)`: an - // `OnDemand` layer surface (a launcher/quicksettings/datemenu - // popup, per `wlr-layer-shell`) claiming focus on click - // (`input.rs`'s `on_demand` branch) goes straight through - // `set_keyboard_focus` without ever touching `WindowManager:: - // focused` - core has no concept of a layer surface to focus, so - // it still correctly points at whatever real toplevel was focused - // before the popup opened. Hardcoding `None` here threw that away - // regardless, leaving nothing focused until the user happened to - // click a window again - reported live (an AGS peer session's - // user) as "focus never returns after using the bar". `sync_ - // keyboard_focus` reads that still-correct core state and restores - // real Wayland focus to it, falling through to `None` only if core - // genuinely has nothing focused either. - if self.seat.get_keyboard().and_then(|k| k.current_focus()).as_ref() == Some(surface.wl_surface()) { - crate::input::sync_keyboard_focus(self); - } - } -} - -/// 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 - } -} - -/// `ext_idle_notify_v1`. All the real logic (per-notification timers, -/// resetting them on activity, honouring inhibition) already lives in -/// smithay's own `IdleNotifierState` - this is just the getter it needs. -/// See `input.rs`'s `notify_idle_activity` for the other half: nothing -/// calls `notify_activity` on its own, that has to happen from every real -/// input path. -impl smithay::wayland::idle_notify::IdleNotifierHandler for CompState { - fn idle_notifier_state(&mut self) -> &mut smithay::wayland::idle_notify::IdleNotifierState<Self> { - &mut self.idle_notifier_state - } -} - -/// `zwp_idle_inhibit_manager_v1`. A video player (or anything else that -/// wants the screen to stay on/unlocked while it runs) creates one of -/// these tied to its own surface; as long as at least one is alive, -/// `IdleNotifierState::set_is_inhibited` stops idle timers from firing at -/// all - see `idle_inhibiting_surfaces`'s doc comment on `CompState` for -/// the one simplification (not workspace-visibility-aware) this takes. -impl smithay::wayland::idle_inhibit::IdleInhibitHandler for CompState { - fn inhibit(&mut self, surface: WlSurface) { - self.idle_inhibiting_surfaces.push(surface); - self.idle_notifier_state.set_is_inhibited(true); - } - - fn uninhibit(&mut self, surface: WlSurface) { - self.idle_inhibiting_surfaces.retain(|s| s != &surface); - self.idle_notifier_state.set_is_inhibited(!self.idle_inhibiting_surfaces.is_empty()); - } -} +use crate::state::CompState; delegate_compositor!(CompState); delegate_xdg_shell!(CompState); @@ -815,6 +43,7 @@ delegate_dmabuf!(CompState); delegate_xdg_activation!(CompState); delegate_text_input_manager!(CompState); delegate_input_method_manager!(CompState); +delegate_virtual_keyboard_manager!(CompState); delegate_seat!(CompState); delegate_output!(CompState); delegate_layer_shell!(CompState); |