From 98a9914676e84817a114cee14fb1ec59f69a4d95 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 26 Aug 2024 00:44:54 +0200 Subject: [PATCH] Add safe area and document coordinate systems Added `Window::safe_area`, which describes the area of the surface that is unobstructed by notches, bezels etc. The drawing code in the examples have been updated to draw a star inside the safe area, and the plain background outside of it. Also renamed `Window::inner_position` to `Window::surface_position`, and changed it to from screen coordinates to window coordinates, to better align how these coordinate systems work together. Finally, added some SVG images and documentation to describe how all of this works. --- Cargo.toml | 1 + docs/res/ATTRIBUTION.md | 7 + docs/res/coordinate-systems-desktop.svg | 1 + docs/res/coordinate-systems-mobile.svg | 1 + docs/res/coordinate-systems.drawio | 132 ++++++++++++++++++ examples/window.rs | 61 +++++++- src/changelog/unreleased.md | 3 + src/event.rs | 5 +- src/lib.rs | 35 +++++ src/monitor.rs | 7 +- src/platform/macos.rs | 3 + src/platform_impl/apple/appkit/window.rs | 10 +- .../apple/appkit/window_delegate.rs | 70 +++++++--- src/platform_impl/apple/uikit/window.rs | 106 ++++++-------- src/window.rs | 70 ++++++---- 15 files changed, 385 insertions(+), 127 deletions(-) create mode 100644 docs/res/coordinate-systems-desktop.svg create mode 100644 docs/res/coordinate-systems-mobile.svg create mode 100644 docs/res/coordinate-systems.drawio diff --git a/Cargo.toml b/Cargo.toml index ea54b49b27..2e67b79f31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,7 @@ objc2-foundation = { version = "0.2.2", features = [ "NSDictionary", "NSDistributedNotificationCenter", "NSEnumerator", + "NSGeometry", "NSKeyValueObserving", "NSNotification", "NSObjCRuntime", diff --git a/docs/res/ATTRIBUTION.md b/docs/res/ATTRIBUTION.md index 268316f946..259a91d2bb 100644 --- a/docs/res/ATTRIBUTION.md +++ b/docs/res/ATTRIBUTION.md @@ -9,3 +9,10 @@ by [Tomiĉo] (https://commons.wikimedia.org/wiki/User:Tomi%C4%89o). It was originally released under the [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en) License. Minor modifications have been made by [John Nunley](https://github.com/notgull), which have been released under the same license as a derivative work. + +## `coordinate-systems*` + +These files are created by [Mads Marquart](https://github.com/madsmtm) using +[draw.io](https://draw.io/), and compressed using [svgomg.net](https://svgomg.net/). + +They are licensed under the [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. diff --git a/docs/res/coordinate-systems-desktop.svg b/docs/res/coordinate-systems-desktop.svg new file mode 100644 index 0000000000..fad51789af --- /dev/null +++ b/docs/res/coordinate-systems-desktop.svg @@ -0,0 +1 @@ +
outer_position
outer...
outer_size
outer...
surface_size
surfa...
surface_position
surfa...
diff --git a/docs/res/coordinate-systems-mobile.svg b/docs/res/coordinate-systems-mobile.svg new file mode 100644 index 0000000000..259ee59ce8 --- /dev/null +++ b/docs/res/coordinate-systems-mobile.svg @@ -0,0 +1 @@ +
safe_area
safe_...
surface_size
surfa...
diff --git a/docs/res/coordinate-systems.drawio b/docs/res/coordinate-systems.drawio new file mode 100644 index 0000000000..98bb4badc0 --- /dev/null +++ b/docs/res/coordinate-systems.drawio @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/window.rs b/examples/window.rs index 650b0a7cb2..9a8a305441 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -216,6 +216,10 @@ impl Application { Action::ToggleResizable => window.toggle_resizable(), Action::ToggleDecorations => window.toggle_decorations(), Action::ToggleFullscreen => window.toggle_fullscreen(), + #[cfg(macos_platform)] + Action::ToggleSimpleFullscreen => { + window.window.set_simple_fullscreen(!window.window.simple_fullscreen()); + }, Action::ToggleMaximize => window.toggle_maximize(), Action::ToggleImeInput => window.toggle_ime(), Action::Minimize => window.minimize(), @@ -896,18 +900,55 @@ impl WindowState { return Ok(()); } - const WHITE: u32 = 0xffffffff; - const DARK_GRAY: u32 = 0xff181818; + let mut buffer = self.surface.buffer_mut()?; + + // Fill the whole surface with a plain background + buffer.fill(match self.theme { + Theme::Light => 0xffffffff, // White + Theme::Dark => 0xff181818, // Dark gray + }); + + // Draw a star (without anti-aliasing) inside the safe area + let surface_size = self.window.surface_size(); + let (origin, size) = self.window.safe_area(); + let in_star = |x, y| -> bool { + // Shamelessly adapted from https://stackoverflow.com/a/2049593. + let sign = |p1: (i32, i32), p2: (i32, i32), p3: (i32, i32)| -> i32 { + (p1.0 - p3.0) * (p2.1 - p3.1) - (p2.0 - p3.0) * (p1.1 - p3.1) + }; + + let pt = (x as i32, y as i32); + let v1 = (0, size.height as i32 / 2); + let v2 = (size.width as i32 / 2, 0); + let v3 = (size.width as i32, size.height as i32 / 2); + let v4 = (size.width as i32 / 2, size.height as i32); + + let d1 = sign(pt, v1, v2); + let d2 = sign(pt, v2, v3); + let d3 = sign(pt, v3, v4); + let d4 = sign(pt, v4, v1); + + let has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0) || (d4 < 0); + let has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0) || (d4 > 0); - let color = match self.theme { - Theme::Light => WHITE, - Theme::Dark => DARK_GRAY, + !(has_neg && has_pos) }; + for y in 0..size.height { + for x in 0..size.width { + if in_star(x, y) { + let index = (origin.y + y) * surface_size.width + (origin.x + x); + buffer[index as usize] = match self.theme { + Theme::Light => 0xffe8e8e8, // Light gray + Theme::Dark => 0xff525252, // Medium gray + }; + } + } + } - let mut buffer = self.surface.buffer_mut()?; - buffer.fill(color); + // Present the buffer self.window.pre_present_notify(); buffer.present()?; + Ok(()) } @@ -944,6 +985,8 @@ enum Action { ToggleDecorations, ToggleResizable, ToggleFullscreen, + #[cfg(macos_platform)] + ToggleSimpleFullscreen, ToggleMaximize, Minimize, NextCursor, @@ -977,6 +1020,8 @@ impl Action { Action::ToggleDecorations => "Toggle decorations", Action::ToggleResizable => "Toggle window resizable state", Action::ToggleFullscreen => "Toggle fullscreen", + #[cfg(macos_platform)] + Action::ToggleSimpleFullscreen => "Toggle simple fullscreen", Action::ToggleMaximize => "Maximize", Action::Minimize => "Minimize", Action::ToggleResizeIncrements => "Use resize increments when resizing window", @@ -1119,6 +1164,8 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[ Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow), Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp), Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen), + #[cfg(macos_platform)] + Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen), Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations), Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput), Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab), diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 6f55ff7f2c..2f1dfa9673 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -65,6 +65,8 @@ changelog entry. - Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. - On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game` to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games. +- Added `Window::surface_position`, which is the position of the surface inside the window. +- Added `Window::safe_area`, which describes the area of the surface that is unobstructed. ### Changed @@ -150,6 +152,7 @@ changelog entry. - Remove `MonitorHandle::size()` and `refresh_rate_millihertz()` in favor of `MonitorHandle::current_video_mode()`. - On Android, remove all `MonitorHandle` support instead of emitting false data. +- Removed `Window::inner_position`, use the new `Window::surface_position` instead. ### Fixed diff --git a/src/event.rs b/src/event.rs index 223992db8f..ac945ccb0d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -156,7 +156,10 @@ pub enum WindowEvent { /// [`Window::surface_size`]: crate::window::Window::surface_size SurfaceResized(PhysicalSize), - /// The position of the window has changed. Contains the window's new position. + /// The position of the window has changed. + /// + /// Contains the window's new position in desktop coordinates (can also be retrieved with + /// [`Window::outer_position`]). /// /// ## Platform-specific /// diff --git a/src/lib.rs b/src/lib.rs index f747c13928..9d7d44f44c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,41 @@ //! [`visible` set to `false`][crate::window::WindowAttributes::with_visible] and explicitly make //! the window visible only once you're ready to render into it. //! +//! # Coordinate systems +//! +//! Windowing systems use many different coordinate systems, and this is reflected in Winit as well; +//! there are "desktop coordinates", which is the coordinates of a window or monitor relative to the +//! desktop at large, "window coordinates" which is the coordinates of the surface, relative to the +//! window, and finally "surface coordinates", which is the coordinates relative to the drawn +//! surface. All of these coordinates are relative to the top-left corner of their respective +//! origin. +//! +//! Most of the functionality in Winit works with surface coordinates, so usually you only need to +//! concern yourself with those. In case you need to convert to some other coordinate system, Winit +//! provides [`Window::surface_position`] and [`Window::surface_size`] to describe the surface's +//! location in window coordinates, and Winit provides [`Window::outer_position`] and +//! [`Window::outer_size`] to describe the window's location in desktop coordinates. Using these +//! methods, you should be able to convert a position in one coordinate system to another. +//! +//! An overview of how these four methods fit together can be seen in the image below: +#![doc = concat!("\n\n", include_str!("../docs/res/coordinate-systems-desktop.svg"), "\n\n")] // Rustfmt removes \n, re-add them +//! On mobile, the situation is usually a bit different; because of the smaller screen space, +//! windows usually fill the whole screen at a time, and as such there is _rarely_ a difference +//! between these three coordinate systems, although you should still strive to handle this, as +//! they're still relevant in more niche area such as Mac Catalyst, or multi-tasking on tablets. +//! +//! There is, however, a different important concept: The "safe area". This can be accessed with +//! [`Window::safe_area`], and describes a rectangle in the surface that is not obscured by notches, +//! the status bar, and so on. You should be drawing your background and non-important content on +//! the entire surface, but restrict important content (such as interactable UIs, text, etc.) to +//! being drawn in the safe area. +#![doc = concat!("\n\n", include_str!("../docs/res/coordinate-systems-mobile.svg"), "\n\n")] // Rustfmt removes \n, re-add them +//! [`Window::surface_position`]: crate::window::Window::surface_position +//! [`Window::surface_size`]: crate::window::Window::surface_size +//! [`Window::outer_position`]: crate::window::Window::outer_position +//! [`Window::outer_size`]: crate::window::Window::outer_size +//! [`Window::safe_area`]: crate::window::Window::safe_area +//! //! # UI scaling //! //! UI scaling is important, go read the docs for the [`dpi`] crate for an diff --git a/src/monitor.rs b/src/monitor.rs index 898f9b2003..aeb64b4a01 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -141,8 +141,11 @@ impl MonitorHandle { self.inner.name() } - /// Returns the top-left corner position of the monitor relative to the larger full - /// screen area. + /// Returns the top-left corner position of the monitor in desktop coordinates. + /// + /// This position is in the same coordinate system as [`Window::outer_position`]. + /// + /// [`Window::outer_position`]: crate::window::Window::outer_position /// /// ## Platform-specific /// diff --git a/src/platform/macos.rs b/src/platform/macos.rs index d36d5694e9..ad3027de29 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -91,6 +91,9 @@ pub trait WindowExtMacOS { /// This is how fullscreen used to work on macOS in versions before Lion. /// And allows the user to have a fullscreen window without using another /// space or taking control over the entire monitor. + /// + /// Make sure you only draw your important content inside the safe area so that it does not + /// overlap with the notch on newer devices, see [`Window::safe_area`] for details. fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; /// Returns whether or not the window has shadow. diff --git a/src/platform_impl/apple/appkit/window.rs b/src/platform_impl/apple/appkit/window.rs index 3a454e0457..183cd2223d 100644 --- a/src/platform_impl/apple/appkit/window.rs +++ b/src/platform_impl/apple/appkit/window.rs @@ -111,12 +111,12 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.reset_dead_keys()); } - fn inner_position(&self) -> Result, RequestError> { - Ok(self.maybe_wait_on_main(|delegate| delegate.inner_position())) + fn surface_position(&self) -> dpi::PhysicalPosition { + self.maybe_wait_on_main(|delegate| delegate.surface_position()) } fn outer_position(&self) -> Result, RequestError> { - Ok(self.maybe_wait_on_main(|delegate| delegate.outer_position())) + self.maybe_wait_on_main(|delegate| delegate.outer_position()) } fn set_outer_position(&self, position: Position) { @@ -135,6 +135,10 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.outer_size()) } + fn safe_area(&self) -> (dpi::PhysicalPosition, dpi::PhysicalSize) { + self.maybe_wait_on_main(|delegate| delegate.safe_area()) + } + fn set_min_surface_size(&self, min_size: Option) { self.maybe_wait_on_main(|delegate| delegate.set_min_surface_size(min_size)) } diff --git a/src/platform_impl/apple/appkit/window_delegate.rs b/src/platform_impl/apple/appkit/window_delegate.rs index fdad0c515e..81677588e4 100644 --- a/src/platform_impl/apple/appkit/window_delegate.rs +++ b/src/platform_impl/apple/appkit/window_delegate.rs @@ -6,7 +6,7 @@ use std::ptr; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use core_graphics::display::{CGDisplay, CGPoint}; +use core_graphics::display::CGDisplay; use monitor::VideoModeHandle; use objc2::rc::{autoreleasepool, Retained}; use objc2::runtime::{AnyObject, ProtocolObject}; @@ -20,10 +20,10 @@ use objc2_app_kit::{ NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, }; use objc2_foundation::{ - ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSKeyValueChangeKey, - NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSObject, - NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, - NSRect, NSSize, NSString, + ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSEdgeInsets, + NSKeyValueChangeKey, NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, + NSKeyValueObservingOptions, NSObject, NSObjectNSDelayedPerforming, + NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, }; use tracing::{trace, warn}; @@ -439,9 +439,15 @@ declare_class!( // NOTE: We don't _really_ need to check the key path, as there should only be one, but // in the future we might want to observe other key paths. if key_path == Some(ns_string!("effectiveAppearance")) { - let change = change.expect("requested a change dictionary in `addObserver`, but none was provided"); - let old = change.get(unsafe { NSKeyValueChangeOldKey }).expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`"); - let new = change.get(unsafe { NSKeyValueChangeNewKey }).expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`"); + let change = change.expect( + "requested a change dictionary in `addObserver`, but none was provided", + ); + let old = change + .get(unsafe { NSKeyValueChangeOldKey }) + .expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`"); + let new = change + .get(unsafe { NSKeyValueChangeNewKey }) + .expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`"); // SAFETY: The value of `effectiveAppearance` is `NSAppearance` let old: *const AnyObject = old; @@ -930,10 +936,10 @@ impl WindowDelegate { LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()) } - pub fn inner_position(&self) -> PhysicalPosition { + pub fn surface_position(&self) -> Result, RequestError> { let content_rect = self.window().contentRectForFrameRect(self.window().frame()); - let position = flip_window_screen_coordinates(content_rect); - LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()) + let logical = LogicalPosition::new(content_rect.origin.x, content_rect.origin.y); + logical.to_physical(self.scale_factor()) } pub fn set_outer_position(&self, position: Position) { @@ -959,6 +965,13 @@ impl WindowDelegate { logical.to_physical(self.scale_factor()) } + pub fn safe_area(&self) -> (PhysicalPosition, PhysicalSize) { + let safe_rect = unsafe { self.view().safeAreaRect() }; + let position = LogicalPosition::new(safe_rect.origin.x, safe_rect.origin.y); + let size = LogicalSize::new(safe_rect.size.width, safe_rect.size.height); + (position.to_physical(self.scale_factor()), size.to_physical(self.scale_factor())) + } + #[inline] pub fn request_surface_size(&self, size: Size) -> Option> { let scale_factor = self.scale_factor(); @@ -1155,13 +1168,12 @@ impl WindowDelegate { #[inline] pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), RequestError> { - let physical_window_position = self.inner_position(); - let scale_factor = self.scale_factor(); - let window_position = physical_window_position.to_logical::(scale_factor); - let logical_cursor_position = cursor_position.to_logical::(scale_factor); - let point = CGPoint { - x: logical_cursor_position.x + window_position.x, - y: logical_cursor_position.y + window_position.y, + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let window_position = flip_window_screen_coordinates(content_rect); + let cursor_position = cursor_position.to_logical::(self.scale_factor()); + let point = core_graphics::display::CGPoint { + x: window_position.x + cursor_position.x, + y: window_position.y + cursor_position.y, }; CGDisplay::warp_mouse_cursor_position(point) .map_err(|status| os_error!(format!("CGError {status}")))?; @@ -1742,12 +1754,15 @@ impl WindowExtMacOS for WindowDelegate { let screen = self.window().screen().expect("expected screen to be available"); self.window().setFrame_display(screen.frame(), true); + // Configure the safe area rectangle, to ensure that we don't obscure the notch. + if NSScreen::class().responds_to(sel!(safeAreaInsets)) { + unsafe { self.view().setAdditionalSafeAreaInsets(screen.safeAreaInsets()) }; + } + // Fullscreen windows can't be resized, minimized, or moved self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false); self.toggle_style_mask(NSWindowStyleMask::Resizable, false); self.window().setMovable(false); - - true } else { let new_mask = self.saved_style(); self.set_style_mask(new_mask); @@ -1760,11 +1775,22 @@ impl WindowExtMacOS for WindowDelegate { app.setPresentationOptions(presentation_opts); } + if NSScreen::class().responds_to(sel!(safeAreaInsets)) { + unsafe { + self.view().setAdditionalSafeAreaInsets(NSEdgeInsets { + top: 0.0, + left: 0.0, + bottom: 0.0, + right: 0.0, + }); + } + } + self.window().setFrame_display(frame, true); self.window().setMovable(true); - - true } + + true } #[inline] diff --git a/src/platform_impl/apple/uikit/window.rs b/src/platform_impl/apple/uikit/window.rs index b4ca0bc721..714fda1515 100644 --- a/src/platform_impl/apple/uikit/window.rs +++ b/src/platform_impl/apple/uikit/window.rs @@ -9,8 +9,8 @@ use objc2_foundation::{ CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol, }; use objc2_ui_kit::{ - UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation, - UIViewController, UIWindow, + UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen, + UIScreenOverscanCompensation, UIViewController, UIWindow, }; use tracing::{debug, warn}; @@ -159,20 +159,19 @@ impl Inner { pub fn pre_present_notify(&self) {} - pub fn inner_position(&self) -> PhysicalPosition { - let safe_area = self.safe_area_screen_space(); + pub fn surface_position(&self) -> PhysicalPosition { + let view_position = self.view.frame().origin; let position = - LogicalPosition { x: safe_area.origin.x as f64, y: safe_area.origin.y as f64 }; - let scale_factor = self.scale_factor(); - position.to_physical(scale_factor) + unsafe { self.window.convertPoint_fromView(view_position, Some(&self.view)) }; + let position = LogicalPosition::new(position.x, position.y); + position.to_physical(self.scale_factor()) } - pub fn outer_position(&self) -> PhysicalPosition { + pub fn outer_position(&self) -> Result, NotSupportedError> { let screen_frame = self.screen_frame(); let position = LogicalPosition { x: screen_frame.origin.x as f64, y: screen_frame.origin.y as f64 }; - let scale_factor = self.scale_factor(); - position.to_physical(scale_factor) + Ok(position.to_physical(self.scale_factor())) } pub fn set_outer_position(&self, physical_position: Position) { @@ -188,29 +187,41 @@ impl Inner { } pub fn surface_size(&self) -> PhysicalSize { - let scale_factor = self.scale_factor(); - let safe_area = self.safe_area_screen_space(); - let size = LogicalSize { - width: safe_area.size.width as f64, - height: safe_area.size.height as f64, - }; - size.to_physical(scale_factor) + let frame = self.view.frame(); + let size = LogicalSize::new(frame.size.width, frame.size.height); + size.to_physical(self.scale_factor()) } pub fn outer_size(&self) -> PhysicalSize { - let scale_factor = self.scale_factor(); let screen_frame = self.screen_frame(); - let size = LogicalSize { - width: screen_frame.size.width as f64, - height: screen_frame.size.height as f64, - }; - size.to_physical(scale_factor) + let size = LogicalSize::new(screen_frame.size.width, screen_frame.size.height); + size.to_physical(self.scale_factor()) } pub fn request_surface_size(&self, _size: Size) -> Option> { Some(self.surface_size()) } + pub fn safe_area(&self) -> (PhysicalPosition, PhysicalSize) { + let frame = self.view.frame(); + let safe_area = if app_state::os_capabilities().safe_area { + 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 position = LogicalPosition::new(safe_area.left, safe_area.top); + let size = LogicalSize::new( + frame.size.width - safe_area.left - safe_area.right, + frame.size.height - safe_area.top - safe_area.bottom, + ); + let scale_factor = self.scale_factor(); + (position.to_physical(scale_factor), size.to_physical(scale_factor)) + } + pub fn set_min_surface_size(&self, _dimensions: Option) { warn!("`Window::set_min_surface_size` is ignored on iOS") } @@ -606,12 +617,12 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.reset_dead_keys()); } - fn inner_position(&self) -> Result, RequestError> { - Ok(self.maybe_wait_on_main(|delegate| delegate.inner_position())) + fn surface_position(&self) -> PhysicalPosition { + self.maybe_wait_on_main(|delegate| delegate.surface_position()) } fn outer_position(&self) -> Result, RequestError> { - Ok(self.maybe_wait_on_main(|delegate| delegate.outer_position())) + self.maybe_wait_on_main(|delegate| delegate.outer_position()) } fn set_outer_position(&self, position: Position) { @@ -630,6 +641,10 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.outer_size()) } + fn safe_area(&self) -> (PhysicalPosition, PhysicalSize) { + self.maybe_wait_on_main(|delegate| delegate.safe_area()) + } + fn set_min_surface_size(&self, min_size: Option) { self.maybe_wait_on_main(|delegate| delegate.set_min_surface_size(min_size)) } @@ -890,7 +905,7 @@ impl Inner { impl Inner { fn screen_frame(&self) -> CGRect { - self.rect_to_screen_space(self.window.bounds()) + self.rect_to_screen_space(self.window.frame()) } fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { @@ -902,43 +917,6 @@ impl Inner { let screen_space = self.window.screen().coordinateSpace(); self.window.convertRect_fromCoordinateSpace(rect, &screen_space) } - - fn safe_area_screen_space(&self) -> CGRect { - let bounds = self.window.bounds(); - if app_state::os_capabilities().safe_area { - let safe_area = self.window.safeAreaInsets(); - let safe_bounds = CGRect { - origin: CGPoint { - x: bounds.origin.x + safe_area.left, - y: bounds.origin.y + safe_area.top, - }, - size: CGSize { - width: bounds.size.width - safe_area.left - safe_area.right, - height: bounds.size.height - safe_area.top - safe_area.bottom, - }, - }; - self.rect_to_screen_space(safe_bounds) - } else { - let screen_frame = self.rect_to_screen_space(bounds); - let status_bar_frame = { - let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); - #[allow(deprecated)] - app.statusBarFrame() - }; - let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { - (screen_frame.origin.y, screen_frame.size.height) - } else { - let y = status_bar_frame.size.height; - let height = screen_frame.size.height - - (status_bar_frame.size.height - screen_frame.origin.y); - (y, height) - }; - CGRect { - origin: CGPoint { x: screen_frame.origin.x, y }, - size: CGSize { width: screen_frame.size.width, height }, - } - } - } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/src/window.rs b/src/window.rs index 1ded68b146..ed1dfb43d2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -590,41 +590,33 @@ pub trait Window: AsAny + Send + Sync { // extension trait fn reset_dead_keys(&self); - /// Returns the position of the top-left hand corner of the window's client area relative to the - /// top-left hand corner of the desktop. + /// The position of the top-left hand corner of the surface relative to the top-left hand corner + /// of the window. /// - /// The same conditions that apply to [`Window::outer_position`] apply to this method. + /// This can be useful for figuring out the size of the window's decorations (such as buttons, + /// title, etc.). /// - /// ## Platform-specific - /// - /// - **iOS:** Returns the top left coordinates of the window's [safe area] in the screen space - /// coordinate system. - /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns - /// the same value as [`Window::outer_position`]._ - /// - **Android / Wayland:** Always returns [`RequestError::NotSupported`]. - /// - /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc - fn inner_position(&self) -> Result, RequestError>; + /// If the window does not have any decorations, and the surface is in the exact same position + /// as the window itself, this simply returns `(0, 0)`. + fn surface_position(&self) -> PhysicalPosition; - /// Returns the position of the top-left hand corner of the window relative to the - /// top-left hand corner of the desktop. + /// The position of the top-left hand corner of the window relative to the top-left hand corner + /// of the desktop. /// /// Note that the top-left hand corner of the desktop is not necessarily the same as /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner - /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. + /// of the desktop is the top-left hand corner of the primary monitor of the desktop. /// /// The coordinates can be negative if the top-left hand corner of the window is outside - /// of the visible screen region. + /// of the visible screen region, or on another monitor than the primary. /// /// ## Platform-specific /// - /// - **iOS:** Returns the top left coordinates of the window in the screen space coordinate - /// system. /// - **Web:** Returns the top-left coordinates relative to the viewport. /// - **Android / Wayland:** Always returns [`RequestError::NotSupported`]. fn outer_position(&self) -> Result, RequestError>; - /// Modifies the position of the window. + /// Sets the position of the window on the desktop. /// /// See [`Window::outer_position`] for more information about the coordinates. /// This automatically un-maximizes the window if it's maximized. @@ -654,16 +646,20 @@ pub trait Window: AsAny + Send + Sync { /// Returns the size of the window's render-able surface. /// - /// This is the dimensions you should pass to things like Wgpu or Glutin when configuring. + /// This is the dimensions you should pass to things like Wgpu or Glutin when configuring the + /// surface for drawing. See [`WindowEvent::SurfaceResized`] for listening to changes to this + /// field. + /// + /// Note that to ensure that your content is not obscured by things such as notches, you will + /// likely want to only draw inside a specific area of the surface, see [`Window::safe_area`] + /// for details. /// /// ## Platform-specific /// - /// - **iOS:** Returns the `PhysicalSize` of the window's [safe area] in screen space - /// coordinates. /// - **Web:** Returns the size of the canvas element. Doesn't account for CSS [`transform`]. /// - /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + /// [`WindowEvent::SurfaceResized`]: crate::event::WindowEvent::SurfaceResized fn surface_size(&self) -> PhysicalSize; /// Request the new size for the surface. @@ -710,11 +706,29 @@ pub trait Window: AsAny + Send + Sync { /// /// ## Platform-specific /// - /// - **iOS:** Returns the [`PhysicalSize`] of the window in screen space coordinates. /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as /// [`Window::surface_size`]._ fn outer_size(&self) -> PhysicalSize; + /// The area of the surface that is unobstructed. + /// + /// On some devices, especially mobile devices, the screen is not a perfect rectangle, and may + /// have rounded corners, notches, bezels, and so on. When drawing your content, you usually + /// want to draw your background and other such unimportant content on the entire surface, while + /// you will want to restrict important content such as text, interactable or visual indicators + /// to the part of the screen that is actually visible; for this, you use the safe area. + /// + /// The safe area is a rectangle that is defined relative to the origin at the top-left corner + /// of the surface, and the size extending downwards to the right. The area will not extend + /// beyond [the bounds of the surface][Window::surface_size]. + /// + /// ## Platform-specific + /// + /// - **Wayland / Android / Orbital:** Unimplemented, returns `((0, 0), surface_size)`. + /// - **macOS:** This must be used when using `set_simple_fullscreen` to prevent overlapping the + /// notch on newer devices. + fn safe_area(&self) -> (PhysicalPosition, PhysicalSize); + /// Sets a minimum dimensions of the window's surface. /// /// ```no_run @@ -987,8 +1001,8 @@ pub trait Window: AsAny + Send + Sync { fn set_window_icon(&self, window_icon: Option); /// Set the IME cursor editing area, where the `position` is the top left corner of that area - /// and `size` is the size of this area starting from the position. An example of such area - /// could be a input field in the UI or line in the editor. + /// in surface coordinates and `size` is the size of this area starting from the position. An + /// example of such area could be a input field in the UI or line in the editor. /// /// The windowing system could place a candidate box close to that area, but try to not obscure /// the specified area, so the user input to it stays visible. @@ -1218,7 +1232,7 @@ pub trait Window: AsAny + Send + Sync { /// - **iOS / Android / Web:** Always returns an [`RequestError::NotSupported`]. fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError>; - /// Show [window menu] at a specified position . + /// Show [window menu] at a specified position in surface coordinates. /// /// This is the context menu that is normally shown when interacting with /// the title bar. This is useful when implementing custom decorations.