1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
|
//! `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::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) {
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());
}
}
|