Skip to main content

leftwm_core/models/
window.rs

1//! Window Information
2#![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
17/// A trait which backend specific window handles need to implement
18pub trait Handle:
19    Serialize + DeserializeOwned + Debug + Clone + Copy + PartialEq + Eq + Default + Send + 'static
20{
21}
22
23/// A Backend-agnostic handle to a window used to identify it
24///
25/// # Serde
26///
27/// Using generics here with serde derive macros causes some wierd behaviour with the compiler, so
28/// as suggested by [this `serde` issue][serde-issue], just adding `#[serde(bound = "")]`
29/// everywhere the generic is declared fixes the bug.
30/// Hopefully this get fixed at some point so we can make this more pleasant to read...
31///
32/// [serde-issue]: https://github.com/serde-rs/serde/issues/1296
33#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
34pub struct WindowHandle<H>(#[serde(bound = "")] pub H)
35where
36    H: Handle;
37
38/// Handle for testing purposes
39pub type MockHandle = i32;
40impl Handle for MockHandle {}
41
42/// Store Window information.
43// We allow this as we're not managing state directly. This could be refactored in the future.
44// TODO: Refactor floating
45#[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    /// Per-window override for the creation-time pointer warp.
59    #[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    // Two strings that are within a XClassHint, kept separate for simpler comparing.
78    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    /// Set a per-window override for the creation-time pointer warp.
132    pub fn set_disable_mouse_grab(&mut self, value: Option<bool>) {
133        self.disable_mouse_grab = value;
134    }
135
136    /// Whether the new-window path may move the pointer into this window.
137    #[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            // NOTE: We float relative to the normal position.
148            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        // We are reparenting.
383        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}