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
50 changes: 41 additions & 9 deletions electron-app/magnifier/rust-sampler/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ fn run() -> Result<(), String> {

// Create channels for command communication
let (cmd_tx, cmd_rx): (std::sync::mpsc::Sender<Command>, Receiver<Command>) = 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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<Command>,
) -> Result<(), String> {
use std::sync::mpsc::TryRecvError;
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion electron-app/magnifier/rust-sampler/src/sampler/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Vec<Color>>, String> {
// Ensure we have a fresh screenshot
self.ensure_fresh_screenshot()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Vec<Color>>, String> {
self.ensure_screenshot_captured()?;

Expand Down
64 changes: 29 additions & 35 deletions electron-app/magnifier/rust-sampler/src/sampler/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, String> {
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,
})
Expand All @@ -47,14 +47,12 @@ impl Drop for WindowsSampler {
impl PixelSampler for WindowsSampler {
fn sample_pixel(&mut self, x: i32, y: i32) -> Result<Color, String> {
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
Expand All @@ -77,13 +75,18 @@ impl PixelSampler for WindowsSampler {
fn get_cursor_position(&self) -> Result<Point, String> {
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,
})
}
}
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion electron-app/magnifier/rust-sampler/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub trait PixelSampler {

/// Get cursor position
fn get_cursor_position(&self) -> Result<Point, String>;

/// 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<Vec<Vec<Color>>, String> {
let half_size = (grid_size / 2) as i32;
Expand Down
78 changes: 39 additions & 39 deletions electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,33 @@ impl PixelSampler for MockWindowsSampler {
}

fn get_cursor_position(&self) -> Result<Point, String> {
// 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<Vec<Vec<Color>>, 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));
Expand All @@ -84,7 +91,7 @@ impl PixelSampler for MockWindowsSampler {
}
grid.push(row_pixels);
}

Ok(grid)
}
}
Expand Down Expand Up @@ -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]
Expand Down