Skip to content
Merged
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
188 changes: 164 additions & 24 deletions crates/jiffshot-capture/src/win/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ use crate::{from_wide, Frame, Rect};
use std::io;
use windows_sys::Win32::Foundation::{BOOL, LPARAM, POINT, RECT, TRUE};
use windows_sys::Win32::Graphics::Gdi::{
BitBlt, EnumDisplayMonitors, GetDC, GetMonitorInfoW, MonitorFromPoint, ReleaseDC, CAPTUREBLT,
HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, SRCCOPY,
BitBlt, DeleteObject, EnumDisplayMonitors, GetDC, GetMonitorInfoW, GetObjectW,
MonitorFromPoint, ReleaseDC, BITMAP, CAPTUREBLT, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW,
MONITOR_DEFAULTTONEAREST, SRCCOPY,
};
use windows_sys::Win32::UI::HiDpi::{
GetDpiForMonitor, SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
MDT_EFFECTIVE_DPI,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CopyIcon, DestroyIcon, DrawIconEx, GetCursorInfo, GetIconInfo, GetSystemMetrics, CURSORINFO,
CURSOR_SHOWING, DI_NORMAL, HICON, ICONINFO, MONITORINFOF_PRIMARY, SM_CXVIRTUALSCREEN,
SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
CopyIcon, CopyImage, DestroyIcon, DrawIconEx, GetCursorInfo, GetIconInfo, GetSystemMetrics,
SystemParametersInfoW, CURSORINFO, CURSOR_SHOWING, DI_NORMAL, HICON, ICONINFO, IMAGE_CURSOR,
LR_COPYFROMRESOURCE, MONITORINFOF_PRIMARY, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN,
SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};

/// Opts the process into per-monitor-v2 DPI awareness so every coordinate is
Expand Down Expand Up @@ -196,6 +198,96 @@ pub struct CursorSnapshot {
/// Top-left of the cursor image in screen coordinates.
pub x: i32,
pub y: i32,
width: i32,
height: i32,
}

/// The default cursor size Windows designs cursor images for.
const CURSOR_DEFAULT_SIZE: i32 = 32;
/// Undocumented `SPI_GETCURSORBASESIZE`: the pointer size chosen in
/// Settings > Accessibility > Mouse pointer (32 is the default).
const SPI_GETCURSORBASESIZE: u32 = 0x2028;

/// The user's pointer size. Cursor handles keep their nominal (32 px)
/// bitmaps and the system enlarges them only when drawing the pointer, so a
/// capture must scale them by the same factor.
fn cursor_base_size() -> i32 {
let mut size: u32 = 0;
// SAFETY: the out-parameter is a plain u32.
let ok = unsafe {
SystemParametersInfoW(
SPI_GETCURSORBASESIZE,
0,
&mut size as *mut u32 as *mut core::ffi::c_void,
0,
)
};
if ok == 0 || !(CURSOR_DEFAULT_SIZE as u32..=1024).contains(&size) {
CURSOR_DEFAULT_SIZE
} else {
size as i32
}
}

/// Hotspot and drawn size of an icon or cursor.
///
/// # Safety
/// `icon` must be a valid icon or cursor handle.
unsafe fn icon_metrics(icon: HICON) -> Option<(i32, i32, i32, i32)> {
let mut icon_info: ICONINFO = std::mem::zeroed();
if GetIconInfo(icon, &mut icon_info) == 0 {
return None;
}
let mut bitmap: BITMAP = std::mem::zeroed();
let has_size = !icon_info.hbmMask.is_null()
&& GetObjectW(
icon_info.hbmMask,
std::mem::size_of::<BITMAP>() as i32,
&mut bitmap as *mut BITMAP as *mut core::ffi::c_void,
) != 0;
// Monochrome cursors stack the AND and XOR masks in one bitmap.
let monochrome = icon_info.hbmColor.is_null();
if !icon_info.hbmMask.is_null() {
DeleteObject(icon_info.hbmMask);
}
if !icon_info.hbmColor.is_null() {
DeleteObject(icon_info.hbmColor);
}
let (width, height) = if has_size {
let height = if monochrome {
bitmap.bmHeight / 2
} else {
bitmap.bmHeight
};
(bitmap.bmWidth, height)
} else {
(CURSOR_DEFAULT_SIZE, CURSOR_DEFAULT_SIZE)
};
Some((
icon_info.xHotspot as i32,
icon_info.yHotspot as i32,
width,
height,
))
}

/// A copy of `cursor` enlarged for the user's pointer size, preferring the
/// cursor file's own larger image over stretching the nominal one. `None`
/// at the default size or when no copy could be made.
///
/// # Safety
/// `cursor` must be a valid cursor handle.
unsafe fn scaled_copy(cursor: HICON, base_size: i32) -> Option<HICON> {
if base_size <= CURSOR_DEFAULT_SIZE {
return None;
}
let (_, _, width, height) = icon_metrics(cursor)?;
let scale = |v: i32| (v * base_size + CURSOR_DEFAULT_SIZE / 2) / CURSOR_DEFAULT_SIZE;
let (width, height) = (scale(width), scale(height));
[LR_COPYFROMRESOURCE, 0].into_iter().find_map(|flags| {
let copy = CopyImage(cursor, IMAGE_CURSOR, width, height, flags);
(!copy.is_null()).then_some(copy as HICON)
})
}

// SAFETY: the icon is an owned copy, only drawn and destroyed.
Expand All @@ -215,50 +307,44 @@ impl CursorSnapshot {
{
return None;
}
let icon = CopyIcon(info.hCursor);
let icon = scaled_copy(info.hCursor, cursor_base_size())
.unwrap_or_else(|| CopyIcon(info.hCursor));
if icon.is_null() {
return None;
}
let mut icon_info: ICONINFO = std::mem::zeroed();
let (mut hot_x, mut hot_y) = (0, 0);
if GetIconInfo(icon, &mut icon_info) != 0 {
hot_x = icon_info.xHotspot as i32;
hot_y = icon_info.yHotspot as i32;
if !icon_info.hbmMask.is_null() {
windows_sys::Win32::Graphics::Gdi::DeleteObject(icon_info.hbmMask);
}
if !icon_info.hbmColor.is_null() {
windows_sys::Win32::Graphics::Gdi::DeleteObject(icon_info.hbmColor);
}
}
let (hot_x, hot_y, width, height) = icon_metrics(icon).unwrap_or((0, 0, 32, 32));
Some(CursorSnapshot {
icon,
x: info.ptScreenPos.x - hot_x,
y: info.ptScreenPos.y - hot_y,
width,
height,
})
}
}

/// The cursor alone, with transparency (drawn once on black and once on
/// white; the difference gives the alpha).
pub fn image(&self) -> io::Result<Frame> {
let size = 64;
let (width, height) = (self.width.max(1), self.height.max(1));
let on = |v: u8| -> io::Result<Frame> {
let mut f = Frame::new(size, size);
let mut f = Frame::new(width as u32, height as u32);
f.pixels.iter_mut().for_each(|b| *b = v);
let probe = CursorSnapshot {
icon: self.icon,
x: 0,
y: 0,
width,
height,
};
let r = probe.draw_onto(&mut f, (0, 0));
std::mem::forget(probe);
r.map(|_| f)
};
let (black, white) = (on(0)?, on(255)?);
let mut out = Frame::new(size, size);
let mut out = Frame::new(width as u32, height as u32);
out.alpha = true;
for i in 0..(size * size) as usize {
for i in 0..(width * height) as usize {
let a = 255
- (white.pixels[i * 4 + 1] as i32 - black.pixels[i * 4 + 1] as i32).clamp(0, 255);
let un = |c: u8| {
Expand Down Expand Up @@ -289,8 +375,8 @@ impl CursorSnapshot {
self.x - origin.0,
self.y - origin.1,
self.icon,
0,
0,
self.width,
self.height,
0,
std::ptr::null_mut(),
DI_NORMAL,
Expand All @@ -310,6 +396,60 @@ impl Drop for CursorSnapshot {
}
}

#[cfg(test)]
mod cursor_scale {
use super::*;
use windows_sys::Win32::UI::WindowsAndMessaging::{LoadCursorW, IDC_ARROW, IDC_IBEAM};

#[test]
fn default_size_keeps_the_nominal_cursor() {
// SAFETY: a shared system cursor.
let arrow = unsafe { LoadCursorW(std::ptr::null_mut(), IDC_ARROW) };
assert!(unsafe { scaled_copy(arrow, CURSOR_DEFAULT_SIZE) }.is_none());
}

#[test]
fn enlarged_pointer_scales_image_and_hotspot() {
// SAFETY: a shared system cursor; the copies are destroyed.
unsafe {
let ibeam = LoadCursorW(std::ptr::null_mut(), IDC_IBEAM);
let (hot_x, hot_y, w, h) = icon_metrics(ibeam).unwrap();
let copy = scaled_copy(ibeam, CURSOR_DEFAULT_SIZE * 3).unwrap();
let (big_hot_x, big_hot_y, big_w, big_h) = icon_metrics(copy).unwrap();
DestroyIcon(copy);
assert_eq!((big_w, big_h), (w * 3, h * 3));
assert!((big_hot_x - hot_x * 3).abs() <= 3, "{big_hot_x} vs {hot_x}");
assert!((big_hot_y - hot_y * 3).abs() <= 3, "{big_hot_y} vs {hot_y}");
}
}

#[test]
fn image_covers_the_whole_cursor() {
// SAFETY: a shared system cursor, copied and owned by the snapshot.
let snapshot = unsafe {
let arrow = LoadCursorW(std::ptr::null_mut(), IDC_ARROW);
let icon = scaled_copy(arrow, CURSOR_DEFAULT_SIZE * 4).unwrap();
let (_, _, width, height) = icon_metrics(icon).unwrap();
CursorSnapshot {
icon,
x: 0,
y: 0,
width,
height,
}
};
let image = snapshot.image().unwrap();
assert_eq!((image.width, image.height), (128, 128));
let opaque_right = (0..image.height)
.flat_map(|y| (0..image.width).map(move |x| (x, y)))
.filter(|&(x, y)| image.pixels[((y * image.width + x) * 4 + 3) as usize] > 0)
.map(|(x, _)| x)
.max()
.unwrap();
assert!(opaque_right > 32, "arrow drawn at nominal size");
}
}

#[cfg(test)]
mod grab_timing {
use super::*;
Expand Down