From 30277233ce37d213f5e0ef347bd0ec3d58898044 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Tue, 7 Oct 2025 12:30:09 +0200 Subject: [PATCH] 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 Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 3 ++ Cargo.toml | 1 + crates/egui-winit/Cargo.toml | 18 ++++++++ crates/egui-winit/src/lib.rs | 16 +++++++ crates/egui-winit/src/safe_area.rs | 56 ++++++++++++++++++++++ crates/egui/src/containers/area.rs | 2 +- crates/egui/src/containers/modal.rs | 2 +- crates/egui/src/containers/panel.rs | 6 +-- crates/egui/src/containers/popup.rs | 2 +- crates/egui/src/containers/resize.rs | 2 +- crates/egui/src/context.rs | 69 +++++++++++++++++++--------- crates/egui/src/data/input.rs | 29 +++++++++++- crates/egui/src/debug_text.rs | 4 +- crates/egui/src/input_state/mod.rs | 59 ++++++++++++++++++++---- crates/egui/src/menu.rs | 10 ++-- crates/egui/src/pass_state.rs | 12 ++--- crates/egui/src/ui.rs | 2 +- crates/egui_demo_app/src/wrap_app.rs | 6 +-- crates/egui_demo_lib/src/lib.rs | 2 +- crates/egui_kittest/src/wgpu.rs | 2 +- crates/emath/src/rect_align.rs | 6 +-- examples/file_dialog/src/main.rs | 6 +-- tests/test_viewports/src/main.rs | 8 +++- 23 files changed, 259 insertions(+), 64 deletions(-) create mode 100644 crates/egui-winit/src/safe_area.rs diff --git a/Cargo.lock b/Cargo.lock index ed40f305..6121aa34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,6 +1314,9 @@ dependencies = [ "document-features", "egui", "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", "profiling", "raw-window-handle", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6be41d27..c3a1195d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ nohash-hasher = "0.2" objc2 = "0.5.1" objc2-app-kit = { version = "0.2.0", default-features = false } objc2-foundation = { version = "0.2.0", default-features = false } +objc2-ui-kit = { version = "0.2.0", default-features = false } parking_lot = "0.12" percent-encoding = "2.1" pollster = "0.4" diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 3f4891f9..814abef4 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -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 } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 914c1775..92e8c341 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -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; diff --git a/crates/egui-winit/src/safe_area.rs b/crates/egui-winit/src/safe_area.rs new file mode 100644 index 00000000..d29d654a --- /dev/null +++ b/crates/egui-winit/src/safe_area.rs @@ -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 isn’t covered by a navigation bar, tab bar, + /// toolbar, or other views a window might provide. Safe areas are essential for avoiding a + /// device’s 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::(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() + } +} diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 07f2e0cf..22ab67d3 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -436,7 +436,7 @@ impl Area { sizing_pass: force_sizing_pass, } = self; - let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect()); + let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.content_rect()); let layer_id = LayerId::new(order, id); diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index e36ad6e1..6b846ab5 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -90,7 +90,7 @@ impl Modal { inner: (inner, backdrop_response), response, } = area.show(ctx, |ui| { - let bg_rect = ui.ctx().screen_rect(); + let bg_rect = ui.ctx().content_rect(); let bg_sense = Sense::CLICK | Sense::DRAG; let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect)); backdrop.set_min_size(bg_rect.size()); diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 1cb10269..15d1b32b 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -389,7 +389,7 @@ impl SidePanel { .layer_id(LayerId::background()) .max_rect(available_rect), ); - panel_ui.set_clip_rect(ctx.screen_rect()); + panel_ui.set_clip_rect(ctx.content_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -887,7 +887,7 @@ impl TopBottomPanel { .layer_id(LayerId::background()) .max_rect(available_rect), ); - panel_ui.set_clip_rect(ctx.screen_rect()); + panel_ui.set_clip_rect(ctx.content_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -1152,7 +1152,7 @@ impl CentralPanel { .layer_id(LayerId::background()) .max_rect(ctx.available_rect().round_ui()), ); - panel_ui.set_clip_rect(ctx.screen_rect()); + panel_ui.set_clip_rect(ctx.content_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a5a5b37b..a9c00661 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -485,7 +485,7 @@ impl<'a> Popup<'a> { .chain(RectAlign::MENU_ALIGNS.iter().copied()), ), ), - self.ctx.screen_rect(), + self.ctx.content_rect(), anchor_rect, self.gap, expected_popup_size, diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index bb001dc6..50cc2877 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -220,7 +220,7 @@ impl Resize { .at_least(self.min_size) .at_most(self.max_size) .at_most( - ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows + ui.ctx().content_rect().size() - ui.spacing().window_margin.sum(), // hack for windows ) .round_ui(); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 03b46b65..44c4d8fa 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -17,10 +17,10 @@ use epaint::{ use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, - ModifierNames, Modifiers, NumExt as _, Order, Painter, Plugin, RawInput, Response, RichText, - ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, - ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, - Widget as _, WidgetRect, WidgetText, + ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, + SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, + ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, + ViewportOutput, Widget as _, WidgetRect, WidgetText, animation_manager::AnimationManager, containers::{self, area::AreaState}, data::output::PlatformOutput, @@ -374,6 +374,7 @@ struct ContextImpl { animation_manager: AnimationManager, plugins: plugin::Plugins, + safe_area: SafeAreaInsets, /// All viewports share the same texture manager and texture namespace. /// @@ -419,6 +420,10 @@ impl ContextImpl { .unwrap_or_default(); let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent_id); + if let Some(safe_area) = new_raw_input.safe_area_insets { + self.safe_area = safe_area; + } + let is_outermost_viewport = self.viewport_stack.is_empty(); // not necessarily root, just outermost immediate viewport self.viewport_stack.push(ids); @@ -432,7 +437,7 @@ impl ContextImpl { let input = &viewport.input; // This is a bit hacky, but is required to avoid jitter: - let mut rect = input.screen_rect; + let mut rect = input.content_rect(); rect.min = (ratio * rect.min.to_vec2()).to_pos2(); rect.max = (ratio * rect.max.to_vec2()).to_pos2(); new_raw_input.screen_rect = Some(rect); @@ -459,9 +464,9 @@ impl ContextImpl { ); let repaint_after = viewport.input.wants_repaint_after(); - let screen_rect = viewport.input.screen_rect; + let content_rect = viewport.input.content_rect(); - viewport.this_pass.begin_pass(screen_rect); + viewport.this_pass.begin_pass(content_rect); { let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); @@ -494,9 +499,9 @@ impl ContextImpl { self.memory.areas_mut().set_state( LayerId::background(), AreaState { - pivot_pos: Some(screen_rect.left_top()), + pivot_pos: Some(content_rect.left_top()), pivot: Align2::LEFT_TOP, - size: Some(screen_rect.size()), + size: Some(content_rect.size()), interactable: true, last_became_visible_at: None, }, @@ -1075,14 +1080,14 @@ impl Context { } let show_error = |widget_rect: Rect, text: String| { - let screen_rect = self.screen_rect(); + let content_rect = self.content_rect(); let text = format!("🔥 {text}"); let color = self.style().visuals.error_fg_color; let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color), StrokeKind::Outside); - let below = widget_rect.bottom() + 32.0 < screen_rect.bottom(); + let below = widget_rect.bottom() + 32.0 < content_rect.bottom(); let text_rect = if below { painter.debug_text( @@ -1426,8 +1431,8 @@ impl Context { /// Get a full-screen painter for a new or existing layer pub fn layer_painter(&self, layer_id: LayerId) -> Painter { - let screen_rect = self.screen_rect(); - Painter::new(self.clone(), layer_id, screen_rect) + let content_rect = self.content_rect(); + Painter::new(self.clone(), layer_id, content_rect) } /// Paint on top of everything else @@ -1861,13 +1866,13 @@ impl Context { }); } - /// Register a [`Plugin`] + /// Register a [`Plugin`](plugin::Plugin) /// /// Plugins are called in the order they are added. /// /// A plugin of the same type can only be added once (further calls with the same type will be ignored). /// This way it's convenient to add plugins in `eframe::run_simple_native`. - pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + pub fn add_plugin(&self, plugin: impl plugin::Plugin + 'static) { let handle = plugin::PluginHandle::new(plugin); let added = self.write(|ctx| ctx.plugins.add(handle.clone())); @@ -1880,7 +1885,10 @@ impl Context { /// Call the provided closure with the plugin of type `T`, if it was registered. /// /// Returns `None` if the plugin was not registered. - pub fn with_plugin(&self, f: impl FnOnce(&mut T) -> R) -> Option { + pub fn with_plugin( + &self, + f: impl FnOnce(&mut T) -> R, + ) -> Option { let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); plugin.map(|plugin| f(plugin.lock().typed_plugin_mut())) } @@ -1889,7 +1897,7 @@ impl Context { /// /// ## Panics /// If the plugin of type `T` was not registered, this will panic. - pub fn plugin(&self) -> TypedPluginHandle { + pub fn plugin(&self) -> TypedPluginHandle { if let Some(plugin) = self.plugin_opt() { plugin } else { @@ -1898,13 +1906,13 @@ impl Context { } /// Get a handle to the plugin of type `T`, if it was registered. - pub fn plugin_opt(&self) -> Option> { + pub fn plugin_opt(&self) -> Option> { let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); plugin.map(TypedPluginHandle::new) } /// Get a handle to the plugin of type `T`, or insert its default. - pub fn plugin_or_default(&self) -> TypedPluginHandle { + pub fn plugin_or_default(&self) -> TypedPluginHandle { if let Some(plugin) = self.plugin_opt() { plugin } else { @@ -2658,9 +2666,28 @@ impl Context { // --------------------------------------------------------------------- + /// Returns the position and size of the egui area that is safe for content rendering. + /// + /// Returns [`Self::viewport_rect`] minus areas that might be partially covered by, for example, + /// the OS status bar or display notches. + pub fn content_rect(&self) -> Rect { + self.input(|i| i.content_rect()).round_ui() + } + + /// Returns the position and size of the full area available to egui + /// + /// This includes reas that might be partially covered by, for example, the OS status bar or + /// display notches. See [`Self::content_rect`] to get a rect that is safe for content. + pub fn viewport_rect(&self) -> Rect { + self.input(|i| i.viewport_rect()).round_ui() + } + /// Position and size of the egui area. + #[deprecated( + note = "screen_rect has been renamed to viewport_rect. Consider switching to content_rect." + )] pub fn screen_rect(&self) -> Rect { - self.input(|i| i.screen_rect()).round_ui() + self.input(|i| i.content_rect()).round_ui() } /// How much space is still available after panels have been added. @@ -3235,7 +3262,7 @@ impl Context { ui.image(SizedTexture::new(texture_id, size)) .on_hover_ui(|ui| { // show larger on hover - let max_size = 0.5 * ui.ctx().screen_rect().size(); + let max_size = 0.5 * ui.ctx().content_rect().size(); let mut size = point_size; size *= max_size.x / size.x.max(max_size.x); size *= max_size.y / size.y.max(max_size.y); diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 7c23a13b..8eecd979 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -1,6 +1,6 @@ //! The input needed by egui. -use epaint::ColorImage; +use epaint::{ColorImage, MarginF32}; use crate::{ Key, OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, @@ -27,6 +27,11 @@ pub struct RawInput { /// Information about all egui viewports. pub viewports: ViewportIdMap, + /// The insets used to only render content in a mobile safe area + /// + /// `None` will be treated as "same as last frame" + pub safe_area_insets: Option, + /// Position and size of the area that egui should use, in points. /// Usually you would set this to /// @@ -98,6 +103,7 @@ impl Default for RawInput { dropped_files: Default::default(), focused: true, // integrations opt into global focus tracking system_theme: None, + safe_area_insets: Default::default(), } } } @@ -122,6 +128,7 @@ impl RawInput { .map(|(id, info)| (*id, info.take())) .collect(), screen_rect: self.screen_rect.take(), + safe_area_insets: self.safe_area_insets.take(), max_texture_side: self.max_texture_side.take(), time: self.time, predicted_dt: self.predicted_dt, @@ -149,6 +156,7 @@ impl RawInput { mut dropped_files, focused, system_theme, + safe_area_insets: safe_area, } = newer; self.viewport_id = viewport_ids; @@ -163,6 +171,7 @@ impl RawInput { self.dropped_files.append(&mut dropped_files); self.focused = focused; self.system_theme = system_theme; + self.safe_area_insets = safe_area; } } @@ -1132,6 +1141,7 @@ impl RawInput { dropped_files, focused, system_theme, + safe_area_insets: safe_area, } = self; ui.label(format!("Active viewport: {viewport_id:?}")); @@ -1161,6 +1171,7 @@ impl RawInput { ui.label(format!("dropped_files: {}", dropped_files.len())); ui.label(format!("focused: {focused}")); ui.label(format!("system_theme: {system_theme:?}")); + ui.label(format!("safe_area: {safe_area:?}")); ui.scope(|ui| { ui.set_min_height(150.0); ui.label(format!("events: {events:#?}")) @@ -1297,3 +1308,19 @@ impl EventFilter { } } } + +/// The 'safe area' insets of the screen +/// +/// This represents the area taken up by the status bar, navigation controls, notches, +/// or any other items that obscure parts of the screen. +#[derive(Debug, PartialEq, Copy, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct SafeAreaInsets(pub MarginF32); + +impl std::ops::Sub for Rect { + type Output = Self; + + fn sub(self, rhs: SafeAreaInsets) -> Self::Output { + self - rhs.0 + } +} diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index ed0bea72..cabde8c6 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -72,7 +72,7 @@ impl DebugTextPlugin { // Show debug-text next to the cursor. let mut pos = ctx .input(|i| i.pointer.latest_pos()) - .unwrap_or_else(|| ctx.screen_rect().center()) + .unwrap_or_else(|| ctx.content_rect().center()) + 8.0 * Vec2::Y; let painter = ctx.debug_painter(); @@ -96,7 +96,7 @@ impl DebugTextPlugin { { // Paint `text` to right of `pos`: - let available_width = ctx.screen_rect().max.x - pos.x; + let available_width = ctx.content_rect().max.x - pos.x; let galley = text.into_galley_impl( ctx, &ctx.style(), diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 5f6aec78..e78342ac 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -5,6 +5,7 @@ use crate::data::input::{ PointerButton, RawInput, TouchDeviceId, ViewportInfo, }; use crate::{ + SafeAreaInsets, emath::{NumExt as _, Pos2, Rect, Vec2, vec2}, util::History, }; @@ -272,7 +273,12 @@ pub struct InputState { // ---------------------------------------------- /// Position and size of the egui area. - pub screen_rect: Rect, + /// + /// This is including the area that may be covered by the `safe_area_insets`. + viewport_rect: Rect, + + /// The safe area insets, subtracted from the `viewport_rect` in [`Self::content_rect`]. + safe_area_insets: SafeAreaInsets, /// Also known as device pixel ratio, > 1 for high resolution screens. pub pixels_per_point: f32, @@ -359,7 +365,8 @@ impl Default for InputState { zoom_factor_delta: 1.0, rotation_radians: 0.0, - screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), + viewport_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), + safe_area_insets: Default::default(), pixels_per_point: 1.0, max_texture_side: 2048, time: 0.0, @@ -397,7 +404,8 @@ impl InputState { new.predicted_dt }; - let screen_rect = new.screen_rect.unwrap_or(self.screen_rect); + let safe_area_insets = new.safe_area_insets.unwrap_or(self.safe_area_insets); + let viewport_rect = new.screen_rect.unwrap_or(self.viewport_rect); self.create_touch_states_for_new_devices(&new.events); for touch_state in self.touch_states.values_mut() { touch_state.begin_pass(time, &new, self.pointer.interact_pos); @@ -437,7 +445,7 @@ impl InputState { let mut delta = match unit { MouseWheelUnit::Point => *delta, MouseWheelUnit::Line => options.line_scroll_speed * *delta, - MouseWheelUnit::Page => screen_rect.height() * *delta, + MouseWheelUnit::Page => viewport_rect.height() * *delta, }; let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier); @@ -552,7 +560,8 @@ impl InputState { zoom_factor_delta, rotation_radians, - screen_rect, + viewport_rect, + safe_area_insets, pixels_per_point, max_texture_side: new.max_texture_side.unwrap_or(self.max_texture_side), time, @@ -574,9 +583,41 @@ impl InputState { self.raw.viewport() } + /// Returns the full area available to egui, including parts that might be partially covered, + /// for example, by the OS status bar or notches (see [`Self::safe_area_insets`]). + /// + /// Usually you want to use [`Self::content_rect`] instead. + pub fn viewport_rect(&self) -> Rect { + self.viewport_rect + } + + /// Returns the region of the screen that is safe for content rendering + /// + /// Returns the `viewport_rect` with the `safe_area_insets` removed. #[inline(always)] + pub fn content_rect(&self) -> Rect { + self.viewport_rect - self.safe_area_insets + } + + /// Returns the full area available to egui, including parts that might be partially + /// covered, for example, by the OS status bar or notches. + /// + /// Usually you want to use [`Self::content_rect`] instead. + #[deprecated( + note = "screen_rect has been renamed to viewport_rect. Consider switching to content_rect." + )] pub fn screen_rect(&self) -> Rect { - self.screen_rect + self.content_rect() + } + + /// Get the safe area insets. + /// + /// This represents the area of the screen covered by status bars, navigation controls, notches, + /// or other items that obscure part of the screen. + /// + /// See [`Self::content_rect`] to get the `viewport_rect` with the safe area insets removed. + pub fn safe_area_insets(&self) -> SafeAreaInsets { + self.safe_area_insets } /// Uniform zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). @@ -1559,7 +1600,8 @@ impl InputState { rotation_radians, zoom_factor_delta, - screen_rect, + viewport_rect, + safe_area_insets, pixels_per_point, max_texture_side, time, @@ -1612,7 +1654,8 @@ impl InputState { ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x")); ui.label(format!("rotation_radians: {rotation_radians:.3} radians")); - ui.label(format!("screen_rect: {screen_rect:?} points")); + ui.label(format!("viewport_rect: {viewport_rect:?} points")); + ui.label(format!("safe_area_insets: {safe_area_insets:?} points")); ui.label(format!( "{pixels_per_point} physical pixels for each logical point" )); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 0e31a593..348f42c2 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -401,14 +401,14 @@ impl MenuRoot { if let Some(root) = root.inner.as_mut() { let menu_rect = root.menu_state.read().rect; - let screen_rect = button.ctx.input(|i| i.screen_rect); + let content_rect = button.ctx.input(|i| i.content_rect()); - if pos.y + menu_rect.height() > screen_rect.max.y { - pos.y = screen_rect.max.y - menu_rect.height() - button.rect.height(); + if pos.y + menu_rect.height() > content_rect.max.y { + pos.y = content_rect.max.y - menu_rect.height() - button.rect.height(); } - if pos.x + menu_rect.width() > screen_rect.max.x { - pos.x = screen_rect.max.x - menu_rect.width(); + if pos.x + menu_rect.width() > content_rect.max.x { + pos.x = content_rect.max.x - menu_rect.width(); } } diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index ca0d1572..66a1f6e1 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -137,7 +137,7 @@ impl DebugRect { let galley = painter.layout_no_wrap(text, font_id, text_color); // Position the text either under or above: - let screen_rect = ctx.screen_rect(); + let content_rect = ctx.content_rect(); let y = if galley.size().y <= rect.top() { // Above rect.top() - galley.size().y - 16.0 @@ -147,12 +147,12 @@ impl DebugRect { }; let y = y - .at_most(screen_rect.bottom() - galley.size().y) + .at_most(content_rect.bottom() - galley.size().y) .at_least(0.0); let x = rect .left() - .at_most(screen_rect.right() - galley.size().x) + .at_most(content_rect.right() - galley.size().x) .at_least(0.0); let text_pos = pos2(x, y); @@ -258,7 +258,7 @@ impl Default for PassState { } impl PassState { - pub(crate) fn begin_pass(&mut self, screen_rect: Rect) { + pub(crate) fn begin_pass(&mut self, content_rect: Rect) { profiling::function_scope!(); let Self { used_ids, @@ -282,8 +282,8 @@ impl PassState { widgets.clear(); tooltips.clear(); layers.clear(); - *available_rect = screen_rect; - *unused_rect = screen_rect; + *available_rect = content_rect; + *unused_rect = content_rect; *used_by_panels = Rect::NOTHING; *scroll_target = [None, None]; *scroll_delta = Default::default(); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a08148a8..e2d89c0b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -142,7 +142,7 @@ impl Ui { "Top-level Ui:s should not have an id_salt" ); - let max_rect = max_rect.unwrap_or_else(|| ctx.screen_rect()); + let max_rect = max_rect.unwrap_or_else(|| ctx.content_rect()); let clip_rect = max_rect; let layout = layout.unwrap_or_default(); let disabled = disabled || invisible; diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 9f9cb6a8..127139a9 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -467,10 +467,10 @@ impl WrapApp { let painter = ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); - let screen_rect = ctx.screen_rect(); - painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192)); + let content_rect = ctx.content_rect(); + painter.rect_filled(content_rect, 0.0, Color32::from_black_alpha(192)); painter.text( - screen_rect.center(), + content_rect.center(), Align2::CENTER_CENTER, text, TextStyle::Heading.resolve(&ctx.style()), diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index be3816a3..76be2885 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -109,6 +109,6 @@ fn test_egui_zero_window_size() { /// Detect narrow screens. This is used to show a simpler UI on mobile devices, /// especially for the web demo at . pub fn is_mobile(ctx: &egui::Context) -> bool { - let screen_size = ctx.screen_rect().size(); + let screen_size = ctx.content_rect().size(); screen_size.x < 550.0 } diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index a71a69d7..d0a6e848 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -151,7 +151,7 @@ impl crate::TestRenderer for WgpuTestRenderer { label: Some("Egui Command Encoder"), }); - let size = ctx.screen_rect().size() * ctx.pixels_per_point(); + let size = ctx.content_rect().size() * ctx.pixels_per_point(); let screen = ScreenDescriptor { pixels_per_point: ctx.pixels_per_point(), size_in_pixels: [size.x.round() as u32, size.y.round() as u32], diff --git a/crates/emath/src/rect_align.rs b/crates/emath/src/rect_align.rs index 31580405..84cf60ce 100644 --- a/crates/emath/src/rect_align.rs +++ b/crates/emath/src/rect_align.rs @@ -236,7 +236,7 @@ impl RectAlign { } /// Look for the first alternative [`RectAlign`] that allows the child rect to fit - /// inside the `screen_rect`. + /// inside the `content_rect`. /// /// If no alternative fits, the first is returned. /// If no alternatives are given, `None` is returned. @@ -246,7 +246,7 @@ impl RectAlign { /// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions pub fn find_best_align( values_to_try: impl Iterator, - screen_rect: Rect, + content_rect: Rect, parent_rect: Rect, gap: f32, expected_size: Vec2, @@ -258,7 +258,7 @@ impl RectAlign { let suggested_popup_rect = align.align_rect(&parent_rect, expected_size, gap); - if screen_rect.contains_rect(suggested_popup_rect) { + if content_rect.contains_rect(suggested_popup_rect) { return Some(align); } } diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index f552adb2..9a8bbe96 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -107,10 +107,10 @@ fn preview_files_being_dropped(ctx: &egui::Context) { let painter = ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); - let screen_rect = ctx.screen_rect(); - painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192)); + let content_rect = ctx.content_rect(); + painter.rect_filled(content_rect, 0.0, Color32::from_black_alpha(192)); painter.text( - screen_rect.center(), + content_rect.center(), Align2::CENTER_CENTER, text, TextStyle::Heading.resolve(&ctx.style()), diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index ad39ffb3..ab31a4ec 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -247,8 +247,12 @@ fn generic_ui(ui: &mut egui::Ui, children: &[Arc>], close_ if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) { ui.label(format!("monitor_size: {monitor_size:?} (points)")); } - if let Some(screen_rect) = ui.input(|i| i.raw.screen_rect) { - ui.label(format!("Screen rect size: Pos: {:?}", screen_rect.size())); + if let Some(viewport_rect) = ui.input(|i| i.raw.screen_rect) { + ui.label(format!( + "Viewport Rect: Pos: {:?}, Size: {:?} (points)", + viewport_rect.min, + viewport_rect.size() + )); } if let Some(inner_rect) = ctx.input(|i| i.viewport().inner_rect) { ui.label(format!(