context_menu.rs

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