platform.rs

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