1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Add support for the safe area on iOS (#7578)

This PR is a continuation of #4915 by @frederik-uni and @lucasmerlin
that introduces support for keeping egui content within the 'safe area'
on iOS (avoiding the notch / dynamic island / menu bar etc.), with the
following changes:

- `SafeArea` now wraps `MarginF32` and has been renamed to
`SafeAreaInsets` to clarify its purpose.
- `InputState::screen_rect` is now marked as deprecated in favour of
either `viewport_rect` (which contains the entire screen), or
`content_rect` (which is the viewport rect with the safe area insets
removed).
- I added some comments to the safe area insets logic pointing out the
[safe area API coming in winit
v0.31](https://github.com/rust-windowing/winit/issues/3910).

---------

Co-authored-by: frederik-uni <147479464+frederik-uni@users.noreply.github.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Ian Hobson
2025-10-07 12:30:09 +02:00
committed by GitHub
parent 65249013c4
commit 30277233ce
23 changed files with 259 additions and 64 deletions

View File

@@ -76,6 +76,24 @@ document-features = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
webbrowser = { version = "1.0.0", optional = true }
[target.'cfg(target_os = "ios")'.dependencies]
objc2 = { workspace = true }
objc2-foundation = { workspace = true, features = [
"std",
"NSThread",
] }
objc2-ui-kit = { workspace = true, features = [
"std",
"UIApplication",
"UIGeometry",
"UIResponder",
"UIScene",
"UISceneDefinitions",
"UIView",
"UIWindow",
"UIWindowScene",
] }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.7.2", optional = true }

View File

@@ -18,6 +18,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
pub use winit;
pub mod clipboard;
mod safe_area;
mod window_settings;
pub use window_settings::WindowSettings;
@@ -274,6 +275,21 @@ impl State {
}
use winit::event::WindowEvent;
#[cfg(target_os = "ios")]
match &event {
WindowEvent::Resized(_)
| WindowEvent::ScaleFactorChanged { .. }
| WindowEvent::Focused(true)
| WindowEvent::Occluded(false) => {
// Once winit v0.31 has been released this can be reworked to get the safe area from
// `Window::safe_area`, and updated from a new event which is being discussed in
// https://github.com/rust-windowing/winit/issues/3911.
self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
}
_ => {}
}
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let native_pixels_per_point = *scale_factor as f32;

View File

@@ -0,0 +1,56 @@
#[cfg(target_os = "ios")]
pub use ios::get_safe_area_insets;
#[cfg(target_os = "ios")]
mod ios {
use egui::{SafeAreaInsets, epaint::MarginF32};
use objc2::{ClassType, rc::Retained};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
use objc2_ui_kit::{UIApplication, UISceneActivationState, UIWindowScene};
/// Gets the ios safe area insets.
///
/// A safe area defines the area within a view that isnt covered by a navigation bar, tab bar,
/// toolbar, or other views a window might provide. Safe areas are essential for avoiding a
/// devices interactive and display features, like Dynamic Island on iPhone or the camera
/// housing on some Mac models.
///
/// Once winit v0.31 has been released this can be removed in favor of
/// `winit::Window::safe_area`.
pub fn get_safe_area_insets() -> SafeAreaInsets {
let Some(main_thread_marker) = MainThreadMarker::new() else {
log::error!("Getting safe area insets needs to be performed on the main thread");
return SafeAreaInsets::default();
};
let app = UIApplication::sharedApplication(main_thread_marker);
#[allow(unsafe_code)]
unsafe {
// Look for the first window scene that's in the foreground
for scene in app.connectedScenes() {
if scene.isKindOfClass(UIWindowScene::class())
&& matches!(
scene.activationState(),
UISceneActivationState::ForegroundActive
| UISceneActivationState::ForegroundInactive
)
{
// Safe to cast, the class kind was checked above
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
if let Some(window) = window_scene.keyWindow() {
let insets = window.safeAreaInsets();
return SafeAreaInsets(MarginF32 {
top: insets.top as f32,
left: insets.left as f32,
right: insets.right as f32,
bottom: insets.bottom as f32,
});
}
}
}
}
SafeAreaInsets::default()
}
}