context_menu.rs

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