platform.rs

  1use super::{BoolExt as _, Dispatcher, FontSystem, Window};
  2use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem};
  3use anyhow::Result;
  4use cocoa::{
  5    appkit::{
  6        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
  7        NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
  8        NSPasteboardTypeString, NSWindow,
  9    },
 10    base::{id, nil, selector},
 11    foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
 12};
 13use ctor::ctor;
 14use objc::{
 15    class,
 16    declare::ClassDecl,
 17    msg_send,
 18    runtime::{Class, Object, Sel},
 19    sel, sel_impl,
 20};
 21use ptr::null_mut;
 22use std::{
 23    cell::RefCell,
 24    ffi::{c_void, CStr},
 25    os::raw::c_char,
 26    path::PathBuf,
 27    ptr,
 28    rc::Rc,
 29    sync::Arc,
 30};
 31
 32const MAC_PLATFORM_IVAR: &'static str = "runner";
 33static mut APP_CLASS: *const Class = ptr::null();
 34static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
 35
 36#[ctor]
 37unsafe fn build_classes() {
 38    APP_CLASS = {
 39        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
 40        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 41        decl.add_method(
 42            sel!(sendEvent:),
 43            send_event as extern "C" fn(&mut Object, Sel, id),
 44        );
 45        decl.register()
 46    };
 47
 48    APP_DELEGATE_CLASS = {
 49        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
 50        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 51        decl.add_method(
 52            sel!(applicationDidFinishLaunching:),
 53            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
 54        );
 55        decl.add_method(
 56            sel!(applicationDidBecomeActive:),
 57            did_become_active as extern "C" fn(&mut Object, Sel, id),
 58        );
 59        decl.add_method(
 60            sel!(applicationDidResignActive:),
 61            did_resign_active as extern "C" fn(&mut Object, Sel, id),
 62        );
 63        decl.add_method(
 64            sel!(handleGPUIMenuItem:),
 65            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
 66        );
 67        decl.add_method(
 68            sel!(application:openFiles:),
 69            open_files as extern "C" fn(&mut Object, Sel, id, id),
 70        );
 71        decl.register()
 72    }
 73}
 74
 75pub struct MacPlatform {
 76    dispatcher: Arc<Dispatcher>,
 77    fonts: Arc<FontSystem>,
 78    callbacks: RefCell<Callbacks>,
 79    menu_item_actions: RefCell<Vec<String>>,
 80}
 81
 82#[derive(Default)]
 83struct Callbacks {
 84    become_active: Option<Box<dyn FnMut()>>,
 85    resign_active: Option<Box<dyn FnMut()>>,
 86    event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
 87    menu_command: Option<Box<dyn FnMut(&str)>>,
 88    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
 89    finish_launching: Option<Box<dyn FnOnce() -> ()>>,
 90}
 91
 92impl MacPlatform {
 93    pub fn new() -> Self {
 94        Self {
 95            dispatcher: Arc::new(Dispatcher),
 96            fonts: Arc::new(FontSystem::new()),
 97            callbacks: Default::default(),
 98            menu_item_actions: Default::default(),
 99        }
100    }
101}
102
103impl platform::Platform for MacPlatform {
104    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
105        self.callbacks.borrow_mut().become_active = Some(callback);
106    }
107
108    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
109        self.callbacks.borrow_mut().resign_active = Some(callback);
110    }
111
112    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
113        self.callbacks.borrow_mut().event = Some(callback);
114    }
115
116    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
117        self.callbacks.borrow_mut().menu_command = Some(callback);
118    }
119
120    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
121        self.callbacks.borrow_mut().open_files = Some(callback);
122    }
123
124    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
125        self.callbacks.borrow_mut().finish_launching = Some(on_finish_launching);
126
127        unsafe {
128            let pool = NSAutoreleasePool::new(nil);
129            let app: id = msg_send![APP_CLASS, sharedApplication];
130            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
131
132            let self_ptr = self as *const Self as *mut c_void;
133            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
134            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
135            app.setDelegate_(app_delegate);
136            app.run();
137            pool.drain();
138            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
139            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
140        }
141    }
142
143    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
144        self.dispatcher.clone()
145    }
146
147    fn activate(&self, ignoring_other_apps: bool) {
148        unsafe {
149            let app = NSApplication::sharedApplication(nil);
150            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
151        }
152    }
153
154    fn open_window(
155        &self,
156        options: platform::WindowOptions,
157        executor: Rc<executor::Foreground>,
158    ) -> Result<Box<dyn platform::Window>> {
159        Ok(Box::new(Window::open(options, executor, self.fonts())?))
160    }
161
162    fn prompt_for_paths(
163        &self,
164        options: platform::PathPromptOptions,
165    ) -> Option<Vec<std::path::PathBuf>> {
166        unsafe {
167            let panel = NSOpenPanel::openPanel(nil);
168            panel.setCanChooseDirectories_(options.directories.to_objc());
169            panel.setCanChooseFiles_(options.files.to_objc());
170            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
171            panel.setResolvesAliases_(false.to_objc());
172            let response = panel.runModal();
173            if response == NSModalResponse::NSModalResponseOk {
174                let mut result = Vec::new();
175                let urls = panel.URLs();
176                for i in 0..urls.count() {
177                    let url = urls.objectAtIndex(i);
178                    let string = url.absoluteString();
179                    let string = std::ffi::CStr::from_ptr(string.UTF8String())
180                        .to_string_lossy()
181                        .to_string();
182                    if let Some(path) = string.strip_prefix("file://") {
183                        result.push(PathBuf::from(path));
184                    }
185                }
186                Some(result)
187            } else {
188                None
189            }
190        }
191    }
192
193    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
194        self.fonts.clone()
195    }
196
197    fn quit(&self) {
198        unsafe {
199            let app = NSApplication::sharedApplication(nil);
200            let _: () = msg_send![app, terminate: nil];
201        }
202    }
203
204    fn copy(&self, text: &str) {
205        unsafe {
206            let data = NSData::dataWithBytes_length_(
207                nil,
208                text.as_ptr() as *const c_void,
209                text.len() as u64,
210            );
211            let pasteboard = NSPasteboard::generalPasteboard(nil);
212            pasteboard.clearContents();
213            pasteboard.setData_forType(data, NSPasteboardTypeString);
214        }
215    }
216
217    fn set_menus(&self, menus: &[Menu]) {
218        unsafe {
219            let app: id = msg_send![APP_CLASS, sharedApplication];
220            app.setMainMenu_(self.create_menu_bar(menus));
221        }
222    }
223}
224
225impl MacPlatform {
226    unsafe fn create_menu_bar(&self, menus: &[Menu]) -> id {
227        let menu_bar = NSMenu::new(nil).autorelease();
228        let mut menu_item_actions = self.menu_item_actions.borrow_mut();
229        menu_item_actions.clear();
230
231        for menu_config in menus {
232            let menu_bar_item = NSMenuItem::new(nil).autorelease();
233            let menu = NSMenu::new(nil).autorelease();
234
235            menu.setTitle_(ns_string(menu_config.name));
236
237            for item_config in menu_config.items {
238                let item;
239
240                match item_config {
241                    MenuItem::Separator => {
242                        item = NSMenuItem::separatorItem(nil);
243                    }
244                    MenuItem::Action {
245                        name,
246                        keystroke,
247                        action,
248                    } => {
249                        if let Some(keystroke) = keystroke {
250                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
251                                panic!(
252                                    "Invalid keystroke for menu item {}:{} - {:?}",
253                                    menu_config.name, name, err
254                                )
255                            });
256
257                            let mut mask = NSEventModifierFlags::empty();
258                            for (modifier, flag) in &[
259                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
260                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
261                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
262                            ] {
263                                if *modifier {
264                                    mask |= *flag;
265                                }
266                            }
267
268                            item = NSMenuItem::alloc(nil)
269                                .initWithTitle_action_keyEquivalent_(
270                                    ns_string(name),
271                                    selector("handleGPUIMenuItem:"),
272                                    ns_string(&keystroke.key),
273                                )
274                                .autorelease();
275                            item.setKeyEquivalentModifierMask_(mask);
276                        } else {
277                            item = NSMenuItem::alloc(nil)
278                                .initWithTitle_action_keyEquivalent_(
279                                    ns_string(name),
280                                    selector("handleGPUIMenuItem:"),
281                                    ns_string(""),
282                                )
283                                .autorelease();
284                        }
285
286                        let tag = menu_item_actions.len() as NSInteger;
287                        let _: () = msg_send![item, setTag: tag];
288                        menu_item_actions.push(action.to_string());
289                    }
290                }
291
292                menu.addItem_(item);
293            }
294
295            menu_bar_item.setSubmenu_(menu);
296            menu_bar.addItem_(menu_bar_item);
297        }
298
299        menu_bar
300    }
301}
302
303unsafe fn get_platform(object: &mut Object) -> &MacPlatform {
304    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
305    assert!(!platform_ptr.is_null());
306    &*(platform_ptr as *const MacPlatform)
307}
308
309extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
310    unsafe {
311        if let Some(event) = Event::from_native(native_event, None) {
312            let platform = get_platform(this);
313            if let Some(callback) = platform.callbacks.borrow_mut().event.as_mut() {
314                if callback(event) {
315                    return;
316                }
317            }
318        }
319
320        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
321    }
322}
323
324extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
325    unsafe {
326        let app: id = msg_send![APP_CLASS, sharedApplication];
327        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
328
329        let platform = get_platform(this);
330        if let Some(callback) = platform.callbacks.borrow_mut().finish_launching.take() {
331            callback();
332        }
333    }
334}
335
336extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
337    let platform = unsafe { get_platform(this) };
338    if let Some(callback) = platform.callbacks.borrow_mut().become_active.as_mut() {
339        callback();
340    }
341}
342
343extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
344    let platform = unsafe { get_platform(this) };
345    if let Some(callback) = platform.callbacks.borrow_mut().resign_active.as_mut() {
346        callback();
347    }
348}
349
350extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
351    let paths = unsafe {
352        (0..paths.count())
353            .into_iter()
354            .filter_map(|i| {
355                let path = paths.objectAtIndex(i);
356                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
357                    Ok(string) => Some(PathBuf::from(string)),
358                    Err(err) => {
359                        log::error!("error converting path to string: {}", err);
360                        None
361                    }
362                }
363            })
364            .collect::<Vec<_>>()
365    };
366    let platform = unsafe { get_platform(this) };
367    if let Some(callback) = platform.callbacks.borrow_mut().open_files.as_mut() {
368        callback(paths);
369    }
370}
371
372extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
373    unsafe {
374        let platform = get_platform(this);
375        if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() {
376            let tag: NSInteger = msg_send![item, tag];
377            let index = tag as usize;
378            if let Some(action) = platform.menu_item_actions.borrow().get(index) {
379                callback(&action);
380            }
381        }
382    }
383}
384
385unsafe fn ns_string(string: &str) -> id {
386    NSString::alloc(nil).init_str(string).autorelease()
387}