NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 00:47:49 +00:00
commit 21dea33089
227 files changed
+80714

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
//! 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::<CURSORINFO>() 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<u8>,
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<CursorBitmap> {
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::<BITMAP>() 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<Vec<u8>> {
let screen = GetDC(None);
let mut bi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() 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<Vec<u8>> {
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<u8>, 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<u8> = 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");
}
}