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        mut self,
 696        label: impl Into<SharedString>,
 697        action: Box<dyn Action>,
 698        checked: bool,
 699    ) -> Self {
 700        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 701            toggle: if checked {
 702                Some((IconPosition::Start, true))
 703            } else {
 704                None
 705            },
 706            label: label.into(),
 707            action: Some(action.boxed_clone()),
 708            handler: Rc::new(move |context, window, cx| {
 709                if let Some(context) = &context {
 710                    window.focus(context, cx);
 711                }
 712                window.dispatch_action(action.boxed_clone(), cx);
 713            }),
 714            secondary_handler: None,
 715            icon: None,
 716            custom_icon_path: None,
 717            custom_icon_svg: None,
 718            icon_position: IconPosition::End,
 719            icon_size: IconSize::Small,
 720            icon_color: None,
 721            disabled: false,
 722            documentation_aside: None,
 723            end_slot_icon: None,
 724            end_slot_title: None,
 725            end_slot_handler: None,
 726            show_end_slot_on_hover: false,
 727        }));
 728        self
 729    }
 730
 731    pub fn action_disabled_when(
 732        mut self,
 733        disabled: bool,
 734        label: impl Into<SharedString>,
 735        action: Box<dyn Action>,
 736    ) -> Self {
 737        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 738            toggle: None,
 739            label: label.into(),
 740            action: Some(action.boxed_clone()),
 741            handler: Rc::new(move |context, window, cx| {
 742                if let Some(context) = &context {
 743                    window.focus(context, cx);
 744                }
 745                window.dispatch_action(action.boxed_clone(), cx);
 746            }),
 747            secondary_handler: None,
 748            icon: None,
 749            custom_icon_path: None,
 750            custom_icon_svg: None,
 751            icon_size: IconSize::Small,
 752            icon_position: IconPosition::End,
 753            icon_color: None,
 754            disabled,
 755            documentation_aside: None,
 756            end_slot_icon: None,
 757            end_slot_title: None,
 758            end_slot_handler: None,
 759            show_end_slot_on_hover: false,
 760        }));
 761        self
 762    }
 763
 764    pub fn link(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
 765        self.link_with_handler(label, action, |_, _| {})
 766    }
 767
 768    pub fn link_with_handler(
 769        mut self,
 770        label: impl Into<SharedString>,
 771        action: Box<dyn Action>,
 772        handler: impl Fn(&mut Window, &mut App) + 'static,
 773    ) -> Self {
 774        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 775            toggle: None,
 776            label: label.into(),
 777            action: Some(action.boxed_clone()),
 778            handler: Rc::new(move |_, window, cx| {
 779                handler(window, cx);
 780                window.dispatch_action(action.boxed_clone(), cx);
 781            }),
 782            secondary_handler: None,
 783            icon: Some(IconName::ArrowUpRight),
 784            custom_icon_path: None,
 785            custom_icon_svg: None,
 786            icon_size: IconSize::XSmall,
 787            icon_position: IconPosition::End,
 788            icon_color: None,
 789            disabled: false,
 790            documentation_aside: None,
 791            end_slot_icon: None,
 792            end_slot_title: None,
 793            end_slot_handler: None,
 794            show_end_slot_on_hover: false,
 795        }));
 796        self
 797    }
 798
 799    pub fn submenu(
 800        mut self,
 801        label: impl Into<SharedString>,
 802        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
 803    ) -> Self {
 804        self.items.push(ContextMenuItem::Submenu {
 805            label: label.into(),
 806            icon: None,
 807            icon_color: None,
 808            builder: Rc::new(builder),
 809        });
 810        self
 811    }
 812
 813    pub fn submenu_with_icon(
 814        mut self,
 815        label: impl Into<SharedString>,
 816        icon: IconName,
 817        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
 818    ) -> Self {
 819        self.items.push(ContextMenuItem::Submenu {
 820            label: label.into(),
 821            icon: Some(icon),
 822            icon_color: None,
 823            builder: Rc::new(builder),
 824        });
 825        self
 826    }
 827
 828    pub fn submenu_with_colored_icon(
 829        mut self,
 830        label: impl Into<SharedString>,
 831        icon: IconName,
 832        icon_color: Color,
 833        builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
 834    ) -> Self {
 835        self.items.push(ContextMenuItem::Submenu {
 836            label: label.into(),
 837            icon: Some(icon),
 838            icon_color: Some(icon_color),
 839            builder: Rc::new(builder),
 840        });
 841        self
 842    }
 843
 844    pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
 845        self.keep_open_on_confirm = keep_open;
 846        self
 847    }
 848
 849    pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 850        let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
 851            return;
 852        };
 853        let ContextMenuItem::Entry(entry) = entry else {
 854            return;
 855        };
 856        let Some(handler) = entry.end_slot_handler.as_ref() else {
 857            return;
 858        };
 859        handler(None, window, cx);
 860    }
 861
 862    pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
 863        self.fixed_width = Some(width);
 864        self
 865    }
 866
 867    pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
 868        self.end_slot_action = Some(action);
 869        self
 870    }
 871
 872    pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
 873        self.key_context = context.into();
 874        self
 875    }
 876
 877    pub fn selected_index(&self) -> Option<usize> {
 878        self.selected_index
 879    }
 880
 881    pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 882        let Some(ix) = self.selected_index else {
 883            return;
 884        };
 885
 886        if let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) {
 887            self.open_submenu(
 888                ix,
 889                builder.clone(),
 890                SubmenuOpenTrigger::Keyboard,
 891                window,
 892                cx,
 893            );
 894
 895            if let SubmenuState::Open(open_submenu) = &self.submenu_state {
 896                let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
 897                window.focus(&focus_handle, cx);
 898                open_submenu.entity.update(cx, |submenu, cx| {
 899                    submenu.select_first(&SelectFirst, window, cx);
 900                });
 901            }
 902
 903            cx.notify();
 904            return;
 905        }
 906
 907        let context = self.action_context.as_ref();
 908
 909        if let Some(
 910            ContextMenuItem::Entry(ContextMenuEntry {
 911                handler,
 912                disabled: false,
 913                ..
 914            })
 915            | ContextMenuItem::CustomEntry { handler, .. },
 916        ) = self.items.get(ix)
 917        {
 918            (handler)(context, window, cx)
 919        }
 920
 921        if self.main_menu.is_some() && !self.keep_open_on_confirm {
 922            self.clicked = true;
 923        }
 924
 925        if self.keep_open_on_confirm {
 926            self.rebuild(window, cx);
 927        } else {
 928            cx.emit(DismissEvent);
 929        }
 930    }
 931
 932    pub fn secondary_confirm(
 933        &mut self,
 934        _: &menu::SecondaryConfirm,
 935        window: &mut Window,
 936        cx: &mut Context<Self>,
 937    ) {
 938        let Some(ix) = self.selected_index else {
 939            return;
 940        };
 941
 942        if let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) {
 943            self.open_submenu(
 944                ix,
 945                builder.clone(),
 946                SubmenuOpenTrigger::Keyboard,
 947                window,
 948                cx,
 949            );
 950
 951            if let SubmenuState::Open(open_submenu) = &self.submenu_state {
 952                let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
 953                window.focus(&focus_handle, cx);
 954                open_submenu.entity.update(cx, |submenu, cx| {
 955                    submenu.select_first(&SelectFirst, window, cx);
 956                });
 957            }
 958
 959            cx.notify();
 960            return;
 961        }
 962
 963        let context = self.action_context.as_ref();
 964
 965        if let Some(ContextMenuItem::Entry(ContextMenuEntry {
 966            handler,
 967            secondary_handler,
 968            disabled: false,
 969            ..
 970        })) = self.items.get(ix)
 971        {
 972            if let Some(secondary) = secondary_handler {
 973                (secondary)(context, window, cx)
 974            } else {
 975                (handler)(context, window, cx)
 976            }
 977        } else if let Some(ContextMenuItem::CustomEntry { handler, .. }) = self.items.get(ix) {
 978            (handler)(context, window, cx)
 979        }
 980
 981        if self.main_menu.is_some() && !self.keep_open_on_confirm {
 982            self.clicked = true;
 983        }
 984
 985        if self.keep_open_on_confirm {
 986            self.rebuild(window, cx);
 987        } else {
 988            cx.emit(DismissEvent);
 989        }
 990    }
 991
 992    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 993        if self.main_menu.is_some() {
 994            cx.emit(DismissEvent);
 995
 996            // Restore keyboard focus to the parent menu so arrow keys / Escape / Enter work again.
 997            if let Some(parent) = &self.main_menu {
 998                let parent_focus = parent.read(cx).focus_handle.clone();
 999
1000                parent.update(cx, |parent, _cx| {
1001                    parent.ignore_blur_until = Some(Instant::now() + Duration::from_millis(200));
1002                });
1003
1004                window.focus(&parent_focus, cx);
1005            }
1006
1007            return;
1008        }
1009
1010        cx.emit(DismissEvent);
1011    }
1012
1013    pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
1014        let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
1015            return;
1016        };
1017        let ContextMenuItem::Entry(entry) = item else {
1018            return;
1019        };
1020        let Some(handler) = entry.end_slot_handler.as_ref() else {
1021            return;
1022        };
1023        handler(None, window, cx);
1024        self.rebuild(window, cx);
1025        cx.notify();
1026    }
1027
1028    pub fn clear_selected(&mut self) {
1029        self.selected_index = None;
1030    }
1031
1032    pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1033        if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
1034            self.select_index(ix, window, cx);
1035        }
1036        cx.notify();
1037    }
1038
1039    pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
1040        for (ix, item) in self.items.iter().enumerate().rev() {
1041            if item.is_selectable() {
1042                return self.select_index(ix, window, cx);
1043            }
1044        }
1045        None
1046    }
1047
1048    fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1049        if self.select_last(window, cx).is_some() {
1050            cx.notify();
1051        }
1052    }
1053
1054    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1055        if let Some(ix) = self.selected_index {
1056            let next_index = ix + 1;
1057            if self.items.len() <= next_index {
1058                self.select_first(&SelectFirst, window, cx);
1059                return;
1060            } else {
1061                for (ix, item) in self.items.iter().enumerate().skip(next_index) {
1062                    if item.is_selectable() {
1063                        self.select_index(ix, window, cx);
1064                        cx.notify();
1065                        return;
1066                    }
1067                }
1068            }
1069        }
1070        self.select_first(&SelectFirst, window, cx);
1071    }
1072
1073    pub fn select_previous(
1074        &mut self,
1075        _: &SelectPrevious,
1076        window: &mut Window,
1077        cx: &mut Context<Self>,
1078    ) {
1079        if let Some(ix) = self.selected_index {
1080            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
1081                if item.is_selectable() {
1082                    self.select_index(ix, window, cx);
1083                    cx.notify();
1084                    return;
1085                }
1086            }
1087        }
1088        self.handle_select_last(&SelectLast, window, cx);
1089    }
1090
1091    pub fn select_submenu_child(
1092        &mut self,
1093        _: &SelectChild,
1094        window: &mut Window,
1095        cx: &mut Context<Self>,
1096    ) {
1097        let Some(ix) = self.selected_index else {
1098            return;
1099        };
1100
1101        let Some(ContextMenuItem::Submenu { builder, .. }) = self.items.get(ix) else {
1102            return;
1103        };
1104
1105        self.open_submenu(
1106            ix,
1107            builder.clone(),
1108            SubmenuOpenTrigger::Keyboard,
1109            window,
1110            cx,
1111        );
1112
1113        if let SubmenuState::Open(open_submenu) = &self.submenu_state {
1114            let focus_handle = open_submenu.entity.read(cx).focus_handle.clone();
1115            window.focus(&focus_handle, cx);
1116            open_submenu.entity.update(cx, |submenu, cx| {
1117                submenu.select_first(&SelectFirst, window, cx);
1118            });
1119        }
1120
1121        cx.notify();
1122    }
1123
1124    pub fn select_submenu_parent(
1125        &mut self,
1126        _: &SelectParent,
1127        window: &mut Window,
1128        cx: &mut Context<Self>,
1129    ) {
1130        if self.main_menu.is_none() {
1131            return;
1132        }
1133
1134        if let Some(parent) = &self.main_menu {
1135            let parent_clone = parent.clone();
1136
1137            let parent_focus = parent.read(cx).focus_handle.clone();
1138            window.focus(&parent_focus, cx);
1139
1140            cx.emit(DismissEvent);
1141
1142            parent_clone.update(cx, |parent, cx| {
1143                if let SubmenuState::Open(open_submenu) = &parent.submenu_state {
1144                    let trigger_index = open_submenu.item_index;
1145                    parent.close_submenu(false, cx);
1146                    let _ = parent.select_index(trigger_index, window, cx);
1147                    cx.notify();
1148                }
1149            });
1150
1151            return;
1152        }
1153
1154        cx.emit(DismissEvent);
1155    }
1156
1157    fn select_index(
1158        &mut self,
1159        ix: usize,
1160        _window: &mut Window,
1161        _cx: &mut Context<Self>,
1162    ) -> Option<usize> {
1163        self.documentation_aside = None;
1164        let item = self.items.get(ix)?;
1165        if item.is_selectable() {
1166            self.selected_index = Some(ix);
1167            match item {
1168                ContextMenuItem::Entry(entry) => {
1169                    if let Some(callback) = &entry.documentation_aside {
1170                        self.documentation_aside = Some((ix, callback.clone()));
1171                    }
1172                }
1173                ContextMenuItem::CustomEntry {
1174                    documentation_aside: Some(callback),
1175                    ..
1176                } => {
1177                    self.documentation_aside = Some((ix, callback.clone()));
1178                }
1179                ContextMenuItem::Submenu { .. } => {}
1180                _ => (),
1181            }
1182        }
1183        Some(ix)
1184    }
1185
1186    fn create_submenu(
1187        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1188        parent_entity: Entity<ContextMenu>,
1189        window: &mut Window,
1190        cx: &mut Context<Self>,
1191    ) -> (Entity<ContextMenu>, Subscription) {
1192        let submenu = Self::build_submenu(builder, parent_entity, window, cx);
1193
1194        let dismiss_subscription = cx.subscribe(&submenu, |this, submenu, _: &DismissEvent, cx| {
1195            let should_dismiss_parent = submenu.read(cx).clicked;
1196
1197            this.close_submenu(false, cx);
1198
1199            if should_dismiss_parent {
1200                cx.emit(DismissEvent);
1201            }
1202        });
1203
1204        (submenu, dismiss_subscription)
1205    }
1206
1207    fn build_submenu(
1208        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1209        parent_entity: Entity<ContextMenu>,
1210        window: &mut Window,
1211        cx: &mut App,
1212    ) -> Entity<ContextMenu> {
1213        cx.new(|cx| {
1214            let focus_handle = cx.focus_handle();
1215
1216            let _on_blur_subscription = cx.on_blur(
1217                &focus_handle,
1218                window,
1219                |_this: &mut ContextMenu, _window, _cx| {},
1220            );
1221
1222            let mut menu = ContextMenu {
1223                builder: None,
1224                items: Default::default(),
1225                focus_handle,
1226                action_context: None,
1227                selected_index: None,
1228                delayed: false,
1229                clicked: false,
1230                end_slot_action: None,
1231                key_context: "menu".into(),
1232                _on_blur_subscription,
1233                keep_open_on_confirm: false,
1234                fixed_width: None,
1235                documentation_aside: None,
1236                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
1237                main_menu: Some(parent_entity),
1238                main_menu_observed_bounds: Rc::new(Cell::new(None)),
1239                submenu_state: SubmenuState::Closed,
1240                hover_target: HoverTarget::MainMenu,
1241                submenu_safety_threshold_x: None,
1242                submenu_trigger_bounds: Rc::new(Cell::new(None)),
1243                submenu_trigger_mouse_down: false,
1244                ignore_blur_until: None,
1245            };
1246
1247            menu = (builder)(menu, window, cx);
1248            menu
1249        })
1250    }
1251
1252    fn close_submenu(&mut self, clear_selection: bool, cx: &mut Context<Self>) {
1253        self.submenu_state = SubmenuState::Closed;
1254        self.hover_target = HoverTarget::MainMenu;
1255        self.submenu_safety_threshold_x = None;
1256        self.main_menu_observed_bounds.set(None);
1257        self.submenu_trigger_bounds.set(None);
1258
1259        if clear_selection {
1260            self.selected_index = None;
1261        }
1262
1263        cx.notify();
1264    }
1265
1266    fn open_submenu(
1267        &mut self,
1268        item_index: usize,
1269        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
1270        reason: SubmenuOpenTrigger,
1271        window: &mut Window,
1272        cx: &mut Context<Self>,
1273    ) {
1274        // If the submenu is already open for this item, don't recreate it.
1275        if matches!(
1276            &self.submenu_state,
1277            SubmenuState::Open(open_submenu) if open_submenu.item_index == item_index
1278        ) {
1279            return;
1280        }
1281
1282        let (submenu, dismiss_subscription) =
1283            Self::create_submenu(builder, cx.entity(), window, cx);
1284
1285        // If we're switching from one submenu item to another, throw away any previously-captured
1286        // offset so we don't reuse a stale position.
1287        self.main_menu_observed_bounds.set(None);
1288        self.submenu_trigger_bounds.set(None);
1289
1290        self.submenu_safety_threshold_x = None;
1291        self.hover_target = HoverTarget::MainMenu;
1292
1293        // When opening a submenu via keyboard, there is a brief moment where focus/hover can
1294        // transition in a way that triggers the parent menu's `on_blur` dismissal.
1295        if matches!(reason, SubmenuOpenTrigger::Keyboard) {
1296            self.ignore_blur_until = Some(Instant::now() + Duration::from_millis(150));
1297        }
1298
1299        let trigger_bounds = self.submenu_trigger_bounds.get();
1300
1301        self.submenu_state = SubmenuState::Open(OpenSubmenu {
1302            item_index,
1303            entity: submenu,
1304            trigger_bounds,
1305            offset: None,
1306            _dismiss_subscription: dismiss_subscription,
1307        });
1308
1309        cx.notify();
1310    }
1311
1312    pub fn on_action_dispatch(
1313        &mut self,
1314        dispatched: &dyn Action,
1315        window: &mut Window,
1316        cx: &mut Context<Self>,
1317    ) {
1318        if self.clicked {
1319            cx.propagate();
1320            return;
1321        }
1322
1323        if let Some(ix) = self.items.iter().position(|item| {
1324            if let ContextMenuItem::Entry(ContextMenuEntry {
1325                action: Some(action),
1326                disabled: false,
1327                ..
1328            }) = item
1329            {
1330                action.partial_eq(dispatched)
1331            } else {
1332                false
1333            }
1334        }) {
1335            self.select_index(ix, window, cx);
1336            self.delayed = true;
1337            cx.notify();
1338            let action = dispatched.boxed_clone();
1339            cx.spawn_in(window, async move |this, cx| {
1340                cx.background_executor()
1341                    .timer(Duration::from_millis(50))
1342                    .await;
1343                cx.update(|window, cx| {
1344                    this.update(cx, |this, cx| {
1345                        this.cancel(&menu::Cancel, window, cx);
1346                        window.dispatch_action(action, cx);
1347                    })
1348                })
1349            })
1350            .detach_and_log_err(cx);
1351        } else {
1352            cx.propagate()
1353        }
1354    }
1355
1356    pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
1357        self._on_blur_subscription = new_subscription;
1358        self
1359    }
1360
1361    fn render_menu_item(
1362        &self,
1363        ix: usize,
1364        item: &ContextMenuItem,
1365        window: &mut Window,
1366        cx: &mut Context<Self>,
1367    ) -> impl IntoElement + use<> {
1368        match item {
1369            ContextMenuItem::Separator => ListSeparator.into_any_element(),
1370            ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
1371                .inset(true)
1372                .into_any_element(),
1373            ContextMenuItem::HeaderWithLink(header, label, url) => {
1374                let url = url.clone();
1375                let link_id = ElementId::Name(format!("link-{}", url).into());
1376                ListSubHeader::new(header.clone())
1377                    .inset(true)
1378                    .end_slot(
1379                        Button::new(link_id, label.clone())
1380                            .color(Color::Muted)
1381                            .label_size(LabelSize::Small)
1382                            .size(ButtonSize::None)
1383                            .style(ButtonStyle::Transparent)
1384                            .on_click(move |_, _, cx| {
1385                                let url = url.clone();
1386                                cx.open_url(&url);
1387                            })
1388                            .into_any_element(),
1389                    )
1390                    .into_any_element()
1391            }
1392            ContextMenuItem::Label(label) => ListItem::new(ix)
1393                .inset(true)
1394                .disabled(true)
1395                .child(Label::new(label.clone()))
1396                .into_any_element(),
1397            ContextMenuItem::Entry(entry) => {
1398                self.render_menu_entry(ix, entry, cx).into_any_element()
1399            }
1400            ContextMenuItem::CustomEntry {
1401                entry_render,
1402                handler,
1403                selectable,
1404                documentation_aside,
1405                ..
1406            } => {
1407                let handler = handler.clone();
1408                let menu = cx.entity().downgrade();
1409                let selectable = *selectable;
1410                let aside_trigger_bounds = self.aside_trigger_bounds.clone();
1411
1412                div()
1413                    .id(("context-menu-child", ix))
1414                    .when_some(documentation_aside.clone(), |this, documentation_aside| {
1415                        this.occlude()
1416                            .on_hover(cx.listener(move |menu, hovered, _, cx| {
1417                            if *hovered {
1418                                menu.documentation_aside = Some((ix, documentation_aside.clone()));
1419                            } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
1420                            {
1421                                menu.documentation_aside = None;
1422                            }
1423                            cx.notify();
1424                        }))
1425                    })
1426                    .when(documentation_aside.is_some(), |this| {
1427                        this.child(
1428                            canvas(
1429                                {
1430                                    let aside_trigger_bounds = aside_trigger_bounds.clone();
1431                                    move |bounds, _window, _cx| {
1432                                        aside_trigger_bounds.borrow_mut().insert(ix, bounds);
1433                                    }
1434                                },
1435                                |_bounds, _state, _window, _cx| {},
1436                            )
1437                            .size_full()
1438                            .absolute()
1439                            .top_0()
1440                            .left_0(),
1441                        )
1442                    })
1443                    .child(
1444                        ListItem::new(ix)
1445                            .inset(true)
1446                            .toggle_state(Some(ix) == self.selected_index)
1447                            .selectable(selectable)
1448                            .when(selectable, |item| {
1449                                item.on_click({
1450                                    let context = self.action_context.clone();
1451                                    let keep_open_on_confirm = self.keep_open_on_confirm;
1452                                    move |_, window, cx| {
1453                                        handler(context.as_ref(), window, cx);
1454                                        menu.update(cx, |menu, cx| {
1455                                            menu.clicked = true;
1456
1457                                            if keep_open_on_confirm {
1458                                                menu.rebuild(window, cx);
1459                                            } else {
1460                                                cx.emit(DismissEvent);
1461                                            }
1462                                        })
1463                                        .ok();
1464                                    }
1465                                })
1466                            })
1467                            .child(entry_render(window, cx)),
1468                    )
1469                    .into_any_element()
1470            }
1471            ContextMenuItem::Submenu {
1472                label,
1473                icon,
1474                icon_color,
1475                ..
1476            } => self
1477                .render_submenu_item_trigger(ix, label.clone(), *icon, *icon_color, cx)
1478                .into_any_element(),
1479        }
1480    }
1481
1482    fn render_submenu_item_trigger(
1483        &self,
1484        ix: usize,
1485        label: SharedString,
1486        icon: Option<IconName>,
1487        icon_color: Option<Color>,
1488        cx: &mut Context<Self>,
1489    ) -> impl IntoElement {
1490        let toggle_state = Some(ix) == self.selected_index
1491            || matches!(
1492                &self.submenu_state,
1493                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1494            );
1495
1496        div()
1497            .id(("context-menu-submenu-trigger", ix))
1498            .capture_any_mouse_down(cx.listener(move |this, event: &MouseDownEvent, _, _| {
1499                // This prevents on_hover(false) from closing the submenu during a click.
1500                if event.button == MouseButton::Left {
1501                    this.submenu_trigger_mouse_down = true;
1502                }
1503            }))
1504            .capture_any_mouse_up(cx.listener(move |this, event: &MouseUpEvent, _, _| {
1505                if event.button == MouseButton::Left {
1506                    this.submenu_trigger_mouse_down = false;
1507                }
1508            }))
1509            .on_mouse_move(cx.listener(move |this, event: &MouseMoveEvent, _, cx| {
1510                if matches!(&this.submenu_state, SubmenuState::Open(_))
1511                    || this.selected_index == Some(ix)
1512                {
1513                    this.submenu_safety_threshold_x = Some(event.position.x - px(100.0));
1514                }
1515
1516                cx.notify();
1517            }))
1518            .child(
1519                ListItem::new(ix)
1520                    .inset(true)
1521                    .toggle_state(toggle_state)
1522                    .child(
1523                        canvas(
1524                            {
1525                                let trigger_bounds_cell = self.submenu_trigger_bounds.clone();
1526                                move |bounds, _window, _cx| {
1527                                    if toggle_state {
1528                                        trigger_bounds_cell.set(Some(bounds));
1529                                    }
1530                                }
1531                            },
1532                            |_bounds, _state, _window, _cx| {},
1533                        )
1534                        .size_full()
1535                        .absolute()
1536                        .top_0()
1537                        .left_0(),
1538                    )
1539                    .on_hover(cx.listener(move |this, hovered, window, cx| {
1540                        let mouse_pos = window.mouse_position();
1541
1542                        if *hovered {
1543                            this.clear_selected();
1544                            window.focus(&this.focus_handle.clone(), cx);
1545                            this.hover_target = HoverTarget::MainMenu;
1546                            this.submenu_safety_threshold_x = Some(mouse_pos.x - px(50.0));
1547
1548                            if let Some(ContextMenuItem::Submenu { builder, .. }) =
1549                                this.items.get(ix)
1550                            {
1551                                this.open_submenu(
1552                                    ix,
1553                                    builder.clone(),
1554                                    SubmenuOpenTrigger::Pointer,
1555                                    window,
1556                                    cx,
1557                                );
1558                            }
1559
1560                            cx.notify();
1561                        } else {
1562                            if this.submenu_trigger_mouse_down {
1563                                return;
1564                            }
1565
1566                            let is_open_for_this_item = matches!(
1567                                &this.submenu_state,
1568                                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1569                            );
1570
1571                            let mouse_in_submenu_zone = this
1572                                .padded_submenu_bounds()
1573                                .is_some_and(|bounds| bounds.contains(&window.mouse_position()));
1574
1575                            if is_open_for_this_item
1576                                && this.hover_target != HoverTarget::Submenu
1577                                && !mouse_in_submenu_zone
1578                            {
1579                                this.close_submenu(false, cx);
1580                                this.clear_selected();
1581                                window.focus(&this.focus_handle.clone(), cx);
1582                                cx.notify();
1583                            }
1584                        }
1585                    }))
1586                    .on_click(cx.listener(move |this, _, window, cx| {
1587                        if matches!(
1588                            &this.submenu_state,
1589                            SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1590                        ) {
1591                            return;
1592                        }
1593
1594                        if let Some(ContextMenuItem::Submenu { builder, .. }) = this.items.get(ix) {
1595                            this.open_submenu(
1596                                ix,
1597                                builder.clone(),
1598                                SubmenuOpenTrigger::Pointer,
1599                                window,
1600                                cx,
1601                            );
1602                        }
1603                    }))
1604                    .child(
1605                        h_flex()
1606                            .w_full()
1607                            .gap_2()
1608                            .justify_between()
1609                            .child(
1610                                h_flex()
1611                                    .gap_1p5()
1612                                    .when_some(icon, |this, icon_name| {
1613                                        this.child(
1614                                            Icon::new(icon_name)
1615                                                .size(IconSize::Small)
1616                                                .color(icon_color.unwrap_or(Color::Muted)),
1617                                        )
1618                                    })
1619                                    .child(Label::new(label).color(Color::Default)),
1620                            )
1621                            .child(
1622                                Icon::new(IconName::ChevronRight)
1623                                    .size(IconSize::Small)
1624                                    .color(Color::Muted),
1625                            ),
1626                    ),
1627            )
1628    }
1629
1630    fn padded_submenu_bounds(&self) -> Option<Bounds<Pixels>> {
1631        let bounds = self.main_menu_observed_bounds.get()?;
1632        Some(Bounds {
1633            origin: Point {
1634                x: bounds.origin.x - px(50.0),
1635                y: bounds.origin.y - px(50.0),
1636            },
1637            size: Size {
1638                width: bounds.size.width + px(100.0),
1639                height: bounds.size.height + px(100.0),
1640            },
1641        })
1642    }
1643
1644    fn render_submenu_container(
1645        &self,
1646        ix: usize,
1647        submenu: Entity<ContextMenu>,
1648        offset: Pixels,
1649        cx: &mut Context<Self>,
1650    ) -> impl IntoElement {
1651        let bounds_cell = self.main_menu_observed_bounds.clone();
1652        let canvas = canvas(
1653            {
1654                move |bounds, _window, _cx| {
1655                    bounds_cell.set(Some(bounds));
1656                }
1657            },
1658            |_bounds, _state, _window, _cx| {},
1659        )
1660        .size_full()
1661        .absolute()
1662        .top_0()
1663        .left_0();
1664
1665        div()
1666            .id(("submenu-container", ix))
1667            .absolute()
1668            .left_full()
1669            .ml_neg_0p5()
1670            .top(offset)
1671            .on_hover(cx.listener(|this, hovered, _, _| {
1672                if *hovered {
1673                    this.hover_target = HoverTarget::Submenu;
1674                }
1675            }))
1676            .child(
1677                anchored()
1678                    .anchor(Corner::TopLeft)
1679                    .snap_to_window_with_margin(px(8.0))
1680                    .child(
1681                        div()
1682                            .id(("submenu-hover-zone", ix))
1683                            .occlude()
1684                            .child(canvas)
1685                            .child(submenu),
1686                    ),
1687            )
1688    }
1689
1690    fn render_menu_entry(
1691        &self,
1692        ix: usize,
1693        entry: &ContextMenuEntry,
1694        cx: &mut Context<Self>,
1695    ) -> impl IntoElement {
1696        let ContextMenuEntry {
1697            toggle,
1698            label,
1699            handler,
1700            icon,
1701            custom_icon_path,
1702            custom_icon_svg,
1703            icon_position,
1704            icon_size,
1705            icon_color,
1706            action,
1707            disabled,
1708            documentation_aside,
1709            end_slot_icon,
1710            end_slot_title,
1711            end_slot_handler,
1712            show_end_slot_on_hover,
1713            secondary_handler: _,
1714        } = entry;
1715        let this = cx.weak_entity();
1716
1717        let handler = handler.clone();
1718        let menu = cx.entity().downgrade();
1719
1720        let icon_color = if *disabled {
1721            Color::Muted
1722        } else if toggle.is_some() {
1723            icon_color.unwrap_or(Color::Accent)
1724        } else {
1725            icon_color.unwrap_or(Color::Default)
1726        };
1727
1728        let label_color = if *disabled {
1729            Color::Disabled
1730        } else {
1731            Color::Default
1732        };
1733
1734        let label_element = if let Some(custom_path) = custom_icon_path {
1735            h_flex()
1736                .gap_1p5()
1737                .when(
1738                    *icon_position == IconPosition::Start && toggle.is_none(),
1739                    |flex| {
1740                        flex.child(
1741                            Icon::from_path(custom_path.clone())
1742                                .size(*icon_size)
1743                                .color(icon_color),
1744                        )
1745                    },
1746                )
1747                .child(Label::new(label.clone()).color(label_color).truncate())
1748                .when(*icon_position == IconPosition::End, |flex| {
1749                    flex.child(
1750                        Icon::from_path(custom_path.clone())
1751                            .size(*icon_size)
1752                            .color(icon_color),
1753                    )
1754                })
1755                .into_any_element()
1756        } else if let Some(custom_icon_svg) = custom_icon_svg {
1757            h_flex()
1758                .gap_1p5()
1759                .when(
1760                    *icon_position == IconPosition::Start && toggle.is_none(),
1761                    |flex| {
1762                        flex.child(
1763                            Icon::from_external_svg(custom_icon_svg.clone())
1764                                .size(*icon_size)
1765                                .color(icon_color),
1766                        )
1767                    },
1768                )
1769                .child(Label::new(label.clone()).color(label_color).truncate())
1770                .when(*icon_position == IconPosition::End, |flex| {
1771                    flex.child(
1772                        Icon::from_external_svg(custom_icon_svg.clone())
1773                            .size(*icon_size)
1774                            .color(icon_color),
1775                    )
1776                })
1777                .into_any_element()
1778        } else if let Some(icon_name) = icon {
1779            h_flex()
1780                .gap_1p5()
1781                .when(
1782                    *icon_position == IconPosition::Start && toggle.is_none(),
1783                    |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
1784                )
1785                .child(Label::new(label.clone()).color(label_color).truncate())
1786                .when(*icon_position == IconPosition::End, |flex| {
1787                    flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
1788                })
1789                .into_any_element()
1790        } else {
1791            Label::new(label.clone())
1792                .color(label_color)
1793                .truncate()
1794                .into_any_element()
1795        };
1796
1797        let aside_trigger_bounds = self.aside_trigger_bounds.clone();
1798
1799        div()
1800            .id(("context-menu-child", ix))
1801            .when_some(documentation_aside.clone(), |this, documentation_aside| {
1802                this.occlude()
1803                    .on_hover(cx.listener(move |menu, hovered, _, cx| {
1804                        if *hovered {
1805                            menu.documentation_aside = Some((ix, documentation_aside.clone()));
1806                        } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
1807                            menu.documentation_aside = None;
1808                        }
1809                        cx.notify();
1810                    }))
1811            })
1812            .when(documentation_aside.is_some(), |this| {
1813                this.child(
1814                    canvas(
1815                        {
1816                            let aside_trigger_bounds = aside_trigger_bounds.clone();
1817                            move |bounds, _window, _cx| {
1818                                aside_trigger_bounds.borrow_mut().insert(ix, bounds);
1819                            }
1820                        },
1821                        |_bounds, _state, _window, _cx| {},
1822                    )
1823                    .size_full()
1824                    .absolute()
1825                    .top_0()
1826                    .left_0(),
1827                )
1828            })
1829            .child(
1830                ListItem::new(ix)
1831                    .group_name("label_container")
1832                    .inset(true)
1833                    .disabled(*disabled)
1834                    .toggle_state(Some(ix) == self.selected_index)
1835                    .when(self.main_menu.is_none() && !*disabled, |item| {
1836                        item.on_hover(cx.listener(move |this, hovered, window, cx| {
1837                            if *hovered {
1838                                this.clear_selected();
1839                                window.focus(&this.focus_handle.clone(), cx);
1840
1841                                if let SubmenuState::Open(open_submenu) = &this.submenu_state {
1842                                    if open_submenu.item_index != ix {
1843                                        this.close_submenu(false, cx);
1844                                        cx.notify();
1845                                    }
1846                                }
1847                            }
1848                        }))
1849                    })
1850                    .when(self.main_menu.is_some(), |item| {
1851                        item.on_click(cx.listener(move |this, _, window, cx| {
1852                            if matches!(
1853                                &this.submenu_state,
1854                                SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
1855                            ) {
1856                                return;
1857                            }
1858
1859                            if let Some(ContextMenuItem::Submenu { builder, .. }) =
1860                                this.items.get(ix)
1861                            {
1862                                this.open_submenu(
1863                                    ix,
1864                                    builder.clone(),
1865                                    SubmenuOpenTrigger::Pointer,
1866                                    window,
1867                                    cx,
1868                                );
1869                            }
1870                        }))
1871                        .on_hover(cx.listener(
1872                            move |this, hovered, window, cx| {
1873                                if *hovered {
1874                                    this.clear_selected();
1875                                    cx.notify();
1876                                }
1877
1878                                if let Some(parent) = &this.main_menu {
1879                                    let mouse_pos = window.mouse_position();
1880                                    let parent_clone = parent.clone();
1881
1882                                    if *hovered {
1883                                        parent.update(cx, |parent, _| {
1884                                            parent.clear_selected();
1885                                            parent.hover_target = HoverTarget::Submenu;
1886                                        });
1887                                    } else {
1888                                        parent_clone.update(cx, |parent, cx| {
1889                                            if matches!(
1890                                                &parent.submenu_state,
1891                                                SubmenuState::Open(_)
1892                                            ) {
1893                                                // Only close if mouse is to the left of the safety threshold
1894                                                // (prevents accidental close when moving diagonally toward submenu)
1895                                                let should_close = parent
1896                                                    .submenu_safety_threshold_x
1897                                                    .map(|threshold_x| mouse_pos.x < threshold_x)
1898                                                    .unwrap_or(true);
1899
1900                                                if should_close {
1901                                                    parent.close_submenu(true, cx);
1902                                                }
1903                                            }
1904                                        });
1905                                    }
1906                                }
1907                            },
1908                        ))
1909                    })
1910                    .when_some(*toggle, |list_item, (position, toggled)| {
1911                        let contents = div()
1912                            .flex_none()
1913                            .child(
1914                                Icon::new(icon.unwrap_or(IconName::Check))
1915                                    .color(icon_color)
1916                                    .size(*icon_size),
1917                            )
1918                            .when(!toggled, |contents| contents.invisible());
1919
1920                        match position {
1921                            IconPosition::Start => list_item.start_slot(contents),
1922                            IconPosition::End => list_item.end_slot(contents),
1923                        }
1924                    })
1925                    .child(
1926                        h_flex()
1927                            .w_full()
1928                            .justify_between()
1929                            .child(label_element)
1930                            .debug_selector(|| format!("MENU_ITEM-{}", label))
1931                            .children(action.as_ref().map(|action| {
1932                                let binding = self
1933                                    .action_context
1934                                    .as_ref()
1935                                    .map(|focus| KeyBinding::for_action_in(&**action, focus, cx))
1936                                    .unwrap_or_else(|| KeyBinding::for_action(&**action, cx));
1937
1938                                div()
1939                                    .ml_4()
1940                                    .child(binding.disabled(*disabled))
1941                                    .when(*disabled && documentation_aside.is_some(), |parent| {
1942                                        parent.invisible()
1943                                    })
1944                            }))
1945                            .when(*disabled && documentation_aside.is_some(), |parent| {
1946                                parent.child(
1947                                    Icon::new(IconName::Info)
1948                                        .size(IconSize::XSmall)
1949                                        .color(Color::Muted),
1950                                )
1951                            }),
1952                    )
1953                    .when_some(
1954                        end_slot_icon
1955                            .as_ref()
1956                            .zip(self.end_slot_action.as_ref())
1957                            .zip(end_slot_title.as_ref())
1958                            .zip(end_slot_handler.as_ref()),
1959                        |el, (((icon, action), title), handler)| {
1960                            el.end_slot({
1961                                let icon_button = IconButton::new("end-slot-icon", *icon)
1962                                    .shape(IconButtonShape::Square)
1963                                    .tooltip({
1964                                        let action_context = self.action_context.clone();
1965                                        let title = title.clone();
1966                                        let action = action.boxed_clone();
1967                                        move |_window, cx| {
1968                                            action_context
1969                                                .as_ref()
1970                                                .map(|focus| {
1971                                                    Tooltip::for_action_in(
1972                                                        title.clone(),
1973                                                        &*action,
1974                                                        focus,
1975                                                        cx,
1976                                                    )
1977                                                })
1978                                                .unwrap_or_else(|| {
1979                                                    Tooltip::for_action(title.clone(), &*action, cx)
1980                                                })
1981                                        }
1982                                    })
1983                                    .on_click({
1984                                        let handler = handler.clone();
1985                                        move |_, window, cx| {
1986                                            handler(None, window, cx);
1987                                            this.update(cx, |this, cx| {
1988                                                this.rebuild(window, cx);
1989                                                cx.notify();
1990                                            })
1991                                            .ok();
1992                                        }
1993                                    });
1994
1995                                if *show_end_slot_on_hover {
1996                                    div()
1997                                        .visible_on_hover("label_container")
1998                                        .child(icon_button)
1999                                        .into_any_element()
2000                                } else {
2001                                    icon_button.into_any_element()
2002                                }
2003                            })
2004                        },
2005                    )
2006                    .on_click({
2007                        let context = self.action_context.clone();
2008                        let keep_open_on_confirm = self.keep_open_on_confirm;
2009                        move |_, window, cx| {
2010                            handler(context.as_ref(), window, cx);
2011                            menu.update(cx, |menu, cx| {
2012                                menu.clicked = true;
2013                                if keep_open_on_confirm {
2014                                    menu.rebuild(window, cx);
2015                                } else {
2016                                    cx.emit(DismissEvent);
2017                                }
2018                            })
2019                            .ok();
2020                        }
2021                    }),
2022            )
2023            .into_any_element()
2024    }
2025}
2026
2027impl ContextMenuItem {
2028    fn is_selectable(&self) -> bool {
2029        match self {
2030            ContextMenuItem::Header(_)
2031            | ContextMenuItem::HeaderWithLink(_, _, _)
2032            | ContextMenuItem::Separator
2033            | ContextMenuItem::Label { .. } => false,
2034            ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
2035            ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
2036            ContextMenuItem::Submenu { .. } => true,
2037        }
2038    }
2039}
2040
2041impl Render for ContextMenu {
2042    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2043        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
2044        let window_size = window.viewport_size();
2045        let rem_size = window.rem_size();
2046        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
2047
2048        let mut focus_submenu: Option<FocusHandle> = None;
2049
2050        let submenu_container = match &mut self.submenu_state {
2051            SubmenuState::Open(open_submenu) => {
2052                let is_initializing = open_submenu.offset.is_none();
2053
2054                let computed_offset = if is_initializing {
2055                    let menu_bounds = self.main_menu_observed_bounds.get();
2056                    let trigger_bounds = open_submenu
2057                        .trigger_bounds
2058                        .or_else(|| self.submenu_trigger_bounds.get());
2059
2060                    match (menu_bounds, trigger_bounds) {
2061                        (Some(menu_bounds), Some(trigger_bounds)) => {
2062                            Some(trigger_bounds.origin.y - menu_bounds.origin.y)
2063                        }
2064                        _ => None,
2065                    }
2066                } else {
2067                    None
2068                };
2069
2070                if let Some(offset) = open_submenu.offset.or(computed_offset) {
2071                    if open_submenu.offset.is_none() {
2072                        open_submenu.offset = Some(offset);
2073                    }
2074
2075                    focus_submenu = Some(open_submenu.entity.read(cx).focus_handle.clone());
2076                    Some((open_submenu.item_index, open_submenu.entity.clone(), offset))
2077                } else {
2078                    None
2079                }
2080            }
2081            _ => None,
2082        };
2083
2084        let aside = self.documentation_aside.clone();
2085        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
2086            WithRemSize::new(ui_font_size)
2087                .occlude()
2088                .elevation_2(cx)
2089                .w_full()
2090                .p_2()
2091                .overflow_hidden()
2092                .when(is_wide_window, |this| this.max_w_96())
2093                .when(!is_wide_window, |this| this.max_w_48())
2094                .child((aside.render)(cx))
2095        };
2096
2097        let render_menu = |cx: &mut Context<Self>, window: &mut Window| {
2098            let bounds_cell = self.main_menu_observed_bounds.clone();
2099            let menu_bounds_measure = canvas(
2100                {
2101                    move |bounds, _window, _cx| {
2102                        bounds_cell.set(Some(bounds));
2103                    }
2104                },
2105                |_bounds, _state, _window, _cx| {},
2106            )
2107            .size_full()
2108            .absolute()
2109            .top_0()
2110            .left_0();
2111
2112            WithRemSize::new(ui_font_size)
2113                .occlude()
2114                .elevation_2(cx)
2115                .flex()
2116                .flex_row()
2117                .flex_shrink_0()
2118                .child(
2119                    v_flex()
2120                        .id("context-menu")
2121                        .max_h(vh(0.75, window))
2122                        .flex_shrink_0()
2123                        .child(menu_bounds_measure)
2124                        .when_some(self.fixed_width, |this, width| {
2125                            this.w(width).overflow_x_hidden()
2126                        })
2127                        .when(self.fixed_width.is_none(), |this| {
2128                            this.min_w(px(200.)).flex_1()
2129                        })
2130                        .overflow_y_scroll()
2131                        .track_focus(&self.focus_handle(cx))
2132                        .key_context(self.key_context.as_ref())
2133                        .on_action(cx.listener(ContextMenu::select_first))
2134                        .on_action(cx.listener(ContextMenu::handle_select_last))
2135                        .on_action(cx.listener(ContextMenu::select_next))
2136                        .on_action(cx.listener(ContextMenu::select_previous))
2137                        .on_action(cx.listener(ContextMenu::select_submenu_child))
2138                        .on_action(cx.listener(ContextMenu::select_submenu_parent))
2139                        .on_action(cx.listener(ContextMenu::confirm))
2140                        .on_action(cx.listener(ContextMenu::secondary_confirm))
2141                        .on_action(cx.listener(ContextMenu::cancel))
2142                        .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
2143                            if *hovered {
2144                                this.hover_target = HoverTarget::MainMenu;
2145                                if let Some(parent) = &this.main_menu {
2146                                    parent.update(cx, |parent, _| {
2147                                        parent.hover_target = HoverTarget::Submenu;
2148                                    });
2149                                }
2150                            }
2151                        }))
2152                        .on_mouse_down_out(cx.listener(
2153                            |this, event: &MouseDownEvent, window, cx| {
2154                                if matches!(&this.submenu_state, SubmenuState::Open(_)) {
2155                                    if let Some(padded_bounds) = this.padded_submenu_bounds() {
2156                                        if padded_bounds.contains(&event.position) {
2157                                            return;
2158                                        }
2159                                    }
2160                                }
2161
2162                                if let Some(parent) = &this.main_menu {
2163                                    let overridden_by_parent_trigger = parent
2164                                        .read(cx)
2165                                        .submenu_trigger_bounds
2166                                        .get()
2167                                        .is_some_and(|bounds| bounds.contains(&event.position));
2168                                    if overridden_by_parent_trigger {
2169                                        return;
2170                                    }
2171                                }
2172
2173                                this.cancel(&menu::Cancel, window, cx)
2174                            },
2175                        ))
2176                        .when_some(self.end_slot_action.as_ref(), |el, action| {
2177                            el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
2178                        })
2179                        .when(!self.delayed, |mut el| {
2180                            for item in self.items.iter() {
2181                                if let ContextMenuItem::Entry(ContextMenuEntry {
2182                                    action: Some(action),
2183                                    disabled: false,
2184                                    ..
2185                                }) = item
2186                                {
2187                                    el = el.on_boxed_action(
2188                                        &**action,
2189                                        cx.listener(ContextMenu::on_action_dispatch),
2190                                    );
2191                                }
2192                            }
2193                            el
2194                        })
2195                        .child(
2196                            List::new().children(
2197                                self.items
2198                                    .iter()
2199                                    .enumerate()
2200                                    .map(|(ix, item)| self.render_menu_item(ix, item, window, cx)),
2201                            ),
2202                        ),
2203                )
2204        };
2205
2206        if let Some(focus_handle) = focus_submenu.as_ref() {
2207            window.focus(focus_handle, cx);
2208        }
2209
2210        if is_wide_window {
2211            let menu_bounds = self.main_menu_observed_bounds.get();
2212            let trigger_bounds = self
2213                .documentation_aside
2214                .as_ref()
2215                .and_then(|(ix, _)| self.aside_trigger_bounds.borrow().get(ix).copied());
2216
2217            let trigger_position = match (menu_bounds, trigger_bounds) {
2218                (Some(menu_bounds), Some(trigger_bounds)) => {
2219                    let relative_top = trigger_bounds.origin.y - menu_bounds.origin.y;
2220                    let height = trigger_bounds.size.height;
2221                    Some((relative_top, height))
2222                }
2223                _ => None,
2224            };
2225
2226            div()
2227                .relative()
2228                .child(render_menu(cx, window))
2229                // Only render the aside once we have trigger bounds to avoid flicker.
2230                .when_some(trigger_position, |this, (top, height)| {
2231                    this.children(aside.map(|(_, aside)| {
2232                        h_flex()
2233                            .absolute()
2234                            .when(aside.side == DocumentationSide::Left, |el| {
2235                                el.right_full().mr_1()
2236                            })
2237                            .when(aside.side == DocumentationSide::Right, |el| {
2238                                el.left_full().ml_1()
2239                            })
2240                            .top(top)
2241                            .h(height)
2242                            .child(render_aside(aside, cx))
2243                    }))
2244                })
2245                .when_some(submenu_container, |this, (ix, submenu, offset)| {
2246                    this.child(self.render_submenu_container(ix, submenu, offset, cx))
2247                })
2248        } else {
2249            v_flex()
2250                .w_full()
2251                .relative()
2252                .gap_1()
2253                .justify_end()
2254                .children(aside.map(|(_, aside)| render_aside(aside, cx)))
2255                .child(render_menu(cx, window))
2256                .when_some(submenu_container, |this, (ix, submenu, offset)| {
2257                    this.child(self.render_submenu_container(ix, submenu, offset, cx))
2258                })
2259        }
2260    }
2261}
2262
2263#[cfg(test)]
2264mod tests {
2265    use gpui::TestAppContext;
2266
2267    use super::*;
2268
2269    #[gpui::test]
2270    fn can_navigate_back_over_headers(cx: &mut TestAppContext) {
2271        let cx = cx.add_empty_window();
2272        let context_menu = cx.update(|window, cx| {
2273            ContextMenu::build(window, cx, |menu, _, _| {
2274                menu.header("First header")
2275                    .separator()
2276                    .entry("First entry", None, |_, _| {})
2277                    .separator()
2278                    .separator()
2279                    .entry("Last entry", None, |_, _| {})
2280                    .header("Last header")
2281            })
2282        });
2283
2284        context_menu.update_in(cx, |context_menu, window, cx| {
2285            assert_eq!(
2286                None, context_menu.selected_index,
2287                "No selection is in the menu initially"
2288            );
2289
2290            context_menu.select_first(&SelectFirst, window, cx);
2291            assert_eq!(
2292                Some(2),
2293                context_menu.selected_index,
2294                "Should select first selectable entry, skipping the header and the separator"
2295            );
2296
2297            context_menu.select_next(&SelectNext, window, cx);
2298            assert_eq!(
2299                Some(5),
2300                context_menu.selected_index,
2301                "Should select next selectable entry, skipping 2 separators along the way"
2302            );
2303
2304            context_menu.select_next(&SelectNext, window, cx);
2305            assert_eq!(
2306                Some(2),
2307                context_menu.selected_index,
2308                "Should wrap around to first selectable entry"
2309            );
2310        });
2311
2312        context_menu.update_in(cx, |context_menu, window, cx| {
2313            assert_eq!(
2314                Some(2),
2315                context_menu.selected_index,
2316                "Should start from the first selectable entry"
2317            );
2318
2319            context_menu.select_previous(&SelectPrevious, window, cx);
2320            assert_eq!(
2321                Some(5),
2322                context_menu.selected_index,
2323                "Should wrap around to previous selectable entry (last)"
2324            );
2325
2326            context_menu.select_previous(&SelectPrevious, window, cx);
2327            assert_eq!(
2328                Some(2),
2329                context_menu.selected_index,
2330                "Should go back to previous selectable entry (first)"
2331            );
2332        });
2333
2334        context_menu.update_in(cx, |context_menu, window, cx| {
2335            context_menu.select_first(&SelectFirst, window, cx);
2336            assert_eq!(
2337                Some(2),
2338                context_menu.selected_index,
2339                "Should start from the first selectable entry"
2340            );
2341
2342            context_menu.select_previous(&SelectPrevious, window, cx);
2343            assert_eq!(
2344                Some(5),
2345                context_menu.selected_index,
2346                "Should wrap around to last selectable entry"
2347            );
2348            context_menu.select_next(&SelectNext, window, cx);
2349            assert_eq!(
2350                Some(2),
2351                context_menu.selected_index,
2352                "Should wrap around to first selectable entry"
2353            );
2354        });
2355    }
2356}