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