Get app menus basically working

- Everything is still disabled when there is no active window.

Co-Authored-By: Marshall <marshall@zed.dev>
This commit is contained in:
Nathan Sobo
2023-12-05 16:37:01 -07:00
co-authored by Marshall
parent 79567d1c87
commit 82534b6612
13 changed files with 437 additions and 229 deletions
+18 -3
View File
@@ -15,10 +15,10 @@ use smol::future::FutureExt;
pub use test_context::*;
use crate::{
current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
WindowHandle, WindowId,
@@ -278,6 +278,8 @@ impl AppContext {
}),
});
init_app_menus(platform.as_ref(), &mut *app.borrow_mut());
platform.on_quit(Box::new({
let cx = app.clone();
move || {
@@ -1059,6 +1061,19 @@ impl AppContext {
.contains_key(&action.as_any().type_id())
}
pub fn set_menus(&mut self, menus: Vec<Menu>) {
if let Some(active_window) = self.active_window() {
active_window
.update(self, |_, cx| {
cx.platform
.set_menus(menus, Some(&cx.window.current_frame.dispatch_tree));
})
.ok();
} else {
self.platform.set_menus(menus, None);
}
}
pub fn dispatch_action(&mut self, action: &dyn Action) {
self.propagate_event = true;
+20 -4
View File
@@ -1,9 +1,10 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
BackgroundExecutor, Bounds, ClipboardItem, Context, Div, Entity, EventEmitter,
ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform,
PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@@ -16,6 +17,7 @@ pub struct TestAppContext {
pub foreground_executor: ForegroundExecutor,
pub dispatcher: TestDispatcher,
pub test_platform: Rc<TestPlatform>,
text_system: Arc<TextSystem>,
}
impl Context for TestAppContext {
@@ -82,6 +84,7 @@ impl TestAppContext {
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
let asset_source = Arc::new(());
let http_client = util::http::FakeHttpClient::with_404_response();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
Self {
app: AppContext::new(platform.clone(), asset_source, http_client),
@@ -89,6 +92,7 @@ impl TestAppContext {
foreground_executor,
dispatcher: dispatcher.clone(),
test_platform: platform,
text_system,
}
}
@@ -155,6 +159,18 @@ impl TestAppContext {
(view, Box::leak(cx))
}
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.test_platform.write_to_clipboard(item)
}
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.test_platform.read_from_clipboard()
}
pub fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+6 -5
View File
@@ -6,10 +6,10 @@ mod mac;
mod test;
use crate::{
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
SharedString, Size, TaskLabel,
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, DispatchTree,
Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent,
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
Scene, SharedString, Size, TaskLabel,
};
use anyhow::{anyhow, bail};
use async_task::Runnable;
@@ -46,7 +46,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new())
}
pub trait Platform: 'static {
pub(crate) trait Platform: 'static {
fn background_executor(&self) -> BackgroundExecutor;
fn foreground_executor(&self) -> ForegroundExecutor;
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
@@ -92,6 +92,7 @@ pub trait Platform: 'static {
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
fn set_menus(&self, menus: Vec<Menu>, dispatch_tree: Option<&DispatchTree>);
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
+1 -1
View File
@@ -52,7 +52,7 @@ pub enum OsAction {
Redo,
}
pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) {
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
platform.on_will_open_app_menu(Box::new({
let cx = cx.to_async();
move || {
+160 -161
View File
@@ -1,8 +1,8 @@
use super::BoolExt;
use super::{events::key_to_native, BoolExt};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker,
MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree,
DisplayId, ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker,
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
};
use anyhow::anyhow;
@@ -10,10 +10,10 @@ use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString,
NSSavePanel, NSWindow,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow,
},
base::{id, nil, BOOL, YES},
base::{id, nil, selector, BOOL, YES},
foundation::{
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
NSUInteger, NSURL,
@@ -201,151 +201,155 @@ impl MacPlatform {
}
}
// unsafe fn create_menu_bar(
// &self,
// menus: Vec<Menu>,
// delegate: id,
// actions: &mut Vec<Box<dyn Action>>,
// keystroke_matcher: &KeymapMatcher,
// ) -> id {
// let application_menu = NSMenu::new(nil).autorelease();
// application_menu.setDelegate_(delegate);
unsafe fn create_menu_bar(
&self,
menus: Vec<Menu>,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
dispatch_tree: Option<&DispatchTree>,
) -> id {
let application_menu = NSMenu::new(nil).autorelease();
application_menu.setDelegate_(delegate);
// for menu_config in menus {
// let menu = NSMenu::new(nil).autorelease();
// menu.setTitle_(ns_string(menu_config.name));
// menu.setDelegate_(delegate);
for menu_config in menus {
let menu = NSMenu::new(nil).autorelease();
menu.setTitle_(ns_string(menu_config.name));
menu.setDelegate_(delegate);
// for item_config in menu_config.items {
// menu.addItem_(self.create_menu_item(
// item_config,
// delegate,
// actions,
// keystroke_matcher,
// ));
// }
for item_config in menu_config.items {
menu.addItem_(self.create_menu_item(item_config, delegate, actions, dispatch_tree));
}
// let menu_item = NSMenuItem::new(nil).autorelease();
// menu_item.setSubmenu_(menu);
// application_menu.addItem_(menu_item);
let menu_item = NSMenuItem::new(nil).autorelease();
menu_item.setSubmenu_(menu);
application_menu.addItem_(menu_item);
// if menu_config.name == "Window" {
// let app: id = msg_send![APP_CLASS, sharedApplication];
// app.setWindowsMenu_(menu);
// }
// }
if menu_config.name == "Window" {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setWindowsMenu_(menu);
}
}
// application_menu
// }
application_menu
}
unsafe fn create_menu_item(
&self,
item: MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keystroke_matcher: &KeystrokeMatcher,
dispatch_tree: Option<&DispatchTree>,
) -> id {
todo!()
// match item {
// MenuItem::Separator => NSMenuItem::separatorItem(nil),
// MenuItem::Action {
// name,
// action,
// os_action,
// } => {
// // TODO
// let keystrokes = keystroke_matcher
// .bindings_for_action(action.id())
// .find(|binding| binding.action().eq(action.as_ref()))
// .map(|binding| binding.keystrokes());
// let selector = match os_action {
// Some(crate::OsAction::Cut) => selector("cut:"),
// Some(crate::OsAction::Copy) => selector("copy:"),
// Some(crate::OsAction::Paste) => selector("paste:"),
// Some(crate::OsAction::SelectAll) => selector("selectAll:"),
// Some(crate::OsAction::Undo) => selector("undo:"),
// Some(crate::OsAction::Redo) => selector("redo:"),
// None => selector("handleGPUIMenuItem:"),
// };
match item {
MenuItem::Separator => NSMenuItem::separatorItem(nil),
MenuItem::Action {
name,
action,
os_action,
} => {
let bindings = dispatch_tree
.map(|tree| tree.bindings_for_action(action.as_ref(), &tree.context_stack))
.unwrap_or_default();
let keystrokes = bindings
.iter()
.find(|binding| binding.action().partial_eq(action.as_ref()))
.map(|binding| binding.keystrokes());
// let item;
// if let Some(keystrokes) = keystrokes {
// if keystrokes.len() == 1 {
// let keystroke = &keystrokes[0];
// let mut mask = NSEventModifierFlags::empty();
// for (modifier, flag) in &[
// (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
// (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
// (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
// (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
// ] {
// if *modifier {
// mask |= *flag;
// }
// }
let selector = match os_action {
Some(crate::OsAction::Cut) => selector("cut:"),
Some(crate::OsAction::Copy) => selector("copy:"),
Some(crate::OsAction::Paste) => selector("paste:"),
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
Some(crate::OsAction::Undo) => selector("undo:"),
Some(crate::OsAction::Redo) => selector("redo:"),
None => selector("handleGPUIMenuItem:"),
};
// item = NSMenuItem::alloc(nil)
// .initWithTitle_action_keyEquivalent_(
// ns_string(name),
// selector,
// ns_string(key_to_native(&keystroke.key).as_ref()),
// )
// .autorelease();
// item.setKeyEquivalentModifierMask_(mask);
// }
// // For multi-keystroke bindings, render the keystroke as part of the title.
// else {
// use std::fmt::Write;
let item;
if let Some(keystrokes) = keystrokes {
if keystrokes.len() == 1 {
let keystroke = &keystrokes[0];
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(
keystroke.modifiers.command,
NSEventModifierFlags::NSCommandKeyMask,
),
(
keystroke.modifiers.control,
NSEventModifierFlags::NSControlKeyMask,
),
(
keystroke.modifiers.alt,
NSEventModifierFlags::NSAlternateKeyMask,
),
(
keystroke.modifiers.shift,
NSEventModifierFlags::NSShiftKeyMask,
),
] {
if *modifier {
mask |= *flag;
}
}
// let mut name = format!("{name} [");
// for (i, keystroke) in keystrokes.iter().enumerate() {
// if i > 0 {
// name.push(' ');
// }
// write!(&mut name, "{}", keystroke).unwrap();
// }
// name.push(']');
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector,
ns_string(key_to_native(&keystroke.key).as_ref()),
)
.autorelease();
item.setKeyEquivalentModifierMask_(mask);
}
// For multi-keystroke bindings, render the keystroke as part of the title.
else {
use std::fmt::Write;
// item = NSMenuItem::alloc(nil)
// .initWithTitle_action_keyEquivalent_(
// ns_string(&name),
// selector,
// ns_string(""),
// )
// .autorelease();
// }
// } else {
// item = NSMenuItem::alloc(nil)
// .initWithTitle_action_keyEquivalent_(
// ns_string(name),
// selector,
// ns_string(""),
// )
// .autorelease();
// }
let mut name = format!("{name} [");
for (i, keystroke) in keystrokes.iter().enumerate() {
if i > 0 {
name.push(' ');
}
write!(&mut name, "{}", keystroke).unwrap();
}
name.push(']');
// let tag = actions.len() as NSInteger;
// let _: () = msg_send![item, setTag: tag];
// actions.push(action);
// item
// }
// MenuItem::Submenu(Menu { name, items }) => {
// let item = NSMenuItem::new(nil).autorelease();
// let submenu = NSMenu::new(nil).autorelease();
// submenu.setDelegate_(delegate);
// for item in items {
// submenu.addItem_(self.create_menu_item(
// item,
// delegate,
// actions,
// keystroke_matcher,
// ));
// }
// item.setSubmenu_(submenu);
// item.setTitle_(ns_string(name));
// item
// }
// }
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(&name),
selector,
ns_string(""),
)
.autorelease();
}
} else {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector,
ns_string(""),
)
.autorelease();
}
let tag = actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
actions.push(action);
item
}
MenuItem::Submenu(Menu { name, items }) => {
let item = NSMenuItem::new(nil).autorelease();
let submenu = NSMenu::new(nil).autorelease();
submenu.setDelegate_(delegate);
for item in items {
submenu.addItem_(self.create_menu_item(item, delegate, actions, dispatch_tree));
}
item.setSubmenu_(submenu);
item.setTitle_(ns_string(name));
item
}
}
}
}
@@ -633,6 +637,18 @@ impl Platform for MacPlatform {
self.0.lock().event = Some(callback);
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.0.lock().menu_command = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.0.lock().will_open_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.0.lock().validate_menu_command = Some(callback);
}
fn os_name(&self) -> &'static str {
"macOS"
}
@@ -675,6 +691,15 @@ impl Platform for MacPlatform {
}
}
fn set_menus(&self, menus: Vec<Menu>, dispatch_tree: Option<&DispatchTree>) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.lock();
let actions = &mut state.menu_actions;
app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, dispatch_tree));
}
}
fn local_timezone(&self) -> UtcOffset {
unsafe {
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
@@ -683,32 +708,6 @@ impl Platform for MacPlatform {
}
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.0.lock().menu_command = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.0.lock().will_open_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.0.lock().validate_menu_command = Some(callback);
}
// fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
// unsafe {
// let app: id = msg_send![APP_CLASS, sharedApplication];
// let mut state = self.0.lock();
// let actions = &mut state.menu_actions;
// app.setMainMenu_(self.create_menu_bar(
// menus,
// app.delegate(),
// actions,
// keystroke_matcher,
// ));
// }
// }
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();
+7 -2
View File
@@ -1,6 +1,7 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, DisplayId,
ForegroundExecutor, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow,
WindowOptions,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -205,6 +206,10 @@ impl Platform for TestPlatform {
unimplemented!()
}
fn set_menus(&self, _menus: Vec<crate::Menu>, _dispatch_tree: Option<&DispatchTree>) {
unimplemented!()
}
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {
unimplemented!()
}