gpui: Add to support check state to MenuItem (#39876)

Release Notes:

- N/A

---


https://github.com/user-attachments/assets/d46b77ae-88ba-43da-93ad-3656a7fecaf9

The system menu is only support for macOS, so here just modify the macOS
platform special code.

The Windows, Linux used `ApplicationMenu`, I have already added
`checked` option to Zed's ContextMenu.

Then later when this PR merged, we can improve "View" menu to show check
state to panels (Project Panel, Outline Panel, ...).
This commit is contained in:
Jason Lee
2025-11-07 09:42:55 +00:00
committed by GitHub
parent 760747f127
commit 29cc3d0e18
6 changed files with 137 additions and 20 deletions
+69 -11
View File
@@ -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<SharedString> 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::<AppState>();
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::<AppState>();
app_state.view_mode.toggle();
set_app_menus(cx);
}
+34 -2
View File
@@ -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<dyn Action>,
/// The OS Action that corresponds to this action, if any
/// See [`OsAction`] for more information
os_action: Option<OsAction>,
/// 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<dyn Action>,
/// The OS Action that corresponds to this action, if any
/// See [`OsAction`] for more information
os_action: Option<OsAction>,
/// 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()),
}
+6 -1
View File
@@ -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());
@@ -41,6 +41,7 @@ fn main() {
name: "Quit".into(),
action: Box::new(Quit),
os_action: None,
checked: false,
}],
}]);
+12 -4
View File
@@ -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
+15 -2
View File
@@ -542,9 +542,22 @@ impl ContextMenu {
self
}
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.action_checked(label, action, false)
}
pub fn action_checked(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
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| {