1use crate::{
   2    Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator,
   3    ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex,
   4};
   5use gpui::{
   6    Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle,
   7    Focusable, IntoElement, Render, Subscription, px,
   8};
   9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
  10use settings::Settings;
  11use std::{rc::Rc, time::Duration};
  12use theme::ThemeSettings;
  13
  14use super::Tooltip;
  15
  16pub enum ContextMenuItem {
  17    Separator,
  18    Header(SharedString),
  19    /// title, link_label, link_url
  20    HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header
  21    Label(SharedString),
  22    Entry(ContextMenuEntry),
  23    CustomEntry {
  24        entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
  25        handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
  26        selectable: bool,
  27        documentation_aside: Option<DocumentationAside>,
  28    },
  29}
  30
  31impl ContextMenuItem {
  32    pub fn custom_entry(
  33        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
  34        handler: impl Fn(&mut Window, &mut App) + 'static,
  35        documentation_aside: Option<DocumentationAside>,
  36    ) -> Self {
  37        Self::CustomEntry {
  38            entry_render: Box::new(entry_render),
  39            handler: Rc::new(move |_, window, cx| handler(window, cx)),
  40            selectable: true,
  41            documentation_aside,
  42        }
  43    }
  44}
  45
  46pub struct ContextMenuEntry {
  47    toggle: Option<(IconPosition, bool)>,
  48    label: SharedString,
  49    icon: Option<IconName>,
  50    custom_icon_path: Option<SharedString>,
  51    icon_position: IconPosition,
  52    icon_size: IconSize,
  53    icon_color: Option<Color>,
  54    handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
  55    action: Option<Box<dyn Action>>,
  56    disabled: bool,
  57    documentation_aside: Option<DocumentationAside>,
  58    end_slot_icon: Option<IconName>,
  59    end_slot_title: Option<SharedString>,
  60    end_slot_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,
  61    show_end_slot_on_hover: bool,
  62}
  63
  64impl ContextMenuEntry {
  65    pub fn new(label: impl Into<SharedString>) -> Self {
  66        ContextMenuEntry {
  67            toggle: None,
  68            label: label.into(),
  69            icon: None,
  70            custom_icon_path: None,
  71            icon_position: IconPosition::Start,
  72            icon_size: IconSize::Small,
  73            icon_color: None,
  74            handler: Rc::new(|_, _, _| {}),
  75            action: None,
  76            disabled: false,
  77            documentation_aside: None,
  78            end_slot_icon: None,
  79            end_slot_title: None,
  80            end_slot_handler: None,
  81            show_end_slot_on_hover: false,
  82        }
  83    }
  84
  85    pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
  86        self.toggle = Some((toggle_position, toggled));
  87        self
  88    }
  89
  90    pub fn icon(mut self, icon: IconName) -> Self {
  91        self.icon = Some(icon);
  92        self
  93    }
  94
  95    pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
  96        self.custom_icon_path = Some(path.into());
  97        self.icon = None; // Clear IconName if custom path is set
  98        self
  99    }
 100
 101    pub fn icon_position(mut self, position: IconPosition) -> Self {
 102        self.icon_position = position;
 103        self
 104    }
 105
 106    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
 107        self.icon_size = icon_size;
 108        self
 109    }
 110
 111    pub fn icon_color(mut self, icon_color: Color) -> Self {
 112        self.icon_color = Some(icon_color);
 113        self
 114    }
 115
 116    pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
 117        self.toggle = Some((toggle_position, toggled));
 118        self
 119    }
 120
 121    pub fn action(mut self, action: Box<dyn Action>) -> Self {
 122        self.action = Some(action);
 123        self
 124    }
 125
 126    pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
 127        self.handler = Rc::new(move |_, window, cx| handler(window, cx));
 128        self
 129    }
 130
 131    pub fn disabled(mut self, disabled: bool) -> Self {
 132        self.disabled = disabled;
 133        self
 134    }
 135
 136    pub fn documentation_aside(
 137        mut self,
 138        side: DocumentationSide,
 139        edge: DocumentationEdge,
 140        render: impl Fn(&mut App) -> AnyElement + 'static,
 141    ) -> Self {
 142        self.documentation_aside = Some(DocumentationAside {
 143            side,
 144            edge,
 145            render: Rc::new(render),
 146        });
 147
 148        self
 149    }
 150}
 151
 152impl FluentBuilder for ContextMenuEntry {}
 153
 154impl From<ContextMenuEntry> for ContextMenuItem {
 155    fn from(entry: ContextMenuEntry) -> Self {
 156        ContextMenuItem::Entry(entry)
 157    }
 158}
 159
 160pub struct ContextMenu {
 161    builder: Option<Rc<dyn Fn(Self, &mut Window, &mut Context<Self>) -> Self>>,
 162    items: Vec<ContextMenuItem>,
 163    focus_handle: FocusHandle,
 164    action_context: Option<FocusHandle>,
 165    selected_index: Option<usize>,
 166    delayed: bool,
 167    clicked: bool,
 168    end_slot_action: Option<Box<dyn Action>>,
 169    key_context: SharedString,
 170    _on_blur_subscription: Subscription,
 171    keep_open_on_confirm: bool,
 172    documentation_aside: Option<(usize, DocumentationAside)>,
 173    fixed_width: Option<DefiniteLength>,
 174}
 175
 176#[derive(Copy, Clone, PartialEq, Eq)]
 177pub enum DocumentationSide {
 178    Left,
 179    Right,
 180}
 181
 182#[derive(Copy, Default, Clone, PartialEq, Eq)]
 183pub enum DocumentationEdge {
 184    #[default]
 185    Top,
 186    Bottom,
 187}
 188
 189#[derive(Clone)]
 190pub struct DocumentationAside {
 191    pub side: DocumentationSide,
 192    pub edge: DocumentationEdge,
 193    pub render: Rc<dyn Fn(&mut App) -> AnyElement>,
 194}
 195
 196impl DocumentationAside {
 197    pub fn new(
 198        side: DocumentationSide,
 199        edge: DocumentationEdge,
 200        render: Rc<dyn Fn(&mut App) -> AnyElement>,
 201    ) -> Self {
 202        Self { side, edge, render }
 203    }
 204}
 205
 206impl Focusable for ContextMenu {
 207    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 208        self.focus_handle.clone()
 209    }
 210}
 211
 212impl EventEmitter<DismissEvent> for ContextMenu {}
 213
 214impl FluentBuilder for ContextMenu {}
 215
 216impl ContextMenu {
 217    pub fn new(
 218        window: &mut Window,
 219        cx: &mut Context<Self>,
 220        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
 221    ) -> Self {
 222        let focus_handle = cx.focus_handle();
 223        let _on_blur_subscription = cx.on_blur(
 224            &focus_handle,
 225            window,
 226            |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
 227        );
 228        window.refresh();
 229
 230        f(
 231            Self {
 232                builder: None,
 233                items: Default::default(),
 234                focus_handle,
 235                action_context: None,
 236                selected_index: None,
 237                delayed: false,
 238                clicked: false,
 239                key_context: "menu".into(),
 240                _on_blur_subscription,
 241                keep_open_on_confirm: false,
 242                documentation_aside: None,
 243                fixed_width: None,
 244                end_slot_action: None,
 245            },
 246            window,
 247            cx,
 248        )
 249    }
 250
 251    pub fn build(
 252        window: &mut Window,
 253        cx: &mut App,
 254        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
 255    ) -> Entity<Self> {
 256        cx.new(|cx| Self::new(window, cx, f))
 257    }
 258
 259    /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation.
 260    ///
 261    /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto
 262    /// it to call it again.
 263    pub fn build_persistent(
 264        window: &mut Window,
 265        cx: &mut App,
 266        builder: impl Fn(Self, &mut Window, &mut Context<Self>) -> Self + 'static,
 267    ) -> Entity<Self> {
 268        cx.new(|cx| {
 269            let builder = Rc::new(builder);
 270
 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| this.cancel(&menu::Cancel, window, cx),
 276            );
 277            window.refresh();
 278
 279            (builder.clone())(
 280                Self {
 281                    builder: Some(builder),
 282                    items: Default::default(),
 283                    focus_handle,
 284                    action_context: None,
 285                    selected_index: None,
 286                    delayed: false,
 287                    clicked: false,
 288                    key_context: "menu".into(),
 289                    _on_blur_subscription,
 290                    keep_open_on_confirm: true,
 291                    documentation_aside: None,
 292                    fixed_width: None,
 293                    end_slot_action: None,
 294                },
 295                window,
 296                cx,
 297            )
 298        })
 299    }
 300
 301    /// Rebuilds the menu.
 302    ///
 303    /// This is used to refresh the menu entries when entries are toggled when the menu is configured with
 304    /// `keep_open_on_confirm = true`.
 305    ///
 306    /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is
 307    /// a no-op.
 308    pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 309        let Some(builder) = self.builder.clone() else {
 310            return;
 311        };
 312
 313        // The way we rebuild the menu is a bit of a hack.
 314        let focus_handle = cx.focus_handle();
 315        let new_menu = (builder.clone())(
 316            Self {
 317                builder: Some(builder),
 318                items: Default::default(),
 319                focus_handle: focus_handle.clone(),
 320                action_context: None,
 321                selected_index: None,
 322                delayed: false,
 323                clicked: false,
 324                key_context: "menu".into(),
 325                _on_blur_subscription: cx.on_blur(
 326                    &focus_handle,
 327                    window,
 328                    |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
 329                ),
 330                keep_open_on_confirm: false,
 331                documentation_aside: None,
 332                fixed_width: None,
 333                end_slot_action: None,
 334            },
 335            window,
 336            cx,
 337        );
 338
 339        self.items = new_menu.items;
 340
 341        cx.notify();
 342    }
 343
 344    pub fn context(mut self, focus: FocusHandle) -> Self {
 345        self.action_context = Some(focus);
 346        self
 347    }
 348
 349    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
 350        self.items.push(ContextMenuItem::Header(title.into()));
 351        self
 352    }
 353
 354    pub fn header_with_link(
 355        mut self,
 356        title: impl Into<SharedString>,
 357        link_label: impl Into<SharedString>,
 358        link_url: impl Into<SharedString>,
 359    ) -> Self {
 360        self.items.push(ContextMenuItem::HeaderWithLink(
 361            title.into(),
 362            link_label.into(),
 363            link_url.into(),
 364        ));
 365        self
 366    }
 367
 368    pub fn separator(mut self) -> Self {
 369        self.items.push(ContextMenuItem::Separator);
 370        self
 371    }
 372
 373    pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
 374        self.items.extend(items.into_iter().map(Into::into));
 375        self
 376    }
 377
 378    pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
 379        self.items.push(item.into());
 380        self
 381    }
 382
 383    pub fn push_item(&mut self, item: impl Into<ContextMenuItem>) {
 384        self.items.push(item.into());
 385    }
 386
 387    pub fn entry(
 388        mut self,
 389        label: impl Into<SharedString>,
 390        action: Option<Box<dyn Action>>,
 391        handler: impl Fn(&mut Window, &mut App) + 'static,
 392    ) -> Self {
 393        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 394            toggle: None,
 395            label: label.into(),
 396            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 397            icon: None,
 398            custom_icon_path: None,
 399            icon_position: IconPosition::End,
 400            icon_size: IconSize::Small,
 401            icon_color: None,
 402            action,
 403            disabled: false,
 404            documentation_aside: None,
 405            end_slot_icon: None,
 406            end_slot_title: None,
 407            end_slot_handler: None,
 408            show_end_slot_on_hover: false,
 409        }));
 410        self
 411    }
 412
 413    pub fn entry_with_end_slot(
 414        mut self,
 415        label: impl Into<SharedString>,
 416        action: Option<Box<dyn Action>>,
 417        handler: impl Fn(&mut Window, &mut App) + 'static,
 418        end_slot_icon: IconName,
 419        end_slot_title: SharedString,
 420        end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
 421    ) -> Self {
 422        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 423            toggle: None,
 424            label: label.into(),
 425            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 426            icon: None,
 427            custom_icon_path: None,
 428            icon_position: IconPosition::End,
 429            icon_size: IconSize::Small,
 430            icon_color: None,
 431            action,
 432            disabled: false,
 433            documentation_aside: None,
 434            end_slot_icon: Some(end_slot_icon),
 435            end_slot_title: Some(end_slot_title),
 436            end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
 437            show_end_slot_on_hover: false,
 438        }));
 439        self
 440    }
 441
 442    pub fn entry_with_end_slot_on_hover(
 443        mut self,
 444        label: impl Into<SharedString>,
 445        action: Option<Box<dyn Action>>,
 446        handler: impl Fn(&mut Window, &mut App) + 'static,
 447        end_slot_icon: IconName,
 448        end_slot_title: SharedString,
 449        end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
 450    ) -> Self {
 451        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 452            toggle: None,
 453            label: label.into(),
 454            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 455            icon: None,
 456            custom_icon_path: None,
 457            icon_position: IconPosition::End,
 458            icon_size: IconSize::Small,
 459            icon_color: None,
 460            action,
 461            disabled: false,
 462            documentation_aside: None,
 463            end_slot_icon: Some(end_slot_icon),
 464            end_slot_title: Some(end_slot_title),
 465            end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
 466            show_end_slot_on_hover: true,
 467        }));
 468        self
 469    }
 470
 471    pub fn toggleable_entry(
 472        mut self,
 473        label: impl Into<SharedString>,
 474        toggled: bool,
 475        position: IconPosition,
 476        action: Option<Box<dyn Action>>,
 477        handler: impl Fn(&mut Window, &mut App) + 'static,
 478    ) -> Self {
 479        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 480            toggle: Some((position, toggled)),
 481            label: label.into(),
 482            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 483            icon: None,
 484            custom_icon_path: None,
 485            icon_position: position,
 486            icon_size: IconSize::Small,
 487            icon_color: None,
 488            action,
 489            disabled: false,
 490            documentation_aside: None,
 491            end_slot_icon: None,
 492            end_slot_title: None,
 493            end_slot_handler: None,
 494            show_end_slot_on_hover: false,
 495        }));
 496        self
 497    }
 498
 499    pub fn custom_row(
 500        mut self,
 501        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 502    ) -> Self {
 503        self.items.push(ContextMenuItem::CustomEntry {
 504            entry_render: Box::new(entry_render),
 505            handler: Rc::new(|_, _, _| {}),
 506            selectable: false,
 507            documentation_aside: None,
 508        });
 509        self
 510    }
 511
 512    pub fn custom_entry(
 513        mut self,
 514        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 515        handler: impl Fn(&mut Window, &mut App) + 'static,
 516    ) -> Self {
 517        self.items.push(ContextMenuItem::CustomEntry {
 518            entry_render: Box::new(entry_render),
 519            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 520            selectable: true,
 521            documentation_aside: None,
 522        });
 523        self
 524    }
 525
 526    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
 527        self.items.push(ContextMenuItem::Label(label.into()));
 528        self
 529    }
 530
 531    pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
 532        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 533            toggle: None,
 534            label: label.into(),
 535            action: Some(action.boxed_clone()),
 536            handler: Rc::new(move |context, window, cx| {
 537                if let Some(context) = &context {
 538                    window.focus(context);
 539                }
 540                window.dispatch_action(action.boxed_clone(), cx);
 541            }),
 542            icon: None,
 543            custom_icon_path: None,
 544            icon_position: IconPosition::End,
 545            icon_size: IconSize::Small,
 546            icon_color: None,
 547            disabled: false,
 548            documentation_aside: None,
 549            end_slot_icon: None,
 550            end_slot_title: None,
 551            end_slot_handler: None,
 552            show_end_slot_on_hover: false,
 553        }));
 554        self
 555    }
 556
 557    pub fn action_disabled_when(
 558        mut self,
 559        disabled: bool,
 560        label: impl Into<SharedString>,
 561        action: Box<dyn Action>,
 562    ) -> Self {
 563        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 564            toggle: None,
 565            label: label.into(),
 566            action: Some(action.boxed_clone()),
 567            handler: Rc::new(move |context, window, cx| {
 568                if let Some(context) = &context {
 569                    window.focus(context);
 570                }
 571                window.dispatch_action(action.boxed_clone(), cx);
 572            }),
 573            icon: None,
 574            custom_icon_path: None,
 575            icon_size: IconSize::Small,
 576            icon_position: IconPosition::End,
 577            icon_color: None,
 578            disabled,
 579            documentation_aside: None,
 580            end_slot_icon: None,
 581            end_slot_title: None,
 582            end_slot_handler: None,
 583            show_end_slot_on_hover: false,
 584        }));
 585        self
 586    }
 587
 588    pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
 589        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 590            toggle: None,
 591            label: label.into(),
 592            action: Some(action.boxed_clone()),
 593            handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
 594            icon: Some(IconName::ArrowUpRight),
 595            custom_icon_path: None,
 596            icon_size: IconSize::XSmall,
 597            icon_position: IconPosition::End,
 598            icon_color: None,
 599            disabled: false,
 600            documentation_aside: None,
 601            end_slot_icon: None,
 602            end_slot_title: None,
 603            end_slot_handler: None,
 604            show_end_slot_on_hover: false,
 605        }));
 606        self
 607    }
 608
 609    pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
 610        self.keep_open_on_confirm = keep_open;
 611        self
 612    }
 613
 614    pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 615        let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
 616            return;
 617        };
 618        let ContextMenuItem::Entry(entry) = entry else {
 619            return;
 620        };
 621        let Some(handler) = entry.end_slot_handler.as_ref() else {
 622            return;
 623        };
 624        handler(None, window, cx);
 625    }
 626
 627    pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
 628        self.fixed_width = Some(width);
 629        self
 630    }
 631
 632    pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
 633        self.end_slot_action = Some(action);
 634        self
 635    }
 636
 637    pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
 638        self.key_context = context.into();
 639        self
 640    }
 641
 642    pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 643        let context = self.action_context.as_ref();
 644        if let Some(
 645            ContextMenuItem::Entry(ContextMenuEntry {
 646                handler,
 647                disabled: false,
 648                ..
 649            })
 650            | ContextMenuItem::CustomEntry { handler, .. },
 651        ) = self.selected_index.and_then(|ix| self.items.get(ix))
 652        {
 653            (handler)(context, window, cx)
 654        }
 655
 656        if self.keep_open_on_confirm {
 657            self.rebuild(window, cx);
 658        } else {
 659            cx.emit(DismissEvent);
 660        }
 661    }
 662
 663    pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 664        cx.emit(DismissEvent);
 665        cx.emit(DismissEvent);
 666    }
 667
 668    pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
 669        let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
 670            return;
 671        };
 672        let ContextMenuItem::Entry(entry) = item else {
 673            return;
 674        };
 675        let Some(handler) = entry.end_slot_handler.as_ref() else {
 676            return;
 677        };
 678        handler(None, window, cx);
 679        self.rebuild(window, cx);
 680        cx.notify();
 681    }
 682
 683    pub fn clear_selected(&mut self) {
 684        self.selected_index = None;
 685    }
 686
 687    pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
 688        if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
 689            self.select_index(ix, window, cx);
 690        }
 691        cx.notify();
 692    }
 693
 694    pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
 695        for (ix, item) in self.items.iter().enumerate().rev() {
 696            if item.is_selectable() {
 697                return self.select_index(ix, window, cx);
 698            }
 699        }
 700        None
 701    }
 702
 703    fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 704        if self.select_last(window, cx).is_some() {
 705            cx.notify();
 706        }
 707    }
 708
 709    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 710        if let Some(ix) = self.selected_index {
 711            let next_index = ix + 1;
 712            if self.items.len() <= next_index {
 713                self.select_first(&SelectFirst, window, cx);
 714                return;
 715            } else {
 716                for (ix, item) in self.items.iter().enumerate().skip(next_index) {
 717                    if item.is_selectable() {
 718                        self.select_index(ix, window, cx);
 719                        cx.notify();
 720                        return;
 721                    }
 722                }
 723            }
 724        }
 725        self.select_first(&SelectFirst, window, cx);
 726    }
 727
 728    pub fn select_previous(
 729        &mut self,
 730        _: &SelectPrevious,
 731        window: &mut Window,
 732        cx: &mut Context<Self>,
 733    ) {
 734        if let Some(ix) = self.selected_index {
 735            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
 736                if item.is_selectable() {
 737                    self.select_index(ix, window, cx);
 738                    cx.notify();
 739                    return;
 740                }
 741            }
 742        }
 743        self.handle_select_last(&SelectLast, window, cx);
 744    }
 745
 746    fn select_index(
 747        &mut self,
 748        ix: usize,
 749        _window: &mut Window,
 750        _cx: &mut Context<Self>,
 751    ) -> Option<usize> {
 752        self.documentation_aside = None;
 753        let item = self.items.get(ix)?;
 754        if item.is_selectable() {
 755            self.selected_index = Some(ix);
 756            match item {
 757                ContextMenuItem::Entry(entry) => {
 758                    if let Some(callback) = &entry.documentation_aside {
 759                        self.documentation_aside = Some((ix, callback.clone()));
 760                    }
 761                }
 762                ContextMenuItem::CustomEntry {
 763                    documentation_aside: Some(callback),
 764                    ..
 765                } => {
 766                    self.documentation_aside = Some((ix, callback.clone()));
 767                }
 768                _ => (),
 769            }
 770        }
 771        Some(ix)
 772    }
 773
 774    pub fn on_action_dispatch(
 775        &mut self,
 776        dispatched: &dyn Action,
 777        window: &mut Window,
 778        cx: &mut Context<Self>,
 779    ) {
 780        if self.clicked {
 781            cx.propagate();
 782            return;
 783        }
 784
 785        if let Some(ix) = self.items.iter().position(|item| {
 786            if let ContextMenuItem::Entry(ContextMenuEntry {
 787                action: Some(action),
 788                disabled: false,
 789                ..
 790            }) = item
 791            {
 792                action.partial_eq(dispatched)
 793            } else {
 794                false
 795            }
 796        }) {
 797            self.select_index(ix, window, cx);
 798            self.delayed = true;
 799            cx.notify();
 800            let action = dispatched.boxed_clone();
 801            cx.spawn_in(window, async move |this, cx| {
 802                cx.background_executor()
 803                    .timer(Duration::from_millis(50))
 804                    .await;
 805                cx.update(|window, cx| {
 806                    this.update(cx, |this, cx| {
 807                        this.cancel(&menu::Cancel, window, cx);
 808                        window.dispatch_action(action, cx);
 809                    })
 810                })
 811            })
 812            .detach_and_log_err(cx);
 813        } else {
 814            cx.propagate()
 815        }
 816    }
 817
 818    pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
 819        self._on_blur_subscription = new_subscription;
 820        self
 821    }
 822
 823    fn render_menu_item(
 824        &self,
 825        ix: usize,
 826        item: &ContextMenuItem,
 827        window: &mut Window,
 828        cx: &mut Context<Self>,
 829    ) -> impl IntoElement + use<> {
 830        match item {
 831            ContextMenuItem::Separator => ListSeparator.into_any_element(),
 832            ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
 833                .inset(true)
 834                .into_any_element(),
 835            ContextMenuItem::HeaderWithLink(header, label, url) => {
 836                let url = url.clone();
 837                let link_id = ElementId::Name(format!("link-{}", url).into());
 838                ListSubHeader::new(header.clone())
 839                    .inset(true)
 840                    .end_slot(
 841                        Button::new(link_id, label.clone())
 842                            .color(Color::Muted)
 843                            .label_size(LabelSize::Small)
 844                            .size(ButtonSize::None)
 845                            .style(ButtonStyle::Transparent)
 846                            .on_click(move |_, _, cx| {
 847                                let url = url.clone();
 848                                cx.open_url(&url);
 849                            })
 850                            .into_any_element(),
 851                    )
 852                    .into_any_element()
 853            }
 854            ContextMenuItem::Label(label) => ListItem::new(ix)
 855                .inset(true)
 856                .disabled(true)
 857                .child(Label::new(label.clone()))
 858                .into_any_element(),
 859            ContextMenuItem::Entry(entry) => {
 860                self.render_menu_entry(ix, entry, cx).into_any_element()
 861            }
 862            ContextMenuItem::CustomEntry {
 863                entry_render,
 864                handler,
 865                selectable,
 866                ..
 867            } => {
 868                let handler = handler.clone();
 869                let menu = cx.entity().downgrade();
 870                let selectable = *selectable;
 871                ListItem::new(ix)
 872                    .inset(true)
 873                    .toggle_state(if selectable {
 874                        Some(ix) == self.selected_index
 875                    } else {
 876                        false
 877                    })
 878                    .selectable(selectable)
 879                    .when(selectable, |item| {
 880                        item.on_click({
 881                            let context = self.action_context.clone();
 882                            let keep_open_on_confirm = self.keep_open_on_confirm;
 883                            move |_, window, cx| {
 884                                handler(context.as_ref(), window, cx);
 885                                menu.update(cx, |menu, cx| {
 886                                    menu.clicked = true;
 887
 888                                    if keep_open_on_confirm {
 889                                        menu.rebuild(window, cx);
 890                                    } else {
 891                                        cx.emit(DismissEvent);
 892                                    }
 893                                })
 894                                .ok();
 895                            }
 896                        })
 897                    })
 898                    .child(entry_render(window, cx))
 899                    .into_any_element()
 900            }
 901        }
 902    }
 903
 904    fn render_menu_entry(
 905        &self,
 906        ix: usize,
 907        entry: &ContextMenuEntry,
 908        cx: &mut Context<Self>,
 909    ) -> impl IntoElement {
 910        let ContextMenuEntry {
 911            toggle,
 912            label,
 913            handler,
 914            icon,
 915            custom_icon_path,
 916            icon_position,
 917            icon_size,
 918            icon_color,
 919            action,
 920            disabled,
 921            documentation_aside,
 922            end_slot_icon,
 923            end_slot_title,
 924            end_slot_handler,
 925            show_end_slot_on_hover,
 926        } = entry;
 927        let this = cx.weak_entity();
 928
 929        let handler = handler.clone();
 930        let menu = cx.entity().downgrade();
 931
 932        let icon_color = if *disabled {
 933            Color::Muted
 934        } else if toggle.is_some() {
 935            icon_color.unwrap_or(Color::Accent)
 936        } else {
 937            icon_color.unwrap_or(Color::Default)
 938        };
 939
 940        let label_color = if *disabled {
 941            Color::Disabled
 942        } else {
 943            Color::Default
 944        };
 945
 946        let label_element = if let Some(custom_path) = custom_icon_path {
 947            h_flex()
 948                .gap_1p5()
 949                .when(
 950                    *icon_position == IconPosition::Start && toggle.is_none(),
 951                    |flex| {
 952                        flex.child(
 953                            Icon::from_path(custom_path.clone())
 954                                .size(*icon_size)
 955                                .color(icon_color),
 956                        )
 957                    },
 958                )
 959                .child(Label::new(label.clone()).color(label_color).truncate())
 960                .when(*icon_position == IconPosition::End, |flex| {
 961                    flex.child(
 962                        Icon::from_path(custom_path.clone())
 963                            .size(*icon_size)
 964                            .color(icon_color),
 965                    )
 966                })
 967                .into_any_element()
 968        } else if let Some(icon_name) = icon {
 969            h_flex()
 970                .gap_1p5()
 971                .when(
 972                    *icon_position == IconPosition::Start && toggle.is_none(),
 973                    |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
 974                )
 975                .child(Label::new(label.clone()).color(label_color).truncate())
 976                .when(*icon_position == IconPosition::End, |flex| {
 977                    flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
 978                })
 979                .into_any_element()
 980        } else {
 981            Label::new(label.clone())
 982                .color(label_color)
 983                .truncate()
 984                .into_any_element()
 985        };
 986
 987        div()
 988            .id(("context-menu-child", ix))
 989            .when_some(documentation_aside.clone(), |this, documentation_aside| {
 990                this.occlude()
 991                    .on_hover(cx.listener(move |menu, hovered, _, cx| {
 992                        if *hovered {
 993                            menu.documentation_aside = Some((ix, documentation_aside.clone()));
 994                        } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
 995                            menu.documentation_aside = None;
 996                        }
 997                        cx.notify();
 998                    }))
 999            })
1000            .child(
1001                ListItem::new(ix)
1002                    .group_name("label_container")
1003                    .inset(true)
1004                    .disabled(*disabled)
1005                    .toggle_state(Some(ix) == self.selected_index)
1006                    .when_some(*toggle, |list_item, (position, toggled)| {
1007                        let contents = div()
1008                            .flex_none()
1009                            .child(
1010                                Icon::new(icon.unwrap_or(IconName::Check))
1011                                    .color(icon_color)
1012                                    .size(*icon_size),
1013                            )
1014                            .when(!toggled, |contents| contents.invisible());
1015
1016                        match position {
1017                            IconPosition::Start => list_item.start_slot(contents),
1018                            IconPosition::End => list_item.end_slot(contents),
1019                        }
1020                    })
1021                    .child(
1022                        h_flex()
1023                            .w_full()
1024                            .justify_between()
1025                            .child(label_element)
1026                            .debug_selector(|| format!("MENU_ITEM-{}", label))
1027                            .children(action.as_ref().map(|action| {
1028                                let binding = self
1029                                    .action_context
1030                                    .as_ref()
1031                                    .map(|focus| KeyBinding::for_action_in(&**action, focus, cx))
1032                                    .unwrap_or_else(|| KeyBinding::for_action(&**action, cx));
1033
1034                                div()
1035                                    .ml_4()
1036                                    .child(binding.disabled(*disabled))
1037                                    .when(*disabled && documentation_aside.is_some(), |parent| {
1038                                        parent.invisible()
1039                                    })
1040                            }))
1041                            .when(*disabled && documentation_aside.is_some(), |parent| {
1042                                parent.child(
1043                                    Icon::new(IconName::Info)
1044                                        .size(IconSize::XSmall)
1045                                        .color(Color::Muted),
1046                                )
1047                            }),
1048                    )
1049                    .when_some(
1050                        end_slot_icon
1051                            .as_ref()
1052                            .zip(self.end_slot_action.as_ref())
1053                            .zip(end_slot_title.as_ref())
1054                            .zip(end_slot_handler.as_ref()),
1055                        |el, (((icon, action), title), handler)| {
1056                            el.end_slot({
1057                                let icon_button = IconButton::new("end-slot-icon", *icon)
1058                                    .shape(IconButtonShape::Square)
1059                                    .tooltip({
1060                                        let action_context = self.action_context.clone();
1061                                        let title = title.clone();
1062                                        let action = action.boxed_clone();
1063                                        move |_window, cx| {
1064                                            action_context
1065                                                .as_ref()
1066                                                .map(|focus| {
1067                                                    Tooltip::for_action_in(
1068                                                        title.clone(),
1069                                                        &*action,
1070                                                        focus,
1071                                                        cx,
1072                                                    )
1073                                                })
1074                                                .unwrap_or_else(|| {
1075                                                    Tooltip::for_action(title.clone(), &*action, cx)
1076                                                })
1077                                        }
1078                                    })
1079                                    .on_click({
1080                                        let handler = handler.clone();
1081                                        move |_, window, cx| {
1082                                            handler(None, window, cx);
1083                                            this.update(cx, |this, cx| {
1084                                                this.rebuild(window, cx);
1085                                                cx.notify();
1086                                            })
1087                                            .ok();
1088                                        }
1089                                    });
1090
1091                                if *show_end_slot_on_hover {
1092                                    div()
1093                                        .visible_on_hover("label_container")
1094                                        .child(icon_button)
1095                                        .into_any_element()
1096                                } else {
1097                                    icon_button.into_any_element()
1098                                }
1099                            })
1100                        },
1101                    )
1102                    .on_click({
1103                        let context = self.action_context.clone();
1104                        let keep_open_on_confirm = self.keep_open_on_confirm;
1105                        move |_, window, cx| {
1106                            handler(context.as_ref(), window, cx);
1107                            menu.update(cx, |menu, cx| {
1108                                menu.clicked = true;
1109                                if keep_open_on_confirm {
1110                                    menu.rebuild(window, cx);
1111                                } else {
1112                                    cx.emit(DismissEvent);
1113                                }
1114                            })
1115                            .ok();
1116                        }
1117                    }),
1118            )
1119            .into_any_element()
1120    }
1121}
1122
1123impl ContextMenuItem {
1124    fn is_selectable(&self) -> bool {
1125        match self {
1126            ContextMenuItem::Header(_)
1127            | ContextMenuItem::HeaderWithLink(_, _, _)
1128            | ContextMenuItem::Separator
1129            | ContextMenuItem::Label { .. } => false,
1130            ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
1131            ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
1132        }
1133    }
1134}
1135
1136impl Render for ContextMenu {
1137    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1138        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1139        let window_size = window.viewport_size();
1140        let rem_size = window.rem_size();
1141        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1142
1143        let aside = self.documentation_aside.clone();
1144        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1145            WithRemSize::new(ui_font_size)
1146                .occlude()
1147                .elevation_2(cx)
1148                .w_full()
1149                .p_2()
1150                .overflow_hidden()
1151                .when(is_wide_window, |this| this.max_w_96())
1152                .when(!is_wide_window, |this| this.max_w_48())
1153                .child((aside.render)(cx))
1154        };
1155
1156        let render_menu =
1157            |cx: &mut Context<Self>, window: &mut Window| {
1158                WithRemSize::new(ui_font_size)
1159                    .occlude()
1160                    .elevation_2(cx)
1161                    .flex()
1162                    .flex_row()
1163                    .flex_shrink_0()
1164                    .child(
1165                        v_flex()
1166                            .id("context-menu")
1167                            .max_h(vh(0.75, window))
1168                            .flex_shrink_0()
1169                            .when_some(self.fixed_width, |this, width| {
1170                                this.w(width).overflow_x_hidden()
1171                            })
1172                            .when(self.fixed_width.is_none(), |this| {
1173                                this.min_w(px(200.)).flex_1()
1174                            })
1175                            .overflow_y_scroll()
1176                            .track_focus(&self.focus_handle(cx))
1177                            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
1178                                this.cancel(&menu::Cancel, window, cx)
1179                            }))
1180                            .key_context(self.key_context.as_ref())
1181                            .on_action(cx.listener(ContextMenu::select_first))
1182                            .on_action(cx.listener(ContextMenu::handle_select_last))
1183                            .on_action(cx.listener(ContextMenu::select_next))
1184                            .on_action(cx.listener(ContextMenu::select_previous))
1185                            .on_action(cx.listener(ContextMenu::confirm))
1186                            .on_action(cx.listener(ContextMenu::cancel))
1187                            .when_some(self.end_slot_action.as_ref(), |el, action| {
1188                                el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
1189                            })
1190                            .when(!self.delayed, |mut el| {
1191                                for item in self.items.iter() {
1192                                    if let ContextMenuItem::Entry(ContextMenuEntry {
1193                                        action: Some(action),
1194                                        disabled: false,
1195                                        ..
1196                                    }) = item
1197                                    {
1198                                        el = el.on_boxed_action(
1199                                            &**action,
1200                                            cx.listener(ContextMenu::on_action_dispatch),
1201                                        );
1202                                    }
1203                                }
1204                                el
1205                            })
1206                            .child(
1207                                List::new().children(
1208                                    self.items.iter().enumerate().map(|(ix, item)| {
1209                                        self.render_menu_item(ix, item, window, cx)
1210                                    }),
1211                                ),
1212                            ),
1213                    )
1214            };
1215
1216        if is_wide_window {
1217            div()
1218                .relative()
1219                .child(render_menu(cx, window))
1220                .children(aside.map(|(_item_index, aside)| {
1221                    h_flex()
1222                        .absolute()
1223                        .when(aside.side == DocumentationSide::Left, |this| {
1224                            this.right_full().mr_1()
1225                        })
1226                        .when(aside.side == DocumentationSide::Right, |this| {
1227                            this.left_full().ml_1()
1228                        })
1229                        .when(aside.edge == DocumentationEdge::Top, |this| this.top_0())
1230                        .when(aside.edge == DocumentationEdge::Bottom, |this| {
1231                            this.bottom_0()
1232                        })
1233                        .child(render_aside(aside, cx))
1234                }))
1235        } else {
1236            v_flex()
1237                .w_full()
1238                .gap_1()
1239                .justify_end()
1240                .children(aside.map(|(_, aside)| render_aside(aside, cx)))
1241                .child(render_menu(cx, window))
1242        }
1243    }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248    use gpui::TestAppContext;
1249
1250    use super::*;
1251
1252    #[gpui::test]
1253    fn can_navigate_back_over_headers(cx: &mut TestAppContext) {
1254        let cx = cx.add_empty_window();
1255        let context_menu = cx.update(|window, cx| {
1256            ContextMenu::build(window, cx, |menu, _, _| {
1257                menu.header("First header")
1258                    .separator()
1259                    .entry("First entry", None, |_, _| {})
1260                    .separator()
1261                    .separator()
1262                    .entry("Last entry", None, |_, _| {})
1263                    .header("Last header")
1264            })
1265        });
1266
1267        context_menu.update_in(cx, |context_menu, window, cx| {
1268            assert_eq!(
1269                None, context_menu.selected_index,
1270                "No selection is in the menu initially"
1271            );
1272
1273            context_menu.select_first(&SelectFirst, window, cx);
1274            assert_eq!(
1275                Some(2),
1276                context_menu.selected_index,
1277                "Should select first selectable entry, skipping the header and the separator"
1278            );
1279
1280            context_menu.select_next(&SelectNext, window, cx);
1281            assert_eq!(
1282                Some(5),
1283                context_menu.selected_index,
1284                "Should select next selectable entry, skipping 2 separators along the way"
1285            );
1286
1287            context_menu.select_next(&SelectNext, window, cx);
1288            assert_eq!(
1289                Some(2),
1290                context_menu.selected_index,
1291                "Should wrap around to first selectable entry"
1292            );
1293        });
1294
1295        context_menu.update_in(cx, |context_menu, window, cx| {
1296            assert_eq!(
1297                Some(2),
1298                context_menu.selected_index,
1299                "Should start from the first selectable entry"
1300            );
1301
1302            context_menu.select_previous(&SelectPrevious, window, cx);
1303            assert_eq!(
1304                Some(5),
1305                context_menu.selected_index,
1306                "Should wrap around to previous selectable entry (last)"
1307            );
1308
1309            context_menu.select_previous(&SelectPrevious, window, cx);
1310            assert_eq!(
1311                Some(2),
1312                context_menu.selected_index,
1313                "Should go back to previous selectable entry (first)"
1314            );
1315        });
1316
1317        context_menu.update_in(cx, |context_menu, window, cx| {
1318            context_menu.select_first(&SelectFirst, window, cx);
1319            assert_eq!(
1320                Some(2),
1321                context_menu.selected_index,
1322                "Should start from the first selectable entry"
1323            );
1324
1325            context_menu.select_previous(&SelectPrevious, window, cx);
1326            assert_eq!(
1327                Some(5),
1328                context_menu.selected_index,
1329                "Should wrap around to last selectable entry"
1330            );
1331            context_menu.select_next(&SelectNext, window, cx);
1332            assert_eq!(
1333                Some(2),
1334                context_menu.selected_index,
1335                "Should wrap around to first selectable entry"
1336            );
1337        });
1338    }
1339}