1use gpui::{
2 elements::*, geometry::vector::Vector2F, platform::CursorStyle, Action, Axis, Entity,
3 RenderContext, SizeConstraint, View, ViewContext,
4};
5use settings::Settings;
6
7pub enum ContextMenuItem {
8 Item {
9 label: String,
10 action: Box<dyn Action>,
11 },
12 Separator,
13}
14
15impl ContextMenuItem {
16 pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
17 Self::Item {
18 label: label.to_string(),
19 action: Box::new(action),
20 }
21 }
22
23 pub fn separator() -> Self {
24 Self::Separator
25 }
26}
27
28pub struct ContextMenu {
29 position: Vector2F,
30 items: Vec<ContextMenuItem>,
31 selected_index: Option<usize>,
32 visible: bool,
33}
34
35impl Entity for ContextMenu {
36 type Event = ();
37}
38
39impl View for ContextMenu {
40 fn ui_name() -> &'static str {
41 "ContextMenu"
42 }
43
44 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
45 if !self.visible {
46 return Empty::new().boxed();
47 }
48
49 // Render the menu once at minimum width.
50 let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
51 let expanded_menu = self
52 .render_menu(cx)
53 .constrained()
54 .dynamically(move |constraint, cx| {
55 SizeConstraint::strict_along(
56 Axis::Horizontal,
57 collapsed_menu.layout(constraint, cx).x(),
58 )
59 })
60 .boxed();
61
62 Overlay::new(expanded_menu)
63 .with_abs_position(self.position)
64 .boxed()
65 }
66
67 fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
68 self.visible = false;
69 cx.notify();
70 }
71}
72
73impl ContextMenu {
74 pub fn new() -> Self {
75 Self {
76 position: Default::default(),
77 items: Default::default(),
78 selected_index: Default::default(),
79 visible: false,
80 }
81 }
82
83 pub fn show(
84 &mut self,
85 position: Vector2F,
86 items: impl IntoIterator<Item = ContextMenuItem>,
87 cx: &mut ViewContext<Self>,
88 ) {
89 let mut items = items.into_iter().peekable();
90 assert!(items.peek().is_some(), "must have at least one item");
91 self.items = items.collect();
92 self.position = position;
93 self.visible = true;
94 cx.focus_self();
95 cx.notify();
96 }
97
98 fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
99 let style = cx.global::<Settings>().theme.context_menu.clone();
100 Flex::row()
101 .with_child(
102 Flex::column()
103 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
104 match item {
105 ContextMenuItem::Item { label, .. } => {
106 let style = style.item.style_for(
107 &Default::default(),
108 Some(ix) == self.selected_index,
109 );
110 Label::new(label.to_string(), style.label.clone()).boxed()
111 }
112 ContextMenuItem::Separator => Empty::new()
113 .collapsed()
114 .contained()
115 .with_style(style.separator)
116 .constrained()
117 .with_height(1.)
118 .boxed(),
119 }
120 }))
121 .boxed(),
122 )
123 .with_child(
124 Flex::column()
125 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
126 match item {
127 ContextMenuItem::Item { action, .. } => {
128 let style = style.item.style_for(
129 &Default::default(),
130 Some(ix) == self.selected_index,
131 );
132 KeystrokeLabel::new(
133 action.boxed_clone(),
134 style.keystroke.container,
135 style.keystroke.text.clone(),
136 )
137 .boxed()
138 }
139 ContextMenuItem::Separator => Empty::new()
140 .collapsed()
141 .constrained()
142 .with_height(1.)
143 .contained()
144 .with_style(style.separator)
145 .boxed(),
146 }
147 }))
148 .boxed(),
149 )
150 .contained()
151 .with_style(style.container)
152 }
153
154 fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
155 enum Tag {}
156 let style = cx.global::<Settings>().theme.context_menu.clone();
157 Flex::column()
158 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
159 match item {
160 ContextMenuItem::Item { label, action } => {
161 let action = action.boxed_clone();
162 MouseEventHandler::new::<Tag, _, _>(ix, cx, |state, _| {
163 let style =
164 style.item.style_for(state, Some(ix) == self.selected_index);
165 Flex::row()
166 .with_child(
167 Label::new(label.to_string(), style.label.clone()).boxed(),
168 )
169 .with_child({
170 KeystrokeLabel::new(
171 action.boxed_clone(),
172 style.keystroke.container,
173 style.keystroke.text.clone(),
174 )
175 .flex_float()
176 .boxed()
177 })
178 .contained()
179 .with_style(style.container)
180 .boxed()
181 })
182 .with_cursor_style(CursorStyle::PointingHand)
183 .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
184 .boxed()
185 }
186 ContextMenuItem::Separator => Empty::new()
187 .constrained()
188 .with_height(1.)
189 .contained()
190 .with_style(style.separator)
191 .boxed(),
192 }
193 }))
194 .contained()
195 .with_style(style.container)
196 }
197}