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