diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 63f52f77b1..14380f19e1 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2,7 +2,7 @@ use crate::{ elements::ElementBox, executor, keymap::{self, Keystroke}, - platform::{self, App as _, WindowOptions}, + platform::{self, Platform as _, WindowOptions}, presenter::Presenter, util::post_inc, AssetCache, AssetSource, FontCache, TextLayoutCache, @@ -88,7 +88,7 @@ impl App { asset_source: A, f: G, ) -> T { - let platform = platform::test::app(); + let platform = platform::test::platform(); let foreground = Rc::new(executor::Foreground::test()); let app = Self(Rc::new(RefCell::new(MutableAppContext::new( foreground.clone(), @@ -269,7 +269,7 @@ impl App { self.0.borrow().font_cache.clone() } - pub fn platform(&self) -> Arc { + pub fn platform(&self) -> Arc { self.0.borrow().platform.clone() } } @@ -309,7 +309,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext); pub struct MutableAppContext { weak_self: Option>>, - platform: Arc, + platform: Arc, font_cache: Arc, assets: Arc, ctx: AppContext, @@ -337,7 +337,7 @@ pub struct MutableAppContext { impl MutableAppContext { pub fn new( foreground: Rc, - platform: Arc, + platform: Arc, asset_source: impl AssetSource, ) -> Self { let fonts = platform.fonts(); @@ -381,7 +381,7 @@ impl MutableAppContext { &self.ctx } - pub fn platform(&self) -> Arc { + pub fn platform(&self) -> Arc { self.platform.clone() } diff --git a/gpui/src/platform/mac/app.rs b/gpui/src/platform/mac/app.rs deleted file mode 100644 index 1965c634a3..0000000000 --- a/gpui/src/platform/mac/app.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::{BoolExt as _, Dispatcher, FontSystem, Window}; -use crate::{executor, platform}; -use anyhow::Result; -use cocoa::{ - appkit::{NSApplication, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString}, - base::nil, - foundation::{NSArray, NSData, NSString, NSURL}, -}; -use objc::{msg_send, sel, sel_impl}; -use std::{ffi::c_void, path::PathBuf, rc::Rc, sync::Arc}; - -pub struct App { - dispatcher: Arc, - fonts: Arc, -} - -impl App { - pub fn new() -> Self { - Self { - dispatcher: Arc::new(Dispatcher), - fonts: Arc::new(FontSystem::new()), - } - } -} - -impl platform::App for App { - fn dispatcher(&self) -> Arc { - self.dispatcher.clone() - } - - fn activate(&self, ignoring_other_apps: bool) { - unsafe { - let app = NSApplication::sharedApplication(nil); - app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); - } - } - - fn open_window( - &self, - options: platform::WindowOptions, - executor: Rc, - ) -> Result> { - Ok(Box::new(Window::open(options, executor, self.fonts())?)) - } - - fn prompt_for_paths( - &self, - options: platform::PathPromptOptions, - ) -> Option> { - unsafe { - let panel = NSOpenPanel::openPanel(nil); - panel.setCanChooseDirectories_(options.directories.to_objc()); - panel.setCanChooseFiles_(options.files.to_objc()); - panel.setAllowsMultipleSelection_(options.multiple.to_objc()); - panel.setResolvesAliases_(false.to_objc()); - let response = panel.runModal(); - if response == NSModalResponse::NSModalResponseOk { - let mut result = Vec::new(); - let urls = panel.URLs(); - for i in 0..urls.count() { - let url = urls.objectAtIndex(i); - let string = url.absoluteString(); - let string = std::ffi::CStr::from_ptr(string.UTF8String()) - .to_string_lossy() - .to_string(); - if let Some(path) = string.strip_prefix("file://") { - result.push(PathBuf::from(path)); - } - } - Some(result) - } else { - None - } - } - } - - fn fonts(&self) -> Arc { - self.fonts.clone() - } - - fn quit(&self) { - unsafe { - let app = NSApplication::sharedApplication(nil); - let _: () = msg_send![app, terminate: nil]; - } - } - - fn copy(&self, text: &str) { - unsafe { - let data = NSData::dataWithBytes_length_( - nil, - text.as_ptr() as *const c_void, - text.len() as u64, - ); - let pasteboard = NSPasteboard::generalPasteboard(nil); - pasteboard.clearContents(); - pasteboard.setData_forType(data, NSPasteboardTypeString); - } - } -} diff --git a/gpui/src/platform/mac/mod.rs b/gpui/src/platform/mac/mod.rs index b7a19c3647..b54d148682 100644 --- a/gpui/src/platform/mac/mod.rs +++ b/gpui/src/platform/mac/mod.rs @@ -1,27 +1,26 @@ -mod app; mod atlas; mod dispatcher; mod event; mod fonts; mod geometry; +mod platform; mod renderer; mod runner; mod sprite_cache; mod window; -use crate::platform; -pub use app::App; use cocoa::base::{BOOL, NO, YES}; pub use dispatcher::Dispatcher; pub use fonts::FontSystem; +use platform::MacPlatform; pub use runner::Runner; use window::Window; -pub fn app() -> impl platform::App { - App::new() +pub fn app() -> impl super::Platform { + MacPlatform::new() } -pub fn runner() -> impl platform::Runner { +pub fn runner() -> impl super::Runner { Runner::new() } diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs new file mode 100644 index 0000000000..d089ba26d2 --- /dev/null +++ b/gpui/src/platform/mac/platform.rs @@ -0,0 +1,387 @@ +use super::{BoolExt as _, Dispatcher, FontSystem, Window}; +use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem}; +use anyhow::Result; +use cocoa::{ + appkit::{ + NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, + NSPasteboardTypeString, NSWindow, + }, + base::{id, nil, selector}, + foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use ptr::null_mut; +use std::{ + cell::RefCell, + ffi::{c_void, CStr}, + os::raw::c_char, + path::PathBuf, + ptr, + rc::Rc, + sync::Arc, +}; + +const MAC_PLATFORM_IVAR: &'static str = "runner"; +static mut APP_CLASS: *const Class = ptr::null(); +static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_classes() { + APP_CLASS = { + let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(sendEvent:), + send_event as extern "C" fn(&mut Object, Sel, id), + ); + decl.register() + }; + + APP_DELEGATE_CLASS = { + let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidBecomeActive:), + did_become_active as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidResignActive:), + did_resign_active as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(handleGPUIMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(application:openFiles:), + open_files as extern "C" fn(&mut Object, Sel, id, id), + ); + decl.register() + } +} + +pub struct MacPlatform { + dispatcher: Arc, + fonts: Arc, + callbacks: RefCell, + menu_item_actions: RefCell>, +} + +#[derive(Default)] +struct Callbacks { + become_active: Option>, + resign_active: Option>, + event: Option bool>>, + menu_command: Option>, + open_files: Option)>>, + finish_launching: Option ()>>, +} + +impl MacPlatform { + pub fn new() -> Self { + Self { + dispatcher: Arc::new(Dispatcher), + fonts: Arc::new(FontSystem::new()), + callbacks: Default::default(), + menu_item_actions: Default::default(), + } + } +} + +impl platform::Platform for MacPlatform { + fn on_become_active(&self, callback: Box) { + self.callbacks.borrow_mut().become_active = Some(callback); + } + + fn on_resign_active(&self, callback: Box) { + self.callbacks.borrow_mut().resign_active = Some(callback); + } + + fn on_event(&self, callback: Box bool>) { + self.callbacks.borrow_mut().event = Some(callback); + } + + fn on_menu_command(&self, callback: Box) { + self.callbacks.borrow_mut().menu_command = Some(callback); + } + + fn on_open_files(&self, callback: Box)>) { + self.callbacks.borrow_mut().open_files = Some(callback); + } + + fn run(&self, on_finish_launching: Box ()>) { + self.callbacks.borrow_mut().finish_launching = Some(on_finish_launching); + + unsafe { + let pool = NSAutoreleasePool::new(nil); + let app: id = msg_send![APP_CLASS, sharedApplication]; + let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; + + let self_ptr = self as *const Self as *mut c_void; + (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + app.setDelegate_(app_delegate); + app.run(); + pool.drain(); + (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + } + } + + fn dispatcher(&self) -> Arc { + self.dispatcher.clone() + } + + fn activate(&self, ignoring_other_apps: bool) { + unsafe { + let app = NSApplication::sharedApplication(nil); + app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); + } + } + + fn open_window( + &self, + options: platform::WindowOptions, + executor: Rc, + ) -> Result> { + Ok(Box::new(Window::open(options, executor, self.fonts())?)) + } + + fn prompt_for_paths( + &self, + options: platform::PathPromptOptions, + ) -> Option> { + unsafe { + let panel = NSOpenPanel::openPanel(nil); + panel.setCanChooseDirectories_(options.directories.to_objc()); + panel.setCanChooseFiles_(options.files.to_objc()); + panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + panel.setResolvesAliases_(false.to_objc()); + let response = panel.runModal(); + if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + let string = url.absoluteString(); + let string = std::ffi::CStr::from_ptr(string.UTF8String()) + .to_string_lossy() + .to_string(); + if let Some(path) = string.strip_prefix("file://") { + result.push(PathBuf::from(path)); + } + } + Some(result) + } else { + None + } + } + } + + fn fonts(&self) -> Arc { + self.fonts.clone() + } + + fn quit(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, terminate: nil]; + } + } + + fn copy(&self, text: &str) { + unsafe { + let data = NSData::dataWithBytes_length_( + nil, + text.as_ptr() as *const c_void, + text.len() as u64, + ); + let pasteboard = NSPasteboard::generalPasteboard(nil); + pasteboard.clearContents(); + pasteboard.setData_forType(data, NSPasteboardTypeString); + } + } + + fn set_menus(&self, menus: &[Menu]) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setMainMenu_(self.create_menu_bar(menus)); + } + } +} + +impl MacPlatform { + unsafe fn create_menu_bar(&self, menus: &[Menu]) -> id { + let menu_bar = NSMenu::new(nil).autorelease(); + let mut menu_item_actions = self.menu_item_actions.borrow_mut(); + menu_item_actions.clear(); + + for menu_config in menus { + let menu_bar_item = NSMenuItem::new(nil).autorelease(); + let menu = NSMenu::new(nil).autorelease(); + + menu.setTitle_(ns_string(menu_config.name)); + + for item_config in menu_config.items { + let item; + + match item_config { + MenuItem::Separator => { + item = NSMenuItem::separatorItem(nil); + } + MenuItem::Action { + name, + keystroke, + action, + } => { + if let Some(keystroke) = keystroke { + let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| { + panic!( + "Invalid keystroke for menu item {}:{} - {:?}", + menu_config.name, name, err + ) + }); + + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(&keystroke.key), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(""), + ) + .autorelease(); + } + + let tag = menu_item_actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + menu_item_actions.push(action.to_string()); + } + } + + menu.addItem_(item); + } + + menu_bar_item.setSubmenu_(menu); + menu_bar.addItem_(menu_bar_item); + } + + menu_bar + } +} + +unsafe fn get_platform(object: &mut Object) -> &MacPlatform { + let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); + assert!(!platform_ptr.is_null()); + &*(platform_ptr as *const MacPlatform) +} + +extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { + unsafe { + if let Some(event) = Event::from_native(native_event, None) { + let platform = get_platform(this); + if let Some(callback) = platform.callbacks.borrow_mut().event.as_mut() { + if callback(event) { + return; + } + } + } + + msg_send![super(this, class!(NSApplication)), sendEvent: native_event] + } +} + +extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + + let platform = get_platform(this); + if let Some(callback) = platform.callbacks.borrow_mut().finish_launching.take() { + callback(); + } + } +} + +extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_platform(this) }; + if let Some(callback) = platform.callbacks.borrow_mut().become_active.as_mut() { + callback(); + } +} + +extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_platform(this) }; + if let Some(callback) = platform.callbacks.borrow_mut().resign_active.as_mut() { + callback(); + } +} + +extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) { + let paths = unsafe { + (0..paths.count()) + .into_iter() + .filter_map(|i| { + let path = paths.objectAtIndex(i); + match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() { + Ok(string) => Some(PathBuf::from(string)), + Err(err) => { + log::error!("error converting path to string: {}", err); + None + } + } + }) + .collect::>() + }; + let platform = unsafe { get_platform(this) }; + if let Some(callback) = platform.callbacks.borrow_mut().open_files.as_mut() { + callback(paths); + } +} + +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let platform = get_platform(this); + if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = platform.menu_item_actions.borrow().get(index) { + callback(&action); + } + } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index ae0f3cddb9..b96bffb979 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -33,8 +33,17 @@ pub trait Runner { fn run(self); } -pub trait App { +pub trait Platform { + fn on_menu_command(&self, callback: Box); + fn on_become_active(&self, callback: Box); + fn on_resign_active(&self, callback: Box); + fn on_event(&self, callback: Box bool>); + fn on_open_files(&self, callback: Box)>); + fn run(&self, on_finish_launching: Box ()>); + fn dispatcher(&self) -> Arc; + fn fonts(&self) -> Arc; + fn activate(&self, ignoring_other_apps: bool); fn open_window( &self, @@ -42,9 +51,9 @@ pub trait App { executor: Rc, ) -> Result>; fn prompt_for_paths(&self, options: PathPromptOptions) -> Option>; - fn fonts(&self) -> Arc; fn quit(&self); fn copy(&self, text: &str); + fn set_menus(&self, menus: &[Menu]); } pub trait Dispatcher: Send + Sync { diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 9f719bd2bc..08ec95ca74 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -2,7 +2,7 @@ use pathfinder_geometry::vector::Vector2F; use std::rc::Rc; use std::sync::Arc; -struct App { +struct Platform { dispatcher: Arc, fonts: Arc, } @@ -19,7 +19,7 @@ pub struct Window { pub struct WindowContext {} -impl App { +impl Platform { fn new() -> Self { Self { dispatcher: Arc::new(Dispatcher), @@ -28,11 +28,29 @@ impl App { } } -impl super::App for App { +impl super::Platform for Platform { + fn on_menu_command(&self, _: Box) {} + + fn on_become_active(&self, _: Box) {} + + fn on_resign_active(&self, _: Box) {} + + fn on_event(&self, _: Box bool>) {} + + fn on_open_files(&self, _: Box)>) {} + + fn run(&self, _on_finish_launching: Box ()>) { + unimplemented!() + } + fn dispatcher(&self) -> Arc { self.dispatcher.clone() } + fn fonts(&self) -> std::sync::Arc { + self.fonts.clone() + } + fn activate(&self, _ignoring_other_apps: bool) {} fn open_window( @@ -43,8 +61,7 @@ impl super::App for App { Ok(Box::new(Window::new(options.bounds.size()))) } - fn fonts(&self) -> std::sync::Arc { - self.fonts.clone() + fn set_menus(&self, _menus: &[crate::Menu]) { } fn quit(&self) {} @@ -102,6 +119,6 @@ impl super::Window for Window { } } -pub fn app() -> impl super::App { - App::new() +pub fn platform() -> impl super::Platform { + Platform::new() }