Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:35:30.318Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

context_menu.rs

2524 lines · 94.4 KB · rust
1use crate::{
2    ButtonCommon, ButtonStyle, IconButtonShape, KeyBinding, List, ListItem, ListSeparator,
3    ListSubHeader, Tooltip, prelude::*, utils::WithRemSize,
4};
5use gpui::{
6    Action, Anchor, AnyElement, App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle,
7    Focusable, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Role,
8    Size, Subscription, TaskExt, anchored, canvas, prelude::*, px,
9};
10use menu::{SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious};
11use std::{
12    cell::{Cell, RefCell},
13    collections::HashMap,
14    rc::Rc,
15    time::{Duration, Instant},
16};
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19enum SubmenuOpenTrigger {
20    Pointer,
21    Keyboard,
22}
23
24struct OpenSubmenu {
25    item_index: usize,
26    entity: Entity<ContextMenu>,
27    trigger_bounds: Option<Bounds<Pixels>>,
28    offset: Option<Pixels>,
29    flip_left: bool,
30    _dismiss_subscription: Subscription,
31}
32
33enum SubmenuState {
34    Closed,
35    Open(OpenSubmenu),
36}
37
38#[derive(Clone, Copy, PartialEq, Eq, Default)]
39enum HoverTarget {
40    #[default]
41    None,
42    MainMenu,
43    Submenu,
44}
45
46pub enum ContextMenuItem {
47    Separator,
48    Header(SharedString),
49    /// title, link_label, link_url
50    HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header
51    Label(SharedString),
52    Entry(ContextMenuEntry),
53    CustomEntry {
54        entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
55        handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
56        selectable: bool,
57        documentation_aside: Option<DocumentationAside>,
58    },
59    Submenu {
60        label: SharedString,
61        icon: Option<IconName>,
62        icon_color: Option<Color>,
63        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
64    },
65}
66
67impl ContextMenuItem {
68    pub fn custom_entry(
69        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
70        handler: impl Fn(&mut Window, &mut App) + 'static,
71        documentation_aside: Option<DocumentationAside>,
72    ) -> Self {
73        Self::CustomEntry {
74            entry_render: Box::new(entry_render),
75            handler: Rc::new(move |_, window, cx| handler(window, cx)),
76            selectable: true,
77            documentation_aside,
78        }
79    }
80}
81
82pub struct ContextMenuEntry {
83    toggle: Option<(IconPosition, bool)>,
84    label: SharedString,
85    icon: Option<IconName>,
86    custom_icon_path: Option<SharedString>,
87    custom_icon_svg: Option<SharedString>,
88    icon_position: IconPosition,
89    icon_size: IconSize,
90    icon_color: Option<Color>,
91    handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
92    secondary_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,
93    action: Option<Box<dyn Action>>,
94    disabled: bool,
95    documentation_aside: Option<DocumentationAside>,
96    end_slot_icon: Option<IconName>,
97    end_slot_title: Option<SharedString>,
98    end_slot_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,
99    show_end_slot_on_hover: bool,
100}
101
102impl ContextMenuEntry {
103    pub fn new(label: impl Into<SharedString>) -> Self {
104        ContextMenuEntry {
105            toggle: None,
106            label: label.into(),
107            icon: None,
108            custom_icon_path: None,
109            custom_icon_svg: None,
110            icon_position: IconPosition::Start,
111            icon_size: IconSize::Small,
112            icon_color: None,
113            handler: Rc::new(|_, _, _| {}),
114            secondary_handler: None,
115            action: None,
116            disabled: false,
117            documentation_aside: None,
118            end_slot_icon: None,
119            end_slot_title: None,
120            end_slot_handler: None,
121            show_end_slot_on_hover: false,
122        }
123    }
124
125    pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
126        self.toggle = Some((toggle_position, toggled));
127        self
128    }
129
130    pub fn icon(mut self, icon: IconName) -> Self {
131        self.icon = Some(icon);
132        self
133    }
134
135    pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
136        self.custom_icon_path = Some(path.into());
137        self.custom_icon_svg = None; // Clear other icon sources if custom path is set
138        self.icon = None;
139        self
140    }
141
142    pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {
143        self.custom_icon_svg = Some(svg.into());
144        self.custom_icon_path = None; // Clear other icon sources if custom path is set
145        self.icon = None;
146        self
147    }
148
149    pub fn icon_position(mut self, position: IconPosition) -> Self {
150        self.icon_position = position;
151        self
152    }
153
154    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
155        self.icon_size = icon_size;
156        self
157    }
158
159    pub fn icon_color(mut self, icon_color: Color) -> Self {
160        self.icon_color = Some(icon_color);
161        self
162    }
163
164    pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
165        self.toggle = Some((toggle_position, toggled));
166        self
167    }
168
169    pub fn action(mut self, action: Box<dyn Action>) -> Self {
170        self.action = Some(action);
171        self
172    }
173
174    pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
175        self.handler = Rc::new(move |_, window, cx| handler(window, cx));
176        self
177    }
178
179    pub fn secondary_handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
180        self.secondary_handler = Some(Rc::new(move |_, window, cx| handler(window, cx)));
181        self
182    }
183
184    pub fn disabled(mut self, disabled: bool) -> Self {
185        self.disabled = disabled;
186        self
187    }
188
189    pub fn documentation_aside(
190        mut self,
191        side: DocumentationSide,
192        render: impl Fn(&mut App) -> AnyElement + 'static,
193    ) -> Self {
194        self.documentation_aside = Some(DocumentationAside {
195            side,
196            render: Rc::new(render),
197        });
198
199        self
200    }
201}
202
203impl FluentBuilder for ContextMenuEntry {}
204
205impl From<ContextMenuEntry> for ContextMenuItem {
206    fn from(entry: ContextMenuEntry) -> Self {
207        ContextMenuItem::Entry(entry)
208    }
209}
210
211pub struct ContextMenu {
212    builder: Option<Rc<dyn Fn(Self, &mut Window, &mut Context<Self>) -> Self>>,
213    items: Vec<ContextMenuItem>,
214    focus_handle: FocusHandle,
215    action_context: Option<FocusHandle>,
216    selected_index: Option<usize>,
217    delayed: bool,
218    clicked: bool,
219    end_slot_action: Option<Box<dyn Action>>,
220    key_context: SharedString,
221    _on_blur_subscription: Subscription,
222    keep_open_on_confirm: bool,
223    fixed_width: Option<DefiniteLength>,
224    main_menu: Option<Entity<ContextMenu>>,
225    main_menu_observed_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
226    // Docs aide-related fields
227    documentation_aside: Option<(usize, DocumentationAside)>,
228    aside_trigger_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
229    // Submenu-related fields
230    submenu_state: SubmenuState,
231    hover_target: HoverTarget,
232    submenu_safety_threshold_x: Option<Pixels>,
233    submenu_trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
234    submenu_trigger_mouse_down: bool,
235    ignore_blur_until: Option<Instant>,
236    /// When set to true, the next on_focus_in callback will not automatically
237    /// select an item. This prevents a visual flash where a submenu close in
238    /// on_hover(false) returns focus to the main menu and on_focus_in
239    /// re-selects the first item before the next on_hover(true) clears it.
240    suppress_focus_selection: bool,
241}
242
243#[derive(Copy, Clone, PartialEq, Eq)]
244pub enum DocumentationSide {
245    Left,
246    Right,
247}
248
249#[derive(Clone)]
250pub struct DocumentationAside {
251    pub side: DocumentationSide,
252    pub render: Rc<dyn Fn(&mut App) -> AnyElement>,
253}
254
255impl DocumentationAside {
256    pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {
257        Self { side, render }
258    }
259}
260
261impl Focusable for ContextMenu {
262    fn focus_handle(&self, _cx: &App) -> FocusHandle {
263        self.focus_handle.clone()
264    }
265}
266
267impl EventEmitter<DismissEvent> for ContextMenu {}
268
269impl FluentBuilder for ContextMenu {}
270
271impl ContextMenu {
272    pub fn new(
273        window: &mut Window,
274        cx: &mut Context<Self>,
275        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
276    ) -> Self {
277        let focus_handle = cx.focus_handle();
278        let _on_blur_subscription = cx.on_blur(
279            &focus_handle,
280            window,
281            |this: &mut ContextMenu, window, cx| {
282                if let Some(ignore_until) = this.ignore_blur_until {
283                    if Instant::now() < ignore_until {
284                        return;
285                    } else {
286                        this.ignore_blur_until = None;
287                    }
288                }
289
290                if this.main_menu.is_none() {
291                    if let SubmenuState::Open(open_submenu) = &this.submenu_state {
292                        let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
293                        if submenu_focus.contains_focused(window, cx) {
294                            return;
295                        }
296                    }
297                }
298
299                this.cancel(&menu::Cancel, window, cx)
300            },
301        );
302        window.refresh();
303
304        // When the menu first receives focus (i.e. when it opens), move the
305        // selection onto a menu item so assistive technology announces a real
306        // item rather than the bare menu container. Per the ARIA menu button
307        // pattern, opening a menu places focus on a menu item; for select-style
308        // menus we prefer the currently-checked item. We only do this when
309        // nothing is selected yet so we don't override an existing selection.
310        cx.on_focus_in(&focus_handle, window, |this, window, cx| {
311            if this.selected_index.is_none() && !this.suppress_focus_selection {
312                this.select_toggled_or_first(window, cx);
313            }
314            this.suppress_focus_selection = false;
315        })
316        .detach();
317
318        f(
319            Self {
320                builder: None,
321                items: Default::default(),
322                focus_handle,
323                action_context: None,
324                selected_index: None,
325                delayed: false,
326                clicked: false,
327                end_slot_action: None,
328                key_context: "menu".into(),
329                _on_blur_subscription,
330                keep_open_on_confirm: false,
331                fixed_width: None,
332                main_menu: None,
333                main_menu_observed_bounds: Rc::new(Cell::new(None)),
334                documentation_aside: None,
335                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
336                submenu_state: SubmenuState::Closed,
337                hover_target: HoverTarget::MainMenu,
338                submenu_safety_threshold_x: None,
339                submenu_trigger_bounds: Rc::new(Cell::new(None)),
340                submenu_trigger_mouse_down: false,
341                ignore_blur_until: None,
342                suppress_focus_selection: false,
343            },
344            window,
345            cx,
346        )
347    }
348
349    pub fn build(
350        window: &mut Window,
351        cx: &mut App,
352        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
353    ) -> Entity<Self> {
354        cx.new(|cx| Self::new(window, cx, f))
355    }
356
357    /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation.
358    ///
359    /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto
360    /// it to call it again.
361    pub fn build_persistent(
362        window: &mut Window,
363        cx: &mut App,
364        builder: impl Fn(Self, &mut Window, &mut Context<Self>) -> Self + 'static,
365    ) -> Entity<Self> {
366        cx.new(|cx| {
367            let builder = Rc::new(builder);
368
369            let focus_handle = cx.focus_handle();
370            let _on_blur_subscription = cx.on_blur(
371                &focus_handle,
372                window,
373                |this: &mut ContextMenu, window, cx| {
374                    if let Some(ignore_until) = this.ignore_blur_until {
375                        if Instant::now() < ignore_until {
376                            return;
377                        } else {
378                            this.ignore_blur_until = None;
379                        }
380                    }
381
382                    if this.main_menu.is_none() {
383                        if let SubmenuState::Open(open_submenu) = &this.submenu_state {
384                            let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
385                            if submenu_focus.contains_focused(window, cx) {
386                                return;
387                            }
388                        }
389                    }
390
391                    this.cancel(&menu::Cancel, window, cx)
392                },
393            );
394            window.refresh();
395
396            // See the note in `ContextMenu::new`: select an item when the menu
397            // opens so screen readers announce it instead of just "menu".
398            cx.on_focus_in(&focus_handle, window, |this, window, cx| {
399                if this.selected_index.is_none() {
400                    this.select_toggled_or_first(window, cx);
401                }
402            })
403            .detach();
404
405            (builder.clone())(
406                Self {
407                    builder: Some(builder),
408                    items: Default::default(),
409                    focus_handle,
410                    action_context: None,
411                    selected_index: None,
412                    delayed: false,
413                    clicked: false,
414                    end_slot_action: None,
415                    key_context: "menu".into(),
416                    _on_blur_subscription,
417                    keep_open_on_confirm: true,
418                    fixed_width: None,
419                    main_menu: None,
420                    main_menu_observed_bounds: Rc::new(Cell::new(None)),
421                    documentation_aside: None,
422                    aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
423                    submenu_state: SubmenuState::Closed,
424                    hover_target: HoverTarget::MainMenu,
425                    submenu_safety_threshold_x: None,
426                    submenu_trigger_bounds: Rc::new(Cell::new(None)),
427                    submenu_trigger_mouse_down: false,
428                    ignore_blur_until: None,
429                    suppress_focus_selection: false,
430                },
431                window,
432                cx,
433            )
434        })
435    }
436
437    /// Rebuilds the menu.
438    ///
439    /// This is used to refresh the menu entries when entries are toggled when the menu is configured with
440    /// `keep_open_on_confirm = true`.
441    ///
442    /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is
443    /// a no-op.
444    pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {
445        let Some(builder) = self.builder.clone() else {
446            return;
447        };
448
449        // The way we rebuild the menu is a bit of a hack.
450        let focus_handle = cx.focus_handle();
451        let new_menu = (builder.clone())(
452            Self {
453                builder: Some(builder),
454                items: Default::default(),
455                focus_handle: focus_handle.clone(),
456                action_context: None,
457                selected_index: None,
458                delayed: false,
459                clicked: false,
460                end_slot_action: None,
461                key_context: "menu".into(),
462                _on_blur_subscription: cx.on_blur(
463                    &focus_handle,
464                    window,
465                    |this: &mut ContextMenu, window, cx| {
466                        if let Some(ignore_until) = this.ignore_blur_until {
467                            if Instant::now() < ignore_until {
468                                return;
469                            } else {
470                                this.ignore_blur_until = None;
471                            }
472                        }
473
474                        if this.main_menu.is_none() {
475                            if let SubmenuState::Open(open_submenu) = &this.submenu_state {
476                                let submenu_focus =
477                                    open_submenu.entity.read(cx).focus_handle.clone();
478                                if submenu_focus.contains_focused(window, cx) {
479                                    return;
480                                }
481                            }
482                        }
483
484                        this.cancel(&menu::Cancel, window, cx)
485                    },
486                ),
487                keep_open_on_confirm: false,
488                fixed_width: None,
489                main_menu: None,
490                main_menu_observed_bounds: Rc::new(Cell::new(None)),
491                documentation_aside: None,
492                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
493                submenu_state: SubmenuState::Closed,
494                hover_target: HoverTarget::MainMenu,
495                submenu_safety_threshold_x: None,
496                submenu_trigger_bounds: Rc::new(Cell::new(None)),
497                submenu_trigger_mouse_down: false,
498                ignore_blur_until: None,
499                suppress_focus_selection: false,
500            },
501            window,
502            cx,
503        );
504
505        self.items = new_menu.items;
506
507        cx.notify();
508    }
509
510    pub fn context(mut self, focus: FocusHandle) -> Self {
511        self.action_context = Some(focus);
512        self
513    }
514
515    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
516        self.items.push(ContextMenuItem::Header(title.into()));
517        self
518    }
519
520    pub fn header_with_link(
521        mut self,
522        title: impl Into<SharedString>,
523        link_label: impl Into<SharedString>,
524        link_url: impl Into<SharedString>,
525    ) -> Self {
526        self.items.push(ContextMenuItem::HeaderWithLink(
527            title.into(),
528            link_label.into(),
529            link_url.into(),
530        ));
531        self
532    }
533
534    pub fn separator(mut self) -> Self {
535        self.items.push(ContextMenuItem::Separator);
536        self
537    }
538
539    pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
540        self.items.extend(items.into_iter().map(Into::into));
541        self
542    }
543
544    pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
545        self.items.push(item.into());
546        self
547    }
548
549    pub fn push_item(&mut self, item: impl Into<ContextMenuItem>) {
550        self.items.push(item.into());
551    }
552
553    pub fn entry(
554        mut self,
555        label: impl Into<SharedString>,
556        action: Option<Box<dyn Action>>,
557        handler: impl Fn(&mut Window, &mut App) + 'static,
558    ) -> Self {
559        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
560            toggle: None,
561            label: label.into(),
562            handler: Rc::new(move |_, window, cx| handler(window, cx)),
563            secondary_handler: None,
564            icon: None,
565            custom_icon_path: None,
566            custom_icon_svg: None,
567            icon_position: IconPosition::End,
568            icon_size: IconSize::Small,
569            icon_color: None,
570            action,
571            disabled: false,
572            documentation_aside: None,
573            end_slot_icon: None,
574            end_slot_title: None,
575            end_slot_handler: None,
576            show_end_slot_on_hover: false,
577        }));
578        self
579    }
580
581    pub fn entry_with_end_slot(
582        mut self,
583        label: impl Into<SharedString>,
584        action: Option<Box<dyn Action>>,
585        handler: impl Fn(&mut Window, &mut App) + 'static,
586        end_slot_icon: IconName,
587        end_slot_title: SharedString,
588        end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
589    ) -> Self {
590        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
591            toggle: None,
592            label: label.into(),
593            handler: Rc::new(move |_, window, cx| handler(window, cx)),
594            secondary_handler: None,
595            icon: None,
596            custom_icon_path: None,
597            custom_icon_svg: None,
598            icon_position: IconPosition::End,
599            icon_size: IconSize::Small,
600            icon_color: None,
601            action,
602            disabled: false,
603            documentation_aside: None,
604            end_slot_icon: Some(end_slot_icon),
605            end_slot_title: Some(end_slot_title),
606            end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
607            show_end_slot_on_hover: false,
608        }));
609        self
610    }
611
612    pub fn entry_with_end_slot_on_hover(
613        mut self,
614        label: impl Into<SharedString>,
615        action: Option<Box<dyn Action>>,
616        handler: impl Fn(&mut Window, &mut App) + 'static,
617        end_slot_icon: IconName,
618        end_slot_title: SharedString,
619        end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
620    ) -> Self {
621        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
622            toggle: None,
623            label: label.into(),
624            handler: Rc::new(move |_, window, cx| handler(window, cx)),
625            secondary_handler: None,
626            icon: None,
627            custom_icon_path: None,
628            custom_icon_svg: None,
629            icon_position: IconPosition::End,
630            icon_size: IconSize::Small,
631            icon_color: None,
632            action,
633            disabled: false,
634            documentation_aside: None,
635            end_slot_icon: Some(end_slot_icon),
636            end_slot_title: Some(end_slot_title),
637            end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
638            show_end_slot_on_hover: true,
639        }));
640        self
641    }
642
643    pub fn toggleable_entry(
644        self,
645        label: impl Into<SharedString>,
646        toggled: bool,
647        position: IconPosition,
648        action: Option<Box<dyn Action>>,
649        handler: impl Fn(&mut Window, &mut App) + 'static,
650    ) -> Self {
651        self.toggleable_entry_disabled_when(label, toggled, false, position, action, handler)
652    }
653
654    /// Like [`Self::toggleable_entry`], but the entry is rendered disabled (and its handler is not
655    /// invoked) when `disabled` is `true`.
656    pub fn toggleable_entry_disabled_when(
657        mut self,
658        label: impl Into<SharedString>,
659        toggled: bool,
660        disabled: bool,
661        position: IconPosition,
662        action: Option<Box<dyn Action>>,
663        handler: impl Fn(&mut Window, &mut App) + 'static,
664    ) -> Self {
665        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
666            toggle: Some((position, toggled)),
667            label: label.into(),
668            handler: Rc::new(move |_, window, cx| handler(window, cx)),
669            secondary_handler: None,
670            icon: None,
671            custom_icon_path: None,
672            custom_icon_svg: None,
673            icon_position: position,
674            icon_size: IconSize::Small,
675            icon_color: None,
676            action,
677            disabled,
678            documentation_aside: None,
679            end_slot_icon: None,
680            end_slot_title: None,
681            end_slot_handler: None,
682            show_end_slot_on_hover: false,
683        }));
684        self
685    }
686
687    pub fn custom_row(
688        mut self,
689        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
690    ) -> Self {
691        self.items.push(ContextMenuItem::CustomEntry {
692            entry_render: Box::new(entry_render),
693            handler: Rc::new(|_, _, _| {}),
694            selectable: false,
695            documentation_aside: None,
696        });
697        self
698    }
699
700    pub fn custom_entry(
701        mut self,
702        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
703        handler: impl Fn(&mut Window, &mut App) + 'static,
704    ) -> Self {
705        self.items.push(ContextMenuItem::CustomEntry {
706            entry_render: Box::new(entry_render),
707            handler: Rc::new(move |_, window, cx| handler(window, cx)),
708            selectable: true,
709            documentation_aside: None,
710        });
711        self
712    }
713
714    pub fn custom_entry_with_docs(
715        mut self,
716        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
717        handler: impl Fn(&mut Window, &mut App) + 'static,
718        documentation_aside: Option<DocumentationAside>,
719    ) -> Self {
720        self.items.push(ContextMenuItem::CustomEntry {
721            entry_render: Box::new(entry_render),
722            handler: Rc::new(move |_, window, cx| handler(window, cx)),
723            selectable: true,
724            documentation_aside,
725        });
726        self
727    }
728
729    pub fn selectable(mut self, selectable: bool) -> Self {
730        if let Some(ContextMenuItem::CustomEntry {
731            selectable: entry_selectable,
732            ..
733        }) = self.items.last_mut()
734        {
735            *entry_selectable = selectable;
736        }
737        self
738    }
739
740    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
741        self.items.push(ContextMenuItem::Label(label.into()));
742        self
743    }
744
745    pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
746        self.action_checked(label, action, false)
747    }
748
749    pub fn action_checked(
750        self,
751        label: impl Into<SharedString>,
752        action: Box<dyn Action>,
753        checked: bool,
754    ) -> Self {
755        self.action_checked_with_disabled(label, action, checked, false)
756    }
757
758    pub fn action_checked_with_disabled(
759        mut self,
760        label: impl Into<SharedString>,
761        action: Box<dyn Action>,
762        checked: bool,
763        disabled: bool,
764    ) -> Self {
765        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
766            toggle: if checked {
767                Some((IconPosition::Start, true))
768            } else {
769                None
770            },
771            label: label.into(),
772            action: Some(action.boxed_clone()),
773            handler: Rc::new(move |context, window, cx| {
774                if let Some(context) = &context {
775                    window.focus(context, cx);
776                }
777                window.dispatch_action(action.boxed_clone(), cx);
778            }),
779            secondary_handler: None,
780            icon: None,
781            custom_icon_path: None,
782            custom_icon_svg: None,
783            icon_position: IconPosition::End,
784            icon_size: IconSize::Small,
785            icon_color: None,
786            disabled,
787            documentation_aside: None,
788            end_slot_icon: None,
789            end_slot_title: None,
790            end_slot_handler: None,
791            show_end_slot_on_hover: false,
792        }));
793        self
794    }
795
796    pub fn action_disabled_when(
797        mut self,
798        disabled: bool,
799        label: impl Into<SharedString>,
800        action: Box<dyn Action>,
801    ) -> Self {
802        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
803            toggle: None,
804            label: label.into(),
805            action: Some(action.boxed_clone()),
806            handler: Rc::new(move |context, window, cx| {
807                if let Some(context) = &context {
808                    window.focus(context, cx);
809                }
810                window.dispatch_action(action.boxed_clone(), cx);
811            }),
812            secondary_handler: None,
813            icon: None,
814            custom_icon_path: None,
815            custom_icon_svg: None,
816            icon_size: IconSize::Small,
817            icon_position: IconPosition::End,
818            icon_color: None,
819            disabled,
820            documentation_aside: None,
821            end_slot_icon: None,
822            end_slot_title: None,
823            end_slot_handler: None,
824            show_end_slot_on_hover: false,
825        }));
826        self
827    }
828
829    pub fn link(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
830        self.link_with_handler(label, action, |_, _| {})
831    }
832
833    pub fn link_with_handler(
834        mut self,
835        label: impl Into<SharedString>,
836        action: Box<dyn Action>,
837        handler: impl Fn(&mut Window, &mut App) + 'static,
838    ) -> Self {
839        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
840            toggle: None,
841            label: label.into(),
842            action: Some(action.boxed_clone()),
843            handler: Rc::new(move |_, window, cx| {
844                handler(window, cx);
845                window.dispatch_action(action.boxed_clone(), cx);
846            }),
847            secondary_handler: None,
848            icon: Some(IconName::ArrowUpRight),
849            custom_icon_path: None,
850            custom_icon_svg: None,
851            icon_size: IconSize::XSmall,
852            icon_position: IconPosition::End,
853            icon_color: None,
854            disabled: false,
855            documentation_aside: None,
856            end_slot_icon: None,
857            end_slot_title: None,
858            end_slot_handler: None,
859            show_end_slot_on_hover: false,
860        }));
861        self
862    }
863
864    pub fn submenu(
865        mut self,
866        label: impl Into<SharedString>,
867        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
868    ) -> Self {
869        self.items.push(ContextMenuItem::Submenu {
870            label: label.into(),
871            icon: None,
872            icon_color: None,
873            builder: Rc::new(builder),
874        });
875        self
876    }
877
878    pub fn submenu_with_icon(
879        mut self,
880        label: impl Into<SharedString>,
881        icon: IconName,
882        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
883    ) -> Self {
884        self.items.push(ContextMenuItem::Submenu {
885            label: label.into(),
886            icon: Some(icon),
887            icon_color: None,
888            builder: Rc::new(builder),
889        });
890        self
891    }
892
893    pub fn submenu_with_colored_icon(
894        mut self,
895        label: impl Into<SharedString>,
896        icon: IconName,
897        icon_color: Color,
898        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
899    ) -> Self {
900        self.items.push(ContextMenuItem::Submenu {
901            label: label.into(),
902            icon: Some(icon),
903            icon_color: Some(icon_color),
904            builder: Rc::new(builder),
905        });
906        self
907    }
908
909    pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
910        self.keep_open_on_confirm = keep_open;
911        self
912    }
913
914    pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
915        let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
916            return;
917        };
918        let ContextMenuItem::Entry(entry) = entry else {
919            return;
920        };
921        let Some(handler) = entry.end_slot_handler.as_ref() else {
922            return;
923        };
924        handler(None, window, cx);
925    }
926
927    pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
928        self.fixed_width = Some(width);
929        self
930    }
931
932    pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
933        self.end_slot_action = Some(action);
934        self
935    }
936
937    pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
938        self.key_context = context.into();
939        self
940    }
941
942    pub fn selected_index(&self) -> Option<usize> {
943        self.selected_index
944    }
945
946    pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
947        let Some(ix) = self.selected_index else {
948            return;
949        };
950
951        if let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) {
952            self.open_submenu(
953                ix,
954                builder.clone(),
955                SubmenuOpenTrigger::Keyboard,
956                window,
957                cx,
958            );
959
960            if let SubmenuState::Open(open_submenu) = &self.submenu_state {
961                let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
962                window.focus(&focus_handle, cx);
963                open_submenu.entity.update(cx, |submenu, cx| {
964                    submenu.select_first(&SelectFirst, window, cx);
965                });
966            }
967
968            cx.notify();
969            return;
970        }
971
972        let context = self.action_context.as_ref();
973
974        if let Some(
975            ContextMenuItem::Entry(ContextMenuEntry {
976                handler,
977                disabled: false,
978                ..
979            })
980            | ContextMenuItem::CustomEntry { handler, .. },
981        ) = self.items.get(ix)
982        {
983            (handler)(context, window, cx)
984        }
985
986        if self.main_menu.is_some() && !self.keep_open_on_confirm {
987            self.clicked = true;
988        }
989
990        if self.keep_open_on_confirm {
991            self.rebuild(window, cx);
992        } else {
993            cx.emit(DismissEvent);
994        }
995    }
996
997    pub fn secondary_confirm(
998        &mut self,
999        _: &menu::SecondaryConfirm,
1000        window: &mut Window,
1001        cx: &mut Context<Self>,
1002    ) {
1003        let Some(ix) = self.selected_index else {
1004            return;
1005        };
1006
1007        if let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) {
1008            self.open_submenu(
1009                ix,
1010                builder.clone(),
1011                SubmenuOpenTrigger::Keyboard,
1012                window,
1013                cx,
1014            );
1015
1016            if let SubmenuState::Open(open_submenu) = &self.submenu_state {
1017                let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
1018                window.focus(&focus_handle, cx);
1019                open_submenu.entity.update(cx, |submenu, cx| {
1020                    submenu.select_first(&SelectFirst, window, cx);
1021                });
1022            }
1023
1024            cx.notify();
1025            return;
1026        }
1027
1028        let context = self.action_context.as_ref();
1029
1030        if let Some(ContextMenuItem::Entry(ContextMenuEntry {
1031            handler,
1032            secondary_handler,
1033            disabled: false,
1034            ..
1035        })) = self.items.get(ix)
1036        {
1037            if let Some(secondary) = secondary_handler {
1038                (secondary)(context, window, cx)
1039            } else {
1040                (handler)(context, window, cx)
1041            }
1042        } else if let Some(ContextMenuItem::CustomEntry { handler, .. }) = self.items.get(ix) {
1043            (handler)(context, window, cx)
1044        }
1045
1046        if self.main_menu.is_some() && !self.keep_open_on_confirm {
1047            self.clicked = true;
1048        }
1049
1050        if self.keep_open_on_confirm {
1051            self.rebuild(window, cx);
1052        } else {
1053            cx.emit(DismissEvent);
1054        }
1055    }
1056
1057    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1058        if self.main_menu.is_some() {
1059            cx.emit(DismissEvent);
1060
1061            // Restore keyboard focus to the parent menu so arrow keys / Escape / Enter work again.
1062            if let Some(parent) = &self.main_menu {
1063                let parent_focus = parent.read(cx).focus_handle.clone();
1064
1065                parent.update(cx, |parent, _cx| {
1066                    parent.ignore_blur_until = Some(Instant::now() + Duration::from_millis(200));
1067                });
1068
1069                window.focus(&parent_focus, cx);
1070            }
1071
1072            return;
1073        }
1074
1075        cx.emit(DismissEvent);
1076    }
1077
1078    pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
1079        let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
1080            return;
1081        };
1082        let ContextMenuItem::Entry(entry) = item else {
1083            return;
1084        };
1085        let Some(handler) = entry.end_slot_handler.as_ref() else {
1086            return;
1087        };
1088        handler(None, window, cx);
1089        self.rebuild(window, cx);
1090        cx.notify();
1091    }
1092
1093    pub fn clear_selected(&mut self) {
1094        self.selected_index = None;
1095    }
1096
1097    pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1098        if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
1099            self.select_index(ix, window, cx);
1100        }
1101        cx.notify();
1102    }
1103
1104    /// Selects the currently-checked entry if one exists (e.g. the active value
1105    /// in a single-select dropdown), otherwise the first selectable item.
1106    ///
1107    /// This is intended to be called when the menu opens. Per the ARIA menu
1108    /// button pattern, opening a menu should place focus on a menu item rather
1109    /// than the menu container, so that assistive technology immediately
1110    /// announces a meaningful item (ideally the current selection) instead of
1111    /// just "menu".
1112    pub fn select_toggled_or_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1113        let toggled_ix = self.items.iter().position(|item| {
1114            matches!(
1115                item,
1116                ContextMenuItem::Entry(ContextMenuEntry {
1117                    toggle: Some((_, true)),
1118                    ..
1119                })
1120            )
1121        });
1122        if let Some(ix) = toggled_ix {
1123            self.select_index(ix, window, cx);
1124            cx.notify();
1125        } else {
1126            self.select_first(&SelectFirst, window, cx);
1127        }
1128    }
1129
1130    pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
1131        for (ix, item) in self.items.iter().enumerate().rev() {
1132            if item.is_selectable() {
1133                return self.select_index(ix, window, cx);
1134            }
1135        }
1136        None
1137    }
1138
1139    fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1140        if self.select_last(window, cx).is_some() {
1141            cx.notify();
1142        }
1143    }
1144
1145    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1146        if let Some(ix) = self.selected_index {
1147            let next_index = ix + 1;
1148            if self.items.len() <= next_index {
1149                self.select_first(&SelectFirst, window, cx);
1150                return;
1151            } else {
1152                for (ix, item) in self.items.iter().enumerate().skip(next_index) {
1153                    if item.is_selectable() {
1154                        self.select_index(ix, window, cx);
1155                        cx.notify();
1156                        return;
1157                    }
1158                }
1159            }
1160        }
1161        self.select_first(&SelectFirst, window, cx);
1162    }
1163
1164    pub fn select_previous(
1165        &mut self,
1166        _: &SelectPrevious,
1167        window: &mut Window,
1168        cx: &mut Context<Self>,
1169    ) {
1170        if let Some(ix) = self.selected_index {
1171            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
1172                if item.is_selectable() {
1173                    self.select_index(ix, window, cx);
1174                    cx.notify();
1175                    return;
1176                }
1177            }
1178        }
1179        self.handle_select_last(&SelectLast, window, cx);
1180    }
1181
1182    pub fn select_submenu_child(
1183        &mut self,
1184        _: &SelectChild,
1185        window: &mut Window,
1186        cx: &mut Context<Self>,
1187    ) {
1188        let Some(ix) = self.selected_index else {
1189            return;
1190        };
1191
1192        let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) else {
1193            return;
1194        };
1195
1196        self.open_submenu(
1197            ix,
1198            builder.clone(),
1199            SubmenuOpenTrigger::Keyboard,
1200            window,
1201            cx,
1202        );
1203
1204        if let SubmenuState::Open(open_submenu) = &self.submenu_state {
1205            let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
1206            window.focus(&focus_handle, cx);
1207            open_submenu.entity.update(cx, |submenu, cx| {
1208                submenu.select_first(&SelectFirst, window, cx);
1209            });
1210        }
1211
1212        cx.notify();
1213    }
1214
1215    pub fn select_submenu_parent(
1216        &mut self,
1217        _: &SelectParent,
1218        window: &mut Window,
1219        cx: &mut Context<Self>,
1220    ) {
1221        if self.main_menu.is_none() {
1222            return;
1223        }
1224
1225        if let Some(parent) = &self.main_menu {
1226            let parent_clone = parent.clone();
1227
1228            let parent_focus = parent.read(cx).focus_handle.clone();
1229            window.focus(&parent_focus, cx);
1230
1231            cx.emit(DismissEvent);
1232
1233            parent_clone.update(cx, |parent, cx| {
1234                if let SubmenuState::Open(open_submenu) = &parent.submenu_state {
1235                    let trigger_index = open_submenu.item_index;
1236                    parent.close_submenu(false, cx);
1237                    let _ = parent.select_index(trigger_index, window, cx);
1238                    cx.notify();
1239                }
1240            });
1241
1242            return;
1243        }
1244
1245        cx.emit(DismissEvent);
1246    }
1247
1248    fn select_index(
1249        &mut self,
1250        ix: usize,
1251        _window: &mut Window,
1252        _cx: &mut Context<Self>,
1253    ) -> Option<usize> {
1254        self.documentation_aside = None;
1255        let item = self.items.get(ix)?;
1256        if item.is_selectable() {
1257            self.selected_index = Some(ix);
1258            match item {
1259                ContextMenuItem::Entry(entry) => {
1260                    if let Some(callback) = &entry.documentation_aside {
1261                        self.documentation_aside = Some((ix, callback.clone()));
1262                    }
1263                }
1264                ContextMenuItem::CustomEntry {
1265                    documentation_aside: Some(callback),
1266                    ..
1267                } => {
1268                    self.documentation_aside = Some((ix, callback.clone()));
1269                }
1270                ContextMenuItem::Submenu { .. } => {}
1271                _ => (),
1272            }
1273        }
1274        Some(ix)
1275    }
1276
1277    fn create_submenu(
1278        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1279        parent_entity: Entity<ContextMenu>,
1280        window: &mut Window,
1281        cx: &mut Context<Self>,
1282    ) -> (Entity<ContextMenu>, Subscription) {
1283        let submenu = Self::build_submenu(builder, parent_entity, window, cx);
1284
1285        let dismiss_subscription = cx.subscribe(&submenu, |this, submenu, _: &DismissEvent, cx| {
1286            let should_dismiss_parent = submenu.read(cx).clicked;
1287
1288            this.close_submenu(false, cx);
1289
1290            if should_dismiss_parent {
1291                cx.emit(DismissEvent);
1292            }
1293        });
1294
1295        (submenu, dismiss_subscription)
1296    }
1297
1298    fn build_submenu(
1299        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1300        parent_entity: Entity<ContextMenu>,
1301        window: &mut Window,
1302        cx: &mut App,
1303    ) -> Entity<ContextMenu> {
1304        cx.new(|cx| {
1305            let focus_handle = cx.focus_handle();
1306
1307            let _on_blur_subscription = cx.on_blur(
1308                &focus_handle,
1309                window,
1310                |_this: &mut ContextMenu, _window, _cx| {},
1311            );
1312
1313            let mut menu = ContextMenu {
1314                builder: None,
1315                items: Default::default(),
1316                focus_handle,
1317                action_context: None,
1318                selected_index: None,
1319                delayed: false,
1320                clicked: false,
1321                end_slot_action: None,
1322                key_context: "menu".into(),
1323                _on_blur_subscription,
1324                keep_open_on_confirm: false,
1325                fixed_width: None,
1326                documentation_aside: None,
1327                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
1328                main_menu: Some(parent_entity),
1329                main_menu_observed_bounds: Rc::new(Cell::new(None)),
1330                submenu_state: SubmenuState::Closed,
1331                hover_target: HoverTarget::MainMenu,
1332                submenu_safety_threshold_x: None,
1333                submenu_trigger_bounds: Rc::new(Cell::new(None)),
1334                submenu_trigger_mouse_down: false,
1335                ignore_blur_until: None,
1336                suppress_focus_selection: false,
1337            };
1338
1339            menu = (builder)(menu, window, cx);
1340            menu
1341        })
1342    }
1343
1344    fn close_submenu(&mut self, clear_selection: bool, cx: &mut Context<Self>) {
1345        self.submenu_state = SubmenuState::Closed;
1346        self.hover_target = HoverTarget::MainMenu;
1347        self.submenu_safety_threshold_x = None;
1348        self.main_menu_observed_bounds.set(None);
1349        self.submenu_trigger_bounds.set(None);
1350
1351        if clear_selection {
1352            self.selected_index = None;
1353        }
1354
1355        cx.notify();
1356    }
1357
1358    fn open_submenu(
1359        &mut self,
1360        item_index: usize,
1361        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1362        reason: SubmenuOpenTrigger,
1363        window: &mut Window,
1364        cx: &mut Context<Self>,
1365    ) {
1366        // If the submenu is already open for this item, don't recreate it.
1367        if matches!(
1368            &self.submenu_state,
1369            SubmenuState::Open(open_submenu) if open_submenu.item_index == item_index
1370        ) {
1371            return;
1372        }
1373
1374        let (submenu, dismiss_subscription) =
1375            Self::create_submenu(builder, cx.entity(), window, cx);
1376
1377        let flip_left = self
1378            .main_menu_observed_bounds
1379            .get()
1380            .is_some_and(|bounds| bounds.right() + px(200.0) > window.viewport_size().width);
1381
1382        // If we're switching from one submenu item to another, throw away any previously-captured
1383        // offset so we don't reuse a stale position.
1384        self.main_menu_observed_bounds.set(None);
1385        self.submenu_trigger_bounds.set(None);
1386
1387        self.submenu_safety_threshold_x = None;
1388        self.hover_target = HoverTarget::MainMenu;
1389
1390        // When opening a submenu via keyboard, there is a brief moment where focus/hover can
1391        // transition in a way that triggers the parent menu's `on_blur` dismissal.
1392        if matches!(reason, SubmenuOpenTrigger::Keyboard) {
1393            self.ignore_blur_until = Some(Instant::now() + Duration::from_millis(150));
1394        }
1395
1396        let trigger_bounds = self.submenu_trigger_bounds.get();
1397
1398        self.submenu_state = SubmenuState::Open(OpenSubmenu {
1399            item_index,
1400            entity: submenu,
1401            trigger_bounds,
1402            offset: None,
1403            flip_left,
1404            _dismiss_subscription: dismiss_subscription,
1405        });
1406
1407        cx.notify();
1408    }
1409
1410    pub fn on_action_dispatch(
1411        &mut self,
1412        dispatched: &dyn Action,
1413        window: &mut Window,
1414        cx: &mut Context<Self>,
1415    ) {
1416        if self.clicked {
1417            cx.propagate();
1418            return;
1419        }
1420
1421        if let Some(ix) = self.items.iter().position(|item| {
1422            if let ContextMenuItem::Entry(ContextMenuEntry {
1423                action: Some(action),
1424                disabled: false,
1425                ..
1426            }) = item
1427            {
1428                action.partial_eq(dispatched)
1429            } else {
1430                false
1431            }
1432        }) {
1433            self.select_index(ix, window, cx);
1434            self.delayed = true;
1435            cx.notify();
1436            let action = dispatched.boxed_clone();
1437            cx.spawn_in(window, async move |this, cx| {
1438                cx.background_executor()
1439                    .timer(Duration::from_millis(50))
1440                    .await;
1441                cx.update(|window, cx| {
1442                    this.update(cx, |this, cx| {
1443                        this.cancel(&menu::Cancel, window, cx);
1444                        window.dispatch_action(action, cx);
1445                    })
1446                })
1447            })
1448            .detach_and_log_err(cx);
1449        } else {
1450            cx.propagate()
1451        }
1452    }
1453
1454    pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
1455        self._on_blur_subscription = new_subscription;
1456        self
1457    }
1458
1459    fn render_menu_item(
1460        &self,
1461        ix: usize,
1462        item: &ContextMenuItem,
1463        window: &mut Window,
1464        cx: &mut Context<Self>,
1465    ) -> impl IntoElement + use<> {
1466        // The menu keeps real focus on its container, so for assistive
1467        // technology to track the selected item we report it as the active
1468        // descendant. GPUI only honors this while the menu actually holds
1469        // focus, so we mark the selected item unconditionally here.
1470        let is_active_descendant = |selectable: bool| selectable && Some(ix) == self.selected_index;
1471        match item {
1472            ContextMenuItem::Separator => ListSeparator.into_any_element(),
1473            ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
1474                .inset(true)
1475                .into_any_element(),
1476            ContextMenuItem::HeaderWithLink(header, label, url) => {
1477                let url = url.clone();
1478                let link_id = ElementId::Name(format!("link-{}", url).into());
1479                ListSubHeader::new(header.clone())
1480                    .inset(true)
1481                    .end_slot(
1482                        Button::new(link_id, label.clone())
1483                            .color(Color::Muted)
1484                            .label_size(LabelSize::Small)
1485                            .size(ButtonSize::None)
1486                            .style(ButtonStyle::Transparent)
1487                            .on_click(move |_, _, cx| {
1488                                let url = url.clone();
1489                                cx.open_url(&url);
1490                            })
1491                            .into_any_element(),
1492                    )
1493                    .into_any_element()
1494            }
1495            ContextMenuItem::Label(label) => ListItem::new(ix)
1496                .inset(true)
1497                .disabled(true)
1498                .child(Label::new(label.clone()))
1499                .into_any_element(),
1500            ContextMenuItem::Entry(entry) => self
1501                .render_menu_entry(ix, entry, is_active_descendant(true), window, cx)
1502                .into_any_element(),
1503            ContextMenuItem::CustomEntry {
1504                entry_render,
1505                handler,
1506                selectable,
1507                documentation_aside,
1508                ..
1509            } => {
1510                let handler = handler.clone();
1511                let menu = cx.entity().downgrade();
1512                let selectable = *selectable;
1513                let aside_trigger_bounds = self.aside_trigger_bounds.clone();
1514
1515                div()
1516                    .id(("context-menu-child", ix))
1517                    .when_some(documentation_aside.clone(), |this, documentation_aside| {
1518                        this.occlude()
1519                            .on_hover(cx.listener(move |menu, hovered, _, cx| {
1520                            if *hovered {
1521                                menu.documentation_aside = Some((ix, documentation_aside.clone()));
1522                            } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
1523                            {
1524                                menu.documentation_aside = None;
1525                            }
1526                            cx.notify();
1527                        }))
1528                    })
1529                    .when(documentation_aside.is_some(), |this| {
1530                        this.child(
1531                            canvas(
1532                                {
1533                                    let aside_trigger_bounds = aside_trigger_bounds.clone();
1534                                    move |bounds, _window, _cx| {
1535                                        aside_trigger_bounds.borrow_mut().insert(ix, bounds);
1536                                    }
1537                                },
1538                                |_bounds, _state, _window, _cx| {},
1539                            )
1540                            .size_full()
1541                            .absolute()
1542                            .top_0()
1543                            .left_0(),
1544                        )
1545                    })
1546                    .child(
1547                        ListItem::new(ix)
1548                            .inset(true)
1549                            .when(selectable, |item| item.aria_role(Role::MenuItem))
1550                            .when(is_active_descendant(selectable), |item| {
1551                                item.aria_active_descendant()
1552                            })
1553                            .toggle_state(Some(ix) == self.selected_index)
1554                            .selectable(selectable)
1555                            .when(selectable, |item| {
1556                                item.on_click({
1557                                    let context = self.action_context.clone();
1558                                    let keep_open_on_confirm = self.keep_open_on_confirm;
1559                                    move |_, window, cx| {
1560                                        handler(context.as_ref(), window, cx);
1561                                        menu.update(cx, |menu, cx| {
1562                                            menu.clicked = true;
1563
1564                                            if keep_open_on_confirm {
1565                                                menu.rebuild(window, cx);
1566                                            } else {
1567                                                cx.emit(DismissEvent);
1568                                            }
1569                                        })
1570                                        .ok();
1571                                    }
1572                                })
1573                            })
1574                            .child(entry_render(window, cx)),
1575                    )
1576                    .into_any_element()
1577            }
1578            ContextMenuItem::Submenu {
1579                label,
1580                icon,
1581                icon_color,
1582                ..
1583            } => self
1584                .render_submenu_item_trigger(
1585                    ix,
1586                    label.clone(),
1587                    *icon,
1588                    *icon_color,
1589                    is_active_descendant(true),
1590                    cx,
1591                )
1592                .into_any_element(),
1593        }
1594    }
1595
1596    fn render_submenu_item_trigger(
1597        &self,
1598        ix: usize,
1599        label: SharedString,
1600        icon: Option<IconName>,
1601        icon_color: Option<Color>,
1602        is_active_descendant: bool,
1603        cx: &mut Context<Self>,
1604    ) -> impl IntoElement {
1605        let toggle_state = Some(ix) == self.selected_index
1606            || matches!(
1607                &self.submenu_state,
1608                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1609            );
1610
1611        div()
1612            .id(("context-menu-submenu-trigger", ix))
1613            .capture_any_mouse_down(cx.listener(move |this, event: &MouseDownEvent, _, _| {
1614                // This prevents on_hover(false) from closing the submenu during a click.
1615                if event.button == MouseButton::Left {
1616                    this.submenu_trigger_mouse_down = true;
1617                }
1618            }))
1619            .capture_any_mouse_up(cx.listener(move |this, event: &MouseUpEvent, _, _| {
1620                if event.button == MouseButton::Left {
1621                    this.submenu_trigger_mouse_down = false;
1622                }
1623            }))
1624            .on_mouse_move(cx.listener(move |this, event: &MouseMoveEvent, _, cx| {
1625                if matches!(&this.submenu_state, SubmenuState::Open(_))
1626                    || this.selected_index == Some(ix)
1627                {
1628                    this.submenu_safety_threshold_x = Some(event.position.x - px(100.0));
1629                }
1630
1631                cx.notify();
1632            }))
1633            .child(
1634                ListItem::new(ix)
1635                    .inset(true)
1636                    .aria_role(Role::MenuItem)
1637                    .when(is_active_descendant, |item| item.aria_active_descendant())
1638                    .aria_label(label.clone())
1639                    .toggle_state(toggle_state)
1640                    .child(
1641                        canvas(
1642                            {
1643                                let trigger_bounds_cell = self.submenu_trigger_bounds.clone();
1644                                move |bounds, _window, _cx| {
1645                                    if toggle_state {
1646                                        trigger_bounds_cell.set(Some(bounds));
1647                                    }
1648                                }
1649                            },
1650                            |_bounds, _state, _window, _cx| {},
1651                        )
1652                        .size_full()
1653                        .absolute()
1654                        .top_0()
1655                        .left_0(),
1656                    )
1657                    .on_hover(cx.listener(move |this, hovered, window, cx| {
1658                        let mouse_pos = window.mouse_position();
1659
1660                        if *hovered {
1661                            this.clear_selected();
1662                            this.suppress_focus_selection = true;
1663                            window.focus(&this.focus_handle.clone(), cx);
1664                            this.hover_target = HoverTarget::MainMenu;
1665                            this.submenu_safety_threshold_x = Some(mouse_pos.x - px(50.0));
1666
1667                            if let Some(ContextMenuItem::Submenu { builder, .. }) =
1668                                this.items.get(ix)
1669                            {
1670                                this.open_submenu(
1671                                    ix,
1672                                    builder.clone(),
1673                                    SubmenuOpenTrigger::Pointer,
1674                                    window,
1675                                    cx,
1676                                );
1677                            }
1678
1679                            cx.notify();
1680                        } else {
1681                            if this.submenu_trigger_mouse_down {
1682                                return;
1683                            }
1684
1685                            let is_open_for_this_item = matches!(
1686                                &this.submenu_state,
1687                                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1688                            );
1689
1690                            let mouse_in_submenu_zone = this
1691                                .padded_submenu_bounds()
1692                                .is_some_and(|bounds| bounds.contains(&window.mouse_position()));
1693
1694                            if is_open_for_this_item
1695                                && this.hover_target != HoverTarget::Submenu
1696                                && !mouse_in_submenu_zone
1697                            {
1698                                this.close_submenu(false, cx);
1699                                this.clear_selected();
1700                                this.suppress_focus_selection = true;
1701                                window.focus(&this.focus_handle.clone(), cx);
1702                                cx.notify();
1703                            }
1704                        }
1705                    }))
1706                    .on_click(cx.listener(move |this, _, window, cx| {
1707                        if matches!(
1708                            &this.submenu_state,
1709                            SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1710                        ) {
1711                            return;
1712                        }
1713
1714                        if let Some(ContextMenuItem::Submenu { builder, .. }) = this.items.get(ix) {
1715                            this.open_submenu(
1716                                ix,
1717                                builder.clone(),
1718                                SubmenuOpenTrigger::Pointer,
1719                                window,
1720                                cx,
1721                            );
1722                        }
1723                    }))
1724                    .child(
1725                        h_flex()
1726                            .w_full()
1727                            .gap_2()
1728                            .justify_between()
1729                            .child(
1730                                h_flex()
1731                                    .gap_1p5()
1732                                    .when_some(icon, |this, icon_name| {
1733                                        this.child(
1734                                            Icon::new(icon_name)
1735                                                .size(IconSize::Small)
1736                                                .color(icon_color.unwrap_or(Color::Muted)),
1737                                        )
1738                                    })
1739                                    .child(Label::new(label).color(Color::Default)),
1740                            )
1741                            .child(
1742                                Icon::new(IconName::ChevronRight)
1743                                    .size(IconSize::Small)
1744                                    .color(Color::Muted),
1745                            ),
1746                    ),
1747            )
1748    }
1749
1750    fn padded_submenu_bounds(&self) -> Option<Bounds<Pixels>> {
1751        let bounds = self.main_menu_observed_bounds.get()?;
1752        Some(Bounds {
1753            origin: Point {
1754                x: bounds.origin.x - px(50.0),
1755                y: bounds.origin.y - px(50.0),
1756            },
1757            size: Size {
1758                width: bounds.size.width + px(100.0),
1759                height: bounds.size.height + px(100.0),
1760            },
1761        })
1762    }
1763
1764    fn render_submenu_container(
1765        &self,
1766        ix: usize,
1767        submenu: Entity<ContextMenu>,
1768        offset: Pixels,
1769        flip_left: bool,
1770        cx: &mut Context<Self>,
1771    ) -> impl IntoElement {
1772        let bounds_cell = self.main_menu_observed_bounds.clone();
1773        let canvas = canvas(
1774            {
1775                move |bounds, _window, _cx| {
1776                    bounds_cell.set(Some(bounds));
1777                }
1778            },
1779            |_bounds, _state, _window, _cx| {},
1780        )
1781        .size_full()
1782        .absolute()
1783        .top_0()
1784        .left_0();
1785
1786        div()
1787            .id(("submenu-container", ix))
1788            .absolute()
1789            .top(offset)
1790            .when(flip_left, |this| this.right_full().mr_neg_0p5())
1791            .when(!flip_left, |this| this.left_full().ml_neg_0p5())
1792            .on_hover(cx.listener(|this, hovered, _, _| {
1793                if *hovered {
1794                    this.hover_target = HoverTarget::Submenu;
1795                }
1796            }))
1797            .child(
1798                anchored()
1799                    .anchor(if flip_left {
1800                        Anchor::TopRight
1801                    } else {
1802                        Anchor::TopLeft
1803                    })
1804                    .snap_to_window_with_margin(px(8.0))
1805                    .child(
1806                        div()
1807                            .id(("submenu-hover-zone", ix))
1808                            .occlude()
1809                            .child(canvas)
1810                            .child(submenu),
1811                    ),
1812            )
1813    }
1814
1815    fn render_menu_entry(
1816        &self,
1817        ix: usize,
1818        entry: &ContextMenuEntry,
1819        is_active_descendant: bool,
1820        window: &mut Window,
1821        cx: &mut Context<Self>,
1822    ) -> impl IntoElement {
1823        let ContextMenuEntry {
1824            toggle,
1825            label,
1826            handler,
1827            icon,
1828            custom_icon_path,
1829            custom_icon_svg,
1830            icon_position,
1831            icon_size,
1832            icon_color,
1833            action,
1834            disabled,
1835            documentation_aside,
1836            end_slot_icon,
1837            end_slot_title,
1838            end_slot_handler,
1839            show_end_slot_on_hover,
1840            secondary_handler: _,
1841        } = entry;
1842        let this = cx.weak_entity();
1843        // Report the item's keyboard shortcut to assistive technology, resolving
1844        // the action's binding the same way the visible accelerator (rendered
1845        // below) is.
1846        let keyboard_shortcut = action.as_ref().and_then(|action| {
1847            let binding = self
1848                .action_context
1849                .as_ref()
1850                .map(|focus| KeyBinding::for_action_in(&**action, focus, cx))
1851                .unwrap_or_else(|| KeyBinding::for_action(&**action, cx));
1852            binding.keyboard_shortcut_text(window, cx)
1853        });
1854
1855        let handler = handler.clone();
1856        let menu = cx.entity().downgrade();
1857
1858        let icon_color = if *disabled {
1859            Color::Muted
1860        } else if toggle.is_some() {
1861            icon_color.unwrap_or(Color::Accent)
1862        } else {
1863            icon_color.unwrap_or(Color::Default)
1864        };
1865
1866        let label_color = if *disabled {
1867            Color::Disabled
1868        } else {
1869            Color::Default
1870        };
1871
1872        let label_element = if let Some(custom_path) = custom_icon_path {
1873            h_flex()
1874                .gap_1p5()
1875                .when(
1876                    *icon_position == IconPosition::Start && toggle.is_none(),
1877                    |flex| {
1878                        flex.child(
1879                            Icon::from_path(custom_path.clone())
1880                                .size(*icon_size)
1881                                .color(icon_color),
1882                        )
1883                    },
1884                )
1885                .child(Label::new(label.clone()).color(label_color).truncate())
1886                .when(*icon_position == IconPosition::End, |flex| {
1887                    flex.child(
1888                        Icon::from_path(custom_path.clone())
1889                            .size(*icon_size)
1890                            .color(icon_color),
1891                    )
1892                })
1893                .into_any_element()
1894        } else if let Some(custom_icon_svg) = custom_icon_svg {
1895            h_flex()
1896                .gap_1p5()
1897                .when(
1898                    *icon_position == IconPosition::Start && toggle.is_none(),
1899                    |flex| {
1900                        flex.child(
1901                            Icon::from_external_svg(custom_icon_svg.clone())
1902                                .size(*icon_size)
1903                                .color(icon_color),
1904                        )
1905                    },
1906                )
1907                .child(Label::new(label.clone()).color(label_color).truncate())
1908                .when(*icon_position == IconPosition::End, |flex| {
1909                    flex.child(
1910                        Icon::from_external_svg(custom_icon_svg.clone())
1911                            .size(*icon_size)
1912                            .color(icon_color),
1913                    )
1914                })
1915                .into_any_element()
1916        } else if let Some(icon_name) = icon {
1917            h_flex()
1918                .gap_1p5()
1919                .when(
1920                    *icon_position == IconPosition::Start && toggle.is_none(),
1921                    |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
1922                )
1923                .child(Label::new(label.clone()).color(label_color).truncate())
1924                .when(*icon_position == IconPosition::End, |flex| {
1925                    flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
1926                })
1927                .into_any_element()
1928        } else {
1929            Label::new(label.clone())
1930                .color(label_color)
1931                .truncate()
1932                .into_any_element()
1933        };
1934
1935        let aside_trigger_bounds = self.aside_trigger_bounds.clone();
1936
1937        div()
1938            .id(("context-menu-child", ix))
1939            .when_some(documentation_aside.clone(), |this, documentation_aside| {
1940                this.occlude()
1941                    .on_hover(cx.listener(move |menu, hovered, _, cx| {
1942                        if *hovered {
1943                            menu.documentation_aside = Some((ix, documentation_aside.clone()));
1944                        } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
1945                            menu.documentation_aside = None;
1946                        }
1947                        cx.notify();
1948                    }))
1949            })
1950            .when(documentation_aside.is_some(), |this| {
1951                this.child(
1952                    canvas(
1953                        {
1954                            let aside_trigger_bounds = aside_trigger_bounds.clone();
1955                            move |bounds, _window, _cx| {
1956                                aside_trigger_bounds.borrow_mut().insert(ix, bounds);
1957                            }
1958                        },
1959                        |_bounds, _state, _window, _cx| {},
1960                    )
1961                    .size_full()
1962                    .absolute()
1963                    .top_0()
1964                    .left_0(),
1965                )
1966            })
1967            .child(
1968                ListItem::new(ix)
1969                    .group_name("label_container")
1970                    .inset(true)
1971                    .disabled(*disabled)
1972                    .aria_role(if toggle.is_some() {
1973                        Role::MenuItemCheckBox
1974                    } else {
1975                        Role::MenuItem
1976                    })
1977                    .when_some(*toggle, |item, (_, checked)| item.aria_checked(checked))
1978                    .when(is_active_descendant, |item| item.aria_active_descendant())
1979                    .aria_label(label.clone())
1980                    .when_some(keyboard_shortcut, |item, keyboard_shortcut| {
1981                        item.aria_keyshortcuts(keyboard_shortcut)
1982                    })
1983                    .toggle_state(Some(ix) == self.selected_index)
1984                    .when(self.main_menu.is_none() && !*disabled, |item| {
1985                        item.on_hover(cx.listener(move |this, hovered, window, cx| {
1986                            if *hovered {
1987                                this.clear_selected();
1988                                this.suppress_focus_selection = true;
1989                                window.focus(&this.focus_handle.clone(), cx);
1990
1991                                if let SubmenuState::Open(open_submenu) = &this.submenu_state {
1992                                    if open_submenu.item_index != ix {
1993                                        this.close_submenu(false, cx);
1994                                        cx.notify();
1995                                    }
1996                                }
1997                            }
1998                        }))
1999                    })
2000                    .when(self.main_menu.is_some(), |item| {
2001                        item.on_click(cx.listener(move |this, _, window, cx| {
2002                            if matches!(
2003                                &this.submenu_state,
2004                                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
2005                            ) {
2006                                return;
2007                            }
2008
2009                            if let Some(ContextMenuItem::Submenu { builder, .. }) =
2010                                this.items.get(ix)
2011                            {
2012                                this.open_submenu(
2013                                    ix,
2014                                    builder.clone(),
2015                                    SubmenuOpenTrigger::Pointer,
2016                                    window,
2017                                    cx,
2018                                );
2019                            }
2020                        }))
2021                        .on_hover(cx.listener(
2022                            move |this, hovered, window, cx| {
2023                                if *hovered {
2024                                    this.clear_selected();
2025                                    cx.notify();
2026                                }
2027
2028                                if let Some(parent) = &this.main_menu {
2029                                    let mouse_pos = window.mouse_position();
2030                                    let parent_clone = parent.clone();
2031
2032                                    if *hovered {
2033                                        parent.update(cx, |parent, _| {
2034                                            parent.clear_selected();
2035                                            parent.hover_target = HoverTarget::Submenu;
2036                                        });
2037                                    } else {
2038                                        parent_clone.update(cx, |parent, cx| {
2039                                            if matches!(
2040                                                &parent.submenu_state,
2041                                                SubmenuState::Open(_)
2042                                            ) {
2043                                                // Only close if mouse is to the left of the safety threshold
2044                                                // (prevents accidental close when moving diagonally toward submenu)
2045                                                let should_close = parent
2046                                                    .submenu_safety_threshold_x
2047                                                    .map(|threshold_x| mouse_pos.x < threshold_x)
2048                                                    .unwrap_or(true);
2049
2050                                                if should_close {
2051                                                    parent.close_submenu(true, cx);
2052                                                }
2053                                            }
2054                                        });
2055                                    }
2056                                }
2057                            },
2058                        ))
2059                    })
2060                    .when_some(*toggle, |list_item, (position, toggled)| {
2061                        let contents = div()
2062                            .flex_none()
2063                            .child(
2064                                Icon::new(icon.unwrap_or(IconName::Check))
2065                                    .color(icon_color)
2066                                    .size(*icon_size),
2067                            )
2068                            .when(!toggled, |contents| contents.invisible());
2069
2070                        match position {
2071                            IconPosition::Start => list_item.start_slot(contents),
2072                            IconPosition::End => list_item.end_slot(contents),
2073                        }
2074                    })
2075                    .child(
2076                        h_flex()
2077                            .w_full()
2078                            .justify_between()
2079                            .child(label_element)
2080                            .debug_selector(|| format!("MENU_ITEM-{}", label))
2081                            .children(action.as_ref().map(|action| {
2082                                let binding = self
2083                                    .action_context
2084                                    .as_ref()
2085                                    .map(|focus| KeyBinding::for_action_in(&**action, focus, cx))
2086                                    .unwrap_or_else(|| KeyBinding::for_action(&**action, cx));
2087
2088                                div()
2089                                    .ml_4()
2090                                    .child(binding.disabled(*disabled))
2091                                    .when(*disabled && documentation_aside.is_some(), |parent| {
2092                                        parent.invisible()
2093                                    })
2094                            }))
2095                            .when(*disabled && documentation_aside.is_some(), |parent| {
2096                                parent.child(
2097                                    Icon::new(IconName::Info)
2098                                        .size(IconSize::XSmall)
2099                                        .color(Color::Muted),
2100                                )
2101                            }),
2102                    )
2103                    .when_some(
2104                        end_slot_icon
2105                            .as_ref()
2106                            .zip(self.end_slot_action.as_ref())
2107                            .zip(end_slot_title.as_ref())
2108                            .zip(end_slot_handler.as_ref()),
2109                        |el, (((icon, action), title), handler)| {
2110                            el.end_slot({
2111                                let icon_button = IconButton::new("end-slot-icon", *icon)
2112                                    .shape(IconButtonShape::Square)
2113                                    .style(ButtonStyle::Subtle)
2114                                    .tooltip({
2115                                        let action_context = self.action_context.clone();
2116                                        let title = title.clone();
2117                                        let action = action.boxed_clone();
2118                                        move |_window, cx| {
2119                                            action_context
2120                                                .as_ref()
2121                                                .map(|focus| {
2122                                                    Tooltip::for_action_in(
2123                                                        title.clone(),
2124                                                        &*action,
2125                                                        focus,
2126                                                        cx,
2127                                                    )
2128                                                })
2129                                                .unwrap_or_else(|| {
2130                                                    Tooltip::for_action(title.clone(), &*action, cx)
2131                                                })
2132                                        }
2133                                    })
2134                                    .on_click({
2135                                        let handler = handler.clone();
2136                                        move |_, window, cx| {
2137                                            handler(None, window, cx);
2138                                            this.update(cx, |this, cx| {
2139                                                this.rebuild(window, cx);
2140                                                cx.notify();
2141                                            })
2142                                            .ok();
2143                                        }
2144                                    });
2145
2146                                if *show_end_slot_on_hover {
2147                                    div()
2148                                        .visible_on_hover("label_container")
2149                                        .child(icon_button)
2150                                        .into_any_element()
2151                                } else {
2152                                    icon_button.into_any_element()
2153                                }
2154                            })
2155                        },
2156                    )
2157                    .on_click({
2158                        let context = self.action_context.clone();
2159                        let keep_open_on_confirm = self.keep_open_on_confirm;
2160                        move |_, window, cx| {
2161                            handler(context.as_ref(), window, cx);
2162                            menu.update(cx, |menu, cx| {
2163                                menu.clicked = true;
2164                                if keep_open_on_confirm {
2165                                    menu.rebuild(window, cx);
2166                                } else {
2167                                    cx.emit(DismissEvent);
2168                                }
2169                            })
2170                            .ok();
2171                        }
2172                    }),
2173            )
2174            .into_any_element()
2175    }
2176}
2177
2178impl ContextMenuItem {
2179    fn is_selectable(&self) -> bool {
2180        match self {
2181            ContextMenuItem::Header(_)
2182            | ContextMenuItem::HeaderWithLink(_, _, _)
2183            | ContextMenuItem::Separator
2184            | ContextMenuItem::Label { .. } => false,
2185            ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
2186            ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
2187            ContextMenuItem::Submenu { .. } => true,
2188        }
2189    }
2190}
2191
2192impl Render for ContextMenu {
2193    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2194        let ui_font_size = theme::theme_settings(cx).ui_font_size(cx);
2195        let window_size = window.viewport_size();
2196        let rem_size = window.rem_size();
2197        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
2198
2199        let mut focus_submenu: Option<FocusHandle> = None;
2200
2201        let submenu_container = match &mut self.submenu_state {
2202            SubmenuState::Open(open_submenu) => {
2203                let is_initializing = open_submenu.offset.is_none();
2204
2205                let computed_offset = if is_initializing {
2206                    let menu_bounds = self.main_menu_observed_bounds.get();
2207                    let trigger_bounds = open_submenu
2208                        .trigger_bounds
2209                        .or_else(|| self.submenu_trigger_bounds.get());
2210
2211                    match (menu_bounds, trigger_bounds) {
2212                        (Some(menu_bounds), Some(trigger_bounds)) => {
2213                            Some(trigger_bounds.origin.y - menu_bounds.origin.y)
2214                        }
2215                        _ => None,
2216                    }
2217                } else {
2218                    None
2219                };
2220
2221                if let Some(offset) = open_submenu.offset.or(computed_offset) {
2222                    if open_submenu.offset.is_none() {
2223                        open_submenu.offset = Some(offset);
2224                    }
2225
2226                    focus_submenu = Some(open_submenu.entity.read(cx).focus_handle.clone());
2227                    Some((
2228                        open_submenu.item_index,
2229                        open_submenu.entity.clone(),
2230                        offset,
2231                        open_submenu.flip_left,
2232                    ))
2233                } else {
2234                    None
2235                }
2236            }
2237            _ => None,
2238        };
2239
2240        let aside = self.documentation_aside.clone();
2241        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
2242            WithRemSize::new(ui_font_size)
2243                .occlude()
2244                .elevation_2(cx)
2245                .w_full()
2246                .p_2()
2247                .overflow_hidden()
2248                .when(is_wide_window, |this| this.max_w_96())
2249                .when(!is_wide_window, |this| this.max_w_48())
2250                .child((aside.render)(cx))
2251        };
2252
2253        let render_menu = |cx: &mut Context<Self>, window: &mut Window| {
2254            let bounds_cell = self.main_menu_observed_bounds.clone();
2255            let menu_bounds_measure = canvas(
2256                {
2257                    move |bounds, _window, _cx| {
2258                        bounds_cell.set(Some(bounds));
2259                    }
2260                },
2261                |_bounds, _state, _window, _cx| {},
2262            )
2263            .size_full()
2264            .absolute()
2265            .top_0()
2266            .left_0();
2267
2268            WithRemSize::new(ui_font_size)
2269                .occlude()
2270                .elevation_2(cx)
2271                .flex()
2272                .flex_row()
2273                .flex_shrink_0()
2274                .child(
2275                    v_flex()
2276                        .id("context-menu")
2277                        .role(Role::Menu)
2278                        .max_h(vh(0.75, window))
2279                        .flex_shrink_0()
2280                        .child(menu_bounds_measure)
2281                        .when_some(self.fixed_width, |this, width| {
2282                            this.w(width).overflow_x_hidden()
2283                        })
2284                        .when(self.fixed_width.is_none(), |this| {
2285                            this.min_w(px(200.)).flex_1()
2286                        })
2287                        .overflow_y_scroll()
2288                        .track_focus(&self.focus_handle(cx))
2289                        .key_context(self.key_context.as_ref())
2290                        .on_action(cx.listener(ContextMenu::select_first))
2291                        .on_action(cx.listener(ContextMenu::handle_select_last))
2292                        .on_action(cx.listener(ContextMenu::select_next))
2293                        .on_action(cx.listener(ContextMenu::select_previous))
2294                        .on_action(cx.listener(ContextMenu::select_submenu_child))
2295                        .on_action(cx.listener(ContextMenu::select_submenu_parent))
2296                        .on_action(cx.listener(ContextMenu::confirm))
2297                        .on_action(cx.listener(ContextMenu::secondary_confirm))
2298                        .on_action(cx.listener(ContextMenu::cancel))
2299                        .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
2300                            if *hovered {
2301                                this.hover_target = HoverTarget::MainMenu;
2302                                if let Some(parent) = &this.main_menu {
2303                                    parent.update(cx, |parent, _| {
2304                                        parent.hover_target = HoverTarget::Submenu;
2305                                    });
2306                                }
2307                            }
2308                        }))
2309                        .on_mouse_down_out(cx.listener(
2310                            |this, event: &MouseDownEvent, window, cx| {
2311                                if matches!(&this.submenu_state, SubmenuState::Open(_)) {
2312                                    if let Some(padded_bounds) = this.padded_submenu_bounds() {
2313                                        if padded_bounds.contains(&event.position) {
2314                                            return;
2315                                        }
2316                                    }
2317                                }
2318
2319                                if let Some(parent) = &this.main_menu {
2320                                    let overridden_by_parent_trigger = parent
2321                                        .read(cx)
2322                                        .submenu_trigger_bounds
2323                                        .get()
2324                                        .is_some_and(|bounds| bounds.contains(&event.position));
2325                                    if overridden_by_parent_trigger {
2326                                        return;
2327                                    }
2328                                }
2329
2330                                this.cancel(&menu::Cancel, window, cx)
2331                            },
2332                        ))
2333                        .when_some(self.end_slot_action.as_ref(), |el, action| {
2334                            el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
2335                        })
2336                        .when(!self.delayed, |mut el| {
2337                            for item in self.items.iter() {
2338                                if let ContextMenuItem::Entry(ContextMenuEntry {
2339                                    action: Some(action),
2340                                    disabled: false,
2341                                    ..
2342                                }) = item
2343                                {
2344                                    el = el.on_boxed_action(
2345                                        &**action,
2346                                        cx.listener(ContextMenu::on_action_dispatch),
2347                                    );
2348                                }
2349                            }
2350                            el
2351                        })
2352                        .child(
2353                            List::new().children(
2354                                self.items
2355                                    .iter()
2356                                    .enumerate()
2357                                    .map(|(ix, item)| self.render_menu_item(ix, item, window, cx)),
2358                            ),
2359                        ),
2360                )
2361        };
2362
2363        if let Some(focus_handle) = focus_submenu.as_ref() {
2364            window.focus(focus_handle, cx);
2365        }
2366
2367        if is_wide_window {
2368            let menu_bounds = self.main_menu_observed_bounds.get();
2369            let trigger_bounds = self
2370                .documentation_aside
2371                .as_ref()
2372                .and_then(|(ix, _)| self.aside_trigger_bounds.borrow().get(ix).copied());
2373
2374            let trigger_position = match (menu_bounds, trigger_bounds) {
2375                (Some(menu_bounds), Some(trigger_bounds)) => {
2376                    let relative_top = trigger_bounds.origin.y - menu_bounds.origin.y;
2377                    let height = trigger_bounds.size.height;
2378                    Some((relative_top, height))
2379                }
2380                _ => None,
2381            };
2382
2383            div()
2384                .relative()
2385                .child(render_menu(cx, window))
2386                // Only render the aside once we have trigger bounds to avoid flicker.
2387                .when_some(trigger_position, |this, (top, height)| {
2388                    this.children(aside.map(|(_, aside)| {
2389                        h_flex()
2390                            .absolute()
2391                            .when(aside.side == DocumentationSide::Left, |el| {
2392                                el.right_full().mr_1()
2393                            })
2394                            .when(aside.side == DocumentationSide::Right, |el| {
2395                                el.left_full().ml_1()
2396                            })
2397                            .top(top)
2398                            .h(height)
2399                            .child(render_aside(aside, cx))
2400                    }))
2401                })
2402                .when_some(
2403                    submenu_container,
2404                    |this, (ix, submenu, offset, flip_left)| {
2405                        this.child(
2406                            self.render_submenu_container(ix, submenu, offset, flip_left, cx),
2407                        )
2408                    },
2409                )
2410        } else {
2411            v_flex()
2412                .w_full()
2413                .relative()
2414                .gap_1()
2415                .justify_end()
2416                .children(aside.map(|(_, aside)| render_aside(aside, cx)))
2417                .child(render_menu(cx, window))
2418                .when_some(
2419                    submenu_container,
2420                    |this, (ix, submenu, offset, flip_left)| {
2421                        this.child(
2422                            self.render_submenu_container(ix, submenu, offset, flip_left, cx),
2423                        )
2424                    },
2425                )
2426        }
2427    }
2428}
2429
2430#[cfg(test)]
2431mod tests {
2432    use gpui::TestAppContext;
2433
2434    use super::*;
2435
2436    #[gpui::test]
2437    fn can_navigate_back_over_headers(cx: &mut TestAppContext) {
2438        let cx = cx.add_empty_window();
2439        let context_menu = cx.update(|window, cx| {
2440            ContextMenu::build(window, cx, |menu, _, _| {
2441                menu.header("First header")
2442                    .separator()
2443                    .entry("First entry", None, |_, _| {})
2444                    .separator()
2445                    .separator()
2446                    .entry("Last entry", None, |_, _| {})
2447                    .header("Last header")
2448            })
2449        });
2450
2451        context_menu.update_in(cx, |context_menu, window, cx| {
2452            assert_eq!(
2453                None, context_menu.selected_index,
2454                "No selection is in the menu initially"
2455            );
2456
2457            context_menu.select_first(&SelectFirst, window, cx);
2458            assert_eq!(
2459                Some(2),
2460                context_menu.selected_index,
2461                "Should select first selectable entry, skipping the header and the separator"
2462            );
2463
2464            context_menu.select_next(&SelectNext, window, cx);
2465            assert_eq!(
2466                Some(5),
2467                context_menu.selected_index,
2468                "Should select next selectable entry, skipping 2 separators along the way"
2469            );
2470
2471            context_menu.select_next(&SelectNext, window, cx);
2472            assert_eq!(
2473                Some(2),
2474                context_menu.selected_index,
2475                "Should wrap around to first selectable entry"
2476            );
2477        });
2478
2479        context_menu.update_in(cx, |context_menu, window, cx| {
2480            assert_eq!(
2481                Some(2),
2482                context_menu.selected_index,
2483                "Should start from the first selectable entry"
2484            );
2485
2486            context_menu.select_previous(&SelectPrevious, window, cx);
2487            assert_eq!(
2488                Some(5),
2489                context_menu.selected_index,
2490                "Should wrap around to previous selectable entry (last)"
2491            );
2492
2493            context_menu.select_previous(&SelectPrevious, window, cx);
2494            assert_eq!(
2495                Some(2),
2496                context_menu.selected_index,
2497                "Should go back to previous selectable entry (first)"
2498            );
2499        });
2500
2501        context_menu.update_in(cx, |context_menu, window, cx| {
2502            context_menu.select_first(&SelectFirst, window, cx);
2503            assert_eq!(
2504                Some(2),
2505                context_menu.selected_index,
2506                "Should start from the first selectable entry"
2507            );
2508
2509            context_menu.select_previous(&SelectPrevious, window, cx);
2510            assert_eq!(
2511                Some(5),
2512                context_menu.selected_index,
2513                "Should wrap around to last selectable entry"
2514            );
2515            context_menu.select_next(&SelectNext, window, cx);
2516            assert_eq!(
2517                Some(2),
2518                context_menu.selected_index,
2519                "Should wrap around to first selectable entry"
2520            );
2521        });
2522    }
2523}
2524
Served at tenant.openagents/omega Member data and write actions are omitted.