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::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
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    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    // Two strings that are within a XClassHint, kept separate for simpler comparing.
75    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            // NOTE: We float relative to the normal position.
129            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        // We are reparenting.
361        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}