freya_components/
context_menu.rs1use 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#[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 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 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 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#[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}