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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 259 additions and 64 deletions

View File

@ -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",

View File

@ -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"

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()
}
}

View File

@ -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);

View File

@ -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());

View File

@ -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);

View File

@ -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,

View File

@ -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();

View File

@ -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<LayerId> = 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<T: Plugin + 'static, R>(&self, f: impl FnOnce(&mut T) -> R) -> Option<R> {
pub fn with_plugin<T: plugin::Plugin + 'static, R>(
&self,
f: impl FnOnce(&mut T) -> R,
) -> Option<R> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
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<T: Plugin>(&self) -> TypedPluginHandle<T> {
pub fn plugin<T: plugin::Plugin>(&self) -> TypedPluginHandle<T> {
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<T: Plugin>(&self) -> Option<TypedPluginHandle<T>> {
pub fn plugin_opt<T: plugin::Plugin>(&self) -> Option<TypedPluginHandle<T>> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
plugin.map(TypedPluginHandle::new)
}
/// Get a handle to the plugin of type `T`, or insert its default.
pub fn plugin_or_default<T: Plugin + Default>(&self) -> TypedPluginHandle<T> {
pub fn plugin_or_default<T: plugin::Plugin + Default>(&self) -> TypedPluginHandle<T> {
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);

View File

@ -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<ViewportInfo>,
/// 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<SafeAreaInsets>,
/// 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<SafeAreaInsets> for Rect {
type Output = Self;
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
self - rhs.0
}
}

View File

@ -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(),

View File

@ -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"
));

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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()),

View File

@ -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 <https://egui.rs>.
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
}

View File

@ -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],

View File

@ -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<Item = Self>,
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);
}
}

View File

@ -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()),

View File

@ -247,8 +247,12 @@ fn generic_ui(ui: &mut egui::Ui, children: &[Arc<RwLock<ViewportState>>], 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!(