use std::cell::{Cell, RefCell};
use std::ffi::c_void;
use std::ffi::CString;
use std::ops::{Deref, DerefMut};
use std::panic::{self, AssertUnwindSafe};
use std::rc::Rc;
use objc2::declare::ClassBuilder;
use objc2::encode::Encoding;
use objc2::ffi::objc_disposeClassPair;
use objc2::rc::{autoreleasepool, Allocated, Retained};
use objc2::runtime::{AnyClass, AnyObject, Bool, MessageReceiver, Sel};
use objc2::{class, msg_send, sel, AnyThread, ClassType, Message, RefEncode};
use objc2_app_kit::{
NSBackingStoreType, NSCursor, NSEvent, NSScreen, NSTrackingArea, NSTrackingAreaOptions, NSView,
NSWindow, NSWindowStyleMask,
};
use objc2_foundation::{NSInteger, NSPoint, NSRect, NSSize, NSString};
use objc2_quartz_core::{kCAFilterNearest, kCAGravityBottomLeft, CALayer};
use super::surface::Surface;
use super::OsError;
use crate::{
Bitmap, Cursor, Error, Event, EventLoop, MouseButton, Point, RawWindow, Rect, Response, Result,
Size, WindowOptions,
};
fn class_name() -> CString {
use std::fmt::Write;
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes).unwrap();
let mut name = "window-".to_string();
for byte in bytes {
write!(&mut name, "{:x}", byte).unwrap();
}
CString::new(name).unwrap()
}
fn mouse_button_from_number(button_number: NSInteger) -> Option<MouseButton> {
match button_number {
0 => Some(MouseButton::Left),
1 => Some(MouseButton::Right),
2 => Some(MouseButton::Middle),
3 => Some(MouseButton::Back),
4 => Some(MouseButton::Forward),
_ => None,
}
}
fn set_contents_opaque(layer: &CALayer, contents_opaque: bool) {
unsafe {
let () = msg_send![layer, setContentsOpaque: contents_opaque];
}
}
fn set_contents_changed(layer: &CALayer) {
unsafe {
let () = msg_send![layer, setContentsChanged];
}
}
#[repr(C)]
pub struct View {
superclass: NSView,
}
unsafe impl RefEncode for View {
const ENCODING_REF: Encoding = NSView::ENCODING_REF;
}
unsafe impl Message for View {}
impl Deref for View {
type Target = NSView;
fn deref(&self) -> &Self::Target {
&self.superclass
}
}
impl DerefMut for View {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.superclass
}
}
impl View {
pub fn register_class() -> Result<&'static AnyClass> {
let name = class_name();
let Some(mut builder) = ClassBuilder::new(&name, class!(NSView)) else {
return Err(Error::Os(OsError::Other(
"could not declare NSView subclass",
)));
};
builder.add_ivar::<Cell<*mut c_void>>(c"windowState");
unsafe {
builder.add_method(
sel!(acceptsFirstMouse:),
Self::accepts_first_mouse as unsafe extern "C" fn(_, _, _) -> _,
);
builder.add_method(
sel!(isFlipped),
Self::is_flipped as unsafe extern "C" fn(_, _) -> _,
);
builder.add_method(
sel!(mouseEntered:),
Self::mouse_entered as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(mouseExited:),
Self::mouse_exited as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(mouseMoved:),
Self::mouse_moved as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(mouseDragged:),
Self::mouse_moved as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(rightMouseDragged:),
Self::mouse_moved as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(otherMouseDragged:),
Self::mouse_moved as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(mouseDown:),
Self::mouse_down as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(mouseUp:),
Self::mouse_up as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(rightMouseDown:),
Self::right_mouse_down as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(rightMouseUp:),
Self::right_mouse_up as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(otherMouseDown:),
Self::other_mouse_down as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(otherMouseUp:),
Self::other_mouse_up as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(scrollWheel:),
Self::scroll_wheel as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(cursorUpdate:),
Self::cursor_update as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(windowShouldClose:),
Self::window_should_close as unsafe extern "C" fn(_, _, _) -> _,
);
builder.add_method(sel!(dealloc), View::dealloc as unsafe extern "C" fn(_, _));
}
Ok(builder.register())
}
pub unsafe fn unregister_class(class: &'static AnyClass) {
objc_disposeClassPair(class as *const AnyClass as *mut AnyClass);
}
fn state_ivar(&self) -> &Cell<*mut c_void> {
let ivar = self.class().instance_variable(c"windowState").unwrap();
unsafe { ivar.load::<Cell<*mut c_void>>(self) }
}
fn state(&self) -> &WindowState {
unsafe { &*(self.state_ivar().get() as *const WindowState) }
}
fn catch_unwind<F: FnOnce()>(&self, f: F) {
let result = panic::catch_unwind(AssertUnwindSafe(f));
if let Err(panic) = result {
self.state().event_loop.state.propagate_panic(panic);
}
}
pub fn retain(&self) -> Retained<View> {
unsafe { Retained::retain(self as *const View as *mut View) }.unwrap()
}
unsafe extern "C" fn accepts_first_mouse(&self, _: Sel, _event: Option<&NSEvent>) -> Bool {
Bool::YES
}
unsafe extern "C" fn is_flipped(&self, _: Sel) -> Bool {
Bool::YES
}
unsafe extern "C" fn mouse_entered(&self, _: Sel, _event: Option<&NSEvent>) {
self.catch_unwind(|| {
self.state().handle_event(Event::MouseEnter);
});
}
unsafe extern "C" fn mouse_exited(&self, _: Sel, _event: Option<&NSEvent>) {
self.catch_unwind(|| {
self.state().handle_event(Event::MouseExit);
});
}
unsafe extern "C" fn mouse_moved(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let Some(event) = event else {
return;
};
let point = self.convertPoint_fromView(event.locationInWindow(), None);
self.state().handle_event(Event::MouseMove(Point {
x: point.x,
y: point.y,
}));
});
}
unsafe extern "C" fn mouse_down(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let result = self.state().handle_event(Event::MouseDown(MouseButton::Left));
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), mouseDown: event];
}
});
}
unsafe extern "C" fn mouse_up(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let result = self.state().handle_event(Event::MouseUp(MouseButton::Left));
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), mouseUp: event];
}
});
}
unsafe extern "C" fn right_mouse_down(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let result = self.state().handle_event(Event::MouseDown(MouseButton::Right));
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), rightMouseDown: event];
}
});
}
unsafe extern "C" fn right_mouse_up(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let result = self.state().handle_event(Event::MouseUp(MouseButton::Right));
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), rightMouseUp: event];
}
});
}
unsafe extern "C" fn other_mouse_down(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let Some(event) = event else {
return;
};
let button_number = event.buttonNumber();
let result = if let Some(button) = mouse_button_from_number(button_number) {
self.state().handle_event(Event::MouseDown(button))
} else {
None
};
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), otherMouseDown: event];
}
});
}
unsafe extern "C" fn other_mouse_up(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let Some(event) = event else {
return;
};
let button_number = event.buttonNumber();
let result = if let Some(button) = mouse_button_from_number(button_number) {
self.state().handle_event(Event::MouseUp(button))
} else {
None
};
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), otherMouseUp: event];
}
});
}
unsafe extern "C" fn scroll_wheel(&self, _: Sel, event: Option<&NSEvent>) {
self.catch_unwind(|| {
let Some(event) = event else {
return;
};
let dx = event.scrollingDeltaX();
let dy = event.scrollingDeltaY();
let delta = if event.hasPreciseScrollingDeltas() {
Point::new(dx, dy)
} else {
Point::new(32.0 * dx, 32.0 * dy)
};
let result = self.state().handle_event(Event::Scroll(delta));
if result != Some(Response::Capture) {
let () = msg_send![super(self, NSView::class()), scrollWheel: event];
}
});
}
unsafe extern "C" fn cursor_update(&self, _: Sel, _event: Option<&NSEvent>) {
self.catch_unwind(|| {
self.state().update_cursor();
});
}
unsafe extern "C" fn window_should_close(&self, _: Sel, _sender: &NSWindow) -> Bool {
self.catch_unwind(|| {
self.state().handle_event(Event::Close);
});
Bool::NO
}
unsafe extern "C" fn dealloc(this: *mut Self, _: Sel) {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
drop(Rc::from_raw(
(*this).state_ivar().get() as *const WindowState
));
}));
if let Err(_panic) = result {
std::process::abort();
}
let () = msg_send![super(this, NSView::class()), dealloc];
}
}
pub struct WindowState {
view: RefCell<Option<Retained<View>>>,
window: RefCell<Option<Retained<NSWindow>>>,
layer: RefCell<Option<Retained<CALayer>>>,
surface: RefCell<Option<Surface>>,
cursor: Cell<Cursor>,
event_loop: EventLoop,
handler: RefCell<Box<dyn FnMut(Event) -> Response>>,
}
impl WindowState {
pub fn view(&self) -> Option<Retained<View>> {
self.view.borrow().as_ref().map(|view| view.retain())
}
pub fn window(&self) -> Option<Retained<NSWindow>> {
self.window.borrow().clone()
}
pub fn handle_event(&self, event: Event) -> Option<Response> {
let mut handler = self.handler.try_borrow_mut().ok()?;
Some(handler(event))
}
fn update_cursor(&self) {
fn try_get_cursor(selector: Sel) -> Retained<NSCursor> {
unsafe {
let class = NSCursor::class();
if objc2::msg_send![class, respondsToSelector: selector] {
let cursor: *mut NSCursor = class.send_message(selector, ());
if let Some(cursor) = Retained::retain(cursor) {
return cursor;
}
}
NSCursor::arrowCursor()
}
}
let cursor = self.cursor.get();
let ns_cursor = match cursor {
Cursor::Arrow => NSCursor::arrowCursor(),
Cursor::Crosshair => NSCursor::crosshairCursor(),
Cursor::Hand => NSCursor::pointingHandCursor(),
Cursor::IBeam => NSCursor::IBeamCursor(),
Cursor::No => NSCursor::operationNotAllowedCursor(),
Cursor::SizeNs => try_get_cursor(sel!(_windowResizeNorthSouthCursor)),
Cursor::SizeWe => try_get_cursor(sel!(_windowResizeEastWestCursor)),
Cursor::SizeNesw => try_get_cursor(sel!(_windowResizeNorthEastSouthWestCursor)),
Cursor::SizeNwse => try_get_cursor(sel!(_windowResizeNorthWestSouthEastCursor)),
Cursor::Wait => try_get_cursor(sel!(_waitCursor)),
Cursor::None => self.event_loop.state.empty_cursor.clone(),
};
ns_cursor.set();
}
pub fn open<F>(
options: &WindowOptions,
event_loop: &EventLoop,
handler: F,
) -> Result<Rc<WindowState>>
where
F: FnMut(Event) -> Response + 'static,
{
autoreleasepool(|_| {
let event_loop_state = &event_loop.state;
let parent_view = if let Some(parent) = options.parent {
if let RawWindow::AppKit(parent_view) = parent {
Some(parent_view as *const NSView)
} else {
return Err(Error::InvalidWindowHandle);
}
} else {
None
};
let origin = options.position.unwrap_or(Point::new(0.0, 0.0));
let frame = NSRect::new(
NSPoint::new(origin.x, origin.y),
NSSize::new(options.size.width, options.size.height),
);
let state = Rc::new(WindowState {
view: RefCell::new(None),
window: RefCell::new(None),
layer: RefCell::new(None),
surface: RefCell::new(None),
cursor: Cell::new(Cursor::Arrow),
event_loop: event_loop.clone(),
handler: RefCell::new(Box::new(handler)),
});
let view: Allocated<View> = unsafe { msg_send![event_loop_state.class, alloc] };
let view: Retained<View> = unsafe { msg_send![view, initWithFrame: frame] };
view.state_ivar().set(Rc::into_raw(Rc::clone(&state)) as *mut c_void);
state.view.replace(Some(view.retain()));
let tracking_options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::CursorUpdate
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect
| NSTrackingAreaOptions::EnabledDuringMouseDrag;
unsafe {
let tracking_area = NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0)),
tracking_options,
Some(&view),
None,
);
view.addTrackingArea(&tracking_area);
}
if let Some(parent_view) = parent_view {
unsafe {
view.setHidden(true);
(*parent_view).addSubview(&view);
}
} else {
let origin = options.position.unwrap_or(Point::new(0.0, 0.0));
let content_rect = NSRect::new(
NSPoint::new(origin.x, origin.y),
NSSize::new(options.size.width, options.size.height),
);
let style_mask = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable;
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
event_loop_state.mtm.alloc::<NSWindow>(),
content_rect,
style_mask,
NSBackingStoreType::Buffered,
false,
)
};
unsafe {
window.setReleasedWhenClosed(false);
window.setTitle(&NSString::from_str(&options.title));
let () = msg_send![&*window, setDelegate: &*view];
window.setContentView(Some(&view));
if options.position.is_none() {
window.center();
}
}
state.window.replace(Some(window));
}
let layer = CALayer::layer();
layer.setOpaque(true);
set_contents_opaque(&layer, true);
layer.setContentsGravity(unsafe { kCAGravityBottomLeft });
layer.setMagnificationFilter(unsafe { kCAFilterNearest });
layer.setContentsScale(state.scale());
view.setLayer(Some(&*layer));
view.setWantsLayer(true);
state.layer.replace(Some(layer));
event_loop_state
.windows
.borrow_mut()
.insert(Retained::as_ptr(&view), Rc::clone(&state));
Ok(state)
})
}
pub fn show(&self) {
autoreleasepool(|_| {
if let Some(window) = self.window() {
window.orderFront(None);
}
if let Some(view) = self.view() {
view.setHidden(false);
}
})
}
pub fn hide(&self) {
autoreleasepool(|_| {
if let Some(window) = self.window() {
window.orderOut(None);
}
if let Some(view) = self.view() {
view.setHidden(true);
}
})
}
pub fn size(&self) -> Size {
autoreleasepool(|_| {
if let Some(view) = self.view() {
let frame = view.frame();
Size::new(frame.size.width, frame.size.height)
} else {
Size::new(0.0, 0.0)
}
})
}
pub fn scale(&self) -> f64 {
autoreleasepool(|_| {
let mtm = self.event_loop.state.mtm;
if let Some(view) = self.view() {
if let Some(window) = view.window() {
return window.backingScaleFactor();
}
let screens = NSScreen::screens(mtm);
if screens.count() > 0 {
return screens.objectAtIndex(0).backingScaleFactor();
}
}
1.0
})
}
pub fn present(&self, bitmap: Bitmap) {
autoreleasepool(|_| {
let mut surface = self.surface.borrow_mut();
let _ = surface.take_if(|surface| {
surface.width() != bitmap.width() || surface.height() != bitmap.height()
});
let surface = surface.get_or_insert_with(|| {
let surface = Surface::new(bitmap.width(), bitmap.height()).unwrap();
if let Some(layer) = &*self.layer.borrow() {
unsafe {
layer.setContents(Some(&*(surface.as_ptr() as *const AnyObject)));
}
}
surface
});
surface.update(bitmap);
if let Some(layer) = &*self.layer.borrow() {
set_contents_changed(layer);
}
})
}
pub fn present_partial(&self, bitmap: Bitmap, _rects: &[Rect]) {
self.present(bitmap);
}
pub fn set_cursor(&self, cursor: Cursor) {
autoreleasepool(|_| {
self.cursor.set(cursor);
self.update_cursor();
})
}
pub fn set_mouse_position(&self, _position: Point) {}
pub fn close(&self) {
autoreleasepool(|_| {
if let Some(window) = self.window.take() {
window.close();
}
if let Some(view) = self.view.take() {
self.event_loop.state.windows.borrow_mut().remove(&Retained::as_ptr(&view));
view.removeFromSuperview();
}
})
}
pub fn as_raw(&self) -> Result<RawWindow> {
if let Some(view) = self.view.borrow().as_ref() {
Ok(RawWindow::AppKit(Retained::as_ptr(view) as *mut c_void))
} else {
Err(Error::WindowClosed)
}
}
}