Start on app menus

This commit is contained in:
Nathan Sobo 2023-12-05 13:17:59 -07:00
parent dffe0ea058
commit 631e264e3c
8 changed files with 272 additions and 116 deletions

View file

@ -425,6 +425,10 @@ impl AppContext {
.collect()
}
pub fn active_window(&self) -> Option<AnyWindowHandle> {
self.platform.active_window()
}
/// Opens a new window with the given option and the root view returned by the given function.
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
/// functionality.
@ -1015,6 +1019,42 @@ impl AppContext {
activate();
subscription
}
pub(crate) fn clear_pending_keystrokes(&mut self) {
for window in self.windows() {
window
.update(self, |_, cx| {
cx.window
.current_frame
.dispatch_tree
.clear_pending_keystrokes()
})
.ok();
}
}
pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
if let Some(window) = self.active_window() {
let window_action_available = window
.update(self, |_, cx| {
if let Some(focus_id) = cx.window.focus {
cx.window
.current_frame
.dispatch_tree
.is_action_available(action, focus_id)
} else {
false
}
})
.unwrap_or(false);
if window_action_available {
return true;
}
}
self.global_action_listeners
.contains_key(&action.as_any().type_id())
}
}
impl Context for AppContext {

View file

@ -82,13 +82,13 @@ impl DispatchTree {
}
}
pub fn clear_keystroke_matchers(&mut self) {
pub fn clear_pending_keystrokes(&mut self) {
self.keystroke_matchers.clear();
}
/// Preserve keystroke matchers from previous frames to support multi-stroke
/// bindings across multiple frames.
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
let dispatch_path = self.dispatch_path(node_id);
@ -163,6 +163,22 @@ impl DispatchTree {
actions
}
pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool {
if let Some(node) = self.focusable_node_ids.get(&target) {
for node_id in self.dispatch_path(*node) {
let node = &self.nodes[node_id.0];
if node
.action_listeners
.iter()
.any(|listener| listener.action_type == action.as_any().type_id())
{
return true;
}
}
}
false
}
pub fn bindings_for_action(
&self,
action: &dyn Action,

View file

@ -1,3 +1,4 @@
mod app_menu;
mod keystroke;
#[cfg(target_os = "macos")]
mod mac;
@ -32,6 +33,7 @@ use std::{
};
use uuid::Uuid;
pub use app_menu::*;
pub use keystroke::*;
#[cfg(target_os = "macos")]
pub use mac::*;
@ -59,7 +61,7 @@ pub trait Platform: 'static {
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
fn main_window(&self) -> Option<AnyWindowHandle>;
fn active_window(&self) -> Option<AnyWindowHandle>;
fn open_window(
&self,
handle: AnyWindowHandle,

View file

@ -0,0 +1,96 @@
use crate::{Action, AppContext, Platform};
use util::ResultExt;
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
os_action: Option<OsAction>,
},
}
impl<'a> MenuItem<'a> {
pub fn separator() -> Self {
Self::Separator
}
pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu)
}
pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: None,
}
}
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: Some(os_action),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction {
Cut,
Copy,
Paste,
SelectAll,
Undo,
Redo,
}
pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) {
platform.on_will_open_menu(Box::new({
let cx = cx.to_async();
move || {
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
}
}));
platform.on_validate_menu_command(Box::new({
let cx = cx.to_async();
move |action| {
cx.update(|cx| cx.is_action_available(action))
.unwrap_or(false)
}
}));
platform.on_menu_command(Box::new({
let cx = cx.to_async();
move |action| {
cx.update(|cx| {
// if let Some(main_window) = cx.active_window() {
// let dispatched = main_window
// .update(&mut *cx, |cx| {
// if let Some(view_id) = cx.focused_view_id() {
// cx.dispatch_action(Some(view_id), action);
// true
// } else {
// false
// }
// })
// .unwrap_or(false);
// if dispatched {
// return;
// }
// }
// cx.dispatch_global_action_any(action);
})
.log_err();
}
}));
}

View file

@ -1,16 +1,17 @@
use super::BoolExt;
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow,
Result, SemanticVersion, VideoTimestamp, WindowOptions,
ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker,
MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
};
use anyhow::anyhow;
use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString,
NSSavePanel, NSWindow,
},
base::{id, nil, BOOL, YES},
foundation::{
@ -237,114 +238,115 @@ impl MacPlatform {
// application_menu
// }
// unsafe fn create_menu_item(
// &self,
// item: MenuItem,
// delegate: id,
// actions: &mut Vec<Box<dyn Action>>,
// keystroke_matcher: &KeymapMatcher,
// ) -> id {
// 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:"),
// };
unsafe fn create_menu_item(
&self,
item: MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keystroke_matcher: &KeystrokeMatcher,
) -> 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:"),
// };
// 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 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;
// }
// }
// 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(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 mut name = format!("{name} [");
// for (i, keystroke) in keystrokes.iter().enumerate() {
// if i > 0 {
// name.push(' ');
// }
// write!(&mut name, "{}", keystroke).unwrap();
// }
// name.push(']');
// 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(""),
// )
// .autorelease();
// }
// } else {
// item = NSMenuItem::alloc(nil)
// .initWithTitle_action_keyEquivalent_(
// ns_string(name),
// selector,
// ns_string(""),
// )
// .autorelease();
// }
// 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,
// keystroke_matcher,
// ));
// }
// item.setSubmenu_(submenu);
// item.setTitle_(ns_string(name));
// item
// }
// }
// }
// 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
// }
// }
}
}
impl Platform for MacPlatform {
@ -479,8 +481,8 @@ impl Platform for MacPlatform {
MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
}
fn main_window(&self) -> Option<AnyWindowHandle> {
MacWindow::main_window()
fn active_window(&self) -> Option<AnyWindowHandle> {
MacWindow::active_window()
}
fn open_window(

View file

@ -662,7 +662,7 @@ impl MacWindow {
}
}
pub fn main_window() -> Option<AnyWindowHandle> {
pub fn active_window() -> Option<AnyWindowHandle> {
unsafe {
let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];

View file

@ -127,7 +127,7 @@ impl Platform for TestPlatform {
self.displays().iter().find(|d| d.id() == id).cloned()
}
fn main_window(&self) -> Option<crate::AnyWindowHandle> {
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
unimplemented!()
}

View file

@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> {
self.window
.current_frame
.dispatch_tree
.clear_keystroke_matchers();
.clear_pending_keystrokes();
self.app.push_effect(Effect::FocusChanged {
window_handle: self.window.handle,
focused: Some(focus_id),
@ -1177,7 +1177,7 @@ impl<'a> WindowContext<'a> {
self.window
.current_frame
.dispatch_tree
.preserve_keystroke_matchers(
.preserve_pending_keystrokes(
&mut self.window.previous_frame.dispatch_tree,
self.window.focus,
);