Add lifecycle methods to Platform trait

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

gpui/src/app.rs                   |  12 
gpui/src/platform/mac/app.rs      | 100 --------
gpui/src/platform/mac/mod.rs      |  11 
gpui/src/platform/mac/platform.rs | 387 +++++++++++++++++++++++++++++++++
gpui/src/platform/mod.rs          |  13 
gpui/src/platform/test.rs         |  31 ++
6 files changed, 433 insertions(+), 121 deletions(-)

Detailed changes

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<dyn platform::App> {
+    pub fn platform(&self) -> Arc<dyn platform::Platform> {
         self.0.borrow().platform.clone()
     }
 }
@@ -309,7 +309,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext);
 
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
-    platform: Arc<dyn platform::App>,
+    platform: Arc<dyn platform::Platform>,
     font_cache: Arc<FontCache>,
     assets: Arc<AssetCache>,
     ctx: AppContext,
@@ -337,7 +337,7 @@ pub struct MutableAppContext {
 impl MutableAppContext {
     pub fn new(
         foreground: Rc<executor::Foreground>,
-        platform: Arc<dyn platform::App>,
+        platform: Arc<dyn platform::Platform>,
         asset_source: impl AssetSource,
     ) -> Self {
         let fonts = platform.fonts();
@@ -381,7 +381,7 @@ impl MutableAppContext {
         &self.ctx
     }
 
-    pub fn platform(&self) -> Arc<dyn platform::App> {
+    pub fn platform(&self) -> Arc<dyn platform::Platform> {
         self.platform.clone()
     }
 

gpui/src/platform/mac/app.rs 🔗

@@ -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<Dispatcher>,
-    fonts: Arc<FontSystem>,
-}
-
-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<dyn platform::Dispatcher> {
-        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<executor::Foreground>,
-    ) -> Result<Box<dyn platform::Window>> {
-        Ok(Box::new(Window::open(options, executor, self.fonts())?))
-    }
-
-    fn prompt_for_paths(
-        &self,
-        options: platform::PathPromptOptions,
-    ) -> Option<Vec<std::path::PathBuf>> {
-        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<dyn platform::FontSystem> {
-        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);
-        }
-    }
-}

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()
 }
 

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<Dispatcher>,
+    fonts: Arc<FontSystem>,
+    callbacks: RefCell<Callbacks>,
+    menu_item_actions: RefCell<Vec<String>>,
+}
+
+#[derive(Default)]
+struct Callbacks {
+    become_active: Option<Box<dyn FnMut()>>,
+    resign_active: Option<Box<dyn FnMut()>>,
+    event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
+    menu_command: Option<Box<dyn FnMut(&str)>>,
+    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
+    finish_launching: Option<Box<dyn FnOnce() -> ()>>,
+}
+
+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<dyn FnMut()>) {
+        self.callbacks.borrow_mut().become_active = Some(callback);
+    }
+
+    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+        self.callbacks.borrow_mut().resign_active = Some(callback);
+    }
+
+    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
+        self.callbacks.borrow_mut().event = Some(callback);
+    }
+
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
+        self.callbacks.borrow_mut().menu_command = Some(callback);
+    }
+
+    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
+        self.callbacks.borrow_mut().open_files = Some(callback);
+    }
+
+    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
+        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::<c_void>());
+            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+        }
+    }
+
+    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
+        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<executor::Foreground>,
+    ) -> Result<Box<dyn platform::Window>> {
+        Ok(Box::new(Window::open(options, executor, self.fonts())?))
+    }
+
+    fn prompt_for_paths(
+        &self,
+        options: platform::PathPromptOptions,
+    ) -> Option<Vec<std::path::PathBuf>> {
+        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<dyn platform::FontSystem> {
+        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::<Vec<_>>()
+    };
+    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()
+}

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<dyn FnMut(&str)>);
+    fn on_become_active(&self, callback: Box<dyn FnMut()>);
+    fn on_resign_active(&self, callback: Box<dyn FnMut()>);
+    fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
+    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
+    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
+
     fn dispatcher(&self) -> Arc<dyn Dispatcher>;
+    fn fonts(&self) -> Arc<dyn FontSystem>;
+
     fn activate(&self, ignoring_other_apps: bool);
     fn open_window(
         &self,
@@ -42,9 +51,9 @@ pub trait App {
         executor: Rc<executor::Foreground>,
     ) -> Result<Box<dyn Window>>;
     fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
-    fn fonts(&self) -> Arc<dyn FontSystem>;
     fn quit(&self);
     fn copy(&self, text: &str);
+    fn set_menus(&self, menus: &[Menu]);
 }
 
 pub trait Dispatcher: Send + Sync {

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<dyn super::Dispatcher>,
     fonts: Arc<dyn super::FontSystem>,
 }
@@ -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<dyn FnMut(&str)>) {}
+
+    fn on_become_active(&self, _: Box<dyn FnMut()>) {}
+
+    fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
+
+    fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
+
+    fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
+
+    fn run(&self, _on_finish_launching: Box<dyn FnOnce() -> ()>) {
+        unimplemented!()
+    }
+
     fn dispatcher(&self) -> Arc<dyn super::Dispatcher> {
         self.dispatcher.clone()
     }
 
+    fn fonts(&self) -> std::sync::Arc<dyn super::FontSystem> {
+        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<dyn super::FontSystem> {
-        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()
 }