context_menu.rs

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