tenseleyflow/hyprkvm / b32a6b7

Browse files

fix: use logical coordinates for multi-monitor edge detection

- Add scale field to input::MonitorInfo to use actual Hyprland scale
- Fix output-to-monitor matching to use real scale instead of guessing
- Calculate screen bounds in logical coordinates (cursor_pos uses logical)
- Add cursor position verification using Hyprland IPC

This fixes edge barriers not triggering on scaled monitors where the
physical vs logical coordinate mismatch caused the edge check to fail.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b32a6b70f19da62408b8d7b9cd8ed4d837cbdfbc
Parents
2b474dd
Tree
697c246

2 changed files

StatusFile+-
M hyprkvm-daemon/src/input/capture.rs 27 40
M hyprkvm-daemon/src/main.rs 45 11
hyprkvm-daemon/src/input/capture.rsmodified
@@ -49,6 +49,7 @@ pub struct MonitorInfo {
49
     pub y: i32,
49
     pub y: i32,
50
     pub width: u32,
50
     pub width: u32,
51
     pub height: u32,
51
     pub height: u32,
52
+    pub scale: f32,
52
 }
53
 }
53
 
54
 
54
 /// Configuration for edge barriers
55
 /// Configuration for edge barriers
@@ -130,27 +131,28 @@ impl EdgeCaptureState {
130
         let mut used_monitors: Vec<bool> = vec![false; self.config.monitors.len()];
131
         let mut used_monitors: Vec<bool> = vec![false; self.config.monitors.len()];
131
         let mut assigned_outputs: Vec<bool> = vec![false; self.outputs.len()];
132
         let mut assigned_outputs: Vec<bool> = vec![false; self.outputs.len()];
132
 
133
 
134
+        // Helper to check if a monitor's logical size matches an output
135
+        let sizes_match = |mon: &MonitorInfo, out_w: u32, out_h: u32| -> bool {
136
+            // Use the monitor's actual scale from Hyprland
137
+            let scale = mon.scale as f64;
138
+            let logical_w = (mon.width as f64 / scale).round() as u32;
139
+            let logical_h = (mon.height as f64 / scale).round() as u32;
140
+
141
+            // Allow 1 pixel tolerance for rounding differences
142
+            let w_match = (logical_w as i32 - out_w as i32).abs() <= 1;
143
+            let h_match = (logical_h as i32 - out_h as i32).abs() <= 1;
144
+            w_match && h_match
145
+        };
146
+
133
         // First pass: try to find unique matches
147
         // First pass: try to find unique matches
134
         for (out_idx, out) in self.outputs.iter_mut().enumerate() {
148
         for (out_idx, out) in self.outputs.iter_mut().enumerate() {
135
-            // Find monitors that could match this output size (considering possible scales)
136
             let mut candidates: Vec<usize> = Vec::new();
149
             let mut candidates: Vec<usize> = Vec::new();
137
             for (i, mon) in self.config.monitors.iter().enumerate() {
150
             for (i, mon) in self.config.monitors.iter().enumerate() {
138
                 if used_monitors[i] {
151
                 if used_monitors[i] {
139
                     continue;
152
                     continue;
140
                 }
153
                 }
141
-                // Check if sizes match directly
154
+                if sizes_match(mon, out.width, out.height) {
142
-                if mon.width == out.width && mon.height == out.height {
143
                     candidates.push(i);
155
                     candidates.push(i);
144
-                    continue;
145
-                }
146
-                // Check common scale factors (1.5, 2.0)
147
-                for scale in [1.5_f64, 2.0_f64] {
148
-                    let scaled_w = (mon.width as f64 / scale).round() as u32;
149
-                    let scaled_h = (mon.height as f64 / scale).round() as u32;
150
-                    if scaled_w == out.width && scaled_h == out.height {
151
-                        candidates.push(i);
152
-                        break;
153
-                    }
154
                 }
156
                 }
155
             }
157
             }
156
 
158
 
@@ -158,8 +160,8 @@ impl EdgeCaptureState {
158
             if candidates.len() == 1 {
160
             if candidates.len() == 1 {
159
                 let i = candidates[0];
161
                 let i = candidates[0];
160
                 let mon = &self.config.monitors[i];
162
                 let mon = &self.config.monitors[i];
161
-                tracing::info!("Matched output {}x{} to monitor {} at ({}, {})",
163
+                tracing::info!("Matched output {}x{} to monitor {} at ({}, {}) scale={}",
162
-                    out.width, out.height, mon.name, mon.x, mon.y);
164
+                    out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
163
                 out.x = mon.x;
165
                 out.x = mon.x;
164
                 out.y = mon.y;
166
                 out.y = mon.y;
165
                 used_monitors[i] = true;
167
                 used_monitors[i] = true;
@@ -176,21 +178,14 @@ impl EdgeCaptureState {
176
             if assigned_outputs[out_idx] {
178
             if assigned_outputs[out_idx] {
177
                 continue; // Already assigned in first pass
179
                 continue; // Already assigned in first pass
178
             }
180
             }
179
-            // Find first unused monitor that could match
181
+            // Find first unused monitor that matches
180
             for (i, mon) in self.config.monitors.iter().enumerate() {
182
             for (i, mon) in self.config.monitors.iter().enumerate() {
181
                 if used_monitors[i] {
183
                 if used_monitors[i] {
182
                     continue;
184
                     continue;
183
                 }
185
                 }
184
-                // Check all possible scales
186
+                if sizes_match(mon, out.width, out.height) {
185
-                let matches = (mon.width == out.width && mon.height == out.height)
187
+                    tracing::info!("Assigned remaining output {}x{} to monitor {} at ({}, {}) scale={}",
186
-                    || [1.5_f64, 2.0_f64].iter().any(|&scale| {
188
+                        out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
187
-                        let scaled_w = (mon.width as f64 / scale).round() as u32;
188
-                        let scaled_h = (mon.height as f64 / scale).round() as u32;
189
-                        scaled_w == out.width && scaled_h == out.height
190
-                    });
191
-                if matches {
192
-                    tracing::info!("Assigned remaining output {}x{} to monitor {} at ({}, {})",
193
-                        out.width, out.height, mon.name, mon.x, mon.y);
194
                     out.x = mon.x;
189
                     out.x = mon.x;
195
                     out.y = mon.y;
190
                     out.y = mon.y;
196
                     used_monitors[i] = true;
191
                     used_monitors[i] = true;
@@ -615,26 +610,18 @@ impl PointerHandler for EdgeCaptureState {
615
                             if should_trigger {
610
                             if should_trigger {
616
                                 self.last_trigger_time.insert(barrier.direction, now);
611
                                 self.last_trigger_time.insert(barrier.direction, now);
617
 
612
 
618
-                                // Calculate screen position
613
+                                // Send edge event with direction only
619
-                                let (min_x, min_y, max_x, max_y) = self.screen_bounds();
614
+                                // Main daemon will query Hyprland for actual cursor position
620
-                                let screen_pos = match barrier.direction {
615
+                                // and verify we're at screen boundary before triggering transfer
621
-                                    Direction::Left => (min_x, min_y + y as i32),
622
-                                    Direction::Right => (max_x - 1, min_y + y as i32),
623
-                                    Direction::Up => (min_x + x as i32, min_y),
624
-                                    Direction::Down => (min_x + x as i32, max_y - 1),
625
-                                };
626
-
627
                                 let edge_event = EdgeEvent {
616
                                 let edge_event = EdgeEvent {
628
                                     direction: barrier.direction,
617
                                     direction: barrier.direction,
629
-                                    position: screen_pos,
618
+                                    position: (0, 0), // Placeholder - main daemon uses Hyprland cursor pos
630
                                     timestamp: now,
619
                                     timestamp: now,
631
                                 };
620
                                 };
632
 
621
 
633
                                 tracing::info!(
622
                                 tracing::info!(
634
-                                    "Edge trigger: {:?} at screen ({}, {})",
623
+                                    "Edge event: {:?} barrier triggered",
635
-                                    barrier.direction,
624
+                                    barrier.direction
636
-                                    screen_pos.0,
637
-                                    screen_pos.1
638
                                 );
625
                                 );
639
 
626
 
640
                                 let _ = self.event_tx.send(edge_event);
627
                                 let _ = self.event_tx.send(edge_event);
hyprkvm-daemon/src/main.rsmodified
@@ -232,14 +232,22 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
232
         info!("  {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height);
232
         info!("  {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height);
233
     }
233
     }
234
 
234
 
235
-    // Calculate screen bounds (supports negative coordinates and multi-monitor layouts)
235
+    // Calculate screen bounds in LOGICAL coordinates (cursor position uses logical coords)
236
+    // Hyprland reports physical dimensions, but cursor_pos() returns logical coordinates
237
+    // Logical size = physical size / scale
236
     let screen_min_x: i32 = monitors.iter().map(|m| m.x).min().unwrap_or(0);
238
     let screen_min_x: i32 = monitors.iter().map(|m| m.x).min().unwrap_or(0);
237
     let screen_min_y: i32 = monitors.iter().map(|m| m.y).min().unwrap_or(0);
239
     let screen_min_y: i32 = monitors.iter().map(|m| m.y).min().unwrap_or(0);
238
-    let screen_max_x: i32 = monitors.iter().map(|m| m.x + m.width as i32).max().unwrap_or(1920);
240
+    let screen_max_x: i32 = monitors.iter().map(|m| {
239
-    let screen_max_y: i32 = monitors.iter().map(|m| m.y + m.height as i32).max().unwrap_or(1080);
241
+        let logical_width = (m.width as f32 / m.scale).round() as i32;
242
+        m.x + logical_width
243
+    }).max().unwrap_or(1920);
244
+    let screen_max_y: i32 = monitors.iter().map(|m| {
245
+        let logical_height = (m.height as f32 / m.scale).round() as i32;
246
+        m.y + logical_height
247
+    }).max().unwrap_or(1080);
240
     let screen_width: u32 = (screen_max_x - screen_min_x) as u32;
248
     let screen_width: u32 = (screen_max_x - screen_min_x) as u32;
241
     let screen_height: u32 = (screen_max_y - screen_min_y) as u32;
249
     let screen_height: u32 = (screen_max_y - screen_min_y) as u32;
242
-    info!("Screen bounds: ({}, {}) to ({}, {}), dimensions: {}x{}",
250
+    info!("Screen bounds (logical): ({}, {}) to ({}, {}), dimensions: {}x{}",
243
           screen_min_x, screen_min_y, screen_max_x, screen_max_y, screen_width, screen_height);
251
           screen_min_x, screen_min_y, screen_max_x, screen_max_y, screen_width, screen_height);
244
 
252
 
245
     // Determine which edges have network neighbors
253
     // Determine which edges have network neighbors
@@ -265,6 +273,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
265
         y: m.y,
273
         y: m.y,
266
         width: m.width,
274
         width: m.width,
267
         height: m.height,
275
         height: m.height,
276
+        scale: m.scale,
268
     }).collect();
277
     }).collect();
269
     let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig {
278
     let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig {
270
         barrier_size: 1,
279
         barrier_size: 1,
@@ -996,6 +1005,31 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
996
                 while let Some(edge_event) = edge_capture.try_recv() {
1005
                 while let Some(edge_event) = edge_capture.try_recv() {
997
                     let direction = edge_event.direction;
1006
                     let direction = edge_event.direction;
998
 
1007
 
1008
+                    // Verify cursor is actually at screen boundary using Hyprland
1009
+                    // (barrier placement can be wrong on multi-monitor setups)
1010
+                    let cursor_pos = match hypr_client.cursor_pos().await {
1011
+                        Ok(pos) => (pos.x, pos.y),
1012
+                        Err(_) => continue, // Can't verify, skip this event
1013
+                    };
1014
+                    let is_at_screen_edge = match direction {
1015
+                        Direction::Left => cursor_pos.0 <= screen_min_x + 5,
1016
+                        Direction::Right => cursor_pos.0 >= screen_max_x - 5,
1017
+                        Direction::Up => cursor_pos.1 <= screen_min_y + 5,
1018
+                        Direction::Down => cursor_pos.1 >= screen_max_y - 5,
1019
+                    };
1020
+
1021
+                    if !is_at_screen_edge {
1022
+                        tracing::debug!(
1023
+                            "EDGE: {:?} barrier triggered but cursor at ({}, {}) not at screen edge (bounds: {} to {}), ignoring",
1024
+                            direction,
1025
+                            cursor_pos.0,
1026
+                            cursor_pos.1,
1027
+                            screen_min_x,
1028
+                            screen_max_x
1029
+                        );
1030
+                        continue;
1031
+                    }
1032
+
999
                     // Check if we have a peer in this direction
1033
                     // Check if we have a peer in this direction
1000
                     let has_peer = {
1034
                     let has_peer = {
1001
                         let peers = peers.read().await;
1035
                         let peers = peers.read().await;
@@ -1011,8 +1045,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1011
                                 info!(
1045
                                 info!(
1012
                                     "EDGE: {:?} at ({}, {}) - returning control",
1046
                                     "EDGE: {:?} at ({}, {}) - returning control",
1013
                                     direction,
1047
                                     direction,
1014
-                                    edge_event.position.0,
1048
+                                    cursor_pos.0,
1015
-                                    edge_event.position.1
1049
+                                    cursor_pos.1
1016
                                 );
1050
                                 );
1017
                                 if let Err(e) = transfer_manager.return_control().await {
1051
                                 if let Err(e) = transfer_manager.return_control().await {
1018
                                     tracing::warn!("Failed to return control: {}", e);
1052
                                     tracing::warn!("Failed to return control: {}", e);
@@ -1033,20 +1067,20 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1033
                             info!(
1067
                             info!(
1034
                                 "EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1068
                                 "EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1035
                                 direction,
1069
                                 direction,
1036
-                                edge_event.position.0,
1070
+                                cursor_pos.0,
1037
-                                edge_event.position.1
1071
+                                cursor_pos.1
1038
                             );
1072
                             );
1039
                         } else {
1073
                         } else {
1040
                             info!(
1074
                             info!(
1041
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
1075
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
1042
                                 direction,
1076
                                 direction,
1043
-                                edge_event.position.0,
1077
+                                cursor_pos.0,
1044
-                                edge_event.position.1
1078
+                                cursor_pos.1
1045
                             );
1079
                             );
1046
 
1080
 
1047
                             if let Err(e) = transfer_manager.initiate_transfer(
1081
                             if let Err(e) = transfer_manager.initiate_transfer(
1048
                                 direction,
1082
                                 direction,
1049
-                                edge_event.position,
1083
+                                cursor_pos,
1050
                                 screen_min_x,
1084
                                 screen_min_x,
1051
                                 screen_min_y,
1085
                                 screen_min_y,
1052
                                 screen_max_x,
1086
                                 screen_max_x,