1use std::collections::HashMap;
5use std::fmt::{self, Debug};
6
7use inset_embedder::{Rect, SystemContextMenuItem};
8use inset_foundation::{App, Handle, Listener};
9
10use crate::text_input::TextInput;
11
12#[derive(Clone, Debug)]
17pub enum IOSSystemContextMenuItemData {
18 Copy,
20 Cut,
22 Paste,
24 SelectAll,
26 LookUp {
28 title: String,
30 },
31 SearchWeb {
33 title: String,
35 },
36 Share {
38 title: String,
40 },
41 LiveText,
43 Custom {
45 title: String,
47 on_pressed: Listener,
49 callback_id: String,
51 },
52}
53
54impl IOSSystemContextMenuItemData {
55 pub fn title(&self) -> Option<&str> {
60 match self {
61 Self::LookUp { title }
62 | Self::SearchWeb { title }
63 | Self::Share { title }
64 | Self::Custom { title, .. } => Some(title.as_str()),
65 _ => None,
66 }
67 }
68
69 fn to_host(&self) -> SystemContextMenuItem {
70 match self {
71 Self::Copy => SystemContextMenuItem::Copy,
72 Self::Cut => SystemContextMenuItem::Cut,
73 Self::Paste => SystemContextMenuItem::Paste,
74 Self::SelectAll => SystemContextMenuItem::SelectAll,
75 Self::LookUp { title } => SystemContextMenuItem::LookUp {
76 title: title.clone(),
77 },
78 Self::SearchWeb { title } => SystemContextMenuItem::SearchWeb {
79 title: title.clone(),
80 },
81 Self::Share { title } => SystemContextMenuItem::Share {
82 title: title.clone(),
83 },
84 Self::LiveText => SystemContextMenuItem::LiveText,
85 Self::Custom {
86 title, callback_id, ..
87 } => SystemContextMenuItem::Custom {
88 id: callback_id.clone(),
89 title: title.clone(),
90 },
91 }
92 }
93}
94
95impl PartialEq for IOSSystemContextMenuItemData {
96 fn eq(&self, other: &IOSSystemContextMenuItemData) -> bool {
97 match (self, other) {
98 (Self::Copy, Self::Copy)
99 | (Self::Cut, Self::Cut)
100 | (Self::Paste, Self::Paste)
101 | (Self::SelectAll, Self::SelectAll)
102 | (Self::LiveText, Self::LiveText) => true,
103 (Self::LookUp { title: a }, Self::LookUp { title: b })
104 | (Self::SearchWeb { title: a }, Self::SearchWeb { title: b })
105 | (Self::Share { title: a }, Self::Share { title: b }) => a == b,
106 (
107 Self::Custom {
108 title: a,
109 on_pressed: a_cb,
110 callback_id: a_id,
111 },
112 Self::Custom {
113 title: b,
114 on_pressed: b_cb,
115 callback_id: b_id,
116 },
117 ) => a == b && a_cb == b_cb && a_id == b_id,
118 _ => false,
119 }
120 }
121}
122
123#[derive(Default)]
126struct SystemContextMenuRegistry {
127 last_shown: Option<Handle<SystemContextMenuController>>,
128}
129
130impl SystemContextMenuRegistry {
131 fn instance(app: &mut App) -> Handle<SystemContextMenuRegistry> {
132 app.singleton::<SystemContextMenuRegistry>()
133 }
134}
135
136pub struct SystemContextMenuController {
149 pub on_system_hide: Option<Listener>,
151 last_target_rect: Option<Rect>,
152 last_items: Option<Vec<IOSSystemContextMenuItemData>>,
153 hidden_by_system: bool,
154 disposed: bool,
155 custom_action_callbacks: HashMap<String, Listener>,
156}
157
158impl Debug for SystemContextMenuController {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.debug_struct("SystemContextMenuController")
161 .field("hidden_by_system", &self.hidden_by_system)
162 .field("disposed", &self.disposed)
163 .finish_non_exhaustive()
164 }
165}
166
167impl SystemContextMenuController {
168 pub fn new(app: &mut App, on_system_hide: Option<Listener>) -> Handle<Self> {
172 app.create(SystemContextMenuController {
173 on_system_hide,
174 last_target_rect: None,
175 last_items: None,
176 hidden_by_system: false,
177 disposed: false,
178 custom_action_callbacks: HashMap::new(),
179 })
180 }
181
182 fn is_visible(self: Handle<Self>, app: &mut App) -> bool {
183 let registry = SystemContextMenuRegistry::instance(app);
184 app.get(registry).last_shown == Some(self) && !app.get(self).hidden_by_system
185 }
186
187 pub fn handle_system_hide(self: Handle<Self>, app: &mut App) {
189 debug_assert!(!app.get(self).disposed);
190 if app.get(self).disposed || !self.is_visible(app) {
191 return;
192 }
193 let registry = SystemContextMenuRegistry::instance(app);
194 if app.get(registry).last_shown == Some(self) {
195 app.get_mut(registry).last_shown = None;
196 }
197 app.get_mut(self).hidden_by_system = true;
198 app.get_mut(self).custom_action_callbacks.clear();
199 if let Some(on_hide) = app.get(self).on_system_hide.clone() {
200 on_hide.call(app);
201 }
202 }
203
204 pub fn handle_custom_context_menu_action(self: Handle<Self>, app: &mut App, callback_id: &str) {
206 let callback = app
207 .get(self)
208 .custom_action_callbacks
209 .get(callback_id)
210 .cloned();
211 debug_assert!(
212 callback.is_some(),
213 "Custom action callback not found for id: {callback_id}. \
214 This may indicate that the menu item was not properly registered."
215 );
216 if let Some(callback) = callback {
217 callback.call(app);
218 }
219 }
220
221 #[deprecated(note = "Use `show_with_items` instead.")]
226 pub fn show(self: Handle<Self>, app: &mut App, target_rect: Rect) {
227 debug_assert!(!app.get(self).disposed);
228 debug_assert!(
229 TextInput::instance(app).current_connection(app).is_some(),
230 "Currently, the system context menu can only be shown for an active text input connection"
231 );
232 if self.is_visible(app) && app.get(self).last_target_rect == Some(target_rect) {
233 return;
234 }
235 debug_assert!(
236 {
237 let registry = SystemContextMenuRegistry::instance(app);
238 let last = app.get(registry).last_shown;
239 last.is_none()
240 || last == Some(self)
241 || last.is_some_and(|last| !last.is_visible(app))
242 },
243 "Attempted to show while another instance was still visible."
244 );
245 let registry = SystemContextMenuRegistry::instance(app);
246 app.get_mut(self).last_target_rect = Some(target_rect);
247 app.get_mut(registry).last_shown = Some(self);
248 app.get_mut(self).hidden_by_system = false;
249 if let Some(menu) = app.platform().system_context_menu() {
250 menu.show(target_rect, None);
251 }
252 }
253
254 pub fn show_with_items(
257 self: Handle<Self>,
258 app: &mut App,
259 target_rect: Rect,
260 items: Vec<IOSSystemContextMenuItemData>,
261 ) {
262 debug_assert!(!app.get(self).disposed);
263 debug_assert!(!items.is_empty());
264 debug_assert!(
265 TextInput::instance(app).current_connection(app).is_some(),
266 "Currently, the system context menu can only be shown for an active text input connection"
267 );
268 if self.is_visible(app)
269 && app.get(self).last_target_rect == Some(target_rect)
270 && app.get(self).last_items.as_ref() == Some(&items)
271 {
272 return;
273 }
274 debug_assert!(
275 {
276 let registry = SystemContextMenuRegistry::instance(app);
277 let last = app.get(registry).last_shown;
278 last.is_none()
279 || last == Some(self)
280 || last.is_some_and(|last| !last.is_visible(app))
281 },
282 "Attempted to show while another instance was still visible."
283 );
284
285 app.get_mut(self).custom_action_callbacks.clear();
286 for item in &items {
287 if let IOSSystemContextMenuItemData::Custom {
288 callback_id,
289 on_pressed,
290 ..
291 } = item
292 {
293 app.get_mut(self)
294 .custom_action_callbacks
295 .insert(callback_id.clone(), on_pressed.clone());
296 }
297 }
298 let host_items: Vec<SystemContextMenuItem> =
299 items.iter().map(|item| item.to_host()).collect();
300 app.get_mut(self).last_target_rect = Some(target_rect);
301 app.get_mut(self).last_items = Some(items);
302 let registry = SystemContextMenuRegistry::instance(app);
303 app.get_mut(registry).last_shown = Some(self);
304 app.get_mut(self).hidden_by_system = false;
305 if let Some(menu) = app.platform().system_context_menu() {
306 menu.show(target_rect, Some(&host_items));
307 }
308 }
309
310 pub fn hide(self: Handle<Self>, app: &mut App) {
312 debug_assert!(!app.get(self).disposed);
313 let registry = SystemContextMenuRegistry::instance(app);
314 if app.get(registry).last_shown != Some(self) {
315 return;
316 }
317 app.get_mut(registry).last_shown = None;
318 app.get_mut(self).custom_action_callbacks.clear();
319 if let Some(menu) = app.platform().system_context_menu() {
320 menu.hide();
321 }
322 }
323
324 pub fn dispose(self: Handle<Self>, app: &mut App) {
326 debug_assert!(!app.get(self).disposed);
327 self.hide(app);
328 app.get_mut(self).disposed = true;
329 }
330
331 pub fn dispatch_system_hide(app: &mut App) {
333 let registry = SystemContextMenuRegistry::instance(app);
334 if let Some(shown) = app.get(registry).last_shown {
335 shown.handle_system_hide(app);
336 }
337 }
338
339 pub fn dispatch_custom_action(app: &mut App, callback_id: &str) {
341 let registry = SystemContextMenuRegistry::instance(app);
342 if let Some(shown) = app.get(registry).last_shown {
343 shown.handle_custom_context_menu_action(app, callback_id);
344 }
345 }
346}