Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/window/x11.rs
1use anyhow::anyhow;
2use pathfinder_geometry::rect::RectF;
3use pathfinder_geometry::vector::vec2f;
4use winit::dpi::{PhysicalPosition, PhysicalSize};
5use x11rb::connection::Connection;
6use x11rb::protocol::randr::{self, MonitorInfo};
7use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt};
8use x11rb::rust_connection::RustConnection;
9 
10pub(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).
16struct 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.
30pub(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 
43impl 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 
226fn 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 
232pub(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`].
240fn rect_area(rect: RectF) -> f32 {
241 rect.width() * rect.height()
242}
243