bevy_ios_safearea 0.6.0

Bevy plugin to query device safe area insets
Documentation
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, system::SystemParam};

/// Resource providing iOS device safe area insets.
/// It is created and added only when there are insets on the running device.
/// It is recommended to access it from systems by using [`IosSafeArea`] SystemParam.
///
/// # Example
/// ```rust
/// use bevy::prelude::*;
/// use bevy_ios_safearea::IosSafeArea;
///
/// fn bevy_system(safe_area: IosSafeArea) {
///     let safe_area_top = safe_area.top();
/// }
// ```
#[derive(Resource, Clone, Debug, Default)]
pub struct IosSafeAreaResource {
    /// The inset from the top of the screen.
    ///
    /// This value accounts for elements like the notch or status bar.
    pub top: f32,
    /// The inset from the bottom of the screen.
    ///
    /// This value accounts for elements like the home indicator.
    pub bottom: f32,
    /// The inset from the left side of the screen.
    ///
    /// This value is non-zero for devices with rounded corners or unique screen shapes.
    pub left: f32,
    /// The inset from the right side of the screen.
    ///
    /// This value is non-zero for devices with rounded corners or unique screen shapes.
    pub right: f32,
}

/// SystemParam helper allowing to read insets while defaulting to 0 if not available.
#[derive(SystemParam)]
pub struct IosSafeArea<'w> {
    resource: Option<Res<'w, IosSafeAreaResource>>,
}

impl IosSafeArea<'_> {
    /// top inset
    pub fn top(&self) -> f32 {
        self.resource.as_ref().map(|r| r.top).unwrap_or(0.)
    }

    /// bottom inset
    pub fn bottom(&self) -> f32 {
        self.resource.as_ref().map(|r| r.bottom).unwrap_or(0.)
    }

    /// left inset
    pub fn left(&self) -> f32 {
        self.resource.as_ref().map(|r| r.left).unwrap_or(0.)
    }

    /// right inset
    pub fn right(&self) -> f32 {
        self.resource.as_ref().map(|r| r.right).unwrap_or(0.)
    }
}

/// Plugin to query iOS device safe area insets.
///
/// # Example
/// ```no_run
/// use bevy::prelude::*;
///
/// App::new()
///     .add_plugins((DefaultPlugins,bevy_ios_safearea::IosSafeAreaPlugin))
///     .run();
/// ```
#[derive(Default)]
pub struct IosSafeAreaPlugin;

impl Plugin for IosSafeAreaPlugin {
    #[cfg_attr(not(target_os = "ios"), allow(unused_variables))]
    fn build(&self, app: &mut App) {
        // `init` reacts to `WindowCreated` instead of running once at `Startup`: the
        // winit `UIWindow` is not guaranteed to be registered in `WINIT_WINDOWS` during
        // the first frame — window creation is event-loop-driven on iOS and can complete
        // a frame or two after Bevy's first update. `bevy_winit` writes `WindowCreated`
        // immediately after registering the window, so that message is the earliest
        // point where the handle is guaranteed to be available.
        #[cfg(target_os = "ios")]
        app.add_systems(Update, init);
    }
}

#[cfg(target_os = "ios")]
fn init(
    _non_send_marker: bevy_ecs::system::NonSendMarker,
    mut window_created: MessageReader<bevy_window::WindowCreated>,
    window: Single<Entity, With<bevy_window::PrimaryWindow>>,
    mut commands: Commands,
) {
    use bevy_log::tracing;
    use winit::raw_window_handle::HasWindowHandle;

    if !window_created
        .read()
        .any(|created| created.window == *window)
    {
        return;
    }

    tracing::debug!("safe area updating");

    let insets = bevy_winit::WINIT_WINDOWS.with_borrow(|windows| {
        // Guaranteed registered at this point (`WindowCreated` is written after
        // registration), but stay panic-free regardless.
        let Some(raw_window) = windows.get_window(*window) else {
            return None;
        };

        let Ok(handle) = raw_window.window_handle() else {
            return None;
        };

        if let winit::raw_window_handle::RawWindowHandle::UiKit(handle) = handle.as_raw() {
            let ui_view: *mut std::ffi::c_void = handle.ui_view.as_ptr();

            let (top, bottom, left, right) = unsafe {
                (
                    crate::native::swift_safearea_top(ui_view),
                    crate::native::swift_safearea_bottom(ui_view),
                    crate::native::swift_safearea_left(ui_view),
                    crate::native::swift_safearea_right(ui_view),
                )
            };

            Some(IosSafeAreaResource {
                top,
                bottom,
                left,
                right,
            })
        } else {
            None
        }
    });

    if let Some(safe_area) = insets {
        tracing::debug!("safe area updated: {:?}", safe_area);
        commands.insert_resource(safe_area);
    }
}