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)]
15pub(crate) enum ClosePhase {
16 PendingPress,
18 Idle,
20 CloseAllowed,
22}
23
24#[derive(Clone, Copy, PartialEq)]
45pub struct ContextMenu {
46 pub(crate) location: State<CursorPoint>,
47 pub(crate) menu: State<Option<(CursorPoint, Menu)>>,
48 pub(crate) close_phase: State<ClosePhase>,
49}
50
51impl ContextMenu {
52 #[track_caller]
56 pub fn get() -> Self {
57 try_consume_root_context()
58 .expect("ContextMenu requires a `ContextMenuViewer` in an ancestor scope")
59 }
60
61 pub fn is_open() -> bool {
62 try_consume_root_context::<Self>().is_some_and(|c| c.menu.read().is_some())
63 }
64
65 pub fn open(menu: Menu) {
67 Self::open_with_phase(menu, ClosePhase::Idle);
68 }
69
70 pub fn open_from_down(menu: Menu) {
72 Self::open_with_phase(menu, ClosePhase::PendingPress);
73 }
74
75 fn open_with_phase(menu: Menu, phase: ClosePhase) {
76 let mut this = Self::get();
77 this.menu.set(Some(((this.location)(), menu)));
78 this.close_phase.set(phase);
79 }
80
81 pub fn close() {
82 if let Some(mut this) = try_consume_root_context::<Self>() {
83 this.menu.set(None);
84 }
85 }
86}
87
88#[derive(Default, Clone, PartialEq)]
106pub struct ContextMenuViewer {
107 key: DiffKey,
108}
109
110impl KeyExt for ContextMenuViewer {
111 fn write_key(&mut self) -> &mut DiffKey {
112 &mut self.key
113 }
114}
115
116impl ContextMenuViewer {
117 pub fn new() -> Self {
118 Self::default()
119 }
120}
121
122impl ComponentOwned for ContextMenuViewer {
123 fn render(self) -> impl IntoElement {
124 let mut context = use_hook(|| {
125 try_consume_root_context::<ContextMenu>().unwrap_or_else(|| {
126 let state = ContextMenu {
127 location: State::create_in_scope(CursorPoint::default(), ScopeId::ROOT),
128 menu: State::create_in_scope(None, ScopeId::ROOT),
129 close_phase: State::create_in_scope(ClosePhase::Idle, ScopeId::ROOT),
130 };
131 provide_context_for_scope_id(state, ScopeId::ROOT);
132 state
133 })
134 });
135
136 use_side_effect(move || {
137 if !*Platform::get().is_app_focused.read() {
138 context.menu.set(None);
139 }
140 });
141
142 rect()
143 .on_global_pointer_move(move |e: Event<PointerEventData>| {
144 context.location.set(e.global_location());
145 })
146 .on_global_pointer_down(move |_: Event<PointerEventData>| {
147 if context.menu.read().is_some() {
148 let phase = match (context.close_phase)() {
149 ClosePhase::PendingPress => ClosePhase::Idle,
150 _ => ClosePhase::CloseAllowed,
151 };
152 context.close_phase.set(phase);
153 }
154 })
155 .maybe_child(context.menu.read().clone().map(|(location, menu)| {
156 let location = location.to_f32();
157 rect()
158 .layer(Layer::Overlay)
159 .position(
160 Position::new_global()
161 .left(location.x.round())
162 .top(location.y.round()),
163 )
164 .child(
165 menu.on_close(move |_| {
166 if (context.close_phase)() == ClosePhase::CloseAllowed {
167 context.menu.set(None);
168 }
169 })
170 .on_escape(move |_| {
171 context.menu.set(None);
172 }),
173 )
174 }))
175 }
176
177 fn render_key(&self) -> DiffKey {
178 self.key.clone().or(self.default_key())
179 }
180}