diff --git a/electron-app/magnifier/rust-sampler/src/main.rs b/electron-app/magnifier/rust-sampler/src/main.rs index b33eb1e5f..8885135d5 100644 --- a/electron-app/magnifier/rust-sampler/src/main.rs +++ b/electron-app/magnifier/rust-sampler/src/main.rs @@ -30,13 +30,13 @@ fn run() -> Result<(), String> { // Create channels for command communication let (cmd_tx, cmd_rx): (std::sync::mpsc::Sender, Receiver) = channel(); - + // Spawn stdin reader thread thread::spawn(move || { let stdin = io::stdin(); let mut reader = stdin.lock(); let mut line = String::new(); - + loop { line.clear(); match reader.read_line(&mut line) { @@ -67,12 +67,36 @@ fn run() -> Result<(), String> { } }); + // Get DPI scale - Windows needs it, others use 1.0 + let dpi_scale = get_dpi_scale(); + + fn get_dpi_scale() -> f64 { + #[cfg(target_os = "windows")] + { + // On Windows, get DPI scale directly from system + use windows::Win32::Graphics::Gdi::{GetDC, GetDeviceCaps, LOGPIXELSX, ReleaseDC}; + unsafe { + let hdc = GetDC(None); + if !hdc.is_invalid() { + let dpi = GetDeviceCaps(hdc, LOGPIXELSX); + let _ = ReleaseDC(None, hdc); + return dpi as f64 / 96.0; + } + } + 1.0 // Fallback + } + #[cfg(not(target_os = "windows"))] + { + 1.0 + } + } + // Main loop - wait for commands from channel loop { match cmd_rx.recv() { Ok(Command::Start { grid_size, sample_rate }) => { eprintln!("Starting sampling: grid_size={}, sample_rate={}", grid_size, sample_rate); - if let Err(e) = run_sampling_loop(&mut *sampler, grid_size, sample_rate, &cmd_rx) { + if let Err(e) = run_sampling_loop(&mut *sampler, grid_size, sample_rate, dpi_scale, &cmd_rx) { eprintln!("Sampling loop error: {}", e); send_error(&e); } @@ -99,6 +123,7 @@ fn run_sampling_loop( sampler: &mut dyn PixelSampler, initial_grid_size: usize, sample_rate: u64, + dpi_scale: f64, cmd_rx: &std::sync::mpsc::Receiver, ) -> Result<(), String> { use std::sync::mpsc::TryRecvError; @@ -134,8 +159,8 @@ fn run_sampling_loop( let loop_start = std::time::Instant::now(); - // Get cursor position - let cursor = match sampler.get_cursor_position() { + // Get cursor position (returns physical coordinates for Electron window positioning) + let physical_cursor = match sampler.get_cursor_position() { Ok(pos) => pos, Err(_e) => { // On Wayland/some platforms, we can't get cursor position directly @@ -146,17 +171,24 @@ fn run_sampling_loop( // Sample every frame regardless of cursor movement for smooth updates // This ensures the UI is responsive even if cursor position can't be tracked - last_cursor = cursor.clone(); + last_cursor = physical_cursor.clone(); + + // Convert physical coordinates back to virtual for sampling + // We know dpi_scale is available here since it's declared at function scope + let virtual_cursor = Point { + x: (physical_cursor.x as f64 / dpi_scale) as i32, + y: (physical_cursor.y as f64 / dpi_scale) as i32, + }; // Sample center pixel - let center_color = sampler.sample_pixel(cursor.x, cursor.y) + let center_color = sampler.sample_pixel(virtual_cursor.x, virtual_cursor.y) .unwrap_or_else(|e| { eprintln!("Failed to sample center pixel: {}", e); Color::new(128, 128, 128) }); // Sample grid - let grid = sampler.sample_grid(cursor.x, cursor.y, current_grid_size, 1.0) + let grid = sampler.sample_grid(virtual_cursor.x, virtual_cursor.y, current_grid_size, 1.0) .unwrap_or_else(|e| { eprintln!("Failed to sample grid: {}", e); vec![vec![Color::new(128, 128, 128); current_grid_size]; current_grid_size] @@ -169,7 +201,7 @@ fn run_sampling_loop( .collect(); let pixel_data = PixelData { - cursor: cursor.clone(), + cursor: physical_cursor.clone(), center: center_color.into(), grid: grid_data, timestamp: SystemTime::now() diff --git a/electron-app/magnifier/rust-sampler/src/sampler/linux.rs b/electron-app/magnifier/rust-sampler/src/sampler/linux.rs index 828f41a32..0391312bb 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/linux.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/linux.rs @@ -252,7 +252,7 @@ impl PixelSampler for LinuxSampler { Ok(Point { x: root_x, y: root_y }) } } - + fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result>, String> { // Ensure we have a fresh screenshot self.ensure_fresh_screenshot()?; diff --git a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs index 409337a35..28e9fbef9 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs @@ -392,7 +392,7 @@ impl PixelSampler for WaylandPortalSampler { Ok(Point { x: root_x, y: root_y }) } } - + fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result>, String> { self.ensure_screenshot_captured()?; diff --git a/electron-app/magnifier/rust-sampler/src/sampler/windows.rs b/electron-app/magnifier/rust-sampler/src/sampler/windows.rs index ada9b988e..e8bdf2925 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/windows.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/windows.rs @@ -10,25 +10,25 @@ use windows::Win32::UI::WindowsAndMessaging::GetCursorPos; pub struct WindowsSampler { hdc: HDC, - dpi_scale: f64, + pub dpi_scale: f64, } impl WindowsSampler { pub fn new() -> Result { unsafe { let hdc = GetDC(None); - + if hdc.is_invalid() { return Err("Failed to get device context".to_string()); } - + // Get DPI scaling factor // GetDeviceCaps returns DPI (e.g., 96 for 100%, 192 for 200%) // Standard DPI is 96, so scale = actual_dpi / 96 let dpi = GetDeviceCaps(hdc, LOGPIXELSX); let dpi_scale = dpi as f64 / 96.0; - - Ok(WindowsSampler { + + Ok(WindowsSampler { hdc, dpi_scale, }) @@ -47,14 +47,12 @@ impl Drop for WindowsSampler { impl PixelSampler for WindowsSampler { fn sample_pixel(&mut self, x: i32, y: i32) -> Result { unsafe { - // Electron is DPI-aware, so: - // - GetCursorPos returns VIRTUAL pixels (e.g., 0-2559 at 200% on 5120 wide screen) - // - GetPixel expects PHYSICAL pixels (e.g., 0-5119) - // We must convert: physical = virtual * dpi_scale - let physical_x = (x as f64 * self.dpi_scale) as i32; - let physical_y = (y as f64 * self.dpi_scale) as i32; - - let color_ref = GetPixel(self.hdc, physical_x, physical_y); + // On Windows, for a DPI-unaware process (which this Rust subprocess is): + // - GetCursorPos returns VIRTUALIZED coordinates (e.g., 0-2559 at 200% on 5120 wide screen) + // - GetDC(None) returns a VIRTUALIZED DC that also uses virtual coordinates + // - GetPixel on that DC expects the SAME virtualized coordinates + // NO conversion needed - both APIs work in the same virtualized space + let color_ref = GetPixel(self.hdc, x, y); // Check for error (CLR_INVALID is returned on error) // COLORREF is a newtype wrapper around u32 @@ -77,13 +75,18 @@ impl PixelSampler for WindowsSampler { fn get_cursor_position(&self) -> Result { unsafe { let mut point = POINT { x: 0, y: 0 }; - + GetCursorPos(&mut point) .map_err(|e| format!("Failed to get cursor position: {}", e))?; - + + // Convert from virtual coordinates (returned by GetCursorPos) to physical coordinates + // Electron (per-monitor DPI aware) expects physical coordinates for window positioning + let physical_x = (point.x as f64 * self.dpi_scale) as i32; + let physical_y = (point.y as f64 * self.dpi_scale) as i32; + Ok(Point { - x: point.x, - y: point.y, + x: physical_x, + y: physical_y, }) } } @@ -94,15 +97,10 @@ impl PixelSampler for WindowsSampler { unsafe { let half_size = (grid_size / 2) as i32; - // Electron is DPI-aware, so GetCursorPos returns virtual coordinates - // but GetDC/BitBlt use physical coordinates - // Convert: physical = virtual * dpi_scale - let physical_center_x = (center_x as f64 * self.dpi_scale) as i32; - let physical_center_y = (center_y as f64 * self.dpi_scale) as i32; - - // Calculate capture region in physical pixel coordinates - let x_start = physical_center_x - half_size; - let y_start = physical_center_y - half_size; + // For a DPI-unaware process, all GDI operations use virtualized coordinates + // No conversion needed + let x_start = center_x - half_size; + let y_start = center_y - half_size; let width = grid_size as i32; let height = grid_size as i32; @@ -222,19 +220,15 @@ impl WindowsSampler { let half_size = (grid_size / 2) as i32; let mut grid = Vec::with_capacity(grid_size); - // Convert virtual cursor coordinates to physical for DC sampling - let physical_center_x = (center_x as f64 * self.dpi_scale) as i32; - let physical_center_y = (center_y as f64 * self.dpi_scale) as i32; - + // For DPI-unaware process, use coordinates directly for row in 0..grid_size { let mut row_pixels = Vec::with_capacity(grid_size); for col in 0..grid_size { - // Calculate physical pixel coordinates - let physical_x = physical_center_x + (col as i32 - half_size); - let physical_y = physical_center_y + (row as i32 - half_size); + // Calculate pixel coordinates (no conversion needed) + let x = center_x + (col as i32 - half_size); + let y = center_y + (row as i32 - half_size); - // Sample using physical coordinates - let color_ref = GetPixel(self.hdc, physical_x, physical_y); + let color_ref = GetPixel(self.hdc, x, y); let color = if color_ref.0 == CLR_INVALID { Color::new(128, 128, 128) // Gray fallback for out-of-bounds diff --git a/electron-app/magnifier/rust-sampler/src/types.rs b/electron-app/magnifier/rust-sampler/src/types.rs index 0f18e933f..c98bd286c 100644 --- a/electron-app/magnifier/rust-sampler/src/types.rs +++ b/electron-app/magnifier/rust-sampler/src/types.rs @@ -82,7 +82,7 @@ pub trait PixelSampler { /// Get cursor position fn get_cursor_position(&self) -> Result; - + /// Sample a grid of pixels around a center point fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result>, String> { let half_size = (grid_size / 2) as i32; diff --git a/electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs b/electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs index 64c69b919..e79051916 100644 --- a/electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs +++ b/electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs @@ -52,26 +52,33 @@ impl PixelSampler for MockWindowsSampler { } fn get_cursor_position(&self) -> Result { - // Simulate GetCursorPos (returns virtual coordinates) - Ok(Point { x: 100, y: 100 }) + // Simulate Windows sampler behavior: return physical coordinates + // (virtual coordinates converted to physical for Electron compatibility) + let virtual_x = 100; + let virtual_y = 100; + let physical_x = (virtual_x as f64 * self.dpi_scale) as i32; + let physical_y = (virtual_y as f64 * self.dpi_scale) as i32; + Ok(Point { x: physical_x, y: physical_y }) } - // Override sample_grid to simulate DPI-aware behavior + // Override sample_grid to simulate production behavior (virtual coordinates) fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result>, String> { let half_size = (grid_size / 2) as i32; let mut grid = Vec::with_capacity(grid_size); - - // Convert center from virtual to physical pixels (matches real implementation) - let physical_center_x = (center_x as f64 * self.dpi_scale) as i32; - let physical_center_y = (center_y as f64 * self.dpi_scale) as i32; - + + // Production sample_grid operates in virtual coordinates (no DPI scaling) for row in 0..grid_size { let mut row_pixels = Vec::with_capacity(grid_size); for col in 0..grid_size { - // Work in physical pixel space (what BitBlt/GetPixel use) - let physical_x = physical_center_x + (col as i32 - half_size); - let physical_y = physical_center_y + (row as i32 - half_size); - + // Calculate virtual pixel coordinates (matches production behavior) + let virtual_x = center_x + (col as i32 - half_size); + let virtual_y = center_y + (row as i32 - half_size); + + // Convert virtual to physical for bounds checking and color calculation + // (since screen_width/screen_height are physical dimensions) + let physical_x = (virtual_x as f64 * self.dpi_scale) as i32; + let physical_y = (virtual_y as f64 * self.dpi_scale) as i32; + // Sample in physical space if physical_x < 0 || physical_y < 0 || physical_x >= self.screen_width || physical_y >= self.screen_height { row_pixels.push(Color::new(128, 128, 128)); @@ -84,7 +91,7 @@ impl PixelSampler for MockWindowsSampler { } grid.push(row_pixels); } - + Ok(grid) } } @@ -681,39 +688,32 @@ fn test_windows_sampler_dpi_grid_edge_alignment() { assert_eq!(grid.len(), grid_size); assert_eq!(grid[0].len(), grid_size); - // Verify center pixel matches what we expect at the physical coordinates - // Virtual (1000, 500) -> Physical (2000, 1000) - let dpi_scale = 2.0; - let physical_center_x = (virtual_center_x as f64 * dpi_scale) as i32; // 2000 - let physical_center_y = (virtual_center_y as f64 * dpi_scale) as i32; // 1000 - + // Verify center pixel matches what we expect + // Mock sample_grid operates in virtual coordinates like production let center_idx = grid_size / 2; // 2 for a 5x5 grid let center_pixel = &grid[center_idx][center_idx]; - - // The mock sampler generates colors based on physical coordinates: - // b = physical_x % 256, g = physical_y % 256, r = (physical_x + physical_y) % 256 - let expected_b = (physical_center_x % 256) as u8; - let expected_g = (physical_center_y % 256) as u8; - let expected_r = ((physical_center_x + physical_center_y) % 256) as u8; - + + // Center samples at virtual position (1000, 500) -> physical (2000, 1000) + // Colors are based on physical coordinates + let expected_b = (2000 % 256) as u8; // 2000 % 256 = 224 + let expected_g = (1000 % 256) as u8; // 1000 % 256 = 232 + let expected_r = ((2000 + 1000) % 256) as u8; // 3000 % 256 = 200 + assert_eq!(center_pixel.r, expected_r, "Center pixel R component mismatch"); assert_eq!(center_pixel.g, expected_g, "Center pixel G component mismatch"); assert_eq!(center_pixel.b, expected_b, "Center pixel B component mismatch"); - - // Verify corner pixels sample the correct physical locations - // Top-left: offset (-2, -2) from center -> physical (1998, 998) + + // Grid samples at virtual offsets from center (1000, 500) + // Virtual half_size = 2 for 5x5 grid + // Top-left: virtual (998, 498) -> physical (1996, 996) let top_left = &grid[0][0]; - let tl_physical_x = physical_center_x - 2; // 1998 - let tl_physical_y = physical_center_y - 2; // 998 - assert_eq!(top_left.b, (tl_physical_x % 256) as u8); - assert_eq!(top_left.g, (tl_physical_y % 256) as u8); - - // Bottom-right: offset (2, 2) from center -> physical (2002, 1002) + assert_eq!(top_left.b, (1996 % 256) as u8); // 1996 % 256 = 220 + assert_eq!(top_left.g, (996 % 256) as u8); // 996 % 256 = 228 + + // Bottom-right: virtual (1002, 502) -> physical (2004, 1004) let bottom_right = &grid[4][4]; - let br_physical_x = physical_center_x + 2; // 2002 - let br_physical_y = physical_center_y + 2; // 1002 - assert_eq!(bottom_right.b, (br_physical_x % 256) as u8); - assert_eq!(bottom_right.g, (br_physical_y % 256) as u8); + assert_eq!(bottom_right.b, (2004 % 256) as u8); // 2004 % 256 = 228 + assert_eq!(bottom_right.g, (1004 % 256) as u8); // 1004 % 256 = 236 } #[test]