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}