Skip to main content

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!(
65                            "There is no Layout with the name {:?}, but was configured on workspace {:?}",
66                            ws_layout_name,
67                            wsid
68                        );
69                    }
70                }
71            }
72            if let Some(default_layout) = &ws.default_layout {
73                let wsid = i + 1;
74                if let Some(layout) = config
75                    .layout_definitions()
76                    .iter()
77                    .find(|layout| layout.name == *default_layout)
78                {
79                    // add the default layout to the available layouts if it's not already there
80                    available_layouts_per_ws
81                        .entry(wsid)
82                        .and_modify(|layouts| {
83                            if !layouts.iter().any(|l| l.name == layout.name) {
84                                layouts.push(layout.clone());
85                            }
86                        })
87                        .or_insert_with(|| vec![layout.clone()]);
88                } else {
89                    tracing::warn!(
90                        "There is no Layout with the name {:?}, but was configured as default on workspace {:?}",
91                        default_layout,
92                        wsid
93                    );
94                }
95            }
96        }
97
98        if available_layouts.is_empty() {
99            tracing::warn!(
100                "No Layouts were loaded from config - defaulting to a single default Layout"
101            );
102            available_layouts.push(Layout::default());
103        }
104
105        tracing::trace!("The general available layouts are: {:?}", available_layouts);
106        tracing::trace!(
107            "The workspace specific available layouts are: {:?}",
108            available_layouts_per_ws
109        );
110
111        let mut layout_manager = Self {
112            mode: config.layout_mode(),
113            available_layouts,
114            available_layouts_per_ws,
115            layouts: HashMap::new(),
116        };
117
118        // set the current layout to the default layout for workspaces that have one configured
119        for (i, ws) in config.workspaces().unwrap_or_default().iter().enumerate() {
120            if let Some(default_layout) = &ws.default_layout {
121                let wsid = i + 1;
122                layout_manager.set_layout(wsid, wsid, default_layout);
123            }
124        }
125
126        layout_manager
127    }
128
129    pub fn restore(&mut self, old: &LayoutManager) {
130        if self.mode != old.mode {
131            tracing::debug!("The LayoutMode has changed, layouts will not be restored");
132            return;
133        }
134        // TODO we could eventually try to map available layouts as best as we can
135        //      and only fallback to default for layouts not avialable anymore
136        if self.available_layouts != old.available_layouts {
137            tracing::debug!("The available Layouts have changed, layouts will not be restored");
138            return;
139        }
140        if self.available_layouts_per_ws != old.available_layouts_per_ws {
141            tracing::debug!(
142                "The available Layouts per Workspace have changed, layouts will not be restored"
143            );
144            return;
145        }
146        self.layouts.clone_from(&old.layouts);
147    }
148
149    /// Get back either the workspace ID or the tag ID, based on the current [`LayoutMode`]
150    fn id(&self, wsid: usize, tagid: usize) -> usize {
151        match self.mode {
152            LayoutMode::Tag => tagid,
153            LayoutMode::Workspace => wsid,
154        }
155    }
156
157    /// Get the layouts for the provided workspace / tag context
158    ///
159    /// If the layouts for the specific workspace / tag have not
160    /// yet been set up, they will be initialized by copying
161    /// from the [`Self::available_layouts`] or [`Self::available_layouts_per_ws`].
162    fn layouts(&mut self, wsid: usize, tagid: usize) -> &Vec<Layout> {
163        self.layouts_mut(wsid, tagid)
164    }
165
166    /// Get the mutable layouts for the provided workspace / tag context
167    ///
168    /// If the layouts for the specific workspace / tag have not
169    /// yet been set up, they will be initialized by copying
170    /// from the [`Self::available_layouts`] or [`Self::available_layouts_per_ws`].
171    fn layouts_mut(&mut self, wsid: usize, tagid: usize) -> &mut Vec<Layout> {
172        let id = self.id(wsid, tagid);
173        self.layouts.entry(id).or_insert_with(|| match &self.mode {
174            LayoutMode::Tag => self.available_layouts.clone(),
175            LayoutMode::Workspace => self
176                .available_layouts_per_ws
177                .get(&wsid)
178                .unwrap_or(&self.available_layouts)
179                .clone(),
180        })
181    }
182
183    /// Get the current [`Layout`] for the provided workspace / tag context
184    ///
185    /// This may return [`None`] if the layouts have not been set up for
186    /// the specific tag / workspace. If unsure, it is probably wiser
187    /// to use [`Self::layout(usize, usize)`], which will initialize
188    /// the layouts automatically.
189    pub fn layout_maybe(&self, wsid: usize, tagid: usize) -> Option<&Layout> {
190        let id = self.id(wsid, tagid);
191        self.layouts.get(&id).and_then(|vec| vec.first())
192    }
193
194    /// Get the current [`Layout`] for the provided workspace / tag context
195    ///
196    /// # Panics
197    /// May panic if `available_layouts` is empty, which shouldn't happen because
198    /// it always falls back to a default layout when it's empty
199    pub fn layout(&mut self, wsid: usize, tagid: usize) -> &Layout {
200        let layouts = self.layouts(wsid, tagid);
201        assert!(
202            !layouts.is_empty(),
203            "there should be always at least one layout, because LeftWM must fallback to a default if empty"
204        );
205        layouts.first().unwrap()
206    }
207
208    /// Get the current [`Layout`] for the provided workspace / tag context as mutable
209    ///
210    /// # Panics
211    /// May panic if `available_layouts` is empty, which shouldn't happen because
212    /// it always falls back to a default layout when it's empty
213    pub fn layout_mut(&mut self, wsid: usize, tagid: usize) -> &mut Layout {
214        let layouts = self.layouts_mut(wsid, tagid);
215        assert!(
216            !layouts.is_empty(),
217            "there should be always at least one layout, because LeftWM must fallback to a default if empty"
218        );
219        layouts.first_mut().unwrap()
220    }
221
222    pub fn cycle_next_layout(&mut self, wsid: usize, tagid: usize) {
223        cycle_vec(self.layouts_mut(wsid, tagid), -1);
224    }
225
226    pub fn cycle_previous_layout(&mut self, wsid: usize, tagid: usize) {
227        cycle_vec(self.layouts_mut(wsid, tagid), 1);
228    }
229
230    pub fn set_layout(&mut self, wsid: usize, tagid: usize, name: &str) {
231        let i = self
232            .layouts(wsid, tagid)
233            .iter()
234            .enumerate()
235            .find(|(_, layout)| layout.name == name)
236            .map(|(i, _)| i);
237
238        match i {
239            Some(index) => cycle_vec(self.layouts_mut(wsid, tagid), -(index as i32)),
240            None => None,
241        };
242    }
243
244    // todo - low priority: reset fn, that resets all the layouts to their unchanged properties
245}
246
247#[cfg(test)]
248mod tests {
249    use leftwm_layouts::layouts::Layouts;
250
251    use crate::{
252        config::tests::TestConfig,
253        layouts::{self, EVEN_VERTICAL, MONOCLE},
254    };
255
256    use super::LayoutManager;
257
258    fn layout_manager() -> LayoutManager {
259        let config = TestConfig {
260            layouts: vec![
261                layouts::MONOCLE.to_string(),
262                layouts::EVEN_VERTICAL.to_string(),
263                layouts::MAIN_AND_HORIZONTAL_STACK.to_string(),
264            ],
265            layout_definitions: Layouts::default().layouts,
266            workspaces: Some(vec![
267                crate::config::Workspace {
268                    layouts: Some(vec![
269                        layouts::CENTER_MAIN.to_string(),
270                        layouts::CENTER_MAIN_BALANCED.to_string(),
271                        layouts::MAIN_AND_DECK.to_string(),
272                    ]),
273                    ..Default::default()
274                },
275                crate::config::Workspace {
276                    ..Default::default()
277                },
278                crate::config::Workspace {
279                    layouts: Some(vec![]),
280                    ..Default::default()
281                },
282                // case where default is available globally
283                crate::config::Workspace {
284                    default_layout: Some(layouts::MAIN_AND_HORIZONTAL_STACK.to_string()),
285                    ..Default::default()
286                },
287                // case where default is available to the workspace, but not globally
288                crate::config::Workspace {
289                    layouts: Some(vec![
290                        layouts::CENTER_MAIN.to_string(),
291                        layouts::CENTER_MAIN_BALANCED.to_string(),
292                        layouts::MAIN_AND_VERT_STACK.to_string(),
293                    ]),
294                    default_layout: Some(layouts::MAIN_AND_VERT_STACK.to_string()),
295                    ..Default::default()
296                },
297                // case where the default exists but is not previously available
298                crate::config::Workspace {
299                    default_layout: Some(layouts::FIBONACCI.to_string()),
300                    ..Default::default()
301                },
302                // same as above, but workspace explicitly given no layouts
303                crate::config::Workspace {
304                    layouts: Some(vec![]),
305                    default_layout: Some(layouts::EVEN_VERTICAL.to_string()),
306                    ..Default::default()
307                },
308                // case where default is available globally, but not locally
309                crate::config::Workspace {
310                    layouts: Some(vec![
311                        layouts::CENTER_MAIN_BALANCED.to_string(),
312                        layouts::MAIN_AND_DECK.to_string(),
313                    ]),
314                    default_layout: Some(layouts::CENTER_MAIN.to_string()),
315                    ..Default::default()
316                },
317            ]),
318            ..Default::default()
319        };
320
321        LayoutManager::new(&config)
322    }
323
324    #[test]
325    fn layouts_should_fallback_to_the_global_list() {
326        let layout_manager = layout_manager();
327        assert_eq!(1, layout_manager.id(1, 2));
328    }
329
330    #[test]
331    fn monocle_layout_only_has_single_windows() {
332        let mut layout_manager = layout_manager();
333        layout_manager.set_layout(2, 1, MONOCLE);
334        assert_eq!(MONOCLE, &layout_manager.layout(2, 1).name);
335        layout_manager.set_layout(2, 1, EVEN_VERTICAL);
336        assert_eq!(EVEN_VERTICAL, &layout_manager.layout(2, 1).name);
337    }
338
339    #[test]
340    fn default_layouts_should_be_set() {
341        let mut layout_manager = layout_manager();
342        assert_eq!(
343            layouts::MAIN_AND_HORIZONTAL_STACK,
344            &layout_manager.layout(4, 1).name
345        );
346        assert_eq!(
347            layouts::MAIN_AND_VERT_STACK,
348            &layout_manager.layout(5, 1).name
349        );
350        assert_eq!(layouts::FIBONACCI, &layout_manager.layout(6, 1).name);
351        assert_eq!(layouts::EVEN_VERTICAL, &layout_manager.layout(7, 1).name);
352        assert_eq!(layouts::CENTER_MAIN, &layout_manager.layout(8, 1).name);
353    }
354}