StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use anyhow::anyhow; |
| 2 | use pathfinder_geometry::rect::RectF; |
| 3 | use pathfinder_geometry::vector::vec2f; |
| 4 | use winit::dpi::{PhysicalPosition, PhysicalSize}; |
| 5 | use x11rb::connection::Connection; |
| 6 | use x11rb::protocol::randr::{self, MonitorInfo}; |
| 7 | use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt}; |
| 8 | use x11rb::rust_connection::RustConnection; |
| 9 | |
| 10 | pub(super) type PhysicalMonitorBounds = (PhysicalPosition<i16>, PhysicalSize<u16>); |
| 11 | |
| 12 | /// Holds a mapping of field names to "atoms" in X11. |
| 13 | /// |
| 14 | /// In X11, "atoms" are basically enums. They are integers that map to strings, primarily to save |
| 15 | /// network bandwidth (X11 does not assume the GUI and the server are running on the same host). |
| 16 | struct Atoms { |
| 17 | /// For specifying the `UTF8_STRING` type. Confusingly, this is different from |
| 18 | /// [`AtomEnum::STRING`]. |
| 19 | utf8_string: u32, |
| 20 | /// For the `_NET_ACTIVE_WINDOW` property on the root window. |
| 21 | net_active_window: u32, |
| 22 | /// For targeting `_NET_SUPPORTING_WM_CHECK` window. |
| 23 | net_supporting_wm_check: u32, |
| 24 | /// For the `_NET_WM_NAME` property. |
| 25 | net_wm_name: u32, |
| 26 | } |
| 27 | |
| 28 | /// An X11 client so that we can talk to an Xorg server for more advanced functionality from a |
| 29 | /// desktop environment. |
| 30 | pub(super) struct X11Manager { |
| 31 | conn: RustConnection, |
| 32 | /// The index among a list of available screens which we are displaying on. |
| 33 | /// |
| 34 | /// A "screen" in X11 parlance is not the concept of a monitor as we typically consider it. |
| 35 | /// Rather, if there are multiple monitors plugged in, they get pooled into a single, shared |
| 36 | /// coordinate space called a "screen". This allows windows to span multiple displays, as X11 |
| 37 | /// does not assume that any window belongs to one monitor. |
| 38 | /// https://docs.google.com/drawings/d/1XeYRd9I7liQMj9w17QQZoeHSNYBJ_U0wEQh-pS_4eKM |
| 39 | screen_index: usize, |
| 40 | atoms: Atoms, |
| 41 | } |
| 42 | |
| 43 | impl X11Manager { |
| 44 | pub(super) fn new() -> anyhow::Result<Self> { |
| 45 | let (conn, screen_index) = RustConnection::connect(None)?; |
| 46 | |
| 47 | let utf8_string = conn.intern_atom(true, b"UTF8_STRING")?.reply()?.atom; |
| 48 | let net_active_window = conn |
| 49 | .intern_atom(false, b"_NET_ACTIVE_WINDOW")? |
| 50 | .reply()? |
| 51 | .atom; |
| 52 | let net_supporting_wm_check = conn |
| 53 | .intern_atom(true, b"_NET_SUPPORTING_WM_CHECK")? |
| 54 | .reply()? |
| 55 | .atom; |
| 56 | let net_wm_name = conn.intern_atom(true, b"_NET_WM_NAME")?.reply()?.atom; |
| 57 | |
| 58 | Ok(Self { |
| 59 | conn, |
| 60 | screen_index, |
| 61 | atoms: Atoms { |
| 62 | net_active_window, |
| 63 | net_supporting_wm_check, |
| 64 | net_wm_name, |
| 65 | utf8_string, |
| 66 | }, |
| 67 | }) |
| 68 | } |
| 69 | |
| 70 | /// Determines the index among a list of monitors for the "active" monitor. It also returns |
| 71 | /// metadata for that active monitor. |
| 72 | /// |
| 73 | /// "Active" here means the monitor which the focused window is on. This may not be a window of |
| 74 | /// your application, but another app's window. Note that windows may span multiple monitors. |
| 75 | /// In that case, we pick the monitor which has the most overlap with the focused window. |
| 76 | pub(super) fn get_active_monitor(&self) -> anyhow::Result<(usize, PhysicalMonitorBounds)> { |
| 77 | // This logic is ported from `xdotool` |
| 78 | // https://github.com/jordansissel/xdotool/blob/7e02cef5d9216bd0ce69b44f62217b587cc7c31e/xdo.c#L208 |
| 79 | let active_window_id = self.get_active_window()?; |
| 80 | |
| 81 | // This determines if the active window is the child of another window, or a child of the |
| 82 | // "root". Indeed, windows in X11 are hierarchical. |
| 83 | let tree_reply = xproto::query_tree(&self.conn, active_window_id)?.reply()?; |
| 84 | |
| 85 | // The meaning of "get_geometry" depends on this window's position in the hierarchy. This |
| 86 | // call gives us the "true" position only if the window is a child of the "root". If not, |
| 87 | // it gives us an offset position from its parent window. |
| 88 | // https://tronche.com/gui/x/xlib/window-information/XGetGeometry.html |
| 89 | let active_window_geometry = xproto::get_geometry(&self.conn, active_window_id)?.reply()?; |
| 90 | |
| 91 | // If this window is a child of the "root", return the reported position. |
| 92 | let absolute_window_origin = if tree_reply.parent == tree_reply.root { |
| 93 | vec2f( |
| 94 | active_window_geometry.x as f32, |
| 95 | active_window_geometry.y as f32, |
| 96 | ) |
| 97 | } else { |
| 98 | // Otherwise, "flatten" or "translate" the coordinates to be relative to the root. |
| 99 | // https://tronche.com/gui/x/xlib/window-information/XTranslateCoordinates.html |
| 100 | let translate_reply = |
| 101 | xproto::translate_coordinates(&self.conn, active_window_id, tree_reply.root, 0, 0)? |
| 102 | .reply()?; |
| 103 | vec2f(translate_reply.dst_x as f32, translate_reply.dst_y as f32) |
| 104 | }; |
| 105 | |
| 106 | let active_window_bounds = RectF::new( |
| 107 | absolute_window_origin, |
| 108 | vec2f( |
| 109 | active_window_geometry.width as f32, |
| 110 | active_window_geometry.height as f32, |
| 111 | ), |
| 112 | ); |
| 113 | |
| 114 | // Get the full list of monitors and calculate which one overlaps with the active window |
| 115 | // the most. |
| 116 | let monitors = self.get_monitors(active_window_id)?; |
| 117 | let (i, monitor_bounds) = monitors |
| 118 | .iter() |
| 119 | .map(monitor_info_to_physical_bounds) |
| 120 | .enumerate() |
| 121 | .max_by(|(_, bounds_a), (_, bounds_b)| { |
| 122 | let intersection_a = active_window_bounds |
| 123 | .intersection(physical_bounds_to_rect(bounds_a, 1.)) |
| 124 | .unwrap_or_default(); |
| 125 | let intersection_b = active_window_bounds |
| 126 | .intersection(physical_bounds_to_rect(bounds_b, 1.)) |
| 127 | .unwrap_or_default(); |
| 128 | rect_area(intersection_a).total_cmp(&rect_area(intersection_b)) |
| 129 | }) |
| 130 | .ok_or(anyhow!( |
| 131 | "active window position doesn't fall on any windows" |
| 132 | ))?; |
| 133 | |
| 134 | Ok((i, monitor_bounds)) |
| 135 | } |
| 136 | |
| 137 | pub(super) fn list_monitor_bounds(&self) -> anyhow::Result<Box<[PhysicalMonitorBounds]>> { |
| 138 | let active_window_id = self.get_active_window()?; |
| 139 | let mut monitors = self.get_monitors(active_window_id)?; |
| 140 | // Ensure the primary display is first. This is not |
| 141 | monitors.sort_by(|a, b| b.primary.cmp(&a.primary)); |
| 142 | Ok(monitors |
| 143 | .iter() |
| 144 | .map(monitor_info_to_physical_bounds) |
| 145 | .collect()) |
| 146 | } |
| 147 | |
| 148 | fn get_monitors(&self, window: xproto::Window) -> anyhow::Result<Vec<MonitorInfo>> { |
| 149 | // For most X11 calls, we reuse `self.conn` for the request. However, the response for |
| 150 | // `get_monitors` gets cached for the client. Subsequest calls just read the cached value, |
| 151 | // which doesn't seem to ever get invalidated. To ensure we read a fresh value, we |
| 152 | // construct a fresh connection client for every request. |
| 153 | let (conn, _) = RustConnection::connect(None)?; |
| 154 | let monitors = randr::get_monitors(&conn, window, false)?.reply()?.monitors; |
| 155 | Ok(monitors) |
| 156 | } |
| 157 | |
| 158 | pub(super) fn os_window_manager_name(&self) -> anyhow::Result<String> { |
| 159 | let wm_check = xproto::get_property( |
| 160 | &self.conn, |
| 161 | false, |
| 162 | self.screen().root, |
| 163 | self.atoms.net_supporting_wm_check, |
| 164 | AtomEnum::WINDOW, |
| 165 | 0, |
| 166 | 1024, |
| 167 | )? |
| 168 | .reply()? |
| 169 | .value32() |
| 170 | .ok_or(anyhow!( |
| 171 | "Error getting _NET_SUPPORTING_WM_CHECK. Invalid response format." |
| 172 | ))? |
| 173 | // X protocol responses are always iterators, even if the response is a single value. |
| 174 | .next() |
| 175 | .ok_or(anyhow!( |
| 176 | "Error getting _NET_SUPPORTING_WM_CHECK. Received empty response." |
| 177 | ))?; |
| 178 | |
| 179 | let wm_name_prop = xproto::get_property( |
| 180 | &self.conn, |
| 181 | false, |
| 182 | wm_check, |
| 183 | self.atoms.net_wm_name, |
| 184 | self.atoms.utf8_string, |
| 185 | 0, |
| 186 | 1024, |
| 187 | )? |
| 188 | .reply()?; |
| 189 | |
| 190 | let wm_name = String::from_utf8(wm_name_prop.value)?; |
| 191 | Ok(wm_name) |
| 192 | } |
| 193 | |
| 194 | fn screen(&self) -> &xproto::Screen { |
| 195 | &self.conn.setup().roots[self.screen_index] |
| 196 | } |
| 197 | |
| 198 | /// Returns X11's window ID for the active window. |
| 199 | /// |
| 200 | /// The "active" window is the one which has keyboard focus. |
| 201 | fn get_active_window(&self) -> anyhow::Result<xproto::Window> { |
| 202 | let active_window_reply = xproto::get_property( |
| 203 | &self.conn, |
| 204 | false, |
| 205 | self.screen().root, |
| 206 | self.atoms.net_active_window, |
| 207 | AtomEnum::WINDOW, |
| 208 | 0, |
| 209 | 1024, |
| 210 | )? |
| 211 | .reply()?; |
| 212 | |
| 213 | let active_window = active_window_reply |
| 214 | .value32() |
| 215 | .ok_or(anyhow!( |
| 216 | "Error getting active window. Invalid response format." |
| 217 | ))? |
| 218 | .next(); |
| 219 | |
| 220 | active_window.ok_or(anyhow!( |
| 221 | "Error getting active window. Received empty response." |
| 222 | )) |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | fn monitor_info_to_physical_bounds(monitor: &MonitorInfo) -> PhysicalMonitorBounds { |
| 227 | let origin = PhysicalPosition::new(monitor.x, monitor.y); |
| 228 | let size = PhysicalSize::new(monitor.width, monitor.height); |
| 229 | (origin, size) |
| 230 | } |
| 231 | |
| 232 | pub(super) fn physical_bounds_to_rect(bounds: &PhysicalMonitorBounds, scale_factor: f32) -> RectF { |
| 233 | let (origin, size) = bounds; |
| 234 | let origin = vec2f(origin.x as f32, origin.y as f32) / scale_factor; |
| 235 | let size = vec2f(size.width as f32, size.height as f32) / scale_factor; |
| 236 | RectF::new(origin, size) |
| 237 | } |
| 238 | |
| 239 | /// Computes the area of a [`RectF`]. |
| 240 | fn rect_area(rect: RectF) -> f32 { |
| 241 | rect.width() * rect.height() |
| 242 | } |
| 243 |