lamco_rdp_input/
coordinates.rs

1//! Coordinate Transformation
2//!
3//! Handles coordinate transformation between RDP client coordinates and
4//! Wayland compositor coordinates with full multi-monitor support, DPI scaling,
5//! and sub-pixel accuracy.
6
7use crate::error::{InputError, Result};
8use tracing::debug;
9
10/// Monitor information
11#[derive(Debug, Clone)]
12pub struct MonitorInfo {
13    /// Monitor ID
14    pub id: u32,
15
16    /// Monitor name
17    pub name: String,
18
19    /// Physical position in virtual desktop (pixels)
20    pub x: i32,
21    pub y: i32,
22
23    /// Monitor dimensions (pixels)
24    pub width: u32,
25    pub height: u32,
26
27    /// DPI setting
28    pub dpi: f64,
29
30    /// Scale factor
31    pub scale_factor: f64,
32
33    /// Stream position (for video encoding)
34    pub stream_x: u32,
35    pub stream_y: u32,
36
37    /// Stream dimensions
38    pub stream_width: u32,
39    pub stream_height: u32,
40
41    /// Is this the primary monitor
42    pub is_primary: bool,
43}
44
45impl MonitorInfo {
46    /// Check if a point is within this monitor
47    pub fn contains_point(&self, x: f64, y: f64) -> bool {
48        x >= self.x as f64
49            && x < (self.x + self.width as i32) as f64
50            && y >= self.y as f64
51            && y < (self.y + self.height as i32) as f64
52    }
53
54    /// Check if a stream coordinate is within this monitor's stream region
55    pub fn contains_stream_point(&self, x: f64, y: f64) -> bool {
56        let end_x = self.stream_x + self.stream_width;
57        let end_y = self.stream_y + self.stream_height;
58
59        x >= self.stream_x as f64 && x < end_x as f64 && y >= self.stream_y as f64 && y < end_y as f64
60    }
61}
62
63/// Coordinate system information
64#[derive(Debug, Clone)]
65pub struct CoordinateSystem {
66    /// RDP coordinate space (client resolution)
67    pub rdp_width: u32,
68    pub rdp_height: u32,
69
70    /// Virtual desktop space (all monitors combined)
71    pub virtual_width: u32,
72    pub virtual_height: u32,
73    pub virtual_x_offset: i32,
74    pub virtual_y_offset: i32,
75
76    /// Stream coordinate space (encoding resolution)
77    pub stream_width: u32,
78    pub stream_height: u32,
79
80    /// DPI scaling factors
81    pub rdp_dpi: f64,
82    pub system_dpi: f64,
83}
84
85/// Coordinate transformer handles all coordinate transformations
86pub struct CoordinateTransformer {
87    /// Coordinate system information
88    coord_system: CoordinateSystem,
89
90    /// Monitor configurations
91    monitors: Vec<MonitorInfo>,
92
93    /// Sub-pixel accumulator for smooth mouse movement
94    sub_pixel_x: f64,
95    sub_pixel_y: f64,
96
97    /// Previous RDP position for delta calculation
98    last_rdp_x: u32,
99    last_rdp_y: u32,
100
101    /// Enable mouse acceleration
102    enable_acceleration: bool,
103
104    /// Acceleration factor
105    acceleration_factor: f64,
106
107    /// Enable sub-pixel precision
108    enable_sub_pixel: bool,
109}
110
111impl CoordinateTransformer {
112    /// Create a new coordinate transformer
113    pub fn new(monitors: Vec<MonitorInfo>) -> Result<Self> {
114        if monitors.is_empty() {
115            return Err(InputError::InvalidMonitorConfig("No monitors configured".to_string()));
116        }
117
118        let coord_system = Self::calculate_coordinate_system(&monitors);
119
120        Ok(Self {
121            coord_system,
122            monitors,
123            sub_pixel_x: 0.0,
124            sub_pixel_y: 0.0,
125            last_rdp_x: 0,
126            last_rdp_y: 0,
127            enable_acceleration: true,
128            acceleration_factor: 1.0,
129            enable_sub_pixel: true,
130        })
131    }
132
133    /// Calculate coordinate system from monitor configuration
134    fn calculate_coordinate_system(monitors: &[MonitorInfo]) -> CoordinateSystem {
135        // Calculate virtual desktop bounds
136        let mut min_x = i32::MAX;
137        let mut min_y = i32::MAX;
138        let mut max_x = i32::MIN;
139        let mut max_y = i32::MIN;
140
141        for monitor in monitors {
142            min_x = min_x.min(monitor.x);
143            min_y = min_y.min(monitor.y);
144            max_x = max_x.max(monitor.x + monitor.width as i32);
145            max_y = max_y.max(monitor.y + monitor.height as i32);
146        }
147
148        let virtual_width = (max_x - min_x) as u32;
149        let virtual_height = (max_y - min_y) as u32;
150
151        // Calculate stream dimensions
152        let stream_width = monitors.iter().map(|m| m.stream_width).max().unwrap_or(0);
153        let stream_height = monitors.iter().map(|m| m.stream_height).max().unwrap_or(0);
154
155        // Get primary monitor for RDP dimensions and DPI
156        let primary = monitors.iter().find(|m| m.is_primary).unwrap_or(&monitors[0]);
157
158        CoordinateSystem {
159            rdp_width: primary.width,
160            rdp_height: primary.height,
161            virtual_width,
162            virtual_height,
163            virtual_x_offset: min_x,
164            virtual_y_offset: min_y,
165            stream_width,
166            stream_height,
167            rdp_dpi: primary.dpi,
168            system_dpi: 96.0, // Default system DPI
169        }
170    }
171
172    /// Transform RDP coordinates to stream coordinates
173    pub fn rdp_to_stream(&mut self, rdp_x: u32, rdp_y: u32) -> Result<(f64, f64)> {
174        // Step 1: Normalize RDP coordinates to [0, 1] range
175        let norm_x = rdp_x as f64 / self.coord_system.rdp_width as f64;
176        let norm_y = rdp_y as f64 / self.coord_system.rdp_height as f64;
177
178        // Step 2: Apply DPI scaling
179        let dpi_scale = self.coord_system.system_dpi / self.coord_system.rdp_dpi;
180        let scaled_x = norm_x * dpi_scale;
181        let scaled_y = norm_y * dpi_scale;
182
183        // Step 3: Map to virtual desktop space
184        let virtual_x = scaled_x * self.coord_system.virtual_width as f64 + self.coord_system.virtual_x_offset as f64;
185        let virtual_y = scaled_y * self.coord_system.virtual_height as f64 + self.coord_system.virtual_y_offset as f64;
186
187        // Step 4: Find target monitor
188        let monitor = self.find_monitor_at_point(virtual_x, virtual_y)?;
189
190        // Step 5: Transform to monitor-local coordinates
191        let local_x = virtual_x - monitor.x as f64;
192        let local_y = virtual_y - monitor.y as f64;
193
194        // Step 6: Apply monitor scaling
195        let monitor_scale_x = monitor.stream_width as f64 / monitor.width as f64;
196        let monitor_scale_y = monitor.stream_height as f64 / monitor.height as f64;
197
198        let stream_x = monitor.stream_x as f64 + (local_x * monitor_scale_x * monitor.scale_factor);
199        let stream_y = monitor.stream_y as f64 + (local_y * monitor_scale_y * monitor.scale_factor);
200
201        // Step 7: Apply sub-pixel accumulation for smooth movement
202        if self.enable_sub_pixel {
203            self.sub_pixel_x += stream_x - stream_x.floor();
204            self.sub_pixel_y += stream_y - stream_y.floor();
205
206            let final_x = stream_x.floor()
207                + if self.sub_pixel_x >= 1.0 {
208                    self.sub_pixel_x -= 1.0;
209                    1.0
210                } else {
211                    0.0
212                };
213
214            let final_y = stream_y.floor()
215                + if self.sub_pixel_y >= 1.0 {
216                    self.sub_pixel_y -= 1.0;
217                    1.0
218                } else {
219                    0.0
220                };
221
222            Ok((final_x, final_y))
223        } else {
224            Ok((stream_x, stream_y))
225        }
226    }
227
228    /// Transform stream coordinates back to RDP coordinates
229    pub fn stream_to_rdp(&self, stream_x: f64, stream_y: f64) -> Result<(u32, u32)> {
230        // Step 1: Find source monitor from stream coordinates
231        let monitor = self.find_monitor_from_stream(stream_x, stream_y)?;
232
233        // Step 2: Convert to monitor-local coordinates
234        let local_stream_x = stream_x - monitor.stream_x as f64;
235        let local_stream_y = stream_y - monitor.stream_y as f64;
236
237        // Step 3: Reverse monitor scaling
238        let monitor_scale_x = monitor.width as f64 / monitor.stream_width as f64;
239        let monitor_scale_y = monitor.height as f64 / monitor.stream_height as f64;
240
241        let local_x = local_stream_x * monitor_scale_x / monitor.scale_factor;
242        let local_y = local_stream_y * monitor_scale_y / monitor.scale_factor;
243
244        // Step 4: Convert to virtual desktop coordinates
245        let virtual_x = monitor.x as f64 + local_x;
246        let virtual_y = monitor.y as f64 + local_y;
247
248        // Step 5: Normalize from virtual desktop
249        let norm_x = (virtual_x - self.coord_system.virtual_x_offset as f64) / self.coord_system.virtual_width as f64;
250        let norm_y = (virtual_y - self.coord_system.virtual_y_offset as f64) / self.coord_system.virtual_height as f64;
251
252        // Step 6: Reverse DPI scaling
253        let dpi_scale = self.coord_system.rdp_dpi / self.coord_system.system_dpi;
254        let scaled_x = norm_x * dpi_scale;
255        let scaled_y = norm_y * dpi_scale;
256
257        // Step 7: Convert to RDP coordinates
258        let rdp_x = (scaled_x * self.coord_system.rdp_width as f64).round() as u32;
259        let rdp_y = (scaled_y * self.coord_system.rdp_height as f64).round() as u32;
260
261        // Clamp to valid range
262        let rdp_x = rdp_x.min(self.coord_system.rdp_width.saturating_sub(1));
263        let rdp_y = rdp_y.min(self.coord_system.rdp_height.saturating_sub(1));
264
265        Ok((rdp_x, rdp_y))
266    }
267
268    /// Apply relative mouse movement with optional acceleration
269    pub fn apply_relative_movement(&mut self, delta_x: i32, delta_y: i32) -> Result<(f64, f64)> {
270        // Apply acceleration if enabled
271        let accel_x = if self.enable_acceleration {
272            delta_x as f64 * self.calculate_acceleration(delta_x.abs())
273        } else {
274            delta_x as f64
275        };
276
277        let accel_y = if self.enable_acceleration {
278            delta_y as f64 * self.calculate_acceleration(delta_y.abs())
279        } else {
280            delta_y as f64
281        };
282
283        // Update RDP position
284        let new_rdp_x = (self.last_rdp_x as i32 + accel_x as i32).max(0) as u32;
285        let new_rdp_y = (self.last_rdp_y as i32 + accel_y as i32).max(0) as u32;
286
287        // Clamp to bounds
288        let new_rdp_x = new_rdp_x.min(self.coord_system.rdp_width.saturating_sub(1));
289        let new_rdp_y = new_rdp_y.min(self.coord_system.rdp_height.saturating_sub(1));
290
291        self.last_rdp_x = new_rdp_x;
292        self.last_rdp_y = new_rdp_y;
293
294        // Transform to stream coordinates
295        self.rdp_to_stream(new_rdp_x, new_rdp_y)
296    }
297
298    /// Calculate mouse acceleration based on movement speed
299    fn calculate_acceleration(&self, speed: i32) -> f64 {
300        // Windows-style mouse acceleration curve
301        let base = self.acceleration_factor;
302        if speed < 2 {
303            base
304        } else if speed < 4 {
305            base * 1.5
306        } else if speed < 6 {
307            base * 2.0
308        } else if speed < 9 {
309            base * 2.5
310        } else if speed < 13 {
311            base * 3.0
312        } else {
313            base * 3.5
314        }
315    }
316
317    /// Find monitor containing the given point
318    fn find_monitor_at_point(&self, x: f64, y: f64) -> Result<&MonitorInfo> {
319        for monitor in &self.monitors {
320            if monitor.contains_point(x, y) {
321                return Ok(monitor);
322            }
323        }
324
325        // Default to primary monitor if point is outside all monitors
326        self.monitors
327            .iter()
328            .find(|m| m.is_primary)
329            .or_else(|| self.monitors.first())
330            .ok_or(InputError::InvalidCoordinate(x, y))
331    }
332
333    /// Find monitor from stream coordinates
334    fn find_monitor_from_stream(&self, stream_x: f64, stream_y: f64) -> Result<&MonitorInfo> {
335        for monitor in &self.monitors {
336            if monitor.contains_stream_point(stream_x, stream_y) {
337                return Ok(monitor);
338            }
339        }
340
341        // Default to first monitor
342        self.monitors
343            .first()
344            .ok_or(InputError::InvalidCoordinate(stream_x, stream_y))
345    }
346
347    /// Clamp coordinates to monitor bounds
348    pub fn clamp_to_bounds(&self, x: f64, y: f64) -> (f64, f64) {
349        let clamped_x = x.max(0.0).min(self.coord_system.stream_width as f64 - 1.0);
350        let clamped_y = y.max(0.0).min(self.coord_system.stream_height as f64 - 1.0);
351        (clamped_x, clamped_y)
352    }
353
354    /// Update monitor configuration
355    pub fn update_monitors(&mut self, monitors: Vec<MonitorInfo>) -> Result<()> {
356        if monitors.is_empty() {
357            return Err(InputError::InvalidMonitorConfig("No monitors configured".to_string()));
358        }
359
360        self.coord_system = Self::calculate_coordinate_system(&monitors);
361        self.monitors = monitors;
362        self.sub_pixel_x = 0.0;
363        self.sub_pixel_y = 0.0;
364
365        debug!("Updated monitor configuration: {} monitors", self.monitors.len());
366        Ok(())
367    }
368
369    /// Set mouse acceleration enabled
370    pub fn set_acceleration_enabled(&mut self, enabled: bool) {
371        self.enable_acceleration = enabled;
372    }
373
374    /// Set acceleration factor
375    pub fn set_acceleration_factor(&mut self, factor: f64) {
376        self.acceleration_factor = factor;
377    }
378
379    /// Set sub-pixel precision enabled
380    pub fn set_sub_pixel_enabled(&mut self, enabled: bool) {
381        self.enable_sub_pixel = enabled;
382    }
383
384    /// Get monitor count
385    pub fn monitor_count(&self) -> usize {
386        self.monitors.len()
387    }
388
389    /// Get monitor by ID
390    pub fn get_monitor(&self, id: u32) -> Option<&MonitorInfo> {
391        self.monitors.iter().find(|m| m.id == id)
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    fn create_test_monitor() -> MonitorInfo {
400        MonitorInfo {
401            id: 1,
402            name: "Primary".to_string(),
403            x: 0,
404            y: 0,
405            width: 1920,
406            height: 1080,
407            dpi: 96.0,
408            scale_factor: 1.0,
409            stream_x: 0,
410            stream_y: 0,
411            stream_width: 1920,
412            stream_height: 1080,
413            is_primary: true,
414        }
415    }
416
417    #[test]
418    fn test_coordinate_transformer_creation() {
419        let monitor = create_test_monitor();
420        let transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
421
422        assert_eq!(transformer.monitor_count(), 1);
423    }
424
425    #[test]
426    fn test_rdp_to_stream_single_monitor() {
427        let monitor = create_test_monitor();
428        let mut transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
429
430        // Test corner cases
431        let (x, y) = transformer.rdp_to_stream(0, 0).unwrap();
432        assert!(x >= 0.0 && x <= 1.0);
433        assert!(y >= 0.0 && y <= 1.0);
434
435        let (x, y) = transformer.rdp_to_stream(1919, 1079).unwrap();
436        assert!(x <= 1920.0);
437        assert!(y <= 1080.0);
438
439        // Test center
440        let (x, y) = transformer.rdp_to_stream(960, 540).unwrap();
441        assert!(x > 900.0 && x < 1000.0);
442        assert!(y > 500.0 && y < 600.0);
443    }
444
445    #[test]
446    fn test_stream_to_rdp_single_monitor() {
447        let monitor = create_test_monitor();
448        let transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
449
450        // Test round-trip
451        let (rdp_x, rdp_y) = transformer.stream_to_rdp(100.0, 100.0).unwrap();
452        assert!(rdp_x < 1920);
453        assert!(rdp_y < 1080);
454
455        let (rdp_x, rdp_y) = transformer.stream_to_rdp(1900.0, 1000.0).unwrap();
456        assert!(rdp_x < 1920);
457        assert!(rdp_y < 1080);
458    }
459
460    #[test]
461    fn test_round_trip_transformation() {
462        let monitor = create_test_monitor();
463        let mut transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
464        transformer.set_sub_pixel_enabled(false); // Disable for exact round-trip
465
466        // Test several points
467        let test_points = vec![(0, 0), (960, 540), (1919, 1079)];
468
469        for (orig_x, orig_y) in test_points {
470            let (stream_x, stream_y) = transformer.rdp_to_stream(orig_x, orig_y).unwrap();
471            let (rdp_x, rdp_y) = transformer.stream_to_rdp(stream_x, stream_y).unwrap();
472
473            // Allow for small rounding errors
474            assert!((rdp_x as i32 - orig_x as i32).abs() <= 1);
475            assert!((rdp_y as i32 - orig_y as i32).abs() <= 1);
476        }
477    }
478
479    #[test]
480    fn test_multi_monitor_configuration() {
481        let monitors = vec![
482            MonitorInfo {
483                id: 1,
484                name: "Left".to_string(),
485                x: 0,
486                y: 0,
487                width: 1920,
488                height: 1080,
489                dpi: 96.0,
490                scale_factor: 1.0,
491                stream_x: 0,
492                stream_y: 0,
493                stream_width: 1920,
494                stream_height: 1080,
495                is_primary: true,
496            },
497            MonitorInfo {
498                id: 2,
499                name: "Right".to_string(),
500                x: 1920,
501                y: 0,
502                width: 1920,
503                height: 1080,
504                dpi: 96.0,
505                scale_factor: 1.0,
506                stream_x: 1920,
507                stream_y: 0,
508                stream_width: 1920,
509                stream_height: 1080,
510                is_primary: false,
511            },
512        ];
513
514        let transformer = CoordinateTransformer::new(monitors).unwrap();
515        assert_eq!(transformer.monitor_count(), 2);
516    }
517
518    #[test]
519    fn test_relative_movement() {
520        let monitor = create_test_monitor();
521        let mut transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
522        transformer.last_rdp_x = 960;
523        transformer.last_rdp_y = 540;
524
525        let (x, y) = transformer.apply_relative_movement(10, 10).unwrap();
526        assert!(x > 960.0);
527        assert!(y > 540.0);
528    }
529
530    #[test]
531    fn test_mouse_acceleration() {
532        let monitor = create_test_monitor();
533        let mut transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
534        transformer.set_acceleration_enabled(true);
535        transformer.set_acceleration_factor(1.0);
536
537        // Small movement should have no acceleration
538        let accel_small = transformer.calculate_acceleration(1);
539        assert_eq!(accel_small, 1.0);
540
541        // Large movement should have acceleration
542        let accel_large = transformer.calculate_acceleration(15);
543        assert!(accel_large > 1.0);
544    }
545
546    #[test]
547    fn test_clamp_to_bounds() {
548        let monitor = create_test_monitor();
549        let transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
550
551        // Test clamping out-of-bounds coordinates
552        let (x, y) = transformer.clamp_to_bounds(-10.0, -10.0);
553        assert_eq!(x, 0.0);
554        assert_eq!(y, 0.0);
555
556        let (x, y) = transformer.clamp_to_bounds(2000.0, 2000.0);
557        assert!(x < 1920.0);
558        assert!(y < 1080.0);
559    }
560
561    #[test]
562    fn test_monitor_contains_point() {
563        let monitor = create_test_monitor();
564
565        assert!(monitor.contains_point(100.0, 100.0));
566        assert!(monitor.contains_point(1919.0, 1079.0));
567        assert!(!monitor.contains_point(-1.0, 0.0));
568        assert!(!monitor.contains_point(1920.0, 0.0));
569    }
570
571    #[test]
572    fn test_update_monitors() {
573        let monitor = create_test_monitor();
574        let mut transformer = CoordinateTransformer::new(vec![monitor]).unwrap();
575
576        let new_monitors = vec![
577            create_test_monitor(),
578            MonitorInfo {
579                id: 2,
580                name: "Secondary".to_string(),
581                x: 1920,
582                y: 0,
583                width: 1920,
584                height: 1080,
585                dpi: 96.0,
586                scale_factor: 1.0,
587                stream_x: 1920,
588                stream_y: 0,
589                stream_width: 1920,
590                stream_height: 1080,
591                is_primary: false,
592            },
593        ];
594
595        transformer.update_monitors(new_monitors).unwrap();
596        assert_eq!(transformer.monitor_count(), 2);
597    }
598
599    #[test]
600    fn test_empty_monitor_list_error() {
601        let result = CoordinateTransformer::new(vec![]);
602        assert!(result.is_err());
603    }
604}