1use std::{collections::HashSet, rc::Rc};
4
5use gpui_kit::{
6 Action, AnyElement, App, FocusHandle, IntoElement, OwnedMenuItem, SharedString, Window,
7 base::Disableable,
8};
9
10use crate::Icon;
11
12type SelectHandler = Rc<dyn Fn(&mut Window, &mut App)>;
13pub(crate) type RowRenderer = Rc<dyn Fn(MenuRowState, &mut Window, &mut App) -> AnyElement>;
14
15enum Activation {
17 Event,
19 Action(Box<dyn Action>),
22 Handler(SelectHandler),
24 Link(SharedString),
26}
27
28impl Clone for Activation {
29 fn clone(&self) -> Self {
30 match self {
31 Self::Event => Self::Event,
32 Self::Action(action) => Self::Action(action.boxed_clone()),
33 Self::Handler(handler) => Self::Handler(handler.clone()),
34 Self::Link(url) => Self::Link(url.clone()),
35 }
36 }
37}
38
39pub(crate) struct PendingActivation {
42 activation: Activation,
43 context: FocusHandle,
44}
45
46impl PendingActivation {
47 pub(crate) fn run(self, window: &mut Window, cx: &mut App) {
49 match self.activation {
50 Activation::Event => {}
51 Activation::Action(action) => self.context.dispatch_action(action.as_ref(), window, cx),
52 Activation::Handler(handler) => handler(window, cx),
53 Activation::Link(url) => cx.open_url(&url),
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub(crate) enum Indicator {
61 #[default]
63 None,
64 Check(bool),
66 Radio(bool),
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72#[non_exhaustive]
73pub struct MenuRowState {
74 pub(crate) highlighted: bool,
75 pub(crate) disabled: bool,
76 pub(crate) checked: bool,
77}
78
79impl MenuRowState {
80 pub fn is_highlighted(&self) -> bool {
82 self.highlighted
83 }
84
85 pub fn is_disabled(&self) -> bool {
87 self.disabled
88 }
89
90 pub fn is_checked(&self) -> bool {
92 self.checked
93 }
94}
95
96#[derive(Clone)]
106#[non_exhaustive]
107pub struct MenuItem {
108 key: SharedString,
109 label: SharedString,
110 description: Option<SharedString>,
111 icon: Option<Icon>,
112 disabled: bool,
113 indicator: Indicator,
114 destructive: bool,
115 external_link_icon: bool,
116 action_context: Option<FocusHandle>,
117 render: Option<RowRenderer>,
118 activation: Activation,
119}
120
121impl MenuItem {
122 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
124 Self {
125 key: key.into(),
126 label: label.into(),
127 description: None,
128 icon: None,
129 disabled: false,
130 indicator: Indicator::None,
131 destructive: false,
132 external_link_icon: true,
133 action_context: None,
134 render: None,
135 activation: Activation::Event,
136 }
137 }
138
139 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
141 self.icon = Some(icon.into());
142 self
143 }
144
145 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
147 self.description = Some(description.into());
148 self
149 }
150
151 pub fn checked(mut self, checked: bool) -> Self {
154 self.indicator = Indicator::Check(checked);
155 self
156 }
157
158 pub fn radio(mut self, selected: bool) -> Self {
162 self.indicator = Indicator::Radio(selected);
163 self
164 }
165
166 pub fn destructive(mut self) -> Self {
169 self.destructive = true;
170 self
171 }
172
173 pub fn action(mut self, action: impl Action) -> Self {
176 self.activation = Activation::Action(Box::new(action));
177 self
178 }
179
180 pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
183 self.action_context = Some(focus_handle.clone());
184 self
185 }
186
187 pub fn on_select(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
189 self.activation = Activation::Handler(Rc::new(handler));
190 self
191 }
192
193 pub fn link(mut self, url: impl Into<SharedString>) -> Self {
196 self.activation = Activation::Link(url.into());
197 self
198 }
199
200 pub fn external_link_icon(mut self, visible: bool) -> Self {
202 self.external_link_icon = visible;
203 self
204 }
205
206 pub fn render<E: IntoElement>(
211 mut self,
212 render: impl Fn(MenuRowState, &mut Window, &mut App) -> E + 'static,
213 ) -> Self {
214 self.render = Some(Rc::new(move |row, window, cx| {
215 render(row, window, cx).into_any_element()
216 }));
217 self
218 }
219
220 pub fn key(&self) -> &SharedString {
222 &self.key
223 }
224
225 pub fn label(&self) -> &SharedString {
227 &self.label
228 }
229
230 pub fn description_text(&self) -> Option<&SharedString> {
232 self.description.as_ref()
233 }
234
235 pub fn is_disabled(&self) -> bool {
237 self.disabled
238 }
239
240 pub fn is_checked(&self) -> bool {
242 matches!(
243 self.indicator,
244 Indicator::Check(true) | Indicator::Radio(true)
245 )
246 }
247
248 pub fn is_destructive(&self) -> bool {
250 self.destructive
251 }
252
253 pub(crate) fn indicator(&self) -> Indicator {
254 self.indicator
255 }
256
257 pub(crate) fn leading_icon(&self) -> Option<&Icon> {
258 self.icon.as_ref()
259 }
260
261 pub(crate) fn renderer(&self) -> Option<&RowRenderer> {
262 self.render.as_ref()
263 }
264
265 pub(crate) fn shows_link_icon(&self) -> bool {
267 self.external_link_icon && matches!(self.activation, Activation::Link(_))
268 }
269
270 pub(crate) fn dispatched_action(&self) -> Option<&dyn Action> {
272 match &self.activation {
273 Activation::Action(action) => Some(action.as_ref()),
274 _ => None,
275 }
276 }
277
278 pub(crate) fn own_action_context(&self) -> Option<&FocusHandle> {
280 self.action_context.as_ref()
281 }
282
283 pub(crate) fn pending(&self, context: FocusHandle) -> PendingActivation {
286 PendingActivation {
287 activation: self.activation.clone(),
288 context: self.action_context.clone().unwrap_or(context),
289 }
290 }
291}
292
293impl Disableable for MenuItem {
294 fn disabled(mut self, disabled: bool) -> Self {
295 self.disabled = disabled;
296 self
297 }
298}
299
300#[derive(Clone)]
302#[non_exhaustive]
303pub struct MenuSubmenu {
304 key: SharedString,
305 label: SharedString,
306 icon: Option<Icon>,
307 disabled: bool,
308 entries: Vec<MenuEntry>,
309}
310
311impl MenuSubmenu {
312 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
314 Self {
315 key: key.into(),
316 label: label.into(),
317 icon: None,
318 disabled: false,
319 entries: Vec::new(),
320 }
321 }
322
323 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
325 self.icon = Some(icon.into());
326 self
327 }
328
329 pub fn entries(mut self, entries: impl IntoIterator<Item = impl Into<MenuEntry>>) -> Self {
331 self.entries.extend(entries.into_iter().map(Into::into));
332 self
333 }
334
335 pub fn key(&self) -> &SharedString {
337 &self.key
338 }
339
340 pub fn label(&self) -> &SharedString {
342 &self.label
343 }
344
345 pub fn is_disabled(&self) -> bool {
347 self.disabled
348 }
349
350 pub(crate) fn leading_icon(&self) -> Option<&Icon> {
351 self.icon.as_ref()
352 }
353
354 pub(crate) fn children(&self) -> &[MenuEntry] {
355 &self.entries
356 }
357
358 pub(crate) fn normalized(mut self) -> Self {
360 self.entries = super::state::normalize(std::mem::take(&mut self.entries));
361 self
362 }
363}
364
365impl Disableable for MenuSubmenu {
366 fn disabled(mut self, disabled: bool) -> Self {
367 self.disabled = disabled;
368 self
369 }
370}
371
372#[derive(Clone)]
374pub enum MenuEntry {
375 Item(MenuItem),
377 Separator,
379 Label(SharedString),
381 Submenu(MenuSubmenu),
383}
384
385impl MenuEntry {
386 pub fn label(text: impl Into<SharedString>) -> Self {
388 Self::Label(text.into())
389 }
390
391 pub fn key(&self) -> Option<&SharedString> {
393 match self {
394 Self::Item(item) => Some(item.key()),
395 Self::Submenu(submenu) => Some(submenu.key()),
396 Self::Separator | Self::Label(_) => None,
397 }
398 }
399
400 pub(crate) fn is_selectable(&self) -> bool {
402 match self {
403 Self::Item(item) => !item.is_disabled(),
404 Self::Submenu(submenu) => !submenu.is_disabled(),
405 Self::Separator | Self::Label(_) => false,
406 }
407 }
408
409 pub(crate) fn submenu(&self) -> Option<&MenuSubmenu> {
410 match self {
411 Self::Submenu(submenu) => Some(submenu),
412 _ => None,
413 }
414 }
415}
416
417impl MenuEntry {
418 pub fn from_app_menu_item(item: OwnedMenuItem) -> Option<Self> {
426 Self::from_app_item_keyed(item, &mut HashSet::new())
427 }
428
429 pub(crate) fn from_app_item_keyed(
432 item: OwnedMenuItem,
433 used: &mut HashSet<SharedString>,
434 ) -> Option<Self> {
435 let mut unique = |key: SharedString, label: &str| {
436 let key = if used.contains(&key) {
437 SharedString::from(format!("{key}/{label}"))
438 } else {
439 key
440 };
441 used.insert(key.clone());
442 key
443 };
444 match item {
445 OwnedMenuItem::Separator => Some(Self::Separator),
446 OwnedMenuItem::Submenu(menu) => {
447 let key = unique(menu.name.clone(), &menu.name);
448 let entries: Vec<MenuEntry> = menu
449 .items
450 .into_iter()
451 .filter_map(|item| Self::from_app_item_keyed(item, used))
452 .collect();
453 Some(
454 MenuSubmenu::new(key, menu.name)
455 .disabled(menu.disabled)
456 .entries(entries)
457 .into(),
458 )
459 }
460 OwnedMenuItem::SystemMenu(_) => None,
461 OwnedMenuItem::Action {
462 name,
463 action,
464 checked,
465 disabled,
466 ..
467 } => {
468 let key = unique(action.name().into(), &name);
469 let mut item = MenuItem::new(key, name).disabled(disabled);
470 if checked {
471 item = item.checked(true);
472 }
473 item.activation = Activation::Action(action);
474 Some(item.into())
475 }
476 }
477 }
478}
479
480impl From<MenuItem> for MenuEntry {
481 fn from(item: MenuItem) -> Self {
482 Self::Item(item)
483 }
484}
485
486impl From<MenuSubmenu> for MenuEntry {
487 fn from(submenu: MenuSubmenu) -> Self {
488 Self::Submenu(submenu)
489 }
490}