gpui: Flash menu in menubar on macOS when action is triggered (#38588)
On macOS, traditionally when a keyboard shortcut is activated, the menu in the menu bar flashes to indicate that the action was recognised. <img width="289" height="172" alt="image" src="https://github.com/user-attachments/assets/a03ecd2f-f159-4f82-b4fd-227f34393703" /> This PR adds this functionality to GPUI, where when a keybind is pressed that triggers an action in the menu, the menu flashes. Release Notes: - N/A
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
use gpui::{
|
||||
App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div,
|
||||
prelude::*, rgb,
|
||||
App, Application, Context, FocusHandle, KeyBinding, Menu, MenuItem, PromptLevel,
|
||||
SystemMenuType, Window, WindowOptions, actions, div, prelude::*, rgb,
|
||||
};
|
||||
|
||||
struct SetMenus;
|
||||
struct SetMenus {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for SetMenus {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("root")
|
||||
.track_focus(&self.focus_handle)
|
||||
.flex()
|
||||
.bg(rgb(0x2e7d32))
|
||||
.size_full()
|
||||
@@ -15,6 +19,16 @@ impl Render for SetMenus {
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
// Actions can also be registered within elements so they are only active when relevant
|
||||
.on_action(|_: &Open, window, cx| {
|
||||
let _ = window.prompt(PromptLevel::Info, "Open action fired", None, &["OK"], cx);
|
||||
})
|
||||
.on_action(|_: &Copy, window, cx| {
|
||||
let _ = window.prompt(PromptLevel::Info, "Copy action fired", None, &["OK"], cx);
|
||||
})
|
||||
.on_action(|_: &Paste, window, cx| {
|
||||
let _ = window.prompt(PromptLevel::Info, "Paste action fired", None, &["OK"], cx);
|
||||
})
|
||||
.child("Set Menus Example")
|
||||
}
|
||||
}
|
||||
@@ -23,24 +37,47 @@ fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
// Bring the menu bar to the foreground (so you can see the menu bar)
|
||||
cx.activate(true);
|
||||
// Bind keys to some menu actions
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("secondary-o", Open, None),
|
||||
KeyBinding::new("secondary-c", Copy, None),
|
||||
KeyBinding::new("secondary-v", Paste, None),
|
||||
]);
|
||||
// Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar
|
||||
cx.on_action(quit);
|
||||
// Add menu items
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "set_menus".into(),
|
||||
items: vec![
|
||||
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Quit", Quit),
|
||||
],
|
||||
}]);
|
||||
cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
|
||||
.unwrap();
|
||||
cx.set_menus(vec![
|
||||
Menu {
|
||||
name: "set_menus".into(),
|
||||
items: vec![
|
||||
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Quit", Quit),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "File".into(),
|
||||
items: vec![MenuItem::action("Open", Open)],
|
||||
},
|
||||
Menu {
|
||||
name: "Edit".into(),
|
||||
items: vec![
|
||||
MenuItem::action("Copy", Copy),
|
||||
MenuItem::action("Paste", Paste),
|
||||
],
|
||||
},
|
||||
]);
|
||||
cx.open_window(WindowOptions::default(), |_, cx| {
|
||||
cx.new(|cx| SetMenus {
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// Associate actions using the `actions!` macro (or `Action` derive macro)
|
||||
actions!(set_menus, [Quit]);
|
||||
actions!(set_menus, [Quit, Open, Copy, Paste]);
|
||||
|
||||
// Define the quit function that is registered with the App
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
|
||||
@@ -250,6 +250,7 @@ pub(crate) trait Platform: 'static {
|
||||
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>);
|
||||
fn on_action_triggered(&self, action: &dyn Action);
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
|
||||
@@ -442,6 +442,8 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
});
|
||||
}
|
||||
|
||||
fn on_action_triggered(&self, _action: &dyn Action) {}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
// get the path of the executable of the current process
|
||||
let app_path = env::current_exe()?;
|
||||
|
||||
@@ -177,6 +177,8 @@ pub(crate) struct MacPlatformState {
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||
action_menus: Vec<(Box<dyn Action>, id)>,
|
||||
handling_menu_item: bool,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
@@ -219,6 +221,8 @@ impl MacPlatform {
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
keyboard_mapper,
|
||||
handling_menu_item: false,
|
||||
action_menus: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -241,6 +245,7 @@ impl MacPlatform {
|
||||
menus: &Vec<Menu>,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
action_menus: &mut Vec<(Box<dyn Action>, id)>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
unsafe {
|
||||
@@ -258,6 +263,7 @@ impl MacPlatform {
|
||||
item_config,
|
||||
delegate,
|
||||
actions,
|
||||
action_menus,
|
||||
keymap,
|
||||
));
|
||||
}
|
||||
@@ -292,6 +298,7 @@ impl MacPlatform {
|
||||
&item_config,
|
||||
delegate,
|
||||
actions,
|
||||
&mut Vec::new(),
|
||||
keymap,
|
||||
));
|
||||
}
|
||||
@@ -304,6 +311,7 @@ impl MacPlatform {
|
||||
item: &MenuItem,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
action_menus: &mut Vec<(Box<dyn Action>, id)>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
static DEFAULT_CONTEXT: OnceLock<Vec<KeyContext>> = OnceLock::new();
|
||||
@@ -412,6 +420,7 @@ impl MacPlatform {
|
||||
let tag = actions.len() as NSInteger;
|
||||
let _: () = msg_send![item, setTag: tag];
|
||||
actions.push(action.boxed_clone());
|
||||
action_menus.push((action.boxed_clone(), item));
|
||||
item
|
||||
}
|
||||
MenuItem::Submenu(Menu { name, items }) => {
|
||||
@@ -419,7 +428,13 @@ impl MacPlatform {
|
||||
let submenu = NSMenu::new(nil).autorelease();
|
||||
submenu.setDelegate_(delegate);
|
||||
for item in items {
|
||||
submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap));
|
||||
submenu.addItem_(Self::create_menu_item(
|
||||
item,
|
||||
delegate,
|
||||
actions,
|
||||
action_menus,
|
||||
keymap,
|
||||
));
|
||||
}
|
||||
item.setSubmenu_(submenu);
|
||||
item.setTitle_(ns_string(name));
|
||||
@@ -888,6 +903,30 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().validate_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn on_action_triggered(&self, action: &dyn Action) {
|
||||
let menu_item = {
|
||||
let mut state = self.0.lock();
|
||||
let Some(menu_item) = state
|
||||
.action_menus
|
||||
.iter()
|
||||
.find(|(menu_action, _)| menu_action.partial_eq(action))
|
||||
.map(|(_, menu)| *menu)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.handling_menu_item = true;
|
||||
menu_item
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let parent: id = msg_send![menu_item, menu];
|
||||
let index: NSInteger = msg_send![parent, indexOfItem: menu_item];
|
||||
let _: () = msg_send![parent, performActionForItemAtIndex: index];
|
||||
}
|
||||
self.0.lock().handling_menu_item = false;
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
@@ -905,15 +944,24 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
|
||||
let mut action_menus = Vec::new();
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
|
||||
let menu = self.create_menu_bar(
|
||||
&menus,
|
||||
NSWindow::delegate(app),
|
||||
actions,
|
||||
&mut action_menus,
|
||||
keymap,
|
||||
);
|
||||
drop(state);
|
||||
app.setMainMenu_(menu);
|
||||
}
|
||||
self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
|
||||
let mut state = self.0.lock();
|
||||
state.menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
|
||||
state.action_menus = action_menus;
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -1465,6 +1513,12 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_mac_platform(this);
|
||||
let mut lock = platform.0.lock();
|
||||
|
||||
// If the menu item is currently being handled, i.e., this action is being triggered as a
|
||||
// result of a keyboard shortcut causing the menu to flash, don't do anything.
|
||||
if lock.handling_menu_item {
|
||||
return;
|
||||
}
|
||||
if let Some(mut callback) = lock.menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
@@ -378,6 +378,8 @@ impl Platform for TestPlatform {
|
||||
|
||||
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
||||
|
||||
fn on_action_triggered(&self, _action: &dyn Action) {}
|
||||
|
||||
fn app_path(&self) -> Result<std::path::PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -536,6 +536,8 @@ impl Platform for WindowsPlatform {
|
||||
.validate_app_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn on_action_triggered(&self, _action: &dyn Action) {}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
Ok(std::env::current_exe()?)
|
||||
}
|
||||
|
||||
@@ -4023,6 +4023,8 @@ impl Window {
|
||||
let any_action = action.as_any();
|
||||
if action_type == any_action.type_id() {
|
||||
cx.propagate_event = false; // Actions stop propagation by default during the bubble phase
|
||||
cx.platform.on_action_triggered(action);
|
||||
|
||||
listener(any_action, DispatchPhase::Bubble, self, cx);
|
||||
|
||||
if !cx.propagate_event {
|
||||
@@ -4040,6 +4042,7 @@ impl Window {
|
||||
for listener in global_listeners.iter().rev() {
|
||||
cx.propagate_event = false; // Actions stop propagation by default during the bubble phase
|
||||
|
||||
cx.platform.on_action_triggered(action);
|
||||
listener(action.as_any(), DispatchPhase::Bubble, cx);
|
||||
if !cx.propagate_event {
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user