#![allow(unsafe_code)]
use std::cell::RefCell;
use std::ffi::{CString, c_void};
use crate::geometry::{PhysicalSize, Point, ScaleFactor};
use crate::platform::ControlFlow;
use crate::platform::error::PlatformError;
use crate::platform::event::{ButtonState, Event, KeyCode, PointerButton, WindowId};
use crate::platform::window::{Window, WindowAttributes};
use crate::render::{Pixmap, Surface};
type Id = *mut c_void;
type Sel = *mut c_void;
type Class = *mut c_void;
#[repr(C)]
#[derive(Clone, Copy)]
struct CgPoint {
x: f64,
y: f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct CgSize {
width: f64,
height: f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct CgRect {
origin: CgPoint,
size: CgSize,
}
#[link(name = "objc", kind = "dylib")]
unsafe extern "C" {
fn objc_getClass(name: *const i8) -> Class;
fn sel_registerName(name: *const i8) -> Sel;
fn objc_msgSend();
fn objc_allocateClassPair(superclass: Class, name: *const i8, extra: usize) -> Class;
fn objc_registerClassPair(cls: Class);
fn class_addMethod(cls: Class, name: Sel, imp: *const c_void, types: *const i8) -> bool;
}
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGColorSpaceCreateDeviceRGB() -> *mut c_void;
fn CGColorSpaceRelease(space: *mut c_void);
fn CGDataProviderCreateWithData(
info: *mut c_void,
data: *const c_void,
size: usize,
release: *const c_void,
) -> *mut c_void;
fn CGDataProviderRelease(provider: *mut c_void);
#[allow(clippy::too_many_arguments)]
fn CGImageCreate(
width: usize,
height: usize,
bits_per_component: usize,
bits_per_pixel: usize,
bytes_per_row: usize,
space: *mut c_void,
bitmap_info: u32,
provider: *mut c_void,
decode: *const f64,
should_interpolate: bool,
intent: i32,
) -> *mut c_void;
fn CGImageRelease(image: *mut c_void);
fn CGContextDrawImage(ctx: *mut c_void, rect: CgRect, image: *mut c_void);
fn CGContextSaveGState(ctx: *mut c_void);
fn CGContextRestoreGState(ctx: *mut c_void);
fn CGContextTranslateCTM(ctx: *mut c_void, tx: f64, ty: f64);
fn CGContextScaleCTM(ctx: *mut c_void, sx: f64, sy: f64);
}
const NS_WINDOW_STYLE_TITLED: u64 = 1;
const NS_WINDOW_STYLE_CLOSABLE: u64 = 2;
const NS_WINDOW_STYLE_RESIZABLE: u64 = 8;
const NS_BACKING_BUFFERED: u64 = 2;
const CG_BITMAP_RGBA8: u32 = 3 | (4 << 12);
const ACTIVATION_REGULAR: i64 = 0;
fn class(name: &str) -> Class {
let c = CString::new(name).unwrap();
unsafe { objc_getClass(c.as_ptr()) }
}
fn sel(name: &str) -> Sel {
let c = CString::new(name).unwrap();
unsafe { sel_registerName(c.as_ptr()) }
}
unsafe fn msg_id(obj: Id, s: Sel) -> Id {
let f: unsafe extern "C" fn(Id, Sel) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_void(obj: Id, s: Sel) {
let f: unsafe extern "C" fn(Id, Sel) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_void_id(obj: Id, s: Sel, a: Id) {
let f: unsafe extern "C" fn(Id, Sel, Id) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn msg_void_bool(obj: Id, s: Sel, a: bool) {
let f: unsafe extern "C" fn(Id, Sel, bool) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn msg_void_i64(obj: Id, s: Sel, a: i64) {
let f: unsafe extern "C" fn(Id, Sel, i64) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn msg_u64(obj: Id, s: Sel) -> u64 {
let f: unsafe extern "C" fn(Id, Sel) -> u64 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_id_u64(obj: Id, s: Sel, a: u64) -> Id {
let f: unsafe extern "C" fn(Id, Sel, u64) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn msg_u16(obj: Id, s: Sel) -> u16 {
let f: unsafe extern "C" fn(Id, Sel) -> u16 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_point(obj: Id, s: Sel) -> CgPoint {
let f: unsafe extern "C" fn(Id, Sel) -> CgPoint =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_rect(obj: Id, s: Sel) -> CgRect {
let f: unsafe extern "C" fn(Id, Sel) -> CgRect =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
fn nsstring(s: &str) -> Id {
let c = CString::new(s).unwrap();
let f: unsafe extern "C" fn(Id, Sel, *const i8) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(class("NSString"), sel("stringWithUTF8String:"), c.as_ptr()) }
}
const NS_LEFT_MOUSE_DOWN: u64 = 1;
const NS_LEFT_MOUSE_UP: u64 = 2;
const NS_RIGHT_MOUSE_DOWN: u64 = 3;
const NS_RIGHT_MOUSE_UP: u64 = 4;
const NS_MOUSE_MOVED: u64 = 5;
const NS_LEFT_MOUSE_DRAGGED: u64 = 6;
const NS_KEY_DOWN: u64 = 10;
fn map_keycode(kc: u16) -> Option<KeyCode> {
Some(match kc {
36 => KeyCode::Enter,
48 => KeyCode::Tab,
51 => KeyCode::Backspace,
53 => KeyCode::Escape,
123 => KeyCode::ArrowLeft,
124 => KeyCode::ArrowRight,
125 => KeyCode::ArrowDown,
126 => KeyCode::ArrowUp,
_ => return None,
})
}
unsafe fn translate_event(etype: u64, ev: Id, view_h: f64) -> Option<Event> {
let pointer = |button: PointerButton, state: ButtonState| {
let p = unsafe { msg_point(ev, sel("locationInWindow")) };
Event::PointerButton {
button,
state,
position: Point::new(p.x, view_h - p.y),
}
};
match etype {
NS_LEFT_MOUSE_DOWN => Some(pointer(PointerButton::Left, ButtonState::Pressed)),
NS_LEFT_MOUSE_UP => Some(pointer(PointerButton::Left, ButtonState::Released)),
NS_RIGHT_MOUSE_DOWN => Some(pointer(PointerButton::Right, ButtonState::Pressed)),
NS_RIGHT_MOUSE_UP => Some(pointer(PointerButton::Right, ButtonState::Released)),
NS_MOUSE_MOVED | NS_LEFT_MOUSE_DRAGGED => {
let p = unsafe { msg_point(ev, sel("locationInWindow")) };
Some(Event::PointerMoved {
position: Point::new(p.x, view_h - p.y),
})
}
NS_KEY_DOWN => {
let kc = unsafe { msg_u16(ev, sel("keyCode")) };
if let Some(code) = map_keycode(kc) {
return Some(Event::Key {
code,
state: ButtonState::Pressed,
modifiers: Default::default(),
});
}
let chars = unsafe { msg_id(ev, sel("characters")) };
if chars.is_null() {
return None;
}
let utf8 = unsafe { msg_id(chars, sel("UTF8String")) } as *const i8;
if utf8.is_null() {
return None;
}
let s = unsafe { std::ffi::CStr::from_ptr(utf8) }
.to_string_lossy()
.into_owned();
if s.chars().all(|c| c.is_control()) {
None
} else {
Some(Event::Text(s))
}
}
_ => None,
}
}
#[derive(Default)]
struct MacCtx {
fb: Vec<u8>, w: usize,
h: usize,
view: Id,
a11y_label: String,
a11y_tree: Option<crate::platform::access::A11yNode>,
should_close: bool,
}
thread_local! {
static CTX: RefCell<MacCtx> = RefCell::new(MacCtx::default());
}
extern "C" fn draw_rect(_this: Id, _cmd: Sel, _dirty: CgRect) {
CTX.with(|c| {
let c = c.borrow();
if c.fb.is_empty() {
return;
}
unsafe {
let gctx_cls = class("NSGraphicsContext");
let current = msg_id(gctx_cls, sel("currentContext"));
if current.is_null() {
return;
}
let cg = msg_id(current, sel("CGContext"));
if cg.is_null() {
return;
}
let space = CGColorSpaceCreateDeviceRGB();
let provider = CGDataProviderCreateWithData(
std::ptr::null_mut(),
c.fb.as_ptr() as *const c_void,
c.fb.len(),
std::ptr::null(),
);
let image = CGImageCreate(
c.w,
c.h,
8,
32,
c.w * 4,
space,
CG_BITMAP_RGBA8,
provider,
std::ptr::null(),
false,
0,
);
let rect = CgRect {
origin: CgPoint { x: 0.0, y: 0.0 },
size: CgSize {
width: c.w as f64,
height: c.h as f64,
},
};
CGContextSaveGState(cg);
CGContextTranslateCTM(cg, 0.0, c.h as f64);
CGContextScaleCTM(cg, 1.0, -1.0);
CGContextDrawImage(cg, rect, image);
CGContextRestoreGState(cg);
CGImageRelease(image);
CGDataProviderRelease(provider);
CGColorSpaceRelease(space);
}
});
}
extern "C" fn is_flipped(_this: Id, _cmd: Sel) -> bool {
true
}
extern "C" fn window_will_close(_this: Id, _cmd: Sel, _notification: Id) {
CTX.with(|c| c.borrow_mut().should_close = true);
}
extern "C" fn acc_is_element(_this: Id, _cmd: Sel) -> bool {
true
}
extern "C" fn acc_role(_this: Id, _cmd: Sel) -> Id {
nsstring("AXGroup")
}
extern "C" fn acc_label(_this: Id, _cmd: Sel) -> Id {
CTX.with(|c| nsstring(&c.borrow().a11y_label))
}
fn ax_role(role: crate::platform::access::A11yRole) -> &'static str {
use crate::platform::access::A11yRole::*;
match role {
Window | Group => "AXGroup",
Button => "AXButton",
TextField => "AXTextField",
Text => "AXStaticText",
}
}
unsafe fn build_ax_element(node: &crate::platform::access::A11yNode, parent: Id) -> Id {
let (x, y, w, h) = node.bounds;
let frame = CgRect {
origin: CgPoint { x, y },
size: CgSize {
width: w,
height: h,
},
};
let make: unsafe extern "C" fn(Id, Sel, Id, CgRect, Id, Id) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
let el = unsafe {
make(
class("NSAccessibilityElement"),
sel("accessibilityElementWithRole:frame:label:parent:"),
nsstring(ax_role(node.role)),
frame,
nsstring(&node.name),
parent,
)
};
if !node.children.is_empty() {
let arr = unsafe { msg_id(class("NSMutableArray"), sel("array")) };
for child in &node.children {
let ce = unsafe { build_ax_element(child, el) };
unsafe { msg_void_id(arr, sel("addObject:"), ce) };
}
unsafe { msg_void_id(el, sel("setAccessibilityChildren:"), arr) };
}
el
}
unsafe fn dump_ax(el: Id, depth: usize) {
let role = nsstring_to_string(unsafe { msg_id(el, sel("accessibilityRole")) });
let label = nsstring_to_string(unsafe { msg_id(el, sel("accessibilityLabel")) });
let indent = " ".repeat(depth);
println!("Cocoa a11y:{indent} role={role} label={label}");
let children = unsafe { msg_id(el, sel("accessibilityChildren")) };
if !children.is_null() {
let n = unsafe { msg_u64(children, sel("count")) };
for i in 0..n {
let child = unsafe { msg_id_u64(children, sel("objectAtIndex:"), i) };
unsafe { dump_ax(child, depth + 1) };
}
}
}
extern "C" fn acc_children(this: Id, _cmd: Sel) -> Id {
CTX.with(|c| {
let c = c.borrow();
let Some(root) = c.a11y_tree.as_ref() else {
return std::ptr::null_mut();
};
unsafe {
let arr = msg_id(class("NSMutableArray"), sel("array"));
for child in &root.children {
let el = build_ax_element(child, this);
msg_void_id(arr, sel("addObject:"), el);
}
arr
}
})
}
fn nsstring_to_string(s: Id) -> String {
if s.is_null() {
return String::new();
}
unsafe {
let f: unsafe extern "C" fn(Id, Sel) -> *const i8 =
std::mem::transmute(objc_msgSend as *const c_void);
let p = f(s, sel("UTF8String"));
if p.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
}
fn register_view_class() -> Class {
let name = c"StippleView";
unsafe {
let existing = objc_getClass(name.as_ptr());
if !existing.is_null() {
return existing;
}
let superclass = class("NSView");
let cls = objc_allocateClassPair(superclass, name.as_ptr(), 0);
let draw_types = c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}";
class_addMethod(
cls,
sel("drawRect:"),
draw_rect as *const c_void,
draw_types.as_ptr(),
);
let flip_types = c"c@:";
class_addMethod(
cls,
sel("isFlipped"),
is_flipped as *const c_void,
flip_types.as_ptr(),
);
let close_types = c"v@:@";
class_addMethod(
cls,
sel("windowWillClose:"),
window_will_close as *const c_void,
close_types.as_ptr(),
);
let bool_types = c"c@:";
let id_types = c"@@:";
class_addMethod(
cls,
sel("isAccessibilityElement"),
acc_is_element as *const c_void,
bool_types.as_ptr(),
);
class_addMethod(
cls,
sel("accessibilityRole"),
acc_role as *const c_void,
id_types.as_ptr(),
);
class_addMethod(
cls,
sel("accessibilityLabel"),
acc_label as *const c_void,
id_types.as_ptr(),
);
class_addMethod(
cls,
sel("accessibilityChildren"),
acc_children as *const c_void,
id_types.as_ptr(),
);
objc_registerClassPair(cls);
cls
}
}
#[derive(Debug)]
struct MacWindow {
size: PhysicalSize,
}
impl Window for MacWindow {
fn id(&self) -> WindowId {
WindowId(1)
}
fn inner_size(&self) -> PhysicalSize {
self.size
}
fn scale_factor(&self) -> ScaleFactor {
ScaleFactor::IDENTITY
}
fn request_redraw(&self) {
CTX.with(|c| {
let view = c.borrow().view;
if !view.is_null() {
unsafe { msg_void_bool(view, sel("setNeedsDisplay:"), true) };
}
});
}
fn set_title(&self, _title: &str) {}
fn create_surface(&self) -> Box<dyn Surface> {
Box::new(MacSurface { size: self.size })
}
fn set_accessibility_tree(&self, root: &crate::platform::access::A11yNode) {
CTX.with(|c| {
let mut c = c.borrow_mut();
if !root.name.is_empty() {
c.a11y_label = root.name.clone();
}
c.a11y_tree = Some(root.clone());
});
}
}
#[derive(Debug)]
struct MacSurface {
size: PhysicalSize,
}
impl Surface for MacSurface {
fn resize(&mut self, size: PhysicalSize) {
self.size = size;
}
fn size(&self) -> PhysicalSize {
self.size
}
fn present(&mut self, pixmap: &Pixmap, _damage: &[crate::geometry::Rect]) {
let size = pixmap.size();
CTX.with(|c| {
let mut c = c.borrow_mut();
c.fb.clear();
c.fb.extend_from_slice(pixmap.as_bytes());
c.w = size.width as usize;
c.h = size.height as usize;
if !c.view.is_null() {
unsafe { msg_void_bool(c.view, sel("setNeedsDisplay:"), true) };
}
});
}
}
pub fn run<H>(attrs: WindowAttributes, mut handler: H) -> Result<(), PlatformError>
where
H: FnMut(Event, &dyn Window) -> ControlFlow,
{
let size = ScaleFactor::IDENTITY.to_physical(attrs.logical_size);
unsafe {
let app = msg_id(class("NSApplication"), sel("sharedApplication"));
if app.is_null() {
return Err(PlatformError::Os("NSApplication unavailable".into()));
}
msg_void_i64(app, sel("setActivationPolicy:"), ACTIVATION_REGULAR);
let content = CgRect {
origin: CgPoint { x: 0.0, y: 0.0 },
size: CgSize {
width: size.width as f64,
height: size.height as f64,
},
};
let window: Id = {
let alloc = msg_id(class("NSWindow"), sel("alloc"));
let init: unsafe extern "C" fn(Id, Sel, CgRect, u64, u64, bool) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
init(
alloc,
sel("initWithContentRect:styleMask:backing:defer:"),
content,
NS_WINDOW_STYLE_TITLED | NS_WINDOW_STYLE_CLOSABLE | NS_WINDOW_STYLE_RESIZABLE,
NS_BACKING_BUFFERED,
false,
)
};
if window.is_null() {
return Err(PlatformError::Os("NSWindow init failed".into()));
}
let view_cls = register_view_class();
let view: Id = {
let alloc = msg_id(view_cls, sel("alloc"));
let init: unsafe extern "C" fn(Id, Sel, CgRect) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
init(alloc, sel("initWithFrame:"), content)
};
msg_void_id(window, sel("setContentView:"), view);
msg_void_bool(window, sel("setReleasedWhenClosed:"), false);
msg_void_id(window, sel("setDelegate:"), view);
CTX.with(|c| {
let mut c = c.borrow_mut();
c.view = view;
c.a11y_label = attrs.title.clone();
});
let mut win = MacWindow { size };
handler(Event::RedrawRequested, &win);
if std::env::var("STIPPLE_COCOA_A11Y").is_ok() {
let role = nsstring_to_string(msg_id(view, sel("accessibilityRole")));
let label = nsstring_to_string(msg_id(view, sel("accessibilityLabel")));
let is_el: bool = {
let f: unsafe extern "C" fn(Id, Sel) -> bool =
std::mem::transmute(objc_msgSend as *const c_void);
f(view, sel("isAccessibilityElement"))
};
println!("Cocoa a11y: element={is_el} role={role} label={label}");
let children = msg_id(view, sel("accessibilityChildren"));
let n = if children.is_null() {
0
} else {
msg_u64(children, sel("count"))
};
println!("Cocoa a11y: children={n}");
for i in 0..n {
let child = msg_id_u64(children, sel("objectAtIndex:"), i);
dump_ax(child, 1);
}
}
msg_void_id(window, sel("center"), std::ptr::null_mut());
msg_void_id(window, sel("makeKeyAndOrderFront:"), std::ptr::null_mut());
msg_void_bool(app, sel("activateIgnoringOtherApps:"), true);
msg_void(app, sel("finishLaunching"));
let distant_future = msg_id(class("NSDate"), sel("distantFuture"));
let mode = nsstring("kCFRunLoopDefaultMode");
let next_sel = sel("nextEventMatchingMask:untilDate:inMode:dequeue:");
let next: unsafe extern "C" fn(Id, Sel, u64, Id, Id, bool) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
let pool_cls = class("NSAutoreleasePool");
let mut last = win.size;
loop {
let pool = msg_id(msg_id(pool_cls, sel("alloc")), sel("init"));
let mut exit = false;
let ev = next(app, next_sel, u64::MAX, distant_future, mode, true);
if !ev.is_null() {
let etype = msg_u64(ev, sel("type"));
if let Some(event) = translate_event(etype, ev, last.height as f64)
&& handler(event, &win) == ControlFlow::Exit
{
exit = true;
}
if !exit {
msg_void_id(app, sel("sendEvent:"), ev);
}
}
if CTX.with(|c| c.borrow().should_close) {
exit = true;
}
if exit {
msg_void(pool, sel("drain"));
break;
}
let b = msg_rect(view, sel("bounds"));
let now =
PhysicalSize::new(b.size.width.max(1.0) as u32, b.size.height.max(1.0) as u32);
if now != last {
last = now;
win.size = now;
handler(Event::Resized(now), &win);
}
msg_void(pool, sel("drain"));
}
}
Ok(())
}