Skip to main content

sequoia_elements/
context_menu.rs

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    /// Opens the menu at a screen-space point, without waiting for a click.
62    ///
63    /// Right-clicking the wrapped child also opens the menu; this method is for
64    /// keyboard shortcuts, toolbar buttons, and tests.
65    pub fn open_at(&self, pos: Point) {
66        self.open_at.set(Some(pos));
67        self.popup.open();
68    }
69
70    /// Closes the menu if it is open.
71    pub fn close(&self) {
72        self.popup.close();
73    }
74
75    /// Whether the menu is currently open.
76    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}