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