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#[derive(Serialize, Deserialize, Clone)]
11pub struct Workspace {
12 pub tag: Option<TagId>, 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 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 #[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 #[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 #[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 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 #[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 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 pub fn set_margin_multiplier(&mut self, margin_multiplier: f32) {
181 self.margin_multiplier = margin_multiplier;
182 }
183
184 #[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}