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() -> Arc<dyn platform::Platform> {
 94        let result = Arc::new(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        unsafe {
102            let app: id = msg_send![APP_CLASS, sharedApplication];
103            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
104            let self_ptr = result.as_ref() as *const Self as *const c_void;
105            app.setDelegate_(app_delegate);
106            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
107            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
108        }
109
110        result
111    }
112
113    pub fn run() {
114        unsafe {
115            let pool = NSAutoreleasePool::new(nil);
116            let app: id = msg_send![APP_CLASS, sharedApplication];
117
118            app.run();
119            pool.drain();
120            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
121            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
122        }
123    }
124
125    unsafe fn create_menu_bar(&self, menus: &[Menu]) -> id {
126        let menu_bar = NSMenu::new(nil).autorelease();
127        let mut menu_item_actions = self.menu_item_actions.borrow_mut();
128        menu_item_actions.clear();
129
130        for menu_config in menus {
131            let menu_bar_item = NSMenuItem::new(nil).autorelease();
132            let menu = NSMenu::new(nil).autorelease();
133
134            menu.setTitle_(ns_string(menu_config.name));
135
136            for item_config in menu_config.items {
137                let item;
138
139                match item_config {
140                    MenuItem::Separator => {
141                        item = NSMenuItem::separatorItem(nil);
142                    }
143                    MenuItem::Action {
144                        name,
145                        keystroke,
146                        action,
147                    } => {
148                        if let Some(keystroke) = keystroke {
149                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
150                                panic!(
151                                    "Invalid keystroke for menu item {}:{} - {:?}",
152                                    menu_config.name, name, err
153                                )
154                            });
155
156                            let mut mask = NSEventModifierFlags::empty();
157                            for (modifier, flag) in &[
158                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
159                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
160                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
161                            ] {
162                                if *modifier {
163                                    mask |= *flag;
164                                }
165                            }
166
167                            item = NSMenuItem::alloc(nil)
168                                .initWithTitle_action_keyEquivalent_(
169                                    ns_string(name),
170                                    selector("handleGPUIMenuItem:"),
171                                    ns_string(&keystroke.key),
172                                )
173                                .autorelease();
174                            item.setKeyEquivalentModifierMask_(mask);
175                        } else {
176                            item = NSMenuItem::alloc(nil)
177                                .initWithTitle_action_keyEquivalent_(
178                                    ns_string(name),
179                                    selector("handleGPUIMenuItem:"),
180                                    ns_string(""),
181                                )
182                                .autorelease();
183                        }
184
185                        let tag = menu_item_actions.len() as NSInteger;
186                        let _: () = msg_send![item, setTag: tag];
187                        menu_item_actions.push(action.to_string());
188                    }
189                }
190
191                menu.addItem_(item);
192            }
193
194            menu_bar_item.setSubmenu_(menu);
195            menu_bar.addItem_(menu_bar_item);
196        }
197
198        menu_bar
199    }
200}
201
202impl platform::Platform for MacPlatform {
203    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
204        self.callbacks.borrow_mut().become_active = Some(callback);
205    }
206
207    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
208        self.callbacks.borrow_mut().resign_active = Some(callback);
209    }
210
211    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
212        self.callbacks.borrow_mut().event = Some(callback);
213    }
214
215    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
216        self.callbacks.borrow_mut().menu_command = Some(callback);
217    }
218
219    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
220        self.callbacks.borrow_mut().open_files = Some(callback);
221    }
222
223    fn on_finish_launching(&self, callback: Box<dyn FnOnce() -> ()>) {
224        self.callbacks.borrow_mut().finish_launching = Some(callback);
225    }
226
227    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
228        self.dispatcher.clone()
229    }
230
231    fn activate(&self, ignoring_other_apps: bool) {
232        unsafe {
233            let app = NSApplication::sharedApplication(nil);
234            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
235        }
236    }
237
238    fn open_window(
239        &self,
240        options: platform::WindowOptions,
241        executor: Rc<executor::Foreground>,
242    ) -> Result<Box<dyn platform::Window>> {
243        Ok(Box::new(Window::open(options, executor, self.fonts())?))
244    }
245
246    fn prompt_for_paths(
247        &self,
248        options: platform::PathPromptOptions,
249    ) -> Option<Vec<std::path::PathBuf>> {
250        unsafe {
251            let panel = NSOpenPanel::openPanel(nil);
252            panel.setCanChooseDirectories_(options.directories.to_objc());
253            panel.setCanChooseFiles_(options.files.to_objc());
254            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
255            panel.setResolvesAliases_(false.to_objc());
256            let response = panel.runModal();
257            if response == NSModalResponse::NSModalResponseOk {
258                let mut result = Vec::new();
259                let urls = panel.URLs();
260                for i in 0..urls.count() {
261                    let url = urls.objectAtIndex(i);
262                    let string = url.absoluteString();
263                    let string = std::ffi::CStr::from_ptr(string.UTF8String())
264                        .to_string_lossy()
265                        .to_string();
266                    if let Some(path) = string.strip_prefix("file://") {
267                        result.push(PathBuf::from(path));
268                    }
269                }
270                Some(result)
271            } else {
272                None
273            }
274        }
275    }
276
277    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
278        self.fonts.clone()
279    }
280
281    fn quit(&self) {
282        unsafe {
283            let app = NSApplication::sharedApplication(nil);
284            let _: () = msg_send![app, terminate: nil];
285        }
286    }
287
288    fn copy(&self, text: &str) {
289        unsafe {
290            let data = NSData::dataWithBytes_length_(
291                nil,
292                text.as_ptr() as *const c_void,
293                text.len() as u64,
294            );
295            let pasteboard = NSPasteboard::generalPasteboard(nil);
296            pasteboard.clearContents();
297            pasteboard.setData_forType(data, NSPasteboardTypeString);
298        }
299    }
300
301    fn set_menus(&self, menus: &[Menu]) {
302        unsafe {
303            let app: id = msg_send![APP_CLASS, sharedApplication];
304            app.setMainMenu_(self.create_menu_bar(menus));
305        }
306    }
307}
308
309unsafe fn get_platform(object: &mut Object) -> &MacPlatform {
310    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
311    assert!(!platform_ptr.is_null());
312    &*(platform_ptr as *const MacPlatform)
313}
314
315extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
316    unsafe {
317        if let Some(event) = Event::from_native(native_event, None) {
318            let platform = get_platform(this);
319            if let Some(callback) = platform.callbacks.borrow_mut().event.as_mut() {
320                if callback(event) {
321                    return;
322                }
323            }
324        }
325
326        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
327    }
328}
329
330extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
331    unsafe {
332        let app: id = msg_send![APP_CLASS, sharedApplication];
333        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
334
335        let platform = get_platform(this);
336        if let Some(callback) = platform.callbacks.borrow_mut().finish_launching.take() {
337            callback();
338        }
339    }
340}
341
342extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
343    let platform = unsafe { get_platform(this) };
344    if let Some(callback) = platform.callbacks.borrow_mut().become_active.as_mut() {
345        callback();
346    }
347}
348
349extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
350    let platform = unsafe { get_platform(this) };
351    if let Some(callback) = platform.callbacks.borrow_mut().resign_active.as_mut() {
352        callback();
353    }
354}
355
356extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
357    let paths = unsafe {
358        (0..paths.count())
359            .into_iter()
360            .filter_map(|i| {
361                let path = paths.objectAtIndex(i);
362                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
363                    Ok(string) => Some(PathBuf::from(string)),
364                    Err(err) => {
365                        log::error!("error converting path to string: {}", err);
366                        None
367                    }
368                }
369            })
370            .collect::<Vec<_>>()
371    };
372    let platform = unsafe { get_platform(this) };
373    if let Some(callback) = platform.callbacks.borrow_mut().open_files.as_mut() {
374        callback(paths);
375    }
376}
377
378extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
379    unsafe {
380        let platform = get_platform(this);
381        if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() {
382            let tag: NSInteger = msg_send![item, tag];
383            let index = tag as usize;
384            if let Some(action) = platform.menu_item_actions.borrow().get(index) {
385                callback(&action);
386            }
387        }
388    }
389}
390
391unsafe fn ns_string(string: &str) -> id {
392    NSString::alloc(nil).init_str(string).autorelease()
393}