use std::{collections::HashSet, rc::Rc};
use gpui_kit::{
Action, AnyElement, App, FocusHandle, IntoElement, OwnedMenuItem, SharedString, Window,
base::Disableable,
};
use crate::Icon;
type SelectHandler = Rc<dyn Fn(&mut Window, &mut App)>;
pub(crate) type RowRenderer = Rc<dyn Fn(MenuRowState, &mut Window, &mut App) -> AnyElement>;
enum Activation {
Event,
Action(Box<dyn Action>),
Handler(SelectHandler),
Link(SharedString),
}
impl Clone for Activation {
fn clone(&self) -> Self {
match self {
Self::Event => Self::Event,
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Handler(handler) => Self::Handler(handler.clone()),
Self::Link(url) => Self::Link(url.clone()),
}
}
}
pub(crate) struct PendingActivation {
activation: Activation,
context: FocusHandle,
}
impl PendingActivation {
pub(crate) fn run(self, window: &mut Window, cx: &mut App) {
match self.activation {
Activation::Event => {}
Activation::Action(action) => self.context.dispatch_action(action.as_ref(), window, cx),
Activation::Handler(handler) => handler(window, cx),
Activation::Link(url) => cx.open_url(&url),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum Indicator {
#[default]
None,
Check(bool),
Radio(bool),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MenuRowState {
pub(crate) highlighted: bool,
pub(crate) disabled: bool,
pub(crate) checked: bool,
}
impl MenuRowState {
pub fn is_highlighted(&self) -> bool {
self.highlighted
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn is_checked(&self) -> bool {
self.checked
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct MenuItem {
key: SharedString,
label: SharedString,
description: Option<SharedString>,
icon: Option<Icon>,
disabled: bool,
indicator: Indicator,
destructive: bool,
external_link_icon: bool,
action_context: Option<FocusHandle>,
render: Option<RowRenderer>,
activation: Activation,
}
impl MenuItem {
pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
key: key.into(),
label: label.into(),
description: None,
icon: None,
disabled: false,
indicator: Indicator::None,
destructive: false,
external_link_icon: true,
action_context: None,
render: None,
activation: Activation::Event,
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.indicator = Indicator::Check(checked);
self
}
pub fn radio(mut self, selected: bool) -> Self {
self.indicator = Indicator::Radio(selected);
self
}
pub fn destructive(mut self) -> Self {
self.destructive = true;
self
}
pub fn action(mut self, action: impl Action) -> Self {
self.activation = Activation::Action(Box::new(action));
self
}
pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
self.action_context = Some(focus_handle.clone());
self
}
pub fn on_select(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.activation = Activation::Handler(Rc::new(handler));
self
}
pub fn link(mut self, url: impl Into<SharedString>) -> Self {
self.activation = Activation::Link(url.into());
self
}
pub fn external_link_icon(mut self, visible: bool) -> Self {
self.external_link_icon = visible;
self
}
pub fn render<E: IntoElement>(
mut self,
render: impl Fn(MenuRowState, &mut Window, &mut App) -> E + 'static,
) -> Self {
self.render = Some(Rc::new(move |row, window, cx| {
render(row, window, cx).into_any_element()
}));
self
}
pub fn key(&self) -> &SharedString {
&self.key
}
pub fn label(&self) -> &SharedString {
&self.label
}
pub fn description_text(&self) -> Option<&SharedString> {
self.description.as_ref()
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn is_checked(&self) -> bool {
matches!(
self.indicator,
Indicator::Check(true) | Indicator::Radio(true)
)
}
pub fn is_destructive(&self) -> bool {
self.destructive
}
pub(crate) fn indicator(&self) -> Indicator {
self.indicator
}
pub(crate) fn leading_icon(&self) -> Option<&Icon> {
self.icon.as_ref()
}
pub(crate) fn renderer(&self) -> Option<&RowRenderer> {
self.render.as_ref()
}
pub(crate) fn shows_link_icon(&self) -> bool {
self.external_link_icon && matches!(self.activation, Activation::Link(_))
}
pub(crate) fn dispatched_action(&self) -> Option<&dyn Action> {
match &self.activation {
Activation::Action(action) => Some(action.as_ref()),
_ => None,
}
}
pub(crate) fn own_action_context(&self) -> Option<&FocusHandle> {
self.action_context.as_ref()
}
pub(crate) fn pending(&self, context: FocusHandle) -> PendingActivation {
PendingActivation {
activation: self.activation.clone(),
context: self.action_context.clone().unwrap_or(context),
}
}
}
impl Disableable for MenuItem {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct MenuSubmenu {
key: SharedString,
label: SharedString,
icon: Option<Icon>,
disabled: bool,
entries: Vec<MenuEntry>,
}
impl MenuSubmenu {
pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
key: key.into(),
label: label.into(),
icon: None,
disabled: false,
entries: Vec::new(),
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn entries(mut self, entries: impl IntoIterator<Item = impl Into<MenuEntry>>) -> Self {
self.entries.extend(entries.into_iter().map(Into::into));
self
}
pub fn key(&self) -> &SharedString {
&self.key
}
pub fn label(&self) -> &SharedString {
&self.label
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub(crate) fn leading_icon(&self) -> Option<&Icon> {
self.icon.as_ref()
}
pub(crate) fn children(&self) -> &[MenuEntry] {
&self.entries
}
pub(crate) fn normalized(mut self) -> Self {
self.entries = super::state::normalize(std::mem::take(&mut self.entries));
self
}
}
impl Disableable for MenuSubmenu {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
#[derive(Clone)]
pub enum MenuEntry {
Item(MenuItem),
Separator,
Label(SharedString),
Submenu(MenuSubmenu),
}
impl MenuEntry {
pub fn label(text: impl Into<SharedString>) -> Self {
Self::Label(text.into())
}
pub fn key(&self) -> Option<&SharedString> {
match self {
Self::Item(item) => Some(item.key()),
Self::Submenu(submenu) => Some(submenu.key()),
Self::Separator | Self::Label(_) => None,
}
}
pub(crate) fn is_selectable(&self) -> bool {
match self {
Self::Item(item) => !item.is_disabled(),
Self::Submenu(submenu) => !submenu.is_disabled(),
Self::Separator | Self::Label(_) => false,
}
}
pub(crate) fn submenu(&self) -> Option<&MenuSubmenu> {
match self {
Self::Submenu(submenu) => Some(submenu),
_ => None,
}
}
}
impl MenuEntry {
pub fn from_app_menu_item(item: OwnedMenuItem) -> Option<Self> {
Self::from_app_item_keyed(item, &mut HashSet::new())
}
pub(crate) fn from_app_item_keyed(
item: OwnedMenuItem,
used: &mut HashSet<SharedString>,
) -> Option<Self> {
let mut unique = |key: SharedString, label: &str| {
let key = if used.contains(&key) {
SharedString::from(format!("{key}/{label}"))
} else {
key
};
used.insert(key.clone());
key
};
match item {
OwnedMenuItem::Separator => Some(Self::Separator),
OwnedMenuItem::Submenu(menu) => {
let key = unique(menu.name.clone(), &menu.name);
let entries: Vec<MenuEntry> = menu
.items
.into_iter()
.filter_map(|item| Self::from_app_item_keyed(item, used))
.collect();
Some(
MenuSubmenu::new(key, menu.name)
.disabled(menu.disabled)
.entries(entries)
.into(),
)
}
OwnedMenuItem::SystemMenu(_) => None,
OwnedMenuItem::Action {
name,
action,
checked,
disabled,
..
} => {
let key = unique(action.name().into(), &name);
let mut item = MenuItem::new(key, name).disabled(disabled);
if checked {
item = item.checked(true);
}
item.activation = Activation::Action(action);
Some(item.into())
}
}
}
}
impl From<MenuItem> for MenuEntry {
fn from(item: MenuItem) -> Self {
Self::Item(item)
}
}
impl From<MenuSubmenu> for MenuEntry {
fn from(submenu: MenuSubmenu) -> Self {
Self::Submenu(submenu)
}
}