atagen

monolith

public
Star0
Watch0

Commit 913b1f9fe652

tarball

atagen <boss@atagen.co>2026-10-19 07:56 UTC

6 files changed+98-1
popups: take xdg popup grabs so menus close on outside clicks

XdgShellHandler::grab was empty, so GTK, Qt and quickshell menus never got popup_done when another client was clicked. The popup keyboard grab hands focus back to the grab's root, so button presses and after_dispatch resync with the layout's focus instead of leaving it on the window the menu came from. A locked session dismisses the popup instead of granting the grab, since a grab could take the keyboard from the lock surface.
Modified file: crates/monolith/src/focus.rs+18 additions
@@ -3,6 +3,7 @@
33
44 use crate::state::State;
55 use smithay::backend::input::KeyState;
6+use smithay::desktop::PopupKind;
67 use smithay::input::Seat;
78 use smithay::input::keyboard::{KeyboardTarget, KeysymHandle, ModifiersState};
89 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -90,3 +91,20 @@ impl KeyboardTarget<State> for Focus {
9091 }
9192 }
9293 }
94+
95+impl From<PopupKind> for Focus {
96+ fn from(p: PopupKind) -> Self {
97+ Focus::Surface(p.wl_surface().clone())
98+ }
99+}
100+
101+impl From<Focus> for WlSurface {
102+ fn from(f: Focus) -> Self {
103+ match f {
104+ Focus::Surface(s) => s,
105+ Focus::X11(x) => x
106+ .wl_surface()
107+ .expect("popup grabs are rooted at wayland surfaces"),
108+ }
109+ }
110+}
Modified file: crates/monolith/src/input.rs+4 additions
@@ -54,6 +54,7 @@ impl State {
5454 let Some(pointer) = self.input.seat.get_pointer() else {
5555 return;
5656 };
57+ let grabbed = self.keyboard_grabbed();
5758 pointer.button(
5859 self,
5960 &ButtonEvent {
@@ -63,6 +64,9 @@ impl State {
6364 time,
6465 },
6566 );
67+ if grabbed {
68+ self.sync_focus();
69+ }
6670 }
6771
6872 pub fn pointer_axis(&mut self, frame: AxisFrame) {
Modified file: crates/monolith/src/main.rs+1 additions
@@ -19,6 +19,7 @@ mod layers;
1919 mod lock;
2020 mod nested;
2121 mod pacing;
22+mod popups;
2223 mod power;
2324 mod scene;
2425 mod seat;
Added file: crates/monolith/src/popups.rs+69 additions
@@ -0,0 +1,69 @@
1+//! xdg popups, from their grabs to where they are placed.
2+
3+use crate::focus::Focus;
4+use crate::state::State;
5+use smithay::desktop::{
6+ PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, find_popup_root_surface,
7+};
8+use smithay::input::Seat;
9+use smithay::input::pointer::Focus as GrabFocus;
10+use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
11+use smithay::utils::Serial;
12+use smithay::wayland::shell::xdg::PopupSurface;
13+
14+impl State {
15+ /// Popup grabs are the only keyboard grabs, and they hand the focus back
16+ /// to the grab's root, not to whatever the layout has focused, so the
17+ /// callers resync once one may have ended.
18+ pub fn keyboard_grabbed(&self) -> bool {
19+ self.input
20+ .seat
21+ .get_keyboard()
22+ .is_some_and(|k| k.is_grabbed())
23+ }
24+
25+ /// Gives an explicit popup grab the keyboard and pointer, so clicking
26+ /// elsewhere dismisses it. A locked session refuses grabs, since a
27+ /// client could otherwise take the keyboard from the lock surface.
28+ pub fn grab_popup(&mut self, surface: PopupSurface, seat: &WlSeat, serial: Serial) {
29+ let Some(seat) = Seat::from_resource(seat) else {
30+ return;
31+ };
32+ if self.services.lock.active() {
33+ surface.send_popup_done();
34+ return;
35+ }
36+ let kind = PopupKind::Xdg(surface);
37+ let Ok(root) = find_popup_root_surface(&kind) else {
38+ return;
39+ };
40+ let Ok(mut grab) = self
41+ .wm
42+ .popups
43+ .grab_popup(Focus::from(root), kind, &seat, serial)
44+ else {
45+ return;
46+ };
47+ if let Some(keyboard) = seat.get_keyboard() {
48+ if keyboard.is_grabbed()
49+ && !(keyboard.has_grab(serial)
50+ || keyboard.has_grab(grab.previous_serial().unwrap_or(serial)))
51+ {
52+ grab.ungrab(PopupUngrabStrategy::All);
53+ return;
54+ }
55+ keyboard.set_focus(self, grab.current_grab(), serial);
56+ keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
57+ }
58+ if let Some(pointer) = seat.get_pointer() {
59+ if pointer.is_grabbed()
60+ && !(pointer.has_grab(serial)
61+ || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial())))
62+ {
63+ grab.ungrab(PopupUngrabStrategy::All);
64+ return;
65+ }
66+ pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, GrabFocus::Keep);
67+ }
68+ }
69+}
Modified file: crates/monolith/src/state.rs+3 additions
@@ -213,6 +213,9 @@ impl State {
213213 pub fn after_dispatch(&mut self) {
214214 self.wm.space.refresh();
215215 self.wm.popups.cleanup();
216+ if self.keyboard_grabbed() {
217+ self.sync_focus();
218+ }
216219 match self.backend {
217220 crate::backend::Backend::Drm(_) => crate::drm::render(self),
218221 crate::backend::Backend::Nested(_) => crate::nested::render(self),
Modified file: crates/monolith/src/xdg.rs+3 additions−1 deletions
@@ -72,7 +72,9 @@ impl XdgShellHandler for State {
7272 surface.send_repositioned(token);
7373 }
7474
75- fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {}
75+ fn grab(&mut self, surface: PopupSurface, seat: WlSeat, serial: Serial) {
76+ self.grab_popup(surface, &seat, serial);
77+ }
7678 }
7779
7880 impl XdgDecorationHandler for State {