srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/protocols.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-30 16:10:00 +0200
committersrdusr <[email protected]>2024-05-30 16:10:00 +0200
commit1c175642d073689ca11b9252411ea5f8446007d0 (patch)
tree00b8f9062cf1261d2d2642c059a14ff5790522e5 /crates/wayland/src/protocols.rs
parent3d3057ae384ef7389284af8988410889e99c6bb9 (diff)
downloadsrdwm-1c175642d073689ca11b9252411ea5f8446007d0.tar.gz
srdwm-1c175642d073689ca11b9252411ea5f8446007d0.zip
Fix decoration drift during animated transitions; checkpoint IPC/global-menu/output-management work
Border/titlebar decoration was built from Window.geometry (the animation's final target) in both wayland backends' render loops, while sync_geometry already draws a window's actual content at window_anims' interpolated rect during any maximize/fullscreen/open-slide tween. Border and content read two different rectangles for the whole transition, so the border visibly detached from the window it was outlining - reported as "borders aren't flush." Both udev.rs and winit.rs now read the same animated rect for titlebar placement, border-strip placement, and the occlusion test against later windows in stacking order. Verified: cargo build --workspace, cargo clippy (0 new warnings), cargo test -p srdwm-core (111/111). Also checkpoints substantial protocol/IPC work from prior sessions that had accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu; zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct maximized/minimized/fullscreen/activated state per window; output_management (ext-output-management + layer-shell exclusive-zone reservation tracking); workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/ ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a theme module (core/src/theme.rs). A peer session working the AGS shell concurrently verified several of these live against a running srdwm: the global menu rendering a real app's File/Edit menu over gtk-shell1, and foreign-toplevel correctly reporting maximized and fullscreen as independent, non-simultaneous states with the geometry each implies (maximize stops at a reserved top bar and past a dock; fullscreen reaches the true monitor edge).
Diffstat (limited to 'crates/wayland/src/protocols.rs')
-rw-r--r--crates/wayland/src/protocols.rs532
1 files changed, 518 insertions, 14 deletions
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs
index f51b35d..a0aed3f 100644
--- a/crates/wayland/src/protocols.rs
+++ b/crates/wayland/src/protocols.rs
@@ -7,34 +7,44 @@
//! 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::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, WlrLayerShellHandler, WlrLayerShellState,
+ 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_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,
+ 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,
};
use crate::state::{ClientState, CompState};
@@ -55,6 +65,52 @@ impl CompositorHandler for CompState {
&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
@@ -67,8 +123,16 @@ impl CompositorHandler for CompState {
if let Some(w) = self.id_to_window.get(&id) {
w.on_commit();
}
+ crate::state::sync_toplevel_metadata(self, id, 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();
}
}
@@ -81,11 +145,204 @@ impl XdgShellHandler for CompState {
self.new_managed_window(surface);
}
- fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {}
+ /// `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) {
+ 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_drag(id, pos.x as i32, pos.y as i32);
+ }
+ }
+
+ 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();
+ }
- fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
+ /// 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);
+ }
+ }
- fn reposition_request(&mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32) {}
+ /// 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 { return };
+ let seat = self.seat.clone();
+ let Ok(grab) = self.popups.grab_popup(root, popup, &seat, serial) else { return };
+ 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());
@@ -99,14 +356,42 @@ impl XdgDecorationHandler for CompState {
});
}
- fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
+ /// 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(DecorationMode::ServerSide);
+ state.decoration_mode = Some(mode);
});
toplevel.send_configure();
+ self.set_decorated_from_mode(toplevel.wl_surface(), mode == DecorationMode::ServerSide);
}
- fn unset_mode(&mut self, _toplevel: ToplevelSurface) {}
+ /// The client dropped its decoration-mode preference. `new_decoration`
+ /// already offered `ServerSide` as the default 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 `ClientSide`, then later unsets it expecting the default
+ /// back, would stay undecorated by us forever.
+ fn unset_mode(&mut self, toplevel: ToplevelSurface) {
+ toplevel.with_pending_state(|state| {
+ state.decoration_mode = Some(DecorationMode::ServerSide);
+ });
+ toplevel.send_configure();
+ self.set_decorated_from_mode(toplevel.wl_surface(), true);
+ }
}
impl ShmHandler for CompState {
@@ -119,8 +404,167 @@ 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;
@@ -145,6 +589,13 @@ impl WlrLayerShellHandler for CompState {
}
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.
@@ -157,14 +608,28 @@ impl WlrLayerShellHandler for CompState {
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);
- if let Err(e) = result {
- log::warn!("wayland: failed to map layer surface: {e}");
+ 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());
// 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<_>>() {
@@ -229,10 +694,44 @@ impl DataControlHandler for CompState {
}
}
+/// `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());
+ }
+}
+
delegate_compositor!(CompState);
delegate_xdg_shell!(CompState);
delegate_xdg_decoration!(CompState);
delegate_shm!(CompState);
+delegate_dmabuf!(CompState);
+delegate_xdg_activation!(CompState);
+delegate_text_input_manager!(CompState);
+delegate_input_method_manager!(CompState);
delegate_seat!(CompState);
delegate_output!(CompState);
delegate_layer_shell!(CompState);
@@ -240,3 +739,8 @@ delegate_data_device!(CompState);
delegate_primary_selection!(CompState);
delegate_data_control!(CompState);
delegate_session_lock!(CompState);
+delegate_cursor_shape!(CompState);
+smithay::delegate_viewporter!(CompState);
+smithay::delegate_fractional_scale!(CompState);
+smithay::delegate_idle_notify!(CompState);
+smithay::delegate_idle_inhibit!(CompState);