1use crate::{
2 h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
3};
4use gpui::{
5 px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
6 IntoElement, Render, View, VisualContext,
7};
8use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
9use std::rc::Rc;
10
11pub enum ContextMenuItem {
12 Separator,
13 Header(SharedString),
14 Entry {
15 label: SharedString,
16 handler: Rc<dyn Fn(&mut WindowContext)>,
17 key_binding: Option<KeyBinding>,
18 },
19}
20
21pub struct ContextMenu {
22 items: Vec<ContextMenuItem>,
23 focus_handle: FocusHandle,
24 selected_index: Option<usize>,
25}
26
27impl FocusableView for ContextMenu {
28 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
29 self.focus_handle.clone()
30 }
31}
32
33impl EventEmitter<DismissEvent> for ContextMenu {}
34
35impl ContextMenu {
36 pub fn build(
37 cx: &mut WindowContext,
38 f: impl FnOnce(Self, &mut WindowContext) -> Self,
39 ) -> View<Self> {
40 // let handle = cx.view().downgrade();
41 cx.build_view(|cx| {
42 f(
43 Self {
44 items: Default::default(),
45 focus_handle: cx.focus_handle(),
46 selected_index: None,
47 },
48 cx,
49 )
50 })
51 }
52
53 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
54 self.items.push(ContextMenuItem::Header(title.into()));
55 self
56 }
57
58 pub fn separator(mut self) -> Self {
59 self.items.push(ContextMenuItem::Separator);
60 self
61 }
62
63 pub fn entry(
64 mut self,
65 label: impl Into<SharedString>,
66 on_click: impl Fn(&mut WindowContext) + 'static,
67 ) -> Self {
68 self.items.push(ContextMenuItem::Entry {
69 label: label.into(),
70 handler: Rc::new(on_click),
71 key_binding: None,
72 });
73 self
74 }
75
76 pub fn action(
77 mut self,
78 label: impl Into<SharedString>,
79 action: Box<dyn Action>,
80 cx: &mut WindowContext,
81 ) -> Self {
82 self.items.push(ContextMenuItem::Entry {
83 label: label.into(),
84 key_binding: KeyBinding::for_action(&*action, cx),
85 handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
86 });
87 self
88 }
89
90 pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
91 if let Some(ContextMenuItem::Entry { handler, .. }) =
92 self.selected_index.and_then(|ix| self.items.get(ix))
93 {
94 (handler)(cx)
95 }
96 cx.emit(DismissEvent);
97 }
98
99 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
100 cx.emit(DismissEvent);
101 }
102
103 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
104 self.selected_index = self.items.iter().position(|item| item.is_selectable());
105 cx.notify();
106 }
107
108 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
109 for (ix, item) in self.items.iter().enumerate().rev() {
110 if item.is_selectable() {
111 self.selected_index = Some(ix);
112 cx.notify();
113 break;
114 }
115 }
116 }
117
118 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
119 if let Some(ix) = self.selected_index {
120 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
121 if item.is_selectable() {
122 self.selected_index = Some(ix);
123 cx.notify();
124 break;
125 }
126 }
127 } else {
128 self.select_first(&Default::default(), cx);
129 }
130 }
131
132 pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
133 if let Some(ix) = self.selected_index {
134 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
135 if item.is_selectable() {
136 self.selected_index = Some(ix);
137 cx.notify();
138 break;
139 }
140 }
141 } else {
142 self.select_last(&Default::default(), cx);
143 }
144 }
145}
146
147impl ContextMenuItem {
148 fn is_selectable(&self) -> bool {
149 matches!(self, Self::Entry { .. })
150 }
151}
152
153impl Render for ContextMenu {
154 type Element = Div;
155
156 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
157 div().elevation_2(cx).flex().flex_row().child(
158 v_stack()
159 .min_w(px(200.))
160 .track_focus(&self.focus_handle)
161 .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
162 .key_context("menu")
163 .on_action(cx.listener(ContextMenu::select_first))
164 .on_action(cx.listener(ContextMenu::select_last))
165 .on_action(cx.listener(ContextMenu::select_next))
166 .on_action(cx.listener(ContextMenu::select_prev))
167 .on_action(cx.listener(ContextMenu::confirm))
168 .on_action(cx.listener(ContextMenu::cancel))
169 .flex_none()
170 .child(
171 List::new().children(self.items.iter().enumerate().map(
172 |(ix, item)| match item {
173 ContextMenuItem::Separator => ListSeparator.into_any_element(),
174 ContextMenuItem::Header(header) => {
175 ListSubHeader::new(header.clone()).into_any_element()
176 }
177 ContextMenuItem::Entry {
178 label: entry,
179 handler: callback,
180 key_binding,
181 } => {
182 let callback = callback.clone();
183 let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
184
185 ListItem::new(entry.clone())
186 .child(
187 h_stack()
188 .w_full()
189 .justify_between()
190 .child(Label::new(entry.clone()))
191 .children(
192 key_binding
193 .clone()
194 .map(|binding| div().ml_1().child(binding)),
195 ),
196 )
197 .selected(Some(ix) == self.selected_index)
198 .on_click(move |event, cx| {
199 callback(cx);
200 dismiss(event, cx)
201 })
202 .into_any_element()
203 }
204 },
205 )),
206 ),
207 )
208 }
209}