atagen

monolith

public
Star0
Watch0

Commit b22c0511d5af

tarball

atagen <boss@atagen.co>2026-10-17 03:59 UTC

12 files changed+75-55
monolith: share enums, modifier mapping and rect tests across crates

ipc now re-exports layout's Dir and Axis, which serialize under a layout feature, in place of copies mapped by hand. The config's Mods learned has, and one held() turns smithay's modifier state into it for binds and drags. Rect::contains replaces four inline point tests, and window keys for clients come from one place, resolved without a scan. A test now holds default.toml to the Default impls, and it caught the border colours drifting by rounding.
Modified file: crates/config/src/lib.rs+28 additions−4 deletions
@@ -98,8 +98,8 @@ impl Default for Decoration {
9898 fn default() -> Self {
9999 Self {
100100 border: 2,
101- focused: Color([0.53, 0.75, 0.82, 1.0]),
102- unfocused: Color([0.23, 0.26, 0.32, 1.0]),
101+ focused: Color::rgb(0x88, 0xbf, 0xd1),
102+ unfocused: Color::rgb(0x3b, 0x42, 0x52),
103103 }
104104 }
105105 }
@@ -108,6 +108,12 @@ impl Default for Decoration {
108108 #[derive(Debug, Clone, Copy, PartialEq)]
109109 pub struct Color(pub [f32; 4]);
110110
111+impl Color {
112+ pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
113+ Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
114+ }
115+}
116+
111117 impl<'de> Deserialize<'de> for Color {
112118 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
113119 let s = String::deserialize(d)?;
@@ -199,6 +205,15 @@ pub struct Mods {
199205 }
200206
201207 impl Mods {
208+ pub fn has(&self, m: Modifier) -> bool {
209+ match m {
210+ Modifier::Super => self.logo,
211+ Modifier::Alt => self.alt,
212+ Modifier::Ctrl => self.ctrl,
213+ Modifier::Shift => self.shift,
214+ }
215+ }
216+
202217 fn set(&mut self, m: Modifier) {
203218 match m {
204219 Modifier::Super => self.logo = true,
@@ -411,9 +426,18 @@ fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, Erro
411426
412427 #[cfg(test)]
413428 mod tests {
429+ use super::*;
430+
431+ /// default.toml documents the defaults for users, and the Default impls
432+ /// apply them to sections a user config leaves out. They must agree.
414433 #[test]
415- fn the_default_config_parses() {
416- let c = super::Config::parse(super::DEFAULT).unwrap();
434+ fn default_toml_states_the_defaults() {
435+ let c = Config::parse(DEFAULT).unwrap();
436+ assert_eq!(c.general, General::default());
437+ assert_eq!(c.output, Outputs::default());
438+ assert_eq!(c.decoration, Decoration::default());
439+ assert_eq!(c.animation, Animation::default());
440+ assert_eq!(c.keyboard, Keyboard::default());
417441 assert!(c.binds.len() > 40);
418442 }
419443 }
Modified file: crates/ipc/Cargo.toml+1 additions
@@ -9,4 +9,5 @@ publish.workspace = true
99 workspace = true
1010
1111 [dependencies]
12+monolith-layout = { workspace = true, features = ["serde"] }
1213 serde.workspace = true
Modified file: crates/ipc/src/lib.rs+1 additions−15 deletions
@@ -25,21 +25,7 @@ pub fn socket_path() -> Option<PathBuf> {
2525 socket_path_for(&std::env::var("WAYLAND_DISPLAY").ok()?)
2626 }
2727
28-#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
29-#[serde(rename_all = "kebab-case")]
30-pub enum Dir {
31- Left,
32- Right,
33- Up,
34- Down,
35-}
36-
37-#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
38-#[serde(rename_all = "kebab-case")]
39-pub enum Axis {
40- Horizontal,
41- Vertical,
42-}
28+pub use monolith_layout::{Axis, Dir};
4329
4430 #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
4531 #[serde(rename_all = "kebab-case")]
Modified file: crates/layout/Cargo.toml+4 additions
@@ -8,8 +8,12 @@ publish.workspace = true
88 [lints]
99 workspace = true
1010
11+[features]
12+serde = ["dep:serde"]
13+
1114 [dependencies]
1215 monolith-planner.workspace = true
16+serde = { workspace = true, optional = true }
1317 slotmap.workspace = true
1418
1519 [dev-dependencies]
Modified file: crates/layout/src/tree.rs+4 additions
@@ -6,6 +6,8 @@ use std::hash::Hash;
66 new_key_type! { pub struct NodeId; }
77
88 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10+#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
911 pub enum Axis {
1012 Horizontal,
1113 Vertical,
@@ -21,6 +23,8 @@ impl Axis {
2123 }
2224
2325 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27+#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
2428 pub enum Dir {
2529 Left,
2630 Right,
Modified file: crates/monolith/src/commands.rs+15 additions−23 deletions
@@ -1,7 +1,7 @@
11 use crate::state::State;
22 use monolith_config::{Config, Mods, MoveMode};
33 use monolith_ipc::Command;
4-use monolith_layout::{Axis, Dir, ExpandScope, WindowId};
4+use monolith_layout::{ExpandScope, WindowId};
55 use smithay::input::keyboard::{Keysym, ModifiersState, xkb};
66 use std::process::Stdio;
77
@@ -26,12 +26,7 @@ impl Binds {
2626
2727 /// Matches on the unshifted keysyms so `Mod+Shift+1` means the 1 key.
2828 pub fn find(&self, mods: &ModifiersState, syms: &[Keysym]) -> Option<Command> {
29- let held = Mods {
30- logo: mods.logo,
31- alt: mods.alt,
32- ctrl: mods.ctrl,
33- shift: mods.shift,
34- };
29+ let held = held(mods);
3530 self.0
3631 .iter()
3732 .find(|(m, sym, _)| *m == held && syms.contains(sym))
@@ -39,12 +34,13 @@ impl Binds {
3934 }
4035 }
4136
42-fn dir(d: monolith_ipc::Dir) -> Dir {
43- match d {
44- monolith_ipc::Dir::Left => Dir::Left,
45- monolith_ipc::Dir::Right => Dir::Right,
46- monolith_ipc::Dir::Up => Dir::Up,
47- monolith_ipc::Dir::Down => Dir::Down,
37+/// The config's view of what smithay reports held.
38+pub fn held(mods: &ModifiersState) -> Mods {
39+ Mods {
40+ logo: mods.logo,
41+ alt: mods.alt,
42+ ctrl: mods.ctrl,
43+ shift: mods.shift,
4844 }
4945 }
5046
@@ -69,26 +65,22 @@ impl State {
6965 Command::Quit => self.core.signal.stop(),
7066 Command::Reload => self.reload()?,
7167 Command::Focus { dir: d } => {
72- self.wm.layout.focus_dir(dir(d));
68+ self.wm.layout.focus_dir(d);
7369 }
7470 Command::Move { dir: d } if merge => {
75- self.wm.layout.merge_window(dir(d));
71+ self.wm.layout.merge_window(d);
7672 }
7773 Command::Move { dir: d } => {
78- self.wm.layout.move_window(dir(d));
74+ self.wm.layout.move_window(d);
7975 }
8076 Command::MoveContainer { dir: d } => {
81- self.wm.layout.move_container(dir(d));
77+ self.wm.layout.move_container(d);
8278 }
8379 Command::Maximize => {
8480 self.wm.layout.expand(ExpandScope::All);
8581 }
8682 Command::InvertMove => self.input.move_inverted = !inverted,
8783 Command::Resize { axis, delta } => {
88- let axis = match axis {
89- monolith_ipc::Axis::Horizontal => Axis::Horizontal,
90- monolith_ipc::Axis::Vertical => Axis::Vertical,
91- };
9284 self.wm.layout.resize(axis, delta);
9385 }
9486 Command::Expand { scope } => {
@@ -130,7 +122,7 @@ impl State {
130122 self.wm.layout.reorder_tag(t, index);
131123 }
132124 Command::FocusOutput { dir: d } => {
133- if let Some(o) = output.and_then(|o| self.wm.layout.output_towards(o, dir(d))) {
125+ if let Some(o) = output.and_then(|o| self.wm.layout.output_towards(o, d)) {
134126 self.wm.layout.set_active_output(o);
135127 if let Some(w) = self.wm.layout.focused_on(o) {
136128 self.wm.layout.focus(w);
@@ -140,7 +132,7 @@ impl State {
140132 Command::SendToOutput { dir: d } => {
141133 if let (Some(w), Some(o)) = (
142134 focused,
143- output.and_then(|o| self.wm.layout.output_towards(o, dir(d))),
135+ output.and_then(|o| self.wm.layout.output_towards(o, d)),
144136 ) {
145137 self.wm.layout.send_to_output(w, o);
146138 }
Modified file: crates/monolith/src/drm.rs+1 additions−3 deletions
@@ -786,9 +786,7 @@ fn clamp(
786786 state: &State,
787787 pos: Point<f64, smithay::utils::Logical>,
788788 ) -> Point<f64, smithay::utils::Logical> {
789- let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| {
790- p.x >= a.x && p.y >= a.y && p.x < a.right() && p.y < a.bottom()
791- };
789+ let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| a.contains(p.x, p.y);
792790 let areas: Vec<Rect> = state.wm.layout.outputs().map(|(_, o)| o.area).collect();
793791 if areas.iter().any(|a| inside(a, pos)) {
794792 return pos;
Modified file: crates/monolith/src/frame.rs+3 additions−2 deletions
@@ -36,8 +36,9 @@ pub fn compose(
3636 ) -> Vec<Draw> {
3737 let mut elements = Vec::new();
3838 if cursor && let Some(area) = ctx.wm.layout.output(o).map(|out| out.area) {
39- let local = ctx.input.pointer_pos - Point::from((area.x, area.y));
40- if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
39+ let pos = ctx.input.pointer_pos;
40+ if area.contains(pos.x, pos.y) {
41+ let local = pos - Point::from((area.x, area.y));
4142 elements.extend(ctx.input.cursor.elements(
4243 ctx.renderer,
4344 &ctx.input.cursor_status,
Modified file: crates/monolith/src/input.rs+1 additions−7 deletions
@@ -2,7 +2,6 @@
22 //! into these calls, with pointer positions in global logical coordinates.
33
44 use crate::state::State;
5-use monolith_config::Modifier;
65 use monolith_layout::{Rect, WindowId};
76 use smithay::backend::input::{ButtonState, InputTime, KeyState};
87 use smithay::backend::session::Session;
@@ -184,12 +183,7 @@ fn start_drag(state: &mut State, button: u32) -> bool {
184183 .seat
185184 .get_keyboard()
186185 .map(|k| k.modifier_state())
187- .is_some_and(|m| match state.settings.config.general.mod_key {
188- Modifier::Super => m.logo,
189- Modifier::Alt => m.alt,
190- Modifier::Ctrl => m.ctrl,
191- Modifier::Shift => m.shift,
192- });
186+ .is_some_and(|m| crate::commands::held(&m).has(state.settings.config.general.mod_key));
193187 let Some(id) = state.wm.layout.focused() else {
194188 return false;
195189 };
Modified file: crates/monolith/src/ipc.rs+1 additions−1 deletions
@@ -179,7 +179,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
179179 .map(|w| (w.app_id.clone(), w.title.clone()))
180180 .unwrap_or_default();
181181 monolith_ipc::Window {
182- id: slotmap::Key::data(&id).as_ffi(),
182+ id: crate::wm::key(id),
183183 app_id,
184184 title,
185185 tag: l
Modified file: crates/monolith/src/wm.rs+11 additions
@@ -45,6 +45,11 @@ pub struct WindowState {
4545 pub title: String,
4646 }
4747
48+/// The id clients see for a window, stable for its lifetime.
49+pub fn key(id: WindowId) -> u64 {
50+ slotmap::Key::data(&id).as_ffi()
51+}
52+
4853 /// Asks a window to close, through xdg-shell or X11.
4954 pub fn close(window: &Window) {
5055 match (window.toplevel(), window.x11_surface()) {
@@ -115,6 +120,12 @@ impl Wm {
115120 }
116121 }
117122
123+ /// The window a client named by `key`, if it still exists.
124+ pub fn by_key(&self, key: u64) -> Option<WindowId> {
125+ let id = WindowId::from(slotmap::KeyData::from_ffi(key));
126+ self.windows.contains_key(id).then_some(id)
127+ }
128+
118129 pub fn id_of(&self, surface: &WlSurface) -> Option<WindowId> {
119130 self.by_surface.get(surface).copied()
120131 }
Modified file: crates/planner/src/geom.rs+5 additions
@@ -21,6 +21,11 @@ impl Rect {
2121 self.y + self.h
2222 }
2323
24+ /// Whether the point lies inside, counting the left and top edges only.
25+ pub fn contains(&self, x: f64, y: f64) -> bool {
26+ x >= self.x && y >= self.y && x < self.right() && y < self.bottom()
27+ }
28+
2429 pub fn center_x(&self) -> f64 {
2530 self.x + self.w / 2.0
2631 }