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