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