From dbcdb6f1b43b6afc09458dd9d0f410abddb7ae41 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 21 Nov 2024 17:37:03 +0100 Subject: [PATCH] Add safe area and document coordinate systems (#3890) 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. This is fully implemented on macOS and iOS, and partially on the web. Co-authored-by: daxpedda --- Cargo.toml | 2 + docs/{res => }/ATTRIBUTION.md | 7 + docs/coordinate-systems.drawio | 130 ++++++++++++++++ docs/res/coordinate-systems-desktop.svg | 1 + docs/res/coordinate-systems-mobile.svg | 1 + dpi/CHANGELOG.md | 2 + dpi/src/lib.rs | 144 ++++++++++++++++++ examples/window.rs | 48 ++++-- src/changelog/unreleased.md | 8 +- src/event.rs | 11 +- src/lib.rs | 39 +++++ src/monitor.rs | 7 +- src/platform/macos.rs | 3 + src/platform_impl/android/mod.rs | 10 +- src/platform_impl/apple/appkit/window.rs | 10 +- .../apple/appkit/window_delegate.rs | 104 ++++++++++--- src/platform_impl/apple/uikit/view.rs | 48 +++--- src/platform_impl/apple/uikit/window.rs | 119 ++++++--------- src/platform_impl/linux/wayland/window/mod.rs | 11 +- src/platform_impl/linux/x11/util/geometry.rs | 11 +- src/platform_impl/linux/x11/window.rs | 32 ++-- src/platform_impl/orbital/window.rs | 20 ++- src/platform_impl/web/event_loop/runner.rs | 10 +- src/platform_impl/web/web_sys/canvas.rs | 4 +- src/platform_impl/web/web_sys/mod.rs | 2 + src/platform_impl/web/web_sys/safe_area.rs | 56 +++++++ src/platform_impl/web/window.rs | 39 ++++- src/platform_impl/windows/window.rs | 16 +- src/window.rs | 111 ++++++++++---- typos.toml | 3 + 30 files changed, 797 insertions(+), 212 deletions(-) rename docs/{res => }/ATTRIBUTION.md (66%) create mode 100644 docs/coordinate-systems.drawio create mode 100644 docs/res/coordinate-systems-desktop.svg create mode 100644 docs/res/coordinate-systems-mobile.svg create mode 100644 src/platform_impl/web/web_sys/safe_area.rs diff --git a/Cargo.toml b/Cargo.toml index 8d822868..bbd6f8d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,7 @@ objc2-foundation = { version = "0.2.2", features = [ "NSDictionary", "NSDistributedNotificationCenter", "NSEnumerator", + "NSGeometry", "NSKeyValueObserving", "NSNotification", "NSObjCRuntime", @@ -296,6 +297,7 @@ web_sys = { package = "web-sys", version = "0.3.70", features = [ "FocusEvent", "HtmlCanvasElement", "HtmlElement", + "HtmlHtmlElement", "HtmlImageElement", "ImageBitmap", "ImageBitmapOptions", diff --git a/docs/res/ATTRIBUTION.md b/docs/ATTRIBUTION.md similarity index 66% rename from docs/res/ATTRIBUTION.md rename to docs/ATTRIBUTION.md index 268316f9..259a91d2 100644 --- a/docs/res/ATTRIBUTION.md +++ b/docs/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/coordinate-systems.drawio b/docs/coordinate-systems.drawio new file mode 100644 index 00000000..2bfb0098 --- /dev/null +++ b/docs/coordinate-systems.drawio @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/res/coordinate-systems-desktop.svg b/docs/res/coordinate-systems-desktop.svg new file mode 100644 index 00000000..1171c53a --- /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 00000000..b1ed4c82 --- /dev/null +++ b/docs/res/coordinate-systems-mobile.svg @@ -0,0 +1 @@ +
surface_size
surfa...
safe_area.top
safe_...
safe_area.bottom
safe_...
diff --git a/dpi/CHANGELOG.md b/dpi/CHANGELOG.md index 0c4e4544..6dd72ea3 100644 --- a/dpi/CHANGELOG.md +++ b/dpi/CHANGELOG.md @@ -11,6 +11,8 @@ Unreleased` header. ## Unreleased +- Added `Insets`, `LogicalInsets` and `PhysicalInsets` types. + ## 0.1.1 - Derive `Debug`, `Copy`, `Clone`, `PartialEq`, `Serialize`, `Deserialize` traits for `PixelUnit`. diff --git a/dpi/src/lib.rs b/dpi/src/lib.rs index 04b7df00..2e6c17f5 100644 --- a/dpi/src/lib.rs +++ b/dpi/src/lib.rs @@ -759,6 +759,150 @@ impl From> for Position { } } +/// The logical distance between the edges of two rectangles. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LogicalInsets

{ + /// The distance to the top edge. + pub top: P, + /// The distance to the left edge. + pub left: P, + /// The distance to the bottom edge. + pub bottom: P, + /// The distance to the right edge. + pub right: P, +} + +impl

LogicalInsets

{ + #[inline] + pub const fn new(top: P, left: P, bottom: P, right: P) -> Self { + Self { top, left, bottom, right } + } +} + +impl LogicalInsets

{ + #[inline] + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) + } + + #[inline] + pub fn to_physical(&self, scale_factor: f64) -> PhysicalInsets { + assert!(validate_scale_factor(scale_factor)); + let top = self.top.into() * scale_factor; + let left = self.left.into() * scale_factor; + let bottom = self.bottom.into() * scale_factor; + let right = self.right.into() * scale_factor; + PhysicalInsets::new(top, left, bottom, right).cast() + } + + #[inline] + pub fn cast(&self) -> LogicalInsets { + LogicalInsets { + top: self.top.cast(), + left: self.left.cast(), + bottom: self.bottom.cast(), + right: self.right.cast(), + } + } +} + +/// The physical distance between the edges of two rectangles. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PhysicalInsets

{ + /// The distance to the top edge. + pub top: P, + /// The distance to the left edge. + pub left: P, + /// The distance to the bottom edge. + pub bottom: P, + /// The distance to the right edge. + pub right: P, +} + +impl

PhysicalInsets

{ + #[inline] + pub const fn new(top: P, left: P, bottom: P, right: P) -> Self { + Self { top, left, bottom, right } + } +} + +impl PhysicalInsets

{ + #[inline] + pub fn from_logical>, X: Pixel>( + logical: T, + scale_factor: f64, + ) -> Self { + logical.into().to_physical(scale_factor) + } + + #[inline] + pub fn to_logical(&self, scale_factor: f64) -> LogicalInsets { + assert!(validate_scale_factor(scale_factor)); + let top = self.top.into() / scale_factor; + let left = self.left.into() / scale_factor; + let bottom = self.bottom.into() / scale_factor; + let right = self.right.into() / scale_factor; + LogicalInsets::new(top, left, bottom, right).cast() + } + + #[inline] + pub fn cast(&self) -> PhysicalInsets { + PhysicalInsets { + top: self.top.cast(), + left: self.left.cast(), + bottom: self.bottom.cast(), + right: self.right.cast(), + } + } +} + +/// Insets that are either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Insets { + Physical(PhysicalInsets), + Logical(LogicalInsets), +} + +impl Insets { + pub fn new>(insets: S) -> Self { + insets.into() + } + + pub fn to_logical(&self, scale_factor: f64) -> LogicalInsets

{ + match *self { + Self::Physical(insets) => insets.to_logical(scale_factor), + Self::Logical(insets) => insets.cast(), + } + } + + pub fn to_physical(&self, scale_factor: f64) -> PhysicalInsets

{ + match *self { + Self::Physical(insets) => insets.cast(), + Self::Logical(insets) => insets.to_physical(scale_factor), + } + } +} + +impl From> for Insets { + #[inline] + fn from(insets: PhysicalInsets

) -> Self { + Self::Physical(insets.cast()) + } +} + +impl From> for Insets { + #[inline] + fn from(insets: LogicalInsets

) -> Self { + Self::Logical(insets.cast()) + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/examples/window.rs b/examples/window.rs index 6f5589f5..3d87e2f8 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -242,6 +242,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(), @@ -941,18 +945,38 @@ impl WindowState { return Ok(()); } - const WHITE: u32 = 0xffffffff; - const DARK_GRAY: u32 = 0xff181818; - - let color = match self.theme { - Theme::Light => WHITE, - Theme::Dark => DARK_GRAY, - }; - let mut buffer = self.surface.buffer_mut()?; - buffer.fill(color); + + // Draw a different color inside the safe area + let surface_size = self.window.surface_size(); + let insets = self.window.safe_area(); + for y in 0..surface_size.height { + for x in 0..surface_size.width { + let index = y as usize * surface_size.width as usize + x as usize; + if insets.left <= x + && x <= (surface_size.width - insets.right) + && insets.top <= y + && y <= (surface_size.height - insets.bottom) + { + // In safe area + buffer[index] = match self.theme { + Theme::Light => 0xffe8e8e8, // Light gray + Theme::Dark => 0xff525252, // Medium gray + }; + } else { + // Outside safe area + buffer[index] = match self.theme { + Theme::Light => 0xffffffff, // White + Theme::Dark => 0xff181818, // Dark gray + }; + } + } + } + + // Present the buffer self.window.pre_present_notify(); buffer.present()?; + Ok(()) } @@ -989,6 +1013,8 @@ enum Action { ToggleDecorations, ToggleResizable, ToggleFullscreen, + #[cfg(macos_platform)] + ToggleSimpleFullscreen, ToggleMaximize, Minimize, NextCursor, @@ -1022,6 +1048,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", @@ -1164,6 +1192,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 6c9ac5c9..4e91f917 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -73,6 +73,8 @@ changelog entry. - Add `DeviceId::into_raw()` and `from_raw()`. - On X11, the `window` example now understands the `X11_VISUAL_ID` and `X11_SCREEN_ID` env variables to test the respective modifiers of window creation. +- 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 @@ -157,7 +159,7 @@ changelog entry. identify a finger in a multi-touch interaction. Replaces the old `Touch::id`. - In the same spirit rename `DeviceEvent::MouseMotion` to `PointerMotion`. - Remove `Force::Calibrated::altitude_angle`. - - On X11, use bottom-right corner for IME hotspot in `Window::set_ime_cursor_area`. +- On X11, use bottom-right corner for IME hotspot in `Window::set_ime_cursor_area`. ### Removed @@ -189,6 +191,7 @@ changelog entry. - Remove `WindowEvent::Touch` and `Touch` in favor of the new `PointerKind`, `PointerSource` and `ButtonSource` as part of the new pointer event overhaul. - Remove `Force::altitude_angle`. +- Removed `Window::inner_position`, use the new `Window::surface_position` instead. ### Fixed @@ -201,4 +204,5 @@ changelog entry. - On Windows, make `ControlFlow::WaitUntil` work more precisely using `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`. - On X11, creating windows on screen that is not the first one (e.g. `DISPLAY=:0.1`) works again. - On X11, creating windows while passing `with_x11_screen(non_default_screen)` works again. -- On X11, fix XInput handling that prevented a new window from getting the focus in some cases. \ No newline at end of file +- On X11, fix XInput handling that prevented a new window from getting the focus in some cases. +- On iOS, fixed `SurfaceResized` and `Window::surface_size` not reporting the size of the actual surface. diff --git a/src/event.rs b/src/event.rs index 7c56417d..84837dbc 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 /// @@ -469,13 +472,15 @@ pub enum WindowEvent { /// Emitted when a window should be redrawn. /// - /// This gets triggered in two scenarios: + /// This gets triggered in a few scenarios: /// - The OS has performed an operation that's invalidated the window's contents (such as - /// resizing the window). + /// resizing the window, or changing [the safe area]). /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. /// /// Winit will aggregate duplicate redraw requests into a single event, to /// help avoid duplicating rendering work. + /// + /// [the safe area]: crate::window::Window::safe_area RedrawRequested, } diff --git a/src/lib.rs b/src/lib.rs index bda0a32b..6502d97c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,45 @@ //! [`visible` set to `false`][crate::window::WindowAttributes::with_visible] and explicitly make //! the window visible only once you're ready to render into it. //! +//! There is another important concept you need to know about when drawing: 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 only being drawn inside the safe area. +//! +//! [`Window::safe_area`]: crate::window::Window::safe_area +//! +//! # 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, adding them like this works around that. +//! 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. +//! +//! This is illustrated in the image below, along with the safe area since it's often relevant on +//! mobile. +#![doc = concat!("\n\n", include_str!("../docs/res/coordinate-systems-mobile.svg"), "\n\n")] // Rustfmt removes \n, adding them like this works around that. +//! [`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 +//! //! # 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 898f9b20..aeb64b4a 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 beeee014..79c0d57c 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -92,6 +92,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/android/mod.rs b/src/platform_impl/android/mod.rs index 713c7cb7..0d07506a 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -13,7 +13,7 @@ use tracing::{debug, trace, warn}; use crate::application::ApplicationHandler; use crate::cursor::Cursor; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{EventLoopError, NotSupportedError, RequestError}; use crate::event::{self, DeviceId, FingerId, Force, StartCause, SurfaceSizeWriter}; use crate::event_loop::{ @@ -833,8 +833,8 @@ impl CoreWindow for Window { fn pre_present_notify(&self) {} - fn inner_position(&self) -> Result, RequestError> { - Err(NotSupportedError::new("inner_position is not supported").into()) + fn surface_position(&self) -> PhysicalPosition { + (0, 0).into() } fn outer_position(&self) -> Result, RequestError> { @@ -857,6 +857,10 @@ impl CoreWindow for Window { screen_size(&self.app) } + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + fn set_min_surface_size(&self, _: Option) {} fn set_max_surface_size(&self, _: Option) {} diff --git a/src/platform_impl/apple/appkit/window.rs b/src/platform_impl/apple/appkit/window.rs index e70f5934..abc3bb2f 100644 --- a/src/platform_impl/apple/appkit/window.rs +++ b/src/platform_impl/apple/appkit/window.rs @@ -107,12 +107,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) { @@ -131,6 +131,10 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.outer_size()) } + fn safe_area(&self) -> dpi::PhysicalInsets { + 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 ddbc371b..62707277 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}; @@ -21,10 +21,10 @@ use objc2_app_kit::{ NSWindowToolbarStyle, }; 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}; @@ -35,7 +35,10 @@ use super::observer::RunLoop; use super::view::WinitView; use super::window::WinitWindow; use super::{ffi, Fullscreen, MonitorHandle}; -use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{ + LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, + Position, Size, +}; use crate::error::{NotSupportedError, RequestError}; use crate::event::{SurfaceSizeWriter, WindowEvent}; use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; @@ -442,9 +445,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; @@ -561,6 +570,12 @@ fn new_window( } if attrs.platform_specific.fullsize_content_view { + // NOTE: If we decide to add an option to change this at runtime, we must emit a + // `SurfaceResized` event to let applications know that the safe area changed. + // + // An alternative would be to add a `WindowEvent::SafeAreaChanged` event, this could be + // done with an observer on `safeAreaRect` / `contentLayoutRect`, see: + // masks |= NSWindowStyleMask::FullSizeContentView; } @@ -934,15 +949,15 @@ impl WindowDelegate { #[inline] pub fn pre_present_notify(&self) {} - pub fn outer_position(&self) -> PhysicalPosition { + pub fn outer_position(&self) -> Result, RequestError> { let position = flip_window_screen_coordinates(self.window().frame()); - LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()) + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) } - pub fn inner_position(&self) -> PhysicalPosition { + pub fn surface_position(&self) -> PhysicalPosition { 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) { @@ -968,6 +983,32 @@ impl WindowDelegate { logical.to_physical(self.scale_factor()) } + pub fn safe_area(&self) -> PhysicalInsets { + // Only available on macOS 11.0 + let insets = if self.view().respondsToSelector(sel!(safeAreaInsets)) { + // Includes NSWindowStyleMask::FullSizeContentView by default, and the notch because + // we've set it up with `additionalSafeAreaInsets`. + unsafe { self.view().safeAreaInsets() } + } else { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + // Includes NSWindowStyleMask::FullSizeContentView + // Convert from window coordinates to view coordinates + let safe_rect = unsafe { + self.view().convertRect_fromView(self.window().contentLayoutRect(), None) + }; + NSEdgeInsets { + top: safe_rect.origin.y - content_rect.origin.y, + left: safe_rect.origin.x - content_rect.origin.x, + bottom: (content_rect.size.height + content_rect.origin.x) + - (safe_rect.size.height + safe_rect.origin.x), + right: (content_rect.size.width + content_rect.origin.y) + - (safe_rect.size.width + safe_rect.origin.y), + } + }; + let insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right); + insets.to_physical(self.scale_factor()) + } + #[inline] pub fn request_surface_size(&self, size: Size) -> Option> { let scale_factor = self.scale_factor(); @@ -1164,13 +1205,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}")))?; @@ -1752,12 +1792,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); @@ -1770,11 +1813,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/view.rs b/src/platform_impl/apple/uikit/view.rs index 21fa353e..a74cfcbb 100644 --- a/src/platform_impl/apple/uikit/view.rs +++ b/src/platform_impl/apple/uikit/view.rs @@ -6,11 +6,12 @@ use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString}; use objc2_ui_kit::{ - UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, - UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, - UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, - UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, + UIEvent, UIForceTouchCapability, UIGestureRecognizer, UIGestureRecognizerDelegate, + UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, UIPinchGestureRecognizer, + UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITextInputTraits, UITouch, + UITouchPhase, UITouchType, UITraitEnvironment, UIView, }; +use tracing::debug; use super::app_state::{self, EventWrapper}; use super::window::WinitUIWindow; @@ -72,26 +73,15 @@ declare_class!( let mtm = MainThreadMarker::new().unwrap(); let _: () = unsafe { msg_send![super(self), layoutSubviews] }; - let window = self.window().unwrap(); - let window_bounds = window.bounds(); - let screen = window.screen(); - let screen_space = screen.coordinateSpace(); - let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space); - let scale_factor = screen.scale(); + let frame = self.frame(); + let scale_factor = self.contentScaleFactor() as f64; let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as f64, - height: screen_frame.size.height as f64, - } - .to_physical(scale_factor as f64); - - // If the app is started in landscape, the view frame and window bounds can be mismatched. - // The view frame will be in portrait and the window bounds in landscape. So apply the - // window bounds to the view frame to make it consistent. - let view_frame = self.frame(); - if view_frame != window_bounds { - self.setFrame(window_bounds); + width: frame.size.width as f64, + height: frame.size.height as f64, } + .to_physical(scale_factor); + let window = self.window().unwrap(); app_state::handle_nonuser_event( mtm, EventWrapper::StaticEvent(Event::WindowEvent { @@ -126,13 +116,10 @@ declare_class!( "invalid scale_factor set on UIView", ); let scale_factor = scale_factor as f64; - let bounds = self.bounds(); - let screen = window.screen(); - let screen_space = screen.coordinateSpace(); - let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); + let frame = self.frame(); let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as f64, - height: screen_frame.size.height as f64, + width: frame.size.width as f64, + height: frame.size.height as f64, }; let window_id = window.id(); app_state::handle_nonuser_events( @@ -153,6 +140,13 @@ declare_class!( ); } + #[method(safeAreaInsetsDidChange)] + fn safe_area_changed(&self) { + debug!("safeAreaInsetsDidChange was called, requesting redraw"); + // When the safe area changes we want to make sure to emit a redraw event + self.setNeedsDisplay(); + } + #[method(touchesBegan:withEvent:)] fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) diff --git a/src/platform_impl/apple/uikit/window.rs b/src/platform_impl/apple/uikit/window.rs index be4bef45..e992e807 100644 --- a/src/platform_impl/apple/uikit/window.rs +++ b/src/platform_impl/apple/uikit/window.rs @@ -8,8 +8,8 @@ use objc2_foundation::{ CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObject, NSObjectProtocol, }; use objc2_ui_kit::{ - UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation, - UIViewController, UIWindow, + UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen, + UIScreenOverscanCompensation, UIViewController, UIWindow, }; use tracing::{debug, warn}; @@ -18,7 +18,10 @@ use super::view::WinitView; use super::view_controller::WinitViewController; use super::{app_state, monitor, ActiveEventLoop, Fullscreen, MonitorHandle}; use crate::cursor::Cursor; -use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{ + LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, + Position, Size, +}; use crate::error::{NotSupportedError, RequestError}; use crate::event::{Event, WindowEvent}; use crate::icon::Icon; @@ -158,20 +161,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, RequestError> { 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) { @@ -187,29 +189,36 @@ 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 frame = self.window.frame(); + let size = LogicalSize::new(frame.size.width, 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) -> PhysicalInsets { + // Only available on iOS 11.0 + let insets = 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 insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right); + insets.to_physical(self.scale_factor()) + } + pub fn set_min_surface_size(&self, _dimensions: Option) { warn!("`Window::set_min_surface_size` is ignored on iOS") } @@ -513,14 +522,9 @@ impl Window { let scale_factor = view.contentScaleFactor(); let scale_factor = scale_factor as f64; if scale_factor != 1.0 { - let bounds = view.bounds(); - let screen = window.screen(); - let screen_space = screen.coordinateSpace(); - let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); - let size = LogicalSize { - width: screen_frame.size.width as f64, - height: screen_frame.size.height as f64, - }; + let frame = view.frame(); + let size = + LogicalSize { width: frame.size.width as f64, height: frame.size.height as f64 }; app_state::handle_nonuser_events( mtm, std::iter::once(EventWrapper::ScaleFactorChanged(app_state::ScaleFactorChanged { @@ -599,12 +603,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) { @@ -623,6 +627,10 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.outer_size()) } + fn safe_area(&self) -> PhysicalInsets { + 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)) } @@ -881,7 +889,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 { @@ -893,43 +901,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(Clone, Debug, Default, PartialEq)] diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 725cb72b..cb731773 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -17,7 +17,7 @@ use super::output::MonitorHandle; use super::state::WinitState; use super::types::xdg_activation::XdgActivationTokenData; use super::ActiveEventLoop; -use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::event::{Ime, WindowEvent}; use crate::event_loop::AsyncRequestSerial; @@ -303,9 +303,8 @@ impl CoreWindow for Window { crate::platform_impl::common::xkb::reset_dead_keys() } - fn inner_position(&self) -> Result, RequestError> { - Err(NotSupportedError::new("window position information is not available on Wayland") - .into()) + fn surface_position(&self) -> PhysicalPosition { + (0, 0).into() } fn outer_position(&self) -> Result, RequestError> { @@ -336,6 +335,10 @@ impl CoreWindow for Window { super::logical_to_physical_rounded(window_state.outer_size(), scale_factor) } + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + fn set_min_surface_size(&self, min_size: Option) { let scale_factor = self.scale_factor(); let min_size = min_size.map(|size| size.to_logical(scale_factor)); diff --git a/src/platform_impl/linux/x11/util/geometry.rs b/src/platform_impl/linux/x11/util/geometry.rs index 70a286b8..c935d162 100644 --- a/src/platform_impl/linux/x11/util/geometry.rs +++ b/src/platform_impl/linux/x11/util/geometry.rs @@ -67,15 +67,20 @@ pub struct FrameExtentsHeuristic { } impl FrameExtentsHeuristic { - pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { + pub fn surface_position(&self) -> (i32, i32) { use self::FrameExtentsHeuristicPath::*; if self.heuristic_path != UnsupportedBordered { - (x - self.frame_extents.left as i32, y - self.frame_extents.top as i32) + (self.frame_extents.left as i32, self.frame_extents.top as i32) } else { - (x, y) + (0, 0) } } + pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { + let (left, top) = self.surface_position(); + (x - left, y - top) + } + pub fn surface_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { ( width.saturating_add( diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 004e8aa0..e3a72ad2 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -21,7 +21,7 @@ use super::{ ffi, ActiveEventLoop, CookieResultExt, ImeRequest, ImeSender, VoidCookie, XConnection, }; use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::event::{Event, SurfaceSizeWriter, WindowEvent}; use crate::event_loop::AsyncRequestSerial; @@ -82,8 +82,8 @@ impl CoreWindow for Window { common::xkb::reset_dead_keys(); } - fn inner_position(&self) -> Result, RequestError> { - self.0.inner_position() + fn surface_position(&self) -> PhysicalPosition { + self.0.surface_position() } fn outer_position(&self) -> Result, RequestError> { @@ -106,6 +106,10 @@ impl CoreWindow for Window { self.0.outer_size() } + fn safe_area(&self) -> PhysicalInsets { + self.0.safe_area() + } + fn set_min_surface_size(&self, min_size: Option) { self.0.set_min_surface_size(min_size) } @@ -1508,7 +1512,7 @@ impl UnownedWindow { } } - pub(crate) fn inner_position_physical(&self) -> (i32, i32) { + fn inner_position_physical(&self) -> (i32, i32) { // This should be okay to unwrap since the only error XTranslateCoordinates can return // is BadWindow, and if the window handle is bad we have bigger problems. self.xconn @@ -1518,8 +1522,14 @@ impl UnownedWindow { } #[inline] - pub fn inner_position(&self) -> Result, RequestError> { - Ok(self.inner_position_physical().into()) + pub fn surface_position(&self) -> PhysicalPosition { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + extents.surface_position().into() + } else { + self.update_cached_frame_extents(); + self.surface_position() + } } pub(crate) fn set_position_inner( @@ -1582,6 +1592,10 @@ impl UnownedWindow { } } + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + pub(crate) fn request_surface_size_physical(&self, width: u32, height: u32) { self.xconn .xcb_connection() @@ -1989,7 +2003,7 @@ impl UnownedWindow { .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) .map_err(|err| os_error!(err))?; - let window_position = self.inner_position()?; + let window_position = self.inner_position_physical(); let atoms = self.xconn.atoms(); let message = atoms[_NET_WM_MOVERESIZE]; @@ -2016,8 +2030,8 @@ impl UnownedWindow { | xproto::EventMask::SUBSTRUCTURE_NOTIFY, ), [ - (window_position.x + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, - (window_position.y + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, + (window_position.0 + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, + (window_position.1 + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, action.try_into().unwrap(), 1, // Button 1 1, diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 76c69836..c800566c 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use super::event_loop::EventLoopProxy; use super::{ActiveEventLoop, MonitorHandle, RedoxSocket, WindowProperties}; use crate::cursor::Cursor; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::monitor::MonitorHandle as CoreMonitorHandle; use crate::window::{self, Fullscreen, ImePurpose, Window as CoreWindow, WindowId}; @@ -198,17 +198,17 @@ impl CoreWindow for Window { } #[inline] - fn inner_position(&self) -> Result, RequestError> { - let mut buf: [u8; 4096] = [0; 4096]; - let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); - let properties = WindowProperties::new(path); - Ok((properties.x, properties.y).into()) + fn surface_position(&self) -> PhysicalPosition { + // TODO: adjust for window decorations + (0, 0).into() } #[inline] fn outer_position(&self) -> Result, RequestError> { - // TODO: adjust for window decorations - self.inner_position() + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + Ok((properties.x, properties.y).into()) } #[inline] @@ -239,6 +239,10 @@ impl CoreWindow for Window { self.surface_size() } + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + #[inline] fn set_min_surface_size(&self, _: Option) {} diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 39523e18..a7a8e920 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -20,7 +20,7 @@ use crate::dpi::PhysicalSize; use crate::event::{DeviceEvent, ElementState, Event, RawKeyEvent, StartCause, WindowEvent}; use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::platform::web::{PollStrategy, WaitUntilStrategy}; -use crate::platform_impl::platform::backend::EventListenerHandle; +use crate::platform_impl::platform::backend::{EventListenerHandle, SafeAreaHandle}; use crate::platform_impl::platform::r#async::DispatchRunner; use crate::platform_impl::platform::window::Inner; use crate::window::WindowId; @@ -57,6 +57,7 @@ struct Execution { redraw_pending: RefCell>, destroy_pending: RefCell>, pub(crate) monitor: Rc, + safe_area: Rc, page_transition_event_handle: RefCell>, device_events: Cell, on_mouse_move: OnEventHandle, @@ -151,6 +152,8 @@ impl Shared { WeakShared(weak.clone()), ); + let safe_area = SafeAreaHandle::new(&window, &document); + Execution { main_thread, event_loop_proxy: Arc::new(proxy_spawner), @@ -170,6 +173,7 @@ impl Shared { redraw_pending: RefCell::new(HashSet::new()), destroy_pending: RefCell::new(VecDeque::new()), monitor: Rc::new(monitor), + safe_area: Rc::new(safe_area), page_transition_event_handle: RefCell::new(None), device_events: Cell::default(), on_mouse_move: RefCell::new(None), @@ -826,6 +830,10 @@ impl Shared { pub(crate) fn monitor(&self) -> &Rc { &self.0.monitor } + + pub(crate) fn safe_area(&self) -> &Rc { + &self.0.safe_area + } } #[derive(Clone, Debug)] diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 8ab4f7e8..c39d6ed4 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -72,8 +72,8 @@ pub struct Common { #[derive(Clone, Debug)] pub struct Style { - read: CssStyleDeclaration, - write: CssStyleDeclaration, + pub(super) read: CssStyleDeclaration, + pub(super) write: CssStyleDeclaration, } impl Canvas { diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index df54b2bf..b73c2045 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -7,6 +7,7 @@ mod intersection_handle; mod media_query_handle; mod pointer; mod resize_scaling; +mod safe_area; mod schedule; use std::cell::OnceCell; @@ -20,6 +21,7 @@ use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, Visib pub use self::canvas::{Canvas, Style}; pub use self::event_handle::EventListenerHandle; pub use self::resize_scaling::ResizeScaleHandle; +pub use self::safe_area::SafeAreaHandle; pub use self::schedule::Schedule; use crate::dpi::{LogicalPosition, LogicalSize}; diff --git a/src/platform_impl/web/web_sys/safe_area.rs b/src/platform_impl/web/web_sys/safe_area.rs new file mode 100644 index 00000000..7ecdfc39 --- /dev/null +++ b/src/platform_impl/web/web_sys/safe_area.rs @@ -0,0 +1,56 @@ +use dpi::{LogicalPosition, LogicalSize}; +use wasm_bindgen::JsCast; +use web_sys::{Document, HtmlHtmlElement, Window}; + +use super::Style; + +pub struct SafeAreaHandle { + style: Style, +} + +impl SafeAreaHandle { + pub fn new(window: &Window, document: &Document) -> Self { + let document: HtmlHtmlElement = document.document_element().unwrap().unchecked_into(); + #[allow(clippy::disallowed_methods)] + let write = document.style(); + write + .set_property( + "--__winit_safe_area", + "env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) \ + env(safe-area-inset-left)", + ) + .expect("unexpected read-only declaration block"); + #[allow(clippy::disallowed_methods)] + let read = window + .get_computed_style(&document) + .expect("failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("invalid pseudo-element"); + + SafeAreaHandle { style: Style { read, write } } + } + + pub fn get(&self) -> (LogicalPosition, LogicalSize) { + let value = self.style.get("--__winit_safe_area"); + + let mut values = value + .split(' ') + .map(|value| value.strip_suffix("px").expect("unexpected unit other then `px` found")); + let top: f64 = values.next().unwrap().parse().unwrap(); + let right: f64 = values.next().unwrap().parse().unwrap(); + let bottom: f64 = values.next().unwrap().parse().unwrap(); + let left: f64 = values.next().unwrap().parse().unwrap(); + assert_eq!(values.next(), None, "unexpected fifth value"); + + let width = super::style_size_property(&self.style, "width") - left - right; + let height = super::style_size_property(&self.style, "height") - top - bottom; + + (LogicalPosition::new(left, top), LogicalSize::new(width, height)) + } +} + +impl Drop for SafeAreaHandle { + fn drop(&mut self) { + self.style.remove("--__winit_safe_area"); + } +} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 639dcfe4..352a231a 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -2,13 +2,14 @@ use std::cell::Ref; use std::rc::Rc; use std::sync::Arc; +use dpi::{LogicalPosition, LogicalSize}; use web_sys::HtmlCanvasElement; use super::main_thread::{MainThreadMarker, MainThreadSafe}; use super::monitor::MonitorHandler; use super::r#async::Dispatcher; use super::{backend, lock, ActiveEventLoop}; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{LogicalInsets, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMonitorHandle; @@ -26,6 +27,7 @@ pub struct Inner { id: WindowId, pub window: web_sys::Window, monitor: Rc, + safe_area: Rc, canvas: Rc, destroy_fn: Option>, } @@ -59,6 +61,7 @@ impl Window { id, window: window.clone(), monitor: Rc::clone(target.runner.monitor()), + safe_area: Rc::clone(target.runner.safe_area()), canvas, destroy_fn: Some(destroy_fn), }; @@ -109,9 +112,9 @@ impl RootWindow for Window { // Not supported } - fn inner_position(&self) -> Result, RequestError> { - // Note: the canvas element has no window decorations, so this is equal to `outer_position`. - self.outer_position() + fn surface_position(&self) -> PhysicalPosition { + // Note: the canvas element has no window decorations. + (0, 0).into() } fn outer_position(&self) -> Result, RequestError> { @@ -152,6 +155,34 @@ impl RootWindow for Window { self.surface_size() } + fn safe_area(&self) -> PhysicalInsets { + self.inner.queue(|inner| { + let (safe_start_pos, safe_size) = inner.safe_area.get(); + let safe_end_pos = LogicalPosition::new( + safe_start_pos.x + safe_size.width, + safe_start_pos.y + safe_size.height, + ); + + let surface_start_pos = inner.canvas.position(); + let surface_size = LogicalSize::new( + backend::style_size_property(inner.canvas.style(), "width"), + backend::style_size_property(inner.canvas.style(), "height"), + ); + let surface_end_pos = LogicalPosition::new( + surface_start_pos.x + surface_size.width, + surface_start_pos.y + surface_size.height, + ); + + let top = f64::max(safe_start_pos.y - surface_start_pos.y, 0.); + let left = f64::max(safe_start_pos.x - surface_start_pos.x, 0.); + let bottom = f64::max(surface_end_pos.y - safe_end_pos.y, 0.); + let right = f64::max(surface_end_pos.x - safe_end_pos.x, 0.); + + let insets = LogicalInsets::new(top, left, bottom, right); + insets.to_physical(inner.scale_factor()) + }) + } + fn set_min_surface_size(&self, min_size: Option) { self.inner.dispatch(move |inner| { let dimensions = min_size.map(|min_size| min_size.to_logical(inner.scale_factor())); diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index a9a2c761..aee882c8 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -46,7 +46,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ }; use crate::cursor::Cursor; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::icon::Icon; use crate::monitor::MonitorHandle as CoreMonitorHandle; @@ -416,15 +416,15 @@ impl CoreWindow for Window { ) } - fn inner_position(&self) -> Result, RequestError> { - let mut position: POINT = unsafe { mem::zeroed() }; - if unsafe { ClientToScreen(self.hwnd(), &mut position) } == false.into() { + fn surface_position(&self) -> PhysicalPosition { + let mut rect: RECT = unsafe { mem::zeroed() }; + if unsafe { GetClientRect(self.hwnd(), &mut rect) } == false.into() { panic!( - "Unexpected ClientToScreen failure: please report this error to \ + "Unexpected GetClientRect failure: please report this error to \ rust-windowing/winit" ) } - Ok(PhysicalPosition::new(position.x, position.y)) + PhysicalPosition::new(rect.left, rect.top) } fn set_outer_position(&self, position: Position) { @@ -494,6 +494,10 @@ impl CoreWindow for Window { None } + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + fn set_min_surface_size(&self, size: Option) { self.window_state_lock().min_size = size; // Make windows re-check the window size bounds. diff --git a/src/window.rs b/src/window.rs index 22af8cbc..cd16d387 100644 --- a/src/window.rs +++ b/src/window.rs @@ -7,7 +7,7 @@ pub use cursor_icon::{CursorIcon, ParseError as CursorIconParseError}; use serde::{Deserialize, Serialize}; pub use crate::cursor::{BadImage, Cursor, CustomCursor, CustomCursorSource, MAX_CURSOR_SIZE}; -use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::RequestError; pub use crate::icon::{BadIcon, Icon}; use crate::monitor::{MonitorHandle, VideoModeHandle}; @@ -574,41 +574,51 @@ 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, combined with [`outer_position`], can be useful for calculating the position of the + /// surface relative to the desktop. /// - /// ## Platform-specific + /// This may also be useful for figuring out the size of the window's decorations (such as + /// buttons, title, etc.), but may also not correspond to that (e.g. if the title bar is made + /// transparent using [`with_titlebar_transparent`] on macOS, or your are drawing window + /// decorations yourself). /// - /// - **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`]. + /// This may be negative. /// - /// [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)`. + /// + /// [`outer_position`]: Self::outer_position + #[cfg_attr( + any(macos_platform, docsrs), + doc = "[`with_titlebar_transparent`]: \ + crate::platform::macos::WindowAttributesExtMacOS::with_titlebar_transparent" + )] + #[cfg_attr( + not(any(macos_platform, docsrs)), + doc = "[`with_titlebar_transparent`]: #only-available-on-macos" + )] + 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. @@ -638,16 +648,21 @@ 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 or the title + /// bar, you will likely want to only draw important content inside a specific area of the + /// surface, see [`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 + /// [`safe_area()`]: Window::safe_area fn surface_size(&self) -> PhysicalSize; /// Request the new size for the surface. @@ -694,11 +709,53 @@ 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 inset 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]. + /// + /// Note that the safe area does not take occlusion from other windows into account; in a way, + /// it is only a "hardware"-level occlusion. + /// + /// If the entire content of the surface is visible, this returns `(0, 0, 0, 0)`. + /// + /// ## Platform-specific + /// + /// - **Android / Orbital / Wayland / Windows / X11:** Unimplemented, returns `(0, 0, 0, 0)`. + /// + /// ## Examples + /// + /// Convert safe area insets to a size and a position. + /// + /// ``` + /// use winit::dpi::{PhysicalPosition, PhysicalSize}; + /// + /// # let surface_size = dpi::PhysicalSize::new(0, 0); + /// # #[cfg(requires_window)] + /// let surface_size = window.surface_size(); + /// # let insets = dpi::PhysicalInsets::new(0, 0, 0, 0); + /// # #[cfg(requires_window)] + /// let insets = window.safe_area(); + /// + /// let origin = PhysicalPosition::new(insets.left, insets.top); + /// let size = PhysicalSize::new( + /// surface_size.width - insets.left - insets.right, + /// surface_size.height - insets.top - insets.bottom, + /// ); + /// ``` + fn safe_area(&self) -> PhysicalInsets; + /// Sets a minimum dimensions of the window's surface. /// /// ```no_run @@ -971,8 +1028,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. @@ -1203,7 +1260,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. diff --git a/typos.toml b/typos.toml index c0527730..c454f23b 100644 --- a/typos.toml +++ b/typos.toml @@ -5,3 +5,6 @@ TME_LEAVE = "TME_LEAVE" # From windows_sys::Win32::UI::Input::Keyboa XF86_Calculater = "XF86_Calculater" # From xkbcommon_dl::keysyms::XF86_Calculater ptd = "ptd" # From windows_sys::Win32::System::Com::FORMATETC { ptd, ..} requestor = "requestor" # From x11_dl::xlib::XSelectionEvent { requestor ..} + +[files] +extend-exclude = ["*.drawio"]