atagen

monolith

public
Star0
Watch0

Commit 783f8b1b03c5

tarball

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

5 files changed+101-19
popups: end popup grabs on lock and when focus moves to another root

lock() left the popup keyboard grab in place, and smithay's PopupKeyboardGrab ignores set_focus until it ends, so the password went to the client that owned the menu. The same grab survived binds and tag switches because sync_focus could not move the keyboard, and after_dispatch re-ran it on every dispatch. State now keeps the PopupGrab, ends it (popup_done plus seat grab release) on lock and when the layout's keyboard target changes root, and only resyncs once the grab has ended. Drag and drop is cancelled on lock and refused while locked.
Modified file: crates/monolith/src/input.rs+1 additions−4 deletions
@@ -154,7 +154,6 @@ impl State {
154154 let Some(pointer) = self.input.seat.get_pointer() else {
155155 return;
156156 };
157- let grabbed = self.keyboard_grabbed();
158157 pointer.button(
159158 self,
160159 &ButtonEvent {
@@ -164,9 +163,7 @@ impl State {
164163 time,
165164 },
166165 );
167- if grabbed {
168- self.sync_focus();
169- }
166+ self.reap_popup_grab();
170167 }
171168
172169 pub fn pointer_axis(&mut self, frame: AxisFrame) {
Modified file: crates/monolith/src/lock.rs+24 additions−2 deletions
@@ -4,6 +4,7 @@
44 use crate::scene::Draw;
55 use crate::state::State;
66 use monolith_render_vk::VulkanRenderer;
7+use smithay::backend::input::InputTime;
78 use smithay::backend::renderer::element::Kind;
89 use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
910 use smithay::desktop::WindowSurfaceType;
@@ -11,11 +12,12 @@ use smithay::desktop::utils::{
1112 OutputPresentationFeedback, send_frames_surface_tree, take_presentation_feedback_surface_tree,
1213 under_from_surface_tree,
1314 };
15+use smithay::input::pointer::MotionEvent;
1416 use smithay::output::Output;
1517 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
1618 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1719 use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind;
18-use smithay::utils::{Logical, Point};
20+use smithay::utils::{Logical, Point, SERIAL_COUNTER};
1921 use smithay::wayland::compositor::SurfaceData;
2022 use smithay::wayland::session_lock::{
2123 LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
@@ -83,7 +85,12 @@ impl SessionLockHandler for State {
8385 self.services.lock.pending = Some(confirmation);
8486 self.services.lock.blanked.clear();
8587 self.input.drag = None;
86- self.wm.resizing = None;
88+ self.end_popup_grab();
89+ self.cancel_dnd();
90+ if self.wm.resizing.take().is_some() {
91+ self.apply_layout();
92+ }
93+ self.refocus_pointer();
8794 self.sync_focus();
8895 self.dirty.redraw = true;
8996 }
@@ -126,6 +133,21 @@ fn configure(surface: &LockSurface, output: &Output) {
126133 }
127134
128135 impl State {
136+ fn refocus_pointer(&mut self) {
137+ let Some(pointer) = self.input.seat.get_pointer() else {
138+ return;
139+ };
140+ let location = self.input.pointer_pos;
141+ let under = self.surface_under(location);
142+ let event = MotionEvent {
143+ location,
144+ serial: SERIAL_COUNTER.next_serial(),
145+ time: InputTime::now(),
146+ };
147+ pointer.motion(self, under, &event);
148+ pointer.frame(self);
149+ }
150+
129151 /// Confirms a pending lock once every output has drawn without the
130152 /// windows, so nothing private is left on screen when the locker is told
131153 /// it holds the session.
Modified file: crates/monolith/src/popups.rs+47 additions−9 deletions
@@ -5,6 +5,7 @@ use crate::scene::Draw;
55 use crate::state::State;
66 use monolith_layout::{Rect, WindowId};
77 use monolith_render_vk::VulkanRenderer;
8+use smithay::backend::input::InputTime;
89 use smithay::backend::renderer::element::Kind;
910 use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
1011 use smithay::desktop::utils::under_from_surface_tree;
@@ -16,18 +17,54 @@ use smithay::input::Seat;
1617 use smithay::input::pointer::Focus as GrabFocus;
1718 use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
1819 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
19-use smithay::utils::{Logical, Physical, Point, Rectangle, Serial};
20+use smithay::utils::{Logical, Physical, Point, Rectangle, SERIAL_COUNTER, Serial};
21+use smithay::wayland::seat::WaylandFocus;
2022 use smithay::wayland::shell::xdg::PopupSurface;
2123
2224 impl State {
23- /// Popup grabs are the only keyboard grabs, and they hand the focus back
24- /// to the grab's root, not to whatever the layout has focused, so the
25- /// callers resync once one may have ended.
26- pub fn keyboard_grabbed(&self) -> bool {
27- self.input
28- .seat
29- .get_keyboard()
30- .is_some_and(|k| k.is_grabbed())
25+ /// Ends the popup grab if it has run its course, and hands the keyboard
26+ /// back to the layout, since a finished grab restores the focus to its own
27+ /// root and not to whatever the layout has focused.
28+ pub fn reap_popup_grab(&mut self) {
29+ if self
30+ .input
31+ .popup_grab
32+ .take_if(|grab| grab.has_ended())
33+ .is_some()
34+ {
35+ self.sync_focus();
36+ }
37+ }
38+
39+ /// Whether the popup grab is rooted at `target`, or nothing is grabbed.
40+ pub fn popup_grab_roots(&self, target: Option<&Focus>) -> bool {
41+ let Some(grab) = &self.input.popup_grab else {
42+ return true;
43+ };
44+ let root = grab.keyboard_grab_start_data().focus.as_ref();
45+ let wl = |f: &Focus| f.wl_surface().map(|s| s.into_owned());
46+ root.and_then(wl) == target.and_then(wl)
47+ }
48+
49+ /// Dismisses the popup grab and every popup in it, and releases the seat
50+ /// grabs it holds without moving the focus, which the caller sets next.
51+ pub fn end_popup_grab(&mut self) {
52+ let Some(mut grab) = self.input.popup_grab.take() else {
53+ return;
54+ };
55+ grab.ungrab(PopupUngrabStrategy::All);
56+ let serials = [Some(grab.serial()), grab.previous_serial()];
57+ let seat = self.input.seat.clone();
58+ if let Some(keyboard) = seat.get_keyboard()
59+ && keyboard.is_grabbed()
60+ {
61+ keyboard.unset_grab(self);
62+ }
63+ if let Some(pointer) = seat.get_pointer()
64+ && serials.into_iter().flatten().any(|s| pointer.has_grab(s))
65+ {
66+ pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), InputTime::now());
67+ }
3168 }
3269
3370 /// Gives an explicit popup grab the keyboard and pointer, so clicking
@@ -63,6 +100,7 @@ impl State {
63100 keyboard.set_focus(self, grab.current_grab(), serial);
64101 keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
65102 }
103+ self.input.popup_grab = Some(grab.clone());
66104 if let Some(pointer) = seat.get_pointer() {
67105 if pointer.is_grabbed()
68106 && !(pointer.has_grab(serial)
Modified file: crates/monolith/src/seat.rs+17 additions−1 deletions
@@ -1,11 +1,12 @@
11 use crate::cursor::DndIcon;
22 use crate::state::State;
3+use smithay::backend::input::InputTime;
34 use smithay::input::dnd::{DnDGrab, DndGrabHandler, DndTarget, GrabType, Source};
45 use smithay::input::pointer::{CursorImageStatus, Focus};
56 use smithay::input::{Seat, SeatHandler, SeatState};
67 use smithay::reexports::wayland_server::Resource;
78 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8-use smithay::utils::{Logical, Point, Serial};
9+use smithay::utils::{Logical, Point, SERIAL_COUNTER, Serial};
910 use smithay::wayland::seat::WaylandFocus;
1011 use smithay::wayland::selection::data_device::{
1112 DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
@@ -122,6 +123,17 @@ impl DndGrabHandler for State {
122123 }
123124
124125 impl State {
126+ /// Drops whatever holds the pointer, which once popup grabs are gone is
127+ /// a drag and drop or a client's button press.
128+ pub fn cancel_dnd(&mut self) {
129+ if let Some(pointer) = self.input.seat.get_pointer()
130+ && pointer.is_grabbed()
131+ {
132+ pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), InputTime::now());
133+ }
134+ self.end_dnd();
135+ }
136+
125137 fn end_dnd(&mut self) {
126138 self.input.dnd_icon = None;
127139 self.dirty.redraw = true;
@@ -141,6 +153,10 @@ impl WaylandDndGrabHandler for State {
141153 serial: Serial,
142154 type_: GrabType,
143155 ) {
156+ if self.services.lock.active() {
157+ source.cancel();
158+ return;
159+ }
144160 self.input.dnd_icon = icon.map(|surface| DndIcon {
145161 surface,
146162 offset: Point::default(),
Modified file: crates/monolith/src/state.rs+12 additions−3 deletions
@@ -70,6 +70,10 @@ pub struct Input {
7070 /// one yields a relative delta even while the pointer is held in place.
7171 pub absolute: Option<Point<f64, Logical>>,
7272 pub inhibitor: Option<smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor>,
73+ /// The explicit popup grab holding the keyboard and pointer, if any.
74+ pub popup_grab: Option<smithay::desktop::PopupGrab<State>>,
75+ /// What the layout last wanted the keyboard on, which a popup grab may be hiding.
76+ pub focus_target: Option<Focus>,
7377 }
7478
7579 /// What needs doing after this batch of events.
@@ -208,6 +212,8 @@ impl State {
208212 pointer_output: None,
209213 absolute: None,
210214 inhibitor: None,
215+ popup_grab: None,
216+ focus_target: None,
211217 },
212218 dirty: Dirty {
213219 redraw: true,
@@ -234,9 +240,7 @@ impl State {
234240 pub fn after_dispatch(&mut self) {
235241 self.wm.space.refresh();
236242 self.wm.popups.cleanup();
237- if self.keyboard_grabbed() {
238- self.sync_focus();
239- }
243+ self.reap_popup_grab();
240244 match self.backend {
241245 crate::backend::Backend::Drm(_) => crate::drm::render(self),
242246 crate::backend::Backend::Nested(_) => crate::nested::render(self),
@@ -497,6 +501,11 @@ impl State {
497501 }
498502 })
499503 };
504+ if std::mem::replace(&mut self.input.focus_target, target.clone()) != target
505+ && !self.popup_grab_roots(target.as_ref())
506+ {
507+ self.end_popup_grab();
508+ }
500509 if let Some(keyboard) = self.input.seat.get_keyboard()
501510 && keyboard.current_focus() != target
502511 {