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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}],
|
||||
}]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user