#![cfg(target_os = "macos")]
#![cfg(feature = "macos")]
#![allow(unused_unsafe)]
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::sel;
use objc2::{define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSEventMask,
NSGraphicsContext, NSView, NSWindow, NSWindowStyleMask,
};
use objc2_core_graphics::CGContext;
use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSNotification, NSRect, NSSize, NSString};
use crate::core::{ObjectId, Point};
use crate::event::Event;
use crate::platform::types::MousePhase;
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
#[derive(Clone, Copy)]
struct NativePtr(*mut std::ffi::c_void);
unsafe impl Send for NativePtr {}
static NATIVE_VIEWS: LazyLock<Mutex<HashMap<u64, NativePtr>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static PENDING_NATIVE_RELEASES: LazyLock<Mutex<Vec<NativePtr>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
static SURFACE_VIEWS: LazyLock<Mutex<HashMap<u64, NativePtr>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn on_main_thread() -> bool {
MainThreadMarker::new().is_some()
}
pub(crate) fn store_native_view(widget_id: u64, view: *mut std::ffi::c_void) {
if view.is_null() {
return;
}
remove_native_view(widget_id);
unsafe {
let object = view as *mut AnyObject;
let _: *mut AnyObject = msg_send![object, retain];
}
NATIVE_VIEWS.lock().unwrap().insert(widget_id, NativePtr(view));
}
pub(crate) fn remove_native_view(widget_id: u64) {
let removed = NATIVE_VIEWS.lock().unwrap().remove(&widget_id);
if let Some(ptr) = removed {
unsafe {
let object = ptr.0 as *mut AnyObject;
let has_superview: bool =
msg_send![object, respondsToSelector: sel!(removeFromSuperview)];
if has_superview {
let _: () = msg_send![object, removeFromSuperview];
}
let _: () = msg_send![object, release];
}
}
}
unsafe fn object_is_window(object: *mut AnyObject) -> bool {
let window_class = <NSWindow as objc2::ClassType>::class();
let is_window: bool = msg_send![object, isKindOfClass: window_class];
is_window
}
pub(crate) enum NativeDestroyOutcome {
Destroyed,
NoNativeObject,
RefusedOffMainThread,
}
pub(crate) fn destroy_native_handle(widget_id: u64) -> NativeDestroyOutcome {
if MainThreadMarker::new().is_none() {
log::error!(
"[macos-objc2] destroy_handle: refused off the AppKit main thread (id={widget_id})"
);
return NativeDestroyOutcome::RefusedOffMainThread;
}
let removed = NATIVE_VIEWS.lock().unwrap().remove(&widget_id);
let Some(ptr) = removed else {
return NativeDestroyOutcome::NoNativeObject;
};
unsafe {
let object = ptr.0 as *mut AnyObject;
if object_is_window(object) {
let _: () = msg_send![object, close];
} else {
let _: () = msg_send![object, removeFromSuperview];
}
let _: () = msg_send![object, release];
}
NativeDestroyOutcome::Destroyed
}
fn release_closed_window(host_id: u64) {
if MainThreadMarker::new().is_none() {
log::error!(
"[macos-objc2] close_window: refused off the AppKit main thread (id={host_id})"
);
return;
}
let removed = NATIVE_VIEWS.lock().unwrap().remove(&host_id);
if let Some(ptr) = removed {
PENDING_NATIVE_RELEASES.lock().unwrap().push(ptr);
}
}
fn drain_pending_native_releases() {
let pending = std::mem::take(&mut *PENDING_NATIVE_RELEASES.lock().unwrap());
for ptr in pending {
unsafe {
let object = ptr.0 as *mut AnyObject;
let _: () = msg_send![object, release];
}
}
}
fn make_rect(x: i32, y: i32, width: u32, height: u32) -> NSRect {
NSRect::new(
objc2_foundation::NSPoint::new(x as f64, y as f64),
NSSize::new(width.max(1) as f64, height.max(1) as f64),
)
}
pub(crate) struct CanvasIvars {
widget_id: std::cell::Cell<u64>,
is_window: std::cell::Cell<bool>,
}
define_class!(
#[unsafe(super(NSView))]
#[thread_kind = MainThreadOnly]
#[name = "RustWidgetsObjc2CanvasView"]
#[ivars = CanvasIvars]
pub(crate) struct RustWidgetsObjc2CanvasView;
impl RustWidgetsObjc2CanvasView {
#[unsafe(method(drawRect:))]
fn draw_rect(&self, _dirty_rect: NSRect) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.blit_frame();
}));
if outcome.is_err() {
log::error!("[macos-objc2] canvas: panic while drawing a self-drawn widget");
}
}
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
false
}
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
true
}
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::Press);
}
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::Release);
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::Drag);
}
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::SecondaryPress);
}
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::SecondaryRelease);
}
#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, event: &NSEvent) {
self.forward_wheel(event);
}
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
self.forward_key(event, false);
}
#[unsafe(method(keyUp:))]
fn key_up(&self, event: &NSEvent) {
self.forward_key(event, true);
}
#[unsafe(method(setFrameSize:))]
fn set_frame_size(&self, new_size: NSSize) {
let _: () = unsafe { msg_send![super(self), setFrameSize: new_size] };
#[cfg(widgets_unstripped)]
if let Some(widget_id) = self.widget_target() {
crate::queue_resize_trigger(
widget_id,
new_size.width.round().max(1.0) as u32,
new_size.height.round().max(1.0) as u32,
);
}
}
#[unsafe(method(windowShouldClose:))]
fn window_should_close(&self, _window: &NSWindow) -> bool {
#[cfg(widgets_unstripped)]
if let Some(widget_id) = self.widget_target() {
let _ = crate::close_widget(widget_id);
}
true
}
#[unsafe(method(windowWillClose:))]
fn window_will_close(&self, _notification: &NSNotification) {
let host_id = self.ivars().widget_id.get();
if host_id != 0 {
release_closed_window(host_id);
}
}
}
);
impl RustWidgetsObjc2CanvasView {
fn blit_frame(&self) {
let widget_id = self.ivars().widget_id.get();
if widget_id == 0 {
log::error!("[macos-objc2] canvas: drawRect: on a view with no widget id");
return;
}
let bounds = self.bounds();
let width = bounds.size.width.round().max(1.0) as u32;
let height = bounds.size.height.round().max(1.0) as u32;
let clear = crate::core::Color::WHITE;
let size = crate::core::Size::new(width, height);
#[cfg(widgets_unstripped)]
let frame = if self.ivars().is_window.get() {
let Some(widget) = crate::widget::runtime::widget_id_for_host_window(widget_id) else {
log::error!(
"[macos-objc2] canvas: drawRect: host id={widget_id} has no owning widget; \
nothing to paint"
);
return;
};
crate::widget::runtime::render_frame_tree(widget, size, clear)
} else {
crate::widget::runtime::render_frame_cached(widget_id, size, clear)
};
#[cfg(widgets_unstripped)]
let Some(frame) = frame
else {
log::error!(
"[macos-objc2] canvas: drawRect: widget id={widget_id} produced no frame \
(unmounted, or it does not implement Draw)"
);
return;
};
#[cfg(not(widgets_unstripped))]
{
log::error!(
"[macos-objc2] canvas: drawRect: widget id={widget_id} cannot render in a \
profile without an unstripped widget set"
);
return;
}
let Some(context) = NSGraphicsContext::currentContext() else {
log::error!(
"[macos-objc2] canvas: drawRect: +[NSGraphicsContext currentContext] was nil"
);
return;
};
let cg_context = context.CGContext();
if !blit_rgba(&cg_context, width, height, &frame) {
log::error!("[macos-objc2] canvas: drawRect: blit failed for {width}x{height}");
}
}
pub(crate) fn new(
mtm: MainThreadMarker,
widget_id: u64,
is_window: bool,
frame: NSRect,
) -> Retained<Self> {
let this = mtm.alloc::<Self>().set_ivars(CanvasIvars {
widget_id: std::cell::Cell::new(widget_id),
is_window: std::cell::Cell::new(is_window),
});
unsafe { msg_send![super(this), initWithFrame: frame] }
}
#[cfg(widgets_unstripped)]
fn widget_target(&self) -> Option<ObjectId> {
let id = self.ivars().widget_id.get();
if id == 0 {
return None;
}
if self.ivars().is_window.get() {
crate::widget::runtime::widget_id_for_host_window(id)
} else {
Some(id)
}
}
#[cfg(widgets_unstripped)]
fn forward_mouse(&self, event: &NSEvent, phase: MousePhase) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Some(widget_id) = self.widget_target() else {
return;
};
let local = self.convertPoint_fromView(event.locationInWindow(), None);
let origin = self.frame().origin;
let position = Point::new(
(origin.x + local.x).round() as i32,
(origin.y + local.y).round() as i32,
);
let modifiers =
crate::platform::macos::types::map_modifiers(event.modifierFlags().bits() as u64);
let translated = match phase {
MousePhase::Press => Event::mouse_press_with(
position.x,
position.y,
crate::event::mouse_button::PRIMARY,
modifiers,
),
MousePhase::Release => Event::MouseRelease {
pos: position,
button: crate::event::mouse_button::PRIMARY,
},
MousePhase::Drag => Event::MouseMove { pos: position },
MousePhase::SecondaryPress => Event::mouse_press_with(
position.x,
position.y,
crate::event::mouse_button::SECONDARY,
modifiers,
),
MousePhase::SecondaryRelease => Event::MouseRelease {
pos: position,
button: crate::event::mouse_button::SECONDARY,
},
MousePhase::DoubleClick => Event::mouse_double_click(
position.x,
position.y,
crate::event::mouse_button::PRIMARY,
),
};
let delivered = crate::platform::platform_facts().route_pointer_event(
widget_id,
&translated,
position,
);
if !delivered {
return;
}
if matches!(
phase,
MousePhase::Press | MousePhase::SecondaryPress | MousePhase::DoubleClick
) {
if let Some(window) = self.window() {
window.makeFirstResponder(Some(self));
}
}
self.setNeedsDisplay(true);
}));
if outcome.is_err() {
log::error!("[macos-objc2] canvas: panic while forwarding a mouse event");
}
}
#[cfg(widgets_unstripped)]
fn forward_wheel(&self, event: &NSEvent) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Some(widget_id) = self.widget_target() else {
return;
};
let local = self.convertPoint_fromView(event.locationInWindow(), None);
let origin = self.frame().origin;
let position = Point::new(
(origin.x + local.x).round() as i32,
(origin.y + local.y).round() as i32,
);
let modifiers =
crate::platform::macos::types::map_modifiers(event.modifierFlags().bits() as u64);
let translated = Event::wheel(
event.scrollingDeltaX().round() as i32,
-(event.scrollingDeltaY().round() as i32),
modifiers,
);
if crate::platform::platform_facts().route_pointer_event(
widget_id,
&translated,
position,
) {
self.setNeedsDisplay(true);
}
}));
if outcome.is_err() {
log::error!("[macos-objc2] canvas: panic while forwarding a wheel event");
}
}
#[cfg(widgets_unstripped)]
fn forward_key(&self, event: &NSEvent, release: bool) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Some(widget_id) = self.widget_target() else {
return;
};
let key = event.keyCode() as u32;
let modifiers =
crate::platform::macos::types::map_modifiers(event.modifierFlags().bits() as u64);
if release {
let target = crate::widget::runtime::focused_widget().unwrap_or(widget_id);
if crate::widget::runtime::dispatch_event(
target,
&Event::KeyRelease { key, modifiers },
) {
self.setNeedsDisplay(true);
}
return;
}
let translated = if let Some(text) = printable_characters(event) {
Event::TextInput { text }
} else {
Event::KeyPress { key, modifiers }
};
let target = crate::widget::runtime::focused_widget().unwrap_or(widget_id);
if crate::widget::runtime::dispatch_event(target, &translated) {
self.setNeedsDisplay(true);
}
}));
if outcome.is_err() {
log::error!("[macos-objc2] canvas: panic while forwarding a key event");
}
}
}
#[cfg(widgets_unstripped)]
fn printable_characters(event: &NSEvent) -> Option<crate::compat::String> {
let characters = event.characters()?;
let text = characters.to_string();
let printable: crate::compat::String = text.chars().filter(|ch| !ch.is_control()).collect();
if printable.is_empty() {
None
} else {
Some(printable)
}
}
fn blit_rgba(context: &CGContext, width: u32, height: u32, frame: &[u8]) -> bool {
use objc2_core_graphics::{
CGBitmapContextCreate, CGBitmapContextCreateImage, CGBitmapContextGetBytesPerRow,
CGBitmapContextGetData, CGColorSpace, CGContext, CGImage, CGImageAlphaInfo,
CGImageByteOrderInfo, CGInterpolationQuality,
};
if width == 0 || height == 0 {
return false;
}
let expected = width as usize * height as usize * 4;
if frame.len() < expected {
log::error!(
"[macos-objc2] canvas: frame is {} bytes, need {} for {width}x{height}",
frame.len(),
expected
);
return false;
}
let Some(color_space) = CGColorSpace::new_device_rgb() else {
log::error!("[macos-objc2] canvas: CGColorSpaceCreateDeviceRGB returned null");
return false;
};
let bitmap_info = CGImageAlphaInfo::PremultipliedLast.0 | CGImageByteOrderInfo::Order32Little.0;
let Some(bitmap) = (unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
width as usize,
height as usize,
8,
width as usize * 4,
Some(&color_space),
bitmap_info,
)
}) else {
log::error!(
"[macos-objc2] canvas: CGBitmapContextCreate returned null for {width}x{height}"
);
return false;
};
let destination = unsafe { CGBitmapContextGetData(Some(&bitmap)) } as *mut u8;
if !destination.is_null() {
let stride = unsafe { CGBitmapContextGetBytesPerRow(Some(&bitmap)) };
for y in 0..height as usize {
for x in 0..width as usize {
let source = (y * width as usize + x) * 4;
let target = y * stride + x * 4;
let alpha = frame[source + 3] as u32;
unsafe {
*destination.add(target) = (frame[source] as u32 * alpha / 255) as u8;
*destination.add(target + 1) = (frame[source + 1] as u32 * alpha / 255) as u8;
*destination.add(target + 2) = (frame[source + 2] as u32 * alpha / 255) as u8;
*destination.add(target + 3) = alpha as u8;
}
}
}
}
let Some(image) = (unsafe { CGBitmapContextCreateImage(Some(&bitmap)) }) else {
log::error!("[macos-objc2] canvas: CGBitmapContextCreateImage returned null");
return false;
};
unsafe {
CGContext::set_interpolation_quality(Some(context), CGInterpolationQuality::None);
let image_width = CGImage::width(Some(&image)) as f64;
let image_height = CGImage::height(Some(&image)) as f64;
let rect = NSRect::new(
objc2_foundation::NSPoint::new(0.0, 0.0),
NSSize::new(image_width, image_height),
);
CGContext::draw_image(Some(context), rect, Some(&image));
}
true
}
pub(crate) fn create_ns_window(
mtm: MainThreadMarker,
widget_id: u64,
title: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<NSWindow> {
let style_mask = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable;
let rect = make_rect(x, y, width, height);
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
mtm.alloc(),
rect,
style_mask,
NSBackingStoreType::Buffered,
false,
)
};
window.setTitle(&NSString::from_str(title));
let canvas =
RustWidgetsObjc2CanvasView::new(mtm, widget_id, true, make_rect(0, 0, width, height));
window.setContentView(Some(&canvas));
let delegate_ptr = Retained::as_ptr(&canvas) as *mut AnyObject;
let _: () = unsafe { msg_send![&*window, setDelegate: delegate_ptr] };
window.makeKeyAndOrderFront(None);
window
}
pub(crate) fn mount_surface_native(
parent: u64,
id: u64,
x: i32,
y: i32,
width: u32,
height: u32,
) -> bool {
let Some(mtm) = MainThreadMarker::new() else {
log::error!(
"[macos-objc2] mount_surface: refused off the AppKit main thread (parent={parent}, id={id})"
);
return false;
};
let parent_ptr = match NATIVE_VIEWS.lock().unwrap().get(&parent).copied() {
Some(ptr) if unsafe { object_is_window(ptr.0 as *mut AnyObject) } => ptr.0 as *mut NSWindow,
Some(_) => {
log::error!(
"[macos-objc2] mount_surface: parent {parent} is not a window; a surface is mounted on\
a window's content view"
);
return false;
}
None => {
log::error!("[macos-objc2] mount_surface: unknown parent window {parent}");
return false;
}
};
let parent_window: &NSWindow = unsafe { &*parent_ptr };
let Some(content_view) = parent_window.contentView() else {
log::error!("[macos-objc2] mount_surface: window {parent} has no content view");
return false;
};
let frame = make_rect(x, y, width, height);
let canvas = RustWidgetsObjc2CanvasView::new(mtm, id, false, frame);
content_view.addSubview(&canvas);
canvas.setNeedsDisplay(true);
crate::notify_native_redraw(id, None);
SURFACE_VIEWS
.lock()
.unwrap()
.insert(id, NativePtr(Retained::as_ptr(&canvas) as *mut std::ffi::c_void));
drop(canvas);
log::debug!("[macos-objc2] mount_surface: id={id} mounted at ({x}, {y}, {width}, {height})");
true
}
pub(crate) fn resize_surface_native(id: u64, x: i32, y: i32, width: u32, height: u32) -> bool {
if MainThreadMarker::new().is_none() {
log::error!("[macos-objc2] resize_surface: refused off the AppKit main thread");
return false;
}
let Some(ptr) = surface_view(id) else {
log::error!("[macos-objc2] resize_surface: id={id} is not mounted");
return false;
};
unsafe {
let view: &NSView = &*(ptr as *const NSView);
view.setFrame(make_rect(x, y, width, height));
view.setNeedsDisplay(true);
}
true
}
pub(crate) fn invalidate_surface_native(id: u64) -> bool {
if MainThreadMarker::new().is_none() {
log::error!("[macos-objc2] invalidate_surface: refused off the AppKit main thread");
return false;
}
let Some(ptr) = surface_view(id) else {
return false;
};
unsafe {
let view: &NSView = &*(ptr as *const NSView);
view.setNeedsDisplay(true);
}
true
}
pub(crate) fn invalidate_window_native(id: u64) -> bool {
if MainThreadMarker::new().is_none() {
log::error!("[macos-objc2] invalidate_window: refused off the AppKit main thread");
return false;
}
#[cfg(widgets_unstripped)]
let host_id = crate::widget::runtime::host_window_for(id).unwrap_or(id);
#[cfg(not(widgets_unstripped))]
let host_id = id;
let window_ptr = match NATIVE_VIEWS.lock().unwrap().get(&host_id).copied() {
Some(ptr) => ptr.0 as *mut NSWindow,
None => return false,
};
unsafe {
let window: &NSWindow = &*window_ptr;
if let Some(content_view) = window.contentView() {
content_view.setNeedsDisplay(true);
}
}
true
}
pub(crate) fn unmount_surface_native(id: u64) -> bool {
if MainThreadMarker::new().is_none() {
log::error!("[macos-objc2] unmount_surface: refused off the AppKit main thread");
return false;
}
let removed = SURFACE_VIEWS.lock().unwrap().remove(&id);
let Some(ptr) = removed else {
log::error!("[macos-objc2] unmount_surface: id={id} is not mounted");
return false;
};
unsafe {
let view: &NSView = &*(ptr.0 as *const NSView);
view.removeFromSuperview();
}
true
}
fn surface_view(id: u64) -> Option<*mut NSView> {
SURFACE_VIEWS.lock().unwrap().get(&id).map(|ptr| ptr.0 as *mut NSView)
}
pub(crate) fn bootstrap_ns_application() -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
app.finishLaunching();
app.activate();
true
}
pub(crate) fn pump_native_event() -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
let app = NSApplication::sharedApplication(mtm);
let mode = unsafe { NSDefaultRunLoopMode };
let event = app.nextEventMatchingMask_untilDate_inMode_dequeue(
NSEventMask::Any,
Some(&NSDate::distantPast()),
mode,
true,
);
let dispatched = if let Some(event) = event {
app.sendEvent(&event);
true
} else {
false
};
drain_pending_native_releases();
dispatched
}