leftwm_core/layouts/
layout_manager.rs

1use crate::{config::Config, utils::helpers::cycle_vec};
2use leftwm_layouts::Layout;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::LayoutMode;
7
8/// The [`LayoutManager`] holds the actual set of [`Layout`].
9#[derive(Serialize, Deserialize, Debug, Clone)]
10pub struct LayoutManager {
11    /// `LayoutMode` to be used when applying layouts
12    mode: LayoutMode,
13
14    /// All the available layouts. Loaded from the config and
15    /// to be unchanged during runtime. The layout manager shall make
16    /// copies of those layouts for the specific workspaces and tags.
17    available_layouts: Vec<Layout>,
18
19    /// All the available layouts per workspace. Different workspaces may
20    /// have different available layouts, if configured that way. If a
21    /// workspace does not have its own set of available layouts, the
22    /// global available layouts from [`available_layouts`] will be used instead.
23    available_layouts_per_ws: HashMap<usize, Vec<Layout>>,
24
25    /// The actual, modifiable layouts grouped by either
26    /// Workspace or Tag, depending on the configured [`LayoutMode`].
27    layouts: HashMap<usize, Vec<Layout>>,
28}
29
30impl LayoutManager {
31    /// Create a new [`LayoutManager`] from the config
32    pub fn new(config: &impl Config) -> Self {
33        let mut available_layouts: Vec<Layout> = Vec::new();
34
35        tracing::trace!("Looking for layouts named: {:?}", config.layouts());
36        for name in config.layouts() {
37            if let Some(def) = config
38                .layout_definitions()
39                .iter()
40                .find(|def| def.name == name)
41            {
42                available_layouts.push(def.clone());
43            } else {
44                tracing::warn!("There is no Layout with the name {:?}", name);
45            }
46        }
47
48        let mut available_layouts_per_ws: HashMap<usize, Vec<Layout>> = HashMap::new();
49
50        for (i, ws) in config.workspaces().unwrap_or_default().iter().enumerate() {
51            if let Some(ws_layout_names) = &ws.layouts {
52                let wsid = i + 1;
53                for ws_layout_name in ws_layout_names {
54                    if let Some(layout) = config
55                        .layout_definitions()
56                        .iter()
57                        .find(|layout| layout.name == *ws_layout_name)
58                    {
59                        available_layouts_per_ws
60                            .entry(wsid)
61                            .and_modify(|layouts| layouts.push(layout.clone()))
62                            .or_insert_with(|| vec![layout.clone()]);
63                    } else {
64                        tracing::warn!("There is no Layout with the name {:?}, but was configured on workspace {:?}", ws_layout_name, wsid);
65                    }
66                }
67            }
68        }
69
70        if available_layouts.is_empty() {
71            tracing::warn!(
72                "No Layouts were loaded from config - defaulting to a single default Layout"
73            );
74            available_layouts.push(Layout::default());
75        }
76
77        tracing::trace!("The general available layouts are: {:?}", available_layouts);
78        tracing::trace!(
79            "The workspace specific available layouts are: {:?}",
80            available_layouts_per_ws
81        );
82
83        Self {
84            mode: config.layout_mode(),
85            available_layouts,
86            available_layouts_per_ws,
87            layouts: HashMap::new(),
88        }
89    }
90
91    pub fn restore(&mut self, old: &LayoutManager) {
92        if self.mode != old.mode {
93            tracing::debug!("The LayoutMode has changed, layouts will not be restored");
94            return;
95        }
96        // TODO we could eventually try to map available layouts as best as we can
97        //      and only fallback to default for layouts not avialable anymore
98        if self.available_layouts != old.available_layouts {
99            tracing::debug!("The available Layouts have changed, layouts will not be restored");
100            return;
101        }
102        if self.available_layouts_per_ws != old.available_layouts_per_ws {
103            tracing::debug!(
104                "The available Layouts per Workspace have changed, layouts will not be restored"
105            );
106            return;
107        }
108        self.layouts.clone_from(&old.layouts);
109    }
110
111    /// Get back either the workspace ID or the tag ID, based on the current [`LayoutMode`]
112    fn id(&self, wsid: usize, tagid: usize) -> usize {
113        match self.mode {
114            LayoutMode::Tag => tagid,
115            LayoutMode::Workspace => wsid,
116        }
117    }
118
119    /// Get the layouts for the provided workspace / tag context
120    ///
121    /// If the layouts for the specific workspace / tag have not
122    /// yet been set up, they will be initialized by copying
123    /// from the [`Self::available_layouts`] or [`Self::available_layouts_per_ws`].
124    fn layouts(&mut self, wsid: usize, tagid: usize) -> &Vec<Layout> {
125        self.layouts_mut(wsid, tagid)
126    }
127
128    /// Get the mutable layouts for the provided workspace / tag context
129    ///
130    /// If the layouts for the specific workspace / tag have not
131    /// yet been set up, they will be initialized by copying
132    /// from the [`Self::available_layouts`] or [`Self::available_layouts_per_ws`].
133    fn layouts_mut(&mut self, wsid: usize, tagid: usize) -> &mut Vec<Layout> {
134        let id = self.id(wsid, tagid);
135        self.layouts.entry(id).or_insert_with(|| match &self.mode {
136            LayoutMode::Tag => self.available_layouts.clone(),
137            LayoutMode::Workspace => self
138                .available_layouts_per_ws
139                .get(&wsid)
140                .unwrap_or(&self.available_layouts)
141                .clone(),
142        })
143    }
144
145    /// Get the current [`Layout`] for the provided workspace / tag context
146    ///
147    /// This may return [`None`] if the layouts have not been set up for
148    /// the specific tag / workspace. If unsure, it is probably wiser
149    /// to use [`Self::layout(usize, usize)`], which will initialize
150    /// the layouts automatically.
151    pub fn layout_maybe(&self, wsid: usize, tagid: usize) -> Option<&Layout> {
152        let id = self.id(wsid, tagid);
153        self.layouts.get(&id).and_then(|vec| vec.first())
154    }
155
156    /// Get the current [`Layout`] for the provided workspace / tag context
157    ///
158    /// # Panics
159    /// May panic if `available_layouts` is empty, which shouldn't happen because
160    /// it always falls back to a default layout when it's empty
161    pub fn layout(&mut self, wsid: usize, tagid: usize) -> &Layout {
162        let layouts = self.layouts(wsid, tagid);
163        assert!(
164            !layouts.is_empty(),
165            "there should be always at least one layout, because LeftWM must fallback to a default if empty"
166        );
167        layouts.first().unwrap()
168    }
169
170    /// Get the current [`Layout`] for the provided workspace / tag context as mutable
171    ///
172    /// # Panics
173    /// May panic if `available_layouts` is empty, which shouldn't happen because
174    /// it always falls back to a default layout when it's empty
175    pub fn layout_mut(&mut self, wsid: usize, tagid: usize) -> &mut Layout {
176        let layouts = self.layouts_mut(wsid, tagid);
177        assert!(
178            !layouts.is_empty(),
179            "there should be always at least one layout, because LeftWM must fallback to a default if empty"
180        );
181        layouts.first_mut().unwrap()
182    }
183
184    pub fn cycle_next_layout(&mut self, wsid: usize, tagid: usize) {
185        cycle_vec(self.layouts_mut(wsid, tagid), -1);
186    }
187
188    pub fn cycle_previous_layout(&mut self, wsid: usize, tagid: usize) {
189        cycle_vec(self.layouts_mut(wsid, tagid), 1);
190    }
191
192    pub fn set_layout(&mut self, wsid: usize, tagid: usize, name: &str) {
193        let i = self
194            .layouts(wsid, tagid)
195            .iter()
196            .enumerate()
197            .find(|(_, layout)| layout.name == name)
198            .map(|(i, _)| i);
199
200        match i {
201            Some(index) => cycle_vec(self.layouts_mut(wsid, tagid), -(index as i32)),
202            None => None,
203        };
204    }
205
206    // todo - low priority: reset fn, that resets all the layouts to their unchanged properties
207}
208
209#[cfg(test)]
210mod tests {
211    use leftwm_layouts::layouts::Layouts;
212
213    use crate::{
214        config::tests::TestConfig,
215        layouts::{self, EVEN_VERTICAL, MONOCLE},
216    };
217
218    use super::LayoutManager;
219
220    fn layout_manager() -> LayoutManager {
221        let config = TestConfig {
222            layouts: vec![
223                layouts::MONOCLE.to_string(),
224                layouts::EVEN_VERTICAL.to_string(),
225                layouts::MAIN_AND_HORIZONTAL_STACK.to_string(),
226            ],
227            layout_definitions: Layouts::default().layouts,
228            workspaces: Some(vec![
229                crate::config::Workspace {
230                    layouts: Some(vec![
231                        layouts::CENTER_MAIN.to_string(),
232                        layouts::CENTER_MAIN_BALANCED.to_string(),
233                        layouts::MAIN_AND_DECK.to_string(),
234                    ]),
235                    ..Default::default()
236                },
237                crate::config::Workspace {
238                    ..Default::default()
239                },
240                crate::config::Workspace {
241                    layouts: Some(vec![]),
242                    ..Default::default()
243                },
244            ]),
245            ..Default::default()
246        };
247
248        LayoutManager::new(&config)
249    }
250
251    #[test]
252    fn layouts_should_fallback_to_the_global_list() {
253        let layout_manager = layout_manager();
254        assert_eq!(1, layout_manager.id(1, 2));
255    }
256
257    #[test]
258    fn monocle_layout_only_has_single_windows() {
259        let mut layout_manager = layout_manager();
260        layout_manager.set_layout(2, 1, MONOCLE);
261        assert_eq!(MONOCLE, &layout_manager.layout(2, 1).name);
262        layout_manager.set_layout(2, 1, EVEN_VERTICAL);
263        assert_eq!(EVEN_VERTICAL, &layout_manager.layout(2, 1).name);
264    }
265}