use super::types::{IosHandleKind, IosMobilePlatform};
use crate::core::PlatformFamily;
use crate::platform::{
DropEvent, Platform, PlatformCapabilities, WidgetTriggerEvent, WidgetTriggerKind,
};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
impl Platform for IosMobilePlatform {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn backend_name(&self) -> &'static str {
"ios-state-backend"
}
fn family(&self) -> PlatformFamily {
PlatformFamily::Mobile
}
fn total_memory_mb(&self) -> Option<u64> {
crate::platform::os_probes::total_memory_mb()
}
fn is_on_battery(&self) -> bool {
crate::platform::os_probes::is_on_battery()
}
fn process_memory_utilization(&self) -> Option<f32> {
crate::platform::os_probes::process_memory_utilization()
}
fn process_cpu_utilization(&self) -> Option<f32> {
crate::platform::os_probes::process_cpu_utilization()
}
fn spawn_print_job(&self, _job_file: &std::path::Path) -> Result<(), String> {
Err("iOS printing requires UIPrintInteractionController (not bound)".to_string())
}
fn shortcut_style(&self) -> crate::shortcut::PlatformShortcutStyle {
crate::shortcut::PlatformShortcutStyle::Mac
}
#[cfg(feature = "mobile-api")]
fn mobile_extension(&self) -> Option<&dyn crate::platform::types::MobilePlatformExtension> {
Some(self)
}
fn capabilities(&self) -> PlatformCapabilities {
PlatformCapabilities {
dpi_scaling: true,
ime: true,
accessibility: true,
native_menu: false,
typed_widget_trigger: true,
}
}
fn init(&self) {
let _ = self.ios_runtime_marker();
self.runtime.initialized.store(true, Ordering::SeqCst);
}
fn run(&self) {
if !self.runtime.initialized.load(Ordering::SeqCst) {
self.init();
}
self.runtime.running.store(true, Ordering::SeqCst);
while self.runtime.running.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(16));
}
}
fn quit(&self) {
self.runtime.running.store(false, Ordering::SeqCst);
}
fn destroy_widget(&self, widget_id: u64) -> bool {
let existed = self.state.destroy_widget(widget_id);
let mut menus = self.menus.lock().expect("ios menus lock poisoned");
menus.attached_menu_bar.retain(|_window, menu_bar| *menu_bar != widget_id);
let destroyed_children = menus.menu_children.remove(&widget_id);
menus.menu_children.retain(|_parent, children| {
children.retain(|child| *child != widget_id);
!children.is_empty()
});
menus.pending_menu_events.retain(|queued| *queued != widget_id);
drop(menus);
for child in destroyed_children.into_iter().flatten() {
self.destroy_widget(child);
}
existed
}
fn create_window(&self, title: &str, x: i32, y: i32, width: u32, height: u32) -> u64 {
let id = self.insert_widget(IosHandleKind::Window, title, x, y, width, height);
#[cfg(all(target_os = "ios", feature = "ios-uikit-ffi"))]
if let Some(mtm) = objc2::MainThreadMarker::new() {
let _window = super::native::create_ui_window(mtm, title, x, y, width, height);
}
id
}
fn set_widget_text(&self, widget_id: u64, text: &str) {
let _ = self.state.set_text(widget_id, text);
}
fn set_widget_geometry(&self, widget_id: u64, x: i32, y: i32, width: u32, height: u32) {
self.state.set_geometry(widget_id, x, y, width, height);
}
fn set_widget_ime_enabled(&self, widget_id: u64, enabled: bool) -> bool {
self.state.set_ime_enabled(widget_id, enabled)
}
fn is_widget_ime_enabled(&self, widget_id: u64) -> bool {
self.state.ime_enabled(widget_id)
}
fn set_widget_accessibility_name(&self, widget_id: u64, name: &str) -> bool {
self.state.set_accessibility_name(widget_id, name)
}
fn get_widget_accessibility_name(&self, widget_id: u64) -> String {
self.state.accessibility_name(widget_id)
}
fn set_clipboard_text(&self, text: &str) -> bool {
self.state.set_clipboard_text(text)
}
fn get_clipboard_text(&self) -> String {
self.state.clipboard_text()
}
fn begin_drag(&self, source_widget_id: u64, mime: &str, payload: &[u8]) -> bool {
self.state.begin_drag(source_widget_id, mime, payload)
}
fn poll_drop_event(&self) -> Option<DropEvent> {
self.state.pop_drop_event()
}
fn inject_drop_event(&self, event: DropEvent) -> bool {
self.state.inject_drop_event(event)
}
fn create_menu_bar(&self, parent: u64, x: i32, y: i32, width: u32, height: u32) -> u64 {
if !matches!(self.kind_of(parent), Some(IosHandleKind::Window)) {
return 0;
}
self.insert_widget(IosHandleKind::MenuBar, "MenuBar", x, y, width, height)
}
fn create_menu(&self, parent: u64, text: &str, x: i32, y: i32, width: u32, height: u32) -> u64 {
if !matches!(self.kind_of(parent), Some(IosHandleKind::MenuBar) | Some(IosHandleKind::Menu))
{
return 0;
}
self.insert_widget(IosHandleKind::Menu, text, x, y, width, height)
}
fn attach_menu_bar_to_window(&self, window: u64, menu_bar: u64) -> bool {
if !matches!(self.kind_of(window), Some(IosHandleKind::Window)) {
return false;
}
if !matches!(self.kind_of(menu_bar), Some(IosHandleKind::MenuBar)) {
return false;
}
let mut menus = self.menus.lock().expect("ios menus lock poisoned");
menus.attached_menu_bar.insert(window, menu_bar);
true
}
fn menu_add_item(&self, parent_menu: u64, text: &str, _shortcut: Option<&str>) -> u64 {
if !matches!(self.kind_of(parent_menu), Some(IosHandleKind::Menu)) {
return 0;
}
let id = self.insert_widget(IosHandleKind::MenuItem, text, 0, 0, 0, 0);
let mut menus = self.menus.lock().expect("ios menus lock poisoned");
menus.menu_children.entry(parent_menu).or_default().push(id);
id
}
fn poll_menu_triggered(&self) -> Option<u64> {
self.menus.lock().expect("ios menus lock poisoned").pending_menu_events.pop_front()
}
fn inject_menu_trigger(&self, menu_item_id: u64) -> bool {
if !matches!(self.kind_of(menu_item_id), Some(IosHandleKind::MenuItem)) {
return false;
}
self.menus
.lock()
.expect("ios menus lock poisoned")
.pending_menu_events
.push_back(menu_item_id);
true
}
fn poll_widget_triggered(&self) -> Option<u64> {
self.poll_widget_trigger_event().map(|event| event.widget_id)
}
fn poll_widget_trigger_event(&self) -> Option<WidgetTriggerEvent> {
self.state.pop_widget_event()
}
fn inject_widget_trigger_event(&self, widget_id: u64, kind: WidgetTriggerKind) -> bool {
if self.kind_of(widget_id).is_none() {
return false;
}
self.state.push_widget_event(WidgetTriggerEvent { widget_id, kind });
true
}
fn show_widget(&self, widget_id: u64) {
self.state.set_visible(widget_id, true);
}
fn hide_widget(&self, widget_id: u64) {
self.state.set_visible(widget_id, false);
}
fn set_widget_enabled(&self, widget_id: u64, enabled: bool) {
self.state.set_enabled(widget_id, enabled);
}
fn is_widget_enabled(&self, widget_id: u64) -> bool {
self.state.enabled(widget_id)
}
fn set_widget_visible(&self, widget_id: u64, visible: bool) {
self.state.set_visible(widget_id, visible);
}
fn is_widget_visible(&self, widget_id: u64) -> bool {
self.state.visible(widget_id)
}
}
#[cfg(all(test, not(alloc_frugal)))]
mod tests {
use super::*;
#[test]
fn ios_platform_window_creation() {
let platform = IosMobilePlatform::new();
platform.init();
let window_id = platform.create_window("Test Window", 0, 0, 320, 568);
assert_ne!(window_id, 0);
assert_eq!(platform.backend_name(), "ios-state-backend");
assert_eq!(platform.family(), PlatformFamily::Mobile);
}
#[test]
fn control_members_report_absence_for_every_parent() {
let platform = IosMobilePlatform::new();
platform.init();
assert_eq!(platform.create_button(999, "Button", 0, 0, 80, 44), 0);
let window_id = platform.create_window("Window", 0, 0, 320, 568);
assert_ne!(window_id, 0);
assert_eq!(platform.create_button(window_id, "Button", 0, 0, 80, 44), 0);
}
#[test]
fn list_and_combo_data_members_report_failure() {
let platform = IosMobilePlatform::new();
platform.init();
let window_id = platform.create_window("Window", 0, 0, 320, 568);
assert!(!platform.list_box_add_item(window_id, "Item 1"));
assert!(!platform.list_box_clear_items(window_id));
assert!(!platform.combo_box_add_item(window_id, "Item 1"));
assert_eq!(platform.combo_box_item_count(window_id), 0);
}
#[cfg(all(feature = "serde_json", feature = "serde", widgets_unstripped))]
#[test]
fn ios_platform_state_serialization() {
let platform = IosMobilePlatform::new();
platform.init();
let _window_id = platform.create_window("Window", 0, 0, 320, 568);
let result = platform.serialize_state();
assert!(result.is_ok());
}
#[test]
fn ios_platform_reports_explicit_mobile_capabilities() {
let platform = IosMobilePlatform::new();
let caps = platform.capabilities();
assert_eq!(platform.family(), PlatformFamily::Mobile);
assert!(caps.dpi_scaling);
assert!(caps.ime);
assert!(caps.accessibility);
assert!(!caps.native_menu);
assert!(caps.typed_widget_trigger);
}
#[test]
fn host_creates_no_controls_only_a_window() {
let platform = IosMobilePlatform::new();
platform.init();
let window = platform.create_window("Window", 0, 0, 320, 568);
assert_ne!(window, 0, "the window is the one primitive the host owns");
assert_eq!(platform.kind_of(window), Some(IosHandleKind::Window));
assert_eq!(platform.create_button(window, "OK", 0, 0, 80, 44), 0);
assert_eq!(platform.create_label(window, "hi", 0, 0, 80, 44), 0);
assert_eq!(platform.create_list_box(window, 0, 0, 320, 120), 0);
assert_eq!(platform.create_combo_box(window, 0, 0, 160, 44), 0);
}
#[test]
fn ios_platform_capabilities_describe_a_self_drawn_host() {
let platform = IosMobilePlatform::new();
let caps = platform.capabilities();
assert!(!caps.native_menu);
assert!(caps.typed_widget_trigger);
}
}