atagen

monolith

public
Star0
Watch0

Commit 62b40078c7e9

tarball

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

5 files changed+44-2
windows: refuse fullscreen from clients without the keyboard

A background client could take the output and the keyboard by asking for fullscreen. The request is now remembered, the window flagged urgent, and it goes fullscreen when focus reaches it unless the client withdraws the request first. Taskbar fullscreen actions keep their direct path since they are the user's own click.
Modified file: crates/monolith/src/state.rs+1 additions
@@ -310,6 +310,7 @@ impl State {
310310
311311 /// Pushes the layout's target geometry into the space and to clients.
312312 pub fn apply_layout(&mut self) {
313+ self.grant_pending_fullscreen();
313314 self.size_new_floats();
314315 let now = now_ns();
315316 let mut shown = Vec::new();
Modified file: crates/monolith/src/windows.rs+37 additions
@@ -151,6 +151,43 @@ impl State {
151151 toggled || moved.is_some()
152152 }
153153
154+ /// Handles a client's own request to go fullscreen or leave it. A client
155+ /// that does not hold the keyboard is refused and flagged urgent, and its
156+ /// wish waits for the focus to reach it. Returns whether to lay out again.
157+ pub(crate) fn request_fullscreen(
158+ &mut self,
159+ id: WindowId,
160+ on: bool,
161+ output: Option<OutputId>,
162+ ) -> bool {
163+ let already = self.wm.layout.window(id).is_some_and(|w| w.fullscreen);
164+ let granted = !on || already || self.wm.layout.is_focused(id);
165+ let Some(s) = self.wm.get_mut(id) else {
166+ return false;
167+ };
168+ s.fullscreen_pending = (!granted).then_some(output);
169+ if granted {
170+ return self.set_window_fullscreen(id, on, output);
171+ }
172+ self.wm.layout.mark_urgent(id);
173+ true
174+ }
175+
176+ /// Makes the focused window fullscreen if it asked while it was out of
177+ /// focus.
178+ pub(crate) fn grant_pending_fullscreen(&mut self) {
179+ let Some(id) = self.wm.layout.focused() else {
180+ return;
181+ };
182+ if let Some(output) = self
183+ .wm
184+ .get_mut(id)
185+ .and_then(|s| s.fullscreen_pending.take())
186+ {
187+ self.set_window_fullscreen(id, true, output);
188+ }
189+ }
190+
154191 /// Feeds the window's modal state to the layout and lets focus follow.
155192 pub(crate) fn sync_modal(&mut self, id: WindowId) {
156193 let modal = self.wm.window(id).is_some_and(is_modal);
Modified file: crates/monolith/src/wm.rs+4 additions
@@ -84,6 +84,9 @@ pub struct WindowState {
8484 /// What it showed when its client committed a null buffer, taken before
8585 /// smithay drops the textures so hiding can leave a ghost.
8686 pub hide_frame: Option<LastFrame>,
87+ /// It asked for fullscreen without the keyboard, which waits for focus.
88+ /// Holds the output it named.
89+ pub fullscreen_pending: Option<Option<OutputId>>,
8790 /// Configured without a size until its first buffer shows the one it
8891 /// picked.
8992 pub natural_size: bool,
@@ -163,6 +166,7 @@ impl Wm {
163166 awaiting_buffer: true,
164167 drawn: false,
165168 hide_frame: None,
169+ fullscreen_pending: None,
166170 natural_size: false,
167171 natural_after: None,
168172 reclassify: false,
Modified file: crates/monolith/src/xdg.rs+1 additions−1 deletions
@@ -234,7 +234,7 @@ impl State {
234234 return;
235235 };
236236 let target = output.as_ref().and_then(|o| self.layout_output(o));
237- if self.set_window_fullscreen(id, on, target) {
237+ if self.request_fullscreen(id, on, target) {
238238 self.apply_layout();
239239 }
240240 }
Modified file: crates/monolith/src/xwayland.rs+1 additions−1 deletions
@@ -564,7 +564,7 @@ impl State {
564564
565565 fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {
566566 if let Some(id) = self.x11_window_id(window)
567- && self.set_window_fullscreen(id, on, None)
567+ && self.request_fullscreen(id, on, None)
568568 {
569569 self.apply_layout();
570570 }