srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/protocols/xdg_shell.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/protocols/xdg_shell.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-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/xdg_shell.rs')
-rw-r--r--crates/wayland/src/protocols/xdg_shell.rs258
1 files changed, 258 insertions, 0 deletions
diff --git a/crates/wayland/src/protocols/xdg_shell.rs b/crates/wayland/src/protocols/xdg_shell.rs
new file mode 100644
index 0000000..4fe0f6f
--- /dev/null
+++ b/crates/wayland/src/protocols/xdg_shell.rs
@@ -0,0 +1,258 @@
+//! `xdg_shell`: toplevel and popup lifecycle, and the client-initiated
+//! move/resize/maximize/fullscreen/minimize requests a CSD client sends
+//! instead of (or alongside) the pointer-driven titlebar handlers in
+//! `input.rs`.
+
+use smithay::desktop::{find_popup_root_surface, PopupKeyboardGrab, PopupKind, PopupPointerGrab};
+use smithay::input::pointer::Focus;
+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_seat;
+use smithay::reexports::wayland_server::Resource;
+use smithay::utils::Serial;
+use smithay::wayland::shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState};
+
+use crate::state::CompState;
+
+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) {
+ // Temporary: live report is that Nemo's right-click context menu
+ // never appears at all (not mispositioned - entirely invisible),
+ // while the exact same xdg_popup mechanism works for Firefox. Logs
+ // the unconstrained geometry this popup gets so a live repro tells
+ // us whether it's landing off-screen/degenerate (the known gap this
+ // function's own doc comment already flags) or something else
+ // entirely. Remove once resolved.
+ let geom = positioner.get_geometry();
+ let parent = surface.get_parent_surface();
+ log::warn!("POPUP-GEOM-DIAG geometry={geom:?} parent={:?}", parent.as_ref().map(|s| s.id()));
+ surface.with_pending_state(|state| {
+ state.geometry = geom;
+ state.positioner = positioner;
+ });
+ if surface.send_configure().is_err() {
+ log::warn!("POPUP-GEOM-DIAG send_configure failed");
+ 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());
+ }
+}