#![allow(unsafe_code)]
use std::cell::RefCell;
use std::ffi::{CString, c_void};
use crate::geometry::{PhysicalSize, Point, ScaleFactor, Size};
use crate::platform::ControlFlow;
use crate::platform::error::PlatformError;
use crate::platform::event::{ButtonState, Event, KeyCode, PointerButton, WindowId};
use crate::platform::input;
use crate::platform::menu::{
MenuPopup, NativeMenu, NativeMenuBar, NativeMenuItem, NativeTray, TRAY_ID_BASE, mnemonic,
};
use crate::platform::window::{CursorIcon, Window, WindowAttributes};
use crate::render::{Pixmap, Surface};
mod display_link;
mod dnd;
mod menu_check;
mod text_input;
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 = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFDataCreate(allocator: *const c_void, bytes: *const u8, length: isize) -> *mut c_void;
fn CFRelease(cf: *const c_void);
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGDataProviderCreateWithCFData(data: *mut c_void) -> *mut c_void;
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 NS_WINDOW_STYLE_FULLSCREEN: u64 = 1 << 14;
const NS_COLLECTION_FULLSCREEN_PRIMARY: u64 = 1 << 7;
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_f64(obj: Id, s: Sel) -> f64 {
let f: unsafe extern "C" fn(Id, Sel) -> f64 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
unsafe fn msg_id_id(obj: Id, s: Sel, a: Id) -> Id {
let f: unsafe extern "C" fn(Id, Sel, Id) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
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_i64(obj: Id, s: Sel) -> i64 {
let f: unsafe extern "C" fn(Id, Sel) -> i64 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) }
}
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_bool(obj: Id, s: Sel) -> bool {
let f: unsafe extern "C" fn(Id, Sel) -> i8 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s) != 0 }
}
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) }
}
unsafe fn msg_void_size(obj: Id, s: Sel, a: CgSize) {
let f: unsafe extern "C" fn(Id, Sel, CgSize) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn msg_void_u64(obj: Id, s: Sel, a: u64) {
let f: unsafe extern "C" fn(Id, Sel, u64) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, s, a) }
}
unsafe fn responds_to(obj: Id, s: Sel) -> bool {
let f: unsafe extern "C" fn(Id, Sel, Sel) -> i8 =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
unsafe { f(obj, sel("respondsToSelector:"), s) != 0 }
}
fn ns_cursor(icon: CursorIcon) -> Id {
let (name, fallback) = match icon {
CursorIcon::Default => ("arrowCursor", "arrowCursor"),
CursorIcon::Text => ("IBeamCursor", "IBeamCursor"),
CursorIcon::Pointer => ("pointingHandCursor", "pointingHandCursor"),
CursorIcon::Crosshair => ("crosshairCursor", "crosshairCursor"),
CursorIcon::NotAllowed => ("operationNotAllowedCursor", "operationNotAllowedCursor"),
CursorIcon::Grab => ("openHandCursor", "openHandCursor"),
CursorIcon::Grabbing => ("closedHandCursor", "closedHandCursor"),
CursorIcon::Move => ("_moveCursor", "openHandCursor"),
CursorIcon::EwResize => ("resizeLeftRightCursor", "resizeLeftRightCursor"),
CursorIcon::NsResize => ("resizeUpDownCursor", "resizeUpDownCursor"),
CursorIcon::NwseResize => ("_windowResizeNorthWestSouthEastCursor", "crosshairCursor"),
CursorIcon::NeswResize => ("_windowResizeNorthEastSouthWestCursor", "crosshairCursor"),
};
let cls = class("NSCursor");
let s = sel(name);
let s = if unsafe { responds_to(cls, s) } {
s
} else {
sel(fallback)
};
unsafe { msg_id(cls, s) }
}
unsafe fn ns_image(icon: &Pixmap) -> Id {
let size = icon.size();
let (w, h) = (size.width as usize, size.height as usize);
let bytes = icon.as_bytes();
unsafe {
let data = CFDataCreate(std::ptr::null(), bytes.as_ptr(), bytes.len() as isize);
let provider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
let space = CGColorSpaceCreateDeviceRGB();
let image = CGImageCreate(
w,
h,
8,
32,
w * 4,
space,
CG_BITMAP_RGBA8,
provider,
std::ptr::null(),
true,
0,
);
CGColorSpaceRelease(space);
CGDataProviderRelease(provider);
if image.is_null() {
return std::ptr::null_mut();
}
let init: unsafe extern "C" fn(Id, Sel, *mut c_void, CgSize) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
let ns = init(
msg_id(class("NSImage"), sel("alloc")),
sel("initWithCGImage:size:"),
image,
CgSize {
width: w as f64,
height: h as f64,
},
);
CGImageRelease(image);
ns
}
}
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;
const NS_KEY_UP: u64 = 11;
const NS_SCROLL_WHEEL: u64 = 22;
unsafe fn event_string(ev: Id, getter: &str) -> Option<String> {
let chars = unsafe { msg_id(ev, sel(getter)) };
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) };
Some(s.to_string_lossy().into_owned())
}
unsafe fn key_event(ev: Id, state: ButtonState) -> Option<Event> {
let modifiers = input::mac_modifiers(unsafe { msg_u64(ev, sel("modifierFlags")) });
let kc = unsafe { msg_u16(ev, sel("keyCode")) };
if let Some(code) = input::mac_keycode(kc) {
return Some(Event::Key {
code,
state,
modifiers,
});
}
if modifiers.meta || modifiers.ctrl {
let s = unsafe { event_string(ev, "charactersIgnoringModifiers") }?;
let mut chars = s.chars();
let c = chars.next().filter(|c| !c.is_control())?;
if chars.next().is_some() || input::mac_is_function_key(&s) {
return None;
}
return Some(Event::Key {
code: KeyCode::Char(c),
state,
modifiers,
});
}
if state != ButtonState::Pressed {
return None;
}
let s = unsafe { event_string(ev, "characters") }?;
if s.chars().all(|c| c.is_control()) || input::mac_is_function_key(&s) {
None
} else {
Some(Event::Text(s))
}
}
fn post_wake_event() {
const APPLICATION_DEFINED: u64 = 15;
unsafe {
let make: unsafe extern "C" fn(
Id,
Sel,
u64,
CgPoint,
u64,
f64,
i64,
Id,
i16,
i64,
i64,
) -> Id = std::mem::transmute(objc_msgSend as *const c_void);
let event = make(
class("NSEvent"),
sel(
"otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:",
),
APPLICATION_DEFINED,
CgPoint { x: 0.0, y: 0.0 },
0,
0.0,
0,
std::ptr::null_mut(),
0,
0,
0,
);
if event.is_null() {
return;
}
let app = msg_id(class("NSApplication"), sel("sharedApplication"));
let post: unsafe extern "C" fn(Id, Sel, Id, bool) =
std::mem::transmute(objc_msgSend as *const c_void);
post(app, sel("postEvent:atStart:"), event, false);
}
}
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")) };
crate::platform::set_pointer_modifiers(input::mac_modifiers(unsafe {
msg_u64(ev, sel("modifierFlags"))
}));
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 => unsafe { key_event(ev, ButtonState::Pressed) },
NS_KEY_UP => unsafe { key_event(ev, ButtonState::Released) },
NS_SCROLL_WHEEL => {
let dx = unsafe { msg_f64(ev, sel("scrollingDeltaX")) };
let dy = unsafe { msg_f64(ev, sel("scrollingDeltaY")) };
let precise = unsafe { msg_bool(ev, sel("hasPreciseScrollingDeltas")) };
let delta = input::mac_scroll(dx, dy, precise);
(delta.dx != 0.0 || delta.dy != 0.0).then_some(Event::Scroll { delta })
}
_ => None,
}
}
#[derive(Default)]
struct MacCtx {
fb: Vec<u8>, w: usize,
h: usize,
view: Id,
window: Id,
scale: f64,
a11y_label: String,
a11y_tree: Option<crate::platform::access::A11yNode>,
should_close: bool,
cursor: CursorIcon,
ime: Option<crate::platform::ImeState>,
marked: String,
ime_events: Vec<Event>,
ime_command: bool,
menu_target: Id,
menu_states: std::collections::HashMap<i64, MenuState>,
pending: Vec<Event>,
context_pick: Option<i64>,
status_item: Id,
drop_accepted: bool,
drag_last: Option<Point>,
drag_out: Option<crate::platform::DropData>,
}
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 (width, height) = (c.w as f64 / c.scale, c.h as f64 / c.scale);
let rect = CgRect {
origin: CgPoint { x: 0.0, y: 0.0 },
size: CgSize { width, height },
};
CGContextSaveGState(cg);
CGContextTranslateCTM(cg, 0.0, height);
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 appearance_changed(_this: Id, _cmd: Sel, _notification: Id) {
crate::platform::native::note_changed();
}
unsafe fn observe_appearance(view: Id) {
let add: unsafe extern "C" fn(Id, Sel, Id, Sel, Id, Id) =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
let selector = sel("stippleAppearanceChanged:");
let observe = sel("addObserver:selector:name:object:");
unsafe {
let distributed = msg_id(
class("NSDistributedNotificationCenter"),
sel("defaultCenter"),
);
for name in [
"AppleInterfaceThemeChangedNotification",
"AppleColorPreferencesChangedNotification",
] {
add(
distributed,
observe,
view,
selector,
nsstring(name),
std::ptr::null_mut(),
);
}
let local = msg_id(class("NSNotificationCenter"), sel("defaultCenter"));
let name = nsstring("NSSystemColorsDidChangeNotification");
add(local, observe, view, selector, name, std::ptr::null_mut());
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {
static NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification: Id;
}
let workspace = msg_id(class("NSWorkspace"), sel("sharedWorkspace"));
let center = msg_id(workspace, sel("notificationCenter"));
add(
center,
observe,
view,
selector,
NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification,
std::ptr::null_mut(),
);
}
}
unsafe fn set_window_dark(window: Id, dark: bool) {
let name = nsstring(if dark {
"NSAppearanceNameDarkAqua"
} else {
"NSAppearanceNameAqua"
});
unsafe {
let appearance = msg_id_id(class("NSAppearance"), sel("appearanceNamed:"), name);
msg_void_id(window, sel("setAppearance:"), appearance);
}
}
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",
List => "AXList",
Cell => "AXCell",
Table => "AXTable",
ListItem | Row | TreeItem => "AXRow",
ColumnHeader => "AXButton",
Tree => "AXOutline",
MenuBar => "AXMenuBar",
Menu => "AXMenu",
MenuItem => "AXMenuItem",
ComboBox | CheckBox | Switch => "AXButton",
SpinButton => "AXTextField",
Splitter | Toolbar | Separator | Alert => "AXGroup",
}
}
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()
}
}
}
#[derive(Clone, Copy, Debug)]
struct MenuState {
enabled: bool,
checked: Option<bool>,
shortcut: Option<crate::platform::Shortcut>,
}
const MENU_QUIT_TAG: i64 = -1;
extern "C" fn menu_action(_this: Id, _cmd: Sel, sender: Id) {
let tag = unsafe { msg_i64(sender, sel("tag")) };
if tag == MENU_QUIT_TAG {
CTX.with(|c| c.borrow_mut().should_close = true);
return;
}
unsafe {
let app = msg_id(class("NSApplication"), sel("sharedApplication"));
let ev = msg_id(app, sel("currentEvent"));
if !ev.is_null() && msg_u64(ev, sel("type")) == NS_KEY_DOWN {
let shortcut = CTX.with(|c| c.borrow().menu_states.get(&tag).and_then(|s| s.shortcut));
if let (
Some(Event::Key {
code, modifiers, ..
}),
Some(shortcut),
) = (key_event(ev, ButtonState::Pressed), shortcut)
&& shortcut.matches(code, modifiers)
{
let key = nsstring_to_string(msg_id(sender, sel("keyEquivalent")));
menu_check::note(format_args!(
"key equivalent {key:?} of tag {tag} left to the app"
));
return;
}
}
}
CTX.with(|c| c.borrow_mut().pending.push(Event::MenuCommand(tag as u32)));
}
extern "C" fn context_action(_this: Id, _cmd: Sel, sender: Id) {
let tag = unsafe { msg_i64(sender, sel("tag")) };
CTX.with(|c| c.borrow_mut().context_pick = Some(tag));
}
extern "C" fn validate_menu_item(_this: Id, _cmd: Sel, item: Id) -> bool {
let tag = unsafe { msg_i64(item, sel("tag")) };
if tag == MENU_QUIT_TAG {
return true;
}
let state = CTX.with(|c| c.borrow().menu_states.get(&tag).copied());
let Some(MenuState {
enabled, checked, ..
}) = state
else {
return false;
};
let on = i64::from(checked == Some(true)); unsafe { msg_void_i64(item, sel("setState:"), on) };
enabled
}
fn menu_target() -> Id {
let existing = CTX.with(|c| c.borrow().menu_target);
if !existing.is_null() {
return existing;
}
let name = c"StippleMenuTarget";
unsafe {
let mut cls = objc_getClass(name.as_ptr());
if cls.is_null() {
cls = objc_allocateClassPair(class("NSObject"), name.as_ptr(), 0);
class_addMethod(
cls,
sel("stippleMenuAction:"),
menu_action as *const c_void,
c"v@:@".as_ptr(),
);
class_addMethod(
cls,
sel("validateMenuItem:"),
validate_menu_item as *const c_void,
c"c@:@".as_ptr(),
);
class_addMethod(
cls,
sel("stippleContextAction:"),
context_action as *const c_void,
c"v@:@".as_ptr(),
);
class_addMethod(
cls,
sel("menuWillOpen:"),
menu_check::menu_will_open as *const c_void,
c"v@:@".as_ptr(),
);
class_addMethod(
cls,
sel("stippleTrayAction:"),
tray_action as *const c_void,
c"v@:@".as_ptr(),
);
objc_registerClassPair(cls);
}
let target = msg_id(msg_id(cls, sel("alloc")), sel("init"));
CTX.with(|c| c.borrow_mut().menu_target = target);
target
}
}
unsafe fn menu_item(title: &str, action: Option<Sel>, key: &str, mask: u64) -> Id {
let init: unsafe extern "C" fn(Id, Sel, Id, Sel, Id) -> Id =
unsafe { std::mem::transmute(objc_msgSend as *const c_void) };
let item = unsafe {
init(
msg_id(class("NSMenuItem"), sel("alloc")),
sel("initWithTitle:action:keyEquivalent:"),
nsstring(title),
action.unwrap_or(std::ptr::null_mut()),
nsstring(key),
)
};
if mask != 0 {
unsafe { msg_void_u64(item, sel("setKeyEquivalentModifierMask:"), mask) };
}
item
}
unsafe fn ns_menu(title: &str) -> Id {
unsafe {
msg_id_id(
msg_id(class("NSMenu"), sel("alloc")),
sel("initWithTitle:"),
nsstring(title),
)
}
}
unsafe fn add_item(menu: Id, item: Id) {
unsafe {
msg_void_id(menu, sel("addItem:"), item);
msg_void(item, sel("release"));
}
}
unsafe fn add_submenu(menu: Id, title: &str, submenu: Id) {
unsafe {
let item = menu_item(title, None, "", 0);
msg_void_id(item, sel("setSubmenu:"), submenu);
msg_void(submenu, sel("release"));
add_item(menu, item);
}
}
unsafe fn fill_menu(menu: Id, items: &[NativeMenuItem], target: Id, context: bool) {
if context {
unsafe { msg_void_bool(menu, sel("setAutoenablesItems:"), false) };
}
let action = sel(if context {
"stippleContextAction:"
} else {
"stippleMenuAction:"
});
for item in items {
match item {
NativeMenuItem::Action {
id,
label,
shortcut,
enabled,
checked,
} => {
let (key, mask) = shortcut
.as_ref()
.and_then(input::mac_key_equivalent)
.unwrap_or_default();
let tag = i64::from(*id);
unsafe {
let it = menu_item(&mnemonic(label).0, Some(action), &key, mask);
msg_void_id(it, sel("setTarget:"), target);
msg_void_i64(it, sel("setTag:"), tag);
msg_void_i64(it, sel("setState:"), i64::from(*checked == Some(true)));
if context {
msg_void_bool(it, sel("setEnabled:"), *enabled);
}
add_item(menu, it);
}
if !context {
CTX.with(|c| {
let state = MenuState {
enabled: *enabled,
checked: *checked,
shortcut: *shortcut,
};
c.borrow_mut().menu_states.insert(tag, state);
});
}
}
NativeMenuItem::Separator => unsafe {
let sep = msg_id(class("NSMenuItem"), sel("separatorItem"));
msg_void_id(menu, sel("addItem:"), sep);
},
NativeMenuItem::Submenu(sub) => unsafe {
let title = mnemonic(&sub.label).0;
let m = ns_menu(&title);
fill_menu(m, &sub.items, target, context);
add_submenu(menu, &title, m);
},
}
}
}
unsafe fn popup_menu(menu: &NativeMenu, at: Point) -> MenuPopup {
let view = CTX.with(|c| c.borrow().view);
if view.is_null() {
return MenuPopup::Unsupported;
}
let target = menu_target();
CTX.with(|c| c.borrow_mut().context_pick = None);
unsafe {
let m = ns_menu("");
fill_menu(m, &menu.items, target, true);
menu_check::watch(m, target);
let pop: unsafe extern "C" fn(Id, Sel, Id, CgPoint, Id) -> i8 =
std::mem::transmute(objc_msgSend as *const c_void);
pop(
m,
sel("popUpMenuPositioningItem:atLocation:inView:"),
std::ptr::null_mut(),
CgPoint { x: at.x, y: at.y },
view,
);
msg_void(m, sel("release"));
}
let picked = CTX.with(|c| c.borrow_mut().context_pick.take());
menu_check::popup_closed(picked);
match picked {
Some(tag) if tag > 0 => MenuPopup::Picked(tag as u32),
_ => MenuPopup::Dismissed,
}
}
extern "C" fn tray_action(_this: Id, _cmd: Sel, _sender: Id) {
CTX.with(|c| c.borrow_mut().pending.push(Event::TrayActivated));
}
unsafe fn set_status_item(tray: Option<&NativeTray>) -> bool {
unsafe {
let bar = msg_id(class("NSStatusBar"), sel("systemStatusBar"));
let existing = CTX.with(|c| c.borrow().status_item);
CTX.with(|c| {
c.borrow_mut()
.menu_states
.retain(|&tag, _| tag < i64::from(TRAY_ID_BASE))
});
let Some(tray) = tray else {
if !existing.is_null() {
msg_void_id(bar, sel("removeStatusItem:"), existing);
msg_void(existing, sel("release"));
CTX.with(|c| c.borrow_mut().status_item = std::ptr::null_mut());
}
return false;
};
if bar.is_null() {
return false;
}
let item = if existing.is_null() {
let with_length: unsafe extern "C" fn(Id, Sel, f64) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
let item = with_length(bar, sel("statusItemWithLength:"), -1.0);
if item.is_null() {
return false;
}
msg_id(item, sel("retain"));
CTX.with(|c| c.borrow_mut().status_item = item);
item
} else {
existing
};
let button = msg_id(item, sel("button"));
if button.is_null() {
return false;
}
match &tray.icon {
Some(icon) => {
let image = ns_image(icon);
if !image.is_null() {
msg_void_size(
image,
sel("setSize:"),
CgSize {
width: 18.0,
height: 18.0,
},
);
msg_void_id(button, sel("setImage:"), image);
msg_void(image, sel("release"));
}
msg_void_id(button, sel("setTitle:"), nsstring(""));
}
None => {
msg_void_id(button, sel("setImage:"), std::ptr::null_mut());
let title = if tray.tooltip.is_empty() {
"\u{25CF}"
} else {
&tray.tooltip
};
msg_void_id(button, sel("setTitle:"), nsstring(title));
}
}
msg_void_id(button, sel("setToolTip:"), nsstring(&tray.tooltip));
let target = menu_target();
if tray.menu.is_empty() {
msg_void_id(item, sel("setMenu:"), std::ptr::null_mut());
msg_void_id(button, sel("setTarget:"), target);
let action: unsafe extern "C" fn(Id, Sel, Sel) =
std::mem::transmute(objc_msgSend as *const c_void);
action(button, sel("setAction:"), sel("stippleTrayAction:"));
} else {
let menu = ns_menu("");
fill_menu(menu, &tray.menu, target, false);
msg_void_id(item, sel("setMenu:"), menu);
msg_void(menu, sel("release"));
}
if std::env::var_os("STIPPLE_DEBUG_SHELL").is_some() {
let window = msg_id(button, sel("window"));
eprintln!(
"stipple shell: NSStatusItem button={:?} in a window={}",
button,
!window.is_null()
);
}
true
}
}
unsafe fn install_menu_bar(bar: &NativeMenuBar) {
let target = menu_target();
CTX.with(|c| {
c.borrow_mut()
.menu_states
.retain(|&tag, _| tag >= i64::from(TRAY_ID_BASE))
});
let name = &bar.app_name;
const CMD: u64 = 1 << 20;
const OPTION: u64 = 1 << 19;
unsafe {
let main = ns_menu("");
let app_menu = ns_menu(name);
let about = format!("About {name}");
add_item(
app_menu,
menu_item(&about, Some(sel("orderFrontStandardAboutPanel:")), "", 0),
);
msg_void_id(
app_menu,
sel("addItem:"),
msg_id(class("NSMenuItem"), sel("separatorItem")),
);
let hide = format!("Hide {name}");
add_item(app_menu, menu_item(&hide, Some(sel("hide:")), "h", CMD));
add_item(
app_menu,
menu_item(
"Hide Others",
Some(sel("hideOtherApplications:")),
"h",
CMD | OPTION,
),
);
add_item(
app_menu,
menu_item("Show All", Some(sel("unhideAllApplications:")), "", 0),
);
msg_void_id(
app_menu,
sel("addItem:"),
msg_id(class("NSMenuItem"), sel("separatorItem")),
);
let quit = format!("Quit {name}");
let quit = menu_item(&quit, Some(sel("stippleMenuAction:")), "q", CMD);
msg_void_id(quit, sel("setTarget:"), target);
msg_void_i64(quit, sel("setTag:"), MENU_QUIT_TAG);
add_item(app_menu, quit);
add_submenu(main, name, app_menu);
for menu in &bar.menus {
let title = mnemonic(&menu.label).0;
let m = ns_menu(&title);
fill_menu(m, &menu.items, target, false);
add_submenu(main, &title, m);
}
let app = msg_id(class("NSApplication"), sel("sharedApplication"));
msg_void_id(app, sel("setMainMenu:"), main);
msg_void(main, sel("release"));
}
}
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(),
);
class_addMethod(
cls,
sel("stippleAppearanceChanged:"),
appearance_changed 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(),
);
text_input::add_methods(cls);
dnd::add_methods(cls);
objc_registerClassPair(cls);
cls
}
}
#[derive(Debug)]
struct MacWindow {
size: PhysicalSize,
}
impl Window for MacWindow {
fn set_drop_accepted(&self, accepted: bool) {
CTX.with(|c| c.borrow_mut().drop_accepted = accepted);
}
fn start_drag(&self, data: &crate::platform::DropData) -> bool {
if !data.can_leave_app() {
return false;
}
CTX.with(|c| c.borrow_mut().drag_out = Some(data.clone()));
true
}
fn set_ime(&self, state: Option<&crate::platform::ImeState>) {
text_input::set_state(state);
}
fn id(&self) -> WindowId {
WindowId(1)
}
fn inner_size(&self) -> PhysicalSize {
self.size
}
fn scale_factor(&self) -> ScaleFactor {
ScaleFactor::new(CTX.with(|c| c.borrow().scale))
}
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) {
let window: Id = CTX.with(|c| c.borrow().window);
if !window.is_null() {
unsafe { msg_void_id(window, sel("setTitle:"), nsstring(title)) };
}
}
fn create_surface(&self) -> Box<dyn Surface> {
Box::new(MacSurface { size: self.size })
}
fn clipboard(&self) -> Option<String> {
crate::platform::clipboard::text()
}
fn set_clipboard(&self, text: &str) {
crate::platform::clipboard::set_text(text);
}
fn set_dark(&self, dark: bool) {
let window: Id = CTX.with(|c| c.borrow().window);
if !window.is_null() {
unsafe { set_window_dark(window, dark) };
}
}
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());
});
}
fn set_cursor(&self, cursor: CursorIcon) {
CTX.with(|c| c.borrow_mut().cursor = cursor);
unsafe { msg_void(ns_cursor(cursor), sel("set")) };
}
fn set_min_size(&self, size: Option<Size>) {
set_content_limit(sel("setContentMinSize:"), size, 0.0);
}
fn set_max_size(&self, size: Option<Size>) {
set_content_limit(sel("setContentMaxSize:"), size, f32::MAX as f64);
}
fn set_maximized(&self, maximized: bool) {
let window: Id = CTX.with(|c| c.borrow().window);
if !window.is_null() && unsafe { msg_bool(window, sel("isZoomed")) } != maximized {
unsafe { msg_void_id(window, sel("zoom:"), std::ptr::null_mut()) };
}
}
fn set_fullscreen(&self, fullscreen: bool) {
let window: Id = CTX.with(|c| c.borrow().window);
if window.is_null() {
return;
}
let now = unsafe { msg_u64(window, sel("styleMask")) } & NS_WINDOW_STYLE_FULLSCREEN != 0;
if now != fullscreen {
unsafe { msg_void_id(window, sel("toggleFullScreen:"), std::ptr::null_mut()) };
}
}
fn set_menu_bar(&self, menu: &NativeMenuBar) -> bool {
unsafe { install_menu_bar(menu) };
true
}
fn popup_menu(&self, menu: &NativeMenu, at: Point) -> MenuPopup {
unsafe { popup_menu(menu, at) }
}
fn set_tray(&self, tray: Option<&NativeTray>) -> bool {
unsafe { set_status_item(tray) }
}
fn set_icon(&self, icon: Option<&Pixmap>) {
unsafe {
let image = icon.map_or(std::ptr::null_mut(), |i| ns_image(i));
let app = msg_id(class("NSApplication"), sel("sharedApplication"));
msg_void_id(app, sel("setApplicationIconImage:"), image);
if !image.is_null() {
msg_void(image, sel("release"));
}
}
}
}
fn set_content_limit(setter: Sel, size: Option<Size>, unbounded: f64) {
let window: Id = CTX.with(|c| c.borrow().window);
if window.is_null() {
return;
}
let size = size.map_or(
CgSize {
width: unbounded,
height: unbounded,
},
|s| CgSize {
width: s.width,
height: s.height,
},
);
unsafe { msg_void_size(window, setter, size) };
}
#[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);
dnd::register(view);
msg_void_id(window, sel("setTitle:"), nsstring(&attrs.title));
if let Some(dark) = attrs.dark {
set_window_dark(window, dark);
}
observe_appearance(view);
msg_void_bool(window, sel("setReleasedWhenClosed:"), false);
msg_void_bool(window, sel("setAcceptsMouseMovedEvents:"), true);
msg_void_id(window, sel("setDelegate:"), view);
msg_id_id(window, sel("makeFirstResponder:"), view);
let scale = msg_f64(window, sel("backingScaleFactor"));
let size = ScaleFactor::new(scale).to_physical(attrs.logical_size);
CTX.with(|c| {
let mut c = c.borrow_mut();
c.view = view;
c.window = window;
c.scale = scale;
c.a11y_label = attrs.title.clone();
});
let mut win = MacWindow { size };
msg_void_u64(
window,
sel("setCollectionBehavior:"),
NS_COLLECTION_FULLSCREEN_PRIMARY,
);
win.set_min_size(attrs.min_size);
win.set_max_size(attrs.max_size);
if attrs.icon.is_some() {
win.set_icon(attrs.icon.as_ref());
}
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 number: i64 = {
let f: unsafe extern "C" fn(Id, Sel) -> i64 =
std::mem::transmute(objc_msgSend as *const c_void);
f(window, sel("windowNumber"))
};
println!("Cocoa window number: {number}");
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}");
println!("Cocoa ime: {}", text_input::self_check(view));
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"));
menu_check::start(window, view);
if attrs.maximized {
win.set_maximized(true);
}
if attrs.fullscreen {
win.set_fullscreen(true);
}
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;
let mut self_drop = std::env::var("STIPPLE_COCOA_DROP").ok();
crate::platform::wake::install(std::sync::Arc::new(post_wake_event), None);
display_link::install();
loop {
let pool = msg_id(msg_id(pool_cls, sel("alloc")), sel("init"));
let mut exit = false;
let until = match crate::platform::wake::timeout(std::time::Instant::now()) {
Some(t) => {
let secs = t.as_secs_f64();
let f: unsafe extern "C" fn(Id, Sel, f64) -> Id =
std::mem::transmute(objc_msgSend as *const c_void);
f(class("NSDate"), sel("dateWithTimeIntervalSinceNow:"), secs)
}
None => distant_future,
};
let ev = next(app, next_sel, u64::MAX, until, mode, true);
if let Some(path) = self_drop.take() {
let at = Point::new(100.0, 150.0);
println!("Cocoa drop: {}", dnd::self_drop(view, &path, at));
}
if !ev.is_null() && menu_check::step_event(ev) {
} else if !ev.is_null() {
let etype = msg_u64(ev, sel("type"));
let view_h = msg_rect(view, sel("bounds")).size.height;
let ime = if etype == NS_KEY_DOWN {
text_input::key_down(view, ev)
} else {
None
};
if let Some(events) = ime {
exit = events
.into_iter()
.any(|event| handler(event, &win) == ControlFlow::Exit);
} else {
if let Some(event) = translate_event(etype, ev, view_h)
&& handler(event, &win) == ControlFlow::Exit
{
exit = true;
}
if let Some(data) = CTX.with(|c| c.borrow_mut().drag_out.take())
&& !exit
&& etype == NS_LEFT_MOUSE_DRAGGED
{
dnd::drag_out::begin(view, ev, &data);
}
if !exit {
msg_void_id(app, sel("sendEvent:"), ev);
}
}
if !exit && etype == NS_MOUSE_MOVED {
let p = msg_point(ev, sel("locationInWindow"));
let b = msg_rect(view, sel("bounds"));
let over =
p.x >= 0.0 && p.y >= 0.0 && p.x < b.size.width && p.y < b.size.height;
let cursor = CTX.with(|c| c.borrow().cursor);
if over && cursor != CursorIcon::Default {
msg_void(ns_cursor(cursor), sel("set"));
}
}
}
let pending = CTX.with(|c| std::mem::take(&mut c.borrow_mut().pending));
for event in pending {
if !exit && handler(event, &win) == ControlFlow::Exit {
exit = true;
}
}
let pending = CTX.with(|c| std::mem::take(&mut c.borrow_mut().pending));
for event in pending {
if !exit && handler(event, &win) == ControlFlow::Exit {
exit = true;
}
}
if !exit
&& crate::platform::wake::take(std::time::Instant::now())
&& handler(Event::Wake, &win) == ControlFlow::Exit
{
exit = true;
}
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 scale = msg_f64(window, sel("backingScaleFactor"));
let now = PhysicalSize::new(
(b.size.width.max(1.0) * scale).round() as u32,
(b.size.height.max(1.0) * scale).round() as u32,
);
if now != last || scale != CTX.with(|c| c.borrow().scale) {
last = now;
win.size = now;
CTX.with(|c| c.borrow_mut().scale = scale);
handler(Event::Resized(now), &win);
}
msg_void(pool, sel("drain"));
}
}
Ok(())
}