#![cfg(apple_uikit)]
#![cfg(feature = "ios-uikit-ffi")]
use crate::compat::{BTreeMap, String, Vec};
use crate::core::Rect;
use crate::platform::clipboard::{ClipboardContent, RichClipboardBackend};
use crate::platform::types::{Display, DisplayId, SafeAreaInsets};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::MainThreadMarker;
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::{NSData, NSLocale, NSString, NSURL};
use objc2_ui_kit::{
NSObjectUIAccessibility, UIAccessibilityDarkerSystemColorsEnabled,
UIAccessibilityIsReduceMotionEnabled, UIApplication, UIColor, UIContentSizeCategory,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge,
UIContentSizeCategoryAccessibilityExtraExtraLarge,
UIContentSizeCategoryAccessibilityExtraLarge, UIContentSizeCategoryAccessibilityLarge,
UIContentSizeCategoryAccessibilityMedium, UIContentSizeCategoryExtraExtraExtraLarge,
UIContentSizeCategoryExtraExtraLarge, UIContentSizeCategoryExtraLarge,
UIContentSizeCategoryExtraSmall, UIContentSizeCategoryLarge, UIContentSizeCategoryMedium,
UIContentSizeCategorySmall, UIPasteboard, UIPrintInfo, UIPrintInteractionController, UIScreen,
UITraitCollection, UIUserInterfaceStyle, UIViewController, UIWindow,
};
thread_local! {
static UI_WINDOWS: core::cell::RefCell<BTreeMap<u64, objc2::rc::Retained<UIWindow>>> =
core::cell::RefCell::new(BTreeMap::new());
}
const HTML_TYPE: &str = "public.html";
const PLAIN_TEXT_TYPE: &str = "public.utf8-plain-text";
const TEXT_TYPES: [&str; 2] = [PLAIN_TEXT_TYPE, "public.text"];
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(
window_id: u64,
mtm: MainThreadMarker,
title: &str,
_x: i32,
_y: i32,
width: u32,
height: u32,
) -> objc2::rc::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);
UI_WINDOWS.with(|windows| {
windows.borrow_mut().insert(window_id, window.clone());
});
window
}
pub(crate) fn forget_ui_window(window_id: u64) {
UI_WINDOWS.with(|windows| {
windows.borrow_mut().remove(&window_id);
});
}
#[allow(deprecated)] pub(crate) fn screen_id_of_window(window_id: u64) -> Option<u64> {
let mtm = MainThreadMarker::new()?;
UI_WINDOWS.with(|windows| {
let windows = windows.borrow();
let window = windows.get(&window_id)?;
let screen = window.screen();
UIScreen::screens(mtm)
.iter()
.position(|candidate| {
core::ptr::eq(
objc2::rc::Retained::as_ptr(&candidate),
objc2::rc::Retained::as_ptr(&screen),
)
})
.map(|index| index as u64)
})
}
pub(crate) fn safe_area_insets(window_id: u64) -> Option<SafeAreaInsets> {
let _mtm = MainThreadMarker::new()?;
let insets = UI_WINDOWS.with(|windows| {
let windows = windows.borrow();
windows.get(&window_id).map(|window| window.safeAreaInsets())
})?;
Some(SafeAreaInsets {
top: logical_pixels(insets.top),
bottom: logical_pixels(insets.bottom),
left: logical_pixels(insets.left),
right: logical_pixels(insets.right),
})
}
fn logical_pixels(value: f64) -> u32 {
value.max(0.0).round() as u32
}
pub(crate) fn system_theme() -> Option<bool> {
let _mtm = MainThreadMarker::new()?;
let traits = unsafe { UITraitCollection::currentTraitCollection() };
let style = unsafe { traits.userInterfaceStyle() };
if style == UIUserInterfaceStyle::Dark {
Some(true)
} else if style == UIUserInterfaceStyle::Light {
Some(false)
} else {
None
}
}
pub(crate) fn high_contrast() -> bool {
UIAccessibilityDarkerSystemColorsEnabled()
}
pub(crate) fn reduce_motion() -> bool {
UIAccessibilityIsReduceMotionEnabled()
}
pub(crate) fn system_locale() -> Option<&'static str> {
let identifier = NSLocale::currentLocale().localeIdentifier().to_string();
crate::platform::locale::intern(Some(identifier.replace('_', "-")))
.or_else(|| crate::platform::locale::intern(crate::platform::locale::from_environment()))
}
pub(crate) fn text_scale() -> Option<f32> {
let mtm = MainThreadMarker::new()?;
let category = UIApplication::sharedApplication(mtm).preferredContentSizeCategory();
let current: &UIContentSizeCategory = &*category;
let table: [(&UIContentSizeCategory, f32); 12] = unsafe {
[
(UIContentSizeCategoryExtraSmall, 1.0),
(UIContentSizeCategorySmall, 1.0),
(UIContentSizeCategoryMedium, 1.0),
(UIContentSizeCategoryLarge, 1.0),
(UIContentSizeCategoryExtraLarge, 1.12),
(UIContentSizeCategoryExtraExtraLarge, 1.24),
(UIContentSizeCategoryExtraExtraExtraLarge, 1.35),
(UIContentSizeCategoryAccessibilityMedium, 1.65),
(UIContentSizeCategoryAccessibilityLarge, 1.94),
(UIContentSizeCategoryAccessibilityExtraLarge, 2.35),
(UIContentSizeCategoryAccessibilityExtraExtraLarge, 2.76),
(UIContentSizeCategoryAccessibilityExtraExtraExtraLarge, 3.0),
]
};
for (name, scale) in table {
if current == name {
return Some(scale);
}
}
None
}
pub(crate) fn system_fonts() -> Vec<crate::platform::types::SystemFont> {
crate::platform::fonts_apple::system_fonts()
}
pub(crate) fn has_print_support() -> bool {
true
}
pub(crate) fn spawn_print_job(job_file: &std::path::Path) -> Result<(), String> {
let mtm = MainThreadMarker::new()
.ok_or_else(|| "printing must be requested from the main thread".to_string())?;
let path = job_file
.to_str()
.ok_or_else(|| format!("print job path is not UTF-8: {}", job_file.display()))?;
let url = NSURL::fileURLWithPath(&NSString::from_str(path));
if !UIPrintInteractionController::canPrintURL(&url, mtm) {
return Err(format!("UIKit cannot print '{}'", job_file.display()));
}
let controller = UIPrintInteractionController::sharedPrintController(mtm);
let info = UIPrintInfo::printInfo(mtm);
info.setJobName(&NSString::from_str("rust-widgets"));
controller.setPrintInfo(Some(&info));
unsafe {
let item: &AnyObject = &*(Retained::as_ptr(&url) as *const AnyObject);
controller.setPrintingItem(Some(item));
}
unsafe {
controller.presentAnimated_completionHandler(true, std::ptr::null_mut());
}
Ok(())
}
fn pasteboard() -> objc2::rc::Retained<UIPasteboard> {
UIPasteboard::generalPasteboard()
}
pub(crate) fn pasteboard_set_text(text: &str) -> bool {
let board = pasteboard();
unsafe {
board.setString(Some(&NSString::from_str(text)));
board.string().is_some_and(|stored| stored.to_string() == text)
}
}
pub(crate) fn pasteboard_text() -> Option<String> {
let board = pasteboard();
unsafe { board.string() }.map(|text| text.to_string())
}
pub(crate) fn pasteboard_set_html(html: &str, plain: &str) -> bool {
let board = pasteboard();
let plain_ok = pasteboard_set_text(plain);
board.setData_forPasteboardType(
&NSData::with_bytes(html.as_bytes()),
&NSString::from_str(HTML_TYPE),
);
let html_ok = board
.dataForPasteboardType(&NSString::from_str(HTML_TYPE))
.is_some_and(|stored| stored.to_vec() == html.as_bytes());
html_ok || plain_ok
}
pub(crate) fn pasteboard_html() -> Option<String> {
let board = pasteboard();
let data = board.dataForPasteboardType(&NSString::from_str(HTML_TYPE))?;
String::from_utf8(data.to_vec()).ok()
}
pub(crate) fn pasteboard_has_format(content_type: &str) -> bool {
match content_type {
"text/html" => pasteboard_html().is_some(),
"text/plain" => pasteboard_text().is_some(),
other => {
let board = pasteboard();
board.dataForPasteboardType(&NSString::from_str(other)).is_some()
}
}
}
pub(crate) fn pasteboard_set_custom(content_type: &str, data: &[u8]) -> bool {
let board = pasteboard();
board.setData_forPasteboardType(&NSData::with_bytes(data), &NSString::from_str(content_type));
board
.dataForPasteboardType(&NSString::from_str(content_type))
.is_some_and(|stored| stored.to_vec() == data)
}
pub(crate) fn pasteboard_custom(content_type: &str) -> Option<Vec<u8>> {
let board = pasteboard();
board.dataForPasteboardType(&NSString::from_str(content_type)).map(|data| data.to_vec())
}
pub(crate) fn pasteboard_custom_type() -> Option<String> {
let board = pasteboard();
let types = unsafe { board.pasteboardTypes() };
for type_name in types.iter() {
let name = type_name.to_string();
if name == HTML_TYPE || TEXT_TYPES.contains(&name.as_str()) {
continue;
}
if !name.contains('/') {
continue;
}
return Some(name);
}
None
}
#[allow(deprecated)] pub(crate) fn interface_orientation() -> Option<crate::platform::ScreenOrientation> {
let _mtm = MainThreadMarker::new()?;
UI_WINDOWS.with(|windows| {
for window in windows.borrow().values() {
let Some(scene) = window.windowScene() else { continue };
return Some(match scene.interfaceOrientation() {
objc2_ui_kit::UIInterfaceOrientation::Portrait => {
crate::platform::ScreenOrientation::Portrait
}
objc2_ui_kit::UIInterfaceOrientation::PortraitUpsideDown => {
crate::platform::ScreenOrientation::PortraitUpsideDown
}
objc2_ui_kit::UIInterfaceOrientation::LandscapeLeft => {
crate::platform::ScreenOrientation::LandscapeLeft
}
objc2_ui_kit::UIInterfaceOrientation::LandscapeRight => {
crate::platform::ScreenOrientation::LandscapeRight
}
_ => return None,
});
}
None
})
}
pub(crate) fn main_screen_scale() -> Option<f32> {
let mtm = MainThreadMarker::new()?;
let screen = UIScreen::mainScreen(mtm);
let scale = screen.scale() as f32;
Some(if scale > 0.0 { scale } else { 1.0 })
}
#[allow(deprecated)] pub(crate) fn displays() -> Vec<Display> {
let Some(mtm) = MainThreadMarker::new() else {
return Vec::new();
};
let screens = UIScreen::screens(mtm);
let main = UIScreen::mainScreen(mtm);
let mut out = Vec::new();
for (index, screen) in screens.iter().enumerate() {
let bounds = screen.bounds();
let scale = screen.scale() as f32;
let rect = Rect::new(
bounds.origin.x as i32,
bounds.origin.y as i32,
bounds.size.width.max(0.0) as u32,
bounds.size.height.max(0.0) as u32,
);
out.push(Display {
id: DisplayId::new(index as u64),
bounds: rect,
work_area: rect,
scale: if scale > 0.0 { scale } else { 1.0 },
is_primary: core::ptr::eq(
objc2::rc::Retained::as_ptr(&screen),
objc2::rc::Retained::as_ptr(&main),
),
});
}
out
}
pub(crate) struct IosClipboard;
impl IosClipboard {
pub(crate) fn new() -> Self {
Self
}
}
impl RichClipboardBackend for IosClipboard {
fn set_contents(&self, content: ClipboardContent) -> bool {
match content {
ClipboardContent::Text(text) => pasteboard_set_text(&text),
ClipboardContent::Html { html, plain } => pasteboard_set_html(&html, &plain),
ClipboardContent::Custom { content_type, data } => {
pasteboard_set_custom(&content_type, &data)
}
other => {
log::warn!(
"[ios clipboard] {} is not carried by this backend; the write is reported \
as refused rather than pretended",
other.content_type()
);
false
}
}
}
fn get_contents(&self) -> Option<ClipboardContent> {
if let Some(content_type) = pasteboard_custom_type() {
if let Some(data) = pasteboard_custom(&content_type) {
return Some(ClipboardContent::Custom { content_type, data });
}
}
if let Some(html) = pasteboard_html() {
return Some(ClipboardContent::Html {
html,
plain: pasteboard_text().unwrap_or_default(),
});
}
pasteboard_text().map(ClipboardContent::Text)
}
fn has_format(&self, content_type: &str) -> bool {
pasteboard_has_format(content_type)
}
fn set_contents_with_type(&self, content_type: &str, data: &[u8]) -> bool {
pasteboard_set_custom(content_type, data)
}
fn get_contents_for_type(&self, content_type: &str) -> Option<Vec<u8>> {
match content_type {
"text/plain" => pasteboard_text().map(String::into_bytes),
"text/html" => pasteboard_html().map(String::into_bytes),
other => pasteboard_custom(other),
}
}
}