//! Remote cursor tracking (RustDesk-style overlay): report the host cursor's //! position and shape to the viewer, sent on the control channel separately //! from video so an idle screen still shows a live, correctly-shaped cursor. //! //! Shapes are reported as CSS names for the standard system cursors (arrow, //! text, hand, resize, …) — which the viewer renders as crisp OS-native //! pointers — so there is no fragile per-frame bitmap extraction. Non-standard //! (app-custom) cursors fall back to the viewer's default pointer. #![cfg(windows)] use windows::Win32::UI::WindowsAndMessaging::{ GetCursorInfo, LoadCursorW, CURSORINFO, CURSOR_SHOWING, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, }; /// `(hcursor_or_0, global_x, global_y)`. `0` means the cursor is hidden. pub fn cursor_state() -> Option<(u64, i32, i32)> { unsafe { let mut ci = CURSORINFO { cbSize: std::mem::size_of::() as u32, ..Default::default() }; if GetCursorInfo(&mut ci).is_err() { return None; } let showing = (ci.flags.0 & CURSOR_SHOWING.0) != 0 && !ci.hCursor.is_invalid(); let h = if showing { ci.hCursor.0 as u64 } else { 0 }; Some((h, ci.ptScreenPos.x, ci.ptScreenPos.y)) } } /// Map a cursor handle to a CSS cursor name by comparing it against the loaded /// standard system cursors. Returns `None` for app-custom cursors. pub fn cursor_css(hcursor: u64) -> Option<&'static str> { if hcursor == 0 { return None; } const STD: &[(windows::core::PCWSTR, &str)] = &[ (IDC_ARROW, "default"), (IDC_IBEAM, "text"), (IDC_HAND, "pointer"), (IDC_CROSS, "crosshair"), (IDC_WAIT, "wait"), (IDC_APPSTARTING, "progress"), (IDC_SIZEALL, "move"), (IDC_SIZENS, "ns-resize"), (IDC_SIZEWE, "ew-resize"), (IDC_SIZENWSE, "nwse-resize"), (IDC_SIZENESW, "nesw-resize"), (IDC_NO, "not-allowed"), (IDC_HELP, "help"), ]; unsafe { for (idc, css) in STD { if let Ok(h) = LoadCursorW(None, *idc) { if h.0 as u64 == hcursor { return Some(css); } } } } None } /// A decoded custom-cursor bitmap ready for `cursor.shape` (PNG + hotspot), so /// app-specific cursors render on the viewer instead of falling back to the /// default pointer. RustDesk sends the raw cursor image the same way. pub struct CursorBitmap { pub png: Vec, pub w: u32, pub h: u32, pub hx: i32, pub hy: i32, } /// Extract a (color) cursor's RGBA bitmap + hotspot and PNG-encode it. Returns /// None for monochrome cursors (rare for app-custom shapes; the standard mono /// ones are already mapped to CSS) and on any GDI failure, so the caller simply /// keeps the default pointer. Mirrors capture.rs's GetDIBits usage. pub fn cursor_bitmap(hcursor: u64) -> Option { use windows::Win32::Foundation::HANDLE; use windows::Win32::Graphics::Gdi::{ DeleteObject, GetDC, GetDIBits, GetObjectW, ReleaseDC, BITMAP, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP, HGDIOBJ, }; use windows::Win32::UI::WindowsAndMessaging::{GetIconInfo, HICON, ICONINFO}; if hcursor == 0 { return None; } unsafe { let mut ii = ICONINFO::default(); if GetIconInfo(HICON(hcursor as *mut std::ffi::c_void), &mut ii).is_err() { return None; } // Always release the bitmaps GetIconInfo created for us. let cleanup = |ii: &ICONINFO| { if !ii.hbmColor.is_invalid() { let _ = DeleteObject(HGDIOBJ(ii.hbmColor.0)); } if !ii.hbmMask.is_invalid() { let _ = DeleteObject(HGDIOBJ(ii.hbmMask.0)); } }; if ii.hbmColor.is_invalid() { // Monochrome cursor — fall back to the default pointer. cleanup(&ii); return None; } // Dimensions from the color bitmap. let mut bm = BITMAP::default(); let got = GetObjectW( HGDIOBJ(ii.hbmColor.0), std::mem::size_of::() as i32, Some(&mut bm as *mut _ as *mut std::ffi::c_void), ); if got == 0 || bm.bmWidth <= 0 || bm.bmHeight <= 0 || bm.bmWidth > 512 || bm.bmHeight > 512 { cleanup(&ii); return None; } let (w, h) = (bm.bmWidth as u32, bm.bmHeight as u32); let read_bgra = |hbm: HBITMAP| -> Option> { let screen = GetDC(None); let mut bi = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { biSize: std::mem::size_of::() as u32, biWidth: w as i32, biHeight: -(h as i32), // top-down biPlanes: 1, biBitCount: 32, biCompression: BI_RGB.0, ..Default::default() }, ..Default::default() }; let mut buf = vec![0u8; (w * h * 4) as usize]; let lines = GetDIBits( screen, hbm, 0, h, Some(buf.as_mut_ptr() as *mut std::ffi::c_void), &mut bi, DIB_RGB_COLORS, ); ReleaseDC(None, screen); if lines == 0 { None } else { Some(buf) } }; let color = read_bgra(ii.hbmColor); // The AND mask (same 32bpp read): nonzero means "transparent" where the // color bitmap carries no alpha of its own. let mask = read_bgra(ii.hbmMask); let hotspot = (ii.xHotspot as i32, ii.yHotspot as i32); cleanup(&ii); let color = color?; // BGRA -> RGBA. Use the color bitmap's own alpha if it has any; otherwise // derive it from the AND mask (mask pixel != 0 => transparent). let has_alpha = color.chunks_exact(4).any(|px| px[3] != 0); let mut rgba = vec![0u8; (w * h * 4) as usize]; for (i, px) in color.chunks_exact(4).enumerate() { let (b, g, r) = (px[0], px[1], px[2]); let a = if has_alpha { px[3] } else if let Some(m) = &mask { // AND mask: a "set" (white) pixel is transparent. let mp = &m[i * 4..i * 4 + 4]; if mp[0] | mp[1] | mp[2] != 0 { 0 } else { 255 } } else { 255 }; let o = i * 4; rgba[o] = r; rgba[o + 1] = g; rgba[o + 2] = b; rgba[o + 3] = a; } let _ = HANDLE::default(); // keep the HANDLE import used across windows versions let png = encode_png(&rgba, w, h)?; Some(CursorBitmap { png, w, h, hx: hotspot.0, hy: hotspot.1 }) } } /// Encode a top-down RGBA buffer to PNG bytes. pub(crate) fn encode_png(rgba: &[u8], w: u32, h: u32) -> Option> { let mut out = Vec::new(); { let mut enc = png::Encoder::new(&mut out, w, h); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); let mut writer = enc.write_header().ok()?; writer.write_image_data(rgba).ok()?; } Some(out) } /// Decode PNG bytes to top-down RGBA8 + (w, h). Used for clipboard images that /// arrive from a viewer, to hand arboard an `ImageData`. pub(crate) fn decode_png(bytes: &[u8]) -> Option<(Vec, u32, u32)> { let dec = png::Decoder::new(std::io::Cursor::new(bytes)); let mut reader = dec.read_info().ok()?; let mut buf = vec![0u8; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).ok()?; buf.truncate(info.buffer_size()); // Normalize to RGBA8. let rgba = match (info.color_type, info.bit_depth) { (png::ColorType::Rgba, png::BitDepth::Eight) => buf, (png::ColorType::Rgb, png::BitDepth::Eight) => { let mut out = Vec::with_capacity((info.width * info.height * 4) as usize); for px in buf.chunks_exact(3) { out.extend_from_slice(&[px[0], px[1], px[2], 255]); } out } _ => return None, // uncommon formats -> skip (clipboard images are RGB/RGBA8) }; Some((rgba, info.width, info.height)) } #[cfg(test)] mod tests { use super::*; // Extract a real system cursor's bitmap (IDC_HAND is a color cursor on // modern Windows). In production cursor_css() catches the standard ones // first; here we call cursor_bitmap directly to prove the GDI extraction + // PNG encode produce a valid, sensibly-sized image. #[test] fn extracts_a_real_cursor_to_png() { let h = unsafe { LoadCursorW(None, IDC_HAND) }; let h = match h { Ok(h) => h.0 as u64, Err(_) => return, // no interactive cursor resource (headless CI) — skip }; if let Some(bmp) = cursor_bitmap(h) { assert!(bmp.w >= 8 && bmp.w <= 512, "width sane: {}", bmp.w); assert!(bmp.h >= 8 && bmp.h <= 512, "height sane: {}", bmp.h); // Valid PNG signature. assert_eq!(&bmp.png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]); // Round-trips through a decoder back to w*h*4 RGBA. let dec = png::Decoder::new(std::io::Cursor::new(&bmp.png)); let mut reader = dec.read_info().expect("png header"); let mut buf = vec![0u8; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).expect("png frame"); assert_eq!(info.width, bmp.w); assert_eq!(info.height, bmp.h); } // If extraction returned None (e.g. a monochrome IDC_HAND on some // builds), that's the documented safe fallback — not a failure. } } #[cfg(test)] mod clip_png_tests { use super::{decode_png, encode_png}; #[test] fn png_rgba_roundtrip_for_clipboard_images() { // A 2x2 RGBA image round-trips encode -> decode byte-for-byte. let w = 2u32; let h = 2u32; let rgba: Vec = vec![ 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 128, 10, 20, 30, 40, ]; let png = encode_png(&rgba, w, h).expect("encode"); assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]); let (got, gw, gh) = decode_png(&png).expect("decode"); assert_eq!((gw, gh), (w, h)); assert_eq!(got, rgba, "clipboard image pixels survive the round-trip"); } }