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