use std::mem;
use std::ptr::NonNull;
use bevy_ecs::{
entity::{hash_map::EntityHashMap, Entity},
event::Event,
query::{Added, Changed, Without},
removal_detection::RemovedComponents,
system::{NonSend, NonSendMut, Query},
world::World,
};
use bevy_window::{PrimaryWindow, Window, WindowEvent, WindowTheme};
use block2::RcBlock;
use objc2::{available, rc::Retained, MainThreadMarker, MainThreadOnly};
use objc2::{define_class, msg_send, AllocAnyThread, Message};
use objc2_core_foundation::{CGFloat, CGSize};
use objc2_foundation::{ns_string, NSDictionary, NSError, NSNumber, NSString, NSUserActivity};
use objc2_ui_kit::{
UIApplication, UISceneActivationRequestOptions, UISceneDestructionRequestOptions,
UIUserInterfaceStyle, UIWindow, UIWindowScene,
};
use tracing::{error, trace};
use crate::{view::ViewController, MainThread, USER_INFO_WINDOW_ENTITY_ID, WINDOW_ACTIVITY_TYPE};
pub(crate) trait WorldHelper {
fn send_window_event(&mut self, event: impl Into<WindowEvent> + Event + Clone);
}
impl WorldHelper for World {
fn send_window_event(&mut self, event: impl Into<WindowEvent> + Event + Clone) {
self.send_event(event.clone());
self.send_event(event.into());
}
}
#[derive(Debug)]
pub struct UIKitWindow {
scene: Option<Retained<UIWindowScene>>,
pub(crate) uiwindow: Retained<BevyWindow>,
}
#[derive(Debug, Default)]
pub struct UIKitWindows {
entity_to_uikit: EntityHashMap<UIKitWindow>,
}
impl UIKitWindows {
pub(crate) fn get(&self, entity: Entity) -> Option<&UIKitWindow> {
self.entity_to_uikit.get(&entity)
}
pub(crate) fn is_initialized(&self, entity: Entity) -> bool {
self.get(entity).is_some()
}
pub(crate) fn insert(&mut self, entity: Entity, uikit_window: UIKitWindow) {
let prev = self.entity_to_uikit.insert(entity, uikit_window);
debug_assert!(prev.is_none(), "tried to create existing window");
}
}
pub(crate) fn setup_window(
scene: Option<&UIWindowScene>,
entity: Entity,
window: &Window,
mtm: MainThreadMarker,
) -> UIKitWindow {
let view_controller = ViewController::new(mtm, entity);
let uiwindow = BevyWindow::alloc(mtm).set_ivars(entity);
let uiwindow: Retained<BevyWindow> = if let Some(scene) = scene {
unsafe { msg_send![super(uiwindow), initWithWindowScene: scene] }
} else {
unsafe { msg_send![super(uiwindow), init] }
};
uiwindow.setRootViewController(Some(&view_controller));
update_window(window, &uiwindow, scene);
uiwindow.makeKeyAndVisible();
UIKitWindow {
scene: scene.map(|scene| scene.retain()),
uiwindow,
}
}
pub fn create_windows(
mut created_windows: Query<Entity, (Added<Window>, Without<PrimaryWindow>)>,
uikit_windows: NonSend<UIKitWindows>,
mtm: NonSend<MainThread>,
) {
for entity in &mut created_windows {
if uikit_windows.is_initialized(entity) {
continue;
};
if available!(ios = 13.0, tvos = 13.0, visionos = 1.0, ..) {
trace!("requesting window creation");
let application = UIApplication::sharedApplication(mtm.0);
let options = unsafe { UISceneActivationRequestOptions::new(mtm.0) };
let user_activity = unsafe {
NSUserActivity::initWithActivityType(
NSUserActivity::alloc(),
ns_string!(WINDOW_ACTIVITY_TYPE),
)
};
let dict = NSDictionary::from_slices(
&[ns_string!(USER_INFO_WINDOW_ENTITY_ID)],
&[NSNumber::new_u64(entity.to_bits()).as_ref()],
);
let dict = unsafe { mem::transmute::<&NSDictionary<NSString>, &NSDictionary>(&*dict) };
unsafe { user_activity.addUserInfoEntriesFromDictionary(&dict) };
let error_handler = RcBlock::new(|err: NonNull<NSError>| {
let err = unsafe { err.as_ref() };
error!(%err, "failed creating window, this is not possible on single-window iOS");
});
#[allow(deprecated, reason = "the replacement API requires newer OS versions")]
unsafe {
application.requestSceneSessionActivation_userActivity_options_errorHandler(
None, Some(&user_activity),
Some(&options),
Some(&error_handler),
)
};
} else {
error!("failed creating window, this is not possible on this version of iOS");
}
}
}
pub fn changed_windows(
changed_windows: Query<(Entity, &Window), Changed<Window>>,
uikit_windows: NonSend<UIKitWindows>,
) {
for (entity, window) in &changed_windows {
trace!(?entity, "detected changes to Window");
let Some(uikit_window) = uikit_windows.get(entity) else {
continue;
};
update_window(
window,
&uikit_window.uiwindow,
uikit_window.scene.as_deref(),
);
}
}
fn update_window(
Window {
canvas: _, clip_children: _, composite_alpha_mode: _, cursor_options: _, decorations: _, desired_maximum_frame_latency: _, enabled_buttons, fit_canvas_to_parent: _, focused: _, fullsize_content_view: _, has_shadow: _, ime_enabled: _, ime_position: _, internal: _, mode: _, movable_by_window_background: _, name: _, position, prefers_home_indicator_hidden: _, prefers_status_bar_hidden: _, present_mode: _, prevent_default_event_handling: _, recognize_doubletap_gesture: _, recognize_pan_gesture: _, recognize_pinch_gesture: _, recognize_rotation_gesture: _, resizable: _, resize_constraints, resolution, skip_taskbar: _, title, titlebar_show_buttons: _, titlebar_show_title: _, titlebar_shown: _, titlebar_transparent: _, transparent: _, visible: _, window_level: _, window_theme, }: &Window,
window: &UIWindow,
scene: Option<&UIWindowScene>,
) {
fn avoid_inf(num: f32) -> CGFloat {
num.min(f32::MAX) as CGFloat
}
unsafe {
if let Some(scene) = scene {
let title = NSString::from_str(&title);
if scene.title() != title {
trace!(?title, "setting UIWindowScene.title");
scene.setTitle(Some(&title));
}
if let Some(size_restrictions) = scene.sizeRestrictions() {
let min = CGSize {
width: avoid_inf(resize_constraints.min_width),
height: avoid_inf(resize_constraints.min_height),
};
if min != size_restrictions.minimumSize() {
trace!(?min, "setting UIWindowScene.sizeRestrictions.minimumSize");
size_restrictions.setMinimumSize(min);
}
let max = CGSize {
width: avoid_inf(resize_constraints.max_width),
height: avoid_inf(resize_constraints.max_height),
};
if max != size_restrictions.maximumSize() {
trace!(?max, "setting UIWindowScene.sizeRestrictions.maximumSize");
size_restrictions.setMaximumSize(max);
}
if cfg!(target_abi = "macabi") && available!(ios = 16.0, ..) {
let val = enabled_buttons.maximize;
if size_restrictions.allowsFullScreen() != val {
trace!(
?val,
"setting UIWindowScene.sizeRestrictions.allowsFullScreen"
);
size_restrictions.setAllowsFullScreen(val);
}
}
}
if available!(ios = 16.0, tvos = 16.0, visionos = 1.0, ..) {
if let Some(behaviours) = scene.windowingBehaviors() {
let val = enabled_buttons.minimize;
if behaviours.isMiniaturizable() != val {
trace!(
?val,
"setting UIWindowScene.windowingBehaviors.miniaturizable"
);
behaviours.setMiniaturizable(val);
}
let val = enabled_buttons.close;
if behaviours.isClosable() != val {
trace!(?val, "setting UIWindowScene.windowingBehaviors.closable");
behaviours.setClosable(val);
}
}
}
if cfg!(target_abi = "macabi") && available!(ios = 16.0) {
}
}
if available!(ios = 13.0, tvos = 13.0, visionos = 1.0, ..) {
let style = match window_theme {
Some(WindowTheme::Light) => UIUserInterfaceStyle::Light,
Some(WindowTheme::Dark) => UIUserInterfaceStyle::Dark,
None => UIUserInterfaceStyle::Unspecified,
};
if window.overrideUserInterfaceStyle() != style {
trace!(?style, "setting UIWindow.overrideUserInterfaceStyle");
window.setOverrideUserInterfaceStyle(style);
}
}
}
}
pub fn despawn_windows(
mut removed_windows: RemovedComponents<Window>,
mut uikit_windows: NonSendMut<UIKitWindows>,
) {
for entity in removed_windows.read() {
trace!(?entity, "detected removed Window");
let Some(uikit_window) = uikit_windows.entity_to_uikit.remove(&entity) else {
continue;
};
if let Some(scene) = uikit_window.scene {
let app = UIApplication::sharedApplication(scene.mtm());
let options = unsafe { UISceneDestructionRequestOptions::new(scene.mtm()) };
let error_handler = RcBlock::new(|err: NonNull<NSError>| {
let err = unsafe { err.as_ref() };
error!(%err, "failed removing window, this is not possible on single-window iOS");
});
unsafe {
app.requestSceneSessionDestruction_options_errorHandler(
&scene.session(),
Some(&options),
Some(&error_handler),
);
}
} else {
error!("tried to remove main window, this is not possible on single-window iOS");
}
}
}
define_class!(
#[unsafe(super(UIWindow))]
#[name = "BevyWindow"]
#[derive(Debug, PartialEq, Eq, Hash)]
#[ivars = Entity]
pub(crate) struct BevyWindow;
);