use std::collections::HashMap;
use std::fmt::{self, Debug};
use inset_embedder::{Rect, SystemContextMenuItem};
use inset_foundation::{App, Handle, Listener};
use crate::text_input::TextInput;
#[derive(Clone, Debug)]
pub enum IOSSystemContextMenuItemData {
Copy,
Cut,
Paste,
SelectAll,
LookUp {
title: String,
},
SearchWeb {
title: String,
},
Share {
title: String,
},
LiveText,
Custom {
title: String,
on_pressed: Listener,
callback_id: String,
},
}
impl IOSSystemContextMenuItemData {
pub fn title(&self) -> Option<&str> {
match self {
Self::LookUp { title }
| Self::SearchWeb { title }
| Self::Share { title }
| Self::Custom { title, .. } => Some(title.as_str()),
_ => None,
}
}
fn to_host(&self) -> SystemContextMenuItem {
match self {
Self::Copy => SystemContextMenuItem::Copy,
Self::Cut => SystemContextMenuItem::Cut,
Self::Paste => SystemContextMenuItem::Paste,
Self::SelectAll => SystemContextMenuItem::SelectAll,
Self::LookUp { title } => SystemContextMenuItem::LookUp {
title: title.clone(),
},
Self::SearchWeb { title } => SystemContextMenuItem::SearchWeb {
title: title.clone(),
},
Self::Share { title } => SystemContextMenuItem::Share {
title: title.clone(),
},
Self::LiveText => SystemContextMenuItem::LiveText,
Self::Custom {
title, callback_id, ..
} => SystemContextMenuItem::Custom {
id: callback_id.clone(),
title: title.clone(),
},
}
}
}
impl PartialEq for IOSSystemContextMenuItemData {
fn eq(&self, other: &IOSSystemContextMenuItemData) -> bool {
match (self, other) {
(Self::Copy, Self::Copy)
| (Self::Cut, Self::Cut)
| (Self::Paste, Self::Paste)
| (Self::SelectAll, Self::SelectAll)
| (Self::LiveText, Self::LiveText) => true,
(Self::LookUp { title: a }, Self::LookUp { title: b })
| (Self::SearchWeb { title: a }, Self::SearchWeb { title: b })
| (Self::Share { title: a }, Self::Share { title: b }) => a == b,
(
Self::Custom {
title: a,
on_pressed: a_cb,
callback_id: a_id,
},
Self::Custom {
title: b,
on_pressed: b_cb,
callback_id: b_id,
},
) => a == b && a_cb == b_cb && a_id == b_id,
_ => false,
}
}
}
#[derive(Default)]
struct SystemContextMenuRegistry {
last_shown: Option<Handle<SystemContextMenuController>>,
}
impl SystemContextMenuRegistry {
fn instance(app: &mut App) -> Handle<SystemContextMenuRegistry> {
app.singleton::<SystemContextMenuRegistry>()
}
}
pub struct SystemContextMenuController {
pub on_system_hide: Option<Listener>,
last_target_rect: Option<Rect>,
last_items: Option<Vec<IOSSystemContextMenuItemData>>,
hidden_by_system: bool,
disposed: bool,
custom_action_callbacks: HashMap<String, Listener>,
}
impl Debug for SystemContextMenuController {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SystemContextMenuController")
.field("hidden_by_system", &self.hidden_by_system)
.field("disposed", &self.disposed)
.finish_non_exhaustive()
}
}
impl SystemContextMenuController {
pub fn new(app: &mut App, on_system_hide: Option<Listener>) -> Handle<Self> {
app.create(SystemContextMenuController {
on_system_hide,
last_target_rect: None,
last_items: None,
hidden_by_system: false,
disposed: false,
custom_action_callbacks: HashMap::new(),
})
}
fn is_visible(self: Handle<Self>, app: &mut App) -> bool {
let registry = SystemContextMenuRegistry::instance(app);
app.get(registry).last_shown == Some(self) && !app.get(self).hidden_by_system
}
pub fn handle_system_hide(self: Handle<Self>, app: &mut App) {
debug_assert!(!app.get(self).disposed);
if app.get(self).disposed || !self.is_visible(app) {
return;
}
let registry = SystemContextMenuRegistry::instance(app);
if app.get(registry).last_shown == Some(self) {
app.get_mut(registry).last_shown = None;
}
app.get_mut(self).hidden_by_system = true;
app.get_mut(self).custom_action_callbacks.clear();
if let Some(on_hide) = app.get(self).on_system_hide.clone() {
on_hide.call(app);
}
}
pub fn handle_custom_context_menu_action(self: Handle<Self>, app: &mut App, callback_id: &str) {
let callback = app
.get(self)
.custom_action_callbacks
.get(callback_id)
.cloned();
debug_assert!(
callback.is_some(),
"Custom action callback not found for id: {callback_id}. \
This may indicate that the menu item was not properly registered."
);
if let Some(callback) = callback {
callback.call(app);
}
}
#[deprecated(note = "Use `show_with_items` instead.")]
pub fn show(self: Handle<Self>, app: &mut App, target_rect: Rect) {
debug_assert!(!app.get(self).disposed);
debug_assert!(
TextInput::instance(app).current_connection(app).is_some(),
"Currently, the system context menu can only be shown for an active text input connection"
);
if self.is_visible(app) && app.get(self).last_target_rect == Some(target_rect) {
return;
}
debug_assert!(
{
let registry = SystemContextMenuRegistry::instance(app);
let last = app.get(registry).last_shown;
last.is_none()
|| last == Some(self)
|| last.is_some_and(|last| !last.is_visible(app))
},
"Attempted to show while another instance was still visible."
);
let registry = SystemContextMenuRegistry::instance(app);
app.get_mut(self).last_target_rect = Some(target_rect);
app.get_mut(registry).last_shown = Some(self);
app.get_mut(self).hidden_by_system = false;
app.platform().show_system_context_menu(target_rect, None);
}
pub fn show_with_items(
self: Handle<Self>,
app: &mut App,
target_rect: Rect,
items: Vec<IOSSystemContextMenuItemData>,
) {
debug_assert!(!app.get(self).disposed);
debug_assert!(!items.is_empty());
debug_assert!(
TextInput::instance(app).current_connection(app).is_some(),
"Currently, the system context menu can only be shown for an active text input connection"
);
if self.is_visible(app)
&& app.get(self).last_target_rect == Some(target_rect)
&& app.get(self).last_items.as_ref() == Some(&items)
{
return;
}
debug_assert!(
{
let registry = SystemContextMenuRegistry::instance(app);
let last = app.get(registry).last_shown;
last.is_none()
|| last == Some(self)
|| last.is_some_and(|last| !last.is_visible(app))
},
"Attempted to show while another instance was still visible."
);
app.get_mut(self).custom_action_callbacks.clear();
for item in &items {
if let IOSSystemContextMenuItemData::Custom {
callback_id,
on_pressed,
..
} = item
{
app.get_mut(self)
.custom_action_callbacks
.insert(callback_id.clone(), on_pressed.clone());
}
}
let host_items: Vec<SystemContextMenuItem> =
items.iter().map(|item| item.to_host()).collect();
app.get_mut(self).last_target_rect = Some(target_rect);
app.get_mut(self).last_items = Some(items);
let registry = SystemContextMenuRegistry::instance(app);
app.get_mut(registry).last_shown = Some(self);
app.get_mut(self).hidden_by_system = false;
app.platform()
.show_system_context_menu(target_rect, Some(&host_items));
}
pub fn hide(self: Handle<Self>, app: &mut App) {
debug_assert!(!app.get(self).disposed);
let registry = SystemContextMenuRegistry::instance(app);
if app.get(registry).last_shown != Some(self) {
return;
}
app.get_mut(registry).last_shown = None;
app.get_mut(self).custom_action_callbacks.clear();
app.platform().hide_system_context_menu();
}
pub fn dispose(self: Handle<Self>, app: &mut App) {
debug_assert!(!app.get(self).disposed);
self.hide(app);
app.get_mut(self).disposed = true;
}
pub fn dispatch_system_hide(app: &mut App) {
let registry = SystemContextMenuRegistry::instance(app);
if let Some(shown) = app.get(registry).last_shown {
shown.handle_system_hide(app);
}
}
pub fn dispatch_custom_action(app: &mut App, callback_id: &str) {
let registry = SystemContextMenuRegistry::instance(app);
if let Some(shown) = app.get(registry).last_shown {
shown.handle_custom_context_menu_action(app, callback_id);
}
}
}