leftwm_core/models/
workspace.rs

1use crate::config::Config;
2use crate::models::{BBox, Gutter, Margins, Side, TagId, Window, Xyhw, XyhwBuilder};
3use leftwm_layouts::geometry::Rect;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use super::{Handle, WorkspaceId};
8
9/// Information for workspaces (screen divisions).
10#[derive(Serialize, Deserialize, Clone)]
11pub struct Workspace {
12    // tag represents the currently visible tag
13    pub tag: Option<TagId>, // TODO: Make this a list.
14    pub margin: Margins,
15    pub margin_multiplier: f32,
16    pub gutters: Vec<Gutter>,
17    #[serde(skip)]
18    pub avoid: Vec<Xyhw>,
19    pub xyhw: Xyhw,
20    pub xyhw_avoided: Xyhw,
21    /// ID of workspace. Starts with 1.
22    pub id: WorkspaceId,
23}
24
25impl fmt::Debug for Workspace {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        write!(
28            f,
29            "Workspace {{ id: {}, tags: {:?}, x: {}, y: {} }}",
30            self.id,
31            self.tag,
32            self.xyhw.x(),
33            self.xyhw.y()
34        )
35    }
36}
37
38impl PartialEq for Workspace {
39    fn eq(&self, other: &Self) -> bool {
40        self.id == other.id
41    }
42}
43
44impl Workspace {
45    #[must_use]
46    pub fn new(bbox: BBox, id: usize) -> Self {
47        Self {
48            tag: None,
49            margin: Margins::new(10),
50            margin_multiplier: 1.0,
51            gutters: vec![],
52            avoid: vec![],
53            xyhw: XyhwBuilder {
54                h: bbox.height,
55                w: bbox.width,
56                x: bbox.x,
57                y: bbox.y,
58                ..XyhwBuilder::default()
59            }
60            .into(),
61            xyhw_avoided: XyhwBuilder {
62                h: bbox.height,
63                w: bbox.width,
64                x: bbox.x,
65                y: bbox.y,
66                ..XyhwBuilder::default()
67            }
68            .into(),
69            id,
70        }
71    }
72
73    pub fn load_config(&mut self, config: &impl Config) {
74        self.margin = config.workspace_margin().unwrap_or_else(|| Margins::new(0));
75        self.gutters = self.get_gutters_for_theme(config);
76    }
77
78    pub fn get_gutters_for_theme(&mut self, config: &impl Config) -> Vec<Gutter> {
79        config
80            .get_list_of_gutters()
81            .into_iter()
82            .filter(|gutter| gutter.id.is_none() || gutter.id == Some(self.id))
83            .fold(vec![], |mut acc, gutter| {
84                match acc.iter().enumerate().find(|(_i, g)| g.side == gutter.side) {
85                    Some((i, x)) => {
86                        if x.id.is_none() {
87                            acc[i] = gutter;
88                        }
89                    }
90                    None => acc.push(gutter),
91                }
92                acc
93            })
94    }
95
96    pub fn show_tag(&mut self, tag: &TagId) {
97        self.tag = Some(*tag);
98    }
99
100    #[must_use]
101    pub const fn contains_point(&self, x: i32, y: i32) -> bool {
102        self.xyhw.contains_point(x, y)
103    }
104
105    #[must_use]
106    pub fn has_tag(&self, tag: &TagId) -> bool {
107        self.tag == Some(*tag)
108    }
109
110    /// Returns true if the workspace is displays a given window.
111    #[must_use]
112    pub fn is_displaying<H: Handle>(&self, window: &Window<H>) -> bool {
113        if let Some(tag) = &window.tag {
114            return self.has_tag(tag);
115        }
116        false
117    }
118
119    /// Returns true if the workspace is to update the locations info of this window.
120    #[must_use]
121    pub fn is_managed<H: Handle>(&self, window: &Window<H>) -> bool {
122        self.is_displaying(window) && window.is_managed()
123    }
124
125    /// Returns the original x position of the workspace
126    #[must_use]
127    pub fn x(&self) -> i32 {
128        let left = self.margin.left as f32;
129        let gutter = self.get_gutter(&Side::Left);
130        self.xyhw_avoided.x() + (self.margin_multiplier * left) as i32 + gutter
131    }
132
133    #[must_use]
134    pub fn y(&self) -> i32 {
135        let top = self.margin.top as f32;
136        let gutter = self.get_gutter(&Side::Top);
137        self.xyhw_avoided.y() + (self.margin_multiplier * top) as i32 + gutter
138    }
139
140    #[must_use]
141    pub fn height(&self) -> i32 {
142        let top = self.margin.top as f32;
143        let bottom = self.margin.bottom as f32;
144        // Only one side
145        let gutter = self.get_gutter(&Side::Top) + self.get_gutter(&Side::Bottom);
146        self.xyhw_avoided.h() - (self.margin_multiplier * (top + bottom)) as i32 - gutter
147    }
148
149    /// Returns the original width for the workspace
150    #[must_use]
151    pub fn width(&self) -> i32 {
152        let left = self.margin.left as f32;
153        let right = self.margin.right as f32;
154        // Only one side
155        let gutter = self.get_gutter(&Side::Left) + self.get_gutter(&Side::Right);
156        self.xyhw_avoided.w() - (self.margin_multiplier * (left + right)) as i32 - gutter
157    }
158
159    fn get_gutter(&self, side: &Side) -> i32 {
160        match self.gutters.iter().find(|g| &g.side == side) {
161            Some(g) => g.value,
162            None => 0,
163        }
164    }
165
166    #[must_use]
167    pub fn center_halfed(&self) -> Xyhw {
168        self.xyhw_avoided.center_halfed()
169    }
170
171    pub fn update_avoided_areas(&mut self) {
172        let mut xyhw = self.xyhw;
173        for a in &self.avoid {
174            xyhw = xyhw.without(a);
175        }
176        self.xyhw_avoided = xyhw;
177    }
178
179    /// Set the tag model's margin multiplier.
180    pub fn set_margin_multiplier(&mut self, margin_multiplier: f32) {
181        self.margin_multiplier = margin_multiplier;
182    }
183
184    /// Get a reference to the tag model's margin multiplier.
185    #[must_use]
186    pub const fn margin_multiplier(&self) -> f32 {
187        self.margin_multiplier
188    }
189
190    pub fn rect(&self) -> Rect {
191        Rect {
192            x: self.x(),
193            y: self.y(),
194            w: self.width().unsigned_abs(),
195            h: self.height().unsigned_abs(),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::models::{BBox, MockHandle, WindowHandle};
204
205    #[test]
206    fn empty_ws_should_not_contain_window() {
207        let subject = Workspace::new(
208            BBox {
209                width: 600,
210                height: 800,
211                x: 0,
212                y: 0,
213            },
214            0,
215        );
216        let w = Window::new(WindowHandle::<MockHandle>(1), None, None);
217        assert!(
218            !subject.is_displaying(&w),
219            "workspace incorrectly owns window"
220        );
221    }
222
223    #[test]
224    fn tagging_a_workspace_to_with_the_same_tag_as_a_window_should_couse_it_to_display() {
225        const TAG_ID: TagId = 1;
226        let mut subject = Workspace::new(
227            BBox {
228                width: 600,
229                height: 800,
230                x: 0,
231                y: 0,
232            },
233            0,
234        );
235        let tag = crate::models::Tag::new(TAG_ID, "test");
236        subject.show_tag(&tag.id);
237        let mut w = Window::new(WindowHandle::<MockHandle>(1), None, None);
238        w.tag(&TAG_ID);
239        assert!(subject.is_displaying(&w), "workspace should include window");
240    }
241}