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