atagen

monolith

public
Star0
Watch0

Commit 60956e3999de

tarball

atagen <boss@atagen.co>2026-10-17 02:55 UTC

13 files changed+232-198
monolith: keep one WindowState per window with a surface index

A window's state was spread over six maps, and removing it cleaned only some of them, leaving stale ids in the awaiting set and the last placement. Each window now has one record in a SecondaryMap keyed by the layout's WindowId, and Wm::insert and Wm::remove are the only ways in and out. Lookups by surface go through an index instead of scanning every window. X11 windows join the index when xwayland-shell associates their surface, which can come after they map. scene::build takes the Wm, which drops it from ten arguments to seven.
Modified file: crates/monolith/src/activation.rs+3 additions−2 deletions
@@ -52,9 +52,10 @@ impl XdgActivationHandler for State {
5252 return;
5353 };
5454 let ctx = data.user_data.get::<Context>();
55- if self.wm.awaiting.contains(&id)
55+ if let Some(w) = self.wm.get(id)
56+ && w.awaiting_buffer
57+ && !w.placed_by_rule
5658 && let Some(ctx) = ctx
57- && !self.wm.placed_by_rule.contains(&id)
5859 {
5960 self.wm.layout.relocate(id, ctx.output, ctx.tag);
6061 }
Modified file: crates/monolith/src/anim.rs+5 additions
@@ -237,6 +237,11 @@ impl Animator {
237237 || !self.spawns.is_empty()
238238 }
239239
240+ pub fn forget(&mut self, id: WindowId) {
241+ self.glides.remove(&id);
242+ self.spawns.remove(&id);
243+ }
244+
240245 pub fn prune(&mut self, now: u64) {
241246 self.glides.retain(|_, g| g.timeline.end_ns() > now);
242247 let phase = self.phase_ns();
Modified file: crates/monolith/src/capture.rs+15 additions−11 deletions
@@ -87,7 +87,11 @@ impl ToplevelCaptureSourceHandler for State {
8787 source: ImageCaptureSource,
8888 toplevel: ForeignToplevelHandle,
8989 ) {
90- if let Some(id) = self.wm.list_handles.window(&toplevel) {
90+ let id = self.wm.iter().find_map(|(id, w)| {
91+ let h = w.list_handle.as_ref()?;
92+ (h.identifier() == toplevel.identifier()).then_some(id)
93+ });
94+ if let Some(id) = id {
9195 source.user_data().insert_if_missing(|| id);
9296 }
9397 }
@@ -151,16 +155,13 @@ impl State {
151155 return Some(Target::Output(output.upgrade()?.name()));
152156 }
153157 let id = *data.get::<WindowId>()?;
154- self.wm
155- .windows
156- .contains_key(&id)
157- .then_some(Target::Window(id))
158+ self.wm.get(id).map(|_| Target::Window(id))
158159 }
159160
160161 fn target_size(&self, target: &Target) -> Option<(i32, i32)> {
161162 let size: (i32, i32) = match target {
162163 Target::Output(name) => self.output_named(name)?.1.current_mode()?.size.into(),
163- Target::Window(id) => self.wm.windows.get(id)?.geometry().size.into(),
164+ Target::Window(id) => self.wm.window(*id)?.geometry().size.into(),
164165 };
165166 (size.0 > 0 && size.1 > 0).then_some(size)
166167 }
@@ -205,7 +206,13 @@ impl State {
205206 };
206207 self.output_elements(o, &output, session.draw_cursor())
207208 }
208- Target::Window(id) => window_elements(&mut self.renderer, &self.wm.windows[id]),
209+ Target::Window(id) => match self.wm.window(*id) {
210+ Some(window) => window_elements(&mut self.renderer, window),
211+ None => {
212+ frame.fail(CaptureFailureReason::Stopped);
213+ return;
214+ }
215+ },
209216 };
210217 let buffer = frame.buffer();
211218 let result = match get_dmabuf(&buffer) {
@@ -284,14 +291,11 @@ impl State {
284291 match crate::lock::elements(&self.services.lock, &mut self.renderer, output) {
285292 Some(locked) => elements.extend(locked),
286293 None => elements.extend(crate::scene::build(
287- &self.wm.layout,
294+ &mut self.wm,
288295 o,
289- &self.wm.windows,
290- &self.wm.anim,
291296 &mut self.renderer,
292297 now,
293298 &self.settings.config.decoration,
294- &mut self.wm.borders,
295299 Some(output),
296300 self.services
297301 .xwayland
Modified file: crates/monolith/src/commands.rs+2 additions−8 deletions
@@ -62,14 +62,8 @@ impl State {
6262 match cmd {
6363 Command::Spawn { argv } => self.spawn(&argv)?,
6464 Command::Close => {
65- if let Some(w) = focused.and_then(|w| self.wm.windows.get(&w)) {
66- match (w.toplevel(), w.x11_surface()) {
67- (Some(t), _) => t.send_close(),
68- (_, Some(x)) => {
69- let _ = x.close();
70- }
71- _ => {}
72- }
65+ if let Some(w) = focused.and_then(|w| self.wm.window(w)) {
66+ crate::wm::close(w);
7367 }
7468 }
7569 Command::Quit => self.core.signal.stop(),
Modified file: crates/monolith/src/drm.rs+7 additions−18 deletions
@@ -608,15 +608,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
608608 let State {
609609 backend: Backend::Drm(drm),
610610 renderer,
611- wm:
612- Wm {
613- layout,
614- windows,
615- anim,
616- borders,
617- space,
618- ..
619- },
611+ wm,
620612 settings,
621613 core,
622614 cursor,
@@ -648,7 +640,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
648640 && match settings.config.output.vrr {
649641 Vrr::Off => false,
650642 Vrr::On => true,
651- Vrr::Fullscreen => layout.arrange(surface.id).fullscreen.is_some(),
643+ Vrr::Fullscreen => wm.layout.arrange(surface.id).fullscreen.is_some(),
652644 };
653645 if want_vrr != surface.vrr_on {
654646 match surface.drm.with_compositor(|c| c.use_vrr(want_vrr)) {
@@ -668,7 +660,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
668660 }
669661 let began = std::time::Instant::now();
670662 let now = target;
671- let Some(area) = layout.output(surface.id).map(|o| o.area) else {
663+ let Some(area) = wm.layout.output(surface.id).map(|o| o.area) else {
672664 return false;
673665 };
674666 let mut elements = Vec::new();
@@ -680,21 +672,18 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
680672 match crate::lock::elements(lock, renderer, &surface.output) {
681673 Some(locked) => elements.extend(locked),
682674 None => elements.extend(crate::scene::build(
683- layout,
675+ wm,
684676 surface.id,
685- windows,
686- anim,
687677 renderer,
688678 now,
689679 &settings.config.decoration,
690- borders,
691680 Some(&surface.output),
692681 xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
693682 )),
694683 }
695684 let scene_ns = scene_began.elapsed().as_nanos() as u64;
696- let animating = anim.active(now);
697- anim.prune(now);
685+ let animating = wm.anim.active(now);
686+ wm.anim.prune(now);
698687 let render_began = std::time::Instant::now();
699688 let cpu_began = thread_cpu_ns();
700689 let mut queue_ns = 0;
@@ -742,7 +731,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
742731 crate::lock::send_frames(lock, &output, elapsed);
743732 return animating && !surface.queued;
744733 }
745- for window in space.elements_for_output(&output) {
734+ for window in wm.space.elements_for_output(&output) {
746735 window.send_frame(&output, elapsed, Some(Duration::ZERO), |_, _| {
747736 Some(output.clone())
748737 });
Modified file: crates/monolith/src/handlers.rs+8 additions−7 deletions
@@ -72,13 +72,14 @@ impl CompositorHandler for State {
7272 while let Some(parent) = get_parent(&root) {
7373 root = parent;
7474 }
75- if let Some(window) = self
76- .wm
77- .windows
78- .values()
79- .chain(&self.wm.unmapped)
80- .find(|w| crate::state::root(w).as_ref() == Some(&root))
81- {
75+ let mapped = self.wm.id_of(&root).and_then(|id| self.wm.window(id));
76+ let window = mapped.or_else(|| {
77+ self.wm
78+ .unmapped
79+ .iter()
80+ .find(|w| crate::state::root(w).as_ref() == Some(&root))
81+ });
82+ if let Some(window) = window {
8283 window.on_commit();
8384 }
8485 if self.services.captures.wants_damage()
Modified file: crates/monolith/src/ipc.rs+1 additions−1 deletions
@@ -173,7 +173,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
173173 let mut windows: Vec<monolith_ipc::Window> = l
174174 .windows()
175175 .map(|(id, w)| {
176- let (app_id, title) = state.wm.windows.get(&id).map(identity).unwrap_or_default();
176+ let (app_id, title) = state.wm.window(id).map(identity).unwrap_or_default();
177177 monolith_ipc::Window {
178178 id: slotmap::Key::data(&id).as_ffi(),
179179 app_id,
Modified file: crates/monolith/src/nested/mod.rs+5 additions−17 deletions
@@ -8,7 +8,6 @@ mod input;
88 use crate::backend::Backend;
99 use crate::state::Services;
1010 use crate::state::State;
11-use crate::wm::Wm;
1211 use calloop_wayland_source::WaylandSource;
1312 use monolith_layout::{OutputId, Rect};
1413 use monolith_render_vk::VulkanAllocator;
@@ -175,15 +174,7 @@ pub fn render(state: &mut State) {
175174 let State {
176175 backend,
177176 renderer,
178- wm:
179- Wm {
180- layout,
181- windows,
182- anim,
183- borders,
184- space,
185- ..
186- },
177+ wm,
187178 needs_redraw,
188179 core,
189180 settings,
@@ -242,14 +233,11 @@ pub fn render(state: &mut State) {
242233 let o = host.output_id;
243234 let elements = crate::lock::elements(lock, renderer, &host.output).unwrap_or_else(|| {
244235 crate::scene::build(
245- layout,
236+ wm,
246237 o,
247- windows,
248- anim,
249238 renderer,
250239 now,
251240 &settings.config.decoration,
252- borders,
253241 Some(&host.output),
254242 xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
255243 )
@@ -270,12 +258,12 @@ pub fn render(state: &mut State) {
270258 }
271259 }
272260 };
273- *needs_redraw = anim.active(now);
274- anim.prune(now);
261+ *needs_redraw = wm.anim.active(now);
262+ wm.anim.prune(now);
275263 lock.shown(&host.output);
276264 let elapsed = core.start.elapsed();
277265 crate::lock::send_frames(lock, &host.output, elapsed);
278- for window in space.elements().filter(|_| !lock.active()) {
266+ for window in wm.space.elements().filter(|_| !lock.active()) {
279267 window.send_frame(&host.output, elapsed, Some(Duration::ZERO), |_, _| {
280268 Some(host.output.clone())
281269 });
Modified file: crates/monolith/src/scene.rs+10 additions−22 deletions
@@ -1,9 +1,9 @@
11 //! What a frame draws. The space holds final geometry for input, while the
22 //! scene draws windows where the animator says they are right now.
33
4-use crate::anim::Animator;
4+use crate::wm::Wm;
55 use monolith_config::Decoration;
6-use monolith_layout::{Layout, OutputId, Rect, WindowId};
6+use monolith_layout::{OutputId, Rect};
77 use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
88 use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
99 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
@@ -21,7 +21,6 @@ use smithay::utils::user_data::UserDataMap;
2121 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
2222 use smithay::wayland::shell::wlr_layer::Layer;
2323 use smithay::xwayland::X11Surface;
24-use std::collections::HashMap;
2524
2625 smithay::backend::renderer::element::render_elements! {
2726 pub Draw<=VulkanRenderer>;
@@ -34,26 +33,18 @@ smithay::backend::renderer::element::render_elements! {
3433 Rescaled=RescaleRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
3534 }
3635
37-/// Border strips per window, kept between frames so unchanged borders cause
38-/// no damage.
39-#[derive(Default)]
40-pub struct Borders(HashMap<WindowId, [SolidColorBuffer; 4]>);
41-
4236 /// Elements for output `o` in its own coordinates, front to back.
43-#[expect(clippy::too_many_arguments)]
4437 #[profiling::function]
4538 pub fn build(
46- layout: &Layout,
39+ wm: &mut Wm,
4740 o: OutputId,
48- windows: &HashMap<WindowId, Window>,
49- anim: &Animator,
5041 renderer: &mut VulkanRenderer,
5142 now: u64,
5243 deco: &Decoration,
53- borders: &mut Borders,
5444 output: Option<&Output>,
5545 unmanaged: &[X11Surface],
5646 ) -> Vec<Draw> {
47+ let (layout, anim, windows) = wm.parts();
5748 let Some(area) = layout.output(o).map(|out| out.area) else {
5849 return Vec::new();
5950 };
@@ -90,7 +81,6 @@ pub fn build(
9081 let glide = anim.gliding(w, now);
9182 above.push((w, local(glide.unwrap_or(area)), false, glide.is_some()));
9283 }
93- borders.0.retain(|id, _| windows.contains_key(id));
9484 let focused = layout.focused();
9585 let mut out = Vec::new();
9686 for x in unmanaged.iter().rev() {
@@ -112,11 +102,11 @@ pub fn build(
112102 }
113103 }
114104 for (id, r, framed, gliding) in tiled.into_iter().chain(above).rev() {
115- let Some(window) = windows.get(&id) else {
105+ let Some(state) = windows.get_mut(id) else {
116106 continue;
117107 };
118108 let effect = anim.effect(id, now);
119- window_elements(&mut out, renderer, window, r, effect, gliding);
109+ window_elements(&mut out, renderer, &state.window, r, effect, gliding);
120110 if framed && deco.border > 0 {
121111 let color = if focused == Some(id) {
122112 deco.focused.0
@@ -124,7 +114,8 @@ pub fn build(
124114 deco.unfocused.0
125115 };
126116 let alpha = effect.map_or(1.0, |e| e.progress);
127- border_elements(&mut out, borders, id, r, deco.border as i32, color, alpha);
117+ let borders = &mut state.borders;
118+ border_elements(&mut out, borders, r, deco.border as i32, color, alpha);
128119 }
129120 }
130121 for (g, r) in ghosts.into_iter().rev() {
@@ -242,8 +233,7 @@ fn window_elements(
242233 /// Four strips just outside `r`, in the gap, so the client keeps its size.
243234 fn border_elements(
244235 out: &mut Vec<Draw>,
245- borders: &mut Borders,
246- id: WindowId,
236+ borders: &mut Option<[SolidColorBuffer; 4]>,
247237 r: Rect,
248238 b: i32,
249239 color: [f32; 4],
@@ -259,9 +249,7 @@ fn border_elements(
259249 ((x + w, y), (b, h)),
260250 ];
261251 let buffers = borders
262- .0
263- .entry(id)
264- .or_insert_with(|| std::array::from_fn(|_| SolidColorBuffer::new((0, 0), color)));
252+ .get_or_insert_with(|| std::array::from_fn(|_| SolidColorBuffer::new((0, 0), color)));
265253 for (buffer, (at, size)) in buffers.iter_mut().zip(strips) {
266254 buffer.update(size, color);
267255 out.push(Draw::Border(SolidColorRenderElement::from_buffer(
Modified file: crates/monolith/src/state.rs+38 additions−47 deletions
@@ -11,7 +11,7 @@ use monolith_render_vk::VulkanTexture;
1111 use smithay::backend::renderer::Renderer;
1212 use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
1313 use smithay::desktop::utils::under_from_surface_tree;
14-use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
14+use smithay::desktop::{Window, WindowSurfaceType};
1515 use smithay::input::keyboard::{Keycode, XkbConfig};
1616 use smithay::input::pointer::CursorImageStatus;
1717 use smithay::input::{Seat, SeatState};
@@ -149,20 +149,7 @@ impl State {
149149 path: config_path,
150150 },
151151 renderer,
152- wm: Wm {
153- layout,
154- windows: HashMap::new(),
155- unmapped: Vec::new(),
156- space: Space::default(),
157- popups: PopupManager::default(),
158- anim: Animator::new(animation),
159- borders: Default::default(),
160- last_kind: HashMap::new(),
161- awaiting: HashSet::new(),
162- placed_by_rule: HashSet::new(),
163- list_handles: Default::default(),
164- layer_focus: None,
165- },
152+ wm: Wm::new(layout, Animator::new(animation)),
166153 backend: crate::backend::Backend::Starting,
167154 cursor: crate::cursor::Cursor::load(),
168155 cursor_status: CursorImageStatus::default_named(),
@@ -258,9 +245,6 @@ impl State {
258245 )
259246 });
260247 let id = self.wm.layout.add_window(output, tag, float);
261- if rule.tag.is_some() || rule.output.is_some() {
262- self.wm.placed_by_rule.insert(id);
263- }
264248 if rule.floating == Some(true) {
265249 self.wm.layout.toggle_floating(id);
266250 }
@@ -270,7 +254,8 @@ impl State {
270254 if let Some(name) = &rule.scratchpad {
271255 self.wm.layout.scratchpad_add(id, name);
272256 }
273- self.wm.windows.insert(id, window);
257+ self.wm
258+ .insert(id, window, rule.tag.is_some() || rule.output.is_some());
274259 self.await_first_buffer(id);
275260 if self.wm.layout.visible(id) {
276261 self.wm.layout.focus(id);
@@ -282,7 +267,6 @@ impl State {
282267 /// spawn animation never plays over an empty slot. Clients that are slow
283268 /// to draw get it anyway after a while.
284269 fn await_first_buffer(&mut self, id: WindowId) {
285- self.wm.awaiting.insert(id);
286270 let timeout = Timer::from_duration(Duration::from_millis(300));
287271 let _ = self.core.handle.insert_source(timeout, move |_, _, state| {
288272 state.first_buffer(id);
@@ -292,7 +276,7 @@ impl State {
292276
293277 /// Called on commits, starts the spawn of a window that now has a buffer.
294278 pub fn on_buffer(&mut self, surface: &WlSurface) {
295- if self.wm.awaiting.is_empty() {
279+ if !self.wm.any_awaiting() {
296280 return;
297281 }
298282 let has_buffer =
@@ -303,8 +287,11 @@ impl State {
303287 }
304288
305289 fn first_buffer(&mut self, id: WindowId) {
306- self.wm.placed_by_rule.remove(&id);
307- if self.wm.awaiting.remove(&id) {
290+ let Some(w) = self.wm.get_mut(id) else {
291+ return;
292+ };
293+ w.placed_by_rule = false;
294+ if std::mem::take(&mut w.awaiting_buffer) {
308295 self.wm.anim.spawned(id, now_ns());
309296 self.apply_layout();
310297 }
@@ -320,12 +307,11 @@ impl State {
320307 }
321308
322309 pub fn remove_window(&mut self, id: WindowId) {
323- if let Some(window) = self.wm.windows.remove(&id) {
324- self.wm.space.unmap_elem(&window);
325- self.leave_ghost(id, &window);
310+ if let Some(state) = self.wm.remove(id) {
311+ self.wm.space.unmap_elem(&state.window);
312+ self.leave_ghost(id, &state.window);
326313 }
327314 self.wm.layout.remove_window(id);
328- self.wm.placed_by_rule.remove(&id);
329315 self.apply_layout();
330316 }
331317
@@ -379,11 +365,7 @@ impl State {
379365 }
380366
381367 pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
382- self.wm
383- .windows
384- .iter()
385- .find(|(_, w)| root(w).as_ref() == Some(surface))
386- .map(|(id, _)| *id)
368+ self.wm.id_of(surface)
387369 }
388370
389371 /// Pushes the layout's target geometry into the space and to clients.
@@ -412,7 +394,7 @@ impl State {
412394 .tiled
413395 .iter()
414396 .map(|p| p.window)
415- .filter(|w| self.wm.awaiting.contains(w))
397+ .filter(|&w| self.wm.get(w).is_some_and(|s| s.awaiting_buffer))
416398 .collect();
417399 let planned = if held.is_empty() {
418400 arr.tiled.clone()
@@ -426,8 +408,8 @@ impl State {
426408
427409 let mut glides = Vec::new();
428410 for &(id, rect, kind, _) in &shown {
429- match self.wm.last_kind.get(&id) {
430- Some(&(was, from, o)) if was != kind => {
411+ match self.wm.get(id).and_then(|s| s.placed) {
412+ Some((was, from, o)) if was != kind => {
431413 let from = self
432414 .wm
433415 .anim
@@ -440,7 +422,7 @@ impl State {
440422 .unwrap_or(from);
441423 glides.push((id, from, rect));
442424 }
443- Some(&(Kind::Floating, from, _)) if from != rect => self.wm.anim.stop_glide(id),
425+ Some((Kind::Floating, from, _)) if from != rect => self.wm.anim.stop_glide(id),
444426 _ => {}
445427 }
446428 }
@@ -469,31 +451,39 @@ impl State {
469451 for (id, from, to) in glides {
470452 self.wm.anim.glide(id, from, to, now);
471453 }
472- self.wm.last_kind = shown.iter().map(|&(id, r, k, o)| (id, (k, r, o))).collect();
454+ for (_, s) in self.wm.iter_mut() {
455+ s.placed = None;
456+ }
457+ for &(id, r, k, o) in &shown {
458+ if let Some(s) = self.wm.get_mut(id) {
459+ s.placed = Some((k, r, o));
460+ }
461+ }
473462
474463 let mut mapped = Vec::new();
475464 for (id, rect, kind, _) in shown {
476- if let Some(window) = self.wm.windows.get(&id)
477- && configure(window, rect, kind, self.wm.layout.maximized(id))
465+ if let Some(window) = self.wm.window(id).cloned()
466+ && configure(&window, rect, kind, self.wm.layout.maximized(id))
478467 {
479468 let loc = (rect.x.round() as i32, rect.y.round() as i32);
480469 let loc = Point::from(loc) - window.geometry().loc;
481- self.wm.space.map_element(window.clone(), loc, false);
470+ self.wm.space.map_element(window, loc, false);
482471 mapped.push(id);
483472 }
484473 }
485474 let hidden: Vec<Window> = self
486475 .wm
487- .windows
488476 .iter()
489477 .filter(|(id, _)| !mapped.contains(id))
490- .map(|(_, w)| w.clone())
478+ .map(|(_, w)| w.window.clone())
491479 .collect();
492480 for window in hidden {
493481 self.wm.space.unmap_elem(&window);
494482 }
495- for id in &mapped {
496- self.wm.space.raise_element(&self.wm.windows[id], false);
483+ for id in mapped {
484+ if let Some(window) = self.wm.window(id).cloned() {
485+ self.wm.space.raise_element(&window, false);
486+ }
497487 }
498488 self.sync_focus();
499489 self.update_inhibit();
@@ -502,8 +492,9 @@ impl State {
502492
503493 pub fn sync_focus(&mut self) {
504494 let focused = self.wm.layout.focused();
505- for (id, window) in &self.wm.windows {
506- if window.set_activated(Some(*id) == focused)
495+ for (id, s) in self.wm.iter() {
496+ let window = &s.window;
497+ if window.set_activated(Some(id) == focused)
507498 && let Some(t) = window.toplevel()
508499 && t.is_initial_configure_sent()
509500 {
@@ -514,7 +505,7 @@ impl State {
514505 self.lock_keyboard_focus().map(Focus::from)
515506 } else {
516507 self.layer_keyboard_focus().map(Focus::from).or_else(|| {
517- let w = focused.and_then(|id| self.wm.windows.get(&id))?;
508+ let w = focused.and_then(|id| self.wm.window(id))?;
518509 match w.x11_surface() {
519510 Some(x) => Some(Focus::X11(Box::new(x.clone()))),
520511 None => w.toplevel().map(|t| Focus::from(t.wl_surface().clone())),
Modified file: crates/monolith/src/toplevels.rs+12 additions−46 deletions
@@ -7,21 +7,8 @@ use monolith_protocols::toplevel::{
77 Action, ForeignToplevelHandler, ForeignToplevelState, Snapshot, Toplevel,
88 };
99 use smithay::wayland::foreign_toplevel_list::{
10- ForeignToplevelHandle, ForeignToplevelListHandler, ForeignToplevelListState,
10+ ForeignToplevelListHandler, ForeignToplevelListState,
1111 };
12-use std::collections::HashMap;
13-
14-#[derive(Default)]
15-pub struct ListHandles(HashMap<WindowId, ForeignToplevelHandle>);
16-
17-impl ListHandles {
18- pub fn window(&self, toplevel: &ForeignToplevelHandle) -> Option<WindowId> {
19- self.0
20- .iter()
21- .find(|(_, h)| h.identifier() == toplevel.identifier())
22- .map(|(id, _)| *id)
23- }
24-}
2512
2613 pub fn key(id: WindowId) -> u64 {
2714 slotmap::Key::data(&id).as_ffi()
@@ -39,7 +26,7 @@ impl ForeignToplevelHandler for State {
3926 }
4027
4128 fn foreign_toplevel_action(&mut self, action: Action) {
42- let id_of = |k: u64| self.wm.windows.keys().copied().find(|&id| key(id) == k);
29+ let id_of = |k: u64| self.wm.iter().map(|(id, _)| id).find(|&id| key(id) == k);
4330 match action {
4431 Action::Activate(k) => {
4532 let Some(id) = id_of(k) else { return };
@@ -54,15 +41,8 @@ impl ForeignToplevelHandler for State {
5441 self.wm.layout.focus(id);
5542 }
5643 Action::Close(k) => {
57- let Some(window) = id_of(k).and_then(|id| self.wm.windows.get(&id)) else {
58- return;
59- };
60- match (window.toplevel(), window.x11_surface()) {
61- (Some(t), _) => t.send_close(),
62- (_, Some(x)) => {
63- let _ = x.close();
64- }
65- _ => {}
44+ if let Some(window) = id_of(k).and_then(|id| self.wm.window(id)) {
45+ crate::wm::close(window);
6646 }
6747 return;
6848 }
@@ -92,17 +72,10 @@ impl ForeignToplevelHandler for State {
9272 impl State {
9373 /// Brings both window lists in line with the windows.
9474 pub fn publish_toplevels(&mut self) {
95- let mut handles = std::mem::take(&mut self.wm.list_handles.0);
96- handles.retain(|id, h| {
97- let keep = self.wm.windows.contains_key(id);
98- if !keep {
99- self.protocols.toplevel_list.remove_toplevel(h);
100- }
101- keep
102- });
103- for (id, window) in &self.wm.windows {
104- let (app_id, title) = identity(window);
105- match handles.get(id) {
75+ let list = &mut self.protocols.toplevel_list;
76+ for (_, w) in self.wm.iter_mut() {
77+ let (app_id, title) = identity(&w.window);
78+ match &w.list_handle {
10679 Some(h) => {
10780 if h.title() != title || h.app_id() != app_id {
10881 h.send_title(&title);
@@ -110,16 +83,10 @@ impl State {
11083 h.send_done();
11184 }
11285 }
113- None => {
114- let h = self
115- .protocols
116- .toplevel_list
117- .new_toplevel::<State>(title, app_id);
118- handles.insert(*id, h);
119- }
86+ None => w.list_handle = Some(list.new_toplevel::<State>(title, app_id)),
12087 }
12188 }
122- self.wm.list_handles.0 = handles;
89+ list.cleanup_closed_handles();
12390
12491 if !self.protocols.foreign_toplevels.has_clients() {
12592 return;
@@ -127,10 +94,9 @@ impl State {
12794 let focused = self.wm.layout.focused();
12895 let toplevels = self
12996 .wm
130- .windows
13197 .iter()
132- .map(|(&id, window)| {
133- let (app_id, title) = identity(window);
98+ .map(|(id, w)| {
99+ let (app_id, title) = identity(&w.window);
134100 let w = self.wm.layout.window(id);
135101 Toplevel {
136102 key: key(id),
Modified file: crates/monolith/src/wm.rs+118 additions−12 deletions
@@ -1,29 +1,135 @@
1-//! The window model: the layout and what monolith keeps per window beside it.
1+//! The window model: the layout, and one record per window for everything
2+//! monolith keeps beside it.
23
34 use crate::anim::Animator;
4-use crate::state::Kind;
5+use crate::state::{Kind, root};
56 use monolith_layout::{Layout, OutputId, Rect, WindowId};
7+use slotmap::SecondaryMap;
8+use smithay::backend::renderer::element::solid::SolidColorBuffer;
69 use smithay::desktop::{PopupManager, Space, Window};
710 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8-use std::collections::{HashMap, HashSet};
11+use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
12+use std::collections::HashMap;
913
1014 pub struct Wm {
1115 pub layout: Layout,
12- pub windows: HashMap<WindowId, Window>,
16+ windows: SecondaryMap<WindowId, WindowState>,
17+ by_surface: HashMap<WlSurface, WindowId>,
1318 /// Toplevels that have not committed yet. Rules need their app_id and
1419 /// title, which clients set after creating the toplevel.
1520 pub unmapped: Vec<Window>,
1621 pub space: Space<Window>,
1722 pub popups: PopupManager,
1823 pub anim: Animator,
19- pub borders: crate::scene::Borders,
20- /// How each shown window was placed by the last layout pass, to spot
21- /// switches between tiled, floating and fullscreen.
22- pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
23- pub awaiting: HashSet<WindowId>,
24- /// Windows a rule sent somewhere, which activation tokens leave alone.
25- pub placed_by_rule: HashSet<WindowId>,
26- pub list_handles: crate::toplevels::ListHandles,
2724 /// A layer surface given the keyboard by a click, like a launcher.
2825 pub layer_focus: Option<WlSurface>,
2926 }
27+
28+pub struct WindowState {
29+ pub window: Window,
30+ /// How the last layout pass placed it, to spot switches between tiled,
31+ /// floating and fullscreen.
32+ pub placed: Option<(Kind, Rect, OutputId)>,
33+ /// Held on its seed until its first buffer lands.
34+ pub awaiting_buffer: bool,
35+ /// A rule sent it somewhere, so activation tokens leave it alone.
36+ pub placed_by_rule: bool,
37+ pub list_handle: Option<ForeignToplevelHandle>,
38+ pub borders: Option<[SolidColorBuffer; 4]>,
39+}
40+
41+/// Asks a window to close, through xdg-shell or X11.
42+pub fn close(window: &Window) {
43+ match (window.toplevel(), window.x11_surface()) {
44+ (Some(t), _) => t.send_close(),
45+ (_, Some(x)) => {
46+ if let Err(e) = x.close() {
47+ tracing::debug!("closing an X11 window failed: {e}");
48+ }
49+ }
50+ _ => {}
51+ }
52+}
53+
54+impl Wm {
55+ pub fn new(layout: Layout, anim: Animator) -> Self {
56+ Self {
57+ layout,
58+ windows: SecondaryMap::new(),
59+ by_surface: HashMap::new(),
60+ unmapped: Vec::new(),
61+ space: Space::default(),
62+ popups: PopupManager::default(),
63+ anim,
64+ layer_focus: None,
65+ }
66+ }
67+
68+ pub fn insert(&mut self, id: WindowId, window: Window, placed_by_rule: bool) {
69+ self.windows.insert(
70+ id,
71+ WindowState {
72+ window,
73+ placed: None,
74+ awaiting_buffer: true,
75+ placed_by_rule,
76+ list_handle: None,
77+ borders: None,
78+ },
79+ );
80+ self.index(id);
81+ }
82+
83+ /// Drops every trace of a window. The layout and space are the caller's.
84+ pub fn remove(&mut self, id: WindowId) -> Option<WindowState> {
85+ let state = self.windows.remove(id)?;
86+ self.by_surface.retain(|_, w| *w != id);
87+ if let Some(h) = &state.list_handle {
88+ h.send_closed();
89+ }
90+ self.anim.forget(id);
91+ Some(state)
92+ }
93+
94+ /// Indexes a window by its surface. X11 windows only have one once
95+ /// xwayland-shell associates it, which may be after they are mapped.
96+ pub fn index(&mut self, id: WindowId) {
97+ if let Some(surface) = self.windows.get(id).and_then(|w| root(&w.window)) {
98+ self.by_surface.insert(surface, id);
99+ }
100+ }
101+
102+ pub fn id_of(&self, surface: &WlSurface) -> Option<WindowId> {
103+ self.by_surface.get(surface).copied()
104+ }
105+
106+ pub fn get(&self, id: WindowId) -> Option<&WindowState> {
107+ self.windows.get(id)
108+ }
109+
110+ pub fn get_mut(&mut self, id: WindowId) -> Option<&mut WindowState> {
111+ self.windows.get_mut(id)
112+ }
113+
114+ pub fn window(&self, id: WindowId) -> Option<&Window> {
115+ self.windows.get(id).map(|w| &w.window)
116+ }
117+
118+ pub fn iter(&self) -> impl Iterator<Item = (WindowId, &WindowState)> {
119+ self.windows.iter()
120+ }
121+
122+ pub fn iter_mut(&mut self) -> impl Iterator<Item = (WindowId, &mut WindowState)> {
123+ self.windows.iter_mut()
124+ }
125+
126+ /// The layout and animator to read alongside the records to write, which
127+ /// a frame needs at once.
128+ pub fn parts(&mut self) -> (&Layout, &Animator, &mut SecondaryMap<WindowId, WindowState>) {
129+ (&self.layout, &self.anim, &mut self.windows)
130+ }
131+
132+ pub fn any_awaiting(&self) -> bool {
133+ self.windows.values().any(|w| w.awaiting_buffer)
134+ }
135+}
Modified file: crates/monolith/src/xwayland.rs+8 additions−7 deletions
@@ -217,7 +217,10 @@ impl XWaylandShellHandler for State {
217217 &mut self.protocols.xwayland_shell_state
218218 }
219219
220- fn surface_associated(&mut self, _xwm: XwmId, surface: WlSurface, _window: X11Surface) {
220+ fn surface_associated(&mut self, _xwm: XwmId, surface: WlSurface, window: X11Surface) {
221+ if let Some(id) = self.x11_window_id(&window) {
222+ self.wm.index(id);
223+ }
221224 self.on_buffer(&surface);
222225 self.needs_redraw = true;
223226 }
@@ -321,10 +324,9 @@ impl XwmHandler for State {
321324 tracing::info!("Xwayland exited");
322325 let gone: Vec<_> = self
323326 .wm
324- .windows
325327 .iter()
326- .filter(|(_, w)| w.x11_surface().is_some())
327- .map(|(id, _)| *id)
328+ .filter(|(_, w)| w.window.x11_surface().is_some())
329+ .map(|(id, _)| id)
328330 .collect();
329331 for id in gone {
330332 self.remove_window(id);
@@ -359,10 +361,9 @@ fn floats(window: &X11Surface) -> bool {
359361 impl State {
360362 pub fn x11_window_id(&self, window: &X11Surface) -> Option<monolith_layout::WindowId> {
361363 self.wm
362- .windows
363364 .iter()
364- .find(|(_, w)| w.x11_surface() == Some(window))
365- .map(|(id, _)| *id)
365+ .find(|(_, w)| w.window.x11_surface() == Some(window))
366+ .map(|(id, _)| id)
366367 }
367368
368369 fn forget_x11(&mut self, window: &X11Surface) {