diff --git a/winit-android/src/event_loop.rs b/winit-android/src/event_loop.rs index 62895a34d2..174911eddb 100644 --- a/winit-android/src/event_loop.rs +++ b/winit-android/src/event_loop.rs @@ -6,9 +6,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use android_activity::input::{InputEvent, KeyAction, Keycode, MotionAction}; -use android_activity::{ - AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect, -}; +use android_activity::{AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent}; use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, trace, warn}; use winit_core::application::ApplicationHandler; @@ -178,6 +176,7 @@ impl EventLoop { let cause = self.cause; let mut pending_redraw = self.pending_redraw; let mut resized = false; + let mut safe_area_changed = false; app.new_events(&self.window_target, cause); @@ -193,9 +192,7 @@ impl EventLoop { }, MainEvent::WindowResized { .. } => resized = true, MainEvent::RedrawNeeded { .. } => pending_redraw = true, - MainEvent::ContentRectChanged { .. } => { - warn!("TODO: find a way to notify application of content rect change"); - }, + MainEvent::ContentRectChanged { .. } => safe_area_changed = true, MainEvent::GainedFocus => { HAS_FOCUS.store(true, Ordering::Relaxed); let event = event::WindowEvent::Focused(true); @@ -286,18 +283,39 @@ impl EventLoop { } if self.running { - if resized { - let size = if let Some(native_window) = self.android_app.native_window().as_ref() { + fn get_window_size(app: &AndroidApp) -> PhysicalSize { + if let Some(native_window) = app.native_window().as_ref() { let width = native_window.width() as _; let height = native_window.height() as _; PhysicalSize::new(width, height) } else { PhysicalSize::new(0, 0) - }; + } + } + + fn get_insets(app: &AndroidApp) -> PhysicalInsets { + let insets = app.content_rect(); + let outer_size = get_window_size(app); + PhysicalInsets { + top: insets.top as u32, + left: insets.left as u32, + bottom: outer_size.height.saturating_sub(insets.bottom as u32), + right: outer_size.width.saturating_sub(insets.right as u32), + } + } + + if resized { + let size = get_window_size(&self.android_app); let event = event::WindowEvent::SurfaceResized(size); app.window_event(&self.window_target, GLOBAL_WINDOW, event); } + if safe_area_changed { + let insets = get_insets(&self.android_app); + let event = event::WindowEvent::SafeAreaChanged(insets); + app.window_event(&self.window_target, GLOBAL_WINDOW, event); + } + pending_redraw |= self.redraw_flag.get_and_reset(); if pending_redraw { pending_redraw = false; @@ -784,10 +802,6 @@ impl Window { self.app.config() } - pub(crate) fn content_rect(&self) -> Rect { - self.app.content_rect() - } - // Allow the usage of HasRawWindowHandle inside this function #[allow(deprecated)] fn raw_window_handle_rwh_06(&self) -> Result { @@ -852,7 +866,7 @@ impl CoreWindow for Window { fn pre_present_notify(&self) {} fn surface_position(&self) -> PhysicalPosition { - (0, 0).into() + (0.0, 0.0).into() } fn outer_position(&self) -> Result, RequestError> { @@ -876,7 +890,14 @@ impl CoreWindow for Window { } fn safe_area(&self) -> PhysicalInsets { - PhysicalInsets::new(0, 0, 0, 0) + let insets = self.app.content_rect(); + let outer_size = self.outer_size(); + PhysicalInsets { + top: insets.top as u32, + left: insets.left as u32, + bottom: outer_size.height.saturating_sub(insets.bottom as u32), + right: outer_size.width.saturating_sub(insets.right as u32), + } } fn set_min_surface_size(&self, _: Option) {} diff --git a/winit-android/src/lib.rs b/winit-android/src/lib.rs index 989c9716df..90b24673d0 100644 --- a/winit-android/src/lib.rs +++ b/winit-android/src/lib.rs @@ -77,7 +77,7 @@ mod keycodes; use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop; use winit_core::window::Window as CoreWindow; -use self::activity::{AndroidApp, ConfigurationRef, Rect}; +use self::activity::{AndroidApp, ConfigurationRef}; pub use crate::event_loop::{ ActiveEventLoop, EventLoop, EventLoopProxy, PlatformSpecificEventLoopAttributes, PlatformSpecificWindowAttributes, Window, @@ -97,17 +97,10 @@ pub trait ActiveEventLoopExtAndroid { /// Additional methods on [`Window`] that are specific to Android. pub trait WindowExtAndroid { - fn content_rect(&self) -> Rect; - fn config(&self) -> ConfigurationRef; } impl WindowExtAndroid for dyn CoreWindow + '_ { - fn content_rect(&self) -> Rect { - let window = self.cast_ref::().unwrap(); - window.content_rect() - } - fn config(&self) -> ConfigurationRef { let window = self.cast_ref::().unwrap(); window.config() diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index ce048295ea..628d22a3da 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -5,7 +5,7 @@ use std::f64; use std::path::PathBuf; use std::sync::{Mutex, Weak}; -use dpi::{PhysicalPosition, PhysicalSize}; +use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use smol_str::SmolStr; @@ -59,6 +59,21 @@ pub enum WindowEvent { /// [`Window::surface_size`]: crate::window::Window::surface_size SurfaceResized(PhysicalSize), + /// The safe area + /// + /// Contains the new safe area insets (can also be retrieved with + /// [`Window::safe_area`]). + /// + /// This event will not necessarily be emitted upon window creation, query + /// [`Window::safe_area`] if you need to determine the initial safe area. + /// + /// ## Platform-specific + /// + /// - **macOS / Orbital / Wayland / Windows / X11:** Unsupported. + /// + /// [`Window::safe_area`]: crate::window::Window::safe_area + SafeAreaChanged(PhysicalInsets), + /// The position of the window has changed. /// /// Contains the window's new position in desktop coordinates (can also be retrieved with diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 6f564ba900..9e8ab971e3 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -768,7 +768,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// ## Platform-specific /// - /// - **Android / Orbital / Wayland / Windows / X11:** Unimplemented, returns `(0, 0, 0, 0)`. + /// - **Orbital / Wayland / Windows / X11:** Unimplemented, returns `(0, 0, 0, 0)`. /// /// ## Example /// diff --git a/winit-uikit/src/view.rs b/winit-uikit/src/view.rs index 96b2647886..059e142739 100644 --- a/winit-uikit/src/view.rs +++ b/winit-uikit/src/view.rs @@ -1,17 +1,17 @@ #![allow(clippy::unnecessary_cast)] use std::cell::{Cell, RefCell}; -use dpi::PhysicalPosition; +use dpi::{LogicalInsets, PhysicalInsets, PhysicalPosition}; use objc2::rc::Retained; use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2::{DefinedClass, MainThreadMarker, available, define_class, msg_send, sel}; use objc2_core_foundation::{CGFloat, CGPoint, CGRect}; use objc2_foundation::{NSObject, NSSet, NSString}; use objc2_ui_kit::{ - UIEvent, UIForceTouchCapability, UIGestureRecognizer, UIGestureRecognizerDelegate, - UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, UIPinchGestureRecognizer, - UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITextInputTraits, UITouch, - UITouchPhase, UITouchType, UITraitEnvironment, UIView, + UIApplication, UIEdgeInsets, UIEvent, UIForceTouchCapability, UIGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, + UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, + UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, }; use tracing::debug; use winit_core::event::{ @@ -127,6 +127,15 @@ define_class!( debug!("safeAreaInsetsDidChange was called, requesting redraw"); // When the safe area changes we want to make sure to emit a redraw event self.setNeedsDisplay(); + + let window = self.window().unwrap(); + let event = EventWrapper::Window { + window_id: window.id(), + event: WindowEvent::SafeAreaChanged(self.safe_area()), + }; + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, event); } #[unsafe(method(touchesBegan:withEvent:))] @@ -368,6 +377,20 @@ impl WinitView { (**self).window().map(|window| window.downcast().unwrap()) } + pub(crate) fn safe_area(&self) -> PhysicalInsets { + let insets = if available!(ios = 11.0, tvos = 11.0, visionos = 1.0) { + self.safeAreaInsets() + } else { + // Assume the status bar frame is the only thing that obscures the view + let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); + #[allow(deprecated)] + let status_bar_frame = app.statusBarFrame(); + UIEdgeInsets { top: status_bar_frame.size.height, left: 0.0, bottom: 0.0, right: 0.0 } + }; + let insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right); + insets.to_physical(self.contentScaleFactor() as f64) + } + pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { let mtm = MainThreadMarker::from(self); if should_recognize { diff --git a/winit-uikit/src/window.rs b/winit-uikit/src/window.rs index 788babcb71..d28bb822eb 100644 --- a/winit-uikit/src/window.rs +++ b/winit-uikit/src/window.rs @@ -5,16 +5,15 @@ use std::sync::{Arc, Mutex}; use dispatch2::MainThreadBound; use dpi::{ - LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, - Position, Size, + LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, }; use objc2::rc::Retained; -use objc2::{MainThreadMarker, available, class, define_class, msg_send}; +use objc2::{MainThreadMarker, class, define_class, msg_send}; use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize}; use objc2_foundation::{NSObject, NSObjectProtocol}; use objc2_ui_kit::{ - UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen, - UIScreenOverscanCompensation, UIViewController, UIWindow, + UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation, UIViewController, + UIWindow, }; use tracing::{debug, warn}; use winit_core::cursor::Cursor; @@ -202,17 +201,7 @@ impl Inner { } pub fn safe_area(&self) -> PhysicalInsets { - let insets = if available!(ios = 11.0, tvos = 11.0, visionos = 1.0) { - self.view.safeAreaInsets() - } else { - // Assume the status bar frame is the only thing that obscures the view - let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); - #[allow(deprecated)] - let status_bar_frame = app.statusBarFrame(); - UIEdgeInsets { top: status_bar_frame.size.height, left: 0.0, bottom: 0.0, right: 0.0 } - }; - let insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right); - insets.to_physical(self.scale_factor()) + self.view.safe_area() } pub fn set_min_surface_size(&self, _dimensions: Option) { diff --git a/winit/examples/application.rs b/winit/examples/application.rs index bc8d7dee69..21583aa32e 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -442,6 +442,12 @@ impl ApplicationHandler for Application { WindowEvent::SurfaceResized(size) => { window.resize(size); }, + WindowEvent::SafeAreaChanged(insets) => { + info!("SafeAreaInsets={insets:?}"); + // Do nothing as in this example we requery the safe area every frame anyway + // You could use this event to cache the safe area, although you also need to + // query the safe area at startup, and in response to SurfaceResize events. + }, WindowEvent::Focused(focused) => { if focused { info!("Window={window_id:?} focused"); diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 1a41586444..c698a8c062 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -44,6 +44,8 @@ changelog entry. - Add `keyboard` support for OpenHarmony. - On iOS, add Apple Pencil support with force, altitude, and azimuth data. +- On Android, implement `Window::safe_area`. This replaces `WindowExtAndroid::content_rect` which has been removed. +- On iOS and Android, add a `WindowEvent::SafeAreaChanged` that notifies the user when the safe area changes. ### Changed