diff --git a/crates/gpui/examples/set_menus.rs b/crates/gpui/examples/set_menus.rs index 8a97a8d8a2ce8135b1af0e981a186586c2d5ebb8..f4e5e5e11234d3140d088195174f617637802811 100644 --- a/crates/gpui/examples/set_menus.rs +++ b/crates/gpui/examples/set_menus.rs @@ -1,6 +1,6 @@ use gpui::{ - App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div, - prelude::*, rgb, + App, Application, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, + WindowOptions, actions, div, prelude::*, rgb, }; struct SetMenus; @@ -21,29 +21,87 @@ impl Render for SetMenus { fn main() { Application::new().run(|cx: &mut App| { + cx.set_global(AppState::new()); + // Bring the menu bar to the foreground (so you can see the menu bar) cx.activate(true); // Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar cx.on_action(quit); + cx.on_action(toggle_check); // 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), - ], - }]); + set_app_menus(cx); cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) .unwrap(); }); } +#[derive(PartialEq)] +enum ViewMode { + List, + Grid, +} + +impl ViewMode { + fn toggle(&mut self) { + *self = match self { + ViewMode::List => ViewMode::Grid, + ViewMode::Grid => ViewMode::List, + } + } +} + +impl Into for ViewMode { + fn into(self) -> SharedString { + match self { + ViewMode::List => "List", + ViewMode::Grid => "Grid", + } + .into() + } +} + +struct AppState { + view_mode: ViewMode, +} + +impl AppState { + fn new() -> Self { + Self { + view_mode: ViewMode::List, + } + } +} + +impl Global for AppState {} + +fn set_app_menus(cx: &mut App) { + let app_state = cx.global::(); + cx.set_menus(vec![Menu { + name: "set_menus".into(), + items: vec![ + MenuItem::os_submenu("Services", SystemMenuType::Services), + MenuItem::separator(), + MenuItem::action(ViewMode::List, ToggleCheck) + .checked(app_state.view_mode == ViewMode::List), + MenuItem::action(ViewMode::Grid, ToggleCheck) + .checked(app_state.view_mode == ViewMode::Grid), + MenuItem::separator(), + MenuItem::action("Quit", Quit), + ], + }]); +} + // Associate actions using the `actions!` macro (or `Action` derive macro) -actions!(set_menus, [Quit]); +actions!(set_menus, [Quit, ToggleCheck]); // Define the quit function that is registered with the App fn quit(_: &Quit, cx: &mut App) { println!("Gracefully quitting the application . . ."); cx.quit(); } + +fn toggle_check(_: &ToggleCheck, cx: &mut App) { + let app_state = cx.global_mut::(); + app_state.view_mode.toggle(); + set_app_menus(cx); +} diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 4069fee7268c18bd5dbdfb3f3fb8a73d2f7f6e92..39e7556b2d210f85fb9fda573244c9f031a92e2c 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -64,12 +64,15 @@ pub enum MenuItem { /// The name of this menu item name: SharedString, - /// the action to perform when this menu item is selected + /// The action to perform when this menu item is selected action: Box, /// The OS Action that corresponds to this action, if any /// See [`OsAction`] for more information os_action: Option, + + /// Whether this action is checked + checked: bool, }, } @@ -98,6 +101,7 @@ impl MenuItem { name: name.into(), action: Box::new(action), os_action: None, + checked: false, } } @@ -111,6 +115,7 @@ impl MenuItem { name: name.into(), action: Box::new(action), os_action: Some(os_action), + checked: false, } } @@ -123,14 +128,36 @@ impl MenuItem { name, action, os_action, + checked, } => OwnedMenuItem::Action { name: name.into(), action, os_action, + checked, }, MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), } } + + /// Set whether this menu item is checked + /// + /// Only for [`MenuItem::Action`], otherwise, will be ignored + pub fn checked(mut self, checked: bool) -> Self { + match self { + MenuItem::Action { + action, + os_action, + name, + .. + } => MenuItem::Action { + name, + action, + os_action, + checked, + }, + _ => self, + } + } } /// OS menus are menus that are recognized by the operating system @@ -171,12 +198,15 @@ pub enum OwnedMenuItem { /// The name of this menu item name: String, - /// the action to perform when this menu item is selected + /// The action to perform when this menu item is selected action: Box, /// The OS Action that corresponds to this action, if any /// See [`OsAction`] for more information os_action: Option, + + /// Whether this action is checked + checked: bool, }, } @@ -189,10 +219,12 @@ impl Clone for OwnedMenuItem { name, action, os_action, + checked, } => OwnedMenuItem::Action { name: name.clone(), action: action.boxed_clone(), os_action: *os_action, + checked: *checked, }, OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 4e0ee9f2c5773c014aa84ff0e69efb83cf5c54a7..101520cdbbc220a49b1fa56584729ad93d507fe7 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -19,7 +19,7 @@ use cocoa::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString, - NSPasteboardTypeTIFF, NSSavePanel, NSWindow, + NSPasteboardTypeTIFF, NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow, }, base::{BOOL, NO, YES, id, nil, selector}, foundation::{ @@ -315,6 +315,7 @@ impl MacPlatform { name, action, os_action, + checked, } => { // Note that this is intentionally using earlier bindings, whereas typically // later ones take display precedence. See the discussion on @@ -409,6 +410,10 @@ impl MacPlatform { .autorelease(); } + if *checked { + item.setState_(NSVisualEffectState::Active); + } + let tag = actions.len() as NSInteger; let _: () = msg_send![item, setTag: tag]; actions.push(action.boxed_clone()); diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index c99abb292ef6d99e8adc3ab9007f4c49eeb05be2..7b9b0183a087d1618e1a177a5ed09fe455c4fcec 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -41,6 +41,7 @@ fn main() { name: "Quit".into(), action: Box::new(Quit), os_action: None, + checked: false, }], }]); diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 4a8cac2435317f02e4aed31105cf3126a9c89e70..01a12260ad03284d77dfda19fdf2286cf6196ca8 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -110,16 +110,24 @@ impl ApplicationMenu { .into_iter() .fold(menu, |menu, item| match item { OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { name, action, .. } => menu.action(name, action), + OwnedMenuItem::Action { + name, + action, + checked, + .. + } => menu.action_checked(name, action, checked), OwnedMenuItem::Submenu(submenu) => { submenu .items .into_iter() .fold(menu, |menu, item| match item { OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { name, action, .. } => { - menu.action(name, action) - } + OwnedMenuItem::Action { + name, + action, + checked, + .. + } => menu.action_checked(name, action, checked), OwnedMenuItem::Submenu(_) => menu, OwnedMenuItem::SystemMenu(_) => { // A system menu doesn't make sense in this context, so ignore it diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index e02578d186774310f403840d32f70f6c32399893..a4bae647408f860ec8425266a26efc173099f225 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -542,9 +542,22 @@ impl ContextMenu { self } - pub fn action(mut self, label: impl Into, action: Box) -> Self { + pub fn action(self, label: impl Into, action: Box) -> Self { + self.action_checked(label, action, false) + } + + pub fn action_checked( + mut self, + label: impl Into, + action: Box, + checked: bool, + ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, + toggle: if checked { + Some((IconPosition::Start, true)) + } else { + None + }, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |context, window, cx| {