From b2f1a3ab4f04061ebe76c2d0afc99cad2f547744 Mon Sep 17 00:00:00 2001 From: Yu-Wei Wu Date: Sat, 7 May 2022 21:19:42 +0800 Subject: [PATCH] Add macos port Some configurations haven't implemented yet or just seem not possible. Co-authored-by: Jason Tsai --- Cargo.toml | 5 + src/platform_impl/macos/menu_item.rs | 194 +++++++++++++++++++++++++++ src/platform_impl/macos/mod.rs | 64 +++++++++ src/platform_impl/mod.rs | 3 + 4 files changed, 266 insertions(+) create mode 100644 src/platform_impl/macos/menu_item.rs create mode 100644 src/platform_impl/macos/mod.rs diff --git a/Cargo.toml b/Cargo.toml index c3345e2..8c944d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,11 @@ features = [ parking_lot = "0.12" gtk = "0.15" +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.24" +objc = "0.2" +#core-graphics = "0.22" + [dev-dependencies] winit = "0.26" tao = { path = "../tao", default-features = false } diff --git a/src/platform_impl/macos/menu_item.rs b/src/platform_impl/macos/menu_item.rs new file mode 100644 index 0000000..f9eb10a --- /dev/null +++ b/src/platform_impl/macos/menu_item.rs @@ -0,0 +1,194 @@ +use cocoa::{ + appkit::{NSButton, NSEventModifierFlags, NSMenuItem}, + base::{id, nil, NO, YES}, + foundation::NSString, +}; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::sync::Once; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +/// Identifier of a custom menu item. +/// +/// Whenever you receive an event arising from a particular menu, this event contains a `MenuId` which +/// identifies its origin. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct MenuId(pub u64); + +impl From for u64 { + fn from(s: MenuId) -> u64 { + s.0 + } +} + +impl MenuId { + /// Return an empty `MenuId`. + pub const EMPTY: MenuId = MenuId(0); + + /// Create new `MenuId` from a String. + pub fn new(unique_string: &str) -> MenuId { + MenuId(hash_string_to_u64(unique_string)) + } + + /// Whenever this menu is empty. + pub fn is_empty(self) -> bool { + Self::EMPTY == self + } +} + +fn hash_string_to_u64(title: &str) -> u64 { + let mut s = DefaultHasher::new(); + title.to_uppercase().hash(&mut s); + s.finish() as u64 +} + +#[derive(Debug, Clone)] +pub struct TextMenuItem { + pub(crate) id: MenuId, + pub(crate) ns_menu_item: id, +} + +impl TextMenuItem { + pub fn new(label: impl AsRef, enabled: bool, selector: Sel) -> Self { + let (id, ns_menu_item) = make_menu_item(label.as_ref(), selector); + + unsafe { + (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id.0); + let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item]; + + if !enabled { + let () = msg_send![ns_menu_item, setEnabled: NO]; + } + } + + Self { id, ns_menu_item } + } + + pub fn label(&self) -> String { + todo!() + } + + pub fn set_label(&mut self, label: impl AsRef) { + todo!() + } + + pub fn enabled(&self) -> bool { + todo!() + } + + pub fn set_enabled(&mut self, enabled: bool) { + todo!() + } + + pub fn id(&self) -> u64 { + self.id.0 + } +} + +pub fn make_menu_item( + title: &str, + selector: Sel, + //key_equivalent: Option, + //menu_type: MenuType, +) -> (MenuId, *mut Object) { + let alloc = make_menu_item_alloc(); + let menu_id = MenuId::new(title); + + unsafe { + let title = NSString::alloc(nil).init_str(title); + let menu_item = make_menu_item_from_alloc(alloc, title, selector); //, key_equivalent, menu_type); + + (menu_id, menu_item) + } +} + +fn make_menu_item_alloc() -> *mut Object { + unsafe { msg_send![make_menu_item_class(), alloc] } +} + +static MENU_IDENTITY: &str = "MenuItemIdentity"; + +fn make_menu_item_class() -> *const Class { + static mut APP_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(NSMenuItem); + let mut decl = ClassDecl::new("MenuItem", superclass).unwrap(); + decl.add_ivar::(MENU_IDENTITY); + + decl.add_method( + sel!(dealloc), + dealloc_custom_menuitem as extern "C" fn(&Object, _), + ); + + decl.add_method( + sel!(fireMenubarAction:), + fire_menu_bar_click as extern "C" fn(&Object, _, id), + ); + + decl.add_method( + sel!(fireStatusbarAction:), + fire_status_bar_click as extern "C" fn(&Object, _, id), + ); + + APP_CLASS = decl.register(); + }); + + unsafe { APP_CLASS } +} + +fn make_menu_item_from_alloc( + alloc: *mut Object, + title: *mut Object, + selector: Sel, + //key_equivalent: Option, + //menu_type: MenuType, +) -> *mut Object { + unsafe { + // let (key, masks) = match key_equivalent { + // Some(ke) => ( + // NSString::alloc(nil).init_str(ke.key), + // ke.masks.unwrap_or_else(NSEventModifierFlags::empty), + // ), + // None => ( + // NSString::alloc(nil).init_str(""), + // NSEventModifierFlags::empty(), + // ), + // }; + let key = NSString::alloc(nil).init_str(""); + + // allocate our item to our class + let item: id = msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key]; + + // item.setKeyEquivalentModifierMask_(masks); + item + } +} + +extern "C" fn fire_menu_bar_click(this: &Object, _: Sel, _item: id) { + send_event(this); +} + +extern "C" fn fire_status_bar_click(this: &Object, _: Sel, _item: id) { + send_event(this); +} + +extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; + } +} + +fn send_event(this: &Object) { + let id: u64 = unsafe { *this.get_ivar(MENU_IDENTITY) }; + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id: id as _ }); +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs new file mode 100644 index 0000000..367ee15 --- /dev/null +++ b/src/platform_impl/macos/mod.rs @@ -0,0 +1,64 @@ +use cocoa::{ + appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, + base::{id, nil, NO}, + foundation::{NSAutoreleasePool, NSString}, +}; +use objc::{msg_send, sel, sel_impl}; + +mod menu_item; +pub use menu_item::TextMenuItem; +use menu_item::*; + +#[derive(Debug, Clone)] +pub struct Menu(id); + +impl Menu { + pub fn new() -> Self { + unsafe { + let ns_menu = NSMenu::alloc(nil).autorelease(); + let () = msg_send![ns_menu, setAutoenablesItems: NO]; + Self(ns_menu) + } + } + + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + let mut sub_menu = Submenu(Menu::new()); + sub_menu.set_label(label); + sub_menu.set_enabled(enabled); + sub_menu + } +} + +#[derive(Debug, Clone)] +pub struct Submenu(Menu); + +impl Submenu { + pub fn label(&self) -> String { + todo!() + } + + pub fn set_label(&mut self, label: impl AsRef) { + unsafe { + let menu_title = NSString::alloc(nil).init_str(label.as_ref()); + let () = msg_send![self.0 .0, setTitle: menu_title]; + } + } + + pub fn enabled(&self) -> bool { + true + } + + pub fn set_enabled(&mut self, _enabled: bool) {} + + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + self.0.add_submenu(label, enabled) + } + + pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + let item = TextMenuItem::new(label, enabled, sel!(fireMenubarAction:)); + unsafe { + self.0 .0.addItem_(item.ns_menu_item); + } + item + } +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 82eb2d1..2280d10 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -6,3 +6,6 @@ mod platform_impl; #[cfg(target_os = "linux")] #[path = "linux.rs"] mod platform_impl; +#[cfg(target_os = "macos")] +#[path = "macos/mod.rs"] +mod platform_impl;