#![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::{
NSAccessibilityButtonRole, NSAccessibilityCheckBoxRole, NSAccessibilityComboBoxRole,
NSAccessibilityGroupRole, NSAccessibilityImageRole, NSAccessibilityIncrementorRole,
NSAccessibilityLinkRole, NSAccessibilityListRole, NSAccessibilityMenuItemRole,
NSAccessibilityMenuRole, NSAccessibilityOutlineRole, NSAccessibilityProgressIndicatorRole,
NSAccessibilityRadioButtonRole, NSAccessibilityRole, NSAccessibilitySliderRole,
NSAccessibilityStaticTextRole, NSAccessibilityTabGroupRole, NSAccessibilityTableRole,
NSAccessibilityTextFieldRole, NSAccessibilityUnknownRole, NSAccessibilityWindowRole,
NSAppearanceNameDarkAqua, NSApplication, NSApplicationActivationPolicy, NSBackingStoreType,
NSCursor, NSEventMask, NSScreen, NSView, NSWindow, NSWindowStyleMask, NSWorkspace,
};
#[cfg(widgets_unstripped)]
use objc2_app_kit::{NSEvent, NSGraphicsContext};
#[cfg(widgets_unstripped)]
use objc2_core_graphics::CGContext;
use objc2_foundation::{
NSArray, NSAttributedString, NSAttributedStringKey, NSDate, NSDefaultRunLoopMode, NSEdgeInsets,
NSLocale, NSNotification, NSRange, NSRect, NSSize, NSString,
};
use crate::compat::Vec;
static IME_BRIDGE: std::sync::OnceLock<crate::platform::ime_macos::MacOsImeBridge> =
std::sync::OnceLock::new();
pub(crate) fn ime_bridge() -> &'static crate::platform::ime_macos::MacOsImeBridge {
IME_BRIDGE.get_or_init(crate::platform::ime_macos::MacOsImeBridge::new)
}
thread_local! {
static IME_INTERPRETING: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
static IME_HANDLED: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
#[cfg(widgets_unstripped)]
use crate::core::{ObjectId, Point};
#[cfg(widgets_unstripped)]
use crate::event::Event;
#[cfg(widgets_unstripped)]
use crate::platform::types::MousePhase;
use crate::core::Rect;
use crate::platform::types::{Display, DisplayId, SafeAreaInsets};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
pub(crate) fn system_theme() -> Option<bool> {
let mtm = MainThreadMarker::new()?;
let app = NSApplication::sharedApplication(mtm);
let appearance = app.effectiveAppearance();
let name = appearance.name();
let dark_name = unsafe { NSAppearanceNameDarkAqua };
Some(&*name == dark_name)
}
#[allow(deprecated)] pub(crate) fn set_cursor(icon: crate::platform::CursorIcon) -> bool {
use crate::platform::CursorIcon;
if MainThreadMarker::new().is_none() {
return false;
}
let cursor = match icon {
CursorIcon::Default => NSCursor::arrowCursor(),
CursorIcon::Pointer | CursorIcon::Help => NSCursor::pointingHandCursor(),
CursorIcon::Text => NSCursor::IBeamCursor(),
CursorIcon::Crosshair | CursorIcon::ZoomIn | CursorIcon::ZoomOut => {
NSCursor::crosshairCursor()
}
CursorIcon::Move | CursorIcon::Grab => NSCursor::openHandCursor(),
CursorIcon::Grabbing => NSCursor::closedHandCursor(),
CursorIcon::NotAllowed => NSCursor::operationNotAllowedCursor(),
CursorIcon::ResizeHorizontal | CursorIcon::ResizeNwSe | CursorIcon::ResizeNeSw => {
NSCursor::resizeLeftRightCursor()
}
CursorIcon::ResizeVertical => NSCursor::resizeUpDownCursor(),
CursorIcon::Wait | CursorIcon::Progress => NSCursor::arrowCursor(),
};
cursor.set();
true
}
pub(crate) fn set_cursor_visible(visible: bool) -> bool {
if MainThreadMarker::new().is_none() {
return false;
}
if visible {
NSCursor::arrowCursor().set();
} else {
NSCursor::hide();
}
true
}
pub(crate) fn system_fonts() -> Vec<crate::platform::types::SystemFont> {
crate::platform::fonts_apple::system_fonts()
}
pub(crate) fn high_contrast() -> bool {
NSWorkspace::sharedWorkspace().accessibilityDisplayShouldIncreaseContrast()
}
pub(crate) fn reduce_motion() -> bool {
NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceMotion()
}
pub(crate) fn system_locale() -> Option<&'static str> {
let identifier = NSLocale::currentLocale().localeIdentifier().to_string();
crate::platform::locale::intern(Some(identifier.replace('_', "-")))
.or_else(|| crate::platform::locale::intern(crate::platform::locale::from_environment()))
}
fn rect_of(rect: NSRect) -> Rect {
Rect::new(
rect.origin.x.round() as i32,
rect.origin.y.round() as i32,
rect.size.width.max(0.0).round() as u32,
rect.size.height.max(0.0).round() as u32,
)
}
fn display_number(screen: &NSScreen) -> Option<u64> {
let description = screen.deviceDescription();
let key = NSString::from_str("NSScreenNumber");
let value = description.objectForKey(&key)?;
let number: u32 = unsafe { msg_send![&*value, unsignedIntValue] };
Some(u64::from(number))
}
fn display_id_at(index: usize, screen: &NSScreen) -> u64 {
display_number(screen).unwrap_or(index as u64)
}
pub(crate) fn displays() -> Vec<Display> {
let Some(mtm) = MainThreadMarker::new() else {
return Vec::new();
};
let screens = NSScreen::screens(mtm);
let main_screen = NSScreen::mainScreen(mtm);
let mut out = Vec::new();
for (index, screen) in screens.iter().enumerate() {
let frame = screen.frame();
let visible = screen.visibleFrame();
let scale = screen.backingScaleFactor();
let is_primary = main_screen.as_ref().is_some_and(|main| {
core::ptr::eq(objc2::rc::Retained::as_ptr(main), objc2::rc::Retained::as_ptr(&screen))
});
out.push(Display {
id: DisplayId::new(display_id_at(index, &screen)),
bounds: rect_of(frame),
work_area: rect_of(visible),
scale: if scale > 0.0 { scale as f32 } else { 1.0 },
is_primary,
});
}
out
}
pub(crate) fn main_scale() -> Option<f32> {
let mtm = MainThreadMarker::new()?;
let screen = NSScreen::mainScreen(mtm)?;
let scale = screen.backingScaleFactor();
if scale > 0.0 {
Some(scale as f32)
} else {
None
}
}
fn native_ptr(window_id: u64) -> Option<*mut AnyObject> {
NATIVE_VIEWS.lock().ok()?.get(&window_id).map(|ptr| ptr.0 as *mut AnyObject)
}
unsafe fn is_window(object: *mut AnyObject) -> bool {
let window_class = <NSWindow as objc2::ClassType>::class();
let result: bool = msg_send![object, isKindOfClass: window_class];
result
}
pub(crate) fn screen_id_of_window(window_id: u64) -> Option<u64> {
let _mtm = MainThreadMarker::new()?;
let object = native_ptr(window_id)?;
if !unsafe { is_window(object) } {
return None;
}
let window: &NSWindow = unsafe { &*(object as *const NSWindow) };
let screen = window.screen()?;
let index = NSScreen::screens(_mtm).iter().position(|candidate| {
core::ptr::eq(objc2::rc::Retained::as_ptr(&candidate), objc2::rc::Retained::as_ptr(&screen))
})?;
Some(display_id_at(index, &screen))
}
fn window_ref(window_id: u64) -> Option<Retained<NSWindow>> {
let _mtm = MainThreadMarker::new()?;
let object = native_ptr(window_id)?;
unsafe {
if !is_window(object) {
return None;
}
Retained::retain(object as *mut NSWindow)
}
}
pub(crate) fn set_window_state(
window_id: u64,
flag: crate::platform::WindowStateFlag,
on: bool,
) -> bool {
use crate::platform::WindowStateFlag;
let Some(window) = window_ref(window_id) else {
return false;
};
match flag {
WindowStateFlag::Maximized => {
if on != window.isZoomed() {
window.zoom(None);
}
}
WindowStateFlag::Minimized => {
if on && !window.isMiniaturized() {
window.performMiniaturize(None);
} else if !on && window.isMiniaturized() {
window.deminiaturize(None);
}
}
WindowStateFlag::Fullscreen => {
let fullscreen = window.styleMask().contains(NSWindowStyleMask::FullScreen);
if on != fullscreen {
window.toggleFullScreen(None);
}
}
WindowStateFlag::Resizable => {
let mask = window.styleMask();
let mut updated = mask;
updated.set(NSWindowStyleMask::Resizable, on);
if updated != mask {
window.setStyleMask(updated);
}
}
WindowStateFlag::Decorated => {
let mask = window.styleMask();
let mut updated = mask;
let chrome = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable;
if on {
updated.insert(chrome);
} else {
updated.remove(chrome);
}
if updated != mask {
window.setStyleMask(updated);
}
}
}
true
}
pub(crate) fn window_state(window_id: u64, flag: crate::platform::WindowStateFlag) -> Option<bool> {
use crate::platform::WindowStateFlag;
let window = window_ref(window_id)?;
let mask = window.styleMask();
Some(match flag {
WindowStateFlag::Maximized => window.isZoomed(),
WindowStateFlag::Minimized => window.isMiniaturized(),
WindowStateFlag::Fullscreen => mask.contains(NSWindowStyleMask::FullScreen),
WindowStateFlag::Resizable => mask.contains(NSWindowStyleMask::Resizable),
WindowStateFlag::Decorated => mask.contains(NSWindowStyleMask::Titled),
})
}
pub(crate) fn set_window_min_size(window_id: u64, width: u32, height: u32) -> bool {
let Some(window) = window_ref(window_id) else {
return false;
};
window.setContentMinSize(NSSize::new(width as f64, height as f64));
true
}
pub(crate) fn window_min_size(window_id: u64) -> Option<(u32, u32)> {
let window = window_ref(window_id)?;
let size = window.contentMinSize();
if size.width > 0.0 && size.height > 0.0 {
Some((size.width.round() as u32, size.height.round() as u32))
} else {
None
}
}
pub(crate) fn window_client_size(window_id: u64) -> Option<(u32, u32)> {
let window = window_ref(window_id)?;
let view = window.contentView()?;
let frame = view.frame();
let width = frame.size.width.max(0.0).round() as u32;
let height = frame.size.height.max(0.0).round() as u32;
(width > 0 && height > 0).then_some((width, height))
}
pub(crate) fn native_handle(window_id: u64) -> Option<usize> {
native_ptr(window_id).map(|ptr| ptr as usize)
}
#[cfg(feature = "raw-window-handle")]
pub(crate) fn raw_view_handle(window_id: u64) -> Option<*mut core::ffi::c_void> {
let window = window_ref(window_id)?;
let view = window.contentView()?;
Some(objc2::rc::Retained::as_ptr(&view) as *mut core::ffi::c_void)
}
pub(crate) fn safe_area_insets(window_id: u64) -> Option<SafeAreaInsets> {
let _mtm = MainThreadMarker::new()?;
let object = native_ptr(window_id)?;
let view = unsafe {
if is_window(object) {
let window: &NSWindow = &*(object as *const NSWindow);
let content = window.contentView()?;
objc2::rc::Retained::as_ptr(&content) as *mut AnyObject
} else {
object
}
};
let responds: bool = unsafe { msg_send![view, respondsToSelector: sel!(safeAreaInsets)] };
if !responds {
return None;
}
let insets: NSEdgeInsets = unsafe { msg_send![view, safeAreaInsets] };
Some(SafeAreaInsets {
top: logical_pixels(insets.top),
bottom: logical_pixels(insets.bottom),
left: logical_pixels(insets.left),
right: logical_pixels(insets.right),
})
}
fn logical_pixels(value: f64) -> u32 {
value.max(0.0).round() as u32
}
#[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 !NATIVE_VIEWS.lock().unwrap().contains_key(&widget_id) {
return NativeDestroyOutcome::NoNativeObject;
}
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>,
#[cfg_attr(not(widgets_unstripped), allow(dead_code))]
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(isAccessibilityElement))]
fn is_accessibility_element(&self) -> bool {
self.ivars().widget_id.get() != 0
}
#[unsafe(method(accessibilityRole))]
fn accessibility_role(&self) -> Option<&'static NSAccessibilityRole> {
Some(ns_accessibility_role_for(self.accessibility_role_of_widget()))
}
#[unsafe(method(accessibilityLabel))]
fn accessibility_label(&self) -> *mut NSString {
let label = self.accessibility_label_of_widget();
objc2::rc::Retained::autorelease_return(NSString::from_str(&label))
}
#[unsafe(method(accessibilityFrame))]
fn accessibility_frame(&self) -> NSRect {
let bounds = self.bounds();
self.window().map(|window| window.convertRectToScreen(bounds)).unwrap_or(bounds)
}
#[unsafe(method(hasMarkedText))]
fn has_marked_text(&self) -> bool {
ime_bridge().has_marked_text()
}
#[unsafe(method(markedRange))]
fn marked_range(&self) -> NSRange {
match ime_bridge().get_marked_text() {
Some(text) => NSRange::new(0, text.encode_utf16().count()),
None => NSRange::new(usize::MAX, 0),
}
}
#[unsafe(method(selectedRange))]
fn selected_range(&self) -> NSRange {
let length =
ime_bridge().get_marked_text().map_or(0, |text| text.encode_utf16().count());
NSRange::new(length, 0)
}
#[unsafe(method(validAttributesForMarkedText))]
fn valid_attributes_for_marked_text(&self) -> *mut NSArray<NSAttributedStringKey> {
objc2::rc::Retained::autorelease_return(NSArray::from_slice(&[]))
}
#[unsafe(method(attributedSubstringForProposedRange:actualRange:))]
fn attributed_substring(
&self,
_range: NSRange,
actual_range: *mut NSRange,
) -> *mut NSAttributedString {
if !actual_range.is_null() {
unsafe { *actual_range = NSRange::new(0, 0) };
}
core::ptr::null_mut()
}
#[unsafe(method(firstRectForCharacterRange:actualRange:))]
fn first_rect_for_character_range(
&self,
range: NSRange,
actual_range: *mut NSRange,
) -> NSRect {
if !actual_range.is_null() {
unsafe { *actual_range = range };
}
let rect = NSRect::new(
objc2_foundation::NSPoint::new(0.0, 0.0),
NSSize::new(1.0, 1.0),
);
self.window().map(|window| window.convertRectToScreen(rect)).unwrap_or(rect)
}
#[unsafe(method(characterIndexForPoint:))]
fn character_index_for_point(&self, _point: objc2_foundation::NSPoint) -> usize {
0
}
#[unsafe(method(unmarkText))]
fn unmark_text(&self) {
ime_bridge().discard_marked_text();
self.setNeedsDisplay(true);
}
#[unsafe(method(setMarkedText:selectedRange:replacementRange:))]
fn set_marked_text(
&self,
string: &AnyObject,
selected_range: NSRange,
_replacement_range: NSRange,
) {
let Some(text) = text_of(string) else { return };
if IME_INTERPRETING.get() {
IME_HANDLED.set(true);
}
ime_bridge().set_marked_text(
&text,
selected_range.location as i32,
selected_range.location.saturating_add(selected_range.length) as i32,
);
self.setNeedsDisplay(true);
}
#[unsafe(method(insertText:replacementRange:))]
fn insert_text(&self, string: &AnyObject, _replacement_range: NSRange) {
let Some(text) = text_of(string) else { return };
if IME_INTERPRETING.get() {
IME_HANDLED.set(true);
}
let bridge = ime_bridge();
if bridge.has_marked_text() {
bridge.commit_text(&text);
} else {
#[cfg(widgets_unstripped)]
{
let _ = self.forward_ime_text(&text);
}
}
self.setNeedsDisplay(true);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &NSEvent) {
if event.clickCount() >= 2 {
self.forward_mouse(event, MousePhase::DoubleClick);
return;
}
self.forward_mouse(event, MousePhase::Press);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::Release);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::Drag);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::SecondaryPress);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MousePhase::SecondaryRelease);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, event: &NSEvent) {
self.forward_wheel(event);
}
#[cfg(widgets_unstripped)]
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
IME_INTERPRETING.set(true);
IME_HANDLED.set(false);
let events = NSArray::from_slice(&[event]);
self.interpretKeyEvents(&events);
let handled = IME_HANDLED.get();
IME_INTERPRETING.set(false);
if !handled {
self.forward_key(event, false);
}
}
#[cfg(widgets_unstripped)]
#[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;
}
#[cfg(widgets_unstripped)]
{
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);
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)
};
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;
};
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}");
}
}
#[cfg(not(widgets_unstripped))]
{
log::error!(
"[macos-objc2] canvas: drawRect: widget id={widget_id} cannot render in a \
profile without an unstripped widget set"
);
}
}
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::accelerator::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 | MousePhase::Move => 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::accelerator::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 accessibility_role_of_widget(&self) -> crate::platform::accessibility::A11yRole {
#[cfg(widgets_unstripped)]
if let Some(id) = self.widget_target() {
if let Some(role) = crate::widget::runtime::with_widget(id, |widget| {
crate::platform::accessibility::A11yRole::from(widget.kind())
}) {
return role;
}
}
crate::platform::accessibility::A11yRole::Unknown
}
fn accessibility_label_of_widget(&self) -> crate::compat::String {
#[cfg(widgets_unstripped)]
if let Some(id) = self.widget_target() {
if let Some(name) =
crate::widget::runtime::with_widget(id, |widget| widget.accessible_name())
{
return name;
}
}
crate::compat::String::new()
}
#[cfg(widgets_unstripped)]
fn forward_ime_text(&self, text: &str) -> bool {
let Some(widget_id) = self.widget_target() else { return false };
let target = crate::widget::runtime::focused_widget().unwrap_or(widget_id);
if crate::widget::runtime::dispatch_event(
target,
&Event::TextInput { text: text.to_string() },
) {
true
} else {
log::debug!("[macos-objc2] IME text dropped, id={target} is not mounted");
false
}
}
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::accelerator::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");
}
}
}
fn ns_accessibility_role_for(
role: crate::platform::accessibility::A11yRole,
) -> &'static NSAccessibilityRole {
use crate::platform::accessibility::A11yRole;
unsafe {
match role {
A11yRole::Button | A11yRole::Switch => NSAccessibilityButtonRole,
A11yRole::Label | A11yRole::Heading | A11yRole::Paragraph => {
NSAccessibilityStaticTextRole
}
A11yRole::TextField => NSAccessibilityTextFieldRole,
A11yRole::CheckBox => NSAccessibilityCheckBoxRole,
A11yRole::RadioButton => NSAccessibilityRadioButtonRole,
A11yRole::Slider => NSAccessibilitySliderRole,
A11yRole::ProgressBar => NSAccessibilityProgressIndicatorRole,
A11yRole::List => NSAccessibilityListRole,
A11yRole::Table => NSAccessibilityTableRole,
A11yRole::Image => NSAccessibilityImageRole,
A11yRole::Link => NSAccessibilityLinkRole,
A11yRole::Group => NSAccessibilityGroupRole,
A11yRole::Window => NSAccessibilityWindowRole,
A11yRole::Dialog | A11yRole::Alert => NSAccessibilityWindowRole,
A11yRole::Menu => NSAccessibilityMenuRole,
A11yRole::MenuItem => NSAccessibilityMenuItemRole,
A11yRole::Tab => NSAccessibilityTabGroupRole,
A11yRole::ComboBox => NSAccessibilityComboBoxRole,
A11yRole::SpinButton => NSAccessibilityIncrementorRole,
A11yRole::StatusBar => NSAccessibilityGroupRole,
A11yRole::ToolTip => NSAccessibilityGroupRole,
A11yRole::Tree => NSAccessibilityOutlineRole,
A11yRole::Unknown => NSAccessibilityUnknownRole,
}
}
}
fn text_of(object: &AnyObject) -> Option<crate::compat::String> {
if let Some(text) = object.downcast_ref::<NSString>() {
return Some(text.to_string());
}
if let Some(text) = object.downcast_ref::<NSAttributedString>() {
return Some(text.string().to_string());
}
None
}
#[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)
}
}
#[cfg(widgets_unstripped)]
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));
crate::platform::accessibility::macos_objc2::bridge()
.register_handle(widget_id, Retained::as_ptr(&canvas) as *mut AnyObject as usize);
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);
crate::platform::accessibility::macos_objc2::bridge()
.register_handle(id, Retained::as_ptr(&canvas) as *mut AnyObject as usize);
content_view.addSubview(&canvas);
canvas.setNeedsDisplay(true);
#[cfg(widgets_unstripped)]
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
}