1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
use std::cmp;

use super::*;

// Friendly neighborhood axis-aligned rectangle
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AaRect {
    x: i64,
    y: i64,
    width: i64,
    height: i64,
}

impl AaRect {
    pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self {
        let (x, y) = (x as i64, y as i64);
        let (width, height) = (width as i64, height as i64);
        AaRect { x, y, width, height }
    }

    pub fn contains_point(&self, x: i64, y: i64) -> bool {
        x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
    }

    pub fn get_overlapping_area(&self, other: &Self) -> i64 {
        let x_overlap = cmp::max(
            0,
            cmp::min(self.x + self.width, other.x + other.width) - cmp::max(self.x, other.x),
        );
        let y_overlap = cmp::max(
            0,
            cmp::min(self.y + self.height, other.y + other.height) - cmp::max(self.y, other.y),
        );
        x_overlap * y_overlap
    }
}

#[derive(Debug, Clone)]
pub struct FrameExtents {
    pub left: u32,
    pub right: u32,
    pub top: u32,
    pub bottom: u32,
}

impl FrameExtents {
    pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self {
        FrameExtents { left, right, top, bottom }
    }

    pub fn from_border(border: u32) -> Self {
        Self::new(border, border, border, border)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameExtentsHeuristicPath {
    Supported,
    UnsupportedNested,
    UnsupportedBordered,
}

#[derive(Debug, Clone)]
pub struct FrameExtentsHeuristic {
    pub frame_extents: FrameExtents,
    pub heuristic_path: FrameExtentsHeuristicPath,
}

impl FrameExtentsHeuristic {
    pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) {
        use self::FrameExtentsHeuristicPath::*;
        if self.heuristic_path != UnsupportedBordered {
            (x - self.frame_extents.left as i32, y - self.frame_extents.top as i32)
        } else {
            (x, y)
        }
    }

    pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) {
        (
            width.saturating_add(
                self.frame_extents.left.saturating_add(self.frame_extents.right) as _
            ),
            height.saturating_add(
                self.frame_extents.top.saturating_add(self.frame_extents.bottom) as _
            ),
        )
    }
}

impl XConnection {
    // This is adequate for inner_position
    pub fn translate_coords(
        &self,
        window: xproto::Window,
        root: xproto::Window,
    ) -> Result<xproto::TranslateCoordinatesReply, X11Error> {
        self.xcb_connection().translate_coordinates(window, root, 0, 0)?.reply().map_err(Into::into)
    }

    // This is adequate for inner_size
    pub fn get_geometry(
        &self,
        window: xproto::Window,
    ) -> Result<xproto::GetGeometryReply, X11Error> {
        self.xcb_connection().get_geometry(window)?.reply().map_err(Into::into)
    }

    fn get_frame_extents(&self, window: xproto::Window) -> Option<FrameExtents> {
        let atoms = self.atoms();
        let extents_atom = atoms[_NET_FRAME_EXTENTS];

        if !hint_is_supported(extents_atom) {
            return None;
        }

        // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't
        // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to
        // be unsupported by many smaller WMs.
        let extents: Option<Vec<u32>> = self
            .get_property(window, extents_atom, xproto::Atom::from(xproto::AtomEnum::CARDINAL))
            .ok();

        extents.and_then(|extents| {
            if extents.len() >= 4 {
                Some(FrameExtents {
                    left: extents[0],
                    right: extents[1],
                    top: extents[2],
                    bottom: extents[3],
                })
            } else {
                None
            }
        })
    }

    pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option<bool> {
        let atoms = self.atoms();
        let client_list_atom = atoms[_NET_CLIENT_LIST];

        if !hint_is_supported(client_list_atom) {
            return None;
        }

        let client_list: Option<Vec<xproto::Window>> = self
            .get_property(root, client_list_atom, xproto::Atom::from(xproto::AtomEnum::WINDOW))
            .ok();

        client_list.map(|client_list| client_list.contains(&(window as xproto::Window)))
    }

    fn get_parent_window(&self, window: xproto::Window) -> Result<xproto::Window, X11Error> {
        let parent = self.xcb_connection().query_tree(window)?.reply()?.parent;
        Ok(parent)
    }

    fn climb_hierarchy(
        &self,
        window: xproto::Window,
        root: xproto::Window,
    ) -> Result<xproto::Window, X11Error> {
        let mut outer_window = window;
        loop {
            let candidate = self.get_parent_window(outer_window)?;
            if candidate == root {
                break;
            }
            outer_window = candidate;
        }
        Ok(outer_window)
    }

    pub fn get_frame_extents_heuristic(
        &self,
        window: xproto::Window,
        root: xproto::Window,
    ) -> FrameExtentsHeuristic {
        use self::FrameExtentsHeuristicPath::*;

        // Position relative to root window.
        // With rare exceptions, this is the position of a nested window. Cases where the window
        // isn't nested are outlined in the comments throughout this function, but in addition to
        // that, fullscreen windows often aren't nested.
        let (inner_y_rel_root, child) = {
            let coords = self
                .translate_coords(window, root)
                .expect("Failed to translate window coordinates");
            (coords.dst_y, coords.child)
        };

        let (width, height, border) = {
            let inner_geometry =
                self.get_geometry(window).expect("Failed to get inner window geometry");
            (inner_geometry.width, inner_geometry.height, inner_geometry.border_width)
        };

        // The first condition is only false for un-nested windows, but isn't always false for
        // un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy:
        // when y is on the range [0, 2] and if the window has been unfocused since being
        // undecorated (or was undecorated upon construction), the first condition is true,
        // requiring us to rely on the second condition.
        let nested = !(window == child || self.is_top_level(child, root) == Some(true));

        // Hopefully the WM supports EWMH, allowing us to get exact info on the window frames.
        if let Some(mut frame_extents) = self.get_frame_extents(window) {
            // Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when
            // decorations are disabled, but since the window becomes un-nested, it's easy to
            // catch.
            if !nested {
                frame_extents = FrameExtents::new(0, 0, 0, 0);
            }

            // The difference between the nested window's position and the outermost window's
            // position is equivalent to the frame size. In most scenarios, this is equivalent to
            // manually climbing the hierarchy as is done in the case below. Here's a list of
            // known discrepancies:
            // * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in
            //   addition to a 1px semi-transparent border. The margin can be easily observed by
            //   using a screenshot tool to get a screenshot of a selected window, and is presumably
            //   used for drawing drop shadows. Getting window geometry information via
            //   hierarchy-climbing results in this margin being included in both the position and
            //   outer size, so a window positioned at (0, 0) would be reported as having a position
            //   (-10, -8).
            // * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px on
            //   all sides, and there's no additional border.
            // * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. Without
            //   decorations, there's no difference. This is presumably related to Enlightenment's
            //   fairly unique concept of window position; it interprets positions given to
            //   XMoveWindow as a client area position rather than a position of the overall window.

            FrameExtentsHeuristic { frame_extents, heuristic_path: Supported }
        } else if nested {
            // If the position value we have is for a nested window used as the client area, we'll
            // just climb up the hierarchy and get the geometry of the outermost window we're
            // nested in.
            let outer_window =
                self.climb_hierarchy(window, root).expect("Failed to climb window hierarchy");
            let (outer_y, outer_width, outer_height) = {
                let outer_geometry =
                    self.get_geometry(outer_window).expect("Failed to get outer window geometry");
                (outer_geometry.y, outer_geometry.width, outer_geometry.height)
            };

            // Since we have the geometry of the outermost window and the geometry of the client
            // area, we can figure out what's in between.
            let diff_x = outer_width.saturating_sub(width) as u32;
            let diff_y = outer_height.saturating_sub(height) as u32;
            let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32;

            let left = diff_x / 2;
            let right = left;
            let top = offset_y;
            let bottom = diff_y.saturating_sub(offset_y);

            let frame_extents = FrameExtents::new(left, right, top, bottom);
            FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested }
        } else {
            // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a
            // border value. This is convenient, since we can use it to get an accurate frame.
            let frame_extents = FrameExtents::from_border(border.into());
            FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedBordered }
        }
    }
}