Skip to main content

freya_components/
context_menu.rs

1use freya_core::{
2    integration::ScopeId,
3    layers::Layer,
4    prelude::*,
5};
6use torin::prelude::{
7    CursorPoint,
8    Position,
9};
10
11use crate::menu::Menu;
12
13#[derive(Clone, Copy, PartialEq)]
14pub(crate) enum ContextMenuCloseRequest {
15    None,
16    Pending,
17}
18
19/// Global context menu state.
20///
21/// Requires a [`ContextMenuViewer`] in an ancestor scope.
22///
23/// # Example
24///
25/// ```rust
26/// # use freya::prelude::*;
27/// fn app() -> impl IntoElement {
28///     rect().child(ContextMenuViewer::new()).child(
29///         rect()
30///             .on_secondary_down(move |e: Event<PressEventData>| {
31///                 ContextMenu::open_from_event(
32///                     &e,
33///                     Menu::new().child(MenuButton::new().child("Option 1")),
34///                 );
35///             })
36///             .child("Right click to open menu"),
37///     )
38/// }
39/// ```
40#[derive(Clone, Copy, PartialEq)]
41pub struct ContextMenu {
42    pub(crate) location: State<CursorPoint>,
43    pub(crate) menu: State<Option<(CursorPoint, Menu)>>,
44    pub(crate) close_request: State<ContextMenuCloseRequest>,
45}
46
47impl ContextMenu {
48    /// # Panics
49    ///
50    /// Panics if no [`ContextMenuViewer`] is mounted in an ancestor scope.
51    pub fn get() -> Self {
52        try_consume_root_context()
53            .expect("ContextMenu requires a `ContextMenuViewer` in an ancestor scope")
54    }
55
56    pub fn is_open() -> bool {
57        try_consume_root_context::<Self>().is_some_and(|c| c.menu.read().is_some())
58    }
59
60    /// Open the context menu with the given menu.
61    /// Prefer using [`ContextMenu::open_from_event`] instead as it correctly handles
62    /// the close behavior based on the source event.
63    pub fn open(menu: Menu) {
64        let mut this = Self::get();
65        this.menu.set(Some(((this.location)(), menu)));
66        this.close_request.set(ContextMenuCloseRequest::None);
67    }
68
69    /// Open the context menu with the given menu, using the source event to determine
70    /// the close behavior. When opened from a primary button (left click) press event,
71    /// the first close request is consumed to prevent the menu from closing immediately.
72    /// When opened from a secondary button (right click) down event, the menu can be
73    /// closed with a single click.
74    pub fn open_from_event(event: &Event<PressEventData>, menu: Menu) {
75        let mut this = Self::get();
76        let was_already_open = this.menu.read().is_some();
77        this.menu.set(Some(((this.location)(), menu)));
78
79        let close_request = match event.data() {
80            PressEventData::Mouse(mouse)
81                if mouse.button == Some(MouseButton::Left) && !was_already_open =>
82            {
83                ContextMenuCloseRequest::Pending
84            }
85            _ => ContextMenuCloseRequest::None,
86        };
87        this.close_request.set(close_request);
88    }
89
90    pub fn close() {
91        if let Some(mut this) = try_consume_root_context::<Self>() {
92            this.menu.set(None);
93        }
94    }
95}
96
97/// Provides the [`ContextMenu`] state and renders the floating menu overlay.
98///
99/// Mount this as high up in your tree as possible (typically in your `app`
100/// component) so the rendered menu inherits styling like `font_size` from
101/// the app's root element.
102///
103/// # Example
104///
105/// ```rust
106/// # use freya::prelude::*;
107/// fn app() -> impl IntoElement {
108///     rect()
109///         .font_size(18.)
110///         .child(ContextMenuViewer::new())
111///         .child("Your app content here")
112/// }
113/// ```
114#[derive(Default, Clone, PartialEq)]
115pub struct ContextMenuViewer {
116    key: DiffKey,
117}
118
119impl KeyExt for ContextMenuViewer {
120    fn write_key(&mut self) -> &mut DiffKey {
121        &mut self.key
122    }
123}
124
125impl ContextMenuViewer {
126    pub fn new() -> Self {
127        Self::default()
128    }
129}
130
131impl ComponentOwned for ContextMenuViewer {
132    fn render(self) -> impl IntoElement {
133        let mut context = use_hook(|| {
134            try_consume_root_context::<ContextMenu>().unwrap_or_else(|| {
135                let state = ContextMenu {
136                    location: State::create_in_scope(CursorPoint::default(), ScopeId::ROOT),
137                    menu: State::create_in_scope(None, ScopeId::ROOT),
138                    close_request: State::create_in_scope(
139                        ContextMenuCloseRequest::None,
140                        ScopeId::ROOT,
141                    ),
142                };
143                provide_context_for_scope_id(state, ScopeId::ROOT);
144                state
145            })
146        });
147
148        use_side_effect(move || {
149            if !*Platform::get().is_app_focused.read() {
150                context.menu.set(None);
151                context.close_request.set(ContextMenuCloseRequest::None);
152            }
153        });
154
155        rect()
156            .on_global_pointer_move(move |e: Event<PointerEventData>| {
157                context.location.set(e.global_location());
158            })
159            .maybe_child(context.menu.read().clone().map(|(location, menu)| {
160                let location = location.to_f32();
161                rect()
162                    .layer(Layer::Overlay)
163                    .position(Position::new_global().left(location.x).top(location.y))
164                    .child(menu.on_close(move |_| match (context.close_request)() {
165                        ContextMenuCloseRequest::None => {
166                            context.close_request.set(ContextMenuCloseRequest::Pending);
167                        }
168                        ContextMenuCloseRequest::Pending => {
169                            context.menu.set(None);
170                            context.close_request.set(ContextMenuCloseRequest::None);
171                        }
172                    }))
173            }))
174    }
175
176    fn render_key(&self) -> DiffKey {
177        self.key.clone().or(self.default_key())
178    }
179}