mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-15 00:41:01 +03:00
fix(cursor): capture complete macOS Retina cursor images
This commit is contained in:
@@ -35,12 +35,14 @@ use std::{
|
|||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod cursor;
|
||||||
|
|
||||||
// macOS boolean_t is defined as `int` in <mach/boolean.h>
|
// macOS boolean_t is defined as `int` in <mach/boolean.h>
|
||||||
type BooleanT = hbb_common::libc::c_int;
|
type BooleanT = hbb_common::libc::c_int;
|
||||||
|
|
||||||
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
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]
|
#[inline]
|
||||||
fn get_update_temp_dir() -> PathBuf {
|
fn get_update_temp_dir() -> PathBuf {
|
||||||
@@ -561,7 +563,7 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
|||||||
|
|
||||||
fn unsafe_get_cursor() -> ResultType<Option<u64>> {
|
fn unsafe_get_cursor() -> ResultType<Option<u64>> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let seed = CGSCurrentCursorSeed();
|
let seed = (CGSCurrentCursorSeed(), cursor::scale()?);
|
||||||
if seed == LATEST_SEED {
|
if seed == LATEST_SEED {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -573,11 +575,11 @@ fn unsafe_get_cursor() -> ResultType<Option<u64>> {
|
|||||||
|
|
||||||
pub fn reset_input_cache() {
|
pub fn reset_input_cache() {
|
||||||
unsafe {
|
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 {
|
unsafe {
|
||||||
let c: id = msg_send![class!(NSCursor), currentSystemCursor];
|
let c: id = msg_send![class!(NSCursor), currentSystemCursor];
|
||||||
if c == nil {
|
if c == nil {
|
||||||
@@ -620,7 +622,8 @@ fn get_cursor_id() -> ResultType<(id, u64)> {
|
|||||||
hcursor += (r + g + b + a) * (255 << i) as f64;
|
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
|
// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c
|
||||||
fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let (c, hcursor2) = get_cursor_id()?;
|
let (c, hcursor2, scale) = get_cursor_id()?;
|
||||||
if hcursor != hcursor2 {
|
if hcursor != hcursor2 {
|
||||||
bail!("cursor changed");
|
bail!("cursor changed");
|
||||||
}
|
}
|
||||||
|
if scale > 1.0 {
|
||||||
|
return cursor::data(c, hcursor, scale);
|
||||||
|
}
|
||||||
let hotspot: NSPoint = msg_send![c, hotSpot];
|
let hotspot: NSPoint = msg_send![c, hotSpot];
|
||||||
let img: id = msg_send![c, image];
|
let img: id = msg_send![c, image];
|
||||||
let size: NSSize = msg_send![img, size];
|
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 g: f64 = msg_send![color, greenComponent];
|
||||||
let b: f64 = msg_send![color, blueComponent];
|
let b: f64 = msg_send![color, blueComponent];
|
||||||
let a: f64 = msg_send![color, alphaComponent];
|
let a: f64 = msg_send![color, alphaComponent];
|
||||||
colors.push((r * 255.) as _);
|
colors.push((r * a * 255.).round() as _);
|
||||||
colors.push((g * 255.) as _);
|
colors.push((g * a * 255.).round() as _);
|
||||||
colors.push((b * 255.) as _);
|
colors.push((b * a * 255.).round() as _);
|
||||||
colors.push((a * 255.) as _);
|
colors.push((a * 255.) as _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,6 +687,7 @@ fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
|||||||
hoty: hotspot.y as _,
|
hoty: hotspot.y as _,
|
||||||
width: size.width as _,
|
width: size.width as _,
|
||||||
height: size.height as _,
|
height: size.height as _,
|
||||||
|
scale: 1.0,
|
||||||
..Default::default()
|
..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