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