diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 82bcf9d6f..70883dba8 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -35,12 +35,14 @@ use std::{ sync::Mutex, }; +mod cursor; + // macOS boolean_t is defined as `int` in 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> { fn unsafe_get_cursor() -> ResultType> { 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> { 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 { // https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { 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 { 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 { hoty: hotspot.y as _, width: size.width as _, height: size.height as _, + scale: 1.0, ..Default::default() }) } diff --git a/src/platform/macos/cursor.rs b/src/platform/macos/cursor.rs new file mode 100644 index 000000000..3d04b45a0 --- /dev/null +++ b/src/platform/macos/cursor.rs @@ -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 { + 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 { + 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 { + 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)); + } +}