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