diff --git a/Cargo.toml b/Cargo.toml index 8d8228685b..bbd6f8d1a1 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 268316f946..259a91d2bb 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 0000000000..2bfb009883 --- /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 0000000000..1171c53a39 --- /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..b1ed4c8274 --- /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 0c4e4544f3..6dd72ea3d8 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 04b7df00bc..2e6c17f57d 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 6f5589f5ce..3d87e2f8d9 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 mut buffer = self.surface.buffer_mut()?; - let color = match self.theme { - Theme::Light => WHITE, - Theme::Dark => DARK_GRAY, - }; + // 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 + }; + } + } + } - let mut buffer = self.surface.buffer_mut()?; - buffer.fill(color); + // 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 6c9ac5c9e2..4e91f91773 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 7c56417d4a..84837dbc76 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 bda0a32ba9..6502d97cbd 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 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 beeee0148d..79c0d57c8b 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 713c7cb700..0d07506a02 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 e70f5934b8..abc3bb2f48 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 ddbc371bda..62707277e8 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 21fa353e4d..a74cfcbb78 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 be4bef4536..e992e8072e 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 725cb72bcb..cb7317737a 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 70a286b81a..c935d162f7 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 004e8aa038..e3a72ad228 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 76c698363d..c800566ca5 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 39523e18bb..a7a8e920de 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 8ab4f7e846..c39d6ed4fd 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 df54b2bf36..b73c2045b7 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 0000000000..7ecdfc3970 --- /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 639dcfe49e..352a231a6a 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 a9a2c7612d..aee882c847 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 22af8cbc1d..cd16d3878a 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. + /// + /// 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)`. /// - /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc - fn inner_position(&self) -> Result, RequestError>; + /// [`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 c0527730e9..c454f23b9b 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"]