1 | use std::cmp; |
2 | |
3 | use super::*; |
4 | |
5 | // Friendly neighborhood axis-aligned rectangle |
6 | #[derive (Debug, Clone, PartialEq, Eq)] |
7 | pub struct AaRect { |
8 | x: i64, |
9 | y: i64, |
10 | width: i64, |
11 | height: i64, |
12 | } |
13 | |
14 | impl AaRect { |
15 | pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self { |
16 | let (x, y) = (x as i64, y as i64); |
17 | let (width, height) = (width as i64, height as i64); |
18 | AaRect { |
19 | x, |
20 | y, |
21 | width, |
22 | height, |
23 | } |
24 | } |
25 | |
26 | pub fn contains_point(&self, x: i64, y: i64) -> bool { |
27 | x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height |
28 | } |
29 | |
30 | pub fn get_overlapping_area(&self, other: &Self) -> i64 { |
31 | let x_overlap = cmp::max( |
32 | 0, |
33 | cmp::min(self.x + self.width, other.x + other.width) - cmp::max(self.x, other.x), |
34 | ); |
35 | let y_overlap = cmp::max( |
36 | 0, |
37 | cmp::min(self.y + self.height, other.y + other.height) - cmp::max(self.y, other.y), |
38 | ); |
39 | x_overlap * y_overlap |
40 | } |
41 | } |
42 | |
43 | #[derive (Debug, Default)] |
44 | pub struct Geometry { |
45 | pub root: xproto::Window, |
46 | // If you want positions relative to the root window, use translate_coords. |
47 | // Note that the overwhelming majority of window managers are reparenting WMs, thus the window |
48 | // ID we get from window creation is for a nested window used as the window's client area. If |
49 | // you call get_geometry with that window ID, then you'll get the position of that client area |
50 | // window relative to the parent it's nested in (the frame), which isn't helpful if you want |
51 | // to know the frame position. |
52 | pub x_rel_parent: c_int, |
53 | pub y_rel_parent: c_int, |
54 | // In that same case, this will give you client area size. |
55 | pub width: c_uint, |
56 | pub height: c_uint, |
57 | // xmonad and dwm were the only WMs tested that use the border return at all. |
58 | // The majority of WMs seem to simply fill it with 0 unconditionally. |
59 | pub border: c_uint, |
60 | pub depth: c_uint, |
61 | } |
62 | |
63 | #[derive (Debug, Clone)] |
64 | pub struct FrameExtents { |
65 | pub left: u32, |
66 | pub right: u32, |
67 | pub top: u32, |
68 | pub bottom: u32, |
69 | } |
70 | |
71 | impl FrameExtents { |
72 | pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self { |
73 | FrameExtents { |
74 | left, |
75 | right, |
76 | top, |
77 | bottom, |
78 | } |
79 | } |
80 | |
81 | pub fn from_border(border: u32) -> Self { |
82 | Self::new(left:border, right:border, top:border, bottom:border) |
83 | } |
84 | } |
85 | |
86 | #[derive (Debug, Clone, PartialEq, Eq)] |
87 | pub enum FrameExtentsHeuristicPath { |
88 | Supported, |
89 | UnsupportedNested, |
90 | UnsupportedBordered, |
91 | } |
92 | |
93 | #[derive (Debug, Clone)] |
94 | pub struct FrameExtentsHeuristic { |
95 | pub frame_extents: FrameExtents, |
96 | pub heuristic_path: FrameExtentsHeuristicPath, |
97 | } |
98 | |
99 | impl FrameExtentsHeuristic { |
100 | pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { |
101 | use self::FrameExtentsHeuristicPath::*; |
102 | if self.heuristic_path != UnsupportedBordered { |
103 | ( |
104 | x - self.frame_extents.left as i32, |
105 | y - self.frame_extents.top as i32, |
106 | ) |
107 | } else { |
108 | (x, y) |
109 | } |
110 | } |
111 | |
112 | pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { |
113 | ( |
114 | width.saturating_add( |
115 | self.frame_extents |
116 | .left |
117 | .saturating_add(self.frame_extents.right) as _, |
118 | ), |
119 | height.saturating_add( |
120 | self.frame_extents |
121 | .top |
122 | .saturating_add(self.frame_extents.bottom) as _, |
123 | ), |
124 | ) |
125 | } |
126 | } |
127 | |
128 | impl XConnection { |
129 | // This is adequate for inner_position |
130 | pub fn translate_coords( |
131 | &self, |
132 | window: xproto::Window, |
133 | root: xproto::Window, |
134 | ) -> Result<xproto::TranslateCoordinatesReply, X11Error> { |
135 | self.xcb_connection() |
136 | .translate_coordinates(window, root, 0, 0)? |
137 | .reply() |
138 | .map_err(Into::into) |
139 | } |
140 | |
141 | // This is adequate for inner_size |
142 | pub fn get_geometry( |
143 | &self, |
144 | window: xproto::Window, |
145 | ) -> Result<xproto::GetGeometryReply, X11Error> { |
146 | self.xcb_connection() |
147 | .get_geometry(window)? |
148 | .reply() |
149 | .map_err(Into::into) |
150 | } |
151 | |
152 | fn get_frame_extents(&self, window: xproto::Window) -> Option<FrameExtents> { |
153 | let atoms = self.atoms(); |
154 | let extents_atom = atoms[_NET_FRAME_EXTENTS]; |
155 | |
156 | if !hint_is_supported(extents_atom) { |
157 | return None; |
158 | } |
159 | |
160 | // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't |
161 | // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to |
162 | // be unsupported by many smaller WMs. |
163 | let extents: Option<Vec<u32>> = self |
164 | .get_property( |
165 | window, |
166 | extents_atom, |
167 | xproto::Atom::from(xproto::AtomEnum::CARDINAL), |
168 | ) |
169 | .ok(); |
170 | |
171 | extents.and_then(|extents| { |
172 | if extents.len() >= 4 { |
173 | Some(FrameExtents { |
174 | left: extents[0], |
175 | right: extents[1], |
176 | top: extents[2], |
177 | bottom: extents[3], |
178 | }) |
179 | } else { |
180 | None |
181 | } |
182 | }) |
183 | } |
184 | |
185 | pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option<bool> { |
186 | let atoms = self.atoms(); |
187 | let client_list_atom = atoms[_NET_CLIENT_LIST]; |
188 | |
189 | if !hint_is_supported(client_list_atom) { |
190 | return None; |
191 | } |
192 | |
193 | let client_list: Option<Vec<xproto::Window>> = self |
194 | .get_property( |
195 | root, |
196 | client_list_atom, |
197 | xproto::Atom::from(xproto::AtomEnum::WINDOW), |
198 | ) |
199 | .ok(); |
200 | |
201 | client_list.map(|client_list| client_list.contains(&(window as xproto::Window))) |
202 | } |
203 | |
204 | fn get_parent_window(&self, window: xproto::Window) -> Result<xproto::Window, X11Error> { |
205 | let parent = self.xcb_connection().query_tree(window)?.reply()?.parent; |
206 | Ok(parent) |
207 | } |
208 | |
209 | fn climb_hierarchy( |
210 | &self, |
211 | window: xproto::Window, |
212 | root: xproto::Window, |
213 | ) -> Result<xproto::Window, X11Error> { |
214 | let mut outer_window = window; |
215 | loop { |
216 | let candidate = self.get_parent_window(outer_window)?; |
217 | if candidate == root { |
218 | break; |
219 | } |
220 | outer_window = candidate; |
221 | } |
222 | Ok(outer_window) |
223 | } |
224 | |
225 | pub fn get_frame_extents_heuristic( |
226 | &self, |
227 | window: xproto::Window, |
228 | root: xproto::Window, |
229 | ) -> FrameExtentsHeuristic { |
230 | use self::FrameExtentsHeuristicPath::*; |
231 | |
232 | // Position relative to root window. |
233 | // With rare exceptions, this is the position of a nested window. Cases where the window |
234 | // isn't nested are outlined in the comments throghout this function, but in addition to |
235 | // that, fullscreen windows often aren't nested. |
236 | let (inner_y_rel_root, child) = { |
237 | let coords = self |
238 | .translate_coords(window, root) |
239 | .expect("Failed to translate window coordinates" ); |
240 | (coords.dst_y, coords.child) |
241 | }; |
242 | |
243 | let (width, height, border) = { |
244 | let inner_geometry = self |
245 | .get_geometry(window) |
246 | .expect("Failed to get inner window geometry" ); |
247 | ( |
248 | inner_geometry.width, |
249 | inner_geometry.height, |
250 | inner_geometry.border_width, |
251 | ) |
252 | }; |
253 | |
254 | // The first condition is only false for un-nested windows, but isn't always false for |
255 | // un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy: |
256 | // when y is on the range [0, 2] and if the window has been unfocused since being |
257 | // undecorated (or was undecorated upon construction), the first condition is true, |
258 | // requiring us to rely on the second condition. |
259 | let nested = !(window == child || self.is_top_level(child, root) == Some(true)); |
260 | |
261 | // Hopefully the WM supports EWMH, allowing us to get exact info on the window frames. |
262 | if let Some(mut frame_extents) = self.get_frame_extents(window) { |
263 | // Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when |
264 | // decorations are disabled, but since the window becomes un-nested, it's easy to |
265 | // catch. |
266 | if !nested { |
267 | frame_extents = FrameExtents::new(0, 0, 0, 0); |
268 | } |
269 | |
270 | // The difference between the nested window's position and the outermost window's |
271 | // position is equivalent to the frame size. In most scenarios, this is equivalent to |
272 | // manually climbing the hierarchy as is done in the case below. Here's a list of |
273 | // known discrepancies: |
274 | // * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in |
275 | // addition to a 1px semi-transparent border. The margin can be easily observed by |
276 | // using a screenshot tool to get a screenshot of a selected window, and is |
277 | // presumably used for drawing drop shadows. Getting window geometry information |
278 | // via hierarchy-climbing results in this margin being included in both the |
279 | // position and outer size, so a window positioned at (0, 0) would be reported as |
280 | // having a position (-10, -8). |
281 | // * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px |
282 | // on all sides, and there's no additional border. |
283 | // * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. |
284 | // Without decorations, there's no difference. This is presumably related to |
285 | // Enlightenment's fairly unique concept of window position; it interprets |
286 | // positions given to XMoveWindow as a client area position rather than a position |
287 | // of the overall window. |
288 | |
289 | FrameExtentsHeuristic { |
290 | frame_extents, |
291 | heuristic_path: Supported, |
292 | } |
293 | } else if nested { |
294 | // If the position value we have is for a nested window used as the client area, we'll |
295 | // just climb up the hierarchy and get the geometry of the outermost window we're |
296 | // nested in. |
297 | let outer_window = self |
298 | .climb_hierarchy(window, root) |
299 | .expect("Failed to climb window hierarchy" ); |
300 | let (outer_y, outer_width, outer_height) = { |
301 | let outer_geometry = self |
302 | .get_geometry(outer_window) |
303 | .expect("Failed to get outer window geometry" ); |
304 | ( |
305 | outer_geometry.y, |
306 | outer_geometry.width, |
307 | outer_geometry.height, |
308 | ) |
309 | }; |
310 | |
311 | // Since we have the geometry of the outermost window and the geometry of the client |
312 | // area, we can figure out what's in between. |
313 | let diff_x = outer_width.saturating_sub(width) as u32; |
314 | let diff_y = outer_height.saturating_sub(height) as u32; |
315 | let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32; |
316 | |
317 | let left = diff_x / 2; |
318 | let right = left; |
319 | let top = offset_y; |
320 | let bottom = diff_y.saturating_sub(offset_y); |
321 | |
322 | let frame_extents = FrameExtents::new(left, right, top, bottom); |
323 | FrameExtentsHeuristic { |
324 | frame_extents, |
325 | heuristic_path: UnsupportedNested, |
326 | } |
327 | } else { |
328 | // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a |
329 | // border value. This is convenient, since we can use it to get an accurate frame. |
330 | let frame_extents = FrameExtents::from_border(border.into()); |
331 | FrameExtentsHeuristic { |
332 | frame_extents, |
333 | heuristic_path: UnsupportedBordered, |
334 | } |
335 | } |
336 | } |
337 | } |
338 | |