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