atagen

monolith

public
Star0
Watch0

Commit a76842e12aca

tarball

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

16 files changed+276-212
monolith: move the window model into Wm

Layout, the smithay windows and space, popups, the animator and every per-window side table now sit together. This is a pure move, so the following change can merge the side tables into one record per window without also chasing field paths.
Modified file: crates/monolith/src/activation.rs+8 additions−8 deletions
@@ -52,17 +52,17 @@ impl XdgActivationHandler for State {
5252 return;
5353 };
5454 let ctx = data.user_data.get::<Context>();
55- if self.awaiting.contains(&id)
55+ if self.wm.awaiting.contains(&id)
5656 && let Some(ctx) = ctx
57- && !self.placed_by_rule.contains(&id)
57+ && !self.wm.placed_by_rule.contains(&id)
5858 {
59- self.layout.relocate(id, ctx.output, ctx.tag);
59+ self.wm.layout.relocate(id, ctx.output, ctx.tag);
6060 }
6161 let may_focus = ctx.is_some_and(|c| c.may_focus) && data.timestamp.elapsed() < FRESH;
62- if may_focus && self.layout.visible(id) {
63- self.layout.focus(id);
62+ if may_focus && self.wm.layout.visible(id) {
63+ self.wm.layout.focus(id);
6464 } else {
65- self.layout.mark_urgent(id);
65+ self.wm.layout.mark_urgent(id);
6666 }
6767 self.apply_layout();
6868 }
@@ -71,8 +71,8 @@ impl XdgActivationHandler for State {
7171 impl State {
7272 /// Where a launch starting now should put its window.
7373 pub fn launch_context(&self) -> (Option<OutputId>, TagId) {
74- let output = self.layout.active_output();
75- let tag = output.map_or(self.layout.homeless(), |o| self.layout.launch_tag(o));
74+ let output = self.wm.layout.active_output();
75+ let tag = output.map_or(self.wm.layout.homeless(), |o| self.wm.layout.launch_tag(o));
7676 (output, tag)
7777 }
7878
Modified file: crates/monolith/src/capture.rs+14 additions−10 deletions
@@ -87,7 +87,7 @@ impl ToplevelCaptureSourceHandler for State {
8787 source: ImageCaptureSource,
8888 toplevel: ForeignToplevelHandle,
8989 ) {
90- if let Some(id) = self.list_handles.window(&toplevel) {
90+ if let Some(id) = self.wm.list_handles.window(&toplevel) {
9191 source.user_data().insert_if_missing(|| id);
9292 }
9393 }
@@ -151,13 +151,16 @@ impl State {
151151 return Some(Target::Output(output.upgrade()?.name()));
152152 }
153153 let id = *data.get::<WindowId>()?;
154- self.windows.contains_key(&id).then_some(Target::Window(id))
154+ self.wm
155+ .windows
156+ .contains_key(&id)
157+ .then_some(Target::Window(id))
155158 }
156159
157160 fn target_size(&self, target: &Target) -> Option<(i32, i32)> {
158161 let size: (i32, i32) = match target {
159162 Target::Output(name) => self.output_named(name)?.1.current_mode()?.size.into(),
160- Target::Window(id) => self.windows.get(id)?.geometry().size.into(),
163+ Target::Window(id) => self.wm.windows.get(id)?.geometry().size.into(),
161164 };
162165 (size.0 > 0 && size.1 > 0).then_some(size)
163166 }
@@ -202,7 +205,7 @@ impl State {
202205 };
203206 self.output_elements(o, &output, session.draw_cursor())
204207 }
205- Target::Window(id) => window_elements(&mut self.renderer, &self.windows[id]),
208+ Target::Window(id) => window_elements(&mut self.renderer, &self.wm.windows[id]),
206209 };
207210 let buffer = frame.buffer();
208211 let result = match get_dmabuf(&buffer) {
@@ -268,7 +271,7 @@ impl State {
268271 pub fn output_elements(&mut self, o: OutputId, output: &Output, cursor: bool) -> Vec<Draw> {
269272 let now = now_ns();
270273 let mut elements = Vec::new();
271- if cursor && let Some(area) = self.layout.output(o).map(|out| out.area) {
274+ if cursor && let Some(area) = self.wm.layout.output(o).map(|out| out.area) {
272275 let local = self.pointer_pos - Point::from((area.x, area.y));
273276 if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
274277 elements.extend(self.cursor.elements(
@@ -281,14 +284,14 @@ impl State {
281284 match crate::lock::elements(&self.services.lock, &mut self.renderer, output) {
282285 Some(locked) => elements.extend(locked),
283286 None => elements.extend(crate::scene::build(
284- &self.layout,
287+ &self.wm.layout,
285288 o,
286- &self.windows,
287- &self.anim,
289+ &self.wm.windows,
290+ &self.wm.anim,
288291 &mut self.renderer,
289292 now,
290293 &self.settings.config.decoration,
291- &mut self.borders,
294+ &mut self.wm.borders,
292295 Some(output),
293296 self.services
294297 .xwayland
@@ -300,7 +303,8 @@ impl State {
300303 }
301304
302305 fn output_named(&self, name: &str) -> Option<(OutputId, Output)> {
303- self.layout
306+ self.wm
307+ .layout
304308 .outputs()
305309 .filter_map(|(o, _)| Some((o, self.backend.output(o)?)))
306310 .find(|(_, out)| out.name() == name)
Modified file: crates/monolith/src/commands.rs+34 additions−33 deletions
@@ -50,19 +50,19 @@ fn dir(d: monolith_ipc::Dir) -> Dir {
5050
5151 impl State {
5252 pub fn run(&mut self, cmd: Command) -> Result<(), String> {
53- let output = self.layout.active_output();
54- let focused = self.layout.focused();
53+ let output = self.wm.layout.active_output();
54+ let focused = self.wm.layout.focused();
5555 let inverted = std::mem::take(&mut self.move_inverted);
5656 let merge = (self.settings.config.general.move_mode == MoveMode::Merge) != inverted;
5757 let on_focused = |state: &mut Self, f: fn(&mut monolith_layout::Layout, WindowId)| {
5858 if let Some(w) = focused {
59- f(&mut state.layout, w);
59+ f(&mut state.wm.layout, w);
6060 }
6161 };
6262 match cmd {
6363 Command::Spawn { argv } => self.spawn(&argv)?,
6464 Command::Close => {
65- if let Some(w) = focused.and_then(|w| self.windows.get(&w)) {
65+ if let Some(w) = focused.and_then(|w| self.wm.windows.get(&w)) {
6666 match (w.toplevel(), w.x11_surface()) {
6767 (Some(t), _) => t.send_close(),
6868 (_, Some(x)) => {
@@ -75,19 +75,19 @@ impl State {
7575 Command::Quit => self.core.signal.stop(),
7676 Command::Reload => self.reload()?,
7777 Command::Focus { dir: d } => {
78- self.layout.focus_dir(dir(d));
78+ self.wm.layout.focus_dir(dir(d));
7979 }
8080 Command::Move { dir: d } if merge => {
81- self.layout.merge_window(dir(d));
81+ self.wm.layout.merge_window(dir(d));
8282 }
8383 Command::Move { dir: d } => {
84- self.layout.move_window(dir(d));
84+ self.wm.layout.move_window(dir(d));
8585 }
8686 Command::MoveContainer { dir: d } => {
87- self.layout.move_container(dir(d));
87+ self.wm.layout.move_container(dir(d));
8888 }
8989 Command::Maximize => {
90- self.layout.expand(ExpandScope::All);
90+ self.wm.layout.expand(ExpandScope::All);
9191 }
9292 Command::InvertMove => self.move_inverted = !inverted,
9393 Command::Resize { axis, delta } => {
@@ -95,73 +95,74 @@ impl State {
9595 monolith_ipc::Axis::Horizontal => Axis::Horizontal,
9696 monolith_ipc::Axis::Vertical => Axis::Vertical,
9797 };
98- self.layout.resize(axis, delta);
98+ self.wm.layout.resize(axis, delta);
9999 }
100100 Command::Expand { scope } => {
101- self.layout.expand(match scope {
101+ self.wm.layout.expand(match scope {
102102 monolith_ipc::ExpandScope::Window => ExpandScope::Window,
103103 monolith_ipc::ExpandScope::Container => ExpandScope::Container,
104104 });
105105 }
106- Command::Flip => output.into_iter().for_each(|o| self.layout.flip(o)),
106+ Command::Flip => output.into_iter().for_each(|o| self.wm.layout.flip(o)),
107107 Command::Float => on_focused(self, |l, w| l.toggle_floating(w)),
108108 Command::Fullscreen => on_focused(self, |l, w| l.toggle_fullscreen(w)),
109109 Command::Sticky => on_focused(self, |l, w| l.toggle_sticky(w)),
110110 Command::View { tag } => {
111- let t = self.layout.tag(&tag);
112- output.into_iter().for_each(|o| self.layout.view(o, t));
111+ let t = self.wm.layout.tag(&tag);
112+ output.into_iter().for_each(|o| self.wm.layout.view(o, t));
113113 }
114114 Command::ToggleView { tag } => {
115- let t = self.layout.tag(&tag);
115+ let t = self.wm.layout.tag(&tag);
116116 output
117117 .into_iter()
118- .for_each(|o| self.layout.toggle_view(o, t));
118+ .for_each(|o| self.wm.layout.toggle_view(o, t));
119119 }
120120 Command::ViewEverywhere { tag } => {
121- let t = self.layout.tag(&tag);
122- self.layout.view_everywhere(t);
121+ let t = self.wm.layout.tag(&tag);
122+ self.wm.layout.view_everywhere(t);
123123 }
124124 Command::Tag { tag } => {
125- let t = self.layout.tag(&tag);
125+ let t = self.wm.layout.tag(&tag);
126126 if let Some(w) = focused {
127- self.layout.set_tag(w, t);
127+ self.wm.layout.set_tag(w, t);
128128 }
129129 }
130130 Command::ReorderTag { tag, index } => {
131131 let t = self
132+ .wm
132133 .layout
133134 .tag_id(&tag)
134135 .ok_or_else(|| format!("no tag {tag}"))?;
135- self.layout.reorder_tag(t, index);
136+ self.wm.layout.reorder_tag(t, index);
136137 }
137138 Command::FocusOutput { dir: d } => {
138- if let Some(o) = output.and_then(|o| self.layout.output_towards(o, dir(d))) {
139- self.layout.set_active_output(o);
140- if let Some(w) = self.layout.focused_on(o) {
141- self.layout.focus(w);
139+ if let Some(o) = output.and_then(|o| self.wm.layout.output_towards(o, dir(d))) {
140+ self.wm.layout.set_active_output(o);
141+ if let Some(w) = self.wm.layout.focused_on(o) {
142+ self.wm.layout.focus(w);
142143 }
143144 }
144145 }
145146 Command::SendToOutput { dir: d } => {
146147 if let (Some(w), Some(o)) = (
147148 focused,
148- output.and_then(|o| self.layout.output_towards(o, dir(d))),
149+ output.and_then(|o| self.wm.layout.output_towards(o, dir(d))),
149150 ) {
150- self.layout.send_to_output(w, o);
151+ self.wm.layout.send_to_output(w, o);
151152 }
152153 }
153154 Command::Scratchpad { name } => {
154155 if let Some(w) = focused {
155- self.layout.scratchpad_add(w, &name);
156+ self.wm.layout.scratchpad_add(w, &name);
156157 }
157158 }
158159 Command::Summon { name } => {
159- if !self.layout.scratchpads().iter().any(|s| s.name == name) {
160+ if !self.wm.layout.scratchpads().iter().any(|s| s.name == name) {
160161 return Err(format!("no scratchpad {name}"));
161162 }
162- self.layout.summon(&name);
163+ self.wm.layout.summon(&name);
163164 }
164- Command::Dismiss { name } => self.layout.dismiss(name.as_deref()),
165+ Command::Dismiss { name } => self.wm.layout.dismiss(name.as_deref()),
165166 }
166167 self.apply_layout();
167168 Ok(())
@@ -200,8 +201,8 @@ impl State {
200201 let config = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;
201202 let keyboard_changed = config.keyboard != self.settings.config.keyboard;
202203 self.settings.binds = Binds::new(&config);
203- self.layout.gaps = crate::state::gaps(&config);
204- self.anim.settings = config.animation.clone();
204+ self.wm.layout.gaps = crate::state::gaps(&config);
205+ self.wm.anim.settings = config.animation.clone();
205206 self.settings.config = config;
206207 if keyboard_changed {
207208 self.reconfigure_keyboard();
Modified file: crates/monolith/src/drm.rs+15 additions−11 deletions
@@ -6,6 +6,7 @@ use crate::backend::Backend;
66 use crate::scene::Draw;
77 use crate::state::Services;
88 use crate::state::{State, now_ns};
9+use crate::wm::Wm;
910 use monolith_config::Vrr;
1011 use monolith_layout::{OutputId, Rect};
1112 use smithay::backend::allocator::Fourcc;
@@ -342,8 +343,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
342343 let State {
343344 backend: Backend::Drm(drm),
344345 renderer,
345- layout,
346- space,
346+ wm: Wm { layout, space, .. },
347347 core,
348348 ..
349349 } = state
@@ -446,8 +446,8 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
446446 drm.handle.remove(token);
447447 }
448448 tracing::info!(name = surface.output.name(), "output disconnected");
449- state.space.unmap_output(&surface.output);
450- state.layout.remove_output(surface.id);
449+ state.wm.space.unmap_output(&surface.output);
450+ state.wm.layout.remove_output(surface.id);
451451 }
452452
453453 impl Drm {
@@ -608,12 +608,16 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
608608 let State {
609609 backend: Backend::Drm(drm),
610610 renderer,
611- layout,
612- windows,
613- anim,
614- borders,
611+ wm:
612+ Wm {
613+ layout,
614+ windows,
615+ anim,
616+ borders,
617+ space,
618+ ..
619+ },
615620 settings,
616- space,
617621 core,
618622 cursor,
619623 cursor_status,
@@ -769,7 +773,7 @@ fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
769773 state.cursor_moved = true;
770774 }
771775 InputEvent::PointerMotionAbsolute { event } => {
772- let Some(area) = state.layout.outputs().next().map(|(_, o)| o.area) else {
776+ let Some(area) = state.wm.layout.outputs().next().map(|(_, o)| o.area) else {
773777 return;
774778 };
775779 let p = event.position_transformed((area.w as i32, area.h as i32).into());
@@ -814,7 +818,7 @@ fn clamp(
814818 let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| {
815819 p.x >= a.x && p.y >= a.y && p.x < a.right() && p.y < a.bottom()
816820 };
817- let areas: Vec<Rect> = state.layout.outputs().map(|(_, o)| o.area).collect();
821+ let areas: Vec<Rect> = state.wm.layout.outputs().map(|(_, o)| o.area).collect();
818822 if areas.iter().any(|a| inside(a, pos)) {
819823 return pos;
820824 }
Modified file: crates/monolith/src/handlers.rs+19 additions−12 deletions
@@ -73,9 +73,10 @@ impl CompositorHandler for State {
7373 root = parent;
7474 }
7575 if let Some(window) = self
76+ .wm
7677 .windows
7778 .values()
78- .chain(&self.unmapped)
79+ .chain(&self.wm.unmapped)
7980 .find(|w| crate::state::root(w).as_ref() == Some(&root))
8081 {
8182 window.on_commit();
@@ -87,11 +88,12 @@ impl CompositorHandler for State {
8788 }
8889 }
8990 if let Some(i) = self
91+ .wm
9092 .unmapped
9193 .iter()
9294 .position(|w| w.toplevel().is_some_and(|t| t.wl_surface() == surface))
9395 {
94- let window = self.unmapped.remove(i);
96+ let window = self.wm.unmapped.remove(i);
9597 let toplevel = window.toplevel().cloned();
9698 self.map_window(window, None);
9799 if let Some(t) = toplevel {
@@ -99,8 +101,8 @@ impl CompositorHandler for State {
99101 }
100102 }
101103 self.on_buffer(surface);
102- self.popups.commit(surface);
103- if let Some(PopupKind::Xdg(popup)) = self.popups.find_popup(surface)
104+ self.wm.popups.commit(surface);
105+ if let Some(PopupKind::Xdg(popup)) = self.wm.popups.find_popup(surface)
104106 && !popup.is_initial_configure_sent()
105107 {
106108 let _ = popup.send_configure();
@@ -141,7 +143,7 @@ impl XdgShellHandler for State {
141143 }
142144
143145 fn new_toplevel(&mut self, surface: ToplevelSurface) {
144- self.unmapped.push(Window::new_wayland_window(surface));
146+ self.wm.unmapped.push(Window::new_wayland_window(surface));
145147 }
146148
147149 fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
@@ -158,7 +160,7 @@ impl XdgShellHandler for State {
158160
159161 fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
160162 surface.with_pending_state(|s| s.geometry = positioner.get_geometry());
161- let _ = self.popups.track_popup(PopupKind::Xdg(surface));
163+ let _ = self.wm.popups.track_popup(PopupKind::Xdg(surface));
162164 }
163165
164166 fn reposition_request(
@@ -361,9 +363,13 @@ fn server_side(toplevel: &ToplevelSurface) {
361363 impl State {
362364 fn set_fullscreen(&mut self, surface: &WlSurface, on: bool) {
363365 if let Some(id) = self.window_id(surface)
364- && self.layout.window(id).is_some_and(|w| w.fullscreen != on)
366+ && self
367+ .wm
368+ .layout
369+ .window(id)
370+ .is_some_and(|w| w.fullscreen != on)
365371 {
366- self.layout.toggle_fullscreen(id);
372+ self.wm.layout.toggle_fullscreen(id);
367373 self.apply_layout();
368374 }
369375 }
@@ -383,6 +389,7 @@ impl WorkspaceHandler for State {
383389 Action::Create { group, name } => (group, name),
384390 };
385391 let Some(o) = self
392+ .wm
386393 .layout
387394 .outputs()
388395 .find(|(_, out)| out.name == *group)
@@ -390,12 +397,12 @@ impl WorkspaceHandler for State {
390397 else {
391398 continue;
392399 };
393- let t = self.layout.tag(tag);
400+ let t = self.wm.layout.tag(tag);
394401 match action {
395- Action::Activate { .. } | Action::Create { .. } => self.layout.view(o, t),
402+ Action::Activate { .. } | Action::Create { .. } => self.wm.layout.view(o, t),
396403 Action::Deactivate { .. } => {
397- if self.layout.is_viewed(o, t) {
398- self.layout.toggle_view(o, t);
404+ if self.wm.layout.is_viewed(o, t) {
405+ self.wm.layout.toggle_view(o, t);
399406 }
400407 }
401408 }
Modified file: crates/monolith/src/idle.rs+2 additions−1 deletions
@@ -59,7 +59,8 @@ impl State {
5959 while let Some(parent) = get_parent(&root) {
6060 root = parent;
6161 }
62- self.space
62+ self.wm
63+ .space
6364 .elements()
6465 .any(|w| crate::state::root(w).as_ref() == Some(&root))
6566 || self.layer_maps_contain(&root)
Modified file: crates/monolith/src/input.rs+9 additions−7 deletions
@@ -115,7 +115,7 @@ impl State {
115115 );
116116 match action {
117117 Some(Some(Action::Run(cmd))) => {
118- self.layer_focus = None;
118+ self.wm.layer_focus = None;
119119 if let Err(e) = self.run(cmd) {
120120 tracing::warn!("bind failed: {e}");
121121 }
@@ -143,22 +143,23 @@ fn focus_under_pointer(state: &mut State) {
143143 if let Some((layer, surface, _)) = state.layer_under(pos, &layers)
144144 && layer.can_receive_keyboard_focus()
145145 {
146- state.layer_focus = Some(surface);
146+ state.wm.layer_focus = Some(surface);
147147 state.sync_focus();
148148 return;
149149 }
150- if state.layer_focus.take().is_some() {
150+ if state.wm.layer_focus.take().is_some() {
151151 state.sync_focus();
152152 }
153153 let id = state
154+ .wm
154155 .space
155156 .element_under(pos)
156157 .and_then(|(w, _)| crate::state::root(w))
157158 .and_then(|s| state.window_id(&s));
158159 if let Some(id) = id
159- && state.layout.focused() != Some(id)
160+ && state.wm.layout.focused() != Some(id)
160161 {
161- state.layout.focus(id);
162+ state.wm.layout.focus(id);
162163 state.apply_layout();
163164 }
164165 }
@@ -188,10 +189,11 @@ fn start_drag(state: &mut State, button: u32) -> bool {
188189 Modifier::Ctrl => m.ctrl,
189190 Modifier::Shift => m.shift,
190191 });
191- let Some(id) = state.layout.focused() else {
192+ let Some(id) = state.wm.layout.focused() else {
192193 return false;
193194 };
194195 let Some(rect) = state
196+ .wm
195197 .layout
196198 .window(id)
197199 .filter(|w| w.floating && !w.fullscreen)
@@ -223,7 +225,7 @@ fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
223225 Rect::new(r.x + dx, r.y + dy, r.w, r.h)
224226 };
225227 let id = d.window;
226- state.layout.set_float_rect(id, rect);
228+ state.wm.layout.set_float_rect(id, rect);
227229 state.apply_layout();
228230 true
229231 }
Modified file: crates/monolith/src/ipc.rs+2 additions−2 deletions
@@ -147,7 +147,7 @@ pub fn broadcast(state: &mut State) {
147147 }
148148
149149 fn snapshot(state: &State) -> monolith_ipc::State {
150- let l = &state.layout;
150+ let l = &state.wm.layout;
151151 let focused = l.focused();
152152 let tags = l
153153 .tags()
@@ -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.windows.get(&id).map(identity).unwrap_or_default();
176+ let (app_id, title) = state.wm.windows.get(&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/layers.rs+15 additions−14 deletions
@@ -35,8 +35,8 @@ impl WlrLayerShellHandler for State {
3535 let output = output
3636 .as_ref()
3737 .and_then(Output::from_resource)
38- .or_else(|| self.space.output_under(self.pointer_pos).next().cloned())
39- .or_else(|| self.space.outputs().next().cloned());
38+ .or_else(|| self.wm.space.output_under(self.pointer_pos).next().cloned())
39+ .or_else(|| self.wm.space.outputs().next().cloned());
4040 let Some(output) = output else {
4141 surface.send_close();
4242 return;
@@ -49,11 +49,11 @@ impl WlrLayerShellHandler for State {
4949 }
5050
5151 fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
52- let _ = self.popups.track_popup(PopupKind::Xdg(popup));
52+ let _ = self.wm.popups.track_popup(PopupKind::Xdg(popup));
5353 }
5454
5555 fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
56- let outputs: Vec<Output> = self.space.outputs().cloned().collect();
56+ let outputs: Vec<Output> = self.wm.space.outputs().cloned().collect();
5757 for output in outputs {
5858 let mut map = layer_map_for_output(&output);
5959 let gone = map
@@ -64,8 +64,8 @@ impl WlrLayerShellHandler for State {
6464 map.unmap_layer(&layer);
6565 }
6666 }
67- if self.layer_focus.as_ref() == Some(surface.wl_surface()) {
68- self.layer_focus = None;
67+ if self.wm.layer_focus.as_ref() == Some(surface.wl_surface()) {
68+ self.wm.layer_focus = None;
6969 }
7070 self.reserve_layers();
7171 }
@@ -77,6 +77,7 @@ impl State {
7777 /// Returns false for surfaces that are not layer surfaces.
7878 pub fn layer_commit(&mut self, surface: &WlSurface) -> bool {
7979 let Some(output) = self
80+ .wm
8081 .space
8182 .outputs()
8283 .find(|o| {
@@ -109,13 +110,13 @@ impl State {
109110
110111 /// Keeps tiled windows out of the space bars reserve.
111112 pub fn reserve_layers(&mut self) {
112- let outputs: Vec<OutputId> = self.layout.outputs().map(|(o, _)| o).collect();
113+ let outputs: Vec<OutputId> = self.wm.layout.outputs().map(|(o, _)| o).collect();
113114 let mut changed = false;
114115 for o in outputs {
115116 let Some(output) = self.backend.output(o) else {
116117 continue;
117118 };
118- let Some(area) = self.layout.output(o).map(|out| out.area) else {
119+ let Some(area) = self.wm.layout.output(o).map(|out| out.area) else {
119120 continue;
120121 };
121122 let mut map = layer_map_for_output(&output);
@@ -128,7 +129,7 @@ impl State {
128129 right: (area.w - f64::from(zone.loc.x + zone.size.w)).max(0.0),
129130 };
130131 drop(map);
131- if self.layout.set_reserved(o, reserved) {
132+ if self.wm.layout.set_reserved(o, reserved) {
132133 tracing::debug!(
133134 output = output.name(),
134135 ?zone,
@@ -148,7 +149,7 @@ impl State {
148149 /// The layer surface that should hold the keyboard, if any. One asking
149150 /// for exclusive focus above the windows wins over everything.
150151 pub fn layer_keyboard_focus(&self) -> Option<WlSurface> {
151- for output in self.space.outputs() {
152+ for output in self.wm.space.outputs() {
152153 let map = layer_map_for_output(output);
153154 let exclusive = map
154155 .layers_on(Layer::Overlay)
@@ -160,11 +161,11 @@ impl State {
160161 return Some(l.wl_surface().clone());
161162 }
162163 }
163- self.layer_focus.clone().filter(|s| s.is_alive())
164+ self.wm.layer_focus.clone().filter(|s| s.is_alive())
164165 }
165166
166167 pub fn layer_maps_contain(&self, surface: &WlSurface) -> bool {
167- self.space.outputs().any(|o| {
168+ self.wm.space.outputs().any(|o| {
168169 layer_map_for_output(o)
169170 .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
170171 .is_some()
@@ -177,8 +178,8 @@ impl State {
177178 pos: Point<f64, Logical>,
178179 layers: &[Layer],
179180 ) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
180- let output = self.space.output_under(pos).next()?;
181- let origin = self.space.output_geometry(output)?.loc;
181+ let output = self.wm.space.output_under(pos).next()?;
182+ let origin = self.wm.space.output_geometry(output)?.loc;
182183 let local = pos - origin.to_f64();
183184 let map = layer_map_for_output(output);
184185 layers.iter().find_map(|&layer| {
Modified file: crates/monolith/src/lock.rs+4 additions−3 deletions
@@ -102,6 +102,7 @@ impl State {
102102 return;
103103 }
104104 let all = self
105+ .wm
105106 .space
106107 .outputs()
107108 .all(|o| self.services.lock.blanked.contains(&o.name()));
@@ -120,7 +121,7 @@ impl State {
120121 }
121122
122123 pub fn lock_keyboard_focus(&self) -> Option<WlSurface> {
123- let under = self.space.output_under(self.pointer_pos).next();
124+ let under = self.wm.space.output_under(self.pointer_pos).next();
124125 under
125126 .and_then(|o| self.services.lock.surface_on(o))
126127 .or_else(|| {
@@ -138,8 +139,8 @@ impl State {
138139 &self,
139140 pos: Point<f64, Logical>,
140141 ) -> Option<(WlSurface, Point<f64, Logical>)> {
141- let output = self.space.output_under(pos).next()?;
142- let origin = self.space.output_geometry(output)?.loc;
142+ let output = self.wm.space.output_under(pos).next()?;
143+ let origin = self.wm.space.output_geometry(output)?.loc;
143144 let surface = self.services.lock.surface_on(output)?;
144145 under_from_surface_tree(
145146 surface.wl_surface(),
Modified file: crates/monolith/src/main.rs+1 additions
@@ -19,6 +19,7 @@ mod nested;
1919 mod scene;
2020 mod state;
2121 mod toplevels;
22+mod wm;
2223 mod xwayland;
2324
2425 #[cfg(feature = "spans")]
Modified file: crates/monolith/src/nested/mod.rs+12 additions−7 deletions
@@ -8,6 +8,7 @@ mod input;
88 use crate::backend::Backend;
99 use crate::state::Services;
1010 use crate::state::State;
11+use crate::wm::Wm;
1112 use calloop_wayland_source::WaylandSource;
1213 use monolith_layout::{OutputId, Rect};
1314 use monolith_render_vk::VulkanAllocator;
@@ -132,14 +133,14 @@ pub fn init(
132133 Some((0, 0).into()),
133134 );
134135 output.set_preferred(mode);
135- state.space.map_output(&output, (0, 0));
136+ state.wm.space.map_output(&output, (0, 0));
136137 let area = Rect::new(
137138 0.0,
138139 0.0,
139140 f64::from(DEFAULT_SIZE.0),
140141 f64::from(DEFAULT_SIZE.1),
141142 );
142- let output_id = state.layout.add_output("nested", area);
143+ let output_id = state.wm.layout.add_output("nested", area);
143144
144145 state.backend = Backend::Nested(Box::new(Host {
145146 output_id,
@@ -174,13 +175,17 @@ pub fn render(state: &mut State) {
174175 let State {
175176 backend,
176177 renderer,
177- space,
178+ wm:
179+ Wm {
180+ layout,
181+ windows,
182+ anim,
183+ borders,
184+ space,
185+ ..
186+ },
178187 needs_redraw,
179188 core,
180- layout,
181- windows,
182- anim,
183- borders,
184189 settings,
185190 services:
186191 Services {
Modified file: crates/monolith/src/state.rs+79 additions−83 deletions
@@ -2,6 +2,7 @@ use crate::anim::{Animator, Ghost};
22 use crate::commands::Binds;
33 use crate::focus::Focus;
44 use crate::globals::Protocols;
5+use crate::wm::Wm;
56 use monolith_config::Config;
67 use monolith_layout::{Layout, OutputId, Rect, WindowId};
78 use monolith_protocols::workspace::{Group, Snapshot, Workspace};
@@ -39,8 +40,7 @@ pub struct State {
3940 pub settings: Settings,
4041 pub protocols: Protocols,
4142 pub renderer: VulkanRenderer,
42- pub space: Space<Window>,
43- pub popups: PopupManager,
43+ pub wm: Wm,
4444 pub backend: crate::backend::Backend,
4545 pub cursor: crate::cursor::Cursor,
4646 pub cursor_status: CursorImageStatus,
@@ -48,28 +48,12 @@ pub struct State {
4848 /// The pointer moved, which on its own only redraws the cursor.
4949 pub cursor_moved: bool,
5050
51- pub layout: Layout,
52- pub windows: HashMap<WindowId, Window>,
53- /// Toplevels that have not committed yet. Rules need their app_id and
54- /// title, which clients set after creating the toplevel.
55- pub unmapped: Vec<Window>,
5651 pub suppressed: Vec<Keycode>,
5752 pub move_inverted: bool,
5853 pub drag: Option<crate::input::Drag>,
5954 /// Global logical position, shared by every backend.
6055 pub pointer_pos: Point<f64, Logical>,
6156 pub services: Services,
62- pub list_handles: crate::toplevels::ListHandles,
63- pub anim: Animator,
64- pub borders: crate::scene::Borders,
65- /// How each shown window was placed by the last layout pass, to spot
66- /// switches between tiled, floating and fullscreen.
67- pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
68- pub awaiting: HashSet<WindowId>,
69- /// Windows a rule sent somewhere, which activation tokens leave alone.
70- pub placed_by_rule: HashSet<WindowId>,
71- /// A layer surface given the keyboard by a click, like a launcher.
72- pub layer_focus: Option<WlSurface>,
7357 pub seat_state: SeatState<State>,
7458 pub seat: Seat<State>,
7559 }
@@ -165,16 +149,25 @@ impl State {
165149 path: config_path,
166150 },
167151 renderer,
168- space: Space::default(),
169- popups: PopupManager::default(),
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+ },
170166 backend: crate::backend::Backend::Starting,
171167 cursor: crate::cursor::Cursor::load(),
172168 cursor_status: CursorImageStatus::default_named(),
173169 needs_redraw: true,
174170 cursor_moved: false,
175- layout,
176- windows: HashMap::new(),
177- unmapped: Vec::new(),
178171 suppressed: Vec::new(),
179172 move_inverted: false,
180173 drag: None,
@@ -188,13 +181,6 @@ impl State {
188181 ipc: Default::default(),
189182 idle,
190183 },
191- list_handles: Default::default(),
192- anim: Animator::new(animation),
193- borders: Default::default(),
194- last_kind: HashMap::new(),
195- awaiting: HashSet::new(),
196- placed_by_rule: HashSet::new(),
197- layer_focus: None,
198184 seat_state,
199185 seat,
200186 })
@@ -202,8 +188,8 @@ impl State {
202188
203189 /// Runs after every batch of events, so many client commits produce one frame.
204190 pub fn after_dispatch(&mut self) {
205- self.space.refresh();
206- self.popups.cleanup();
191+ self.wm.space.refresh();
192+ self.wm.popups.cleanup();
207193 match self.backend {
208194 crate::backend::Backend::Drm(_) => crate::drm::render(self),
209195 crate::backend::Backend::Nested(_) => crate::nested::render(self),
@@ -227,7 +213,7 @@ impl State {
227213 };
228214 host.output
229215 .change_current_state(Some(mode), None, None, None);
230- self.layout.set_output_area(o, area);
216+ self.wm.layout.set_output_area(o, area);
231217 self.reserve_layers();
232218 self.reconfigure_lock();
233219 self.apply_layout();
@@ -252,14 +238,15 @@ impl State {
252238 .output
253239 .as_deref()
254240 .and_then(|name| {
255- self.layout
241+ self.wm
242+ .layout
256243 .outputs()
257244 .find(|(_, o)| o.name == name)
258245 .map(|(id, _)| id)
259246 })
260247 .or(launch.and_then(|l| l.0));
261248 let tag = match rule.tag.as_deref() {
262- Some(t) => Some(self.layout.tag(t)),
249+ Some(t) => Some(self.wm.layout.tag(t)),
263250 None => launch.map(|l| l.1),
264251 };
265252 let float = float.map(|r| {
@@ -270,23 +257,23 @@ impl State {
270257 f64::from(r.size.h),
271258 )
272259 });
273- let id = self.layout.add_window(output, tag, float);
260+ let id = self.wm.layout.add_window(output, tag, float);
274261 if rule.tag.is_some() || rule.output.is_some() {
275- self.placed_by_rule.insert(id);
262+ self.wm.placed_by_rule.insert(id);
276263 }
277264 if rule.floating == Some(true) {
278- self.layout.toggle_floating(id);
265+ self.wm.layout.toggle_floating(id);
279266 }
280267 if rule.sticky == Some(true) {
281- self.layout.toggle_sticky(id);
268+ self.wm.layout.toggle_sticky(id);
282269 }
283270 if let Some(name) = &rule.scratchpad {
284- self.layout.scratchpad_add(id, name);
271+ self.wm.layout.scratchpad_add(id, name);
285272 }
286- self.windows.insert(id, window);
273+ self.wm.windows.insert(id, window);
287274 self.await_first_buffer(id);
288- if self.layout.visible(id) {
289- self.layout.focus(id);
275+ if self.wm.layout.visible(id) {
276+ self.wm.layout.focus(id);
290277 }
291278 self.apply_layout();
292279 }
@@ -295,7 +282,7 @@ impl State {
295282 /// spawn animation never plays over an empty slot. Clients that are slow
296283 /// to draw get it anyway after a while.
297284 fn await_first_buffer(&mut self, id: WindowId) {
298- self.awaiting.insert(id);
285+ self.wm.awaiting.insert(id);
299286 let timeout = Timer::from_duration(Duration::from_millis(300));
300287 let _ = self.core.handle.insert_source(timeout, move |_, _, state| {
301288 state.first_buffer(id);
@@ -305,7 +292,7 @@ impl State {
305292
306293 /// Called on commits, starts the spawn of a window that now has a buffer.
307294 pub fn on_buffer(&mut self, surface: &WlSurface) {
308- if self.awaiting.is_empty() {
295+ if self.wm.awaiting.is_empty() {
309296 return;
310297 }
311298 let has_buffer =
@@ -316,44 +303,48 @@ impl State {
316303 }
317304
318305 fn first_buffer(&mut self, id: WindowId) {
319- self.placed_by_rule.remove(&id);
320- if self.awaiting.remove(&id) {
321- self.anim.spawned(id, now_ns());
306+ self.wm.placed_by_rule.remove(&id);
307+ if self.wm.awaiting.remove(&id) {
308+ self.wm.anim.spawned(id, now_ns());
322309 self.apply_layout();
323310 }
324311 }
325312
326313 pub fn unmap_window(&mut self, surface: &WlSurface) {
327- self.unmapped.retain(|w| root(w).as_ref() != Some(surface));
314+ self.wm
315+ .unmapped
316+ .retain(|w| root(w).as_ref() != Some(surface));
328317 if let Some(id) = self.window_id(surface) {
329318 self.remove_window(id);
330319 }
331320 }
332321
333322 pub fn remove_window(&mut self, id: WindowId) {
334- if let Some(window) = self.windows.remove(&id) {
335- self.space.unmap_elem(&window);
323+ if let Some(window) = self.wm.windows.remove(&id) {
324+ self.wm.space.unmap_elem(&window);
336325 self.leave_ghost(id, &window);
337326 }
338- self.layout.remove_window(id);
339- self.placed_by_rule.remove(&id);
327+ self.wm.layout.remove_window(id);
328+ self.wm.placed_by_rule.remove(&id);
340329 self.apply_layout();
341330 }
342331
343332 /// Keeps a visible tiled window's last frame shrinking to its seed while
344333 /// its neighbours close over it.
345334 fn leave_ghost(&mut self, id: WindowId, window: &Window) {
346- let Some(o) = self.layout.window(id).and_then(|w| w.output) else {
335+ let Some(o) = self.wm.layout.window(id).and_then(|w| w.output) else {
347336 return;
348337 };
349338 let tiled = self
339+ .wm
350340 .layout
351341 .output(o)
352342 .is_some_and(|out| out.tree.contains(id));
353- if !tiled || !self.layout.visible(id) {
343+ if !tiled || !self.wm.layout.visible(id) {
354344 return;
355345 }
356346 let Some(seed) = self
347+ .wm
357348 .layout
358349 .arrange_hiding(o, &[id])
359350 .tiled
@@ -374,7 +365,7 @@ impl State {
374365 })
375366 .flatten();
376367 if let Some((texture, scale, transform)) = snapshot {
377- self.anim.add_ghost(Ghost {
368+ self.wm.anim.add_ghost(Ghost {
378369 id,
379370 output: o,
380371 seed: seed.rect,
@@ -388,7 +379,8 @@ impl State {
388379 }
389380
390381 pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
391- self.windows
382+ self.wm
383+ .windows
392384 .iter()
393385 .find(|(_, w)| root(w).as_ref() == Some(surface))
394386 .map(|(id, _)| *id)
@@ -399,10 +391,10 @@ impl State {
399391 let now = now_ns();
400392 let mut shown = Vec::new();
401393 let mut tracks = Vec::new();
402- let outputs: Vec<OutputId> = self.layout.outputs().map(|(o, _)| o).collect();
394+ let outputs: Vec<OutputId> = self.wm.layout.outputs().map(|(o, _)| o).collect();
403395 for o in outputs {
404- let arr = self.layout.arrange(o);
405- let area = self.layout.output(o).map(|o| o.area).unwrap_or_default();
396+ let arr = self.wm.layout.arrange(o);
397+ let area = self.wm.layout.output(o).map(|o| o.area).unwrap_or_default();
406398 for p in &arr.tiled {
407399 if p.visible && arr.fullscreen != Some(p.window) {
408400 shown.push((p.window, p.rect, Kind::Tiled, o));
@@ -420,12 +412,12 @@ impl State {
420412 .tiled
421413 .iter()
422414 .map(|p| p.window)
423- .filter(|w| self.awaiting.contains(w))
415+ .filter(|w| self.wm.awaiting.contains(w))
424416 .collect();
425417 let planned = if held.is_empty() {
426418 arr.tiled.clone()
427419 } else {
428- self.layout.arrange_hiding(o, &held).tiled
420+ self.wm.layout.arrange_hiding(o, &held).tiled
429421 };
430422 let targets: Vec<(WindowId, Rect)> =
431423 planned.iter().map(|p| (p.window, p.rect)).collect();
@@ -434,71 +426,74 @@ impl State {
434426
435427 let mut glides = Vec::new();
436428 for &(id, rect, kind, _) in &shown {
437- match self.last_kind.get(&id) {
429+ match self.wm.last_kind.get(&id) {
438430 Some(&(was, from, o)) if was != kind => {
439431 let from = self
432+ .wm
440433 .anim
441434 .gliding(id, now)
442435 .or_else(|| {
443436 (was == Kind::Tiled)
444- .then(|| self.anim.tiled(o, id, now))
437+ .then(|| self.wm.anim.tiled(o, id, now))
445438 .flatten()
446439 })
447440 .unwrap_or(from);
448441 glides.push((id, from, rect));
449442 }
450- Some(&(Kind::Floating, from, _)) if from != rect => self.anim.stop_glide(id),
443+ Some(&(Kind::Floating, from, _)) if from != rect => self.wm.anim.stop_glide(id),
451444 _ => {}
452445 }
453446 }
454- let gap = self.layout.gaps.inner;
447+ let gap = self.wm.layout.gaps.inner;
455448 for (o, targets) in tracks {
456449 let new: Vec<WindowId> = targets
457450 .iter()
458451 .map(|t| t.0)
459- .filter(|&id| self.anim.tiled(o, id, now).is_none())
452+ .filter(|&id| self.wm.anim.tiled(o, id, now).is_none())
460453 .collect();
461454 let seeds: HashMap<WindowId, Rect> = if new.is_empty() {
462455 HashMap::new()
463456 } else {
464- self.layout
457+ self.wm
458+ .layout
465459 .arrange_hiding(o, &new)
466460 .tiled
467461 .into_iter()
468462 .map(|p| (p.window, p.rect))
469463 .collect()
470464 };
471- self.anim.retarget(o, now, &targets, gap, |id| {
465+ self.wm.anim.retarget(o, now, &targets, gap, |id| {
472466 seeds.get(&id).copied().unwrap_or_default()
473467 });
474468 }
475469 for (id, from, to) in glides {
476- self.anim.glide(id, from, to, now);
470+ self.wm.anim.glide(id, from, to, now);
477471 }
478- self.last_kind = shown.iter().map(|&(id, r, k, o)| (id, (k, r, o))).collect();
472+ self.wm.last_kind = shown.iter().map(|&(id, r, k, o)| (id, (k, r, o))).collect();
479473
480474 let mut mapped = Vec::new();
481475 for (id, rect, kind, _) in shown {
482- if let Some(window) = self.windows.get(&id)
483- && configure(window, rect, kind, self.layout.maximized(id))
476+ if let Some(window) = self.wm.windows.get(&id)
477+ && configure(window, rect, kind, self.wm.layout.maximized(id))
484478 {
485479 let loc = (rect.x.round() as i32, rect.y.round() as i32);
486480 let loc = Point::from(loc) - window.geometry().loc;
487- self.space.map_element(window.clone(), loc, false);
481+ self.wm.space.map_element(window.clone(), loc, false);
488482 mapped.push(id);
489483 }
490484 }
491485 let hidden: Vec<Window> = self
486+ .wm
492487 .windows
493488 .iter()
494489 .filter(|(id, _)| !mapped.contains(id))
495490 .map(|(_, w)| w.clone())
496491 .collect();
497492 for window in hidden {
498- self.space.unmap_elem(&window);
493+ self.wm.space.unmap_elem(&window);
499494 }
500495 for id in &mapped {
501- self.space.raise_element(&self.windows[id], false);
496+ self.wm.space.raise_element(&self.wm.windows[id], false);
502497 }
503498 self.sync_focus();
504499 self.update_inhibit();
@@ -506,8 +501,8 @@ impl State {
506501 }
507502
508503 pub fn sync_focus(&mut self) {
509- let focused = self.layout.focused();
510- for (id, window) in &self.windows {
504+ let focused = self.wm.layout.focused();
505+ for (id, window) in &self.wm.windows {
511506 if window.set_activated(Some(*id) == focused)
512507 && let Some(t) = window.toplevel()
513508 && t.is_initial_configure_sent()
@@ -519,7 +514,7 @@ impl State {
519514 self.lock_keyboard_focus().map(Focus::from)
520515 } else {
521516 self.layer_keyboard_focus().map(Focus::from).or_else(|| {
522- let w = focused.and_then(|id| self.windows.get(&id))?;
517+ let w = focused.and_then(|id| self.wm.windows.get(&id))?;
523518 match w.x11_surface() {
524519 Some(x) => Some(Focus::X11(Box::new(x.clone()))),
525520 None => w.toplevel().map(|t| Focus::from(t.wl_surface().clone())),
@@ -537,7 +532,7 @@ impl State {
537532 if !self.protocols.workspaces.has_clients() {
538533 return;
539534 }
540- let l = &self.layout;
535+ let l = &self.wm.layout;
541536 let occupied: HashSet<(OutputId, monolith_layout::TagId)> = l
542537 .windows()
543538 .filter(|(_, w)| w.scratchpad.is_none())
@@ -609,11 +604,11 @@ impl State {
609604 return Some((s, (p + geo.loc).to_f64()));
610605 }
611606 }
612- let fullscreen = self.layout.outputs().any(|(o, out)| {
607+ let fullscreen = self.wm.layout.outputs().any(|(o, out)| {
613608 let a = out.area;
614609 (a.x..a.right()).contains(&pos.x)
615610 && (a.y..a.bottom()).contains(&pos.y)
616- && self.layout.arrange(o).fullscreen.is_some()
611+ && self.wm.layout.arrange(o).fullscreen.is_some()
617612 });
618613 let above: &[Layer] = if fullscreen {
619614 &[Layer::Overlay]
@@ -623,7 +618,8 @@ impl State {
623618 let layer = |layers| self.layer_under(pos, layers).map(|(_, s, p)| (s, p));
624619 layer(above)
625620 .or_else(|| {
626- self.space
621+ self.wm
622+ .space
627623 .element_under(pos)
628624 .and_then(|(window, location)| {
629625 window
Modified file: crates/monolith/src/toplevels.rs+24 additions−18 deletions
@@ -39,22 +39,22 @@ impl ForeignToplevelHandler for State {
3939 }
4040
4141 fn foreign_toplevel_action(&mut self, action: Action) {
42- let id_of = |k: u64| self.windows.keys().copied().find(|&id| key(id) == k);
42+ let id_of = |k: u64| self.wm.windows.keys().copied().find(|&id| key(id) == k);
4343 match action {
4444 Action::Activate(k) => {
4545 let Some(id) = id_of(k) else { return };
46- if !self.layout.visible(id)
47- && let Some(w) = self.layout.window(id)
46+ if !self.wm.layout.visible(id)
47+ && let Some(w) = self.wm.layout.window(id)
4848 && let Some(o) = w.output
4949 && w.scratchpad.is_none()
5050 {
5151 let tag = w.tag;
52- self.layout.toggle_view(o, tag);
52+ self.wm.layout.toggle_view(o, tag);
5353 }
54- self.layout.focus(id);
54+ self.wm.layout.focus(id);
5555 }
5656 Action::Close(k) => {
57- let Some(window) = id_of(k).and_then(|id| self.windows.get(&id)) else {
57+ let Some(window) = id_of(k).and_then(|id| self.wm.windows.get(&id)) else {
5858 return;
5959 };
6060 match (window.toplevel(), window.x11_surface()) {
@@ -68,15 +68,20 @@ impl ForeignToplevelHandler for State {
6868 }
6969 Action::Maximize(k, on) => {
7070 let Some(id) = id_of(k) else { return };
71- if self.layout.maximized(id) != on {
72- self.layout.focus(id);
73- self.layout.expand(monolith_layout::ExpandScope::All);
71+ if self.wm.layout.maximized(id) != on {
72+ self.wm.layout.focus(id);
73+ self.wm.layout.expand(monolith_layout::ExpandScope::All);
7474 }
7575 }
7676 Action::Fullscreen(k, on) => {
7777 let Some(id) = id_of(k) else { return };
78- if self.layout.window(id).is_some_and(|w| w.fullscreen != on) {
79- self.layout.toggle_fullscreen(id);
78+ if self
79+ .wm
80+ .layout
81+ .window(id)
82+ .is_some_and(|w| w.fullscreen != on)
83+ {
84+ self.wm.layout.toggle_fullscreen(id);
8085 }
8186 }
8287 }
@@ -87,15 +92,15 @@ impl ForeignToplevelHandler for State {
8792 impl State {
8893 /// Brings both window lists in line with the windows.
8994 pub fn publish_toplevels(&mut self) {
90- let mut handles = std::mem::take(&mut self.list_handles.0);
95+ let mut handles = std::mem::take(&mut self.wm.list_handles.0);
9196 handles.retain(|id, h| {
92- let keep = self.windows.contains_key(id);
97+ let keep = self.wm.windows.contains_key(id);
9398 if !keep {
9499 self.protocols.toplevel_list.remove_toplevel(h);
95100 }
96101 keep
97102 });
98- for (id, window) in &self.windows {
103+ for (id, window) in &self.wm.windows {
99104 let (app_id, title) = identity(window);
100105 match handles.get(id) {
101106 Some(h) => {
@@ -114,18 +119,19 @@ impl State {
114119 }
115120 }
116121 }
117- self.list_handles.0 = handles;
122+ self.wm.list_handles.0 = handles;
118123
119124 if !self.protocols.foreign_toplevels.has_clients() {
120125 return;
121126 }
122- let focused = self.layout.focused();
127+ let focused = self.wm.layout.focused();
123128 let toplevels = self
129+ .wm
124130 .windows
125131 .iter()
126132 .map(|(&id, window)| {
127133 let (app_id, title) = identity(window);
128- let w = self.layout.window(id);
134+ let w = self.wm.layout.window(id);
129135 Toplevel {
130136 key: key(id),
131137 title,
@@ -136,7 +142,7 @@ impl State {
136142 .into_iter()
137143 .collect(),
138144 activated: focused == Some(id),
139- maximized: self.layout.maximized(id),
145+ maximized: self.wm.layout.maximized(id),
140146 fullscreen: w.is_some_and(|w| w.fullscreen),
141147 }
142148 })
Added file: crates/monolith/src/wm.rs+29 additions
@@ -0,0 +1,29 @@
1+//! The window model: the layout and what monolith keeps per window beside it.
2+
3+use crate::anim::Animator;
4+use crate::state::Kind;
5+use monolith_layout::{Layout, OutputId, Rect, WindowId};
6+use smithay::desktop::{PopupManager, Space, Window};
7+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8+use std::collections::{HashMap, HashSet};
9+
10+pub struct Wm {
11+ pub layout: Layout,
12+ pub windows: HashMap<WindowId, Window>,
13+ /// Toplevels that have not committed yet. Rules need their app_id and
14+ /// title, which clients set after creating the toplevel.
15+ pub unmapped: Vec<Window>,
16+ pub space: Space<Window>,
17+ pub popups: PopupManager,
18+ 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,
27+ /// A layer surface given the keyboard by a click, like a launcher.
28+ pub layer_focus: Option<WlSurface>,
29+}
Modified file: crates/monolith/src/xwayland.rs+9 additions−3 deletions
@@ -320,6 +320,7 @@ impl XwmHandler for State {
320320 fn disconnected(&mut self, _xwm: XwmId) {
321321 tracing::info!("Xwayland exited");
322322 let gone: Vec<_> = self
323+ .wm
323324 .windows
324325 .iter()
325326 .filter(|(_, w)| w.x11_surface().is_some())
@@ -357,7 +358,8 @@ fn floats(window: &X11Surface) -> bool {
357358
358359 impl State {
359360 pub fn x11_window_id(&self, window: &X11Surface) -> Option<monolith_layout::WindowId> {
360- self.windows
361+ self.wm
362+ .windows
361363 .iter()
362364 .find(|(_, w)| w.x11_surface() == Some(window))
363365 .map(|(id, _)| *id)
@@ -375,9 +377,13 @@ impl State {
375377
376378 fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {
377379 if let Some(id) = self.x11_window_id(window)
378- && self.layout.window(id).is_some_and(|w| w.fullscreen != on)
380+ && self
381+ .wm
382+ .layout
383+ .window(id)
384+ .is_some_and(|w| w.fullscreen != on)
379385 {
380- self.layout.toggle_fullscreen(id);
386+ self.wm.layout.toggle_fullscreen(id);
381387 self.apply_layout();
382388 }
383389 }