atagen

monolith

public
Star0
Watch0

Commit cadf3a07a5dc

tarball

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

9 files changed+102-17
windows: keep a toplevel hidden to a tray in the layout

Hiding went through remove_window, which lost the floating, sticky, scratchpad and tag state, the float rect, the children's parent links and the foreign-toplevel handle, and left no close ghost. The layout entry is now marked hidden and restored on the next commit. The last frame is taken in the pre-commit hook because smithay clears the textures on a null-buffer commit before on_buffer runs.
Modified file: crates/layout/src/lib.rs+3 additions−4 deletions
@@ -294,10 +294,9 @@ impl Layout {
294294 if win.scratchpad.is_some() || self.is_viewed(o, tag) {
295295 return;
296296 }
297- let alone = !self
298- .windows
299- .iter()
300- .any(|(x, v)| x != w && v.output == Some(o) && v.tag == tag && v.scratchpad.is_none());
297+ let alone = !self.windows.iter().any(|(x, v)| {
298+ x != w && v.output == Some(o) && v.tag == tag && v.scratchpad.is_none() && !v.hidden
299+ });
301300 if alone {
302301 self.toggle_view(o, tag);
303302 }
Modified file: crates/layout/src/scratchpad.rs+5 additions−1 deletions
@@ -40,7 +40,11 @@ impl Layout {
4040 let o = self.active?;
4141 let sp = &mut self.scratchpads[s];
4242 let mut next = sp.shown.map_or(0, |i| i + 1);
43- while sp.windows.get(next).is_some_and(|&w| self.windows[w].hidden) {
43+ while sp
44+ .windows
45+ .get(next)
46+ .is_some_and(|&w| self.windows[w].hidden)
47+ {
4448 next += 1;
4549 }
4650 if next >= sp.windows.len() {
Modified file: crates/monolith/src/anim.rs+1 additions
@@ -89,6 +89,7 @@ impl Animator {
8989 .ghosts
9090 .iter()
9191 .filter(|g| g.output == o && g.fixed.is_none())
92+ .filter(|g| !targets.iter().any(|t| t.0 == g.id))
9293 .map(|g| (g.id, g.seed));
9394 let targets: Vec<(WindowId, Rect)> = ghosts.chain(targets.iter().copied()).collect();
9495 let ids: Vec<WindowId> = targets.iter().map(|(id, _)| *id).collect();
Modified file: crates/monolith/src/commit.rs+10 additions
@@ -27,6 +27,7 @@ impl CompositorHandler for State {
2727 fn new_surface(&mut self, surface: &WlSurface) {
2828 add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {
2929 crate::timing::watch(state, surface);
30+ state.keep_frame_for_hide(surface);
3031 hold_until_readable(state, surface);
3132 });
3233 }
@@ -97,6 +98,15 @@ impl CompositorHandler for State {
9798 t.send_configure();
9899 }
99100 }
101+ if let Some(id) = self
102+ .window_id(surface)
103+ .filter(|&id| self.wm.layout.window(id).is_some_and(|w| w.hidden))
104+ {
105+ self.show_window(id);
106+ if let Some(t) = self.wm.window(id).and_then(|w| w.toplevel()) {
107+ t.send_configure();
108+ }
109+ }
100110 self.on_buffer(surface);
101111 self.wm.popups.commit(surface);
102112 if let Some(PopupKind::Xdg(popup)) = self.wm.popups.find_popup(surface)
Modified file: crates/monolith/src/ipc.rs+1 additions
@@ -177,6 +177,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
177177 .collect();
178178 let mut windows: Vec<monolith_ipc::Window> = l
179179 .windows()
180+ .filter(|(_, w)| !w.hidden)
180181 .map(|(id, w)| {
181182 let (app_id, title) = state
182183 .wm
Modified file: crates/monolith/src/toplevels.rs+6 additions−1 deletions
@@ -70,8 +70,12 @@ impl ForeignToplevelHandler for State {
7070 impl State {
7171 /// Brings both window lists in line with the windows.
7272 pub fn publish_toplevels(&mut self) {
73+ let hidden: Vec<_> = (self.wm.layout.windows())
74+ .filter(|(_, w)| w.hidden)
75+ .map(|(id, _)| id)
76+ .collect();
7377 let list = &mut self.protocols.toplevel_list;
74- for (_, w) in self.wm.iter_mut() {
78+ for (_, w) in self.wm.iter_mut().filter(|(id, _)| !hidden.contains(id)) {
7579 match &w.list_handle {
7680 Some(h) => {
7781 if h.title() != w.title || h.app_id() != w.app_id {
@@ -92,6 +96,7 @@ impl State {
9296 let toplevels = self
9397 .wm
9498 .iter()
99+ .filter(|(id, _)| !hidden.contains(id))
95100 .map(|(id, w)| {
96101 let (app_id, title) = (w.app_id.clone(), w.title.clone());
97102 let w = self.wm.layout.window(id);
Modified file: crates/monolith/src/windows.rs+71 additions−10 deletions
@@ -12,7 +12,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::Sta
1212 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
1313 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1414 use smithay::utils::{Logical, Point, Rectangle, Serial};
15-use smithay::wayland::compositor::with_states;
15+use smithay::wayland::compositor::{BufferAssignment, SurfaceAttributes, with_states};
1616 use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
1717 use std::time::Duration;
1818
@@ -157,12 +157,67 @@ impl State {
157157 }
158158 }
159159
160- /// Takes a toplevel that dropped its buffer, as clients do to hide to a
161- /// tray, out of the layout. Smithay has reset its role, so its next commit
162- /// maps it afresh with a new initial configure.
160+ /// Hides a toplevel that dropped its buffer, as clients do to hide to a
161+ /// tray. The layout keeps its entry, so its next commit, which smithay
162+ /// treats as a fresh map with a new initial configure, restores it.
163163 fn hide_window(&mut self, id: WindowId) {
164- if let Some(window) = self.remove_window(id) {
165- self.wm.unmapped.push(window);
164+ let Some(s) = self.wm.get_mut(id) else {
165+ return;
166+ };
167+ let window = s.window.clone();
168+ let saved = s.hide_frame.take();
169+ let opacity = s.rule.opacity.unwrap_or(1.0);
170+ if let Some(h) = s.list_handle.take() {
171+ h.send_closed();
172+ }
173+ self.leave_ghost(id, &window, saved, opacity);
174+ self.wm.layout.set_hidden(id, true);
175+ self.wm.anim.forget(id);
176+ self.apply_layout();
177+ }
178+
179+ /// Brings back a hidden toplevel on its first commit since, where it was
180+ /// and as it was, to be held on its seed until its buffer lands.
181+ pub(crate) fn show_window(&mut self, id: WindowId) {
182+ let Some(s) = self.wm.get_mut(id) else {
183+ return;
184+ };
185+ s.awaiting_buffer = true;
186+ s.drawn = false;
187+ s.hide_frame = None;
188+ let window = s.window.clone();
189+ let focus = s.rule.focus != Some(false);
190+ let parent = self.parent_of(&window);
191+ self.wm.layout.set_hidden(id, false);
192+ self.wm.layout.set_parent(id, parent);
193+ self.wm.layout.set_modal(id, is_modal(&window));
194+ self.refresh_identity(id);
195+ self.view_if_first(id);
196+ self.await_first_buffer(id);
197+ if focus {
198+ self.wm.layout.focus_unless_covered(id);
199+ }
200+ self.apply_layout();
201+ }
202+
203+ /// Called before a commit applies. A null-buffer commit makes smithay
204+ /// drop the surface's textures before `on_buffer` runs, so the frame a
205+ /// hiding window leaves behind has to be taken here.
206+ pub fn keep_frame_for_hide(&mut self, surface: &WlSurface) {
207+ let Some(id) = self.window_id(surface) else {
208+ return;
209+ };
210+ let removes = with_states(surface, |s| {
211+ let mut attrs = s.cached_state.get::<SurfaceAttributes>();
212+ matches!(attrs.pending().buffer, Some(BufferAssignment::Removed))
213+ });
214+ let Some(s) = self.wm.get(id).filter(|s| removes && s.drawn) else {
215+ return;
216+ };
217+ let window = s.window.clone();
218+ let frame = self.last_frame(&window);
219+ if let Some(s) = self.wm.get_mut(id) {
220+ s.hide_frame = frame;
166221 }
167222 }
168223
@@ -247,7 +302,7 @@ impl State {
247302 .wm
248303 .layout
249304 .windows()
250- .filter(|(_, w)| w.floating && w.float_default && w.scratchpad.is_none())
305+ .filter(|(_, w)| w.floating && w.float_default && w.scratchpad.is_none() && !w.hidden)
251306 .map(|(id, _)| id)
252307 .collect();
253308 for id in defaults {
@@ -435,7 +490,7 @@ impl State {
435490 if let Some(state) = &state {
436491 self.wm.space.unmap_elem(&state.window);
437492 let opacity = state.rule.opacity.unwrap_or(1.0);
438- self.leave_ghost(id, &state.window, opacity);
493+ self.leave_ghost(id, &state.window, None, opacity);
439494 }
440495 self.wm.layout.remove_window(id);
441496 self.apply_layout();
@@ -444,7 +499,13 @@ impl State {
444499
445500 /// Keeps a visible tiled window's last frame shrinking to its seed while
446501 /// its neighbours close over it.
447- fn leave_ghost(&mut self, id: WindowId, window: &Window, opacity: f32) {
502+ fn leave_ghost(
503+ &mut self,
504+ id: WindowId,
505+ window: &Window,
506+ saved: Option<LastFrame>,
507+ opacity: f32,
508+ ) {
448509 let Some(o) = self.wm.layout.window(id).and_then(|w| w.output) else {
449510 return;
450511 };
@@ -466,7 +527,7 @@ impl State {
466527 else {
467528 return;
468529 };
469- if let Some(frame) = self.last_frame(window) {
530+ if let Some(frame) = saved.or_else(|| self.last_frame(window)) {
470531 self.wm.last_frames.insert(id, frame);
471532 self.wm.anim.add_ghost(Ghost {
472533 id,
Modified file: crates/monolith/src/wm.rs+4 additions
@@ -81,6 +81,9 @@ pub struct WindowState {
8181 pub awaiting_buffer: bool,
8282 /// Has shown a buffer, so one committed without is an unmap.
8383 pub drawn: bool,
84+ /// What it showed when its client committed a null buffer, taken before
85+ /// smithay drops the textures so hiding can leave a ghost.
86+ pub hide_frame: Option<LastFrame>,
8487 /// Configured without a size until its first buffer shows the one it
8588 /// picked.
8689 pub natural_size: bool,
@@ -159,6 +162,7 @@ impl Wm {
159162 placed: None,
160163 awaiting_buffer: true,
161164 drawn: false,
165+ hide_frame: None,
162166 natural_size: false,
163167 natural_after: None,
164168 reclassify: false,
Modified file: crates/monolith/src/workspaces.rs+1 additions−1 deletions
@@ -56,7 +56,7 @@ impl State {
5656 let l = &self.wm.layout;
5757 let occupied: HashSet<(OutputId, monolith_layout::TagId)> = l
5858 .windows()
59- .filter(|(_, w)| w.scratchpad.is_none())
59+ .filter(|(_, w)| w.scratchpad.is_none() && !w.hidden)
6060 .filter_map(|(_, w)| Some((w.output?, w.tag)))
6161 .collect();
6262 let groups = l