1#![allow(clippy::module_name_repetitions)]
3
4use std::fmt::Debug;
5
6use super::WindowState;
7use super::WindowType;
8use crate::Workspace;
9use crate::config::WindowHidingStrategy;
10use crate::models::Margins;
11use crate::models::TagId;
12use crate::models::Xyhw;
13use crate::models::XyhwBuilder;
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 #[serde(default)]
60 disable_mouse_grab: Option<bool>,
61 pub urgent: bool,
62 pub debugging: bool,
63 pub name: Option<String>,
64 pub legacy_name: Option<String>,
65 pub pid: Option<u32>,
66 pub r#type: WindowType,
67 pub tag: Option<TagId>,
68 pub border: i32,
69 pub margin: Margins,
70 pub margin_multiplier: f32,
71 pub states: Vec<WindowState>,
72 pub requested: Option<Xyhw>,
73 pub normal: Xyhw,
74 pub start_loc: Option<Xyhw>,
75 pub container_size: Option<Xyhw>,
76 pub strut: Option<Xyhw>,
77 pub res_name: Option<String>,
79 pub res_class: Option<String>,
80 pub hiding_strategy: Option<WindowHidingStrategy>,
81}
82
83impl<H: Handle> Window<H> {
84 #[must_use]
85 pub fn new(h: WindowHandle<H>, name: Option<String>, pid: Option<u32>) -> Self {
86 Self {
87 handle: h,
88 transient: None,
89 visible: false,
90 can_resize: true,
91 is_floating: false,
92 must_float: false,
93 debugging: false,
94 never_focus: false,
95 disable_mouse_grab: None,
96 urgent: false,
97 name,
98 pid,
99 legacy_name: None,
100 r#type: WindowType::Normal,
101 tag: None,
102 border: 1,
103 margin: Margins::new(10),
104 margin_multiplier: 1.0,
105 states: vec![],
106 normal: XyhwBuilder::default().into(),
107 requested: None,
108 floating: None,
109 start_loc: None,
110 container_size: None,
111 strut: None,
112 res_name: None,
113 res_class: None,
114 hiding_strategy: None,
115 }
116 }
117
118 pub fn set_visible(&mut self, value: bool) {
119 self.visible = value;
120 }
121
122 #[must_use]
123 pub fn visible(&self) -> bool {
124 self.visible
125 || self.r#type == WindowType::Menu
126 || self.r#type == WindowType::Splash
127 || self.r#type == WindowType::Toolbar
128 || self.r#type == WindowType::Notification
129 }
130
131 pub fn set_disable_mouse_grab(&mut self, value: Option<bool>) {
133 self.disable_mouse_grab = value;
134 }
135
136 #[must_use]
138 pub fn allows_mouse_warp(&self) -> bool {
139 match self.disable_mouse_grab {
140 Some(disabled) => !disabled,
141 None => self.r#type != WindowType::Utility,
142 }
143 }
144
145 pub fn set_floating(&mut self, value: bool) {
146 if !self.is_floating && value && self.floating.is_none() {
147 self.reset_float_offset();
149 }
150 self.is_floating = value;
151 }
152
153 #[must_use]
154 pub fn floating(&self) -> bool {
155 self.is_floating || self.must_float()
156 }
157
158 #[must_use]
159 pub const fn get_floating_offsets(&self) -> Option<Xyhw> {
160 self.floating
161 }
162
163 pub fn reset_float_offset(&mut self) {
164 let mut new_value = Xyhw::default();
165 new_value.clear_minmax();
166 self.floating = Some(new_value);
167 }
168
169 pub fn set_floating_offsets(&mut self, value: Option<Xyhw>) {
170 self.floating = value;
171 if let Some(value) = &mut self.floating {
172 value.clear_minmax();
173 }
174 }
175
176 pub fn set_floating_exact(&mut self, value: Xyhw) {
177 let mut new_value = value - self.normal;
178 new_value.clear_minmax();
179 self.floating = Some(new_value);
180 }
181
182 #[must_use]
183 pub fn is_fullscreen(&self) -> bool {
184 self.states.contains(&WindowState::Fullscreen)
185 }
186
187 #[must_use]
188 pub fn is_maximized(&self) -> bool {
189 self.states.contains(&WindowState::Maximized)
190 }
191
192 #[must_use]
193 pub fn is_sticky(&self) -> bool {
194 self.states.contains(&WindowState::Sticky)
195 }
196
197 #[must_use]
198 pub fn must_float(&self) -> bool {
199 self.must_float
200 || self.transient.is_some()
201 || !self.is_managed()
202 || self.r#type == WindowType::Splash
203 }
204 #[must_use]
205 pub fn can_move(&self) -> bool {
206 self.is_managed()
207 }
208 #[must_use]
209 pub fn can_resize(&self) -> bool {
210 self.can_resize && self.is_managed()
211 }
212
213 #[must_use]
214 pub fn can_focus(&self) -> bool {
215 !self.never_focus && self.is_managed() && self.visible()
216 }
217
218 pub fn set_width(&mut self, width: i32) {
219 self.normal.set_w(width);
220 }
221
222 pub fn set_height(&mut self, height: i32) {
223 self.normal.set_h(height);
224 }
225
226 pub fn apply_margin_multiplier(&mut self, value: f32) {
227 self.margin_multiplier = value.abs();
228 if value < 0 as f32 {
229 tracing::warn!(
230 "Negative margin multiplier detected. Will be applied as absolute: {:?}",
231 self.margin_multiplier()
232 );
233 }
234 }
235
236 #[must_use]
237 pub const fn margin_multiplier(&self) -> f32 {
238 self.margin_multiplier
239 }
240
241 #[must_use]
242 pub fn width(&self) -> i32 {
243 let mut value;
244 if self.is_fullscreen() {
245 value = self.normal.w();
246 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
247 let relative = self.normal + self.floating.unwrap_or_default();
248 value = relative.w() - (self.border * 2);
249 } else {
250 value = self.normal.w()
251 - (((self.margin.left + self.margin.right) as f32) * self.margin_multiplier) as i32
252 - (self.border * 2);
253 }
254 let limit = match self.requested {
255 Some(requested) if requested.minw() > 0 && self.floating() => requested.minw(),
256 _ => 100,
257 };
258 if value < limit && self.is_managed() {
259 value = limit;
260 }
261 value
262 }
263
264 #[must_use]
265 pub fn height(&self) -> i32 {
266 let mut value;
267 if self.is_fullscreen() {
268 value = self.normal.h();
269 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
270 let relative = self.normal + self.floating.unwrap_or_default();
271 value = relative.h() - (self.border * 2);
272 } else {
273 value = self.normal.h()
274 - (((self.margin.top + self.margin.bottom) as f32) * self.margin_multiplier) as i32
275 - (self.border * 2);
276 }
277 let limit = match self.requested {
278 Some(requested) if requested.minh() > 0 && self.floating() => requested.minh(),
279 _ => 100,
280 };
281 if value < limit && self.is_managed() {
282 value = limit;
283 }
284 value
285 }
286
287 pub fn set_x(&mut self, x: i32) {
288 self.normal.set_x(x);
289 }
290 pub fn set_y(&mut self, y: i32) {
291 self.normal.set_y(y);
292 }
293
294 #[must_use]
295 pub fn border(&self) -> i32 {
296 if self.is_fullscreen() { 0 } else { self.border }
297 }
298
299 #[must_use]
300 pub fn x(&self) -> i32 {
301 if self.is_fullscreen() {
302 self.normal.x()
303 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
304 let relative = self.normal + self.floating.unwrap_or_default();
305 relative.x()
306 } else {
307 self.normal.x() + (self.margin.left as f32 * self.margin_multiplier) as i32
308 }
309 }
310
311 #[must_use]
312 pub fn y(&self) -> i32 {
313 if self.is_fullscreen() {
314 self.normal.y()
315 } else if self.floating() && self.floating.is_some() && !self.is_maximized() {
316 let relative = self.normal + self.floating.unwrap_or_default();
317 relative.y()
318 } else {
319 self.normal.y() + (self.margin.top as f32 * self.margin_multiplier) as i32
320 }
321 }
322
323 #[must_use]
324 pub fn calculated_xyhw(&self) -> Xyhw {
325 XyhwBuilder {
326 h: self.height(),
327 w: self.width(),
328 x: self.x(),
329 y: self.y(),
330 ..XyhwBuilder::default()
331 }
332 .into()
333 }
334
335 #[must_use]
336 pub fn exact_xyhw(&self) -> Xyhw {
337 if self.floating() && self.floating.is_some() {
338 self.normal + self.floating.unwrap_or_default()
339 } else {
340 self.normal
341 }
342 }
343
344 #[must_use]
345 pub fn contains_point(&self, x: i32, y: i32) -> bool {
346 self.calculated_xyhw().contains_point(x, y)
347 }
348
349 pub fn tag(&mut self, tag: &TagId) {
350 self.tag = Some(*tag);
351 }
352
353 #[must_use]
354 pub fn has_tag(&self, tag: &TagId) -> bool {
355 self.tag == Some(*tag)
356 }
357
358 pub fn untag(&mut self) {
359 self.tag = None;
360 }
361
362 #[must_use]
363 pub fn is_managed(&self) -> bool {
364 self.r#type != WindowType::Desktop
365 && self.r#type != WindowType::Dock
366 && self.r#type != WindowType::DropdownMenu
367 && self.r#type != WindowType::PopupMenu
368 && self.r#type != WindowType::Tooltip
369 && self.r#type != WindowType::Notification
370 && self.r#type != WindowType::Combo
371 && self.r#type != WindowType::Dnd
372 }
373
374 #[must_use]
375 pub fn is_normal(&self) -> bool {
376 self.r#type == WindowType::Normal
377 }
378
379 pub fn snap_to_workspace(&mut self, workspace: &Workspace) -> bool {
380 self.set_floating(false);
381
382 if self.tag != workspace.tag {
384 self.tag = workspace.tag;
385 let mut offset = self.get_floating_offsets().unwrap_or_default();
386 let mut start_loc = self.start_loc.unwrap_or_default();
387 let x = offset.x() + self.normal.x();
388 let y = offset.y() + self.normal.y();
389 offset.set_x(x - workspace.xyhw.x());
390 offset.set_y(y - workspace.xyhw.y());
391 self.set_floating_offsets(Some(offset));
392
393 let x = start_loc.x() + self.normal.x();
394 let y = start_loc.y() + self.normal.y();
395 start_loc.set_x(x - workspace.xyhw.x());
396 start_loc.set_y(y - workspace.xyhw.y());
397 self.start_loc = Some(start_loc);
398 }
399 true
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn should_be_able_to_tag_a_window() {
409 let mut subject = Window::new(WindowHandle::<MockHandle>(1), None, None);
410 subject.tag(&1);
411 assert!(subject.has_tag(&1), "was unable to tag the window");
412 }
413
414 #[test]
415 fn should_be_able_to_untag_a_window() {
416 let mut subject = Window::new(WindowHandle::<MockHandle>(1), None, None);
417 subject.tag(&1);
418 subject.untag();
419 assert!(!subject.has_tag(&1), "was unable to untag the window");
420 }
421
422 #[test]
423 fn mouse_warp_policy_honors_type_defaults_and_explicit_overrides() {
424 let cases = [
425 (1, WindowType::Normal, None, true),
426 (2, WindowType::Utility, None, false),
427 (3, WindowType::Normal, Some(true), false),
428 (4, WindowType::Utility, Some(false), true),
429 ];
430
431 for (handle, window_type, disable_mouse_grab, expected) in cases {
432 let mut subject = Window::new(WindowHandle::<MockHandle>(handle), None, None);
433 subject.r#type = window_type;
434 subject.set_disable_mouse_grab(disable_mouse_grab);
435
436 assert_eq!(
437 subject.allows_mouse_warp(),
438 expected,
439 "unexpected mouse-warp policy for {:?} with disable_mouse_grab={:?}",
440 subject.r#type,
441 disable_mouse_grab,
442 );
443 }
444 }
445
446 #[test]
447 fn missing_mouse_grab_policy_deserializes_as_the_type_default() {
448 let mut subject = Window::new(WindowHandle::<MockHandle>(1), None, None);
449 subject.r#type = WindowType::Utility;
450 subject.set_disable_mouse_grab(Some(false));
451
452 let mut serialized = serde_json::to_value(subject).expect("window should serialize");
453 let fields = serialized
454 .as_object_mut()
455 .expect("a serialized window should be a map");
456 assert!(fields.remove("disable_mouse_grab").is_some());
457
458 let restored: Window<MockHandle> =
459 serde_json::from_value(serialized).expect("legacy window state should deserialize");
460
461 assert_eq!(restored.disable_mouse_grab, None);
462 assert!(!restored.allows_mouse_warp());
463 }
464}