#![cfg(target_os = "macos")]
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::sync::Arc;
use block2::RcBlock;
use core::ptr::NonNull;
use core_graphics::color_space::CGColorSpace;
use core_graphics::context::CGContext;
use core_graphics::data_provider::CGDataProvider;
use core_graphics::image::CGImage;
use objc2::rc::Retained;
use objc2::runtime::{NSObject, NSObjectProtocol, ProtocolObject};
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSAutoresizingMaskOptions,
NSBackingStoreType, NSCursor, NSEvent, NSGraphicsContext, NSMenu, NSMenuItem,
NSModalResponseOK, NSOpenPanel, NSPasteboard, NSSavePanel, NSView, NSWindow, NSWindowDelegate,
NSWindowStyleMask,
};
use objc2_foundation::{
MainThreadMarker, NSNotification, NSPoint, NSRect, NSSize, NSString, NSTimer,
};
use super::{CloseBehavior, Window};
use crate::element::context::Context;
use crate::element::ElementPtr;
use crate::support::canvas::Canvas;
use crate::support::color::Color;
use crate::support::point::{Extent, Point};
use crate::support::rect::Rect;
use crate::view::{modifiers, CursorType, KeyCode, MouseButton, MouseButtonKind, View};
fn ns_point_to_point(p: NSPoint) -> Point {
Point::new(p.x as f32, p.y as f32)
}
fn point_to_ns_point(p: Point) -> NSPoint {
NSPoint::new(p.x as f64, p.y as f64)
}
fn ns_size_to_extent(s: NSSize) -> Extent {
Extent::new(s.width as f32, s.height as f32)
}
fn extent_to_ns_size(e: Extent) -> NSSize {
NSSize::new(e.x as f64, e.y as f64)
}
pub fn translate_key(keycode: u16) -> KeyCode {
match keycode {
0x00 => KeyCode::A,
0x01 => KeyCode::S,
0x02 => KeyCode::D,
0x03 => KeyCode::F,
0x04 => KeyCode::H,
0x05 => KeyCode::G,
0x06 => KeyCode::Z,
0x07 => KeyCode::X,
0x08 => KeyCode::C,
0x09 => KeyCode::V,
0x0B => KeyCode::B,
0x0C => KeyCode::Q,
0x0D => KeyCode::W,
0x0E => KeyCode::E,
0x0F => KeyCode::R,
0x10 => KeyCode::Y,
0x11 => KeyCode::T,
0x12 => KeyCode::Key1,
0x13 => KeyCode::Key2,
0x14 => KeyCode::Key3,
0x15 => KeyCode::Key4,
0x16 => KeyCode::Key6,
0x17 => KeyCode::Key5,
0x19 => KeyCode::Key9,
0x1A => KeyCode::Key7,
0x1C => KeyCode::Key8,
0x1D => KeyCode::Key0,
0x1F => KeyCode::O,
0x20 => KeyCode::U,
0x22 => KeyCode::I,
0x23 => KeyCode::P,
0x25 => KeyCode::L,
0x26 => KeyCode::J,
0x28 => KeyCode::K,
0x2D => KeyCode::N,
0x2E => KeyCode::M,
0x24 => KeyCode::Enter,
0x30 => KeyCode::Tab,
0x31 => KeyCode::Space,
0x33 => KeyCode::Backspace,
0x35 => KeyCode::Escape,
0x37 => KeyCode::LeftSuper,
0x38 => KeyCode::LeftShift,
0x39 => KeyCode::CapsLock,
0x3A => KeyCode::LeftAlt,
0x3B => KeyCode::LeftControl,
0x3C => KeyCode::RightShift,
0x3D => KeyCode::RightAlt,
0x3E => KeyCode::RightControl,
0x60 => KeyCode::F5,
0x61 => KeyCode::F6,
0x62 => KeyCode::F7,
0x63 => KeyCode::F3,
0x64 => KeyCode::F8,
0x65 => KeyCode::F9,
0x67 => KeyCode::F11,
0x6D => KeyCode::F10,
0x6F => KeyCode::F12,
0x72 => KeyCode::Insert,
0x73 => KeyCode::Home,
0x74 => KeyCode::PageUp,
0x75 => KeyCode::Delete,
0x76 => KeyCode::F4,
0x77 => KeyCode::End,
0x78 => KeyCode::F2,
0x79 => KeyCode::PageDown,
0x7A => KeyCode::F1,
0x7B => KeyCode::Left,
0x7C => KeyCode::Right,
0x7D => KeyCode::Down,
0x7E => KeyCode::Up,
_ => KeyCode::Unknown,
}
}
pub fn translate_flags(flags: usize) -> i32 {
let mut mods = 0i32;
if flags & (1 << 17) != 0 {
mods |= modifiers::SHIFT;
}
if flags & (1 << 18) != 0 {
mods |= modifiers::CONTROL;
}
if flags & (1 << 19) != 0 {
mods |= modifiers::ALT;
}
if flags & (1 << 20) != 0 {
mods |= modifiers::SUPER;
}
if flags & (1 << 16) != 0 {
mods |= modifiers::CAPS_LOCK;
}
mods
}
pub fn set_cursor(cursor: CursorType) {
unsafe {
match cursor {
CursorType::Arrow => {
let cursor = NSCursor::arrowCursor();
cursor.set();
}
CursorType::IBeam => {
let cursor = NSCursor::IBeamCursor();
cursor.set();
}
CursorType::CrossHair => {
let cursor = NSCursor::crosshairCursor();
cursor.set();
}
CursorType::Hand => {
let cursor = NSCursor::openHandCursor();
cursor.set();
}
CursorType::HResize => {
let cursor = NSCursor::resizeLeftRightCursor();
cursor.set();
}
CursorType::VResize => {
let cursor = NSCursor::resizeUpDownCursor();
cursor.set();
}
}
}
}
pub fn get_clipboard() -> String {
unsafe {
let pasteboard = NSPasteboard::generalPasteboard();
pasteboard
.stringForType(objc2_app_kit::NSPasteboardTypeString)
.map(|s| s.to_string())
.unwrap_or_default()
}
}
pub fn set_clipboard(text: &str) {
unsafe {
let pasteboard = NSPasteboard::generalPasteboard();
pasteboard.clearContents();
let string = NSString::from_str(text);
pasteboard.setString_forType(&string, objc2_app_kit::NSPasteboardTypeString);
}
}
pub fn choose_folder(title: &str, initial_dir: Option<&std::path::Path>) -> Option<PathBuf> {
let mtm = MainThreadMarker::new()?;
unsafe {
let panel = NSOpenPanel::openPanel(mtm);
panel.setCanChooseDirectories(true);
panel.setCanChooseFiles(false);
panel.setAllowsMultipleSelection(false);
panel.setTitle(Some(&NSString::from_str(title)));
if let Some(dir) = initial_dir {
let dir_str = NSString::from_str(&dir.display().to_string());
panel.setDirectoryURL(Some(&objc2_foundation::NSURL::fileURLWithPath(&dir_str)));
}
if panel.runModal() != NSModalResponseOK {
return None;
}
let urls = panel.URLs();
url_to_path(urls.iter().next()?)
}
}
pub fn choose_file_to_open(title: &str) -> Option<PathBuf> {
let mtm = MainThreadMarker::new()?;
unsafe {
let panel = NSOpenPanel::openPanel(mtm);
panel.setCanChooseDirectories(false);
panel.setCanChooseFiles(true);
panel.setAllowsMultipleSelection(false);
panel.setTitle(Some(&NSString::from_str(title)));
if panel.runModal() != NSModalResponseOK {
return None;
}
let urls = panel.URLs();
url_to_path(urls.iter().next()?)
}
}
pub fn choose_file_to_save(title: &str, default_name: &str) -> Option<PathBuf> {
let mtm = MainThreadMarker::new()?;
unsafe {
let panel = NSSavePanel::savePanel(mtm);
panel.setTitle(Some(&NSString::from_str(title)));
panel.setNameFieldStringValue(&NSString::from_str(default_name));
if panel.runModal() != NSModalResponseOK {
return None;
}
let url = panel.URL()?;
url_to_path(&url)
}
}
unsafe fn url_to_path(url: &objc2_foundation::NSURL) -> Option<PathBuf> {
Some(PathBuf::from(url.path()?.to_string()))
}
pub struct MacOSApp {
app: Retained<NSApplication>,
mtm: MainThreadMarker,
delegate: RefCell<Option<Retained<MKAppDelegate>>>,
menu_action_targets: RefCell<Vec<Retained<MenuActionTarget>>>,
}
impl MacOSApp {
pub fn new() -> Option<Self> {
let mtm = MainThreadMarker::new()?;
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
let macos_app = Self {
app,
mtm,
delegate: RefCell::new(None),
menu_action_targets: RefCell::new(Vec::new()),
};
macos_app.setup_menu();
Some(macos_app)
}
pub fn set_close_behavior(&self, behavior: CloseBehavior) {
let (quit_on_last_window_closed, rebuild) = match behavior {
CloseBehavior::QuitApp => (true, None),
CloseBehavior::KeepRunning(rebuild) => (false, Some(rebuild)),
};
let delegate = MKAppDelegate::new(self.mtm, quit_on_last_window_closed, rebuild);
self.app
.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
*self.delegate.borrow_mut() = Some(delegate);
}
pub fn schedule_timer(
&self,
interval_secs: f64,
repeats: bool,
callback: impl FnMut() + 'static,
) -> Retained<NSTimer> {
let callback = RefCell::new(callback);
let app = self.app.clone();
let block = RcBlock::new(move |_timer: NonNull<NSTimer>| {
(callback.borrow_mut())();
for window in app.windows().iter() {
if let Some(content_view) = window.contentView() {
unsafe { content_view.setNeedsDisplay(true) };
}
}
});
unsafe {
NSTimer::scheduledTimerWithTimeInterval_repeats_block(interval_secs, repeats, &block)
}
}
fn setup_menu(&self) {
use crate::element::menu::get_native_menu_bar;
let config = get_native_menu_bar().unwrap_or_default();
self.menu_action_targets.borrow_mut().clear();
unsafe {
let main_menu = NSMenu::new(self.mtm);
if config.include_app_menu {
self.add_app_menu(&main_menu);
}
for custom_menu in &config.menus {
self.add_custom_menu(&main_menu, custom_menu);
}
if config.include_edit_menu {
self.add_edit_menu(&main_menu);
}
if config.include_window_menu {
self.add_window_menu(&main_menu);
}
self.app.setMainMenu(Some(&main_menu));
}
}
unsafe fn add_app_menu(&self, main_menu: &NSMenu) {
let app_menu_item = NSMenuItem::new(self.mtm);
let app_menu = NSMenu::new(self.mtm);
let about_title = NSString::from_str("About");
let about_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&about_title,
Some(objc2::sel!(orderFrontStandardAboutPanel:)),
&NSString::from_str(""),
);
app_menu.addItem(&about_item);
app_menu.addItem(&NSMenuItem::separatorItem(self.mtm));
let services_title = NSString::from_str("Services");
let services_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&services_title,
None,
&NSString::from_str(""),
);
let services_menu = NSMenu::initWithTitle(self.mtm.alloc(), &services_title);
services_item.setSubmenu(Some(&services_menu));
app_menu.addItem(&services_item);
self.app.setServicesMenu(Some(&services_menu));
app_menu.addItem(&NSMenuItem::separatorItem(self.mtm));
let hide_title = NSString::from_str("Hide");
let hide_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&hide_title,
Some(objc2::sel!(hide:)),
&NSString::from_str("h"),
);
app_menu.addItem(&hide_item);
let hide_others_title = NSString::from_str("Hide Others");
let hide_others_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&hide_others_title,
Some(objc2::sel!(hideOtherApplications:)),
&NSString::from_str("h"),
);
hide_others_item.setKeyEquivalentModifierMask(
objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagCommand
| objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagOption,
);
app_menu.addItem(&hide_others_item);
let show_all_title = NSString::from_str("Show All");
let show_all_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&show_all_title,
Some(objc2::sel!(unhideAllApplications:)),
&NSString::from_str(""),
);
app_menu.addItem(&show_all_item);
app_menu.addItem(&NSMenuItem::separatorItem(self.mtm));
let quit_title = NSString::from_str("Quit");
let quit_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&quit_title,
Some(objc2::sel!(terminate:)),
&NSString::from_str("q"),
);
app_menu.addItem(&quit_item);
app_menu_item.setSubmenu(Some(&app_menu));
main_menu.addItem(&app_menu_item);
}
unsafe fn add_edit_menu(&self, main_menu: &NSMenu) {
let edit_menu_item = NSMenuItem::new(self.mtm);
let edit_title = NSString::from_str("Edit");
let edit_menu = NSMenu::initWithTitle(self.mtm.alloc(), &edit_title);
let undo_title = NSString::from_str("Undo");
let undo_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&undo_title,
Some(objc2::sel!(undo:)),
&NSString::from_str("z"),
);
edit_menu.addItem(&undo_item);
let redo_title = NSString::from_str("Redo");
let redo_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&redo_title,
Some(objc2::sel!(redo:)),
&NSString::from_str("Z"),
);
edit_menu.addItem(&redo_item);
edit_menu.addItem(&NSMenuItem::separatorItem(self.mtm));
let cut_title = NSString::from_str("Cut");
let cut_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&cut_title,
Some(objc2::sel!(cut:)),
&NSString::from_str("x"),
);
edit_menu.addItem(&cut_item);
let copy_title = NSString::from_str("Copy");
let copy_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
©_title,
Some(objc2::sel!(copy:)),
&NSString::from_str("c"),
);
edit_menu.addItem(©_item);
let paste_title = NSString::from_str("Paste");
let paste_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&paste_title,
Some(objc2::sel!(paste:)),
&NSString::from_str("v"),
);
edit_menu.addItem(&paste_item);
let select_all_title = NSString::from_str("Select All");
let select_all_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&select_all_title,
Some(objc2::sel!(selectAll:)),
&NSString::from_str("a"),
);
edit_menu.addItem(&select_all_item);
edit_menu_item.setSubmenu(Some(&edit_menu));
main_menu.addItem(&edit_menu_item);
}
unsafe fn add_window_menu(&self, main_menu: &NSMenu) {
let window_menu_item = NSMenuItem::new(self.mtm);
let window_title = NSString::from_str("Window");
let window_menu = NSMenu::initWithTitle(self.mtm.alloc(), &window_title);
let minimize_title = NSString::from_str("Minimize");
let minimize_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&minimize_title,
Some(objc2::sel!(performMiniaturize:)),
&NSString::from_str("m"),
);
window_menu.addItem(&minimize_item);
let zoom_title = NSString::from_str("Zoom");
let zoom_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&zoom_title,
Some(objc2::sel!(performZoom:)),
&NSString::from_str(""),
);
window_menu.addItem(&zoom_item);
window_menu.addItem(&NSMenuItem::separatorItem(self.mtm));
let bring_all_title = NSString::from_str("Bring All to Front");
let bring_all_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&bring_all_title,
Some(objc2::sel!(arrangeInFront:)),
&NSString::from_str(""),
);
window_menu.addItem(&bring_all_item);
window_menu_item.setSubmenu(Some(&window_menu));
main_menu.addItem(&window_menu_item);
self.app.setWindowsMenu(Some(&window_menu));
}
unsafe fn add_custom_menu(
&self,
main_menu: &NSMenu,
custom_menu: &crate::element::menu::NativeMenu,
) {
let menu_item = NSMenuItem::new(self.mtm);
let title = NSString::from_str(&custom_menu.title);
let ns_menu = NSMenu::initWithTitle(self.mtm.alloc(), &title);
for item in &custom_menu.items {
self.add_native_menu_item(&ns_menu, item);
}
menu_item.setSubmenu(Some(&ns_menu));
main_menu.addItem(&menu_item);
}
unsafe fn add_native_menu_item(
&self,
menu: &NSMenu,
item: &crate::element::menu::NativeMenuItem,
) {
if item.is_separator() {
menu.addItem(&NSMenuItem::separatorItem(self.mtm));
return;
}
let title = NSString::from_str(&item.label);
let key_equiv = item
.shortcut
.as_ref()
.map(|s| s.key.to_string())
.unwrap_or_default();
let key_str = NSString::from_str(&key_equiv);
let ns_item = NSMenuItem::initWithTitle_action_keyEquivalent(
self.mtm.alloc(),
&title,
None,
&key_str,
);
if let Some(ref callback) = item.action {
let target = MenuActionTarget::new(self.mtm, callback.clone());
ns_item.setTarget(Some(&target));
ns_item.setAction(Some(objc2::sel!(performMenuAction:)));
self.menu_action_targets.borrow_mut().push(target);
}
if let Some(ref shortcut) = item.shortcut {
let mut mask = objc2_app_kit::NSEventModifierFlags::empty();
if shortcut.modifiers.command {
mask |= objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagCommand;
}
if shortcut.modifiers.shift {
mask |= objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagShift;
}
if shortcut.modifiers.option {
mask |= objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagOption;
}
if shortcut.modifiers.control {
mask |= objc2_app_kit::NSEventModifierFlags::NSEventModifierFlagControl;
}
ns_item.setKeyEquivalentModifierMask(mask);
}
if !item.enabled {
ns_item.setEnabled(false);
}
if let Some(ref submenu_items) = item.submenu {
let submenu_title = NSString::from_str(&item.label);
let submenu = NSMenu::initWithTitle(self.mtm.alloc(), &submenu_title);
for sub_item in submenu_items {
self.add_native_menu_item(&submenu, sub_item);
}
ns_item.setSubmenu(Some(&submenu));
}
menu.addItem(&ns_item);
}
pub fn run(&self) {
unsafe {
self.app.run();
}
}
pub fn stop(&self) {
self.app.stop(None);
}
}
struct MKAppDelegateIvars {
quit_on_last_window_closed: Cell<bool>,
rebuild: RefCell<Option<Box<dyn Fn() -> Window>>>,
current_window: RefCell<Option<Window>>,
}
declare_class!(
struct MKAppDelegate;
unsafe impl ClassType for MKAppDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "MKAppDelegate";
}
impl DeclaredClass for MKAppDelegate {
type Ivars = MKAppDelegateIvars;
}
unsafe impl NSObjectProtocol for MKAppDelegate {}
unsafe impl NSApplicationDelegate for MKAppDelegate {
#[method(applicationShouldTerminateAfterLastWindowClosed:)]
fn should_terminate_after_last_window_closed(&self, _sender: &NSApplication) -> bool {
self.ivars().quit_on_last_window_closed.get()
}
#[method(applicationShouldHandleReopen:hasVisibleWindows:)]
fn should_handle_reopen(&self, _sender: &NSApplication, has_visible_windows: bool) -> bool {
if !has_visible_windows {
let rebuilt = self.ivars().rebuild.borrow().as_ref().map(|rebuild| {
let mut window = rebuild();
window.show();
window
});
if let Some(window) = rebuilt {
*self.ivars().current_window.borrow_mut() = Some(window);
}
}
true
}
}
);
impl MKAppDelegate {
fn new(
mtm: MainThreadMarker,
quit_on_last_window_closed: bool,
rebuild: Option<Box<dyn Fn() -> Window>>,
) -> Retained<Self> {
let this = mtm.alloc::<Self>().set_ivars(MKAppDelegateIvars {
quit_on_last_window_closed: Cell::new(quit_on_last_window_closed),
rebuild: RefCell::new(rebuild),
current_window: RefCell::new(None),
});
unsafe { msg_send_id![super(this), init] }
}
}
struct MenuActionTargetIvars {
callback: Arc<dyn Fn() + Send + Sync>,
}
declare_class!(
struct MenuActionTarget;
unsafe impl ClassType for MenuActionTarget {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "MKMenuActionTarget";
}
impl DeclaredClass for MenuActionTarget {
type Ivars = MenuActionTargetIvars;
}
unsafe impl NSObjectProtocol for MenuActionTarget {}
unsafe impl MenuActionTarget {
#[method(performMenuAction:)]
fn perform_menu_action(&self, _sender: &NSObject) {
(self.ivars().callback)();
}
}
);
impl MenuActionTarget {
fn new(mtm: MainThreadMarker, callback: Arc<dyn Fn() + Send + Sync>) -> Retained<Self> {
let this = mtm
.alloc::<Self>()
.set_ivars(MenuActionTargetIvars { callback });
unsafe { msg_send_id![super(this), init] }
}
}
struct WindowFocusDelegateIvars {
callback: Box<dyn Fn()>,
}
declare_class!(
struct WindowFocusDelegate;
unsafe impl ClassType for WindowFocusDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "MKWindowFocusDelegate";
}
impl DeclaredClass for WindowFocusDelegate {
type Ivars = WindowFocusDelegateIvars;
}
unsafe impl NSObjectProtocol for WindowFocusDelegate {}
unsafe impl NSWindowDelegate for WindowFocusDelegate {
#[method(windowDidBecomeKey:)]
fn window_did_become_key(&self, _notification: &NSNotification) {
(self.ivars().callback)();
}
}
);
impl WindowFocusDelegate {
fn new(mtm: MainThreadMarker, callback: Box<dyn Fn()>) -> Retained<Self> {
let this = mtm
.alloc::<Self>()
.set_ivars(WindowFocusDelegateIvars { callback });
unsafe { msg_send_id![super(this), init] }
}
}
#[derive(Default)]
struct MKViewIvars {
canvas: RefCell<Option<Canvas>>,
content: RefCell<Option<ElementPtr>>,
size: RefCell<Extent>,
}
declare_class!(
struct MKView;
unsafe impl ClassType for MKView {
type Super = NSView;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "MKView";
}
impl DeclaredClass for MKView {
type Ivars = MKViewIvars;
}
unsafe impl MKView {
#[method(isFlipped)]
fn is_flipped(&self) -> bool {
true
}
#[method(acceptsFirstResponder)]
fn accepts_first_responder(&self) -> bool {
true
}
#[method(mouseDown:)]
fn mouse_down(&self, event: &NSEvent) {
self.handle_mouse_event(event, true);
}
#[method(mouseUp:)]
fn mouse_up(&self, event: &NSEvent) {
self.handle_mouse_event(event, false);
}
#[method(rightMouseDown:)]
fn right_mouse_down(&self, event: &NSEvent) {
self.handle_mouse_event(event, true);
}
#[method(rightMouseUp:)]
fn right_mouse_up(&self, event: &NSEvent) {
self.handle_mouse_event(event, false);
}
#[method(mouseDragged:)]
fn mouse_dragged(&self, event: &NSEvent) {
self.handle_mouse_drag(event);
}
#[method(rightMouseDragged:)]
fn right_mouse_dragged(&self, event: &NSEvent) {
self.handle_mouse_drag(event);
}
#[method(scrollWheel:)]
fn scroll_wheel(&self, event: &NSEvent) {
self.handle_scroll(event);
}
#[method(keyDown:)]
fn key_down(&self, event: &NSEvent) {
self.handle_key_event(event, true);
}
#[method(keyUp:)]
fn key_up(&self, event: &NSEvent) {
self.handle_key_event(event, false);
}
#[method(drawRect:)]
fn draw_rect(&self, _dirty_rect: NSRect) {
let ivars = self.ivars();
let frame = self.frame();
let size = Extent::new(frame.size.width as f32, frame.size.height as f32);
*ivars.size.borrow_mut() = size;
if size.x <= 0.0 || size.y <= 0.0 {
return;
}
let scale: f32 = unsafe {
let window_ptr: *mut objc2::runtime::AnyObject = objc2::msg_send![self, window];
if window_ptr.is_null() {
1.0
} else {
let factor: f64 = objc2::msg_send![window_ptr, backingScaleFactor];
factor as f32
}
};
let pixel_width = (size.x * scale).round().max(1.0) as u32;
let pixel_height = (size.y * scale).round().max(1.0) as u32;
{
let mut canvas_opt = ivars.canvas.borrow_mut();
let needs_new = match &*canvas_opt {
Some(c) => c.width() != pixel_width || c.height() != pixel_height,
None => true,
};
if needs_new {
*canvas_opt = Canvas::new(pixel_width, pixel_height);
}
}
let mut canvas_opt = ivars.canvas.borrow_mut();
if let Some(ref mut canvas) = *canvas_opt {
canvas.clear(Color::new(0.2, 0.2, 0.2, 1.0));
canvas.reset_transform();
canvas.scale(scale, scale);
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
let mut temp_view = View::new(size);
temp_view.set_scale(scale);
let temp_canvas = std::mem::replace(canvas, Canvas::new(1, 1).unwrap());
let canvas_cell = RefCell::new(temp_canvas);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
content.draw(&ctx);
content.draw_overlay(&ctx);
*canvas = canvas_cell.into_inner();
}
Self::blit_to_screen(canvas, size);
}
}
}
);
impl MKView {
fn new(mtm: MainThreadMarker, size: Extent) -> Retained<Self> {
let frame = NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(size.x as f64, size.y as f64),
);
let this = mtm.alloc::<MKView>().set_ivars(MKViewIvars {
canvas: RefCell::new(None),
content: RefCell::new(None),
size: RefCell::new(size),
});
unsafe { msg_send_id![super(this), initWithFrame: frame] }
}
fn set_content(&self, content: ElementPtr) {
*self.ivars().content.borrow_mut() = Some(content);
unsafe {
self.setNeedsDisplay(true);
}
}
fn set_size(&self, size: Extent) {
*self.ivars().size.borrow_mut() = size;
}
fn handle_mouse_event(&self, event: &NSEvent, down: bool) {
unsafe {
let location_in_window = event.locationInWindow();
let location = self.convertPoint_fromView(location_in_window, None);
let pos = ns_point_to_point(location);
let button_number = event.buttonNumber();
let button_kind = match button_number {
0 => MouseButtonKind::Left,
1 => MouseButtonKind::Right,
2 => MouseButtonKind::Middle,
_ => MouseButtonKind::Left,
};
let mouse_btn = MouseButton {
down,
click_count: event.clickCount() as i32,
button: button_kind,
modifiers: translate_flags(event.modifierFlags().bits()),
pos,
};
let ivars = self.ivars();
let size = *ivars.size.borrow();
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
if let Some(dummy_canvas) = Canvas::new(1, 1) {
let canvas_cell = RefCell::new(dummy_canvas);
let temp_view = View::new(size);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
if down {
content.clear_focus();
}
let handled = content.handle_click(&ctx, mouse_btn);
self.setNeedsDisplay(true);
}
}
}
}
fn handle_mouse_drag(&self, event: &NSEvent) {
unsafe {
let location_in_window = event.locationInWindow();
let location = self.convertPoint_fromView(location_in_window, None);
let pos = ns_point_to_point(location);
let button_number = event.buttonNumber();
let button_kind = match button_number {
0 => MouseButtonKind::Left,
1 => MouseButtonKind::Right,
2 => MouseButtonKind::Middle,
_ => MouseButtonKind::Left,
};
let mouse_btn = MouseButton {
down: true,
click_count: 1,
button: button_kind,
modifiers: translate_flags(event.modifierFlags().bits()),
pos,
};
let ivars = self.ivars();
let size = *ivars.size.borrow();
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
if let Some(dummy_canvas) = Canvas::new(1, 1) {
let canvas_cell = RefCell::new(dummy_canvas);
let temp_view = View::new(size);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
content.handle_drag(&ctx, mouse_btn);
self.setNeedsDisplay(true);
}
}
}
}
fn handle_scroll(&self, event: &NSEvent) {
unsafe {
let location_in_window = event.locationInWindow();
let location = self.convertPoint_fromView(location_in_window, None);
let pos = ns_point_to_point(location);
let delta_x = event.scrollingDeltaX() as f32;
let delta_y = event.scrollingDeltaY() as f32;
let dir = Point::new(delta_x, delta_y);
let ivars = self.ivars();
let size = *ivars.size.borrow();
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
if let Some(dummy_canvas) = Canvas::new(1, 1) {
let canvas_cell = RefCell::new(dummy_canvas);
let temp_view = View::new(size);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
if content.handle_scroll(&ctx, dir, pos) {
self.setNeedsDisplay(true);
}
}
}
}
}
fn handle_key_event(&self, event: &NSEvent, down: bool) {
unsafe {
use crate::view::{KeyAction, KeyInfo};
let keycode = event.keyCode();
let key = translate_key(keycode);
let modifiers = translate_flags(event.modifierFlags().bits());
let action = if down {
KeyAction::Press
} else {
KeyAction::Release
};
let key_info = KeyInfo {
key,
action,
modifiers,
};
let ivars = self.ivars();
let size = *ivars.size.borrow();
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
if let Some(dummy_canvas) = Canvas::new(1, 1) {
let canvas_cell = RefCell::new(dummy_canvas);
let temp_view = View::new(size);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
if content.handle_key(&ctx, key_info) {
self.setNeedsDisplay(true);
}
}
}
if down {
if let Some(characters) = event.characters() {
let text: String = characters.to_string();
if !text.is_empty() {
for c in text.chars() {
if c.is_control() && c != '\n' && c != '\t' {
continue;
}
let text_info = crate::view::TextInfo {
codepoint: c,
modifiers,
};
let content_ref = ivars.content.borrow();
if let Some(ref content) = *content_ref {
let bounds = Rect {
left: 0.0,
top: 0.0,
right: size.x,
bottom: size.y,
};
if let Some(dummy_canvas) = Canvas::new(1, 1) {
let canvas_cell = RefCell::new(dummy_canvas);
let temp_view = View::new(size);
let ctx = Context::new(&temp_view, &canvas_cell, bounds);
if content.handle_text(&ctx, text_info) {
self.setNeedsDisplay(true);
}
}
}
}
}
}
}
}
}
fn blit_to_screen(canvas: &Canvas, logical_size: Extent) {
unsafe {
let Some(ns_ctx) = NSGraphicsContext::currentContext() else {
return;
};
let cg_ctx_ptr: *mut std::ffi::c_void = objc2::msg_send![&ns_ctx, graphicsPort];
if cg_ctx_ptr.is_null() {
return;
}
let pixel_width = canvas.width();
let pixel_height = canvas.height();
let pixmap = canvas.pixmap();
let data = pixmap.data();
let color_space = CGColorSpace::create_device_rgb();
let provider = CGDataProvider::from_slice(data);
let cg_image = CGImage::new(
pixel_width as usize,
pixel_height as usize,
8,
32,
pixel_width as usize * 4,
&color_space,
1, &provider,
false,
0, );
let rect = core_graphics::geometry::CGRect::new(
&core_graphics::geometry::CGPoint::new(0.0, 0.0),
&core_graphics::geometry::CGSize::new(logical_size.x as f64, logical_size.y as f64),
);
let cg_ctx = CGContext::from_existing_context_ptr(cg_ctx_ptr as *mut _);
cg_ctx.save();
cg_ctx.translate(0.0, logical_size.y as f64);
cg_ctx.scale(1.0, -1.0);
cg_ctx.draw_image(rect, &cg_image);
cg_ctx.restore();
}
}
}
pub struct MacOSWindow {
window: Retained<NSWindow>,
mk_view: Retained<MKView>,
view: Option<View>,
focus_delegate: RefCell<Option<Retained<WindowFocusDelegate>>>,
}
impl MacOSWindow {
pub fn new(title: &str, size: Extent, mtm: MainThreadMarker) -> Self {
Self::new_with_style(title, size, super::WindowStyle::default(), mtm)
}
pub fn new_with_style(
title: &str,
size: Extent,
style: super::WindowStyle,
mtm: MainThreadMarker,
) -> Self {
let frame = NSRect::new(NSPoint::new(0.0, 0.0), extent_to_ns_size(size));
let style = if style.borderless {
NSWindowStyleMask::Borderless
} else {
let mut mask = NSWindowStyleMask::Titled;
if style.closable {
mask |= NSWindowStyleMask::Closable;
}
if style.miniaturizable {
mask |= NSWindowStyleMask::Miniaturizable;
}
if style.resizable {
mask |= NSWindowStyleMask::Resizable;
}
mask
};
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
mtm.alloc(),
frame,
style,
NSBackingStoreType::NSBackingStoreBuffered,
false,
)
};
let title_str = NSString::from_str(title);
window.setTitle(&title_str);
window.center();
let mk_view = MKView::new(mtm, size);
unsafe {
mk_view.setAutoresizingMask(
NSAutoresizingMaskOptions::NSViewWidthSizable
| NSAutoresizingMaskOptions::NSViewHeightSizable,
);
}
window.setContentView(Some(&mk_view));
Self {
window,
mk_view,
view: Some(View::new(size)),
focus_delegate: RefCell::new(None),
}
}
pub fn on_focus(&self, callback: impl Fn() + 'static) {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let delegate = WindowFocusDelegate::new(mtm, Box::new(callback));
self.window
.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
*self.focus_delegate.borrow_mut() = Some(delegate);
}
pub fn show(&self) {
self.window.makeKeyAndOrderFront(None);
}
pub fn hide(&self) {
self.window.orderOut(None);
}
pub fn close(&self) {
self.window.close();
}
pub fn set_title(&self, title: &str) {
let title_str = NSString::from_str(title);
self.window.setTitle(&title_str);
}
pub fn size(&self) -> Extent {
let frame = self.window.frame();
ns_size_to_extent(frame.size)
}
pub fn set_size(&self, size: Extent) {
let mut frame = self.window.frame();
frame.size = extent_to_ns_size(size);
self.window.setFrame_display(frame, true);
self.mk_view.set_size(size);
}
pub fn set_content(&self, content: ElementPtr) {
self.mk_view.set_content(content);
}
pub fn view(&self) -> Option<&View> {
self.view.as_ref()
}
pub fn view_mut(&mut self) -> Option<&mut View> {
self.view.as_mut()
}
pub fn refresh(&self) {
unsafe {
self.mk_view.setNeedsDisplay(true);
}
}
pub fn native_window_handle(&self) -> *mut std::ffi::c_void {
Retained::as_ptr(&self.window) as *mut std::ffi::c_void
}
}