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