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    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}