mod ime;
pub(super) mod scene;
use std::ffi::CString;
use objc2::encode::Encode;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Imp, Sel};
use objc2::{ClassType, Message, msg_send, sel};
use objc2_ui_kit::{UIEdgeInsets, UIView, UIWindow};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
pub(super) fn full_screen(
cc: &eframe::CreationContext<'_>,
app: Box<dyn eframe::App>,
) -> Box<dyn eframe::App> {
let Some(view) = ui_view(cc) else {
tracing::warn!("no UIView behind the window: the safe area is left to eframe");
return app;
};
let Some(window) = view.window() else {
tracing::warn!("the view has no UIWindow: the safe area is left to eframe");
return app;
};
hide_safe_area(&window);
view.setNeedsLayout();
if !ime::install(&view, &cc.egui_ctx) {
tracing::warn!("the view does not take UITextInput: keyboards that compose type nothing");
}
Box::new(IosWindow {
app,
window,
view,
focused: None,
})
}
fn ui_view(cc: &eframe::CreationContext<'_>) -> Option<Retained<UIView>> {
let RawWindowHandle::UiKit(handle) = cc.window_handle().ok()?.as_raw() else {
return None;
};
let view: &UIView = unsafe { handle.ui_view.cast().as_ref() };
Some(view.retain())
}
fn hide_safe_area(window: &UIWindow) {
extern "C-unwind" fn none(_this: &AnyObject, _cmd: Sel) -> UIEdgeInsets {
UIEdgeInsets {
top: 0.0,
left: 0.0,
bottom: 0.0,
right: 0.0,
}
}
let class = window.class();
if class == UIWindow::class() {
tracing::warn!("the window is a plain UIWindow: the safe area is left to eframe");
return;
}
let types = CString::new(format!("{}@:", UIEdgeInsets::ENCODING)).expect("no NUL");
unsafe {
let imp: Imp =
std::mem::transmute(none as extern "C-unwind" fn(&AnyObject, Sel) -> UIEdgeInsets);
objc2::ffi::class_addMethod(
(class as *const objc2::runtime::AnyClass).cast_mut(),
sel!(safeAreaInsets),
imp,
types.as_ptr(),
);
}
}
fn safe_area(window: &UIWindow) -> egui::SafeAreaInsets {
let insets: UIEdgeInsets =
unsafe { msg_send![super(window, UIWindow::class()), safeAreaInsets] };
egui::SafeAreaInsets(egui::epaint::MarginF32 {
left: insets.left as f32,
right: insets.right as f32,
top: insets.top as f32,
bottom: insets.bottom as f32,
})
}
struct IosWindow {
app: Box<dyn eframe::App>,
window: Retained<UIWindow>,
view: Retained<UIView>,
focused: Option<egui::Id>,
}
impl eframe::App for IosWindow {
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
raw_input.safe_area_insets = Some(safe_area(&self.window));
raw_input.events.extend(ime::take_events());
self.app.raw_input_hook(ctx, raw_input);
}
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.app.logic(ctx, frame);
}
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
self.app.ui(ui, frame);
let ctx = ui.ctx();
let focused = ctx.memory(|memory| memory.focused());
if focused != self.focused {
self.focused = focused;
ime::reset(&self.view);
}
if let Some(output) = ctx.output(|output| output.ime) {
ime::set_caret(output.cursor_rect * ctx.zoom_factor());
}
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
self.app.save(storage);
}
fn on_exit(&mut self) {
self.app.on_exit();
}
fn auto_save_interval(&self) -> std::time::Duration {
self.app.auto_save_interval()
}
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
self.app.clear_color(visuals)
}
fn persist_egui_memory(&self) -> bool {
self.app.persist_egui_memory()
}
}