#![cfg(target_os = "ios")]
#![cfg(feature = "ios-uikit-ffi")]
#![allow(dead_code)]
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
use objc2::rc::{Allocated, Retained};
use objc2::runtime::{AnyClass, AnyObject, NSObject};
use objc2::MainThreadMarker;
use objc2::{msg_send, DefinedClass};
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::NSString;
use objc2_ui_kit::{
NSObjectUIAccessibility, UIActivityIndicatorView, UIActivityIndicatorViewStyle, UIButton,
UIColor, UIControlState, UILabel, UIPickerView, UIProgressView, UIScrollView, UISlider,
UIStackView, UISwitch, UITableView, UITableViewStyle, UITextField, UIView, UIViewController,
UIWindow,
};
#[derive(Clone, Copy)]
struct NativePtr(*mut std::ffi::c_void);
unsafe impl Send 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 set_native_frame(widget_id: u64, x: i32, y: i32, width: u32, height: u32) {
let Some(ptr) = get_native_view(widget_id) else {
return;
};
if MainThreadMarker::new().is_none() {
return;
}
let frame = make_rect(x, y, width, height);
unsafe {
let view: *mut AnyObject = ptr as *mut AnyObject;
let _: () = msg_send![view, setFrame: frame];
}
}
pub(crate) fn set_native_hidden(widget_id: u64, hidden: bool) {
let Some(ptr) = get_native_view(widget_id) else {
return;
};
if MainThreadMarker::new().is_none() {
return;
}
unsafe {
let view: *mut AnyObject = ptr as *mut AnyObject;
let _: () = msg_send![view, setHidden: hidden];
}
}
pub(crate) fn set_native_enabled(widget_id: u64, enabled: bool) {
let Some(ptr) = get_native_view(widget_id) else {
return;
};
if MainThreadMarker::new().is_none() {
return;
}
unsafe {
let view: *mut AnyObject = ptr as *mut AnyObject;
let selector = sel!(setEnabled:);
let responds: bool = msg_send![view, respondsToSelector: selector];
if responds {
let _: () = msg_send![view, setEnabled: enabled];
}
}
}
pub(crate) fn set_native_text(widget_id: u64, text: &str) {
let Some(ptr) = get_native_view(widget_id) else {
return;
};
if MainThreadMarker::new().is_none() {
return;
}
let value = NSString::from_str(text);
unsafe {
let view: *mut AnyObject = ptr as *mut AnyObject;
let set_title_selector = sel!(setTitle:forState:);
if msg_send![view, respondsToSelector: set_title_selector] {
let _: () = msg_send![view, setTitle: &*value, forState: UIControlState::Normal.0];
return;
}
let set_text_selector = sel!(setText:);
if msg_send![view, respondsToSelector: set_text_selector] {
let _: () = msg_send![view, setText: &*value];
return;
}
let set_label_selector = sel!(setAccessibilityLabel:);
if msg_send![view, respondsToSelector: set_label_selector] {
let _: () = msg_send![view, setAccessibilityLabel: &*value];
}
}
}
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 AnyObject = parent_ptr as *mut AnyObject;
let root_vc: *mut AnyObject = msg_send![parent, rootViewController];
if root_vc.is_null() {
return;
}
let content_view: *mut AnyObject = msg_send![root_vc, view];
if content_view.is_null() {
return;
}
let child: *mut AnyObject = child_ptr as *mut AnyObject;
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::define_class;
use objc2::sel;
define_class!(
#[unsafe(super(NSObject))]
#[ivars = u64]
struct ButtonTarget;
impl ButtonTarget {
#[unsafe(method(buttonTapped:))]
fn handle_tap(&self, _sender: &NSObject) {
BUTTON_EVENT_QUEUE.lock().unwrap().push_back(*self.ivars());
}
}
);
impl ButtonTarget {
fn new(mtm: MainThreadMarker, widget_id: u64) -> Retained<Self> {
let obj: Allocated<Self> = mtm.alloc();
let obj = obj.set_ivars(widget_id);
unsafe { msg_send![super(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 AnyObject = ptr as *mut AnyObject;
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);
#[allow(deprecated)]
let window = UIWindow::initWithFrame(mtm.alloc(), frame);
let bg = UIColor::whiteColor();
window.setBackgroundColor(Some(&bg));
let vc = UIViewController::initWithNibName_bundle(mtm.alloc(), None, None);
window.setRootViewController(Some(&vc));
window.makeKeyAndVisible();
window.setAccessibilityLabel(Some(&NSString::from_str(title)), mtm);
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 = UIButton::initWithFrame(mtm.alloc(), frame);
button.setTitle_forState(Some(&NSString::from_str(text)), UIControlState::Normal);
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 = UILabel::initWithFrame(mtm.alloc(), frame);
label.setText(Some(&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 = UISwitch::initWithFrame(mtm.alloc(), frame);
switch_ctl.setAccessibilityLabel(Some(&NSString::from_str(text)), mtm);
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 = UITextField::initWithFrame(mtm.alloc(), frame);
text_field.setText(Some(&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 = UIButton::initWithFrame(mtm.alloc(), frame);
button.setTitle_forState(Some(&NSString::from_str(text)), UIControlState::Normal);
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 = 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);
UIProgressView::initWithFrame(mtm.alloc(), frame)
}
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);
UIPickerView::initWithFrame(mtm.alloc(), frame)
}
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);
UITableView::initWithFrame_style(mtm.alloc(), frame, UITableViewStyle::Plain)
}
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 = UIView::initWithFrame(mtm.alloc(), frame);
let bg = UIColor::clearColor();
panel.setBackgroundColor(Some(&bg));
panel
}
pub(crate) fn create_ui_scroll(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIScrollView> {
let frame = make_rect(x, y, width, height);
let scroll = UIScrollView::initWithFrame(mtm.alloc(), frame);
scroll.setScrollEnabled(true);
scroll.setContentSize(frame.size);
scroll
}
pub(crate) fn create_ui_alert(
_mtm: MainThreadMarker,
title: &str,
text: &str,
) -> Retained<AnyObject> {
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<AnyObject> = 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<AnyObject> = msg_send![action_cls, actionWithTitle: &*ok_str, style: 0u64, handler: std::ptr::null_mut::<AnyObject>()];
let _: () = msg_send![&*alert, addAction: &*action];
alert
}
}
pub(crate) fn create_ui_stack(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIStackView> {
let frame = make_rect(x, y, width, height);
let stack = UIStackView::initWithFrame(mtm.alloc(), frame);
stack.setSpacing(8.0);
stack
}
pub(crate) fn create_ui_spinner(
mtm: MainThreadMarker,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Retained<UIActivityIndicatorView> {
let frame = make_rect(x, y, width, height);
let spinner = UIActivityIndicatorView::initWithActivityIndicatorStyle(
mtm.alloc(),
UIActivityIndicatorViewStyle::Medium,
);
spinner.setFrame(frame);
spinner.startAnimating();
spinner
}