runner.rs

  1use crate::{keymap::Keystroke, platform::Event, Menu, MenuItem};
  2use cocoa::{
  3    appkit::{
  4        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
  5        NSEventModifierFlags, NSMenu, NSMenuItem, NSWindow,
  6    },
  7    base::{id, nil, selector},
  8    foundation::{NSArray, NSAutoreleasePool, NSInteger, NSString},
  9};
 10use ctor::ctor;
 11use objc::{
 12    class,
 13    declare::ClassDecl,
 14    msg_send,
 15    runtime::{Class, Object, Sel},
 16    sel, sel_impl,
 17};
 18use std::{
 19    ffi::CStr,
 20    os::raw::{c_char, c_void},
 21    path::PathBuf,
 22    ptr,
 23};
 24
 25const RUNNER_IVAR: &'static str = "runner";
 26static mut APP_CLASS: *const Class = ptr::null();
 27static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
 28
 29#[ctor]
 30unsafe fn build_classes() {
 31    APP_CLASS = {
 32        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
 33        decl.add_ivar::<*mut c_void>(RUNNER_IVAR);
 34        decl.add_method(
 35            sel!(sendEvent:),
 36            send_event as extern "C" fn(&mut Object, Sel, id),
 37        );
 38        decl.register()
 39    };
 40
 41    APP_DELEGATE_CLASS = {
 42        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
 43        decl.add_ivar::<*mut c_void>(RUNNER_IVAR);
 44        decl.add_method(
 45            sel!(applicationDidFinishLaunching:),
 46            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
 47        );
 48        decl.add_method(
 49            sel!(applicationDidBecomeActive:),
 50            did_become_active as extern "C" fn(&mut Object, Sel, id),
 51        );
 52        decl.add_method(
 53            sel!(applicationDidResignActive:),
 54            did_resign_active as extern "C" fn(&mut Object, Sel, id),
 55        );
 56        decl.add_method(
 57            sel!(handleGPUIMenuItem:),
 58            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
 59        );
 60        decl.add_method(
 61            sel!(application:openFiles:),
 62            open_files as extern "C" fn(&mut Object, Sel, id, id),
 63        );
 64        decl.register()
 65    }
 66}
 67
 68#[derive(Default)]
 69pub struct Runner {
 70    finish_launching_callback: Option<Box<dyn FnOnce()>>,
 71    become_active_callback: Option<Box<dyn FnMut()>>,
 72    resign_active_callback: Option<Box<dyn FnMut()>>,
 73    event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
 74    open_files_callback: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
 75    menu_command_callback: Option<Box<dyn FnMut(&str)>>,
 76    menu_item_actions: Vec<String>,
 77}
 78
 79impl Runner {
 80    pub fn new() -> Self {
 81        Default::default()
 82    }
 83
 84    unsafe fn create_menu_bar(&mut self, menus: &[Menu]) -> id {
 85        let menu_bar = NSMenu::new(nil).autorelease();
 86        self.menu_item_actions.clear();
 87
 88        for menu_config in menus {
 89            let menu_bar_item = NSMenuItem::new(nil).autorelease();
 90            let menu = NSMenu::new(nil).autorelease();
 91
 92            menu.setTitle_(ns_string(menu_config.name));
 93
 94            for item_config in menu_config.items {
 95                let item;
 96
 97                match item_config {
 98                    MenuItem::Separator => {
 99                        item = NSMenuItem::separatorItem(nil);
100                    }
101                    MenuItem::Action {
102                        name,
103                        keystroke,
104                        action,
105                    } => {
106                        if let Some(keystroke) = keystroke {
107                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
108                                panic!(
109                                    "Invalid keystroke for menu item {}:{} - {:?}",
110                                    menu_config.name, name, err
111                                )
112                            });
113
114                            let mut mask = NSEventModifierFlags::empty();
115                            for (modifier, flag) in &[
116                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
117                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
118                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
119                            ] {
120                                if *modifier {
121                                    mask |= *flag;
122                                }
123                            }
124
125                            item = NSMenuItem::alloc(nil)
126                                .initWithTitle_action_keyEquivalent_(
127                                    ns_string(name),
128                                    selector("handleGPUIMenuItem:"),
129                                    ns_string(&keystroke.key),
130                                )
131                                .autorelease();
132                            item.setKeyEquivalentModifierMask_(mask);
133                        } else {
134                            item = NSMenuItem::alloc(nil)
135                                .initWithTitle_action_keyEquivalent_(
136                                    ns_string(name),
137                                    selector("handleGPUIMenuItem:"),
138                                    ns_string(""),
139                                )
140                                .autorelease();
141                        }
142
143                        let tag = self.menu_item_actions.len() as NSInteger;
144                        let _: () = msg_send![item, setTag: tag];
145                        self.menu_item_actions.push(action.to_string());
146                    }
147                }
148
149                menu.addItem_(item);
150            }
151
152            menu_bar_item.setSubmenu_(menu);
153            menu_bar.addItem_(menu_bar_item);
154        }
155
156        menu_bar
157    }
158}
159
160impl crate::platform::Runner for Runner {
161    fn on_finish_launching<F: 'static + FnOnce()>(mut self, callback: F) -> Self {
162        self.finish_launching_callback = Some(Box::new(callback));
163        self
164    }
165
166    fn on_menu_command<F: 'static + FnMut(&str)>(mut self, callback: F) -> Self {
167        self.menu_command_callback = Some(Box::new(callback));
168        self
169    }
170
171    fn on_become_active<F: 'static + FnMut()>(mut self, callback: F) -> Self {
172        log::info!("become active");
173        self.become_active_callback = Some(Box::new(callback));
174        self
175    }
176
177    fn on_resign_active<F: 'static + FnMut()>(mut self, callback: F) -> Self {
178        self.resign_active_callback = Some(Box::new(callback));
179        self
180    }
181
182    fn on_event<F: 'static + FnMut(Event) -> bool>(mut self, callback: F) -> Self {
183        self.event_callback = Some(Box::new(callback));
184        self
185    }
186
187    fn on_open_files<F: 'static + FnMut(Vec<PathBuf>)>(mut self, callback: F) -> Self {
188        self.open_files_callback = Some(Box::new(callback));
189        self
190    }
191
192    fn set_menus(mut self, menus: &[Menu]) -> Self {
193        unsafe {
194            let app: id = msg_send![APP_CLASS, sharedApplication];
195            app.setMainMenu_(self.create_menu_bar(menus));
196        }
197        self
198    }
199
200    fn run(self) {
201        unsafe {
202            let self_ptr = Box::into_raw(Box::new(self));
203
204            let pool = NSAutoreleasePool::new(nil);
205            let app: id = msg_send![APP_CLASS, sharedApplication];
206            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
207
208            (*app).set_ivar(RUNNER_IVAR, self_ptr as *mut c_void);
209            (*app_delegate).set_ivar(RUNNER_IVAR, self_ptr as *mut c_void);
210            app.setDelegate_(app_delegate);
211            app.run();
212            pool.drain();
213
214            // The Runner is done running when we get here, so we can reinstantiate the Box and drop it.
215            Box::from_raw(self_ptr);
216        }
217    }
218}
219
220unsafe fn get_runner(object: &mut Object) -> &mut Runner {
221    let runner_ptr: *mut c_void = *object.get_ivar(RUNNER_IVAR);
222    &mut *(runner_ptr as *mut Runner)
223}
224
225extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
226    let event = unsafe { Event::from_native(native_event, None) };
227
228    if let Some(event) = event {
229        let runner = unsafe { get_runner(this) };
230        if let Some(callback) = runner.event_callback.as_mut() {
231            if callback(event) {
232                return;
233            }
234        }
235    }
236
237    unsafe {
238        let _: () = msg_send![super(this, class!(NSApplication)), sendEvent: native_event];
239    }
240}
241
242extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
243    unsafe {
244        let app: id = msg_send![APP_CLASS, sharedApplication];
245        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
246
247        let runner = get_runner(this);
248        if let Some(callback) = runner.finish_launching_callback.take() {
249            callback();
250        }
251    }
252}
253
254extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
255    let runner = unsafe { get_runner(this) };
256    if let Some(callback) = runner.become_active_callback.as_mut() {
257        callback();
258    }
259}
260
261extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
262    let runner = unsafe { get_runner(this) };
263    if let Some(callback) = runner.resign_active_callback.as_mut() {
264        callback();
265    }
266}
267
268extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
269    let paths = unsafe {
270        (0..paths.count())
271            .into_iter()
272            .filter_map(|i| {
273                let path = paths.objectAtIndex(i);
274                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
275                    Ok(string) => Some(PathBuf::from(string)),
276                    Err(err) => {
277                        log::error!("error converting path to string: {}", err);
278                        None
279                    }
280                }
281            })
282            .collect::<Vec<_>>()
283    };
284    let runner = unsafe { get_runner(this) };
285    if let Some(callback) = runner.open_files_callback.as_mut() {
286        callback(paths);
287    }
288}
289
290extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
291    unsafe {
292        let runner = get_runner(this);
293        if let Some(callback) = runner.menu_command_callback.as_mut() {
294            let tag: NSInteger = msg_send![item, tag];
295            let index = tag as usize;
296            if let Some(action) = runner.menu_item_actions.get(index) {
297                callback(&action);
298            }
299        }
300    }
301}
302
303unsafe fn ns_string(string: &str) -> id {
304    NSString::alloc(nil).init_str(string).autorelease()
305}