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.