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}