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 action_disabled_when(
 507        mut self,
 508        disabled: bool,
 509        label: impl Into<SharedString>,
 510        action: Box<dyn Action>,
 511    ) -> Self {
 512        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 513            toggle: None,
 514            label: label.into(),
 515            action: Some(action.boxed_clone()),
 516            handler: Rc::new(move |context, window, cx| {
 517                if let Some(context) = &context {
 518                    window.focus(context);
 519                }
 520                window.dispatch_action(action.boxed_clone(), cx);
 521            }),
 522            icon: None,
 523            icon_size: IconSize::Small,
 524            icon_position: IconPosition::End,
 525            icon_color: None,
 526            disabled,
 527            documentation_aside: None,
 528            end_slot_icon: None,
 529            end_slot_title: None,
 530            end_slot_handler: None,
 531            show_end_slot_on_hover: false,
 532        }));
 533        self
 534    }
 535
 536    pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
 537        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
 538            toggle: None,
 539            label: label.into(),
 540            action: Some(action.boxed_clone()),
 541            handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
 542            icon: Some(IconName::ArrowUpRight),
 543            icon_size: IconSize::XSmall,
 544            icon_position: IconPosition::End,
 545            icon_color: None,
 546            disabled: false,
 547            documentation_aside: None,
 548            end_slot_icon: None,
 549            end_slot_title: None,
 550            end_slot_handler: None,
 551            show_end_slot_on_hover: false,
 552        }));
 553        self
 554    }
 555
 556    pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
 557        self.keep_open_on_confirm = keep_open;
 558        self
 559    }
 560
 561    pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 562        let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
 563            return;
 564        };
 565        let ContextMenuItem::Entry(entry) = entry else {
 566            return;
 567        };
 568        let Some(handler) = entry.end_slot_handler.as_ref() else {
 569            return;
 570        };
 571        handler(None, window, cx);
 572    }
 573
 574    pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
 575        self.fixed_width = Some(width);
 576        self
 577    }
 578
 579    pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
 580        self.end_slot_action = Some(action);
 581        self
 582    }
 583
 584    pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
 585        self.key_context = context.into();
 586        self
 587    }
 588
 589    pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 590        let context = self.action_context.as_ref();
 591        if let Some(
 592            ContextMenuItem::Entry(ContextMenuEntry {
 593                handler,
 594                disabled: false,
 595                ..
 596            })
 597            | ContextMenuItem::CustomEntry { handler, .. },
 598        ) = self.selected_index.and_then(|ix| self.items.get(ix))
 599        {
 600            (handler)(context, window, cx)
 601        }
 602
 603        if self.keep_open_on_confirm {
 604            self.rebuild(window, cx);
 605        } else {
 606            cx.emit(DismissEvent);
 607        }
 608    }
 609
 610    pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 611        cx.emit(DismissEvent);
 612        cx.emit(DismissEvent);
 613    }
 614
 615    pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
 616        let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
 617            return;
 618        };
 619        let ContextMenuItem::Entry(entry) = item else {
 620            return;
 621        };
 622        let Some(handler) = entry.end_slot_handler.as_ref() else {
 623            return;
 624        };
 625        handler(None, window, cx);
 626        self.rebuild(window, cx);
 627        cx.notify();
 628    }
 629
 630    pub fn clear_selected(&mut self) {
 631        self.selected_index = None;
 632    }
 633
 634    pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
 635        if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
 636            self.select_index(ix, window, cx);
 637        }
 638        cx.notify();
 639    }
 640
 641    pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
 642        for (ix, item) in self.items.iter().enumerate().rev() {
 643            if item.is_selectable() {
 644                return self.select_index(ix, window, cx);
 645            }
 646        }
 647        None
 648    }
 649
 650    fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 651        if self.select_last(window, cx).is_some() {
 652            cx.notify();
 653        }
 654    }
 655
 656    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 657        if let Some(ix) = self.selected_index {
 658            let next_index = ix + 1;
 659            if self.items.len() <= next_index {
 660                self.select_first(&SelectFirst, window, cx);
 661            } else {
 662                for (ix, item) in self.items.iter().enumerate().skip(next_index) {
 663                    if item.is_selectable() {
 664                        self.select_index(ix, window, cx);
 665                        cx.notify();
 666                        break;
 667                    }
 668                }
 669            }
 670        } else {
 671            self.select_first(&SelectFirst, window, cx);
 672        }
 673    }
 674
 675    pub fn select_previous(
 676        &mut self,
 677        _: &SelectPrevious,
 678        window: &mut Window,
 679        cx: &mut Context<Self>,
 680    ) {
 681        if let Some(ix) = self.selected_index {
 682            if ix == 0 {
 683                self.handle_select_last(&SelectLast, window, cx);
 684            } else {
 685                for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
 686                    if item.is_selectable() {
 687                        self.select_index(ix, window, cx);
 688                        cx.notify();
 689                        break;
 690                    }
 691                }
 692            }
 693        } else {
 694            self.handle_select_last(&SelectLast, window, cx);
 695        }
 696    }
 697
 698    fn select_index(
 699        &mut self,
 700        ix: usize,
 701        _window: &mut Window,
 702        _cx: &mut Context<Self>,
 703    ) -> Option<usize> {
 704        self.documentation_aside = None;
 705        let item = self.items.get(ix)?;
 706        if item.is_selectable() {
 707            self.selected_index = Some(ix);
 708            if let ContextMenuItem::Entry(entry) = item {
 709                if let Some(callback) = &entry.documentation_aside {
 710                    self.documentation_aside = Some((ix, callback.clone()));
 711                }
 712            }
 713        }
 714        Some(ix)
 715    }
 716
 717    pub fn on_action_dispatch(
 718        &mut self,
 719        dispatched: &dyn Action,
 720        window: &mut Window,
 721        cx: &mut Context<Self>,
 722    ) {
 723        if self.clicked {
 724            cx.propagate();
 725            return;
 726        }
 727
 728        if let Some(ix) = self.items.iter().position(|item| {
 729            if let ContextMenuItem::Entry(ContextMenuEntry {
 730                action: Some(action),
 731                disabled: false,
 732                ..
 733            }) = item
 734            {
 735                action.partial_eq(dispatched)
 736            } else {
 737                false
 738            }
 739        }) {
 740            self.select_index(ix, window, cx);
 741            self.delayed = true;
 742            cx.notify();
 743            let action = dispatched.boxed_clone();
 744            cx.spawn_in(window, async move |this, cx| {
 745                cx.background_executor()
 746                    .timer(Duration::from_millis(50))
 747                    .await;
 748                cx.update(|window, cx| {
 749                    this.update(cx, |this, cx| {
 750                        this.cancel(&menu::Cancel, window, cx);
 751                        window.dispatch_action(action, cx);
 752                    })
 753                })
 754            })
 755            .detach_and_log_err(cx);
 756        } else {
 757            cx.propagate()
 758        }
 759    }
 760
 761    pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
 762        self._on_blur_subscription = new_subscription;
 763        self
 764    }
 765
 766    fn render_menu_item(
 767        &self,
 768        ix: usize,
 769        item: &ContextMenuItem,
 770        window: &mut Window,
 771        cx: &mut Context<Self>,
 772    ) -> impl IntoElement + use<> {
 773        match item {
 774            ContextMenuItem::Separator => ListSeparator.into_any_element(),
 775            ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
 776                .inset(true)
 777                .into_any_element(),
 778            ContextMenuItem::HeaderWithLink(header, label, url) => {
 779                let url = url.clone();
 780                let link_id = ElementId::Name(format!("link-{}", url).into());
 781                ListSubHeader::new(header.clone())
 782                    .inset(true)
 783                    .end_slot(
 784                        Button::new(link_id, label.clone())
 785                            .color(Color::Muted)
 786                            .label_size(LabelSize::Small)
 787                            .size(ButtonSize::None)
 788                            .style(ButtonStyle::Transparent)
 789                            .on_click(move |_, _, cx| {
 790                                let url = url.clone();
 791                                cx.open_url(&url);
 792                            })
 793                            .into_any_element(),
 794                    )
 795                    .into_any_element()
 796            }
 797            ContextMenuItem::Label(label) => ListItem::new(ix)
 798                .inset(true)
 799                .disabled(true)
 800                .child(Label::new(label.clone()))
 801                .into_any_element(),
 802            ContextMenuItem::Entry(entry) => self
 803                .render_menu_entry(ix, entry, window, cx)
 804                .into_any_element(),
 805            ContextMenuItem::CustomEntry {
 806                entry_render,
 807                handler,
 808                selectable,
 809            } => {
 810                let handler = handler.clone();
 811                let menu = cx.entity().downgrade();
 812                let selectable = *selectable;
 813                ListItem::new(ix)
 814                    .inset(true)
 815                    .toggle_state(if selectable {
 816                        Some(ix) == self.selected_index
 817                    } else {
 818                        false
 819                    })
 820                    .selectable(selectable)
 821                    .when(selectable, |item| {
 822                        item.on_click({
 823                            let context = self.action_context.clone();
 824                            let keep_open_on_confirm = self.keep_open_on_confirm;
 825                            move |_, window, cx| {
 826                                handler(context.as_ref(), window, cx);
 827                                menu.update(cx, |menu, cx| {
 828                                    menu.clicked = true;
 829
 830                                    if keep_open_on_confirm {
 831                                        menu.rebuild(window, cx);
 832                                    } else {
 833                                        cx.emit(DismissEvent);
 834                                    }
 835                                })
 836                                .ok();
 837                            }
 838                        })
 839                    })
 840                    .child(entry_render(window, cx))
 841                    .into_any_element()
 842            }
 843        }
 844    }
 845
 846    fn render_menu_entry(
 847        &self,
 848        ix: usize,
 849        entry: &ContextMenuEntry,
 850        window: &mut Window,
 851        cx: &mut Context<Self>,
 852    ) -> impl IntoElement {
 853        let ContextMenuEntry {
 854            toggle,
 855            label,
 856            handler,
 857            icon,
 858            icon_position,
 859            icon_size,
 860            icon_color,
 861            action,
 862            disabled,
 863            documentation_aside,
 864            end_slot_icon,
 865            end_slot_title,
 866            end_slot_handler,
 867            show_end_slot_on_hover,
 868        } = entry;
 869        let this = cx.weak_entity();
 870
 871        let handler = handler.clone();
 872        let menu = cx.entity().downgrade();
 873
 874        let icon_color = if *disabled {
 875            Color::Muted
 876        } else if toggle.is_some() {
 877            icon_color.unwrap_or(Color::Accent)
 878        } else {
 879            icon_color.unwrap_or(Color::Default)
 880        };
 881
 882        let label_color = if *disabled {
 883            Color::Disabled
 884        } else {
 885            Color::Default
 886        };
 887
 888        let label_element = if let Some(icon_name) = icon {
 889            h_flex()
 890                .gap_1p5()
 891                .when(
 892                    *icon_position == IconPosition::Start && toggle.is_none(),
 893                    |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
 894                )
 895                .child(Label::new(label.clone()).color(label_color).truncate())
 896                .when(*icon_position == IconPosition::End, |flex| {
 897                    flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
 898                })
 899                .into_any_element()
 900        } else {
 901            Label::new(label.clone())
 902                .color(label_color)
 903                .truncate()
 904                .into_any_element()
 905        };
 906
 907        div()
 908            .id(("context-menu-child", ix))
 909            .when_some(documentation_aside.clone(), |this, documentation_aside| {
 910                this.occlude()
 911                    .on_hover(cx.listener(move |menu, hovered, _, cx| {
 912                        if *hovered {
 913                            menu.documentation_aside = Some((ix, documentation_aside.clone()));
 914                        } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
 915                            menu.documentation_aside = None;
 916                        }
 917                        cx.notify();
 918                    }))
 919            })
 920            .child(
 921                ListItem::new(ix)
 922                    .group_name("label_container")
 923                    .inset(true)
 924                    .disabled(*disabled)
 925                    .toggle_state(Some(ix) == self.selected_index)
 926                    .when_some(*toggle, |list_item, (position, toggled)| {
 927                        let contents = div()
 928                            .flex_none()
 929                            .child(
 930                                Icon::new(icon.unwrap_or(IconName::Check))
 931                                    .color(icon_color)
 932                                    .size(*icon_size),
 933                            )
 934                            .when(!toggled, |contents| contents.invisible());
 935
 936                        match position {
 937                            IconPosition::Start => list_item.start_slot(contents),
 938                            IconPosition::End => list_item.end_slot(contents),
 939                        }
 940                    })
 941                    .child(
 942                        h_flex()
 943                            .w_full()
 944                            .justify_between()
 945                            .child(label_element)
 946                            .debug_selector(|| format!("MENU_ITEM-{}", label))
 947                            .children(action.as_ref().and_then(|action| {
 948                                self.action_context
 949                                    .as_ref()
 950                                    .map(|focus| {
 951                                        KeyBinding::for_action_in(&**action, focus, window, cx)
 952                                    })
 953                                    .unwrap_or_else(|| {
 954                                        KeyBinding::for_action(&**action, window, cx)
 955                                    })
 956                                    .map(|binding| {
 957                                        div().ml_4().child(binding.disabled(*disabled)).when(
 958                                            *disabled && documentation_aside.is_some(),
 959                                            |parent| parent.invisible(),
 960                                        )
 961                                    })
 962                            }))
 963                            .when(*disabled && documentation_aside.is_some(), |parent| {
 964                                parent.child(
 965                                    Icon::new(IconName::Info)
 966                                        .size(IconSize::XSmall)
 967                                        .color(Color::Muted),
 968                                )
 969                            }),
 970                    )
 971                    .when_some(
 972                        end_slot_icon
 973                            .as_ref()
 974                            .zip(self.end_slot_action.as_ref())
 975                            .zip(end_slot_title.as_ref())
 976                            .zip(end_slot_handler.as_ref()),
 977                        |el, (((icon, action), title), handler)| {
 978                            el.end_slot({
 979                                let icon_button = IconButton::new("end-slot-icon", *icon)
 980                                    .shape(IconButtonShape::Square)
 981                                    .tooltip({
 982                                        let action_context = self.action_context.clone();
 983                                        let title = title.clone();
 984                                        let action = action.boxed_clone();
 985                                        move |window, cx| {
 986                                            action_context
 987                                                .as_ref()
 988                                                .map(|focus| {
 989                                                    Tooltip::for_action_in(
 990                                                        title.clone(),
 991                                                        &*action,
 992                                                        focus,
 993                                                        window,
 994                                                        cx,
 995                                                    )
 996                                                })
 997                                                .unwrap_or_else(|| {
 998                                                    Tooltip::for_action(
 999                                                        title.clone(),
1000                                                        &*action,
1001                                                        window,
1002                                                        cx,
1003                                                    )
1004                                                })
1005                                        }
1006                                    })
1007                                    .on_click({
1008                                        let handler = handler.clone();
1009                                        move |_, window, cx| {
1010                                            handler(None, window, cx);
1011                                            this.update(cx, |this, cx| {
1012                                                this.rebuild(window, cx);
1013                                                cx.notify();
1014                                            })
1015                                            .ok();
1016                                        }
1017                                    });
1018
1019                                if *show_end_slot_on_hover {
1020                                    div()
1021                                        .visible_on_hover("label_container")
1022                                        .child(icon_button)
1023                                        .into_any_element()
1024                                } else {
1025                                    icon_button.into_any_element()
1026                                }
1027                            })
1028                        },
1029                    )
1030                    .on_click({
1031                        let context = self.action_context.clone();
1032                        let keep_open_on_confirm = self.keep_open_on_confirm;
1033                        move |_, window, cx| {
1034                            handler(context.as_ref(), window, cx);
1035                            menu.update(cx, |menu, cx| {
1036                                menu.clicked = true;
1037                                if keep_open_on_confirm {
1038                                    menu.rebuild(window, cx);
1039                                } else {
1040                                    cx.emit(DismissEvent);
1041                                }
1042                            })
1043                            .ok();
1044                        }
1045                    }),
1046            )
1047            .into_any_element()
1048    }
1049}
1050
1051impl ContextMenuItem {
1052    fn is_selectable(&self) -> bool {
1053        match self {
1054            ContextMenuItem::Header(_)
1055            | ContextMenuItem::HeaderWithLink(_, _, _)
1056            | ContextMenuItem::Separator
1057            | ContextMenuItem::Label { .. } => false,
1058            ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
1059            ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
1060        }
1061    }
1062}
1063
1064impl Render for ContextMenu {
1065    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1066        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1067        let window_size = window.viewport_size();
1068        let rem_size = window.rem_size();
1069        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1070
1071        let aside = self.documentation_aside.clone();
1072        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1073            WithRemSize::new(ui_font_size)
1074                .occlude()
1075                .elevation_2(cx)
1076                .p_2()
1077                .overflow_hidden()
1078                .when(is_wide_window, |this| this.max_w_96())
1079                .when(!is_wide_window, |this| this.max_w_48())
1080                .child((aside.render)(cx))
1081        };
1082
1083        h_flex()
1084            .when(is_wide_window, |this| this.flex_row())
1085            .when(!is_wide_window, |this| this.flex_col())
1086            .w_full()
1087            .items_start()
1088            .gap_1()
1089            .child(div().children(aside.clone().and_then(|(_, aside)| {
1090                (aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx))
1091            })))
1092            .child(
1093                WithRemSize::new(ui_font_size)
1094                    .occlude()
1095                    .elevation_2(cx)
1096                    .flex()
1097                    .flex_row()
1098                    .child(
1099                        v_flex()
1100                            .id("context-menu")
1101                            .max_h(vh(0.75, window))
1102                            .when_some(self.fixed_width, |this, width| {
1103                                this.w(width).overflow_x_hidden()
1104                            })
1105                            .when(self.fixed_width.is_none(), |this| {
1106                                this.min_w(px(200.)).flex_1()
1107                            })
1108                            .overflow_y_scroll()
1109                            .track_focus(&self.focus_handle(cx))
1110                            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
1111                                this.cancel(&menu::Cancel, window, cx)
1112                            }))
1113                            .key_context(self.key_context.as_ref())
1114                            .on_action(cx.listener(ContextMenu::select_first))
1115                            .on_action(cx.listener(ContextMenu::handle_select_last))
1116                            .on_action(cx.listener(ContextMenu::select_next))
1117                            .on_action(cx.listener(ContextMenu::select_previous))
1118                            .on_action(cx.listener(ContextMenu::confirm))
1119                            .on_action(cx.listener(ContextMenu::cancel))
1120                            .when_some(self.end_slot_action.as_ref(), |el, action| {
1121                                el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
1122                            })
1123                            .when(!self.delayed, |mut el| {
1124                                for item in self.items.iter() {
1125                                    if let ContextMenuItem::Entry(ContextMenuEntry {
1126                                        action: Some(action),
1127                                        disabled: false,
1128                                        ..
1129                                    }) = item
1130                                    {
1131                                        el = el.on_boxed_action(
1132                                            &**action,
1133                                            cx.listener(ContextMenu::on_action_dispatch),
1134                                        );
1135                                    }
1136                                }
1137                                el
1138                            })
1139                            .child(
1140                                List::new().children(
1141                                    self.items.iter().enumerate().map(|(ix, item)| {
1142                                        self.render_menu_item(ix, item, window, cx)
1143                                    }),
1144                                ),
1145                            ),
1146                    ),
1147            )
1148            .child(div().children(aside.and_then(|(_, aside)| {
1149                (aside.side == DocumentationSide::Right).then(|| render_aside(aside, cx))
1150            })))
1151    }
1152}