Rust · 10077 bytes Raw Blame History
1 //! Monitor layout management
2 //!
3 //! Tracks monitor geometry and calculates adjacency relationships.
4
5 use std::collections::HashMap;
6
7 use hyprkvm_common::Direction;
8
9 use super::ipc::{HyprlandClient, HyprlandError, Monitor};
10
11 /// Manages the monitor layout
12 pub struct MonitorLayout {
13 monitors: HashMap<String, MonitorInfo>,
14 }
15
16 /// Monitor information with computed adjacency
17 #[derive(Debug, Clone)]
18 pub struct MonitorInfo {
19 pub name: String,
20 pub x: i32,
21 pub y: i32,
22 pub width: u32,
23 pub height: u32,
24 pub scale: f32,
25 pub active_workspace_id: i32,
26 pub active_workspace_name: String,
27 pub focused: bool,
28 pub adjacency: Adjacency,
29 }
30
31 /// Adjacent monitors in each direction
32 #[derive(Debug, Clone, Default)]
33 pub struct Adjacency {
34 pub left: Option<String>,
35 pub right: Option<String>,
36 pub up: Option<String>,
37 pub down: Option<String>,
38 }
39
40 impl MonitorLayout {
41 /// Build layout from current Hyprland state
42 pub async fn from_hyprland(client: &HyprlandClient) -> Result<Self, HyprlandError> {
43 let monitors = client.monitors().await?;
44 Ok(Self::from_monitors(&monitors))
45 }
46
47 /// Build layout from monitor list
48 pub fn from_monitors(monitors: &[Monitor]) -> Self {
49 let mut layout = Self {
50 monitors: HashMap::new(),
51 };
52
53 // First pass: add all monitors
54 for mon in monitors {
55 layout.monitors.insert(
56 mon.name.clone(),
57 MonitorInfo {
58 name: mon.name.clone(),
59 x: mon.x,
60 y: mon.y,
61 width: mon.width,
62 height: mon.height,
63 scale: mon.scale,
64 active_workspace_id: mon.active_workspace.id,
65 active_workspace_name: mon.active_workspace.name.clone(),
66 focused: mon.focused,
67 adjacency: Adjacency::default(),
68 },
69 );
70 }
71
72 // Second pass: compute adjacency
73 let names: Vec<String> = layout.monitors.keys().cloned().collect();
74 for name in &names {
75 let adjacency = layout.compute_adjacency(name);
76 if let Some(mon) = layout.monitors.get_mut(name) {
77 mon.adjacency = adjacency;
78 }
79 }
80
81 layout
82 }
83
84 /// Compute adjacency for a monitor
85 fn compute_adjacency(&self, name: &str) -> Adjacency {
86 let Some(current) = self.monitors.get(name) else {
87 return Adjacency::default();
88 };
89
90 let mut adjacency = Adjacency::default();
91
92 for (other_name, other) in &self.monitors {
93 if other_name == name {
94 continue;
95 }
96
97 // Check if monitors are vertically aligned (overlap in Y axis)
98 let v_overlap = current.y < other.y + other.height as i32
99 && current.y + current.height as i32 > other.y;
100
101 // Check if monitors are horizontally aligned (overlap in X axis)
102 let h_overlap = current.x < other.x + other.width as i32
103 && current.x + current.width as i32 > other.x;
104
105 // Left neighbor: other is to the left and vertically aligned
106 if v_overlap && other.x + other.width as i32 <= current.x {
107 if adjacency.left.is_none()
108 || self.is_closer_left(current, other, &adjacency.left)
109 {
110 adjacency.left = Some(other_name.clone());
111 }
112 }
113
114 // Right neighbor: other is to the right and vertically aligned
115 if v_overlap && other.x >= current.x + current.width as i32 {
116 if adjacency.right.is_none()
117 || self.is_closer_right(current, other, &adjacency.right)
118 {
119 adjacency.right = Some(other_name.clone());
120 }
121 }
122
123 // Up neighbor: other is above and horizontally aligned
124 if h_overlap && other.y + other.height as i32 <= current.y {
125 if adjacency.up.is_none()
126 || self.is_closer_up(current, other, &adjacency.up)
127 {
128 adjacency.up = Some(other_name.clone());
129 }
130 }
131
132 // Down neighbor: other is below and horizontally aligned
133 if h_overlap && other.y >= current.y + current.height as i32 {
134 if adjacency.down.is_none()
135 || self.is_closer_down(current, other, &adjacency.down)
136 {
137 adjacency.down = Some(other_name.clone());
138 }
139 }
140 }
141
142 adjacency
143 }
144
145 // Helper functions to find closest neighbor
146 fn is_closer_left(&self, _current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
147 let Some(existing_name) = existing else { return true };
148 let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
149 other.x + other.width as i32 > existing_mon.x + existing_mon.width as i32
150 }
151
152 fn is_closer_right(&self, _current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
153 let Some(existing_name) = existing else { return true };
154 let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
155 other.x < existing_mon.x
156 }
157
158 fn is_closer_up(&self, _current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
159 let Some(existing_name) = existing else { return true };
160 let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
161 other.y + other.height as i32 > existing_mon.y + existing_mon.height as i32
162 }
163
164 fn is_closer_down(&self, _current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
165 let Some(existing_name) = existing else { return true };
166 let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
167 other.y < existing_mon.y
168 }
169
170 /// Get the focused monitor
171 pub fn focused(&self) -> Option<&MonitorInfo> {
172 self.monitors.values().find(|m| m.focused)
173 }
174
175 /// Get a monitor by name
176 pub fn get(&self, name: &str) -> Option<&MonitorInfo> {
177 self.monitors.get(name)
178 }
179
180 /// Get monitor in given direction from focused monitor
181 pub fn neighbor(&self, direction: Direction) -> Option<&MonitorInfo> {
182 let focused = self.focused()?;
183 let neighbor_name = match direction {
184 Direction::Left => focused.adjacency.left.as_ref()?,
185 Direction::Right => focused.adjacency.right.as_ref()?,
186 Direction::Up => focused.adjacency.up.as_ref()?,
187 Direction::Down => focused.adjacency.down.as_ref()?,
188 };
189 self.monitors.get(neighbor_name)
190 }
191
192 /// Check if we're at an edge (no local monitor in that direction)
193 pub fn is_at_edge(&self, direction: Direction) -> bool {
194 self.neighbor(direction).is_none()
195 }
196
197 /// Get screen bounds (bounding box of all monitors)
198 pub fn bounds(&self) -> (i32, i32, i32, i32) {
199 let mut min_x = i32::MAX;
200 let mut min_y = i32::MAX;
201 let mut max_x = i32::MIN;
202 let mut max_y = i32::MIN;
203
204 for mon in self.monitors.values() {
205 min_x = min_x.min(mon.x);
206 min_y = min_y.min(mon.y);
207 max_x = max_x.max(mon.x + mon.width as i32);
208 max_y = max_y.max(mon.y + mon.height as i32);
209 }
210
211 (min_x, min_y, max_x, max_y)
212 }
213
214 /// Check if a cursor position is at a screen edge
215 pub fn cursor_at_edge(&self, x: i32, y: i32, threshold: i32) -> Option<Direction> {
216 let (min_x, min_y, max_x, max_y) = self.bounds();
217
218 if x <= min_x + threshold {
219 Some(Direction::Left)
220 } else if x >= max_x - threshold {
221 Some(Direction::Right)
222 } else if y <= min_y + threshold {
223 Some(Direction::Up)
224 } else if y >= max_y - threshold {
225 Some(Direction::Down)
226 } else {
227 None
228 }
229 }
230
231 /// Get all monitor names
232 pub fn monitor_names(&self) -> impl Iterator<Item = &String> {
233 self.monitors.keys()
234 }
235
236 /// Get number of monitors
237 pub fn len(&self) -> usize {
238 self.monitors.len()
239 }
240
241 /// Check if empty
242 pub fn is_empty(&self) -> bool {
243 self.monitors.is_empty()
244 }
245 }
246
247 #[cfg(test)]
248 mod tests {
249 use super::*;
250 use crate::hyprland::ipc::WorkspaceRef;
251
252 fn make_monitor(name: &str, x: i32, y: i32, width: u32, height: u32, focused: bool) -> Monitor {
253 Monitor {
254 id: 0,
255 name: name.to_string(),
256 description: String::new(),
257 x,
258 y,
259 width,
260 height,
261 scale: 1.0,
262 active_workspace: WorkspaceRef {
263 id: 1,
264 name: "1".to_string(),
265 },
266 focused,
267 }
268 }
269
270 #[test]
271 fn test_horizontal_layout() {
272 let monitors = vec![
273 make_monitor("DP-1", 0, 0, 1920, 1080, true),
274 make_monitor("DP-2", 1920, 0, 1920, 1080, false),
275 ];
276
277 let layout = MonitorLayout::from_monitors(&monitors);
278
279 let dp1 = layout.get("DP-1").unwrap();
280 assert!(dp1.adjacency.left.is_none());
281 assert_eq!(dp1.adjacency.right, Some("DP-2".to_string()));
282
283 let dp2 = layout.get("DP-2").unwrap();
284 assert_eq!(dp2.adjacency.left, Some("DP-1".to_string()));
285 assert!(dp2.adjacency.right.is_none());
286 }
287
288 #[test]
289 fn test_edge_detection() {
290 let monitors = vec![
291 make_monitor("DP-1", 0, 0, 1920, 1080, true),
292 ];
293
294 let layout = MonitorLayout::from_monitors(&monitors);
295
296 assert!(layout.is_at_edge(Direction::Left));
297 assert!(layout.is_at_edge(Direction::Right));
298 assert!(layout.is_at_edge(Direction::Up));
299 assert!(layout.is_at_edge(Direction::Down));
300 }
301 }
302