1#![allow(clippy::module_name_repetitions)]
3
4use std::fmt::Debug;
5
6use super::WindowState;
7use super::WindowType;
8use crate::config::WindowHidingStrategy;
9use crate::models::Margins;
10use crate::models::TagId;
11use crate::models::Xyhw;
12use crate::models::XyhwBuilder;
13use crate::Workspace;
14use serde::de::DeserializeOwned;
15use serde::{Deserialize, Serialize};
16
17pub trait Handle:
19 Serialize + DeserializeOwned + Debug + Clone + Copy + PartialEq + Eq + Default + Send + 'static
20{
21}
22
23#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
34pub struct WindowHandle<H>(#[serde(bound = "")] pub H)
35where
36 H: Handle;
37
38pub type MockHandle = i32;
40impl Handle for MockHandle {}
41
42#[allow(clippy::struct_excessive_bools)]
46#[derive(Serialize, Deserialize, Debug, Clone)]
47pub struct Window<H: Handle> {
48 #[serde(bound = "")]
49 pub handle: WindowHandle<H>,
50 #[serde(bound = "")]
51 pub transient: Option<WindowHandle<H>>,
52 visible: bool,
53 pub can_resize: bool,
54 is_floating: bool,
55 pub(crate) must_float: bool,
56 floating: Option<Xyhw>,
57 pub never_focus: bool,
58 pub urgent: bool,
59 pub debugging: bool,
60 pub name: Option<String>,
61 pub legacy_name: Option<String>,
62 pub pid: Option<u32>,
63 pub r#type: WindowType,
64 pub tag: Option<TagId>,
65 pub border: i32,
66 pub margin: Margins,
67 pub margin_multiplier: f32,
68 pub states: Vec<WindowState>,
69 pub requested: Option<Xyhw>,
70 pub normal: Xyhw,
71 pub start_loc: Option<Xyhw>,
72 pub container_size: Option<Xyhw>,
73 pub strut: Option<Xyhw>,
74 pub res_name: Option<String>,
76 pub res_class: Option<String>,
77 pub hiding_strategy: Option<WindowHidingStrategy>,
78}
79
80impl<H: Handle> Window<H> {
81 #[must_use]
82 pub fn new(h: WindowHandle<H>, name: Option<String>, pid: Option<u32>) -> Self {
83 Self {
84 handle: h,
85 transient: None,
86 visible: false,
87 can_resize: true,
88 is_floating: false,
89 must_float: false,
90 debugging: false,
91 never_focus: false,
92 urgent: false,
93 name,
94 pid,
95 legacy_name: None,
96 r#type: WindowType::Normal,
97 tag: None,
98 border: 1,
99 margin: Margins::new(10),
100 margin_multiplier: 1.0,
101 states: vec![],
102 normal: XyhwBuilder::default().into(),
103 requested: None,
104 floating: None,
105 start_loc: None,
106 container_size: None,
107 strut: None,
108 res_name: None,
109 res_class: None,
110 hiding_strategy: None,
111 }
112 }
113
114 pub fn set_visible(&mut self, value: bool) {
115 self.visible = value;
116 }
117
118 #[must_use]
119 pub fn visible(&self) -> bool {
120 self.visible
121 || self.r#type == WindowType::Menu
122 || self.r#type == WindowType::Splash
123 || self.r#type == WindowType::Toolbar
124 }
125
126 pub fn set_floating(&mut self, value: bool) {
127 if !self.is_floating && value && self.floating.is_none() {
128 self.reset_float_offset();
130 }
131 self.is_floating = value;
132 }
133
134 #[must_use]
135 pub fn floating(&self) -> bool {
136 self.is_floating || self.must_float()
137 }
138
139 #[must_use]
140 pub const fn get_floating_offsets(&self) -> Option<Xyhw> {
141 self.floating
142 }
143
144 pub fn reset_float_offset(&mut self) {
145 let mut new_value = Xyhw::default();
146 new_value.clear_minmax();
147 self.floating = Some(new_value);
148 }
149
150 pub fn set_floating_offsets(&mut self, value: Option<Xyhw>) {
151 self.floating = value;
152 if let Some(value) = &mut self.floating {
153 value.clear_minmax();
154 }
155 }
156
157 pub fn set_floating_exact(&mut self, value: Xyhw) {
158 let mut new_value = value - self.normal;
159 new_value.clear_minmax();
160 self.floating = Some(new_value);
161 }
162
163 #[must_use]
164 pub fn is_fullscreen(&self) -> bool {
165 self.states.contains(&WindowState::Fullscreen)
166 }
167
168 #[must_use]
169 pub fn is_maximized(&self) -> bool {
170 self.states.contains(&WindowState::Maximized)
171 }
172
173 #[must_use]
174 pub fn is_sticky(&self) -> bool {
175 self.states.contains(&WindowState::Sticky)
176 }
177
178 #[must_use]
179 pub fn must_float(&self) -> bool {
180 self.must_float
181 || self.transient.is_some()
182 || !self.is_managed()
183 || self.r#type == WindowType::Splash
184 }
185 #[must_use]
186 pub fn can_move(&self) -> bool {
187 self.is_managed()
188 }
189 #[must_use]
190 pub fn can_resize(&self) -> bool {
191 self.can_resize && self.is_managed()
192 }
193
194 #[must_use]
195 pub fn can_focus(&self) -> bool {
196 !self.never_focus && self.is_managed() && self.visible()
197 }
198
199 pub fn set_width(&mut self, width: i32) {
200 self.normal.set_w(width);
201 }
202
203 pub fn set_height(&mut self, height: i32) {
204 self.normal.set_h(height);
205 }
206
207 pub fn apply_margin_multiplier(&mut self, value: f32) {
208 self.margin_multiplier = value.abs();
209 if value < 0 as f32 {
210 tracing::warn!(
211 "Negative margin multiplier detected. Will be applied as absolute: {:?}",
212 self.margin_multiplier()
213 );
214 };
215 }
216
217 #[must_use]
218 pub const fn margin_multiplier(&self) -> f32 {
219 self.margin_multiplier
220 }
221
222 #[must_use]
223 pub fn width(&self) -> i32 {
224 let mut value;
225 if self.is_fullscreen() {
226 value = self.normal.w();
227 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
228 let relative = self.normal + self.floating.unwrap_or_default();
229 value = relative.w() - (self.border * 2);
230 } else {
231 value = self.normal.w()
232 - (((self.margin.left + self.margin.right) as f32) * self.margin_multiplier) as i32
233 - (self.border * 2);
234 }
235 let limit = match self.requested {
236 Some(requested) if requested.minw() > 0 && self.floating() => requested.minw(),
237 _ => 100,
238 };
239 if value < limit && self.is_managed() {
240 value = limit;
241 }
242 value
243 }
244
245 #[must_use]
246 pub fn height(&self) -> i32 {
247 let mut value;
248 if self.is_fullscreen() {
249 value = self.normal.h();
250 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
251 let relative = self.normal + self.floating.unwrap_or_default();
252 value = relative.h() - (self.border * 2);
253 } else {
254 value = self.normal.h()
255 - (((self.margin.top + self.margin.bottom) as f32) * self.margin_multiplier) as i32
256 - (self.border * 2);
257 }
258 let limit = match self.requested {
259 Some(requested) if requested.minh() > 0 && self.floating() => requested.minh(),
260 _ => 100,
261 };
262 if value < limit && self.is_managed() {
263 value = limit;
264 }
265 value
266 }
267
268 pub fn set_x(&mut self, x: i32) {
269 self.normal.set_x(x);
270 }
271 pub fn set_y(&mut self, y: i32) {
272 self.normal.set_y(y);
273 }
274
275 #[must_use]
276 pub fn border(&self) -> i32 {
277 if self.is_fullscreen() {
278 0
279 } else {
280 self.border
281 }
282 }
283
284 #[must_use]
285 pub fn x(&self) -> i32 {
286 if self.is_fullscreen() {
287 self.normal.x()
288 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
289 let relative = self.normal + self.floating.unwrap_or_default();
290 relative.x()
291 } else {
292 self.normal.x() + (self.margin.left as f32 * self.margin_multiplier) as i32
293 }
294 }
295
296 #[must_use]
297 pub fn y(&self) -> i32 {
298 if self.is_fullscreen() {
299 self.normal.y()
300 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
301 let relative = self.normal + self.floating.unwrap_or_default();
302 relative.y()
303 } else {
304 self.normal.y() + (self.margin.top as f32 * self.margin_multiplier) as i32
305 }
306 }
307
308 #[must_use]
309 pub fn calculated_xyhw(&self) -> Xyhw {
310 XyhwBuilder {
311 h: self.height(),
312 w: self.width(),
313 x: self.x(),
314 y: self.y(),
315 ..XyhwBuilder::default()
316 }
317 .into()
318 }
319
320 #[must_use]
321 pub fn exact_xyhw(&self) -> Xyhw {
322 if self.floating() && self.floating.is_some() {
323 self.normal + self.floating.unwrap_or_default()
324 } else {
325 self.normal
326 }
327 }
328
329 #[must_use]
330 pub fn contains_point(&self, x: i32, y: i32) -> bool {
331 self.calculated_xyhw().contains_point(x, y)
332 }
333
334 pub fn tag(&mut self, tag: &TagId) {
335 self.tag = Some(*tag);
336 }
337
338 #[must_use]
339 pub fn has_tag(&self, tag: &TagId) -> bool {
340 self.tag == Some(*tag)
341 }
342
343 pub fn untag(&mut self) {
344 self.tag = None;
345 }
346
347 #[must_use]
348 pub fn is_managed(&self) -> bool {
349 self.r#type != WindowType::Desktop && self.r#type != WindowType::Dock
350 }
351
352 #[must_use]
353 pub fn is_normal(&self) -> bool {
354 self.r#type == WindowType::Normal
355 }
356
357 pub fn snap_to_workspace(&mut self, workspace: &Workspace) -> bool {
358 self.set_floating(false);
359
360 if self.tag != workspace.tag {
362 self.tag = workspace.tag;
363 let mut offset = self.get_floating_offsets().unwrap_or_default();
364 let mut start_loc = self.start_loc.unwrap_or_default();
365 let x = offset.x() + self.normal.x();
366 let y = offset.y() + self.normal.y();
367 offset.set_x(x - workspace.xyhw.x());
368 offset.set_y(y - workspace.xyhw.y());
369 self.set_floating_offsets(Some(offset));
370
371 let x = start_loc.x() + self.normal.x();
372 let y = start_loc.y() + self.normal.y();
373 start_loc.set_x(x - workspace.xyhw.x());
374 start_loc.set_y(y - workspace.xyhw.y());
375 self.start_loc = Some(start_loc);
376 }
377 true
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn should_be_able_to_tag_a_window() {
387 let mut subject = Window::new(WindowHandle::<MockHandle>(1), None, None);
388 subject.tag(&1);
389 assert!(subject.has_tag(&1), "was unable to tag the window");
390 }
391
392 #[test]
393 fn should_be_able_to_untag_a_window() {
394 let mut subject = Window::new(WindowHandle::<MockHandle>(1), None, None);
395 subject.tag(&1);
396 subject.untag();
397 assert!(!subject.has_tag(&1), "was unable to untag the window");
398 }
399}