Skip to main content

freya_components/
context_menu.rs

1use freya_core::{
2    integration::ScopeId,
3    prelude::*,
4};
5use torin::prelude::CursorPoint;
6
7use crate::menu::Menu;
8
9#[derive(Clone, Copy, PartialEq)]
10pub(crate) enum ContextMenuCloseRequest {
11    None,
12    Pending,
13}
14
15/// Context for managing a global context menu.
16///
17/// # Example
18///
19/// ```rust
20/// # use freya::prelude::*;
21/// fn app() -> impl IntoElement {
22///     rect()
23///         .on_secondary_down(move |e: Event<PressEventData>| {
24///             ContextMenu::open_from_event(
25///                 &e,
26///                 Menu::new().child(MenuButton::new().child("Option 1")),
27///             );
28///         })
29///         .child("Right click to open menu")
30/// }
31/// ```
32#[derive(Clone, Copy, PartialEq)]
33pub struct ContextMenu {
34    pub(crate) location: State<CursorPoint>,
35    pub(crate) menu: State<Option<(CursorPoint, Menu)>>,
36    pub(crate) close_request: State<ContextMenuCloseRequest>,
37}
38
39impl ContextMenu {
40    pub fn get() -> Self {
41        match try_consume_root_context() {
42            Some(rt) => rt,
43            None => {
44                let context_menu_state = ContextMenu {
45                    location: State::create_in_scope(CursorPoint::default(), ScopeId::ROOT),
46                    menu: State::create_in_scope(None, ScopeId::ROOT),
47                    close_request: State::create_in_scope(
48                        ContextMenuCloseRequest::None,
49                        ScopeId::ROOT,
50                    ),
51                };
52                provide_context_for_scope_id(context_menu_state, ScopeId::ROOT);
53                context_menu_state
54            }
55        }
56    }
57
58    pub fn is_open() -> bool {
59        Self::get().menu.read().is_some()
60    }
61
62    /// Open the context menu with the given menu.
63    /// Prefer using [`ContextMenu::open_from_event`] instead as it correctly handles
64    /// the close behavior based on the source event.
65    pub fn open(menu: Menu) {
66        let mut this = Self::get();
67        this.menu.set(Some(((this.location)(), menu)));
68        this.close_request.set(ContextMenuCloseRequest::None);
69    }
70
71    /// Open the context menu with the given menu, using the source event to determine
72    /// the close behavior. When opened from a primary button (left click) press event,
73    /// the first close request is consumed to prevent the menu from closing immediately.
74    /// When opened from a secondary button (right click) down event, the menu can be
75    /// closed with a single click.
76    pub fn open_from_event(event: &Event<PressEventData>, menu: Menu) {
77        let mut this = Self::get();
78        this.menu.set(Some(((this.location)(), menu)));
79
80        let close_request = match event.data() {
81            PressEventData::Mouse(mouse) if mouse.button == Some(MouseButton::Left) => {
82                ContextMenuCloseRequest::Pending
83            }
84            _ => ContextMenuCloseRequest::None,
85        };
86        this.close_request.set(close_request);
87    }
88
89    pub fn close() {
90        Self::get().menu.set(None);
91    }
92}