diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82094815db..955344727f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: steps: - uses: taiki-e/checkout-action@v1 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check log-level: error diff --git a/Cargo.toml b/Cargo.toml index 0f1a038068..34e2b6d7f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.30.5" +version = "0.30.6" authors = [ "The winit contributors", "Pierre Krieger ", @@ -14,7 +14,17 @@ rust-version.workspace = true repository.workspace = true license.workspace = true edition.workspace = true -exclude = ["/.cargo"] +include = [ + "/build.rs", + "/docs", + "/examples", + "/FEATURES.md", + "/LICENSE", + "/src", + "!/src/platform_impl/web/script", + "/src/platform_impl/web/script/**/*.min.js", + "/tests", +] [package.metadata.docs.rs] features = [ @@ -189,6 +199,8 @@ features = [ "UIEvent", "UIGeometry", "UIGestureRecognizer", + "UITextInput", + "UITextInputTraits", "UIOrientation", "UIPanGestureRecognizer", "UIPinchGestureRecognizer", @@ -220,6 +232,7 @@ features = [ "Win32_System_Com", "Win32_System_LibraryLoader", "Win32_System_Ole", + "Win32_Security", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", @@ -278,60 +291,63 @@ xkbcommon-dl = "0.4.2" orbclient = { version = "0.3.47", default-features = false } redox_syscall = "0.4.1" -[target.'cfg(target_family = "wasm")'.dependencies.web_sys] -package = "web-sys" -version = "0.3.64" -features = [ - 'AbortController', - 'AbortSignal', - 'Blob', - 'BlobPropertyBag', - 'console', - 'CssStyleDeclaration', - 'Document', - 'DomException', - 'DomRect', - 'DomRectReadOnly', - 'Element', - 'Event', - 'EventTarget', - 'FocusEvent', - 'HtmlCanvasElement', - 'HtmlElement', - 'HtmlImageElement', - 'ImageBitmap', - 'ImageBitmapOptions', - 'ImageBitmapRenderingContext', - 'ImageData', - 'IntersectionObserver', - 'IntersectionObserverEntry', - 'KeyboardEvent', - 'MediaQueryList', - 'MessageChannel', - 'MessagePort', - 'Navigator', - 'Node', - 'PageTransitionEvent', - 'PointerEvent', - 'PremultiplyAlpha', - 'ResizeObserver', - 'ResizeObserverBoxOptions', - 'ResizeObserverEntry', - 'ResizeObserverOptions', - 'ResizeObserverSize', - 'VisibilityState', - 'Window', - 'WheelEvent', - 'Worker', - 'Url', -] - [target.'cfg(target_family = "wasm")'.dependencies] -js-sys = "0.3.64" +js-sys = "0.3.70" pin-project = "1" -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" +wasm-bindgen = "0.2.93" +wasm-bindgen-futures = "0.4.43" web-time = "1" +web_sys = { package = "web-sys", version = "0.3.70", features = [ + "AbortController", + "AbortSignal", + "Blob", + "BlobPropertyBag", + "console", + "CssStyleDeclaration", + "Document", + "DomException", + "DomRect", + "DomRectReadOnly", + "Element", + "Event", + "EventTarget", + "FocusEvent", + "HtmlCanvasElement", + "HtmlElement", + "HtmlImageElement", + "ImageBitmap", + "ImageBitmapOptions", + "ImageBitmapRenderingContext", + "ImageData", + "IntersectionObserver", + "IntersectionObserverEntry", + "KeyboardEvent", + "MediaQueryList", + "MessageChannel", + "MessagePort", + "Navigator", + "Node", + "OrientationLockType", + "OrientationType", + "PageTransitionEvent", + "Permissions", + "PermissionState", + "PermissionStatus", + "PointerEvent", + "PremultiplyAlpha", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", + "Screen", + "ScreenOrientation", + "Url", + "VisibilityState", + "WheelEvent", + "Window", + "Worker", +] } [target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies] atomic-waker = "1" diff --git a/README.md b/README.md index 981dcebb24..236fa7d7a9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ```toml [dependencies] -winit = "0.30.5" +winit = "0.30.6" ``` ## [Documentation](https://docs.rs/winit) diff --git a/deny.toml b/deny.toml index 27fe32cd01..a6aef58f06 100644 --- a/deny.toml +++ b/deny.toml @@ -1,15 +1,20 @@ -# https://embarkstudios.github.io/cargo-deny/ +# https://embarkstudios.github.io/cargo-deny # cargo install cargo-deny -# cargo update && cargo deny --all-features --log-level error --target aarch64-apple-ios check +# cargo update && cargo deny --target aarch64-apple-ios check # Note: running just `cargo deny check` without a `--target` will result in # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 +[graph] +all-features = true +exclude-dev = true targets = [ { triple = "aarch64-apple-ios" }, { triple = "aarch64-linux-android" }, { triple = "i686-pc-windows-gnu" }, { triple = "i686-pc-windows-msvc" }, { triple = "i686-unknown-linux-gnu" }, - { triple = "wasm32-unknown-unknown" }, + { triple = "wasm32-unknown-unknown", features = [ + "atomics", + ] }, { triple = "x86_64-apple-darwin" }, { triple = "x86_64-apple-ios" }, { triple = "x86_64-pc-windows-gnu" }, @@ -18,45 +23,52 @@ targets = [ { triple = "x86_64-unknown-redox" }, ] - -[advisories] -vulnerability = "deny" -unmaintained = "warn" -yanked = "deny" -ignore = [] - +[licenses] +allow = [ + "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) + "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) + "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) + "ISC", # https://tldrlegal.com/license/isc-license + "MIT", # https://tldrlegal.com/license/mit-license + "Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html +] +confidence-threshold = 1.0 +private = { ignore = true } [bans] multiple-versions = "deny" -wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed -deny = [] skip = [ - { name = "raw-window-handle" }, # we intentionally have multiple versions of this - { name = "bitflags" }, # the ecosystem is in the process of migrating. + { crate = "raw-window-handle", reason = "we depend on multiple behind features" }, + { crate = "bitflags@1", reason = "the ecosystem is in the process of migrating" }, ] -skip-tree = [] +wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed +[bans.build] +include-archives = true +interpreted = "deny" -[licenses] -private = { ignore = true } -unlicensed = "deny" -allow-osi-fsf-free = "neither" -confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text -copyleft = "deny" +[[bans.build.bypass]] allow = [ - "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html - "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) - "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) - "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) - "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained - "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ - "ISC", # https://tldrlegal.com/license/-isc-license - "LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321 - "MIT-0", # https://choosealicense.com/licenses/mit-0/ - "MIT", # https://tldrlegal.com/license/mit-license - "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. - "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html - "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux - "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html - "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) + { path = "generate-bindings.sh", checksum = "268ec23248218d779e33853cdc60e2985e70214ff004716cd734270de1f6b561" }, ] +crate = "android-activity" + +[[bans.build.bypass]] +allow-globs = ["freetype2/*"] +crate = "freetype-sys" + +[[bans.build.bypass]] +allow-globs = ["lib/*.a"] +crate = "windows_i686_gnu" + +[[bans.build.bypass]] +allow-globs = ["lib/*.lib"] +crate = "windows_i686_msvc" + +[[bans.build.bypass]] +allow-globs = ["lib/*.a"] +crate = "windows_x86_64_gnu" + +[[bans.build.bypass]] +allow-globs = ["lib/*.lib"] +crate = "windows_x86_64_msvc" diff --git a/examples/window.rs b/examples/window.rs index 11f324713c..48afadf9f3 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -31,6 +31,8 @@ use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMac use winit::platform::startup_notify::{ self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, }; +#[cfg(x11_platform)] +use winit::platform::x11::WindowAttributesExtX11; #[path = "util/tracing.rs"] mod tracing; @@ -140,6 +142,28 @@ impl Application { window_attributes = window_attributes.with_activation_token(token); } + #[cfg(x11_platform)] + match std::env::var("X11_VISUAL_ID") { + Ok(visual_id_str) => { + info!("Using X11 visual id {visual_id_str}"); + let visual_id = visual_id_str.parse()?; + window_attributes = window_attributes.with_x11_visual(visual_id); + }, + Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"), + } + + #[cfg(x11_platform)] + match std::env::var("X11_SCREEN_ID") { + Ok(screen_id_str) => { + info!("Placing the window on X11 screen {screen_id_str}"); + let screen_id = screen_id_str.parse()?; + window_attributes = window_attributes.with_x11_screen(screen_id); + }, + Err(_) => info!( + "Set the X11_SCREEN_ID env variable to place the window on non-default screen" + ), + } + #[cfg(macos_platform)] if let Some(tab_id) = _tab_id { window_attributes = window_attributes.with_tabbing_identifier(&tab_id); diff --git a/src/changelog/v0.30.md b/src/changelog/v0.30.md index 11af4a99af..b51aaa95f6 100644 --- a/src/changelog/v0.30.md +++ b/src/changelog/v0.30.md @@ -1,3 +1,29 @@ +## 0.30.6 + +### Added + +- On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game` + to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games. +- 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. +- On Android, the soft keyboard can now be shown using `Window::set_ime_allowed`. +- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. + +### Fixed + +- On macOS, fix `WindowEvent::Moved` sometimes being triggered unnecessarily on resize. +- On macOS, package manifest definitions of `LSUIElement` will no longer be overridden with the + default activation policy, unless explicitly provided during initialization. +- On macOS, fix crash when calling `drag_window()` without a left click present. +- On X11, key events forward to IME anyway, even when it's disabled. +- 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. +- On macOS, fix crash when pressing Caps Lock in certain configurations. +- On iOS, fixed `MonitorHandle`'s `PartialEq` and `Hash` implementations. +- On macOS, fixed undocumented cursors (e.g. zoom, resize, help) always appearing to be invalid and falling back to the default cursor. + ## 0.30.5 ### Added diff --git a/src/event_loop.rs b/src/event_loop.rs index b5d5d26402..233374beec 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -501,7 +501,7 @@ unsafe impl rwh_05::HasRawDisplayHandle for ActiveEventLoop { /// A proxy for the underlying display handle. /// -/// The purpose of this type is to provide a cheaply clonable handle to the underlying +/// The purpose of this type is to provide a cheaply cloneable handle to the underlying /// display handle. This is often used by graphics APIs to connect to the underlying APIs. /// It is difficult to keep a handle to the [`EventLoop`] type or the [`ActiveEventLoop`] /// type. In contrast, this type involves no lifetimes and can be persisted for as long as diff --git a/src/lib.rs b/src/lib.rs index eb1bd68633..ba3e7cd179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,12 @@ //! //! ```no_run //! use winit::event_loop::EventLoop; -//! let event_loop = EventLoop::new().unwrap(); +//! +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! // ... +//! } //! ``` //! //! Then you create a [`Window`] with [`create_window`]. @@ -84,19 +89,22 @@ //! } //! } //! -//! let event_loop = EventLoop::new().unwrap(); +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); //! -//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't -//! // dispatched any events. This is ideal for games and similar applications. -//! event_loop.set_control_flow(ControlFlow::Poll); +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! event_loop.set_control_flow(ControlFlow::Poll); //! -//! // ControlFlow::Wait pauses the event loop if no events are available to process. -//! // This is ideal for non-game applications that only update in response to user -//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. -//! event_loop.set_control_flow(ControlFlow::Wait); +//! // ControlFlow::Wait pauses the event loop if no events are available to process. +//! // This is ideal for non-game applications that only update in response to user +//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. +//! event_loop.set_control_flow(ControlFlow::Wait); //! -//! let mut app = App::default(); -//! event_loop.run_app(&mut app); +//! let mut app = App::default(); +//! event_loop.run_app(&mut app); +//! } //! ``` //! //! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be diff --git a/src/platform/android.rs b/src/platform/android.rs index 823ef5a37b..5313215137 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -62,7 +62,7 @@ //! If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building //! with `cargo apk`, then the minimal changes would be: //! 1. Remove `ndk-glue` from your `Cargo.toml` -//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.5", +//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.6", //! features = [ "android-native-activity" ] }` //! 3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc //! macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 66f25c92b2..424c9d6213 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -94,6 +94,12 @@ pub trait WindowExtMacOS { /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. fn option_as_alt(&self) -> OptionAsAlt; + + /// Disable the Menu Bar and Dock in Borderless Fullscreen mode. Useful for games. + fn set_borderless_game(&self, borderless_game: bool); + + /// Getter for the [`WindowExtMacOS::set_borderless_game`]. + fn is_borderless_game(&self) -> bool; } impl WindowExtMacOS for Window { @@ -166,6 +172,16 @@ impl WindowExtMacOS for Window { fn option_as_alt(&self) -> OptionAsAlt { self.window.maybe_wait_on_main(|w| w.option_as_alt()) } + + #[inline] + fn set_borderless_game(&self, borderless_game: bool) { + self.window.maybe_wait_on_main(|w| w.set_borderless_game(borderless_game)) + } + + #[inline] + fn is_borderless_game(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_borderless_game()) + } } /// Corresponds to `NSApplicationActivationPolicy`. @@ -216,6 +232,8 @@ pub trait WindowAttributesExtMacOS { /// /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self; + /// See [`WindowExtMacOS::set_borderless_game`] for details on what this means if set. + fn with_borderless_game(self, borderless_game: bool) -> Self; } impl WindowAttributesExtMacOS for WindowAttributes { @@ -284,12 +302,21 @@ impl WindowAttributesExtMacOS for WindowAttributes { self.platform_specific.option_as_alt = option_as_alt; self } + + #[inline] + fn with_borderless_game(mut self, borderless_game: bool) -> Self { + self.platform_specific.borderless_game = borderless_game; + self + } } pub trait EventLoopBuilderExtMacOS { - /// Sets the activation policy for the application. + /// Sets the activation policy for the application. If used, this will override + /// any relevant settings provided in the package manifest. + /// For instance, `with_activation_policy(ActivationPolicy::Regular)` will prevent + /// the application from running as an "agent", even if LSUIElement is set to true. /// - /// It is set to [`ActivationPolicy::Regular`] by default. + /// If unused, the Winit will honor the package manifest. /// /// # Example /// @@ -341,7 +368,7 @@ pub trait EventLoopBuilderExtMacOS { impl EventLoopBuilderExtMacOS for EventLoopBuilder { #[inline] fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self { - self.platform_specific.activation_policy = activation_policy; + self.platform_specific.activation_policy = Some(activation_policy); self } diff --git a/src/platform/web.rs b/src/platform/web.rs index a48c6b09b4..f257ca416b 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -422,7 +422,7 @@ impl fmt::Display for BadAnimation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Empty => write!(f, "No cursors supplied"), - Self::Animation => write!(f, "A supplied cursor is an animtion"), + Self::Animation => write!(f, "A supplied cursor is an animation"), } } } diff --git a/src/platform/x11.rs b/src/platform/x11.rs index 749c400ab0..4ab900c94c 100644 --- a/src/platform/x11.rs +++ b/src/platform/x11.rs @@ -81,9 +81,7 @@ pub type XWindow = u32; #[inline] pub fn register_xlib_error_hook(hook: XlibErrorHook) { // Append new hook. - unsafe { - crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook); - } + crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook); } /// Additional methods on [`ActiveEventLoop`] that are specific to X11. diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 950e310299..bc0ad680e5 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -908,7 +908,13 @@ impl Window { pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} - pub fn set_ime_allowed(&self, _allowed: bool) {} + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + self.app.show_soft_input(true); + } else { + self.app.hide_soft_input(true); + } + } pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} diff --git a/src/platform_impl/apple/appkit/window.rs b/src/platform_impl/apple/appkit/window.rs new file mode 100644 index 0000000000..be59dbed99 --- /dev/null +++ b/src/platform_impl/apple/appkit/window.rs @@ -0,0 +1,369 @@ +#![allow(clippy::unnecessary_cast)] + +use dpi::{Position, Size}; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{NSResponder, NSWindow}; +use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject}; + +use super::event_loop::ActiveEventLoop; +use super::window_delegate::WindowDelegate; +use crate::error::RequestError; +use crate::monitor::MonitorHandle as CoreMonitorHandle; +use crate::window::{ + Cursor, Fullscreen, Icon, ImePurpose, Theme, UserAttentionType, Window as CoreWindow, + WindowAttributes, WindowButtons, WindowId, WindowLevel, +}; + +pub(crate) struct Window { + window: MainThreadBound>, + /// The window only keeps a weak reference to this, so we must keep it around here. + delegate: MainThreadBound>, +} + +impl Window { + pub(crate) fn new( + window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let mtm = window_target.mtm; + let delegate = + autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; + Ok(Window { + window: MainThreadBound::new(delegate.window().retain(), mtm), + delegate: MainThreadBound::new(delegate, mtm), + }) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&WindowDelegate) -> R + Send, + ) -> R { + self.delegate.get_on_main(|delegate| f(delegate)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.delegate.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new())) + } +} + +impl Drop for Window { + fn drop(&mut self) { + // Restore the video mode. + if matches!(self.fullscreen(), Some(Fullscreen::Exclusive(_))) { + self.set_fullscreen(None); + } + + self.window.get_on_main(|window| autoreleasepool(|_| window.close())) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for Window { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.raw_display_handle_rwh_06()?; + unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) } + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasWindowHandle for Window { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.raw_window_handle_rwh_06()?; + unsafe { Ok(rwh_06::WindowHandle::borrow_raw(raw)) } + } +} + +impl CoreWindow for Window { + fn id(&self) -> crate::window::WindowId { + self.maybe_wait_on_main(|delegate| delegate.id()) + } + + fn scale_factor(&self) -> f64 { + self.maybe_wait_on_main(|delegate| delegate.scale_factor()) + } + + fn request_redraw(&self) { + self.maybe_wait_on_main(|delegate| delegate.request_redraw()); + } + + fn pre_present_notify(&self) { + self.maybe_wait_on_main(|delegate| delegate.pre_present_notify()); + } + + fn reset_dead_keys(&self) { + 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 outer_position(&self) -> Result, RequestError> { + Ok(self.maybe_wait_on_main(|delegate| delegate.outer_position())) + } + + fn set_outer_position(&self, position: Position) { + self.maybe_wait_on_main(|delegate| delegate.set_outer_position(position)); + } + + fn surface_size(&self) -> dpi::PhysicalSize { + self.maybe_wait_on_main(|delegate| delegate.surface_size()) + } + + fn request_surface_size(&self, size: Size) -> Option> { + self.maybe_wait_on_main(|delegate| delegate.request_surface_size(size)) + } + + fn outer_size(&self) -> dpi::PhysicalSize { + self.maybe_wait_on_main(|delegate| delegate.outer_size()) + } + + fn set_min_surface_size(&self, min_size: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_min_surface_size(min_size)) + } + + fn set_max_surface_size(&self, max_size: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_max_surface_size(max_size)); + } + + fn surface_resize_increments(&self) -> Option> { + self.maybe_wait_on_main(|delegate| delegate.surface_resize_increments()) + } + + fn set_surface_resize_increments(&self, increments: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_surface_resize_increments(increments)); + } + + fn set_title(&self, title: &str) { + self.maybe_wait_on_main(|delegate| delegate.set_title(title)); + } + + fn set_transparent(&self, transparent: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_transparent(transparent)); + } + + fn set_blur(&self, blur: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_blur(blur)); + } + + fn set_visible(&self, visible: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_visible(visible)); + } + + fn is_visible(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.is_visible()) + } + + fn set_resizable(&self, resizable: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_resizable(resizable)) + } + + fn is_resizable(&self) -> bool { + self.maybe_wait_on_main(|delegate| delegate.is_resizable()) + } + + fn set_enabled_buttons(&self, buttons: WindowButtons) { + self.maybe_wait_on_main(|delegate| delegate.set_enabled_buttons(buttons)) + } + + fn enabled_buttons(&self) -> WindowButtons { + self.maybe_wait_on_main(|delegate| delegate.enabled_buttons()) + } + + fn set_minimized(&self, minimized: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_minimized(minimized)); + } + + fn is_minimized(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.is_minimized()) + } + + fn set_maximized(&self, maximized: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_maximized(maximized)); + } + + fn is_maximized(&self) -> bool { + self.maybe_wait_on_main(|delegate| delegate.is_maximized()) + } + + fn set_fullscreen(&self, fullscreen: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_fullscreen(fullscreen.map(Into::into))) + } + + fn fullscreen(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.fullscreen().map(Into::into)) + } + + fn set_decorations(&self, decorations: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_decorations(decorations)); + } + + fn is_decorated(&self) -> bool { + self.maybe_wait_on_main(|delegate| delegate.is_decorated()) + } + + fn set_window_level(&self, level: WindowLevel) { + self.maybe_wait_on_main(|delegate| delegate.set_window_level(level)); + } + + fn set_window_icon(&self, window_icon: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_window_icon(window_icon)); + } + + fn set_ime_cursor_area(&self, position: Position, size: Size) { + self.maybe_wait_on_main(|delegate| delegate.set_ime_cursor_area(position, size)); + } + + fn set_ime_allowed(&self, allowed: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_ime_allowed(allowed)); + } + + fn set_ime_purpose(&self, purpose: ImePurpose) { + self.maybe_wait_on_main(|delegate| delegate.set_ime_purpose(purpose)); + } + + fn focus_window(&self) { + self.maybe_wait_on_main(|delegate| delegate.focus_window()); + } + + fn has_focus(&self) -> bool { + self.maybe_wait_on_main(|delegate| delegate.has_focus()) + } + + fn request_user_attention(&self, request_type: Option) { + self.maybe_wait_on_main(|delegate| delegate.request_user_attention(request_type)); + } + + fn set_theme(&self, theme: Option) { + self.maybe_wait_on_main(|delegate| delegate.set_theme(theme)); + } + + fn theme(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.theme()) + } + + fn set_content_protected(&self, protected: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_content_protected(protected)); + } + + fn title(&self) -> String { + self.maybe_wait_on_main(|delegate| delegate.title()) + } + + fn set_cursor(&self, cursor: Cursor) { + self.maybe_wait_on_main(|delegate| delegate.set_cursor(cursor)); + } + + fn set_cursor_position(&self, position: Position) -> Result<(), RequestError> { + self.maybe_wait_on_main(|delegate| delegate.set_cursor_position(position)) + } + + fn set_cursor_grab(&self, mode: crate::window::CursorGrabMode) -> Result<(), RequestError> { + self.maybe_wait_on_main(|delegate| delegate.set_cursor_grab(mode)) + } + + fn set_cursor_visible(&self, visible: bool) { + self.maybe_wait_on_main(|delegate| delegate.set_cursor_visible(visible)) + } + + fn drag_window(&self) -> Result<(), RequestError> { + self.maybe_wait_on_main(|delegate| delegate.drag_window()) + } + + fn drag_resize_window( + &self, + direction: crate::window::ResizeDirection, + ) -> Result<(), RequestError> { + Ok(self.maybe_wait_on_main(|delegate| delegate.drag_resize_window(direction))?) + } + + fn show_window_menu(&self, position: Position) { + self.maybe_wait_on_main(|delegate| delegate.show_window_menu(position)) + } + + fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> { + self.maybe_wait_on_main(|delegate| delegate.set_cursor_hittest(hittest)); + Ok(()) + } + + fn current_monitor(&self) -> Option { + self.maybe_wait_on_main(|delegate| { + delegate.current_monitor().map(|inner| CoreMonitorHandle { inner }) + }) + } + + fn available_monitors(&self) -> Box> { + self.maybe_wait_on_main(|delegate| { + Box::new( + delegate.available_monitors().into_iter().map(|inner| CoreMonitorHandle { inner }), + ) + }) + } + + fn primary_monitor(&self) -> Option { + self.maybe_wait_on_main(|delegate| { + delegate.primary_monitor().map(|inner| CoreMonitorHandle { inner }) + }) + } + + #[cfg(feature = "rwh_06")] + fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { + self + } + + #[cfg(feature = "rwh_06")] + fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { + self + } +} + +declare_class!( + #[derive(Debug)] + pub struct WinitWindow; + + unsafe impl ClassType for WinitWindow { + #[inherits(NSResponder, NSObject)] + type Super = NSWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindow"; + } + + impl DeclaredClass for WinitWindow {} + + unsafe impl WinitWindow { + #[method(canBecomeMainWindow)] + fn can_become_main_window(&self) -> bool { + trace_scope!("canBecomeMainWindow"); + true + } + + #[method(canBecomeKeyWindow)] + fn can_become_key_window(&self) -> bool { + trace_scope!("canBecomeKeyWindow"); + true + } + } +); + +impl WinitWindow { + pub(super) fn id(&self) -> WindowId { + WindowId::from_raw(self as *const Self as usize) + } +} diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index f62fe5e70a..30019a0701 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -147,6 +147,8 @@ impl AppState { // must be mut because plain `static` requires `Sync` static mut APP_STATE: RefCell> = RefCell::new(None); + #[allow(unknown_lints)] // New lint below + #[allow(static_mut_refs)] // TODO: Use `MainThreadBound` instead. let mut guard = unsafe { APP_STATE.borrow_mut() }; if guard.is_none() { #[inline(never)] diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 1c707a3ac4..9f017a246c 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -102,13 +102,20 @@ impl Clone for MonitorHandle { impl hash::Hash for MonitorHandle { fn hash(&self, state: &mut H) { - (self as *const Self).hash(state); + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).hash(state); } } impl PartialEq for MonitorHandle { fn eq(&self, other: &Self) -> bool { - ptr::eq(self, other) + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + ptr::eq( + Retained::as_ptr(self.ui_screen.get(mtm)), + Retained::as_ptr(other.ui_screen.get(mtm)), + ) } } @@ -122,8 +129,10 @@ impl PartialOrd for MonitorHandle { impl Ord for MonitorHandle { fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // SAFETY: Only getting the pointer. // TODO: Make a better ordering - (self as *const Self).cmp(&(other as *const Self)) + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).cmp(&Retained::as_ptr(other.ui_screen.get(mtm))) } } @@ -242,3 +251,27 @@ pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { #[allow(deprecated)] UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect() } + +#[cfg(test)] +mod tests { + use objc2_foundation::NSSet; + + use super::*; + + // Test that UIScreen pointer comparisons are correct. + #[test] + #[allow(deprecated)] + fn screen_comparisons() { + // Test code, doesn't matter that it's not thread safe + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + + assert!(ptr::eq(&*UIScreen::mainScreen(mtm), &*UIScreen::mainScreen(mtm))); + + let main = UIScreen::mainScreen(mtm); + assert!(UIScreen::screens(mtm).iter().any(|screen| ptr::eq(screen, &*main))); + + assert!(unsafe { + NSSet::setWithArray(&UIScreen::screens(mtm)).containsObject(&UIScreen::mainScreen(mtm)) + }); + } +} diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index 75386d368e..9be43e4130 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -4,19 +4,21 @@ use std::cell::{Cell, RefCell}; use objc2::rc::Retained; 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}; +use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString}; use objc2_ui_kit::{ UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, - UIGestureRecognizerDelegate, UIGestureRecognizerState, UIPanGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, - UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, + UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, }; use super::app_state::{self, EventWrapper}; use super::window::WinitUIWindow; use crate::dpi::PhysicalPosition; -use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent}; +use crate::event::{ElementState, Event, Force, KeyEvent, Touch, TouchPhase, WindowEvent}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey}; use crate::platform_impl::platform::DEVICE_ID; +use crate::platform_impl::KeyEventExtra; use crate::window::{WindowAttributes, WindowId as RootWindowId}; pub struct WinitViewState { @@ -314,6 +316,11 @@ declare_class!( let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_event(mtm, gesture_event); } + + #[method(canBecomeFirstResponder)] + fn can_become_first_responder(&self) -> bool { + true + } } unsafe impl NSObjectProtocol for WinitView {} @@ -324,6 +331,26 @@ declare_class!( true } } + + unsafe impl UITextInputTraits for WinitView { + } + + unsafe impl UIKeyInput for WinitView { + #[method(hasText)] + fn has_text(&self) -> bool { + true + } + + #[method(insertText:)] + fn insert_text(&self, text: &NSString) { + self.handle_insert_text(text) + } + + #[method(deleteBackward)] + fn delete_backward(&self) { + self.handle_delete_backward() + } + } ); impl WinitView { @@ -512,4 +539,69 @@ impl WinitView { let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_events(mtm, touch_events); } + + fn handle_insert_text(&self, text: &NSString) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + // send individual events for each character + app_state::handle_nonuser_events( + mtm, + text.to_string().chars().flat_map(|c| { + let text = smol_str::SmolStr::from_iter([c]); + // Emit both press and release events + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + event: KeyEvent { + text: if state == ElementState::Pressed { + Some(text.clone()) + } else { + None + }, + state, + location: KeyLocation::Standard, + repeat: false, + logical_key: Key::Character(text.clone()), + physical_key: PhysicalKey::Unidentified( + NativeKeyCode::Unidentified, + ), + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + device_id: DEVICE_ID, + }, + }) + }) + }), + ); + } + + fn handle_delete_backward(&self) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events( + mtm, + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: KeyEvent { + state, + logical_key: Key::Named(NamedKey::Backspace), + physical_key: PhysicalKey::Code(KeyCode::Backspace), + platform_specific: KeyEventExtra {}, + repeat: false, + location: KeyLocation::Standard, + text: None, + }, + is_synthetic: false, + }, + }) + }), + ); + } } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 675fdfeef8..be0275952d 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -367,12 +367,24 @@ impl Inner { warn!("`Window::set_ime_cursor_area` is ignored on iOS") } - pub fn set_ime_allowed(&self, _allowed: bool) { - warn!("`Window::set_ime_allowed` is ignored on iOS") + /// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`, + /// requesting focus for the [WinitView]. Since [WinitView] implements + /// [objc2_ui_kit::UIKeyInput], the keyboard will be shown. + /// + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + unsafe { + self.view.becomeFirstResponder(); + } + } else { + unsafe { + self.view.resignFirstResponder(); + } + } } pub fn set_ime_purpose(&self, _purpose: ImePurpose) { - warn!("`Window::set_ime_allowed` is ignored on iOS") + warn!("`Window::set_ime_purpose` is ignored on iOS") } pub fn focus_window(&self) { diff --git a/src/platform_impl/linux/common/xkb/mod.rs b/src/platform_impl/linux/common/xkb/mod.rs index 0b951b666c..e2e7680065 100644 --- a/src/platform_impl/linux/common/xkb/mod.rs +++ b/src/platform_impl/linux/common/xkb/mod.rs @@ -184,7 +184,7 @@ pub struct KeyContext<'a> { scratch_buffer: &'a mut Vec, } -impl<'a> KeyContext<'a> { +impl KeyContext<'_> { pub fn process_key_event( &mut self, keycode: u32, diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index bee440189d..c4670ce09d 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -117,6 +117,8 @@ pub(crate) static X11_BACKEND: Lazy, XNotSupported pub enum OsError { Misc(&'static str), #[cfg(x11_platform)] + XNotSupported(XNotSupported), + #[cfg(x11_platform)] XError(Arc), #[cfg(wayland_platform)] WaylandError(Arc), @@ -127,6 +129,8 @@ impl fmt::Display for OsError { match *self { OsError::Misc(e) => _f.pad(e), #[cfg(x11_platform)] + OsError::XNotSupported(ref e) => fmt::Display::fmt(e, _f), + #[cfg(x11_platform)] OsError::XError(ref e) => fmt::Display::fmt(e, _f), #[cfg(wayland_platform)] OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f), @@ -643,18 +647,18 @@ pub(crate) enum PlatformCustomCursor { /// Hooks for X11 errors. #[cfg(x11_platform)] -pub(crate) static mut XLIB_ERROR_HOOKS: Mutex> = Mutex::new(Vec::new()); +pub(crate) static XLIB_ERROR_HOOKS: Mutex> = Mutex::new(Vec::new()); #[cfg(x11_platform)] unsafe extern "C" fn x_error_callback( display: *mut x11::ffi::Display, event: *mut x11::ffi::XErrorEvent, ) -> c_int { - let xconn_lock = X11_BACKEND.lock().unwrap(); + let xconn_lock = X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()); if let Ok(ref xconn) = *xconn_lock { // Call all the hooks. let mut error_handled = false; - for hook in unsafe { XLIB_ERROR_HOOKS.lock() }.unwrap().iter() { + for hook in XLIB_ERROR_HOOKS.lock().unwrap().iter() { error_handled |= hook(display as *mut _, event as *mut _); } @@ -777,9 +781,11 @@ impl EventLoop { #[cfg(x11_platform)] fn new_x11_any_thread() -> Result, EventLoopError> { - let xconn = match X11_BACKEND.lock().unwrap().as_ref() { + let xconn = match X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { Ok(xconn) => xconn.clone(), - Err(_) => return Err(EventLoopError::NotSupported(NotSupportedError::new())), + Err(err) => { + return Err(EventLoopError::Os(os_error!(OsError::XNotSupported(err.clone())))) + }, }; Ok(EventLoop::X(x11::EventLoop::new(xconn))) diff --git a/src/platform_impl/linux/x11/activation.rs b/src/platform_impl/linux/x11/activation.rs index a5e961bb9d..5f83e1c0a7 100644 --- a/src/platform_impl/linux/x11/activation.rs +++ b/src/platform_impl/linux/x11/activation.rs @@ -165,7 +165,7 @@ fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { buffer: &'a mut Vec, } - impl<'a> std::fmt::Write for Writer<'a> { + impl std::fmt::Write for Writer<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.buffer.extend_from_slice(s.as_bytes()); Ok(()) diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 9c6b95a2ac..14f80b67ce 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -145,8 +145,19 @@ impl EventProcessor { { let event_type = xev.get_type(); - if self.filter_event(xev) { - if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + // If we have IME disabled, don't try to `filter_event`, since only IME can consume them + // and forward back. This is not desired for e.g. games since some IMEs may delay the input + // and game can toggle IME back when e.g. typing into some field where latency won't really + // matter. + if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + let wt = Self::window_target(&self.target); + let ime = wt.ime.as_ref(); + let window = self.active_window.map(|window| window as XWindow); + let forward_to_ime = ime + .and_then(|ime| window.map(|window| ime.borrow().is_ime_allowed(window))) + .unwrap_or(false); + + if forward_to_ime && self.filter_event(xev) { let xev: &XKeyEvent = xev.as_ref(); if self.xmodmap.is_modifier(xev.keycode as u8) { // Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen @@ -159,7 +170,8 @@ impl EventProcessor { self.xfiltered_modifiers.push_front(xev.serial); } } - return; + } else { + self.filter_event(xev); } match event_type { diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index 606e00d9f3..063598a3ea 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -226,6 +226,16 @@ impl Ime { // Create new context supporting IME input. let _ = self.create_context(window, allowed); } + + pub fn is_ime_allowed(&self, window: ffi::Window) -> bool { + if self.is_destroyed() { + false + } else if let Some(Some(context)) = self.inner.contexts.get(&window) { + context.is_allowed() + } else { + false + } + } } impl Drop for Ime { diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 3aafd7b316..8b9b690fb2 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -751,14 +751,14 @@ impl<'a> DeviceInfo<'a> { } } -impl<'a> Drop for DeviceInfo<'a> { +impl Drop for DeviceInfo<'_> { fn drop(&mut self) { assert!(!self.info.is_null()); unsafe { (self.xconn.xinput2.XIFreeDeviceInfo)(self.info as *mut _) }; } } -impl<'a> Deref for DeviceInfo<'a> { +impl Deref for DeviceInfo<'_> { type Target = [ffi::XIDeviceInfo]; fn deref(&self) -> &Self::Target { @@ -957,7 +957,7 @@ trait CookieResultExt { fn expect_then_ignore_error(self, msg: &str); } -impl<'a, E: fmt::Debug> CookieResultExt for Result, E> { +impl CookieResultExt for Result, E> { fn expect_then_ignore_error(self, msg: &str) { self.expect(msg).ignore_error() } diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 6580ac30de..1964bc9383 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -301,7 +301,7 @@ impl XConnection { let info = self .xcb_connection() .extension_information(randr::X11_EXTENSION_NAME)? - .ok_or_else(|| X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; + .ok_or(X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; // Select input data. let event_mask = diff --git a/src/platform_impl/linux/x11/util/memory.rs b/src/platform_impl/linux/x11/util/memory.rs index d32eb8cebe..4e052a758c 100644 --- a/src/platform_impl/linux/x11/util/memory.rs +++ b/src/platform_impl/linux/x11/util/memory.rs @@ -19,7 +19,7 @@ impl<'a, T> XSmartPointer<'a, T> { } } -impl<'a, T> Deref for XSmartPointer<'a, T> { +impl Deref for XSmartPointer<'_, T> { type Target = T; fn deref(&self) -> &T { @@ -27,13 +27,13 @@ impl<'a, T> Deref for XSmartPointer<'a, T> { } } -impl<'a, T> DerefMut for XSmartPointer<'a, T> { +impl DerefMut for XSmartPointer<'_, T> { fn deref_mut(&mut self) -> &mut T { unsafe { &mut *self.ptr } } } -impl<'a, T> Drop for XSmartPointer<'a, T> { +impl Drop for XSmartPointer<'_, T> { fn drop(&mut self) { unsafe { (self.xconn.xlib.XFree)(self.ptr as *mut _); diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index ea7c02354a..9eff6dd028 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -144,12 +144,26 @@ impl UnownedWindow { ) -> Result { let xconn = &event_loop.xconn; let atoms = xconn.atoms(); + + let screen_id = match window_attrs.platform_specific.x11.screen_id { + Some(id) => id, + None => xconn.default_screen_index() as c_int, + }; + + let screen = { + let screen_id_usize = usize::try_from(screen_id) + .map_err(|_| os_error!(OsError::Misc("screen id must be non-negative")))?; + xconn.xcb_connection().setup().roots.get(screen_id_usize).ok_or(os_error!( + OsError::Misc("requested screen id not present in server's response") + ))? + }; + #[cfg(feature = "rwh_06")] let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) { Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window, Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"), - None => event_loop.root, + None => screen.root, }; #[cfg(not(feature = "rwh_06"))] let root = event_loop.root; @@ -207,18 +221,10 @@ impl UnownedWindow { dimensions }; - let screen_id = match window_attrs.platform_specific.x11.screen_id { - Some(id) => id, - None => xconn.default_screen_index() as c_int, - }; - - // An iterator over all of the visuals combined with their depths. - let mut all_visuals = xconn - .xcb_connection() - .setup() - .roots + // An iterator over the visuals matching screen id combined with their depths. + let mut all_visuals = screen + .allowed_depths .iter() - .flat_map(|root| &root.allowed_depths) .flat_map(|depth| depth.visuals.iter().map(move |visual| (visual, depth.depth))); // creating @@ -484,6 +490,20 @@ impl UnownedWindow { ); leap!(result).ignore_error(); + // Select XInput2 events + let mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + | xinput::XIEventMask::TOUCH_BEGIN + | xinput::XIEventMask::TOUCH_UPDATE + | xinput::XIEventMask::TOUCH_END; + leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) + .ignore_error(); + // Set visibility (map window) if window_attrs.visible { leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); @@ -507,20 +527,6 @@ impl UnownedWindow { } } - // Select XInput2 events - let mask = xinput::XIEventMask::MOTION - | xinput::XIEventMask::BUTTON_PRESS - | xinput::XIEventMask::BUTTON_RELEASE - | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE - | xinput::XIEventMask::FOCUS_IN - | xinput::XIEventMask::FOCUS_OUT - | xinput::XIEventMask::TOUCH_BEGIN - | xinput::XIEventMask::TOUCH_UPDATE - | xinput::XIEventMask::TOUCH_END; - leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) - .ignore_error(); - // Try to create input context for the window. if let Some(ime) = event_loop.ime.as_ref() { let result = ime.borrow_mut().create_context(window.xwindow as ffi::Window, false); diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index ee149384b7..35076b6d19 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -5,7 +5,9 @@ use std::time::Instant; use objc2::rc::Retained; use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; -use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSRunningApplication, +}; use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol}; use super::event_handler::EventHandler; @@ -18,7 +20,7 @@ use crate::window::WindowId as RootWindowId; #[derive(Debug)] pub(super) struct AppState { - activation_policy: NSApplicationActivationPolicy, + activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, run_loop: RunLoop, @@ -74,7 +76,7 @@ declare_class!( impl ApplicationDelegate { pub(super) fn new( mtm: MainThreadMarker, - activation_policy: NSApplicationActivationPolicy, + activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, ) -> Retained { @@ -111,7 +113,24 @@ impl ApplicationDelegate { // We need to delay setting the activation policy and activating the app // until `applicationDidFinishLaunching` has been called. Otherwise the // menu bar is initially unresponsive on macOS 10.15. - app.setActivationPolicy(self.ivars().activation_policy); + // If no activation policy is explicitly provided, do not set it at all + // to allow the package manifest to define behavior via LSUIElement. + if let Some(activation_policy) = self.ivars().activation_policy { + app.setActivationPolicy(activation_policy); + } else { + // If no activation policy is explicitly provided, and the application + // is bundled, do not set the activation policy at all, to allow the + // package manifest to define the behavior via LSUIElement. + // + // See: + // - https://github.com/rust-windowing/winit/issues/261 + // - https://github.com/rust-windowing/winit/issues/3958 + let is_bundled = + unsafe { NSRunningApplication::currentApplication().bundleIdentifier().is_some() }; + if !is_bundled { + app.setActivationPolicy(NSApplicationActivationPolicy::Regular); + } + } window_activation_hack(&app); #[allow(deprecated)] diff --git a/src/platform_impl/macos/cursor.rs b/src/platform_impl/macos/cursor.rs index cc8f5f3088..9e14e8be61 100644 --- a/src/platform_impl/macos/cursor.rs +++ b/src/platform_impl/macos/cursor.rs @@ -4,7 +4,7 @@ use std::sync::OnceLock; use objc2::rc::Retained; use objc2::runtime::Sel; -use objc2::{msg_send_id, sel, ClassType}; +use objc2::{msg_send, msg_send_id, sel, ClassType}; use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage}; use objc2_foundation::{ ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, @@ -66,7 +66,7 @@ pub(crate) fn default_cursor() -> Retained { unsafe fn try_cursor_from_selector(sel: Sel) -> Option> { let cls = NSCursor::class(); - if cls.responds_to(sel) { + if msg_send![cls, respondsToSelector: sel] { let cursor: Retained = unsafe { msg_send_id![cls, performSelector: sel] }; Some(cursor) } else { diff --git a/src/platform_impl/macos/event.rs b/src/platform_impl/macos/event.rs index 602ab6278f..4eacb3dc44 100644 --- a/src/platform_impl/macos/event.rs +++ b/src/platform_impl/macos/event.rs @@ -92,17 +92,12 @@ fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key { /// Create `KeyEvent` for the given `NSEvent`. /// /// This function shouldn't be called when the IME input is in process. -pub(crate) fn create_key_event( - ns_event: &NSEvent, - is_press: bool, - is_repeat: bool, - key_override: Option, -) -> KeyEvent { +pub(crate) fn create_key_event(ns_event: &NSEvent, is_press: bool, is_repeat: bool) -> KeyEvent { use ElementState::{Pressed, Released}; let state = if is_press { Pressed } else { Released }; let scancode = unsafe { ns_event.keyCode() }; - let mut physical_key = key_override.unwrap_or_else(|| scancode_to_physicalkey(scancode as u32)); + let mut physical_key = scancode_to_physicalkey(scancode as u32); // NOTE: The logical key should heed both SHIFT and ALT if possible. // For instance: @@ -111,20 +106,15 @@ pub(crate) fn create_key_event( // * Pressing CTRL SHIFT A: logical key should also be "A" // This is not easy to tease out of `NSEvent`, but we do our best. - let text_with_all_modifiers: Option = if key_override.is_some() { + let characters = unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default(); + let text_with_all_modifiers = if characters.is_empty() { None } else { - let characters = - unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default(); - if characters.is_empty() { - None - } else { - if matches!(physical_key, PhysicalKey::Unidentified(_)) { - // The key may be one of the funky function keys - physical_key = extra_function_key_to_code(scancode, &characters); - } - Some(SmolStr::new(characters)) + if matches!(physical_key, PhysicalKey::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); } + Some(SmolStr::new(characters)) }; let key_from_code = code_to_key(physical_key, scancode); diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 9b7be282f6..cb0620ac95 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -202,18 +202,14 @@ pub struct EventLoop { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes { - pub(crate) activation_policy: ActivationPolicy, + pub(crate) activation_policy: Option, pub(crate) default_menu: bool, pub(crate) activate_ignoring_other_apps: bool, } impl Default for PlatformSpecificEventLoopAttributes { fn default() -> Self { - Self { - activation_policy: Default::default(), // Regular - default_menu: true, - activate_ignoring_other_apps: true, - } + Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true } } } @@ -235,9 +231,10 @@ impl EventLoop { } let activation_policy = match attributes.activation_policy { - ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, - ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, - ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + None => None, + Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular), + Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory), + Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited), }; let delegate = ApplicationDelegate::new( mtm, diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 50cffcee67..d2a9948fc1 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -20,13 +20,13 @@ use super::app_state::ApplicationDelegate; use super::cursor::{default_cursor, invisible_cursor}; use super::event::{ code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed, - scancode_to_physicalkey, + scancode_to_physicalkey, KeyEventExtra, }; use super::window::WinitWindow; use super::DEVICE_ID; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::event::{ - DeviceEvent, ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, }; use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}; @@ -399,7 +399,7 @@ declare_class!( unsafe { &*string }.to_string() }; - let is_control = string.chars().next().map_or(false, |c| c.is_control()); + let is_control = string.chars().next().is_some_and(|c| c.is_control()); // Commit only if we have marked text. if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control { @@ -482,7 +482,7 @@ declare_class!( }; if !had_ime_input || self.ivars().forward_key_to_app.get() { - let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }, None); + let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }); self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, event: key_event, @@ -505,7 +505,7 @@ declare_class!( ) { self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, - event: create_key_event(&event, false, false, None), + event: create_key_event(&event, false, false), is_synthetic: false, }); } @@ -552,7 +552,7 @@ declare_class!( .expect("could not find current event"); self.update_modifiers(&event, false); - let event = create_key_event(&event, true, unsafe { event.isARepeat() }, None); + let event = create_key_event(&event, true, unsafe { event.isARepeat() }); self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, @@ -933,22 +933,36 @@ impl WinitView { let scancode = unsafe { ns_event.keyCode() }; let physical_key = scancode_to_physicalkey(scancode as u32); - // We'll correct the `is_press` later. - let mut event = create_key_event(ns_event, false, false, Some(physical_key)); - - let key = code_to_key(physical_key, scancode); + let logical_key = code_to_key(physical_key, scancode); // Ignore processing of unknown modifiers because we can't determine whether // it was pressed or release reliably. - let Some(event_modifier) = key_to_modifier(&key) else { + // + // Furthermore, sometimes normal keys are reported inside flagsChanged:, such as + // when holding Caps Lock while pressing another key, see: + // https://github.com/alacritty/alacritty/issues/8268 + let Some(event_modifier) = key_to_modifier(&logical_key) else { break 'send_event; }; - event.physical_key = physical_key; - event.logical_key = key.clone(); - event.location = code_to_location(physical_key); + + let mut event = KeyEvent { + location: code_to_location(physical_key), + logical_key: logical_key.clone(), + physical_key, + repeat: false, + // We'll correct this later. + state: Pressed, + text: None, + platform_specific: KeyEventExtra { + text_with_all_modifiers: None, + key_without_modifiers: logical_key.clone(), + }, + }; + let location_mask = ModLocationMask::from_location(event.location); let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut(); - let phys_mod = phys_mod_state.entry(key).or_insert(ModLocationMask::empty()); + let phys_mod = + phys_mod_state.entry(logical_key).or_insert(ModLocationMask::empty()); let is_active = current_modifiers.state().contains(event_modifier); let mut events = VecDeque::with_capacity(2); diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index 2a7877b8bc..19dc605e09 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -55,6 +55,7 @@ pub struct PlatformSpecificWindowAttributes { pub accepts_first_mouse: bool, pub tabbing_identifier: Option, pub option_as_alt: OptionAsAlt, + pub borderless_game: bool, } impl Default for PlatformSpecificWindowAttributes { @@ -72,6 +73,7 @@ impl Default for PlatformSpecificWindowAttributes { accepts_first_mouse: true, tabbing_identifier: None, option_as_alt: Default::default(), + borderless_game: false, } } } @@ -85,8 +87,8 @@ pub(crate) struct State { // During `windowDidResize`, we use this to only send Moved if the position changed. // - // This is expressed in native screen coordinates. - previous_position: Cell>, + // This is expressed in desktop coordinates, and flipped to match Winit's coordinate system. + previous_position: Cell, // Used to prevent redundant events. previous_scale_factor: Cell, @@ -120,6 +122,7 @@ pub(crate) struct State { standard_frame: Cell>, is_simple_fullscreen: Cell, saved_style: Cell>, + is_borderless_game: Cell, } declare_class!( @@ -711,7 +714,7 @@ impl WindowDelegate { let delegate = mtm.alloc().set_ivars(State { app_delegate: app_delegate.retain(), window: window.retain(), - previous_position: Cell::new(None), + previous_position: Cell::new(flip_window_screen_coordinates(window.frame())), previous_scale_factor: Cell::new(scale_factor), resize_increments: Cell::new(resize_increments), decorations: Cell::new(attrs.decorations), @@ -725,6 +728,7 @@ impl WindowDelegate { standard_frame: Cell::new(None), is_simple_fullscreen: Cell::new(false), saved_style: Cell::new(None), + is_borderless_game: Cell::new(attrs.platform_specific.borderless_game), }); let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; @@ -835,13 +839,12 @@ impl WindowDelegate { } fn emit_move_event(&self) { - let frame = self.window().frame(); - if self.ivars().previous_position.get() == Some(frame.origin) { + let position = flip_window_screen_coordinates(self.window().frame()); + if self.ivars().previous_position.get() == position { return; } - self.ivars().previous_position.set(Some(frame.origin)); + self.ivars().previous_position.set(position); - let position = flip_window_screen_coordinates(frame); let position = LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()); self.queue_event(WindowEvent::Moved(position)); @@ -1160,7 +1163,8 @@ impl WindowDelegate { #[inline] pub fn drag_window(&self) -> Result<(), ExternalError> { let mtm = MainThreadMarker::from(self); - let event = NSApplication::sharedApplication(mtm).currentEvent().unwrap(); + let event = + NSApplication::sharedApplication(mtm).currentEvent().ok_or(ExternalError::Ignored)?; self.window().performWindowDragWithEvent(&event); Ok(()) } @@ -1409,7 +1413,7 @@ impl WindowDelegate { } match (old_fullscreen, fullscreen) { - (None, Some(_)) => { + (None, Some(fullscreen)) => { // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we // set a normal style temporarily. The previous state will be // restored in `WindowDelegate::window_did_exit_fullscreen`. @@ -1419,6 +1423,17 @@ impl WindowDelegate { self.set_style_mask(required); self.ivars().saved_style.set(Some(curr_mask)); } + + // In borderless games, we want to disable the dock and menu bar + // by setting the presentation options. We do this here rather than in + // `window:willUseFullScreenPresentationOptions` because for some reason + // the menu bar remains interactable despite being hidden. + if self.is_borderless_game() && matches!(fullscreen, Fullscreen::Borderless(_)) { + let presentation_options = NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + } + toggle_fullscreen(self.window()); }, (Some(Fullscreen::Borderless(_)), None) => { @@ -1829,6 +1844,14 @@ impl WindowExtMacOS for WindowDelegate { fn option_as_alt(&self) -> OptionAsAlt { self.view().option_as_alt() } + + fn set_borderless_game(&self, borderless_game: bool) { + self.ivars().is_borderless_game.set(borderless_game); + } + + fn is_borderless_game(&self) -> bool { + self.ivars().is_borderless_game.get() + } } const DEFAULT_STANDARD_FRAME: NSRect = diff --git a/src/platform_impl/orbital/mod.rs b/src/platform_impl/orbital/mod.rs index f210a8ce04..2d1525137a 100644 --- a/src/platform_impl/orbital/mod.rs +++ b/src/platform_impl/orbital/mod.rs @@ -154,7 +154,7 @@ impl<'a> WindowProperties<'a> { } } -impl<'a> fmt::Display for WindowProperties<'a> { +impl fmt::Display for WindowProperties<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, diff --git a/src/platform_impl/web/async/channel.rs b/src/platform_impl/web/async/channel.rs index 42401a2824..11a7a47957 100644 --- a/src/platform_impl/web/async/channel.rs +++ b/src/platform_impl/web/async/channel.rs @@ -23,7 +23,7 @@ pub struct Sender(Arc>); struct SenderInner { // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't // be accessed on the main thread, as it could block. Additionally we need - // to wrap `Sender` in an `Arc` to make it clonable on the main thread without + // to wrap `Sender` in an `Arc` to make it cloneable on the main thread without // having to block. sender: Mutex>, shared: Arc, diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 0da977af95..214ff45a5e 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -542,8 +542,8 @@ fn from_rgba( // // We call `createImageBitmap()` before spawning the future, // to not have to clone the image buffer. - let mut options = ImageBitmapOptions::new(); - options.premultiply_alpha(PremultiplyAlpha::None); + let options = ImageBitmapOptions::new(); + options.set_premultiply_alpha(PremultiplyAlpha::None); let bitmap = JsFuture::from( window .create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options) diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index ba8abbd477..d9ba3d3734 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,3 +1,15 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashSet, VecDeque}; +use std::iter; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::rc::{Rc, Weak}; + +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_time::{Duration, Instant}; + use super::super::main_thread::MainThreadMarker; use super::super::DeviceId; use super::backend; @@ -14,18 +26,6 @@ use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawne use crate::platform_impl::platform::window::Inner; use crate::window::WindowId; -use js_sys::Function; -use std::cell::{Cell, RefCell}; -use std::collections::{HashSet, VecDeque}; -use std::iter; -use std::num::NonZeroUsize; -use std::ops::Deref; -use std::rc::{Rc, Weak}; -use wasm_bindgen::prelude::{wasm_bindgen, Closure}; -use wasm_bindgen::JsCast; -use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; -use web_time::{Duration, Instant}; - pub struct Shared(Rc); pub(super) type EventHandler = dyn FnMut(Event<()>); @@ -459,33 +459,24 @@ impl Shared { if local { // If the loop is not running and triggered locally, queue on next microtick. - if let Ok(RunnerEnum::Running(ref runner)) = + if let Ok(RunnerEnum::Running(_)) = self.0.runner.try_borrow().as_ref().map(Deref::deref) { - // If we're currently polling let `send_events` do its job. - if !matches!(runner.state, State::Poll { .. }) { - #[wasm_bindgen] - extern "C" { - #[wasm_bindgen(js_name = queueMicrotask)] - fn queue_microtask(task: Function); - } - - queue_microtask( - Closure::once_into_js({ - let this = Rc::downgrade(&self.0); - move || { - if let Some(shared) = this.upgrade() { - Shared(shared).send_events( - iter::repeat(Event::UserEvent(())).take(count.get()), - ) - } + self.window().queue_microtask( + &Closure::once_into_js({ + let this = Rc::downgrade(&self.0); + move || { + if let Some(shared) = this.upgrade() { + Shared(shared).send_events( + iter::repeat(Event::UserEvent(())).take(count.get()), + ) } - }) - .unchecked_into(), - ); + } + }) + .unchecked_into(), + ); - return; - } + return; } } diff --git a/src/platform_impl/web/web_sys/resize_scaling.rs b/src/platform_impl/web/web_sys/resize_scaling.rs index fdfda75acd..4d10b3ac01 100644 --- a/src/platform_impl/web/web_sys/resize_scaling.rs +++ b/src/platform_impl/web/web_sys/resize_scaling.rs @@ -139,10 +139,9 @@ impl ResizeScaleInternal { // Safari doesn't support `devicePixelContentBoxSize` if has_device_pixel_support() { - observer.observe_with_options( - canvas, - ResizeObserverOptions::new().box_(ResizeObserverBoxOptions::DevicePixelContentBox), - ); + let options = ResizeObserverOptions::new(); + options.set_box(ResizeObserverBoxOptions::DevicePixelContentBox); + observer.observe_with_options(canvas, &options); } else { observer.observe(canvas); } diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs index dfb500b4b8..eb9706843d 100644 --- a/src/platform_impl/web/web_sys/schedule.rs +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -286,8 +286,8 @@ struct ScriptUrl(String); impl ScriptUrl { fn new(script: &str) -> Self { let sequence = Array::of1(&script.into()); - let mut property = BlobPropertyBag::new(); - property.type_("text/javascript"); + let property = BlobPropertyBag::new(); + property.set_type("text/javascript"); let blob = Blob::new_with_str_sequence_and_options(&sequence, &property) .expect("`new Blob()` should never throw"); diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 639a93865d..3fdf98d550 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -6,6 +6,7 @@ use std::cell::Cell; use std::collections::VecDeque; use std::ffi::c_void; use std::marker::PhantomData; +use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle, RawHandle}; use std::rc::Rc; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; @@ -16,13 +17,18 @@ use std::{mem, panic, ptr}; use crate::utils::Lazy; use windows_sys::Win32::Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE; -use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM}; +use windows_sys::Win32::Foundation::{ + GetLastError, FALSE, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_FAILED, WPARAM, +}; use windows_sys::Win32::Graphics::Gdi::{ GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient, ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE, }; use windows_sys::Win32::System::Ole::RevokeDragDrop; -use windows_sys::Win32::System::Threading::{GetCurrentThreadId, INFINITE}; +use windows_sys::Win32::System::Threading::{ + CreateWaitableTimerExW, GetCurrentThreadId, SetWaitableTimer, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, INFINITE, TIMER_ALL_ACCESS, +}; use windows_sys::Win32::UI::Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}; use windows_sys::Win32::UI::Input::Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}; use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ @@ -38,15 +44,15 @@ use windows_sys::Win32::UI::Input::Touch::{ use windows_sys::Win32::UI::Input::{RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE}; use windows_sys::Win32::UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, GetCursorPos, - GetMenu, GetMessageW, KillTimer, LoadCursorW, PeekMessageW, PostMessageW, RegisterClassExW, - RegisterWindowMessageA, SetCursor, SetTimer, SetWindowPos, TranslateMessage, CREATESTRUCTW, - GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO, - MNC_CLOSE, MSG, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, PT_TOUCH, RI_MOUSE_HWHEEL, - RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, - WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, - WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, - WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, + GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW, + RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, TranslateMessage, + CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, + MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, + PT_TOUCH, QS_ALLEVENTS, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, + SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, + WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, + WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, + WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, @@ -150,6 +156,10 @@ pub struct EventLoop { user_event_receiver: Receiver, window_target: RootAEL, msg_hook: Option bool + 'static>>, + // It is a timer used on timed waits. + // It is created lazily in case if we have `ControlFlow::WaitUntil`. + // Keep it as a field to avoid recreating it on every `ControlFlow::WaitUntil`. + high_resolution_timer: Option, } pub(crate) struct PlatformSpecificEventLoopAttributes { @@ -208,6 +218,7 @@ impl EventLoop { _marker: PhantomData, }, msg_hook: attributes.msg_hook.take(), + high_resolution_timer: None, }) } @@ -256,8 +267,9 @@ impl EventLoop { } let exit_code = loop { - self.wait_and_dispatch_message(None); - + self.wait_for_messages(None); + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. if let Some(code) = self.exit_code() { break code; } @@ -316,8 +328,11 @@ impl EventLoop { } } - self.wait_and_dispatch_message(timeout); - + if self.exit_code().is_none() { + self.wait_for_messages(timeout); + } + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. if self.exit_code().is_none() { self.dispatch_peeked_messages(); } @@ -347,101 +362,27 @@ impl EventLoop { status } - /// Wait for one message and dispatch it, optionally with a timeout - fn wait_and_dispatch_message(&mut self, timeout: Option) { - fn get_msg_with_timeout(msg: &mut MSG, timeout: Option) -> PumpStatus { - unsafe { - // A timeout of None means wait indefinitely (so we don't need to call SetTimer) - let timer_id = timeout.map(|timeout| SetTimer(0, 0, dur2timeout(timeout), None)); - let get_status = GetMessageW(msg, 0, 0, 0); - if let Some(timer_id) = timer_id { - KillTimer(0, timer_id); - } - // A return value of 0 implies `WM_QUIT` - if get_status == 0 { - PumpStatus::Exit(0) - } else { - PumpStatus::Continue - } - } - } - - /// Fetch the next MSG either via PeekMessage or GetMessage depending on whether the - /// requested timeout is `ZERO` (and so we don't want to block) - /// - /// Returns `None` if no MSG was read, else a `Continue` or `Exit` status - fn wait_for_msg(msg: &mut MSG, timeout: Option) -> Option { - if timeout == Some(Duration::ZERO) { - unsafe { - if PeekMessageW(msg, 0, 0, 0, PM_REMOVE) != 0 { - Some(PumpStatus::Continue) - } else { - None - } - } - } else { - Some(get_msg_with_timeout(msg, timeout)) - } - } - + /// Waits until new event messages arrive to be peeked. + /// Doesn't peek messages itself. + /// + /// Parameter timeout is optional. This method would wait for the smaller timeout + /// between the argument and a timeout from control flow. + fn wait_for_messages(&mut self, timeout: Option) { let runner = &self.window_target.p.runner_shared; // We aim to be consistent with the MacOS backend which has a RunLoop // observer that will dispatch AboutToWait when about to wait for // events, and NewEvents after the RunLoop wakes up. // - // We emulate similar behaviour by treating `GetMessage` as our wait + // We emulate similar behaviour by treating `MsgWaitForMultipleObjectsEx` as our wait // point and wake up point (when it returns) and we drain all other // pending messages via `PeekMessage` until we come back to "wait" via - // `GetMessage` + // `MsgWaitForMultipleObjectsEx`. // runner.prepare_wait(); - - let control_flow_timeout = match runner.control_flow() { - ControlFlow::Wait => None, - ControlFlow::Poll => Some(Duration::ZERO), - ControlFlow::WaitUntil(wait_deadline) => { - let start = Instant::now(); - Some(wait_deadline.saturating_duration_since(start)) - }, - }; - let timeout = min_timeout(control_flow_timeout, timeout); - - // # Safety - // The Windows API has no documented requirement for bitwise - // initializing a `MSG` struct (it can be uninitialized memory for the C - // API) and there's no API to construct or initialize a `MSG`. This - // is the simplest way avoid uninitialized memory in Rust - let mut msg = unsafe { mem::zeroed() }; - let msg_status = wait_for_msg(&mut msg, timeout); - + wait_for_messages_impl(&mut self.high_resolution_timer, runner.control_flow(), timeout); // Before we potentially exit, make sure to consistently emit an event for the wake up runner.wakeup(); - - match msg_status { - None => {}, // No MSG to dispatch - Some(PumpStatus::Exit(code)) => { - runner.set_exit_code(code); - }, - Some(PumpStatus::Continue) => { - unsafe { - let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { - callback(&mut msg as *mut _ as *mut _) - } else { - false - }; - if !handled { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } - - if let Err(payload) = runner.take_panic_error() { - runner.reset_runner(); - panic::resume_unwind(payload); - } - }, - } } /// Dispatch all queued messages via `PeekMessageW` @@ -460,7 +401,7 @@ impl EventLoop { // initializing a `MSG` struct (it can be uninitialized memory for the C // API) and there's no API to construct or initialize a `MSG`. This // is the simplest way avoid uninitialized memory in Rust - let mut msg = unsafe { mem::zeroed() }; + let mut msg: MSG = unsafe { mem::zeroed() }; loop { unsafe { @@ -623,7 +564,7 @@ impl OwnedDisplayHandle { fn main_thread_id() -> u32 { static mut MAIN_THREAD_ID: u32 = 0; - /// Function pointer used in CRT initialization section to set the above static field's value. + // Function pointer used in CRT initialization section to set the above static field's value. // Mark as used so this is not removable. #[used] @@ -682,6 +623,143 @@ impl Drop for EventLoop { } } +/// Set upper limit for waiting time to avoid overflows. +/// I chose 50 days as a limit because it is used in dur2timeout. +const FIFTY_DAYS: Duration = Duration::from_secs(50_u64 * 24 * 60 * 60); +/// Waitable timers use 100 ns intervals to indicate due time. +/// +/// And there is no point waiting using other ways for such small timings +/// because they are even less precise (can overshoot by few ms). +const MIN_WAIT: Duration = Duration::from_nanos(100); + +fn create_high_resolution_timer() -> Option { + unsafe { + let handle: HANDLE = CreateWaitableTimerExW( + ptr::null(), + ptr::null(), + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + TIMER_ALL_ACCESS, + ); + // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is supported only after + // Win10 1803 but it is already default option for rustc + // (std uses it to implement `std::thread::sleep`). + if handle == 0 { + None + } else { + Some(OwnedHandle::from_raw_handle(handle as *mut c_void)) + } + } +} + +/// This function should not return error if parameters are valid +/// but there is no guarantee about that at MSDN docs +/// so we return result of GetLastError if fail. +/// +/// ## Safety +/// +/// timer must be a valid timer handle created by [create_high_resolution_timer]. +/// timeout divided by 100 nanoseconds must be more than 0 and less than i64::MAX. +unsafe fn set_high_resolution_timer(timer: RawHandle, timeout: Duration) -> Result<(), u32> { + const INTERVAL_NS: u32 = MIN_WAIT.subsec_nanos(); + const INTERVALS_IN_SEC: u64 = (Duration::from_secs(1).as_nanos() / INTERVAL_NS as u128) as u64; + let intervals_to_wait: u64 = + timeout.as_secs() * INTERVALS_IN_SEC + u64::from(timeout.subsec_nanos() / INTERVAL_NS); + debug_assert!(intervals_to_wait < i64::MAX as u64, "Must be called with smaller duration",); + // Use negative time to indicate relative time. + let due_time: i64 = -(intervals_to_wait as i64); + unsafe { + let set_result = SetWaitableTimer(timer as HANDLE, &due_time, 0, None, ptr::null(), FALSE); + if set_result != FALSE { + Ok(()) + } else { + Err(GetLastError()) + } + } +} + +/// Implementation detail of [EventLoop::wait_for_messages]. +/// +/// Does actual system-level waiting and doesn't process any messages itself, +/// including winits internal notifications about waiting and new messages arrival. +fn wait_for_messages_impl( + high_resolution_timer: &mut Option, + control_flow: ControlFlow, + timeout: Option, +) { + let timeout = { + let control_flow_timeout = match control_flow { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + let start = Instant::now(); + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + let timeout = min_timeout(timeout, control_flow_timeout); + if timeout == Some(Duration::ZERO) { + // Do not wait if we don't have time. + return; + } + // Now we decided to wait so need to do some clamping + // to avoid problems with overflow and calling WinAPI with invalid parameters. + timeout + .map(|t| t.min(FIFTY_DAYS)) + // If timeout is less than minimally supported by Windows, + // increase it to that minimum. Who want less than microsecond delays anyway? + .map(|t| t.max(MIN_WAIT)) + }; + + if timeout.is_some() && high_resolution_timer.is_none() { + *high_resolution_timer = create_high_resolution_timer(); + } + + let high_resolution_timer: Option = + high_resolution_timer.as_ref().map(OwnedHandle::as_raw_handle); + + let use_timer: bool; + if let (Some(handle), Some(timeout)) = (high_resolution_timer, timeout) { + let res = unsafe { + // Safety: handle can be Some only if we succeeded in creating high resolution + // timer. We properly clamped timeout so it can be used as argument + // to timer. + set_high_resolution_timer(handle, timeout) + }; + if let Err(error_code) = res { + // We successfully got timer but failed to set it? + // Should be some bug in our code. + tracing::trace!("Failed to set high resolution timer: last error {}", error_code); + use_timer = false; + } else { + use_timer = true; + } + } else { + use_timer = false; + } + + unsafe { + // Either: + // 1. User wants to wait indefinely if timeout is not set. + // 2. We failed to get and set high resolution timer and we need something instead of it. + let wait_duration_ms = timeout.map(dur2timeout).unwrap_or(INFINITE); + + let (num_handles, raw_handles) = + if use_timer { (1, [high_resolution_timer.unwrap()]) } else { (0, [ptr::null_mut()]) }; + + let result = MsgWaitForMultipleObjectsEx( + num_handles, + raw_handles.as_ptr() as *const _, + wait_duration_ms, + QS_ALLEVENTS, + MWMO_INPUTAVAILABLE, + ); + if result == WAIT_FAILED { + // Well, nothing smart to do in such case. + // Treat it as spurious wake up. + tracing::warn!("Failed to MsgWaitForMultipleObjectsEx: error code {}", GetLastError(),); + } + } +} + pub(crate) struct EventLoopThreadExecutor { thread_id: u32, target_window: HWND, diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs index ad6c801965..3243ec47d4 100644 --- a/src/platform_impl/windows/event_loop/runner.rs +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -377,19 +377,19 @@ impl BufferedEvent { match self { Self::Event(event) => dispatch(event), Self::ScaleFactorChanged(window_id, scale_factor, new_inner_size) => { - let user_new_innner_size = Arc::new(Mutex::new(new_inner_size)); + let user_new_inner_size = Arc::new(Mutex::new(new_inner_size)); dispatch(Event::WindowEvent { window_id, event: WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer: InnerSizeWriter::new(Arc::downgrade( - &user_new_innner_size, + &user_new_inner_size, )), }, }); - let inner_size = *user_new_innner_size.lock().unwrap(); + let inner_size = *user_new_inner_size.lock().unwrap(); - drop(user_new_innner_size); + drop(user_new_inner_size); if inner_size != new_inner_size { let window_flags = unsafe { diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs index 46716f4c67..ab4a0f4070 100644 --- a/src/platform_impl/windows/keyboard.rs +++ b/src/platform_impl/windows/keyboard.rs @@ -98,7 +98,7 @@ impl KeyEventBuilder { MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) }, WM_KILLFOCUS => { - // sythesize keyup events + // synthesize keyup events let kbd_state = get_kbd_state(); let key_events = Self::synthesize_kbd_state(ElementState::Released, &kbd_state); MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) @@ -334,11 +334,11 @@ impl KeyEventBuilder { // We are synthesizing the press event for caps-lock first for the following reasons: // 1. If caps-lock is *not* held down but *is* active, then we have to synthesize all // printable keys, respecting the caps-lock state. - // 2. If caps-lock is held down, we could choose to sythesize its keypress after every other - // key, in which case all other keys *must* be sythesized as if the caps-lock state was - // be the opposite of what it currently is. + // 2. If caps-lock is held down, we could choose to synthesize its keypress after every + // other key, in which case all other keys *must* be sythesized as if the caps-lock state + // was be the opposite of what it currently is. // -- - // For the sake of simplicity we are choosing to always sythesize + // For the sake of simplicity we are choosing to always synthesize // caps-lock first, and always use the current caps-lock state // to determine the produced text if is_key_pressed!(VK_CAPITAL) { diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 23fec32b34..a8b375c9b5 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1129,7 +1129,7 @@ pub(super) struct InitData<'a> { pub window: Option, } -impl<'a> InitData<'a> { +impl InitData<'_> { unsafe fn create_window(&self, window: HWND) -> Window { // Register for touch events if applicable { diff --git a/src/window.rs b/src/window.rs index f19265e3a1..9bd002ab4c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1274,7 +1274,8 @@ impl Window { /// /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are /// combined. - /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **iOS / Android:** This will show / hide the soft keyboard. + /// - **Web / Orbital:** Unsupported. /// - **X11**: Enabling IME will disable dead keys reporting during compose. /// /// [`Ime`]: crate::event::WindowEvent::Ime