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 {
4949
     pub y: i32,
5050
     pub width: u32,
5151
     pub height: u32,
52
+    pub scale: f32,
5253
 }
5354
 
5455
 /// Configuration for edge barriers
@@ -130,27 +131,28 @@ impl EdgeCaptureState {
130131
         let mut used_monitors: Vec<bool> = vec![false; self.config.monitors.len()];
131132
         let mut assigned_outputs: Vec<bool> = vec![false; self.outputs.len()];
132133
 
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
+
133147
         // First pass: try to find unique matches
134148
         for (out_idx, out) in self.outputs.iter_mut().enumerate() {
135
-            // Find monitors that could match this output size (considering possible scales)
136149
             let mut candidates: Vec<usize> = Vec::new();
137150
             for (i, mon) in self.config.monitors.iter().enumerate() {
138151
                 if used_monitors[i] {
139152
                     continue;
140153
                 }
141
-                // Check if sizes match directly
142
-                if mon.width == out.width && mon.height == out.height {
154
+                if sizes_match(mon, out.width, out.height) {
143155
                     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
-                    }
154156
                 }
155157
             }
156158
 
@@ -158,8 +160,8 @@ impl EdgeCaptureState {
158160
             if candidates.len() == 1 {
159161
                 let i = candidates[0];
160162
                 let mon = &self.config.monitors[i];
161
-                tracing::info!("Matched output {}x{} to monitor {} at ({}, {})",
162
-                    out.width, out.height, mon.name, mon.x, mon.y);
163
+                tracing::info!("Matched output {}x{} to monitor {} at ({}, {}) scale={}",
164
+                    out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
163165
                 out.x = mon.x;
164166
                 out.y = mon.y;
165167
                 used_monitors[i] = true;
@@ -176,21 +178,14 @@ impl EdgeCaptureState {
176178
             if assigned_outputs[out_idx] {
177179
                 continue; // Already assigned in first pass
178180
             }
179
-            // Find first unused monitor that could match
181
+            // Find first unused monitor that matches
180182
             for (i, mon) in self.config.monitors.iter().enumerate() {
181183
                 if used_monitors[i] {
182184
                     continue;
183185
                 }
184
-                // Check all possible scales
185
-                let matches = (mon.width == out.width && mon.height == out.height)
186
-                    || [1.5_f64, 2.0_f64].iter().any(|&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);
186
+                if sizes_match(mon, out.width, out.height) {
187
+                    tracing::info!("Assigned remaining output {}x{} to monitor {} at ({}, {}) scale={}",
188
+                        out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
194189
                     out.x = mon.x;
195190
                     out.y = mon.y;
196191
                     used_monitors[i] = true;
@@ -615,26 +610,18 @@ impl PointerHandler for EdgeCaptureState {
615610
                             if should_trigger {
616611
                                 self.last_trigger_time.insert(barrier.direction, now);
617612
 
618
-                                // Calculate screen position
619
-                                let (min_x, min_y, max_x, max_y) = self.screen_bounds();
620
-                                let screen_pos = match barrier.direction {
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
-
613
+                                // Send edge event with direction only
614
+                                // Main daemon will query Hyprland for actual cursor position
615
+                                // and verify we're at screen boundary before triggering transfer
627616
                                 let edge_event = EdgeEvent {
628617
                                     direction: barrier.direction,
629
-                                    position: screen_pos,
618
+                                    position: (0, 0), // Placeholder - main daemon uses Hyprland cursor pos
630619
                                     timestamp: now,
631620
                                 };
632621
 
633622
                                 tracing::info!(
634
-                                    "Edge trigger: {:?} at screen ({}, {})",
635
-                                    barrier.direction,
636
-                                    screen_pos.0,
637
-                                    screen_pos.1
623
+                                    "Edge event: {:?} barrier triggered",
624
+                                    barrier.direction
638625
                                 );
639626
 
640627
                                 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<()> {
232232
         info!("  {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height);
233233
     }
234234
 
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
236238
     let screen_min_x: i32 = monitors.iter().map(|m| m.x).min().unwrap_or(0);
237239
     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);
239
-    let screen_max_y: i32 = monitors.iter().map(|m| m.y + m.height as i32).max().unwrap_or(1080);
240
+    let screen_max_x: i32 = monitors.iter().map(|m| {
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);
240248
     let screen_width: u32 = (screen_max_x - screen_min_x) as u32;
241249
     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{}",
243251
           screen_min_x, screen_min_y, screen_max_x, screen_max_y, screen_width, screen_height);
244252
 
245253
     // Determine which edges have network neighbors
@@ -265,6 +273,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
265273
         y: m.y,
266274
         width: m.width,
267275
         height: m.height,
276
+        scale: m.scale,
268277
     }).collect();
269278
     let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig {
270279
         barrier_size: 1,
@@ -996,6 +1005,31 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
9961005
                 while let Some(edge_event) = edge_capture.try_recv() {
9971006
                     let direction = edge_event.direction;
9981007
 
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
+
9991033
                     // Check if we have a peer in this direction
10001034
                     let has_peer = {
10011035
                         let peers = peers.read().await;
@@ -1011,8 +1045,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
10111045
                                 info!(
10121046
                                     "EDGE: {:?} at ({}, {}) - returning control",
10131047
                                     direction,
1014
-                                    edge_event.position.0,
1015
-                                    edge_event.position.1
1048
+                                    cursor_pos.0,
1049
+                                    cursor_pos.1
10161050
                                 );
10171051
                                 if let Err(e) = transfer_manager.return_control().await {
10181052
                                     tracing::warn!("Failed to return control: {}", e);
@@ -1033,20 +1067,20 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
10331067
                             info!(
10341068
                                 "EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
10351069
                                 direction,
1036
-                                edge_event.position.0,
1037
-                                edge_event.position.1
1070
+                                cursor_pos.0,
1071
+                                cursor_pos.1
10381072
                             );
10391073
                         } else {
10401074
                             info!(
10411075
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
10421076
                                 direction,
1043
-                                edge_event.position.0,
1044
-                                edge_event.position.1
1077
+                                cursor_pos.0,
1078
+                                cursor_pos.1
10451079
                             );
10461080
 
10471081
                             if let Err(e) = transfer_manager.initiate_transfer(
10481082
                                 direction,
1049
-                                edge_event.position,
1083
+                                cursor_pos,
10501084
                                 screen_min_x,
10511085
                                 screen_min_y,
10521086
                                 screen_max_x,