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 = "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>>,
 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    unsafe fn create_menu_bar(&self, menus: &[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
111            menu.setTitle_(ns_string(menu_config.name));
112
113            for item_config in menu_config.items {
114                let item;
115
116                match item_config {
117                    MenuItem::Separator => {
118                        item = NSMenuItem::separatorItem(nil);
119                    }
120                    MenuItem::Action {
121                        name,
122                        keystroke,
123                        action,
124                    } => {
125                        if let Some(keystroke) = keystroke {
126                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
127                                panic!(
128                                    "Invalid keystroke for menu item {}:{} - {:?}",
129                                    menu_config.name, name, err
130                                )
131                            });
132
133                            let mut mask = NSEventModifierFlags::empty();
134                            for (modifier, flag) in &[
135                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
136                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
137                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
138                            ] {
139                                if *modifier {
140                                    mask |= *flag;
141                                }
142                            }
143
144                            item = NSMenuItem::alloc(nil)
145                                .initWithTitle_action_keyEquivalent_(
146                                    ns_string(name),
147                                    selector("handleGPUIMenuItem:"),
148                                    ns_string(&keystroke.key),
149                                )
150                                .autorelease();
151                            item.setKeyEquivalentModifierMask_(mask);
152                        } else {
153                            item = NSMenuItem::alloc(nil)
154                                .initWithTitle_action_keyEquivalent_(
155                                    ns_string(name),
156                                    selector("handleGPUIMenuItem:"),
157                                    ns_string(""),
158                                )
159                                .autorelease();
160                        }
161
162                        let tag = menu_item_actions.len() as NSInteger;
163                        let _: () = msg_send![item, setTag: tag];
164                        menu_item_actions.push(action.to_string());
165                    }
166                }
167
168                menu.addItem_(item);
169            }
170
171            menu_bar_item.setSubmenu_(menu);
172            menu_bar.addItem_(menu_bar_item);
173        }
174
175        menu_bar
176    }
177}
178
179impl platform::Platform for MacPlatform {
180    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
181        self.callbacks.borrow_mut().become_active = Some(callback);
182    }
183
184    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
185        self.callbacks.borrow_mut().resign_active = Some(callback);
186    }
187
188    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
189        self.callbacks.borrow_mut().event = Some(callback);
190    }
191
192    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
193        self.callbacks.borrow_mut().menu_command = Some(callback);
194    }
195
196    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
197        self.callbacks.borrow_mut().open_files = Some(callback);
198    }
199
200    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
201        self.callbacks.borrow_mut().finish_launching = Some(on_finish_launching);
202
203        unsafe {
204            let app: id = msg_send![APP_CLASS, sharedApplication];
205            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
206            app.setDelegate_(app_delegate);
207
208            let self_ptr = self as *const Self as *const c_void;
209            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
210            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
211
212            let pool = NSAutoreleasePool::new(nil);
213            app.run();
214            pool.drain();
215
216            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
217            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
218        }
219    }
220
221    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
222        self.dispatcher.clone()
223    }
224
225    fn activate(&self, ignoring_other_apps: bool) {
226        unsafe {
227            let app = NSApplication::sharedApplication(nil);
228            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
229        }
230    }
231
232    fn open_window(
233        &self,
234        options: platform::WindowOptions,
235        executor: Rc<executor::Foreground>,
236    ) -> Result<Box<dyn platform::Window>> {
237        Ok(Box::new(Window::open(options, executor, self.fonts())?))
238    }
239
240    fn prompt_for_paths(
241        &self,
242        options: platform::PathPromptOptions,
243    ) -> Option<Vec<std::path::PathBuf>> {
244        unsafe {
245            let panel = NSOpenPanel::openPanel(nil);
246            panel.setCanChooseDirectories_(options.directories.to_objc());
247            panel.setCanChooseFiles_(options.files.to_objc());
248            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
249            panel.setResolvesAliases_(false.to_objc());
250            let response = panel.runModal();
251            if response == NSModalResponse::NSModalResponseOk {
252                let mut result = Vec::new();
253                let urls = panel.URLs();
254                for i in 0..urls.count() {
255                    let url = urls.objectAtIndex(i);
256                    let string = url.absoluteString();
257                    let string = std::ffi::CStr::from_ptr(string.UTF8String())
258                        .to_string_lossy()
259                        .to_string();
260                    if let Some(path) = string.strip_prefix("file://") {
261                        result.push(PathBuf::from(path));
262                    }
263                }
264                Some(result)
265            } else {
266                None
267            }
268        }
269    }
270
271    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
272        self.fonts.clone()
273    }
274
275    fn quit(&self) {
276        unsafe {
277            let app = NSApplication::sharedApplication(nil);
278            let _: () = msg_send![app, terminate: nil];
279        }
280    }
281
282    fn copy(&self, text: &str) {
283        unsafe {
284            let data = NSData::dataWithBytes_length_(
285                nil,
286                text.as_ptr() as *const c_void,
287                text.len() as u64,
288            );
289            let pasteboard = NSPasteboard::generalPasteboard(nil);
290            pasteboard.clearContents();
291            pasteboard.setData_forType(data, NSPasteboardTypeString);
292        }
293    }
294
295    fn set_menus(&self, menus: &[Menu]) {
296        unsafe {
297            let app: id = msg_send![APP_CLASS, sharedApplication];
298            app.setMainMenu_(self.create_menu_bar(menus));
299        }
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}