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