Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions electron-app/magnifier/rust-sampler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_HiDpi",
] }

[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
47 changes: 8 additions & 39 deletions electron-app/magnifier/rust-sampler/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,36 +67,12 @@ 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, dpi_scale, &cmd_rx) {
if let Err(e) = run_sampling_loop(&mut *sampler, grid_size, sample_rate, &cmd_rx) {
eprintln!("Sampling loop error: {}", e);
send_error(&e);
}
Expand All @@ -123,7 +99,6 @@ fn run_sampling_loop(
sampler: &mut dyn PixelSampler,
initial_grid_size: usize,
sample_rate: u64,
dpi_scale: f64,
cmd_rx: &std::sync::mpsc::Receiver<Command>,
) -> Result<(), String> {
use std::sync::mpsc::TryRecvError;
Expand Down Expand Up @@ -159,8 +134,8 @@ fn run_sampling_loop(

let loop_start = std::time::Instant::now();

// Get cursor position (returns physical coordinates for Electron window positioning)
let physical_cursor = match sampler.get_cursor_position() {
// Get cursor position (returns logical coordinates, DPI-aware)
let cursor_pos = match sampler.get_cursor_position() {
Ok(pos) => pos,
Err(_e) => {
// On Wayland/some platforms, we can't get cursor position directly
Expand All @@ -171,24 +146,18 @@ 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 = 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,
};
last_cursor = cursor_pos.clone();

// Samplers handle DPI internally (like macOS), so pass coordinates directly
// Sample center pixel
let center_color = sampler.sample_pixel(virtual_cursor.x, virtual_cursor.y)
let center_color = sampler.sample_pixel(cursor_pos.x, cursor_pos.y)
.unwrap_or_else(|e| {
eprintln!("Failed to sample center pixel: {}", e);
Color::new(128, 128, 128)
});

// Sample grid
let grid = sampler.sample_grid(virtual_cursor.x, virtual_cursor.y, current_grid_size, 1.0)
let grid = sampler.sample_grid(cursor_pos.x, cursor_pos.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]
Expand All @@ -201,7 +170,7 @@ fn run_sampling_loop(
.collect();

let pixel_data = PixelData {
cursor: physical_cursor.clone(),
cursor: cursor_pos.clone(),
center: center_color.into(),
grid: grid_data,
timestamp: SystemTime::now()
Expand Down
60 changes: 39 additions & 21 deletions electron-app/magnifier/rust-sampler/src/sampler/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use windows::Win32::Graphics::Gdi::{
GetDeviceCaps, GetDIBits, GetPixel, LOGPIXELSX, ReleaseDC, SelectObject, BITMAPINFO,
BITMAPINFOHEADER, BI_RGB, CLR_INVALID, DIB_RGB_COLORS, HDC, SRCCOPY,
};
use windows::Win32::UI::HiDpi::{SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2};
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;

pub struct WindowsSampler {
Expand All @@ -16,6 +17,11 @@ pub struct WindowsSampler {
impl WindowsSampler {
pub fn new() -> Result<Self, String> {
unsafe {
// Set DPI awareness to per-monitor v2 so we can access physical pixels
// This must be done before any GDI calls
// Ignore errors - if it fails, we'll fall back to system DPI awareness
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

let hdc = GetDC(None);

if hdc.is_invalid() {
Expand All @@ -27,6 +33,8 @@ impl WindowsSampler {
// Standard DPI is 96, so scale = actual_dpi / 96
let dpi = GetDeviceCaps(hdc, LOGPIXELSX);
let dpi_scale = dpi as f64 / 96.0;

eprintln!("[WindowsSampler] DPI scale factor: {}", dpi_scale);

Ok(WindowsSampler {
hdc,
Expand All @@ -47,12 +55,13 @@ impl Drop for WindowsSampler {
impl PixelSampler for WindowsSampler {
fn sample_pixel(&mut self, x: i32, y: i32) -> Result<Color, String> {
unsafe {
// 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);
// With DPI awareness enabled, follow the macOS pattern:
// - x, y are logical coordinates (like CGWindowListCreateImage on macOS)
// - Convert to physical coordinates internally for GDI
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);

// Check for error (CLR_INVALID is returned on error)
// COLORREF is a newtype wrapper around u32
Expand All @@ -79,14 +88,16 @@ impl PixelSampler for WindowsSampler {
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;

// With DPI awareness enabled, follow the macOS pattern:
// - GetCursorPos returns physical coordinates
// - Convert to logical coordinates (like macOS CGEventGetLocation)
// - This matches Electron's coordinate system and main.rs expectations
let logical_x = (point.x as f64 / self.dpi_scale) as i32;
let logical_y = (point.y as f64 / self.dpi_scale) as i32;

Ok(Point {
x: physical_x,
y: physical_y,
x: logical_x,
y: logical_y,
})
}
}
Expand All @@ -97,10 +108,14 @@ impl PixelSampler for WindowsSampler {
unsafe {
let half_size = (grid_size / 2) as i32;

// 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;
// With DPI awareness enabled, follow the macOS pattern:
// - center_x, center_y are logical coordinates
// - Convert to physical coordinates for GDI operations
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;

let x_start = physical_center_x - half_size;
let y_start = physical_center_y - half_size;
let width = grid_size as i32;
let height = grid_size as i32;

Expand Down Expand Up @@ -220,13 +235,16 @@ impl WindowsSampler {
let half_size = (grid_size / 2) as i32;
let mut grid = Vec::with_capacity(grid_size);

// For DPI-unaware process, use coordinates directly
// center_x, center_y are logical coordinates - convert to physical
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 row in 0..grid_size {
let mut row_pixels = Vec::with_capacity(grid_size);
for col in 0..grid_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);
// Calculate physical pixel coordinates
let x = physical_center_x + (col as i32 - half_size);
let y = physical_center_y + (row as i32 - half_size);

let color_ref = GetPixel(self.hdc, x, y);

Expand Down
44 changes: 23 additions & 21 deletions electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ impl MockWindowsSampler {

impl PixelSampler for MockWindowsSampler {
fn sample_pixel(&mut self, x: i32, y: i32) -> Result<Color, String> {
// Simulate DPI coordinate conversion (virtual -> physical)
// In DPI-aware Electron: GetCursorPos gives virtual, GetPixel expects physical
// With DPI awareness enabled, follow the macOS pattern:
// - x, y are logical coordinates
// - Convert to physical coordinates internally for sampling
let physical_x = (x as f64 * self.dpi_scale) as i32;
let physical_y = (y as f64 * self.dpi_scale) as i32;

Expand All @@ -52,32 +53,32 @@ impl PixelSampler for MockWindowsSampler {
}

fn get_cursor_position(&self) -> Result<Point, String> {
// 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 })
// With DPI awareness, GetCursorPos returns physical coordinates
// But we return logical coordinates (physical / dpi_scale) for Electron
let physical_x = 200; // Simulated physical cursor position
let physical_y = 200;
let logical_x = (physical_x as f64 / self.dpi_scale) as i32;
let logical_y = (physical_y as f64 / self.dpi_scale) as i32;
Ok(Point { x: logical_x, y: logical_y })
}

// Override sample_grid to simulate production behavior (virtual coordinates)
// Override sample_grid to simulate production behavior (logical coordinates)
fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result<Vec<Vec<Color>>, String> {
let half_size = (grid_size / 2) as i32;
let mut grid = Vec::with_capacity(grid_size);

// Production sample_grid operates in virtual coordinates (no DPI scaling)
// Production sample_grid operates in logical coordinates like macOS
for row in 0..grid_size {
let mut row_pixels = Vec::with_capacity(grid_size);
for col in 0..grid_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);
// Calculate logical pixel coordinates (matches production behavior)
let logical_x = center_x + (col as i32 - half_size);
let logical_y = center_y + (row as i32 - half_size);

// Convert virtual to physical for bounds checking and color calculation
// Convert logical 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;
let physical_x = (logical_x as f64 * self.dpi_scale) as i32;
let physical_y = (logical_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 {
Expand Down Expand Up @@ -128,8 +129,9 @@ fn test_windows_sampler_cursor_position() {
let sampler = MockWindowsSampler::new(1920, 1080);

let cursor = sampler.get_cursor_position().unwrap();
assert_eq!(cursor.x, 100);
assert_eq!(cursor.y, 100);
// With DPI awareness, physical 200 / scale 1.0 = logical 200
assert_eq!(cursor.x, 200);
assert_eq!(cursor.y, 200);
}

#[test]
Expand Down Expand Up @@ -496,10 +498,10 @@ fn test_windows_sampler_dpi_150_percent() {
#[test]
fn test_windows_sampler_dpi_200_percent() {
// Test 200% DPI scaling (2x) - the reported issue
// Physical screen: 5120x2880, Virtual screen: 2560x1440
// Physical screen: 5120x2880, Logical screen: 2560x1440
let mut sampler = MockWindowsSampler::new_with_dpi(5120, 2880, 2.0);

// Virtual coordinate 1000 should map to physical 2000 (1000 * 2.0)
// Logical coordinate 1000 should map to physical 2000 (1000 * 2.0) internally
let color = sampler.sample_pixel(1000, 500).unwrap();

// Color should be based on physical coordinates (2000, 1000)
Expand Down