atagen

monolith

public
Star0
Watch0

Commit 9e8937bb4297

tarball

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

2 files changed+37-12
windows: unmap a toplevel that commits without a buffer

Telegram, Discord and Steam hide to the tray by attaching a null buffer. Nothing noticed, so the tile stayed empty and focused, and the window came back without passing through map_window. on_buffer now removes a drawn toplevel from the layout and puts its Window back into wm.unmapped. Smithay has already reset the role at that point, so the next commit maps it again with a fresh initial configure.
Modified file: crates/monolith/src/windows.rs+28 additions−6 deletions
@@ -94,18 +94,38 @@ impl State {
9494 });
9595 }
9696
97- /// Called on commits, starts the spawn of a window that now has a buffer.
97+ /// Called on commits, starts the spawn of a window that now has a buffer
98+ /// and unmaps one that lost it.
9899 pub fn on_buffer(&mut self, surface: &WlSurface) {
99- if !self.wm.any_settling() {
100+ let Some(id) = self.window_id(surface) else {
100101 return;
101- }
102+ };
102103 let has_buffer =
103104 with_renderer_surface_state(surface, |s| s.buffer_size().is_some()).unwrap_or(false);
104- if has_buffer && let Some(id) = self.window_id(surface) {
105+ let Some(w) = self.wm.get_mut(id) else {
106+ return;
107+ };
108+ if !has_buffer {
109+ if std::mem::take(&mut w.drawn) && w.window.toplevel().is_some() {
110+ self.hide_window(id);
111+ }
112+ return;
113+ }
114+ w.drawn = true;
115+ if w.settling() {
105116 self.first_buffer(id);
106117 }
107118 }
108119
120+ /// Takes a toplevel that dropped its buffer, as clients do to hide to a
121+ /// tray, out of the layout. Smithay has reset its role, so its next commit
122+ /// maps it afresh with a new initial configure.
123+ fn hide_window(&mut self, id: WindowId) {
124+ if let Some(window) = self.remove_window(id) {
125+ self.wm.unmapped.push(window);
126+ }
127+ }
128+
109129 fn first_buffer(&mut self, id: WindowId) {
110130 let Some(w) = self.wm.get_mut(id) else {
111131 return;
@@ -265,13 +285,15 @@ impl State {
265285 }
266286 }
267287
268- pub fn remove_window(&mut self, id: WindowId) {
269- if let Some(state) = self.wm.remove(id) {
288+ pub fn remove_window(&mut self, id: WindowId) -> Option<Window> {
289+ let state = self.wm.remove(id);
290+ if let Some(state) = &state {
270291 self.wm.space.unmap_elem(&state.window);
271292 self.leave_ghost(id, &state.window);
272293 }
273294 self.wm.layout.remove_window(id);
274295 self.apply_layout();
296+ state.map(|s| s.window)
275297 }
276298
277299 /// Keeps a visible tiled window's last frame shrinking to its seed while
Modified file: crates/monolith/src/wm.rs+9 additions−6 deletions
@@ -55,6 +55,8 @@ pub struct WindowState {
5555 pub placed: Option<(Kind, Rect, OutputId)>,
5656 /// Held on its seed until its first buffer lands.
5757 pub awaiting_buffer: bool,
58+ /// Has shown a buffer, so one committed without is an unmap.
59+ pub drawn: bool,
5860 /// Configured without a size until its first buffer shows the one it
5961 /// picked.
6062 pub natural_size: bool,
@@ -72,6 +74,12 @@ pub struct WindowState {
7274 pub title: String,
7375 }
7476
77+impl WindowState {
78+ pub fn settling(&self) -> bool {
79+ self.awaiting_buffer || self.natural_size || self.reclassify
80+ }
81+}
82+
7583 /// The id clients see for a window, stable for its lifetime.
7684 pub fn key(id: WindowId) -> u64 {
7785 slotmap::Key::data(&id).as_ffi()
@@ -119,6 +127,7 @@ impl Wm {
119127 window,
120128 placed: None,
121129 awaiting_buffer: true,
130+ drawn: false,
122131 natural_size: false,
123132 natural_after: None,
124133 reclassify: false,
@@ -198,12 +207,6 @@ impl Wm {
198207 self.last_frames
199208 .retain(|id, _| ghosts.iter().any(|g| g.id == *id));
200209 }
201-
202- pub fn any_settling(&self) -> bool {
203- self.windows
204- .values()
205- .any(|w| w.awaiting_buffer || w.natural_size || w.reclassify)
206- }
207210 }
208211
209212 #[derive(Clone, Copy, Debug, PartialEq, Eq)]