#![cfg(target_os = "ios")]
#![cfg(feature = "ios-uikit-ffi")]
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
use objc2::msg_send;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, Object};
use objc2::MainThreadMarker;
use objc2_foundation::{CGPoint, CGRect, CGSize, NSString};
use objc2_ui_kit::{
UIButton, UIButtonType, UIColor, UILabel, UIPickerView, UIProgressView, UISlider, UISwitch,
UITableView, UITableViewStyle, UITextField, UIView, UIViewController, UIWindow,
};
#[derive(Clone, Copy)]
struct NativePtr(*mut std::ffi::c_void);
unsafe impl Send for NativePtr {}
unsafe impl Sync for NativePtr {}
static NATIVE_VIEWS: LazyLock<Mutex<HashMap<u64, NativePtr>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn store_native_view(widget_id: u64, view: *mut std::ffi::c_void) {
NATIVE_VIEWS.lock().unwrap().insert(widget_id, NativePtr(view));
}
pub(crate) fn get_native_view(widget_id: u64) -> Option<*mut std::ffi::c_void> {
NATIVE_VIEWS.lock().unwrap().get(&widget_id).map(|p| p.0)
}
pub(crate) fn remove_native_view(widget_id: u64) {
NATIVE_VIEWS.lock().unwrap().remove(&widget_id);
}
static BUTTON_TARGETS: LazyLock<Mutex<HashMap<u64, Retained<ButtonTarget>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn remove_button_target(widget_id: u64) {
BUTTON_TARGETS.lock().unwrap().remove(&widget_id);
}
static PARENT_MAP: LazyLock<Mutex<HashMap<u64, u64>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn set_parent(widget_id: u64, parent_id: u64) {
PARENT_MAP.lock().unwrap().insert(widget_id, parent_id);
}
#[allow(dead_code)]
pub(crate) fn get_parent(widget_id: u64) -> Option<u64> {
PARENT_MAP.lock().unwrap().get(&widget_id).copied()
}
pub(crate) fn add_as_subview(widget_id: u64, parent_id: u64) {
let views = NATIVE_VIEWS.lock().unwrap();
let Some(parent_ptr) = views.get(&parent_id).map(|p| p.0) else {
return;
};
let Some(child_ptr) = views.get(&widget_id).map(|c| c.0) else {
return;
};
drop(views);
unsafe {
let parent: *mut Object = parent_ptr as *mut Object;
let root_vc: *mut Object = msg_send![parent, rootViewController];
if root_vc.is_null() {
return;
}
let content_view: *mut Object = msg_send![root_vc, view];
if content_view.is_null() {
return;
}
let child: *mut Object = child_ptr as *mut Object;
let _: () = msg_send![content_view, addSubview: child];
}
}
use std::collections::VecDeque;
static BUTTON_EVENT_QUEUE: LazyLock<Mutex<VecDeque<u64>>> =
LazyLock::new(|| Mutex::new(VecDeque::new()));
pub(crate) fn drain_button_events() -> Vec<u64> {
let mut queue = BUTTON_EVENT_QUEUE.lock().unwrap();
queue.drain(..).collect()
}
use objc2::declare_class;
use objc2::runtime::NSObject;
use objc2::sel;
declare_class!(
struct ButtonTarget {
widget_id: u64,
}
unsafe impl ClassType for ButtonTarget {
type Super = NSObject;
}
unsafe impl ButtonTarget {
#[sel(buttonTapped:)]
fn handle_tap(&self, _sender: &NSObject) {
BUTTON_EVENT_QUEUE.lock().unwrap().push_back(self.widget_id);
}
}
);
impl ButtonTarget {
fn new(mtm: MainThreadMarker, widget_id: u64) -> Retained<Self> {
let obj = unsafe { mtm.alloc() };
let obj = obj.set_ivar(widget_id);
unsafe { obj.init() }
}
}
pub(crate) fn wire_button_action(widget_id: u64) {
let Some(ptr) = get_native_view(widget_id) else {
return;
};
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let target = ButtonTarget::new(mtm, widget_id);
let action_sel = sel!(buttonTapped:);
unsafe {
let button: *mut Object = ptr as *mut Object;
let _: () = msg_send![button,
addTarget: &*target
action: action_sel
forControlEvents: 64u64
];
}
BUTTON_TARGETS.lock().unwrap().insert(widget_id, target);
}
fn make_rect(x: i32, y: i32, width: u32, height: u32) -> CGRect {
CGRect::new(
CGPoint::new(x as f64, y as f64),
CGSize::new(width.max(1) as f64, height.max(1) as f64),
)
}
pub(crate) fn create_ui_window(
mtm: MainThreadMarker,
title: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIWindow> {
let frame = make_rect(0, 0, width, height);
let window = unsafe { UIWindow::initWithFrame(mtm.alloc(), frame) };
window.setBackgroundColor(UIColor::white());
let vc = unsafe { UIViewController::initWithNibName_bundle(mtm.alloc(), None, None) };
window.setRootViewController(Some(&vc));
window.makeKeyAndVisible();
window.setAccessibilityLabel(&NSString::from_str(title));
window
}
pub(crate) fn create_ui_button(
mtm: MainThreadMarker,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIButton> {
let frame = make_rect(x, y, width, height);
let button = unsafe { UIButton::initWithFrame(mtm.alloc(), frame) };
button.setTitle(&NSString::from_str(text));
button.setButtonType(UIButtonType::System);
button
}
pub(crate) fn create_ui_label(
mtm: MainThreadMarker,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UILabel> {
let frame = make_rect(x, y, width, height);
let label = unsafe { UILabel::initWithFrame(mtm.alloc(), frame) };
label.setText(&NSString::from_str(text));
label
}
pub(crate) fn create_ui_checkbox(
mtm: MainThreadMarker,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UISwitch> {
let frame = make_rect(x, y, width, height);
let switch_ctl = unsafe { UISwitch::initWithFrame(mtm.alloc(), frame) };
switch_ctl.setAccessibilityLabel(&NSString::from_str(text));
switch_ctl
}
pub(crate) fn create_ui_line_edit(
mtm: MainThreadMarker,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UITextField> {
let frame = make_rect(x, y, width, height);
let text_field = unsafe { UITextField::initWithFrame(mtm.alloc(), frame) };
text_field.setText(&NSString::from_str(text));
text_field
}
pub(crate) fn create_ui_radio_button(
mtm: MainThreadMarker,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIButton> {
let frame = make_rect(x, y, width, height);
let button = unsafe { UIButton::initWithFrame(mtm.alloc(), frame) };
button.setTitle(&NSString::from_str(text));
button.setButtonType(UIButtonType::System);
button
}
pub(crate) fn create_ui_slider(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UISlider> {
let frame = make_rect(x, y, width, height);
let slider = unsafe { UISlider::initWithFrame(mtm.alloc(), frame) };
slider.setMinimumValue(0.0);
slider.setMaximumValue(100.0);
slider
}
pub(crate) fn create_ui_progress_bar(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIProgressView> {
let frame = make_rect(x, y, width, height);
let progress = unsafe { UIProgressView::initWithFrame(mtm.alloc(), frame) };
progress
}
pub(crate) fn create_ui_combo_box(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIPickerView> {
let frame = make_rect(x, y, width, height);
let picker = unsafe { UIPickerView::initWithFrame(mtm.alloc(), frame) };
picker
}
pub(crate) fn create_ui_list_box(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UITableView> {
let frame = make_rect(x, y, width, height);
let table =
unsafe { UITableView::initWithFrame_style(mtm.alloc(), frame, UITableViewStyle::Plain) };
table
}
pub(crate) fn create_ui_panel(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIView> {
let frame = make_rect(x, y, width, height);
let panel = unsafe { UIView::initWithFrame(mtm.alloc(), frame) };
panel.setBackgroundColor(UIColor::clear());
panel
}
pub(crate) fn create_ui_scroll(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<Object> {
let frame = make_rect(x, y, width, height);
unsafe {
let cls = AnyClass::get(c"UIScrollView").unwrap();
let scroll: Retained<Object> = msg_send![cls, alloc];
let scroll: Retained<Object> = msg_send![scroll, initWithFrame: frame];
let yes: u8 = 1;
let _: () = msg_send![&*scroll, setScrollEnabled: yes];
let _: () = msg_send![&*scroll, setContentSize: frame.size];
scroll
}
}
pub(crate) fn create_ui_alert(_mtm: MainThreadMarker, title: &str, text: &str) -> Retained<Object> {
unsafe {
let cls = AnyClass::get(c"UIAlertController").unwrap();
let title_str = NSString::from_str(title);
let text_str = NSString::from_str(text);
let alert: Retained<Object> = msg_send![cls,
alertControllerWithTitle: &*title_str
message: &*text_str
preferredStyle: 1u64
];
let action_cls = AnyClass::get(c"UIAlertAction").unwrap();
let ok_str = NSString::from_str("OK");
let action: Retained<Object> = msg_send![action_cls,
actionWithTitle: &*ok_str
style: 0u64
handler: 0u64 as *mut Object
];
let _: () = msg_send![&*alert, addAction: &*action];
alert
}
}
pub(crate) fn create_ui_stack(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<Object> {
let frame = make_rect(x, y, width, height);
unsafe {
let cls = AnyClass::get(c"UIStackView").unwrap();
let stack: Retained<Object> = msg_send![cls, alloc];
let stack: Retained<Object> = msg_send![stack, initWithFrame: frame];
let axis: u64 = 1; let _: () = msg_send![&*stack, setAxis: axis];
let spacing: f64 = 8.0;
let _: () = msg_send![&*stack, setSpacing: spacing];
stack
}
}
pub(crate) fn create_ui_spinner(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<Object> {
let frame = make_rect(x, y, width, height);
unsafe {
let cls = AnyClass::get(c"UIActivityIndicatorView").unwrap();
let spinner: Retained<Object> = msg_send![cls, alloc];
let style: u64 = 100; let spinner: Retained<Object> = msg_send![spinner, initWithActivityIndicatorStyle: style];
let _: () = msg_send![&*spinner, setFrame: frame];
let yes: u8 = 1;
let _: () = msg_send![&*spinner, startAnimating];
spinner
}
}