use objc2::rc::Retained;
use objc2::ClassType;
use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
use objc2_foundation::{NSPoint, NSSize};
use systemless::display::CursorImage;
use winit::window::Window;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CursorRgba {
pub pixels: Vec<u8>,
pub width: usize,
pub height: usize,
pub hot_h: usize,
pub hot_v: usize,
}
pub struct HostCursor {
enabled: bool,
pointer_inside: bool,
cursor: Option<Retained<NSCursor>>,
key: Option<(Option<CursorImage>, u64, u64)>,
}
impl HostCursor {
pub fn new() -> Self {
Self {
enabled: std::env::var_os("SYSTEMLESS_SOFTWARE_CURSOR").is_none(),
pointer_inside: false,
cursor: None,
key: None,
}
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn set_pointer_inside(&mut self, inside: bool) {
self.pointer_inside = inside;
}
pub fn sync(&mut self, window: &Window, cursor: Option<&CursorImage>, scale_phys: f64) {
if !self.enabled {
return;
}
let backing = window.scale_factor().max(0.01);
let key = (cursor.cloned(), scale_phys.to_bits(), backing.to_bits());
if self.key.as_ref() != Some(&key) {
match cursor {
None => {
self.cursor = None;
window.set_cursor_visible(false);
}
Some(image) => {
let rgba = cursor_rgba(image);
let scale = scale_phys;
let n = (scale.round() as usize).max(1);
let pixels = upscale_rgba(&rgba.pixels, rgba.width, rgba.height, n);
let points_per_guest_px = scale / backing;
let size = (
rgba.width as f64 * points_per_guest_px,
rgba.height as f64 * points_per_guest_px,
);
let hotspot = (
rgba.hot_h as f64 * points_per_guest_px,
rgba.hot_v as f64 * points_per_guest_px,
);
self.cursor = Some(make_ns_cursor(
&pixels,
rgba.width * n,
rgba.height * n,
size,
hotspot,
));
window.set_cursor_visible(true);
}
}
self.key = Some(key);
}
self.reassert();
}
pub fn reassert(&self) {
if !self.enabled || !self.pointer_inside {
return;
}
if let Some(ours) = self.cursor.as_ref() {
let current = unsafe { NSCursor::currentCursor() };
if !std::ptr::eq(&*current, &**ours) {
unsafe { ours.set() };
}
}
}
}
pub fn presentation_scale(
content_width: u32,
content_height: u32,
drawable_width: u32,
drawable_height: u32,
) -> f64 {
if content_width == 0 || content_height == 0 || drawable_width == 0 || drawable_height == 0 {
return 1.0;
}
(f64::from(drawable_width) / f64::from(content_width))
.min(f64::from(drawable_height) / f64::from(content_height))
}
pub fn cursor_rgba(image: &CursorImage) -> CursorRgba {
fn bit(plane: &[u8; 32], row: usize, col: usize) -> bool {
(plane[row * 2 + col / 8] >> (7 - (col % 8))) & 1 == 1
}
fn clamp_hot(hot: i16, max: usize) -> usize {
usize::try_from(hot).unwrap_or(0).min(max.saturating_sub(1))
}
match image {
CursorImage::Mono {
data,
mask,
hot_v,
hot_h,
} => {
let mut pixels = vec![0u8; 16 * 16 * 4];
for row in 0..16 {
for col in 0..16 {
let (d, m) = (bit(data, row, col), bit(mask, row, col));
let px = match (m, d) {
(true, true) => [0, 0, 0, 255],
(true, false) => [255, 255, 255, 255],
(false, true) => [0, 0, 0, 255],
(false, false) => [0, 0, 0, 0],
};
pixels[(row * 16 + col) * 4..][..4].copy_from_slice(&px);
}
}
CursorRgba {
pixels,
width: 16,
height: 16,
hot_h: clamp_hot(*hot_h, 16),
hot_v: clamp_hot(*hot_v, 16),
}
}
CursorImage::Color {
width,
height,
pixels_argb,
mask,
hot_v,
hot_h,
..
} => {
let (w, h) = (usize::from(*width).max(1), usize::from(*height).max(1));
let mut pixels = vec![0u8; w * h * 4];
for row in 0..h {
for col in 0..w {
let argb = pixels_argb.get(row * w + col).copied().unwrap_or(0);
let opaque = row < 16 && col < 16 && bit(mask, row, col);
let px = if opaque {
[(argb >> 16) as u8, (argb >> 8) as u8, argb as u8, 255]
} else {
[0, 0, 0, 0]
};
pixels[(row * w + col) * 4..][..4].copy_from_slice(&px);
}
}
CursorRgba {
pixels,
width: w,
height: h,
hot_h: clamp_hot(*hot_h, w),
hot_v: clamp_hot(*hot_v, h),
}
}
}
}
pub fn upscale_rgba(rgba: &[u8], width: usize, height: usize, n: usize) -> Vec<u8> {
if n <= 1 {
return rgba.to_vec();
}
let (out_w, out_h) = (width * n, height * n);
let mut out = vec![0u8; out_w * out_h * 4];
for y in 0..out_h {
for x in 0..out_w {
let src = ((y / n) * width + x / n) * 4;
let dst = (y * out_w + x) * 4;
out[dst..dst + 4].copy_from_slice(&rgba[src..src + 4]);
}
}
out
}
fn make_ns_cursor(
rgba: &[u8],
width: usize,
height: usize,
size: (f64, f64),
hotspot: (f64, f64),
) -> Retained<NSCursor> {
let bitmap = unsafe {
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
std::ptr::null_mut(),
width as isize,
height as isize,
8,
4,
true,
false,
NSDeviceRGBColorSpace,
(width * 4) as isize,
32,
)
.expect("NSBitmapImageRep for the guest cursor")
};
unsafe {
std::slice::from_raw_parts_mut(bitmap.bitmapData(), rgba.len()).copy_from_slice(rgba)
};
let size = NSSize::new(size.0, size.1);
unsafe { bitmap.setSize(size) };
let image = unsafe { NSImage::initWithSize(NSImage::alloc(), size) };
unsafe { image.addRepresentation(&bitmap) };
NSCursor::initWithImage_hotSpot(
NSCursor::alloc(),
&image,
NSPoint::new(hotspot.0, hotspot.1),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn px(rgba: &CursorRgba, row: usize, col: usize) -> [u8; 4] {
let i = (row * rgba.width + col) * 4;
rgba.pixels[i..i + 4].try_into().unwrap()
}
#[test]
fn presentation_scale_uses_the_binding_axis_for_height_constrained_windows() {
assert_eq!(presentation_scale(800, 600, 1600, 900), 1.5);
}
#[test]
fn presentation_scale_follows_a_cropped_presentation() {
assert_eq!(presentation_scale(640, 480, 800, 600), 1.25);
assert_eq!(
presentation_scale(0, 480, 800, 600),
1.0,
"zero dims fall back"
);
}
#[test]
fn mono_cursor_follows_the_quickdraw_mask_rules() {
let mut data = [0u8; 32];
let mut mask = [0u8; 32];
data[0] = 0b1000_0000; mask[0] = 0b1100_0000; data[2] = 0b1000_0000; let rgba = cursor_rgba(&CursorImage::mono(data, mask, 3, 2));
assert_eq!(
(rgba.width, rgba.height, rgba.hot_h, rgba.hot_v),
(16, 16, 2, 3)
);
assert_eq!(px(&rgba, 0, 0), [0, 0, 0, 255]);
assert_eq!(px(&rgba, 0, 1), [255, 255, 255, 255]);
assert_eq!(px(&rgba, 1, 0), [0, 0, 0, 255]);
assert_eq!(px(&rgba, 1, 1), [0, 0, 0, 0]);
}
#[test]
fn hotspot_is_clamped_into_the_image() {
let rgba = cursor_rgba(&CursorImage::mono([0; 32], [0; 32], -4, 40));
assert_eq!((rgba.hot_h, rgba.hot_v), (15, 0));
}
#[test]
fn color_cursor_takes_alpha_from_the_mask() {
let mut mask = [0u8; 32];
mask[0] = 0b1000_0000;
let image = CursorImage::Color {
width: 2,
height: 1,
pixels_argb: vec![0x00FF8040, 0x00123456],
mask,
hot_v: 0,
hot_h: 1,
mono_data: [0; 32],
mono_mask: [0; 32],
};
let rgba = cursor_rgba(&image);
assert_eq!((rgba.width, rgba.height, rgba.hot_h), (2, 1, 1));
assert_eq!(px(&rgba, 0, 0), [0xFF, 0x80, 0x40, 255]);
assert_eq!(px(&rgba, 0, 1), [0, 0, 0, 0]);
}
#[test]
fn upscale_duplicates_pixels() {
let src = [1, 2, 3, 4, 5, 6, 7, 8]; let up = upscale_rgba(&src, 2, 1, 2);
assert_eq!(up.len(), 4 * 2 * 4);
assert_eq!(&up[0..8], &[1, 2, 3, 4, 1, 2, 3, 4]);
assert_eq!(&up[8..16], &[5, 6, 7, 8, 5, 6, 7, 8]);
assert_eq!(&up[16..24], &[1, 2, 3, 4, 1, 2, 3, 4]);
assert_eq!(upscale_rgba(&src, 2, 1, 1), src.to_vec());
}
}