context_menu.rs

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