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