1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length, Point};
5use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
6use sequoia_layout::LayoutStyle;
7use sequoia_reactive::{Environment, Signal};
8use sequoia_style::{ContextMenuStyle, VisualState};
9use sequoia_view::{
10 Align, Anchor, AnyView, Column, Dismiss, EventCtx, OverlayEntry, OverlayLayer, PaintCtx,
11 Placement, Row, Text, Unit, View,
12};
13
14use crate::container::Container;
15use crate::popup::PopupState;
16
17#[derive(Clone)]
18enum MenuAction {
19 Plain(Rc<dyn Fn()>),
20 Env(Rc<dyn Fn(&Environment)>),
21}
22
23#[derive(Clone)]
24struct MenuRow {
25 label: String,
26 action: MenuAction,
27}
28
29pub struct ContextMenu {
30 child: AnyView,
31 items: Vec<MenuRow>,
32 popup: PopupState,
33
34 open_at: Rc<Cell<Option<Point>>>,
35 style: LayoutStyle,
36 appearance: Option<ContextMenuStyle>,
37 menu_background: Option<Color>,
38 menu_border: Option<Color>,
39 menu_radius: Option<f32>,
40 menu_text_color: Option<Color>,
41 menu_font_size: Option<f32>,
42}
43
44impl ContextMenu {
45 pub fn new(child: impl View + 'static) -> Self {
46 Self {
47 child: AnyView::new(child),
48 items: Vec::new(),
49 popup: PopupState::new(Signal::new(false)),
50 open_at: Rc::new(Cell::new(None)),
51 style: LayoutStyle::default(),
52 appearance: None,
53 menu_background: None,
54 menu_border: None,
55 menu_radius: None,
56 menu_text_color: None,
57 menu_font_size: None,
58 }
59 }
60
61 pub fn open_at(&self, pos: Point) {
66 self.open_at.set(Some(pos));
67 self.popup.open();
68 }
69
70 pub fn close(&self) {
72 self.popup.close();
73 }
74
75 pub fn is_open(&self) -> bool {
77 self.popup.is_open()
78 }
79
80 pub fn item(mut self, label: impl Into<String>, action: impl Fn() + 'static) -> Self {
81 self.items.push(MenuRow {
82 label: label.into(),
83 action: MenuAction::Plain(Rc::new(action)),
84 });
85 self
86 }
87
88 pub fn item_env(
89 mut self,
90 label: impl Into<String>,
91 action: impl Fn(&Environment) + 'static,
92 ) -> Self {
93 self.items.push(MenuRow {
94 label: label.into(),
95 action: MenuAction::Env(Rc::new(action)),
96 });
97 self
98 }
99
100 pub fn appearance(mut self, style: ContextMenuStyle) -> Self {
101 self.appearance = Some(style);
102 self
103 }
104
105 pub fn menu_background(mut self, color: Color) -> Self {
106 self.menu_background = Some(color);
107 self
108 }
109
110 pub fn menu_border(mut self, color: Color) -> Self {
111 self.menu_border = Some(color);
112 self
113 }
114
115 pub fn menu_radius(mut self, radius: f32) -> Self {
116 self.menu_radius = Some(radius);
117 self
118 }
119
120 pub fn menu_text_color(mut self, color: Color) -> Self {
121 self.menu_text_color = Some(color);
122 self
123 }
124
125 pub fn menu_font_size(mut self, size: f32) -> Self {
126 self.menu_font_size = Some(size);
127 self
128 }
129
130 pub fn width(mut self, width: impl Into<Length>) -> Self {
131 self.style = self.style.width(width.into());
132 self
133 }
134
135 pub fn height(mut self, height: impl Into<Length>) -> Self {
136 self.style = self.style.height(height.into());
137 self
138 }
139
140 pub fn min_width(mut self, v: f32) -> Self {
141 self.style = self.style.min_width(v);
142 self
143 }
144
145 pub fn max_width(mut self, v: f32) -> Self {
146 self.style = self.style.max_width(v);
147 self
148 }
149
150 pub fn min_height(mut self, v: f32) -> Self {
151 self.style = self.style.min_height(v);
152 self
153 }
154
155 pub fn max_height(mut self, v: f32) -> Self {
156 self.style = self.style.max_height(v);
157 self
158 }
159
160 pub fn flex(mut self, weight: f32) -> Self {
161 self.style = self.style.flex(weight);
162 self
163 }
164
165 pub fn expand(self) -> Self {
166 self.flex(1.0)
167 }
168
169 pub fn fill_width(mut self) -> Self {
170 self.style = self.style.width(Length::Fill(1.0));
171 self
172 }
173
174 pub fn fill_height(mut self) -> Self {
175 self.style = self.style.height(Length::Fill(1.0));
176 self
177 }
178
179 pub fn fill_size(self) -> Self {
180 self.fill_width().fill_height()
181 }
182
183 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
184 self.style = self.style.margin(margin);
185 self
186 }
187
188 pub fn margin_xy(mut self, x: f32, y: f32) -> Self {
189 self.style = self.style.margin_xy(x, y);
190 self
191 }
192
193 pub fn margin_x(mut self, x: f32) -> Self {
194 self.style = self.style.margin_x(x);
195 self
196 }
197
198 pub fn margin_y(mut self, y: f32) -> Self {
199 self.style = self.style.margin_y(y);
200 self
201 }
202
203 pub fn margin_left(mut self, v: f32) -> Self {
204 self.style = self.style.margin_left(v);
205 self
206 }
207
208 pub fn margin_right(mut self, v: f32) -> Self {
209 self.style = self.style.margin_right(v);
210 self
211 }
212
213 pub fn margin_top(mut self, v: f32) -> Self {
214 self.style = self.style.margin_top(v);
215 self
216 }
217
218 pub fn margin_bottom(mut self, v: f32) -> Self {
219 self.style = self.style.margin_bottom(v);
220 self
221 }
222
223 fn menu_view(&self) -> AnyView {
224 let popup = self.popup.clone();
225 let appearance = self.appearance.unwrap_or_default();
226 let bg = self
227 .menu_background
228 .or(appearance.background)
229 .unwrap_or(Color::hex(0x2B2B2B));
230 let border = self
231 .menu_border
232 .or(appearance.border)
233 .unwrap_or(Color::hex(0x454545));
234 let radius = self.menu_radius.or(appearance.radius).unwrap_or(4.0);
235 let font_size = self.menu_font_size.or(appearance.font_size).unwrap_or(12.0);
236 let text_color = self.menu_text_color.or(appearance.text);
237 let (pad_x, pad_y) = appearance.padding.unwrap_or((6.0, 6.0));
238
239 let mut col = Column::new().fill_width().gap(2.0);
240 for row in &self.items {
241 let (action, popup) = (row.action.clone(), popup.clone());
242 let mut label = Text::new(row.label.clone()).size(font_size);
243 if let Some(color) = text_color {
244 label = label.color(color);
245 }
246 let cell = Container::new()
247 .fill_width()
248 .background(Color::TRANSPARENT)
249 .radius(3.0)
250 .padding_xy(10.0, 7.0)
251 .child(
252 Row::new()
253 .fill_width()
254 .align_items(Align::Center)
255 .child(label),
256 );
257 let cell = match action {
258 MenuAction::Plain(f) => cell.on_click(move || {
259 f();
260 popup.close();
261 }),
262 MenuAction::Env(f) => cell.on_click_env(move |env| {
263 f(env);
264 popup.close();
265 }),
266 };
267 col = col.child(cell);
268 }
269 AnyView::new(
270 Container::new()
271 .width(240.0)
272 .background(bg)
273 .border(1.0, border)
274 .radius(radius)
275 .padding_xy(pad_x, pad_y)
276 .child(col),
277 )
278 }
279}
280
281impl View for ContextMenu {
282 fn body(&self) -> impl View + 'static {
283 self.popup.subscribe();
284 Unit
285 }
286
287 fn children_override(&self) -> Option<Vec<AnyView>> {
288 Some(vec![self.child.clone()])
289 }
290
291 fn layout_style(&self) -> LayoutStyle {
292 self.style
293 }
294
295 fn focusable(&self) -> bool {
296 true
297 }
298
299 fn has_visual_state(&self) -> bool {
300 true
301 }
302
303 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
304 CursorIcon::Default
305 }
306
307 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
308 match event {
309 Event::Pointer(PointerEvent::Down {
310 button: PointerButton::Right,
311 pos,
312 ..
313 }) => {
314 if ctx.phase == EventPhase::Capture || self.items.is_empty() {
315 return;
316 }
317
318 self.open_at.set(Some(*pos));
319 self.popup.open();
320 ctx.request_focus();
321 ctx.handled();
322 }
323 Event::Key(key) => {
324 if ctx.phase != EventPhase::Target || !ctx.state.focused {
325 return;
326 }
327 if key.key == Key::ESCAPE {
328 self.popup.close_in(ctx);
329 }
330 }
331 _ => {}
332 }
333 }
334
335 fn on_unmount(&self) {
336 self.popup.forget();
337 }
338
339 fn paint_self(&self, ctx: &mut PaintCtx) {
340 if self.items.is_empty() {
341 return;
342 }
343
344 if !self.popup.is_open() {
345 self.open_at.set(None);
346 }
347 let menu = self.menu_view();
348 let open_at = self.open_at.clone();
349 self.popup.sync_show(ctx, move || {
350 let anchor = open_at.get().map(Anchor::Point).unwrap_or(Anchor::Cursor);
351 OverlayEntry::new(menu)
352 .anchor(anchor)
353 .placement(Placement::Cursor)
354 .layer(OverlayLayer::Menu)
355 .dismiss(Dismiss::OutsideClick)
356 });
357 }
358}