mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
fix(cursor): capture complete macOS Retina cursor images
This commit is contained in:
@@ -35,12 +35,14 @@ use std::{
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
mod cursor;
|
||||
|
||||
// macOS boolean_t is defined as `int` in <mach/boolean.h>
|
||||
type BooleanT = hbb_common::libc::c_int;
|
||||
|
||||
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
||||
static mut LATEST_SEED: i32 = 0;
|
||||
static mut LATEST_SEED: (i32, f64) = (0, 0.0);
|
||||
|
||||
#[inline]
|
||||
fn get_update_temp_dir() -> PathBuf {
|
||||
@@ -561,7 +563,7 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
|
||||
fn unsafe_get_cursor() -> ResultType<Option<u64>> {
|
||||
unsafe {
|
||||
let seed = CGSCurrentCursorSeed();
|
||||
let seed = (CGSCurrentCursorSeed(), cursor::scale()?);
|
||||
if seed == LATEST_SEED {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -573,11 +575,11 @@ fn unsafe_get_cursor() -> ResultType<Option<u64>> {
|
||||
|
||||
pub fn reset_input_cache() {
|
||||
unsafe {
|
||||
LATEST_SEED = 0;
|
||||
LATEST_SEED = (0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cursor_id() -> ResultType<(id, u64)> {
|
||||
fn get_cursor_id() -> ResultType<(id, u64, f64)> {
|
||||
unsafe {
|
||||
let c: id = msg_send![class!(NSCursor), currentSystemCursor];
|
||||
if c == nil {
|
||||
@@ -620,7 +622,8 @@ fn get_cursor_id() -> ResultType<(id, u64)> {
|
||||
hcursor += (r + g + b + a) * (255 << i) as f64;
|
||||
}
|
||||
}
|
||||
Ok((c, hcursor as _))
|
||||
let scale = cursor::scale()?;
|
||||
Ok((c, cursor::cache_id(hcursor as _, scale), scale))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,10 +634,13 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c
|
||||
fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
unsafe {
|
||||
let (c, hcursor2) = get_cursor_id()?;
|
||||
let (c, hcursor2, scale) = get_cursor_id()?;
|
||||
if hcursor != hcursor2 {
|
||||
bail!("cursor changed");
|
||||
}
|
||||
if scale > 1.0 {
|
||||
return cursor::data(c, hcursor, scale);
|
||||
}
|
||||
let hotspot: NSPoint = msg_send![c, hotSpot];
|
||||
let img: id = msg_send![c, image];
|
||||
let size: NSSize = msg_send![img, size];
|
||||
@@ -668,9 +674,9 @@ fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
let g: f64 = msg_send![color, greenComponent];
|
||||
let b: f64 = msg_send![color, blueComponent];
|
||||
let a: f64 = msg_send![color, alphaComponent];
|
||||
colors.push((r * 255.) as _);
|
||||
colors.push((g * 255.) as _);
|
||||
colors.push((b * 255.) as _);
|
||||
colors.push((r * a * 255.).round() as _);
|
||||
colors.push((g * a * 255.).round() as _);
|
||||
colors.push((b * a * 255.).round() as _);
|
||||
colors.push((a * 255.) as _);
|
||||
}
|
||||
}
|
||||
@@ -681,6 +687,7 @@ fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
hoty: hotspot.y as _,
|
||||
width: size.width as _,
|
||||
height: size.height as _,
|
||||
scale: 1.0,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
175
src/platform/macos/cursor.rs
Normal file
175
src/platform/macos/cursor.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use super::{CursorData, ResultType};
|
||||
use cocoa::{
|
||||
appkit::NSCompositingOperation,
|
||||
base::{id, nil, NO, YES},
|
||||
foundation::{NSInteger, NSPoint, NSRect, NSSize, NSString},
|
||||
};
|
||||
use hbb_common::{anyhow::Context, bail};
|
||||
use objc::{class, msg_send, rc::StrongPtr, sel, sel_impl};
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
ptr, slice,
|
||||
};
|
||||
|
||||
const CHANNELS: usize = 4;
|
||||
const BITS_PER_SAMPLE: NSInteger = 8;
|
||||
|
||||
pub(super) fn scale() -> ResultType<f64> {
|
||||
if !*scrap::quartz::ENABLE_RETINA.lock().unwrap() {
|
||||
return Ok(1.0);
|
||||
}
|
||||
unsafe {
|
||||
let point: NSPoint = msg_send![class!(NSEvent), mouseLocation];
|
||||
let screens: id = msg_send![class!(NSScreen), screens];
|
||||
let count: usize = msg_send![screens, count];
|
||||
for index in 0..count {
|
||||
let screen: id = msg_send![screens, objectAtIndex: index];
|
||||
let frame: NSRect = msg_send![screen, frame];
|
||||
// AppKit's bottom-left coordinates include the upper screen edge.
|
||||
if point.x >= frame.origin.x
|
||||
&& point.y > frame.origin.y
|
||||
&& point.x < frame.origin.x + frame.size.width
|
||||
&& point.y <= frame.origin.y + frame.size.height
|
||||
{
|
||||
return Ok(msg_send![screen, backingScaleFactor]);
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("No macOS display contains the cursor")
|
||||
}
|
||||
|
||||
pub(super) fn cache_id(cursor: u64, scale: f64) -> u64 {
|
||||
let mut hash = DefaultHasher::new();
|
||||
(cursor, scale.to_bits()).hash(&mut hash);
|
||||
hash.finish()
|
||||
}
|
||||
|
||||
unsafe fn bitmap(size: NSSize) -> ResultType<StrongPtr> {
|
||||
let color_space = StrongPtr::new(NSString::alloc(nil).init_str("NSDeviceRGBColorSpace"));
|
||||
let bitmap: id = msg_send![class!(NSBitmapImageRep), alloc];
|
||||
let bitmap: id = msg_send![bitmap,
|
||||
initWithBitmapDataPlanes: ptr::null_mut::<*mut u8>()
|
||||
pixelsWide: size.width as NSInteger pixelsHigh: size.height as NSInteger
|
||||
bitsPerSample: BITS_PER_SAMPLE samplesPerPixel: CHANNELS as NSInteger
|
||||
hasAlpha: YES isPlanar: NO colorSpaceName: *color_space
|
||||
bitmapFormat: 0usize bytesPerRow: (size.width as usize * CHANNELS) as NSInteger
|
||||
bitsPerPixel: BITS_PER_SAMPLE * CHANNELS as NSInteger];
|
||||
if bitmap == nil {
|
||||
bail!("Could not allocate the macOS cursor bitmap");
|
||||
}
|
||||
Ok(StrongPtr::new(bitmap))
|
||||
}
|
||||
|
||||
unsafe fn render(image: id, bitmap: id, size: NSSize) -> ResultType<()> {
|
||||
let context: id =
|
||||
msg_send![class!(NSGraphicsContext), graphicsContextWithBitmapImageRep: bitmap];
|
||||
if context == nil {
|
||||
bail!("Could not create the macOS cursor graphics context");
|
||||
}
|
||||
let (): () = msg_send![class!(NSGraphicsContext), saveGraphicsState];
|
||||
let (): () = msg_send![class!(NSGraphicsContext), setCurrentContext: context];
|
||||
// Drawing at the pixel size lets AppKit select the matching image representation.
|
||||
let (): () = msg_send![image,
|
||||
drawInRect: NSRect::new(NSPoint::new(0.0, 0.0), size)
|
||||
fromRect: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))
|
||||
operation: NSCompositingOperation::NSCompositeCopy fraction: 1.0f64];
|
||||
let (): () = msg_send![class!(NSGraphicsContext), restoreGraphicsState];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) unsafe fn data(cursor: id, id: u64, scale: f64) -> ResultType<CursorData> {
|
||||
let image: id = msg_send![cursor, image];
|
||||
let logical: NSSize = msg_send![image, size];
|
||||
let size = NSSize::new(
|
||||
(logical.width * scale).round(),
|
||||
(logical.height * scale).round(),
|
||||
);
|
||||
if !size.width.is_finite()
|
||||
|| !size.height.is_finite()
|
||||
|| size.width <= 0.0
|
||||
|| size.height <= 0.0
|
||||
|| size.width > i32::MAX as f64
|
||||
|| size.height > i32::MAX as f64
|
||||
{
|
||||
bail!("Invalid macOS cursor dimensions");
|
||||
}
|
||||
let length = (size.width as usize)
|
||||
.checked_mul(size.height as usize)
|
||||
.and_then(|pixels| pixels.checked_mul(CHANNELS))
|
||||
.context("Cursor bitmap size overflow")?;
|
||||
let bitmap = bitmap(size)?;
|
||||
render(image, *bitmap, size)?;
|
||||
let pixels: *const u8 = msg_send![*bitmap, bitmapData];
|
||||
if pixels.is_null() {
|
||||
bail!("Could not read the macOS cursor bitmap");
|
||||
}
|
||||
let hotspot: NSPoint = msg_send![cursor, hotSpot];
|
||||
Ok(CursorData {
|
||||
id,
|
||||
colors: slice::from_raw_parts(pixels, length).to_vec().into(),
|
||||
hotx: (hotspot.x * size.width / logical.width).round() as _,
|
||||
hoty: (hotspot.y * size.height / logical.height).round() as _,
|
||||
width: size.width as _,
|
||||
height: size.height as _,
|
||||
scale,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use objc::rc::autoreleasepool;
|
||||
|
||||
#[test]
|
||||
fn retina_cursor_uses_complete_high_resolution_artwork() {
|
||||
autoreleasepool(|| unsafe {
|
||||
let logical = NSSize::new(9.0, 18.0);
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
let image = StrongPtr::new(msg_send![image, initWithSize: logical]);
|
||||
let mut expected = Vec::new();
|
||||
for scale in [1, 2] {
|
||||
let width = logical.width as usize * scale;
|
||||
let height = logical.height as usize * scale;
|
||||
let rep = bitmap(NSSize::new(width as f64, height as f64)).unwrap();
|
||||
let mut pixels = vec![0; width * height * CHANNELS];
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
if y == 0 || y == height - 1 || x == width / 2 {
|
||||
let color = if y == 0 {
|
||||
[255, 0, 0, 255]
|
||||
} else {
|
||||
[0, 255, 0, 255]
|
||||
};
|
||||
pixels[(y * width + x) * CHANNELS..(y * width + x + 1) * CHANNELS]
|
||||
.copy_from_slice(&color);
|
||||
}
|
||||
}
|
||||
}
|
||||
let buffer: *mut u8 = msg_send![*rep, bitmapData];
|
||||
ptr::copy_nonoverlapping(pixels.as_ptr(), buffer, pixels.len());
|
||||
let (): () = msg_send![*rep, setSize: logical];
|
||||
let (): () = msg_send![*image, addRepresentation: *rep];
|
||||
if scale == 2 {
|
||||
expected = pixels;
|
||||
}
|
||||
}
|
||||
let cursor: id = msg_send![class!(NSCursor), alloc];
|
||||
let cursor = StrongPtr::new(
|
||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(4.0, 9.0)],
|
||||
);
|
||||
let result = data(*cursor, 1, 2.0).unwrap();
|
||||
assert_eq!(
|
||||
(result.width, result.height, result.hotx, result.hoty),
|
||||
(18, 36, 8, 18)
|
||||
);
|
||||
assert_eq!(result.colors.as_ref(), expected.as_slice());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_cache_changes_with_display_scale() {
|
||||
assert_ne!(cache_id(123, 1.0), cache_id(123, 2.0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user