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