diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a7339145ef..89b5752ec9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ # Android -/src/platform/android.rs @msiglreith @MarijnS95 -/src/platform_impl/android @msiglreith @MarijnS95 +/src/platform/android.rs @MarijnS95 +/src/platform_impl/android @MarijnS95 # iOS /src/platform/ios.rs @madsmtm @@ -26,8 +26,8 @@ /src/platform_impl/web @daxpedda # Windows -/src/platform/windows.rs @msiglreith -/src/platform_impl/windows @msiglreith +/src/platform/windows.rs @notgull +/src/platform_impl/windows @notgull # Orbital (Redox OS) /src/platform/orbital.rs @jackpot51 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecf78fcf25..aa80f40957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: name: Check formatting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: taiki-e/checkout-action@v1 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: taiki-e/checkout-action@v1 - uses: taiki-e/install-action@v2 with: tool: typos-cli @@ -88,7 +88,7 @@ jobs: CMD: ${{ matrix.platform.cmd }} steps: - - uses: actions/checkout@v4 + - uses: taiki-e/checkout-action@v1 - name: Restore cache of cargo folder # We use `restore` and later `save`, so that we can create the key after @@ -107,7 +107,17 @@ jobs: - name: Generate lockfile # Also updates the crates.io index - run: cargo generate-lockfile && cargo update -p ahash --precise 0.8.7 && cargo update -p bumpalo --precise 3.14.0 + run: | + cargo generate-lockfile + cargo update -p ahash --precise 0.8.7 + cargo update -p bumpalo --precise 3.14.0 + cargo update -p softbuffer --precise 0.4.0 + cargo update -p objc2-encode --precise 4.0.3 + cargo update -p orbclient --precise 0.3.47 + cargo update -p image --precise 0.25.0 + cargo update -p gethostname@1.1.0 --precise 1.0.2 + cargo update -p unicode-ident --precise 1.0.10 + cargo update -p syn --precise 2.0.114 - name: Install GCC Multilib if: (matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686') @@ -220,9 +230,24 @@ jobs: - { name: 'Windows', target: x86_64-pc-windows-gnu } steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: taiki-e/checkout-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check log-level: error arguments: --all-features --target ${{ matrix.platform.target }} + + swc: + name: Minimize JavaScript + runs-on: ubuntu-latest + + steps: + - uses: taiki-e/checkout-action@v1 + - name: Install SWC + run: sudo npm i -g @swc/cli + - name: Run SWC + run: | + swc src/platform_impl/web/web_sys/worker.js -o src/platform_impl/web/web_sys/worker.min.js + - name: Check for diff + run: | + [[ -z $(git status -s) ]] diff --git a/.gitignore b/.gitignore index ebed8e3651..2caffd8242 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,5 @@ target/ rls/ .vscode/ *~ -*.wasm -*.ts -*.js #*# .DS_Store diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000000..1d7eccb99d --- /dev/null +++ b/.swcrc @@ -0,0 +1,12 @@ +{ + "minify": true, + "jsc": { + "target": "es2022", + "minify": { + "compress": { + "unused": true + }, + "mangle": true + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74351b2785..dfcb93a323 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,11 @@ All patches have to be sent on Github as [pull requests][prs]. To simplify your life during review it's recommended to check the "give contributors write access to the branch" checkbox. +We use unstable Rustfmt options across the project, so please run +`cargo +nightly fmt` before submitting your work. If you are unable to do so, +the maintainers can do it for you before merging, just state so in your pull +request description. + #### Handling review During the review process certain events could require an action from your side, diff --git a/Cargo.toml b/Cargo.toml index e87e17d864..5b2d09a29f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.30.1" +version = "0.30.13" 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 = [ @@ -86,11 +96,11 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = [ ], optional = true } serde = { workspace = true, optional = true } smol_str = "0.2.0" -tracing = { version = "0.1.40", default_features = false } +tracing = { version = "0.1.40", default-features = false } [dev-dependencies] image = { version = "0.25.0", default-features = false, features = ["png"] } -tracing = { version = "0.1.40", default_features = false, features = ["log"] } +tracing = { version = "0.1.40", default-features = false, features = ["log"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } winit = { path = ".", features = ["rwh_05"] } @@ -107,12 +117,12 @@ android-activity = "0.6.0" ndk = { version = "0.9.0", default-features = false } [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +block2 = "0.5.1" core-foundation = "0.9.3" -objc2 = "0.5.2" +objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] } [target.'cfg(target_os = "macos")'.dependencies] core-graphics = "0.23.1" -block2 = "0.5.1" [target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] version = "0.2.2" @@ -125,6 +135,7 @@ features = [ "NSDictionary", "NSDistributedNotificationCenter", "NSEnumerator", + "NSKeyValueObserving", "NSNotification", "NSObjCRuntime", "NSPathUtilities", @@ -142,6 +153,7 @@ features = [ "NSApplication", "NSBitmapImageRep", "NSButton", + "NSColor", "NSControl", "NSCursor", "NSDragging", @@ -168,11 +180,13 @@ features = [ [target.'cfg(target_os = "ios")'.dependencies.objc2-foundation] version = "0.2.2" features = [ + "block2", "dispatch", "NSArray", "NSEnumerator", "NSGeometry", "NSObjCRuntime", + "NSOperation", "NSString", "NSProcessInfo", "NSThread", @@ -187,6 +201,8 @@ features = [ "UIEvent", "UIGeometry", "UIGestureRecognizer", + "UITextInput", + "UITextInputTraits", "UIOrientation", "UIPanGestureRecognizer", "UIPinchGestureRecognizer", @@ -219,6 +235,7 @@ features = [ "Win32_System_Com", "Win32_System_LibraryLoader", "Win32_System_Ole", + "Win32_Security", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", @@ -238,7 +255,7 @@ features = [ [target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies] ahash = { version = "0.8.7", features = ["no-rng"], optional = true } bytemuck = { version = "1.13.1", default-features = false, optional = true } -calloop = "0.12.3" +calloop = "0.13.0" libc = "0.2.64" memmap2 = { version = "0.9.0", optional = true } percent-encoding = { version = "2.0", optional = true } @@ -248,18 +265,18 @@ rustix = { version = "0.38.4", default-features = false, features = [ "thread", "process", ] } -sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = [ +sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-features = false, features = [ "calloop", ], optional = true } -sctk-adwaita = { version = "0.9.0", default_features = false, optional = true } -wayland-backend = { version = "0.3.0", default_features = false, features = [ +sctk-adwaita = { version = "0.10.1", default-features = false, optional = true } +wayland-backend = { version = "0.3.10", default-features = false, features = [ "client_system", ], optional = true } -wayland-client = { version = "0.31.1", optional = true } -wayland-protocols = { version = "0.31.0", features = [ +wayland-client = { version = "0.31.10", optional = true } +wayland-protocols = { version = "0.32.8", features = [ "staging", ], optional = true } -wayland-protocols-plasma = { version = "0.2.0", features = [ +wayland-protocols-plasma = { version = "0.3.8", features = [ "client", ], optional = true } x11-dl = { version = "2.19.1", optional = true } @@ -277,57 +294,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', - 'console', - 'CssStyleDeclaration', - 'Document', - 'DomException', - 'DomRect', - 'DomRectReadOnly', - 'Element', - 'Event', - 'EventTarget', - 'FocusEvent', - 'HtmlCanvasElement', - 'HtmlElement', - 'HtmlImageElement', - 'ImageBitmap', - 'ImageBitmapOptions', - 'ImageBitmapRenderingContext', - 'ImageData', - 'IntersectionObserver', - 'IntersectionObserverEntry', - 'KeyboardEvent', - 'MediaQueryList', - 'MessageChannel', - 'MessagePort', - 'Node', - 'PageTransitionEvent', - 'PointerEvent', - 'PremultiplyAlpha', - 'ResizeObserver', - 'ResizeObserverBoxOptions', - 'ResizeObserverEntry', - 'ResizeObserverOptions', - 'ResizeObserverSize', - 'VisibilityState', - 'Window', - 'WheelEvent', - '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 b66142122b..ab958874a7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ```toml [dependencies] -winit = "0.30.1" +winit = "0.30.13" ``` ## [Documentation](https://docs.rs/winit) @@ -19,7 +19,7 @@ For features _outside_ the scope of winit, see [Are we GUI Yet?](https://arewegu ## Contact Us -Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room. If you don't get an answer there, try [![Libera.Chat](https://img.shields.io/badge/libera.chat-%23winit-red.svg)](https://web.libera.chat/#winit). +Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room. The maintainers have a meeting every friday at UTC 15. The meeting notes can be found [here](https://hackmd.io/@winit-meetings). @@ -33,6 +33,10 @@ Winit is designed to be a low-level brick in a hierarchy of libraries. Consequen show something on the window you need to use the platform-specific getters provided by winit, or another library. +## CONTRIBUTING + +For contributing guidelines see [CONTRIBUTING.md](./CONTRIBUTING.md). + ## MSRV Policy This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to diff --git a/deny.toml b/deny.toml index 317542b864..735c336fd9 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,46 +23,58 @@ 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. - { name = "libloading" }, # x11rb uses a different version until the next update + { crate = "raw-window-handle", reason = "we depend on multiple behind features" }, + { crate = "bitflags@1", reason = "the ecosystem is in the process of migrating" }, + { crate = "rustix@0.38", reason = "the ecosystem is in the process of migrating" }, + { crate = "linux-raw-sys@0.4", 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 = ["ci/*", "githooks/*", "cargo.sh"] +crate = "zerocopy" + +[[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 3a4bd61519..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); @@ -212,6 +236,12 @@ impl Application { Action::PrintHelp => self.print_help(), #[cfg(macos_platform)] Action::CycleOptionAsAlt => window.cycle_option_as_alt(), + Action::SetTheme(theme) => { + window.window.set_theme(theme); + // Get the resulting current theme to draw with + let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark); + window.set_draw_theme(actual_theme); + }, #[cfg(macos_platform)] Action::CreateNewTab => { let tab_id = window.window.tabbing_identifier(); @@ -334,7 +364,7 @@ impl ApplicationHandler for Application { }, WindowEvent::ThemeChanged(theme) => { info!("Theme changed to {theme:?}"); - window.set_theme(theme); + window.set_draw_theme(theme); }, WindowEvent::RedrawRequested => { if let Err(err) = window.draw() { @@ -733,8 +763,8 @@ impl WindowState { self.window.request_redraw(); } - /// Change the theme. - fn set_theme(&mut self, theme: Theme) { + /// Change the theme that things are drawn in. + fn set_draw_theme(&mut self, theme: Theme) { self.theme = theme; self.window.request_redraw(); } @@ -884,6 +914,7 @@ enum Action { ShowWindowMenu, #[cfg(macos_platform)] CycleOptionAsAlt, + SetTheme(Option), #[cfg(macos_platform)] CreateNewTab, RequestResize, @@ -915,6 +946,9 @@ impl Action { Action::ShowWindowMenu => "Show window menu", #[cfg(macos_platform)] Action::CycleOptionAsAlt => "Cycle option as alt mode", + Action::SetTheme(None) => "Change to the system theme", + Action::SetTheme(Some(Theme::Light)) => "Change to a light theme", + Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme", #[cfg(macos_platform)] Action::CreateNewTab => "Create new tab", Action::RequestResize => "Request a resize", @@ -1059,6 +1093,10 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[ Action::AnimationCustomCursor, ), Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility), + // K. + Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)), + Binding::new("K", ModifiersState::SUPER, Action::SetTheme(Some(Theme::Light))), + Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))), #[cfg(macos_platform)] Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab), #[cfg(macos_platform)] diff --git a/src/changelog/v0.20.md b/src/changelog/v0.20.md index 55ad78e81b..eb4ba6a1de 100644 --- a/src/changelog/v0.20.md +++ b/src/changelog/v0.20.md @@ -137,7 +137,7 @@ - On X11, non-resizable windows now have maximize explicitly disabled. - On Windows, support paths longer than MAX_PATH (260 characters) in `WindowEvent::DroppedFile` -and `WindowEvent::HoveredFile`. + and `WindowEvent::HoveredFile`. - On Mac, implement `DeviceEvent::Button`. - Change `Event::Suspended(true / false)` to `Event::Suspended` and `Event::Resumed`. - On X11, fix sanity check which checks that a monitor's reported width and height (in millimeters) are non-zero when calculating the DPI factor. diff --git a/src/changelog/v0.29.md b/src/changelog/v0.29.md index 4558dd6090..fc17f1701a 100644 --- a/src/changelog/v0.29.md +++ b/src/changelog/v0.29.md @@ -250,7 +250,7 @@ - On Web, fix some `WindowBuilder` methods doing nothing. - On Web, fix some `Window` methods using incorrect HTML attributes instead of CSS properties. - On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events. -- On Web, fix touch input not gaining or loosing focus. +- On Web, fix touch input not gaining or losing focus. - On Web, fix touch location to be as accurate as mouse position. - On Web, handle coalesced pointer events, which increases the resolution of pointer inputs. - On Web, implement `Window::focus_window()`. diff --git a/src/changelog/v0.30.md b/src/changelog/v0.30.md index 92efbb66ba..6b0b47dd3b 100644 --- a/src/changelog/v0.30.md +++ b/src/changelog/v0.30.md @@ -1,3 +1,177 @@ +## 0.30.13 + +### Added + +- On Wayland, add `Window::set_resize_increments`. + +### Fixed + +- On macOS, fixed crash when dragging non-file content onto window. +- On X11, fix `set_hittest` not working on some window managers. +- On X11, fix debug mode overflow panic in `set_timestamp`. +- On macOS, fix crash in `set_marked_text` when native Pinyin IME sends out-of-bounds `selected_range`. +- On Windows, fix `WM_IME_SETCONTEXT` IME UI flag masking on `lParam`. +- On Android, populate `KeyEvent::text` and `KeyEvent::text_with_all_modifiers` via `Key::to_text()`. + +## 0.30.12 + +### Fixed + +- On macOS, fix crash on macOS 26 by using objc2's `relax-sign-encoding` feature. + +## 0.30.11 + +### Fixed + +- On Windows, fixed crash in should_apps_use_dark_mode() for Windows versions < 17763. +- On Wayland, fixed `pump_events` driven loop deadlocking when loop was not drained before exit. + +## 0.30.10 + +### Added + +- On Windows, add `IconExtWindows::from_resource_name`. +- On Windows, add `CursorGrabMode::Locked`. +- On Wayland, add `WindowExtWayland::xdg_toplevel`. + +### Changed + +- On macOS, no longer need control of the main `NSApplication` class (which means you can now override it yourself). +- On iOS, remove custom application delegates. You are now allowed to override the + application delegate yourself. +- On iOS, no longer act as-if the application successfully open all URLs. Override + `application:didFinishLaunchingWithOptions:` and provide the desired behaviour yourself. + +### Fixed + +- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw. +- On macOS, `WindowExtMacOS::set_simple_fullscreen` now honors `WindowExtMacOS::set_borderless_game` +- On X11 and Wayland, fixed pump_events with `Some(Duration::Zero)` blocking with `Wait` polling mode +- On Wayland, fixed a crash when consequently calling `set_cursor_grab` without pointer focus. +- On Wayland, ensure that external event loop is woken-up when using pump_events and integrating via `FD`. +- On Wayland, apply fractional scaling to custom cursors. +- On macOS, fixed `run_app_on_demand` returning without closing open windows. +- On macOS, fixed `VideoMode::refresh_rate_millihertz` for fractional refresh rates. +- On macOS, store monitor handle to avoid panics after going in/out of sleep. +- On macOS, allow certain invalid monitor handles and return `None` instead of panicking. +- On Windows, fixed `Ime::Preedit` cursor offset calculation. + +## 0.30.9 + +### Changed + +- On Wayland, no longer send an explicit clearing `Ime::Preedit` just prior to a new `Ime::Preedit`. + +### Fixed + +- On X11, fix crash with uim. +- On X11, fix modifiers for keys that were sent by the same X11 request. +- On iOS, fix high CPU usage even when using `ControlFlow::Wait`. + +## 0.30.8 + +### Added + +- `ActivationToken::from_raw` and `ActivationToken::into_raw`. +- On X11, add a workaround for disabling IME on GNOME. + +### Fixed + +- On Windows, fixed the event loop not waking on accessibility requests. +- On X11, fixed cursor grab mode state tracking on error. + +## 0.30.7 + +### Fixed + +- On X11, fixed KeyboardInput delivered twice when IME enabled. + +## 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 + +- Add `ActiveEventLoop::system_theme()`, returning the current system theme. +- On Web, implement `Error` for `platform::web::CustomCursorError`. +- On Android, add `{Active,}EventLoopExtAndroid::android_app()` to access the app used to create the loop. + +### Fixed + +- On MacOS, fix building with `feature = "rwh_04"`. +- On Web, pen events are now routed through to `WindowEvent::Cursor*`. +- On macOS, fix panic when releasing not available monitor. +- On MacOS, return the system theme in `Window::theme()` if no theme override is set. + +## 0.30.4 + +### Changed + +- `DeviceId::dummy()` and `WindowId::dummy()` are no longer marked `unsafe`. + +### Fixed + +- On Wayland, avoid crashing when compositor is misbehaving. +- On Web, fix `WindowEvent::Resized` not using `requestAnimationFrame` when sending + `WindowEvent::RedrawRequested` and also potentially causing `WindowEvent::RedrawRequested` + to not be de-duplicated. +- Account for different browser engine implementations of pointer movement coordinate space. + +## 0.30.3 + +### Added + +- On Web, add `EventLoopExtWebSys::(set_)poll_strategy()` to allow setting + control flow strategies before starting the event loop. +- On Web, add `WaitUntilStrategy`, which allows to set different strategies for + `ControlFlow::WaitUntil`. By default the Prioritized Task Scheduling API is + used, with a fallback to `setTimeout()` with a trick to circumvent throttling + to 4ms. But an option to use a Web worker to schedule the timer is available + as well, which commonly prevents any throttling when the window is not focused. + +### Changed + +- On macOS, set the window theme on the `NSWindow` instead of application-wide. + +### Fixed + +- On X11, build on arm platforms. +- On macOS, fixed `WindowBuilder::with_theme` not having any effect on the window. + +## 0.30.2 + +### Fixed + +- On Web, fix `EventLoopProxy::send_event()` triggering event loop immediately + when not called from inside the event loop. Now queues a microtask instead. +- On Web, stop overwriting default cursor with `CursorIcon::Default`. +- On Web, prevent crash when using `InnerSizeWriter::request_inner_size()`. +- On macOS, fix not working opacity for entire window. + ## 0.30.1 ### Added diff --git a/src/changelog/v0.9.md b/src/changelog/v0.9.md index ff36cf3f6d..2a9e8cc320 100644 --- a/src/changelog/v0.9.md +++ b/src/changelog/v0.9.md @@ -3,20 +3,20 @@ - Added event `WindowEvent::HiDPIFactorChanged`. - Added method `MonitorId::get_hidpi_factor`. - Deprecated `get_inner_size_pixels` and `get_inner_size_points` methods of `Window` in favor of -`get_inner_size`. + `get_inner_size`. - **Breaking:** `EventsLoop` is `!Send` and `!Sync` because of platform-dependant constraints, but `Window`, `WindowId`, `DeviceId` and `MonitorId` guaranteed to be `Send`. - `MonitorId::get_position` now returns `(i32, i32)` instead of `(u32, u32)`. - Rewrite of the wayland backend to use wayland-client-0.11 - Support for dead keys on wayland for keyboard utf8 input - Monitor enumeration on Windows is now implemented using `EnumDisplayMonitors` instead of -`EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`. + `EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`. - On Windows added `MonitorIdExt::hmonitor` method - Impl `Clone` for `EventsLoopProxy` - `EventsLoop::get_primary_monitor()` on X11 will fallback to any available monitor if no primary is found - Support for touch event on wayland - `WindowEvent`s `MouseMoved`, `MouseEntered`, and `MouseLeft` have been renamed to -`CursorMoved`, `CursorEntered`, and `CursorLeft`. + `CursorMoved`, `CursorEntered`, and `CursorLeft`. - New `DeviceEvent`s added, `MouseMotion` and `MouseWheel`. - Send `CursorMoved` event after `CursorEntered` and `Focused` events. - Add support for `ModifiersState`, `MouseMove`, `MouseInput`, `MouseMotion` for emscripten backend. diff --git a/src/event.rs b/src/event.rs index 5cd3877a26..4e01420af3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -389,6 +389,8 @@ pub enum WindowEvent { /// Applications might wish to react to this to change the theme of the content of the window /// when the system changes the window theme. /// + /// This only reports a change if the window theme was not overridden by [`Window::set_theme`]. + /// /// ## Platform-specific /// /// - **iOS / Android / X11 / Wayland / Orbital:** Unsupported. @@ -447,16 +449,13 @@ pub struct DeviceId(pub(crate) platform_impl::DeviceId); impl DeviceId { /// Returns a dummy id, useful for unit testing. /// - /// # Safety + /// # Notes /// /// The only guarantee made about the return value of this function is that /// it will always be equal to itself and to future values returned by this function. /// No other guarantees are made. This may be equal to a real `DeviceId`. - /// - /// **Passing this into a winit function will result in undefined behavior.** - pub const unsafe fn dummy() -> Self { - #[allow(unused_unsafe)] - DeviceId(unsafe { platform_impl::DeviceId::dummy() }) + pub const fn dummy() -> Self { + DeviceId(platform_impl::DeviceId::dummy()) } } @@ -533,11 +532,11 @@ pub struct KeyEvent { /// ## Caveats /// /// - Certain niche hardware will shuffle around physical key positions, e.g. a keyboard that - /// implements DVORAK in hardware (or firmware) + /// implements DVORAK in hardware (or firmware) /// - Your application will likely have to handle keyboards which are missing keys that your - /// own keyboard has. + /// own keyboard has. /// - Certain `KeyCode`s will move between a couple of different positions depending on what - /// layout the keyboard was manufactured to support. + /// layout the keyboard was manufactured to support. /// /// **Because of these caveats, it is important that you provide users with a way to configure /// most (if not all) keybinds in your application.** @@ -559,8 +558,7 @@ pub struct KeyEvent { /// /// This has two use cases: /// - Allows querying whether the current input is a Dead key. - /// - Allows handling key-bindings on platforms which don't - /// support [`key_without_modifiers`]. + /// - Allows handling key-bindings on platforms which don't support [`key_without_modifiers`]. /// /// If you use this field (or [`key_without_modifiers`] for that matter) for keyboard /// shortcuts, **it is important that you provide users with a way to configure your @@ -568,8 +566,8 @@ pub struct KeyEvent { /// incompatible keyboard layout.** /// /// ## Platform-specific - /// - **Web:** Dead keys might be reported as the real key instead - /// of `Dead` depending on the browser/OS. + /// - **Web:** Dead keys might be reported as the real key instead of `Dead` depending on the + /// browser/OS. /// /// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers pub logical_key: keyboard::Key, @@ -623,7 +621,7 @@ pub struct KeyEvent { /// /// # Example /// - /// In games, you often want to ignore repated key events - this can be + /// In games, you often want to ignore repeated key events - this can be /// done by ignoring events where this property is set. /// /// ``` @@ -851,8 +849,8 @@ pub struct Touch { /// /// - Only available on **iOS** 9.0+, **Windows** 8+, **Web**, and **Android**. /// - **Android**: This will never be [None]. If the device doesn't support pressure - /// sensitivity, force will either be 0.0 or 1.0. Also see the - /// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE). + /// sensitivity, force will either be 0.0 or 1.0. Also see the + /// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE). pub force: Option, /// Unique identifier of a finger. pub id: u64, @@ -1019,7 +1017,7 @@ mod tests { ($closure:expr) => {{ #[allow(unused_mut)] let mut x = $closure; - let did = unsafe { event::DeviceId::dummy() }; + let did = event::DeviceId::dummy(); #[allow(deprecated)] { @@ -1029,7 +1027,7 @@ mod tests { use crate::window::WindowId; // Mainline events. - let wid = unsafe { WindowId::dummy() }; + let wid = WindowId::dummy(); x(UserEvent(())); x(NewEvents(event::StartCause::Init)); x(AboutToWait); @@ -1154,11 +1152,11 @@ mod tests { #[test] fn ensure_attrs_do_not_panic() { foreach_event!(|event: event::Event<()>| { - let _ = format!("{:?}", event); + let _ = format!("{event:?}"); }); let _ = event::StartCause::Init.clone(); - let did = unsafe { crate::event::DeviceId::dummy() }.clone(); + let did = crate::event::DeviceId::dummy().clone(); HashSet::new().insert(did); let mut set = [did, did, did]; set.sort_unstable(); diff --git a/src/event_loop.rs b/src/event_loop.rs index 99e72040f8..233374beec 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -23,7 +23,7 @@ use crate::error::{EventLoopError, OsError}; use crate::event::Event; use crate::monitor::MonitorHandle; use crate::platform_impl; -use crate::window::{CustomCursor, CustomCursorSource, Window, WindowAttributes}; +use crate::window::{CustomCursor, CustomCursorSource, Theme, Window, WindowAttributes}; /// Provides a way to retrieve events from the system and from the windows that were registered to /// the events loop. @@ -437,6 +437,17 @@ impl ActiveEventLoop { self.p.listen_device_events(allowed); } + /// Returns the current system theme. + /// + /// Returns `None` if it cannot be determined on the current platform. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. + pub fn system_theme(&self) -> Option { + self.p.system_theme() + } + /// Sets the [`ControlFlow`]. pub fn set_control_flow(&self, control_flow: ControlFlow) { self.p.set_control_flow(control_flow) @@ -490,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/keyboard.rs b/src/keyboard.rs index 49d55c1e1f..7b406f0890 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1232,7 +1232,7 @@ pub enum NamedKey { Dimmer, /// Swap video sources. (`VK_DISPLAY_SWAP`) DisplaySwap, - /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + /// Select Digital Video Recorder. (`KEYCODE_DVR`) DVR, /// Exit the current application. (`VK_EXIT`) Exit, diff --git a/src/lib.rs b/src/lib.rs index eb1bd68633..41a55b234e 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 @@ -157,7 +165,6 @@ //! [`Window`]: window::Window //! [`WindowId`]: window::WindowId //! [`WindowAttributes`]: window::WindowAttributes -//! [window_new]: window::Window::new //! [`create_window`]: event_loop::ActiveEventLoop::create_window //! [`Window::id()`]: window::Window::id //! [`WindowEvent`]: event::WindowEvent @@ -175,8 +182,11 @@ #![cfg_attr(clippy, deny(warnings))] // Doc feature labels can be tested locally by running RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly // doc -#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc, docsrs)))] +#![cfg_attr(docsrs, feature(doc_cfg), doc(auto_cfg(hide(doc, docsrs))))] #![allow(clippy::missing_safety_doc)] +#![warn(clippy::uninlined_format_args)] +// TODO: wasm-binding needs to be updated for that to be resolved, for now just silence it. +#![cfg_attr(web_platform, allow(unknown_lints, renamed_and_removed_lints, wasm_c_abi))] #[cfg(feature = "rwh_04")] pub use rwh_04 as raw_window_handle_04; diff --git a/src/platform/android.rs b/src/platform/android.rs index de8ff40ad7..b76159c095 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.1", +//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.13", //! 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 @@ -76,12 +76,22 @@ use crate::window::{Window, WindowAttributes}; use self::activity::{AndroidApp, ConfigurationRef, Rect}; /// Additional methods on [`EventLoop`] that are specific to Android. -pub trait EventLoopExtAndroid {} +pub trait EventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} -impl EventLoopExtAndroid for EventLoop {} +impl EventLoopExtAndroid for EventLoop { + fn android_app(&self) -> &AndroidApp { + &self.event_loop.android_app + } +} /// Additional methods on [`ActiveEventLoop`] that are specific to Android. -pub trait ActiveEventLoopExtAndroid {} +pub trait ActiveEventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} /// Additional methods on [`Window`] that are specific to Android. pub trait WindowExtAndroid { @@ -100,7 +110,11 @@ impl WindowExtAndroid for Window { } } -impl ActiveEventLoopExtAndroid for ActiveEventLoop {} +impl ActiveEventLoopExtAndroid for ActiveEventLoop { + fn android_app(&self) -> &AndroidApp { + &self.p.app + } +} /// Additional methods on [`WindowAttributes`] that are specific to Android. pub trait WindowAttributesExtAndroid {} @@ -108,9 +122,9 @@ pub trait WindowAttributesExtAndroid {} impl WindowAttributesExtAndroid for WindowAttributes {} pub trait EventLoopBuilderExtAndroid { - /// Associates the `AndroidApp` that was passed to `android_main()` with the event loop + /// Associates the [`AndroidApp`] that was passed to `android_main()` with the event loop /// - /// This must be called on Android since the `AndroidApp` is not global state. + /// This must be called on Android since the [`AndroidApp`] is not global state. fn with_android_app(&mut self, app: AndroidApp) -> &mut Self; /// Calling this will mark the volume keys to be manually handled by the application @@ -147,7 +161,7 @@ impl EventLoopBuilderExtAndroid for EventLoopBuilder { /// depending on the `android_activity` crate, and instead consume the API that /// is re-exported by Winit. /// -/// For compatibility applications should then import the `AndroidApp` type for +/// For compatibility applications should then import the [`AndroidApp`] type for /// their `android_main(app: AndroidApp)` function like: /// ```rust /// #[cfg(target_os = "android")] diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 59f5396163..6e62876834 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -3,11 +3,14 @@ //! Winit has an OS requirement of iOS 8 or higher, and is regularly tested on //! iOS 9.3. //! +//! ## Window initialization +//! //! iOS's main `UIApplicationMain` does some init work that's required by all //! UI-related code (see issue [#1705]). It is best to create your windows -//! inside `Event::Resumed`. +//! inside [`ApplicationHandler::resumed`]. //! //! [#1705]: https://github.com/rust-windowing/winit/issues/1705 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed //! //! ## Building app //! @@ -63,6 +66,16 @@ //! opengl will result in segfault. //! //! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed. +//! +//! ## Custom `UIApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to access some of the more niche stuff that [the application +//! delegate][app-delegate] provides. This functionality is not exposed directly in Winit, since it +//! would increase the API surface by quite a lot. Instead, Winit guarantees that it will not +//! register an application delegate, so you can set up a custom one in a nib file instead. +//! +//! [app-delegate]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate?language=objc use std::os::raw::c_void; diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 66f25c92b2..30623b01e0 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -3,16 +3,84 @@ //! Winit has an OS requirement of macOS 10.11 or higher (same as Rust //! itself), and is regularly tested on macOS 10.14. //! +//! ## Window initialization +//! //! A lot of functionality expects the application to be ready before you //! start doing anything; this includes creating windows, fetching monitors, //! drawing, and so on, see issues [#2238], [#2051] and [#2087]. //! //! If you encounter problems, you should try doing your initialization inside -//! `Event::Resumed`. +//! [`ApplicationHandler::resumed`]. //! //! [#2238]: https://github.com/rust-windowing/winit/issues/2238 //! [#2051]: https://github.com/rust-windowing/winit/issues/2051 //! [#2087]: https://github.com/rust-windowing/winit/issues/2087 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed +//! +//! ## Custom `NSApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to do more niche stuff, such as [handle when the user re-activates the +//! application][reopen]. Such functionality is not exposed directly in Winit, since it would +//! increase the API surface by quite a lot. +//! +//! [reopen]: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen?language=objc +//! +//! Instead, Winit guarantees that it will not register an application delegate, so the solution is +//! to register your own application delegate, as outlined in the following example (see +//! `objc2-app-kit` for more detailed information). +#![cfg_attr(target_os = "macos", doc = "```")] +#![cfg_attr(not(target_os = "macos"), doc = "```ignore")] +//! use objc2::rc::Retained; +//! use objc2::runtime::ProtocolObject; +//! use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +//! use objc2_app_kit::{NSApplication, NSApplicationDelegate}; +//! use objc2_foundation::{NSArray, NSURL, MainThreadMarker, NSObject, NSObjectProtocol}; +//! use winit::event_loop::EventLoop; +//! +//! declare_class!( +//! struct AppDelegate; +//! +//! unsafe impl ClassType for AppDelegate { +//! type Super = NSObject; +//! type Mutability = mutability::MainThreadOnly; +//! const NAME: &'static str = "MyAppDelegate"; +//! } +//! +//! impl DeclaredClass for AppDelegate {} +//! +//! unsafe impl NSObjectProtocol for AppDelegate {} +//! +//! unsafe impl NSApplicationDelegate for AppDelegate { +//! #[method(application:openURLs:)] +//! fn application_openURLs(&self, application: &NSApplication, urls: &NSArray) { +//! // Note: To specifically get `application:openURLs:` to work, you _might_ +//! // have to bundle your application. This is not done in this example. +//! println!("open urls: {application:?}, {urls:?}"); +//! } +//! } +//! ); +//! +//! impl AppDelegate { +//! fn new(mtm: MainThreadMarker) -> Retained { +//! unsafe { msg_send_id![super(mtm.alloc().set_ivars(())), init] } +//! } +//! } +//! +//! fn main() -> Result<(), Box> { +//! let event_loop = EventLoop::new()?; +//! +//! let mtm = MainThreadMarker::new().unwrap(); +//! let delegate = AppDelegate::new(mtm); +//! // Important: Call `sharedApplication` after `EventLoop::new`, +//! // doing it before is not yet supported. +//! let app = NSApplication::sharedApplication(mtm); +//! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); +//! +//! // event_loop.run_app(&mut my_app); +//! Ok(()) +//! } +//! ``` use std::os::raw::c_void; @@ -94,6 +162,14 @@ pub trait WindowExtMacOS { /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. fn option_as_alt(&self) -> OptionAsAlt; + + /// Disable the Menu Bar and Dock in Simple or Borderless Fullscreen mode. Useful for games. + /// The effect is applied when [`WindowExtMacOS::set_simple_fullscreen`] or + /// [`Window::set_fullscreen`] is called. + 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 +242,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 +302,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 +372,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 +438,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/pump_events.rs b/src/platform/pump_events.rs index 7eedda2816..4476635356 100644 --- a/src/platform/pump_events.rs +++ b/src/platform/pump_events.rs @@ -51,19 +51,19 @@ pub trait EventLoopExtPumpEvents { /// buffered and handled outside of Winit include: /// - `RedrawRequested` events, used to schedule rendering. /// - /// macOS for example uses a `drawRect` callback to drive rendering - /// within applications and expects rendering to be finished before - /// the `drawRect` callback returns. + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. /// - /// For portability it's strongly recommended that applications should - /// keep their rendering inside the closure provided to Winit. + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. /// - Any lifecycle events, such as `Suspended` / `Resumed`. /// - /// The handling of these events needs to be synchronized with the - /// operating system and it would never be appropriate to buffer a - /// notification that your application has been suspended or resumed and - /// then handled that later since there would always be a chance that - /// other lifecycle events occur while the event is buffered. + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. /// /// ## Supported Platforms /// @@ -74,13 +74,13 @@ pub trait EventLoopExtPumpEvents { /// /// ## Unsupported Platforms /// - /// - **Web:** This API is fundamentally incompatible with the event-based way in which - /// Web browsers work because it's not possible to have a long-running external - /// loop that would block the browser and there is nothing that can be - /// polled to ask for new new events. Events are delivered via callbacks based - /// on an event loop that is internal to the browser itself. + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new new events. + /// Events are delivered via callbacks based on an event loop that is internal to the browser + /// itself. /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so - /// there's no way to support the same approach to polling as on MacOS. + /// there's no way to support the same approach to polling as on MacOS. /// /// ## Platform-specific /// diff --git a/src/platform/run_on_demand.rs b/src/platform/run_on_demand.rs index ecb22395a7..0bb96e92ed 100644 --- a/src/platform/run_on_demand.rs +++ b/src/platform/run_on_demand.rs @@ -42,7 +42,9 @@ pub trait EventLoopExtRunOnDemand { /// # Caveats /// - This extension isn't available on all platforms, since it's not always possible to return /// to the caller (specifically this is impossible on iOS and Web - though with the Web - /// backend it is possible to use `EventLoopExtWebSys::spawn()`[^1] more than once instead). + /// backend it is possible to use `EventLoopExtWebSys::spawn()` + #[cfg_attr(not(web_platform), doc = "[^1]")] + /// more than once instead). /// - No [`Window`] state can be carried between separate runs of the event loop. /// /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you @@ -61,8 +63,8 @@ pub trait EventLoopExtRunOnDemand { /// are delivered via callbacks based on an event loop that is internal to the browser itself. /// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS. #[cfg_attr(not(web_platform), doc = "[^1]: `spawn()` is only available on `wasm` platforms.")] - #[rustfmt::skip] /// + #[rustfmt::skip] /// [`exit()`]: ActiveEventLoop::exit() /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() fn run_app_on_demand>( diff --git a/src/platform/startup_notify.rs b/src/platform/startup_notify.rs index c9047a03b2..6fab284e1a 100644 --- a/src/platform/startup_notify.rs +++ b/src/platform/startup_notify.rs @@ -64,7 +64,7 @@ impl EventLoopExtStartupNotify for ActiveEventLoop { crate::platform_impl::ActiveEventLoop::X(_) => env::var(X11_VAR), } .ok() - .map(ActivationToken::_new) + .map(ActivationToken::from_raw) } } @@ -94,6 +94,6 @@ pub fn reset_activation_token_env() { /// /// This could be used before running daemon processes. pub fn set_activation_token_env(token: ActivationToken) { - env::set_var(X11_VAR, &token._token); - env::set_var(WAYLAND_VAR, token._token); + env::set_var(X11_VAR, &token.token); + env::set_var(WAYLAND_VAR, token.token); } diff --git a/src/platform/wayland.rs b/src/platform/wayland.rs index db6a217dcb..9c1a8e4379 100644 --- a/src/platform/wayland.rs +++ b/src/platform/wayland.rs @@ -13,6 +13,10 @@ //! * `wayland-csd-adwaita` (default). //! * `wayland-csd-adwaita-crossfont`. //! * `wayland-csd-adwaita-notitle`. + +use std::ffi::c_void; +use std::ptr::NonNull; + use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; use crate::monitor::MonitorHandle; use crate::window::{Window, WindowAttributes}; @@ -72,9 +76,25 @@ impl EventLoopBuilderExtWayland for EventLoopBuilder { } /// Additional methods on [`Window`] that are specific to Wayland. -pub trait WindowExtWayland {} +/// +/// [`Window`]: crate::window::Window +pub trait WindowExtWayland { + /// Returns `xdg_toplevel` of the window or [`None`] if the window is X11 window. + fn xdg_toplevel(&self) -> Option>; +} -impl WindowExtWayland for Window {} +impl WindowExtWayland for Window { + #[inline] + fn xdg_toplevel(&self) -> Option> { + #[allow(clippy::single_match)] + match &self.window { + #[cfg(x11_platform)] + crate::platform_impl::Window::X(_) => None, + #[cfg(wayland_platform)] + crate::platform_impl::Window::Wayland(window) => window.xdg_toplevel(), + } + } +} /// Additional methods on [`WindowAttributes`] that are specific to Wayland. pub trait WindowAttributesExtWayland { diff --git a/src/platform/web.rs b/src/platform/web.rs index 25765ae8e3..f257ca416b 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -190,6 +190,34 @@ pub trait EventLoopExtWebSys { fn spawn(self, event_handler: F) where F: 'static + FnMut(Event, &ActiveEventLoop); + + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; } impl EventLoopExtWebSys for EventLoop { @@ -207,6 +235,22 @@ impl EventLoopExtWebSys for EventLoop { { self.event_loop.spawn(event_handler) } + + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.event_loop.set_poll_strategy(strategy); + } + + fn poll_strategy(&self) -> PollStrategy { + self.event_loop.poll_strategy() + } + + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.event_loop.set_wait_until_strategy(strategy); + } + + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.event_loop.wait_until_strategy() + } } pub trait ActiveEventLoopExtWebSys { @@ -224,6 +268,20 @@ pub trait ActiveEventLoopExtWebSys { /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll fn poll_strategy(&self) -> PollStrategy; + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; + /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the /// cursor has completely finished loading. fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture; @@ -244,6 +302,16 @@ impl ActiveEventLoopExtWebSys for ActiveEventLoop { fn poll_strategy(&self) -> PollStrategy { self.p.poll_strategy() } + + #[inline] + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.p.set_wait_until_strategy(strategy); + } + + #[inline] + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.p.wait_until_strategy() + } } /// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. @@ -272,6 +340,29 @@ pub enum PollStrategy { Scheduler, } +/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WaitUntilStrategy { + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy is commonly not affected by browser throttling unless the window is not + /// focused. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, + /// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker]. + /// + /// This strategy is commonly not affected by browser throttling regardless of window focus. + /// + /// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API + Worker, +} + pub trait CustomCursorExtWebSys { /// Returns if this cursor is an animation. fn is_animation(&self) -> bool; @@ -331,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"), } } } @@ -370,3 +461,5 @@ impl Display for CustomCursorError { } } } + +impl Error for CustomCursorError {} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 5b79771a91..e4593be10d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -660,6 +660,17 @@ impl DeviceIdExtWindows for DeviceId { } /// Additional methods on `Icon` that are specific to Windows. +/// +/// Windows icons can be created from files, or from the [`embedded resources`](https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files). +/// +/// The `ICON` resource definition statement use the following syntax: +/// ```rc +/// nameID ICON filename +/// ``` +/// `nameID` is a unique name or a 16-bit unsigned integer value identifying the resource, +/// `filename` is the name of the file that contains the resource. +/// +/// More information about the `ICON` resource can be found at [`Microsoft Learn`](https://learn.microsoft.com/en-us/windows/win32/menurc/icon-resource) portal. pub trait IconExtWindows: Sized { /// Create an icon from a file path. /// @@ -671,7 +682,12 @@ pub trait IconExtWindows: Sized { fn from_path>(path: P, size: Option>) -> Result; - /// Create an icon from a resource embedded in this executable or library. + /// Create an icon from a resource embedded in this executable or library by its ordinal id. + /// + /// The valid `ordinal` values range from 1 to [`u16::MAX`] (inclusive). The value `0` is an + /// invalid ordinal id, but it can be used with [`from_resource_name`] as `"0"`. + /// + /// [`from_resource_name`]: IconExtWindows::from_resource_name /// /// Specify `size` to load a specific icon size from the file, or `None` to load the default /// icon size from the file. @@ -679,6 +695,55 @@ pub trait IconExtWindows: Sized { /// In cases where the specified size does not exist in the file, Windows may perform scaling /// to get an icon of the desired size. fn from_resource(ordinal: u16, size: Option>) -> Result; + + /// Create an icon from a resource embedded in this executable or library by its name. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + /// + /// # Notes + /// + /// Consider the following resource definition statements: + /// ```rc + /// app ICON "app.ico" + /// 1 ICON "a.ico" + /// 0027 ICON "custom.ico" + /// 0 ICON "alt.ico" + /// ``` + /// + /// Due to some internal implementation details of the resource embedding/loading process on + /// Windows platform, strings that can be interpreted as 16-bit unsigned integers (`"1"`, + /// `"002"`, etc.) cannot be used as valid resource names, and instead should be passed into + /// [`from_resource`]: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// use winit::platform::windows::IconExtWindows; + /// use winit::window::Icon; + /// + /// assert!(Icon::from_resource_name("app", None).is_ok()); + /// assert!(Icon::from_resource(1, None).is_ok()); + /// assert!(Icon::from_resource(27, None).is_ok()); + /// assert!(Icon::from_resource_name("27", None).is_err()); + /// assert!(Icon::from_resource_name("0027", None).is_err()); + /// ``` + /// + /// While `0` cannot be used as an ordinal id (see [`from_resource`]), it can be used as a + /// name: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// # use winit::platform::windows::IconExtWindows; + /// # use winit::window::Icon; + /// assert!(Icon::from_resource_name("0", None).is_ok()); + /// assert!(Icon::from_resource(0, None).is_err()); + /// ``` + fn from_resource_name(name: &str, size: Option>) -> Result; } impl IconExtWindows for Icon { @@ -694,4 +759,9 @@ impl IconExtWindows for Icon { let win_icon = crate::platform_impl::WinIcon::from_resource(ordinal, size)?; Ok(Icon { inner: win_icon }) } + + fn from_resource_name(name: &str, size: Option>) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource_name(name, size)?; + Ok(Icon { inner: win_icon }) + } } 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 31285b63f9..c6a7416f46 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -1,5 +1,3 @@ -#![cfg(android_platform)] - use std::cell::Cell; use std::collections::VecDeque; use std::hash::Hash; @@ -134,7 +132,7 @@ impl RedrawRequester { pub struct KeyEventExtra {} pub struct EventLoop { - android_app: AndroidApp, + pub(crate) android_app: AndroidApp, window_target: event_loop::ActiveEventLoop, redraw_flag: SharedFlag, user_events_sender: mpsc::Sender, @@ -448,6 +446,13 @@ impl EventLoop { &mut self.combining_accent, ); + let logical_key = keycodes::to_logical(key_char, keycode); + let text = if state == event::ElementState::Pressed { + logical_key.to_text().map(smol_str::SmolStr::new) + } else { + None + }; + let event = event::Event::WindowEvent { window_id: window::WindowId(WindowId), event: event::WindowEvent::KeyboardInput { @@ -455,10 +460,10 @@ impl EventLoop { event: event::KeyEvent { state, physical_key: keycodes::to_physical_key(keycode), - logical_key: keycodes::to_logical(key_char, keycode), + logical_key, location: keycodes::to_location(keycode), repeat: key.repeat_count() > 0, - text: None, + text, platform_specific: KeyEventExtra {}, }, is_synthetic: false, @@ -648,7 +653,7 @@ impl EventLoopProxy { } pub struct ActiveEventLoop { - app: AndroidApp, + pub(crate) app: AndroidApp, control_flow: Cell, exit: Cell, redraw_requester: RedrawRequester, @@ -679,6 +684,11 @@ impl ActiveEventLoop { rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) } + #[inline] + pub fn system_theme(&self) -> Option { + None + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_display_handle_rwh_06( @@ -905,7 +915,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/ios/app_delegate.rs b/src/platform_impl/ios/app_delegate.rs deleted file mode 100644 index 2328c58c50..0000000000 --- a/src/platform_impl/ios/app_delegate.rs +++ /dev/null @@ -1,60 +0,0 @@ -use objc2::{declare_class, mutability, ClassType, DeclaredClass}; -use objc2_foundation::{MainThreadMarker, NSObject}; -use objc2_ui_kit::UIApplication; - -use super::app_state::{self, send_occluded_event_for_all_windows, EventWrapper}; -use crate::event::Event; - -declare_class!( - pub struct AppDelegate; - - unsafe impl ClassType for AppDelegate { - type Super = NSObject; - type Mutability = mutability::InteriorMutable; - const NAME: &'static str = "WinitApplicationDelegate"; - } - - impl DeclaredClass for AppDelegate {} - - // UIApplicationDelegate protocol - unsafe impl AppDelegate { - #[method(application:didFinishLaunchingWithOptions:)] - fn did_finish_launching(&self, _application: &UIApplication, _: *mut NSObject) -> bool { - app_state::did_finish_launching(MainThreadMarker::new().unwrap()); - true - } - - #[method(applicationDidBecomeActive:)] - fn did_become_active(&self, _application: &UIApplication) { - let mtm = MainThreadMarker::new().unwrap(); - app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)) - } - - #[method(applicationWillResignActive:)] - fn will_resign_active(&self, _application: &UIApplication) { - let mtm = MainThreadMarker::new().unwrap(); - app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)) - } - - #[method(applicationWillEnterForeground:)] - fn will_enter_foreground(&self, application: &UIApplication) { - send_occluded_event_for_all_windows(application, false); - } - - #[method(applicationDidEnterBackground:)] - fn did_enter_background(&self, application: &UIApplication) { - send_occluded_event_for_all_windows(application, true); - } - - #[method(applicationWillTerminate:)] - fn will_terminate(&self, application: &UIApplication) { - app_state::terminated(application); - } - - #[method(applicationDidReceiveMemoryWarning:)] - fn did_receive_memory_warning(&self, _application: &UIApplication) { - let mtm = MainThreadMarker::new().unwrap(); - app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning)) - } - } -); diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 172ffe8e42..e34bf43c97 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)] @@ -373,6 +375,7 @@ impl AppState { (ControlFlow::Wait, ControlFlow::Wait) => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.stop() }, (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) if old_instant == new_instant => @@ -798,7 +801,7 @@ impl EventLoopWaker { // future, but that gets changed to fire immediately in did_finish_launching let timer = CFRunLoopTimerCreate( ptr::null_mut(), - std::f64::MAX, + f64::MAX, 0.000_000_1, 0, 0, @@ -812,11 +815,11 @@ impl EventLoopWaker { } fn stop(&mut self) { - unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } } fn start(&mut self) { - unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } } fn start_at(&mut self, instant: Instant) { diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index f05429598e..a093b7b590 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -13,8 +13,14 @@ use core_foundation::runloop::{ }; use objc2::rc::Retained; use objc2::{msg_send_id, ClassType}; -use objc2_foundation::{MainThreadMarker, NSString}; -use objc2_ui_kit::{UIApplication, UIApplicationMain, UIDevice, UIScreen, UIUserInterfaceIdiom}; +use objc2_foundation::{MainThreadMarker, NSNotificationCenter, NSObject}; +use objc2_ui_kit::{ + UIApplication, UIApplicationDidBecomeActiveNotification, + UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification, + UIApplicationDidReceiveMemoryWarningNotification, UIApplicationMain, + UIApplicationWillEnterForegroundNotification, UIApplicationWillResignActiveNotification, + UIApplicationWillTerminateNotification, UIDevice, UIScreen, UIUserInterfaceIdiom, +}; use crate::error::EventLoopError; use crate::event::Event; @@ -22,11 +28,11 @@ use crate::event_loop::{ ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopClosed, }; use crate::platform::ios::Idiom; -use crate::platform_impl::platform::app_state::{EventLoopHandler, HandlePendingUserEvents}; -use crate::window::{CustomCursor, CustomCursorSource}; +use crate::platform_impl::ios::app_state::{EventLoopHandler, HandlePendingUserEvents}; +use crate::window::{CustomCursor, CustomCursorSource, Theme}; -use super::app_delegate::AppDelegate; -use super::app_state::AppState; +use super::app_state::{send_occluded_event_for_all_windows, AppState, EventWrapper}; +use super::notification_center::create_observer; use super::{app_state, monitor, MonitorHandle}; #[derive(Debug)] @@ -58,6 +64,11 @@ impl ActiveEventLoop { rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) } + #[inline] + pub fn system_theme(&self) -> Option { + None + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_display_handle_rwh_06( @@ -127,6 +138,18 @@ pub struct EventLoop { sender: Sender, receiver: Receiver, window_target: RootActiveEventLoop, + + // Since iOS 9.0, we no longer need to remove the observers before they are deallocated; the + // system instead cleans it up next time it would have posted a notification to it. + // + // Though we do still need to keep the observers around to prevent them from being deallocated. + _did_finish_launching_observer: Retained, + _did_become_active_observer: Retained, + _will_resign_active_observer: Retained, + _will_enter_foreground_observer: Retained, + _did_enter_background_observer: Retained, + _will_terminate_observer: Retained, + _did_receive_memory_warning_observer: Retained, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -153,11 +176,97 @@ impl EventLoop { // this line sets up the main run loop before `UIApplicationMain` setup_control_flow_observers(); + let center = unsafe { NSNotificationCenter::defaultCenter() }; + + let _did_finish_launching_observer = create_observer( + ¢er, + // `application:didFinishLaunchingWithOptions:` + unsafe { UIApplicationDidFinishLaunchingNotification }, + move |_| { + app_state::did_finish_launching(mtm); + }, + ); + let _did_become_active_observer = create_observer( + ¢er, + // `applicationDidBecomeActive:` + unsafe { UIApplicationDidBecomeActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)); + }, + ); + let _will_resign_active_observer = create_observer( + ¢er, + // `applicationWillResignActive:` + unsafe { UIApplicationWillResignActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)); + }, + ); + let _will_enter_foreground_observer = create_observer( + ¢er, + // `applicationWillEnterForeground:` + unsafe { UIApplicationWillEnterForegroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationWillEnterForegroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationWillEnterForegroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, false); + }, + ); + let _did_enter_background_observer = create_observer( + ¢er, + // `applicationDidEnterBackground:` + unsafe { UIApplicationDidEnterBackgroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationDidEnterBackgroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationDidEnterBackgroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, true); + }, + ); + let _will_terminate_observer = create_observer( + ¢er, + // `applicationWillTerminate:` + unsafe { UIApplicationWillTerminateNotification }, + move |notification| { + let app = unsafe { notification.object() } + .expect("UIApplicationWillTerminateNotification to have application object"); + // SAFETY: The `object` in `UIApplicationWillTerminateNotification` is + // (somewhat) documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + app_state::terminated(&app); + }, + ); + let _did_receive_memory_warning_observer = create_observer( + ¢er, + // `applicationDidReceiveMemoryWarning:` + unsafe { UIApplicationDidReceiveMemoryWarningNotification }, + move |_| { + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::MemoryWarning), + ); + }, + ); + Ok(EventLoop { mtm, sender, receiver, window_target: RootActiveEventLoop { p: ActiveEventLoop { mtm }, _marker: PhantomData }, + _did_finish_launching_observer, + _did_become_active_observer, + _will_resign_active_observer, + _will_enter_foreground_observer, + _did_enter_background_observer, + _will_terminate_observer, + _did_receive_memory_warning_observer, }) } @@ -187,9 +296,6 @@ impl EventLoop { app_state::will_launch(self.mtm, handler); - // Ensure application delegate is initialized - let _ = AppDelegate::class(); - extern "C" { // These functions are in crt_externs.h. fn _NSGetArgc() -> *mut c_int; @@ -200,8 +306,10 @@ impl EventLoop { UIApplicationMain( *_NSGetArgc(), NonNull::new(*_NSGetArgv()).unwrap(), + // We intentionally override neither the application nor the delegate, to allow the + // user to do so themselves! + None, None, - Some(&NSString::from_str(AppDelegate::NAME)), ) }; unreachable!() @@ -274,8 +382,7 @@ impl EventLoopProxy { cancel: None, perform: event_loop_proxy_handler, }; - let source = - CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::max_value() - 1, &mut context); + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); CFRunLoopWakeUp(rl); @@ -358,7 +465,7 @@ fn setup_control_flow_observers() { ptr::null_mut(), kCFRunLoopAfterWaiting, 1, // repeat = true - CFIndex::min_value(), + CFIndex::MIN, control_flow_begin_handler, ptr::null_mut(), ); @@ -378,7 +485,7 @@ fn setup_control_flow_observers() { ptr::null_mut(), kCFRunLoopExit | kCFRunLoopBeforeWaiting, 1, // repeat = true - CFIndex::max_value(), + CFIndex::MAX, control_flow_end_handler, ptr::null_mut(), ); diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index bf2c011f25..69e79c921e 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -1,10 +1,9 @@ -#![cfg(ios_platform)] #![allow(clippy::let_unit_value)] -mod app_delegate; mod app_state; mod event_loop; mod monitor; +mod notification_center; mod view; mod view_controller; mod window; @@ -33,7 +32,7 @@ pub(crate) use crate::platform_impl::Fullscreen; pub struct DeviceId; impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { DeviceId } } 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/notification_center.rs b/src/platform_impl/ios/notification_center.rs new file mode 100644 index 0000000000..652bf1d079 --- /dev/null +++ b/src/platform_impl/ios/notification_center.rs @@ -0,0 +1,27 @@ +use std::ptr::NonNull; + +use block2::RcBlock; +use objc2::rc::Retained; +use objc2_foundation::{NSNotification, NSNotificationCenter, NSNotificationName, NSObject}; + +/// Observe the given notification. +/// +/// This is used in Winit as an alternative to declaring an application delegate, as we want to +/// give the user full control over those. +pub fn create_observer( + center: &NSNotificationCenter, + name: &NSNotificationName, + handler: impl Fn(&NSNotification) + 'static, +) -> Retained { + let block = RcBlock::new(move |notification: NonNull| { + handler(unsafe { notification.as_ref() }); + }); + unsafe { + center.addObserverForName_object_queue_usingBlock( + Some(name), + None, // No sender filter + None, // No queue, run on posting thread (i.e. main thread) + &block, + ) + } +} diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index 75386d368e..418968c70c 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 { @@ -188,7 +190,7 @@ declare_class!( // Pass -delta so that action is reversed (TouchPhase::Cancelled, -recognizer.scale()) } - state => panic!("unexpected recognizer state: {:?}", state), + state => panic!("unexpected recognizer state: {state:?}"), }; let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { @@ -247,7 +249,7 @@ declare_class!( // Pass -delta so that action is reversed (TouchPhase::Cancelled, -recognizer.rotation()) } - state => panic!("unexpected recognizer state: {:?}", state), + state => panic!("unexpected recognizer state: {state:?}"), }; // Make delta negative to match macos, convert to degrees @@ -298,7 +300,7 @@ declare_class!( // Pass -delta so that action is reversed (TouchPhase::Cancelled, -last_pan.x, -last_pan.y) } - state => panic!("unexpected recognizer state: {:?}", state), + state => panic!("unexpected recognizer state: {state:?}"), }; @@ -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 8ea7aeae91..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) { @@ -701,7 +713,7 @@ pub struct WindowId { } impl WindowId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { WindowId { window: std::ptr::null_mut() } } } diff --git a/src/platform_impl/linux/common/xkb/mod.rs b/src/platform_impl/linux/common/xkb/mod.rs index 0b951b666c..706397f6f2 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, @@ -320,7 +320,7 @@ impl<'a, 'b> KeyEventResults<'a, 'b> { // The current behaviour makes it so composing a character overrides attempts to input a // control character with the `Ctrl` key. We can potentially add a configuration option - // if someone specifically wants the oppsite behaviour. + // if someone specifically wants the opposite behaviour. pub fn text_with_all_modifiers(&mut self) -> Option { match self.composed_text() { Ok(text) => text, diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 776689fe20..bc0e71c80c 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), @@ -157,7 +161,7 @@ impl From for WindowId { } impl WindowId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { Self(0) } } @@ -171,11 +175,11 @@ pub enum DeviceId { } impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { #[cfg(wayland_platform)] - return DeviceId::Wayland(unsafe { wayland::DeviceId::dummy() }); + return DeviceId::Wayland(wayland::DeviceId::dummy()); #[cfg(all(not(wayland_platform), x11_platform))] - return DeviceId::X(unsafe { x11::DeviceId::dummy() }); + return DeviceId::X(x11::DeviceId::dummy()); } } @@ -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 _); } @@ -692,6 +696,7 @@ unsafe extern "C" fn x_error_callback( 0 } +#[allow(clippy::large_enum_variant)] pub enum EventLoop { #[cfg(wayland_platform)] Wayland(Box>), @@ -764,9 +769,9 @@ impl EventLoop { // Create the display based on the backend. match backend { #[cfg(wayland_platform)] - Backend::Wayland => EventLoop::new_wayland_any_thread().map_err(Into::into), + Backend::Wayland => EventLoop::new_wayland_any_thread(), #[cfg(x11_platform)] - Backend::X => EventLoop::new_x11_any_thread().map_err(Into::into), + Backend::X => EventLoop::new_x11_any_thread(), } } @@ -777,9 +782,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))) @@ -843,6 +850,7 @@ impl EventLoopProxy { } } +#[allow(clippy::large_enum_variant)] pub enum ActiveEventLoop { #[cfg(wayland_platform)] Wayland(wayland::ActiveEventLoop), @@ -897,6 +905,11 @@ impl ActiveEventLoop { x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_05()) } + #[inline] + pub fn system_theme(&self) -> Option { + None + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_display_handle_rwh_06( diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index f379841a6a..fef13f4558 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -4,15 +4,21 @@ use std::cell::{Cell, RefCell}; use std::io::Result as IOResult; use std::marker::PhantomData; use std::mem; +use std::os::fd::OwnedFd; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::rc::Rc; use std::sync::atomic::Ordering; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread::JoinHandle; use std::time::{Duration, Instant}; +use calloop::ping::Ping; +use rustix::event::{PollFd, PollFlags}; +use rustix::pipe::{self, PipeFlags}; use sctk::reexports::calloop::Error as CalloopError; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{globals, Connection, QueueHandle}; +use tracing::warn; use crate::cursor::OnlyCursorImage; use crate::dpi::LogicalSize; @@ -68,6 +74,8 @@ pub struct EventLoop { // XXX drop after everything else, just to be safe. /// Calloop's event loop. event_loop: calloop::EventLoop<'static, WinitState>, + + pump_event_notifier: Option, } impl EventLoop { @@ -168,6 +176,7 @@ impl EventLoop { p: PlatformActiveEventLoop::Wayland(window_target), _marker: PhantomData, }, + pump_event_notifier: None, }; Ok(event_loop) @@ -223,6 +232,27 @@ impl EventLoop { PumpStatus::Exit(code) } else { + // NOTE: spawn a wake-up thread, thus if we have code reading the wayland connection + // in parallel to winit, we ensure that the loop itself is marked as having events. + if timeout.is_some() && self.pump_event_notifier.is_none() { + let awakener = match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.clone() + }, + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + }; + + self.pump_event_notifier = + Some(PumpEventNotifier::spawn(self.connection.clone(), awakener)); + } + + if let Some(pump_event_notifier) = self.pump_event_notifier.as_ref() { + // Notify that we don't have to wait, since we're out of winit. + *pump_event_notifier.control.0.lock().unwrap() = PumpEventNotifierAction::Monitor; + pump_event_notifier.control.1.notify_one(); + } + PumpStatus::Continue } } @@ -285,7 +315,10 @@ impl EventLoop { // Reduce spurious wake-ups. let dispatched_events = self.with_state(|state| state.dispatched_events); - if matches!(cause, StartCause::WaitCancelled { .. }) && !dispatched_events { + if matches!(cause, StartCause::WaitCancelled { .. }) + && !dispatched_events + && timeout.is_none() + { continue; } @@ -600,7 +633,7 @@ impl AsRawFd for EventLoop { pub struct ActiveEventLoop { /// The event loop wakeup source. - pub event_loop_awakener: calloop::ping::Ping, + pub event_loop_awakener: Ping, /// The main queue used by the event loop. pub queue_handle: QueueHandle, @@ -684,3 +717,94 @@ impl ActiveEventLoop { .into()) } } + +#[derive(Debug)] +struct PumpEventNotifier { + /// Whether we're in winit or not. + control: Arc<(Mutex, Condvar)>, + /// Waker handle for the working thread. + worker_waker: Option, + /// Thread handle. + handle: Option>, +} + +impl Drop for PumpEventNotifier { + fn drop(&mut self) { + // Wake-up the thread. + if let Some(worker_waker) = self.worker_waker.as_ref() { + let _ = rustix::io::write(worker_waker.as_fd(), &[0u8]); + } + *self.control.0.lock().unwrap() = PumpEventNotifierAction::Shutdown; + self.control.1.notify_one(); + + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } +} + +impl PumpEventNotifier { + fn spawn(connection: Connection, awakener: Ping) -> Self { + // Start from the waiting state. + let control = Arc::new((Mutex::new(PumpEventNotifierAction::Pause), Condvar::new())); + let control_thread = Arc::clone(&control); + + let (read, write) = match pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK) { + Ok((read, write)) => (read, write), + Err(_) => return Self { control, handle: None, worker_waker: None }, + }; + + let handle = + std::thread::Builder::new().name(String::from("pump_events mon")).spawn(move || { + let (lock, cvar) = &*control_thread; + 'outer: loop { + let mut wait = lock.lock().unwrap(); + while *wait == PumpEventNotifierAction::Pause { + wait = cvar.wait(wait).unwrap(); + } + + // Exit the loop when we're asked to. Given that we poll + // only once we can take the `prepare_read`, but in some cases + // it could be not possible, we may block on `join`. + if *wait == PumpEventNotifierAction::Shutdown { + break 'outer; + } + + // Wake-up the main loop and put this one back to sleep. + *wait = PumpEventNotifierAction::Pause; + drop(wait); + + while let Some(read_guard) = connection.prepare_read() { + let _ = connection.flush(); + let poll_fd = PollFd::from_borrowed_fd(connection.as_fd(), PollFlags::IN); + let pipe_poll_fd = PollFd::from_borrowed_fd(read.as_fd(), PollFlags::IN); + // Read from the `fd` before going back to poll. + if Ok(1) == rustix::io::read(read.as_fd(), &mut [0u8; 1]) { + break 'outer; + } + let _ = rustix::event::poll(&mut [poll_fd, pipe_poll_fd], -1); + // Non-blocking read the connection. + let _ = read_guard.read_without_dispatch(); + } + + awakener.ping(); + } + }); + + if let Some(err) = handle.as_ref().err() { + warn!("failed to spawn pump_events wake-up thread: {err}"); + } + + PumpEventNotifier { control, handle: handle.ok(), worker_waker: Some(write) } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum PumpEventNotifierAction { + /// Monitor the wayland queue. + Monitor, + /// Pause monitoring. + Pause, + /// Shutdown the thread. + Shutdown, +} diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index 3e5939f6db..63052b78af 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -1,5 +1,3 @@ -#![cfg(wayland_platform)] - //! Winit's Wayland backend. use std::fmt::Display; @@ -68,7 +66,7 @@ impl From for OsError { pub struct DeviceId; impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { DeviceId } } diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs index 4e731d47e7..f84c386735 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -18,7 +18,6 @@ use crate::keyboard::ModifiersState; use crate::platform_impl::common::xkb::Context; use crate::platform_impl::wayland::event_loop::sink::EventSink; -use crate::platform_impl::wayland::seat::WinitSeatState; use crate::platform_impl::wayland::state::WinitState; use crate::platform_impl::wayland::{self, DeviceId, WindowId}; @@ -33,7 +32,17 @@ impl Dispatch for WinitState { ) { let seat_state = match state.seats.get_mut(&data.seat.id()) { Some(seat_state) => seat_state, - None => return, + None => { + warn!("Received keyboard event {event:?} without seat"); + return; + }, + }; + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => { + warn!("Received keyboard event {event:?} without keyboard"); + return; + }, }; match event { @@ -43,7 +52,7 @@ impl Dispatch for WinitState { warn!("non-xkb compatible keymap") }, WlKeymapFormat::XkbV1 => { - let context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context; + let context = &mut keyboard_state.xkb_context; context.set_keymap_from_fd(fd, size as usize); }, _ => unreachable!(), @@ -67,7 +76,6 @@ impl Dispatch for WinitState { }; // Drop the repeat, if there were any. - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); keyboard_state.current_repeat = None; if let Some(token) = keyboard_state.repeat_token.take() { keyboard_state.loop_handle.remove(token); @@ -91,9 +99,8 @@ impl Dispatch for WinitState { WlKeyboardEvent::Leave { surface, .. } => { let window_id = wayland::make_wid(&surface); - // NOTE: we should drop the repeat regardless whethere it was for the present + // NOTE: we should drop the repeat regardless whether it was for the present // window of for the window which just went gone. - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); keyboard_state.current_repeat = None; if let Some(token) = keyboard_state.repeat_token.take() { keyboard_state.loop_handle.remove(token); @@ -128,7 +135,7 @@ impl Dispatch for WinitState { let key = key + 8; key_input( - seat_state, + keyboard_state, &mut state.events_sink, data, key, @@ -136,7 +143,6 @@ impl Dispatch for WinitState { false, ); - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); let delay = match keyboard_state.repeat_info { RepeatInfo::Repeat { delay, .. } => delay, RepeatInfo::Disable => return, @@ -163,18 +169,25 @@ impl Dispatch for WinitState { state.dispatched_events = true; let data = wl_keyboard.data::().unwrap(); - let seat_state = state.seats.get_mut(&data.seat.id()).unwrap(); - - // NOTE: The removed on event source is batched, but key change to - // `None` is instant. - let repeat_keycode = - match seat_state.keyboard_state.as_ref().unwrap().current_repeat { - Some(repeat_keycode) => repeat_keycode, - None => return TimeoutAction::Drop, - }; + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => return TimeoutAction::Drop, + }; + + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => return TimeoutAction::Drop, + }; + + // NOTE: The removed on event source is batched, but key change to `None` + // is instant. + let repeat_keycode = match keyboard_state.current_repeat { + Some(repeat_keycode) => repeat_keycode, + None => return TimeoutAction::Drop, + }; key_input( - seat_state, + keyboard_state, &mut state.events_sink, data, repeat_keycode, @@ -183,7 +196,7 @@ impl Dispatch for WinitState { ); // NOTE: the gap could change dynamically while repeat is going. - match seat_state.keyboard_state.as_ref().unwrap().repeat_info { + match keyboard_state.repeat_info { RepeatInfo::Repeat { gap, .. } => TimeoutAction::ToDuration(gap), RepeatInfo::Disable => TimeoutAction::Drop, } @@ -194,7 +207,7 @@ impl Dispatch for WinitState { let key = key + 8; key_input( - seat_state, + keyboard_state, &mut state.events_sink, data, key, @@ -202,7 +215,6 @@ impl Dispatch for WinitState { false, ); - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); if keyboard_state.repeat_info != RepeatInfo::Disable && keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key) && Some(key) == keyboard_state.current_repeat @@ -216,7 +228,7 @@ impl Dispatch for WinitState { WlKeyboardEvent::Modifiers { mods_depressed, mods_latched, mods_locked, group, .. } => { - let xkb_context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context; + let xkb_context = &mut keyboard_state.xkb_context; let xkb_state = match xkb_context.state_mut() { Some(state) => state, None => return, @@ -240,7 +252,6 @@ impl Dispatch for WinitState { ); }, WlKeyboardEvent::RepeatInfo { rate, delay } => { - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); keyboard_state.repeat_info = if rate == 0 { // Stop the repeat once we get a disable event. keyboard_state.current_repeat = None; @@ -348,7 +359,7 @@ impl KeyboardData { } fn key_input( - seat_state: &mut WinitSeatState, + keyboard_state: &mut KeyboardState, event_sink: &mut EventSink, data: &KeyboardData, keycode: u32, @@ -360,8 +371,6 @@ fn key_input( None => return, }; - let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); - let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); if let Some(mut key_context) = keyboard_state.xkb_context.key_context() { let event = key_context.process_key_event(keycode, state, repeat); diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index 103cf6fce0..eaecd93b33 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use ahash::AHashMap; +use tracing::warn; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; @@ -76,7 +77,13 @@ impl SeatHandler for WinitState { seat: WlSeat, capability: SeatCapability, ) { - let seat_state = self.seats.get_mut(&seat.id()).unwrap(); + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::new_capability for unknown seat"); + return; + }, + }; match capability { SeatCapability::Touch if seat_state.touch.is_none() => { @@ -89,8 +96,12 @@ impl SeatHandler for WinitState { }, SeatCapability::Pointer if seat_state.pointer.is_none() => { let surface = self.compositor_state.create_surface(queue_handle); + let viewport = self + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(&surface, queue_handle)); let surface_id = surface.id(); - let pointer_data = WinitPointerData::new(seat.clone()); + let pointer_data = WinitPointerData::new(seat.clone(), viewport); let themed_pointer = self .seat_state .get_pointer_with_theme_and_data( @@ -139,7 +150,13 @@ impl SeatHandler for WinitState { seat: WlSeat, capability: SeatCapability, ) { - let seat_state = self.seats.get_mut(&seat.id()).unwrap(); + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::remove_capability for unknown seat"); + return; + }, + }; if let Some(text_input) = seat_state.text_input.take() { text_input.destroy(); diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index f26c326e9c..3dcb00b269 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -4,6 +4,8 @@ use std::ops::Deref; use std::sync::{Arc, Mutex}; use std::time::Duration; +use tracing::warn; + use sctk::reexports::client::delegate_dispatch; use sctk::reexports::client::protocol::wl_pointer::WlPointer; use sctk::reexports::client::protocol::wl_seat::WlSeat; @@ -16,6 +18,7 @@ use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_ma use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}; use sctk::reexports::client::globals::{BindError, GlobalList}; use sctk::reexports::csd_frame::FrameClick; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; use sctk::compositor::SurfaceData; use sctk::globals::GlobalData; @@ -41,7 +44,21 @@ impl PointerHandler for WinitState { events: &[PointerEvent], ) { let seat = pointer.winit_data().seat(); - let seat_state = self.seats.get(&seat.id()).unwrap(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + }, + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + }, + }; let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); @@ -78,9 +95,7 @@ impl PointerHandler for WinitState { event.position.0, event.position.1, ) { - if let Some(pointer) = seat_state.pointer.as_ref() { - let _ = pointer.set_cursor(connection, icon); - } + let _ = themed_pointer.set_cursor(connection, icon); } }, PointerEventKind::Leave { .. } if parent_surface != surface => { @@ -113,9 +128,7 @@ impl PointerHandler for WinitState { self.events_sink .push_window_event(WindowEvent::CursorEntered { device_id }, window_id); - if let Some(pointer) = seat_state.pointer.as_ref().map(Arc::downgrade) { - window.pointer_entered(pointer); - } + window.pointer_entered(Arc::downgrade(themed_pointer)); // Set the currently focused surface. pointer.winit_data().inner.lock().unwrap().surface = Some(window_id); @@ -126,9 +139,7 @@ impl PointerHandler for WinitState { ); }, PointerEventKind::Leave { .. } => { - if let Some(pointer) = seat_state.pointer.as_ref().map(Arc::downgrade) { - window.pointer_left(pointer); - } + window.pointer_left(Arc::downgrade(themed_pointer)); // Remove the active surface. pointer.winit_data().inner.lock().unwrap().surface = None; @@ -183,15 +194,15 @@ impl PointerHandler for WinitState { pointer_data.phase = phase; // Mice events have both pixel and discrete delta's at the same time. So prefer - // the descrite values if they are present. + // the discrete values if they are present. let delta = if has_discrete_scroll { - // XXX Wayland sign convention is the inverse of winit. + // NOTE: Wayland sign convention is the inverse of winit. MouseScrollDelta::LineDelta( (-horizontal.discrete) as f32, (-vertical.discrete) as f32, ) } else { - // XXX Wayland sign convention is the inverse of winit. + // NOTE: Wayland sign convention is the inverse of winit. MouseScrollDelta::PixelDelta( LogicalPosition::new(-horizontal.absolute, -vertical.absolute) .to_physical(scale_factor), @@ -215,13 +226,17 @@ pub struct WinitPointerData { /// The data required by the sctk. sctk_data: PointerData, + + /// Viewport for fractional cursor. + viewport: Option, } impl WinitPointerData { - pub fn new(seat: WlSeat) -> Self { + pub fn new(seat: WlSeat, viewport: Option) -> Self { Self { inner: Mutex::new(WinitPointerDataInner::default()), sctk_data: PointerData::new(seat), + viewport, } } @@ -302,6 +317,18 @@ impl WinitPointerData { locked_pointer.set_cursor_position_hint(surface_x, surface_y); } } + + pub fn viewport(&self) -> Option<&WpViewport> { + self.viewport.as_ref() + } +} + +impl Drop for WinitPointerData { + fn drop(&mut self) { + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + } } impl PointerDataExt for WinitPointerData { diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs index 49d9363597..db724893c5 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -121,11 +121,15 @@ impl Dispatch for TextInputState { None => return, }; - // Clear preedit at the start of `Done`. - state.events_sink.push_window_event( - WindowEvent::Ime(Ime::Preedit(String::new(), None)), - window_id, - ); + // Clear preedit, unless all we'll be doing next is sending a new preedit. + if text_input_data.pending_commit.is_some() + || text_input_data.pending_preedit.is_none() + { + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(String::new(), None)), + window_id, + ); + } // Send `Commit`. if let Some(text) = text_input_data.pending_commit.take() { diff --git a/src/platform_impl/linux/wayland/seat/touch/mod.rs b/src/platform_impl/linux/wayland/seat/touch/mod.rs index a037ae9f37..124504feea 100644 --- a/src/platform_impl/linux/wayland/seat/touch/mod.rs +++ b/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -1,5 +1,7 @@ //! Touch handling. +use tracing::warn; + use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::protocol::wl_touch::WlTouch; @@ -31,11 +33,16 @@ impl TouchHandler for WinitState { None => return, }; - let location = LogicalPosition::::from(position); - - let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap(); + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::down without seat"); + return; + }, + }; // Update the state of the point. + let location = LogicalPosition::::from(position); seat_state.touch_map.insert(id, TouchPoint { surface, location }); self.events_sink.push_window_event( @@ -61,7 +68,13 @@ impl TouchHandler for WinitState { _: u32, id: i32, ) { - let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap(); + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::up without seat"); + return; + }, + }; // Remove the touch point. let touch_point = match seat_state.touch_map.remove(&id) { @@ -98,7 +111,13 @@ impl TouchHandler for WinitState { id: i32, position: (f64, f64), ) { - let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap(); + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::motion without seat"); + return; + }, + }; // Remove the touch point. let touch_point = match seat_state.touch_map.get_mut(&id) { @@ -129,7 +148,13 @@ impl TouchHandler for WinitState { } fn cancel(&mut self, _: &Connection, _: &QueueHandle, touch: &WlTouch) { - let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap(); + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::cancel without seat"); + return; + }, + }; for (id, touch_point) in seat_state.touch_map.drain() { let window_id = wayland::make_wid(&touch_point.surface); diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index 8c021bb9fd..13ef99c26a 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -339,12 +339,30 @@ impl CompositorHandler for WinitState { &mut self, _: &Connection, _: &QueueHandle, - _: &wayland_client::protocol::wl_surface::WlSurface, + _: &WlSurface, _: wayland_client::protocol::wl_output::Transform, ) { // TODO(kchibisov) we need to expose it somehow in winit. } + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + fn scale_factor_changed( &mut self, _: &Connection, diff --git a/src/platform_impl/linux/wayland/types/xdg_activation.rs b/src/platform_impl/linux/wayland/types/xdg_activation.rs index 8bd21d0abb..9efc75da34 100644 --- a/src/platform_impl/linux/wayland/types/xdg_activation.rs +++ b/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -80,7 +80,7 @@ impl Dispatch for XdgA state.events_sink.push_window_event( crate::event::WindowEvent::ActivationTokenDone { serial: *serial, - token: ActivationToken::_new(token), + token: ActivationToken::from_raw(token), }, *window_id, ); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 08e4504a83..6d29a5a5f0 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -1,5 +1,7 @@ //! The Wayland window. +use std::ffi::c_void; +use std::ptr::NonNull; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -164,11 +166,17 @@ impl Window { Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), } + // Apply resize increments. + if let Some(increments) = attributes.resize_increments { + let increments = increments.to_logical(window_state.scale_factor()); + window_state.set_resize_increments(Some(increments)); + } + // Activate the window when the token is passed. if let (Some(xdg_activation), Some(token)) = (xdg_activation.as_ref(), attributes.platform_specific.activation_token) { - xdg_activation.activate(token._token, &surface); + xdg_activation.activate(token.token, &surface); } // XXX Do initial commit. @@ -223,6 +231,10 @@ impl Window { window_events_sink, }) } + + pub(crate) fn xdg_toplevel(&self) -> Option> { + NonNull::new(self.window.xdg_toplevel().id().as_ptr().cast()) + } } impl Window { @@ -327,12 +339,19 @@ impl Window { #[inline] pub fn resize_increments(&self) -> Option> { - None + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + window_state + .resize_increments() + .map(|size| super::logical_to_physical_rounded(size, scale_factor)) } #[inline] - pub fn set_resize_increments(&self, _increments: Option) { - warn!("`set_resize_increments` is not implemented for Wayland"); + pub fn set_resize_increments(&self, increments: Option) { + let mut window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + let increments = increments.map(|size| size.to_logical(scale_factor)); + window_state.set_resize_increments(increments); } #[inline] diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 7685b85952..1ef7a0656c 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -31,7 +31,7 @@ use sctk::subcompositor::SubcompositorState; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; use crate::cursor::CustomCursor as RootCustomCursor; -use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size}; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size}; use crate::error::{ExternalError, NotSupportedError}; use crate::platform_impl::wayland::logical_to_physical_rounded; use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor}; @@ -127,6 +127,7 @@ pub struct WindowState { /// Min size. min_inner_size: LogicalSize, max_inner_size: Option>, + resize_increments: Option>, /// The size of the window when no states were applied to it. The primary use for it /// is to fallback to original window size, before it was maximized, if the compositor @@ -202,6 +203,7 @@ impl WindowState { last_configure: None, max_inner_size: None, min_inner_size: MIN_WINDOW_SIZE, + resize_increments: None, pointer_constraints, pointers: Default::default(), queue_handle: queue_handle.clone(), @@ -222,9 +224,9 @@ impl WindowState { } /// Apply closure on the given pointer. - fn apply_on_pointer, &WinitPointerData)>( + fn apply_on_pointer, &WinitPointerData)>( &self, - callback: F, + mut callback: F, ) { self.pointers.iter().filter_map(Weak::upgrade).for_each(|pointer| { let data = pointer.pointer().winit_data(); @@ -340,6 +342,42 @@ impl WindowState { .unwrap_or(new_size.height); } + // Apply size increments. + // + // We conditionally apply increments to avoid conflicts with the compositor's layout rules: + // 1. If the window is floating (constrain == true), we snap to increments to ensure the + // app's grid alignment. + // 2. If the user is interactively resizing (is_resizing), we snap the size to provide + // feedback. + // + // However, we MUST NOT snap if the compositor enforces a specific size (constrain == false, + // or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would + // shrink the window below the allocated area, creating visible gaps between valid + // windows or screen edges. + if (constrain || configure.is_resizing()) + && !configure.is_maximized() + && !configure.is_fullscreen() + && !configure.is_tiled() + { + if let Some(increments) = self.resize_increments { + // We use min size as a base size for the increments, similar to how X11 does it. + // + // This ensures that we can always reach the min size and the increments are + // calculated from it. + let (delta_width, delta_height) = ( + new_size.width.saturating_sub(self.min_inner_size.width), + new_size.height.saturating_sub(self.min_inner_size.height), + ); + + let width = + self.min_inner_size.width + (delta_width / increments.width) * increments.width; + let height = self.min_inner_size.height + + (delta_height / increments.height) * increments.height; + + new_size = (width, height).into(); + } + } + let new_state = configure.state; let old_state = self.last_configure.as_ref().map(|configure| configure.state); @@ -725,18 +763,39 @@ impl WindowState { self.selected_cursor = SelectedCursor::Custom(cursor); } + /// Set the resize increments of the window. + pub fn set_resize_increments(&mut self, increments: Option>) { + self.resize_increments = increments; + // NOTE: We don't update the window size here, because it will be done on the next resize + // or configure event. + } + + /// Get the resize increments of the window. + pub fn resize_increments(&self) -> Option> { + self.resize_increments + } + fn apply_custom_cursor(&self, cursor: &CustomCursor) { - self.apply_on_pointer(|pointer, _| { + self.apply_on_pointer(|pointer, data| { let surface = pointer.surface(); - let scale = surface.data::().unwrap().surface_data().scale_factor(); + let scale = if let Some(viewport) = data.viewport() { + let scale = self.scale_factor(); + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + viewport.set_destination(size.width, size.height); + scale + } else { + let scale = surface.data::().unwrap().surface_data().scale_factor(); + surface.set_buffer_scale(scale); + scale as f64 + }; - surface.set_buffer_scale(scale); surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); if surface.version() >= 4 { surface.damage_buffer(0, 0, cursor.w, cursor.h); } else { - surface.damage(0, 0, cursor.w / scale, cursor.h / scale); + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + surface.damage(0, 0, size.width, size.height); } surface.commit(); @@ -746,12 +805,9 @@ impl WindowState { .and_then(|data| data.pointer_data().latest_enter_serial()) .unwrap(); - pointer.pointer().set_cursor( - serial, - Some(surface), - cursor.hotspot_x / scale, - cursor.hotspot_y / scale, - ); + let hotspot = + PhysicalPosition::new(cursor.hotspot_x, cursor.hotspot_y).to_logical(scale); + pointer.pointer().set_cursor(serial, Some(surface), hotspot.x, hotspot.y); }); } @@ -827,34 +883,51 @@ impl WindowState { None => return Err(ExternalError::NotSupported(NotSupportedError::new())), }; - // Replace the current mode. - let old_mode = std::mem::replace(&mut self.cursor_grab_mode.current_grab_mode, mode); - - match old_mode { - CursorGrabMode::None => (), + let mut unset_old = false; + match self.cursor_grab_mode.current_grab_mode { + CursorGrabMode::None => unset_old = true, CursorGrabMode::Confined => self.apply_on_pointer(|_, data| { data.unconfine_pointer(); + unset_old = true; }), CursorGrabMode::Locked => { - self.apply_on_pointer(|_, data| data.unlock_pointer()); + self.apply_on_pointer(|_, data| { + data.unlock_pointer(); + unset_old = true; + }); }, } + // In case we haven't unset the old mode, it means that we don't have a cursor above + // the window, thus just wait for it to re-appear. + if !unset_old { + return Ok(()); + } + + let mut set_mode = false; let surface = self.window.wl_surface(); match mode { CursorGrabMode::Locked => self.apply_on_pointer(|pointer, data| { let pointer = pointer.pointer(); - data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle) + data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; }), CursorGrabMode::Confined => self.apply_on_pointer(|pointer, data| { let pointer = pointer.pointer(); - data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle) + data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; }), CursorGrabMode::None => { // Current lock/confine was already removed. + set_mode = true; }, } + // Replace the current grab mode after we've ensure that it got updated. + if set_mode { + self.cursor_grab_mode.current_grab_mode = mode; + } + Ok(()) } diff --git a/src/platform_impl/linux/x11/activation.rs b/src/platform_impl/linux/x11/activation.rs index a5e961bb9d..8f4aa7962d 100644 --- a/src/platform_impl/linux/x11/activation.rs +++ b/src/platform_impl/linux/x11/activation.rs @@ -165,14 +165,14 @@ 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(()) } } - write!(Writer { buffer }, "{}", display).unwrap(); + write!(Writer { buffer }, "{display}").unwrap(); } #[cfg(test)] diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 4b233ec692..79f5c114f0 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -66,7 +66,10 @@ pub struct EventProcessor { pub active_window: Option, /// Latest modifiers we've sent for the user to trigger change in event. pub modifiers: Cell, - pub xfiltered_modifiers: VecDeque, + // Track modifiers based on keycodes. NOTE: that serials generally don't work for tracking + // since they are not unique and could be duplicated in case of sequence of key events is + // delivered at near the same time. + pub xfiltered_modifiers: VecDeque, pub xmodmap: util::ModifierKeymap, pub is_composing: bool, } @@ -129,6 +132,7 @@ impl EventProcessor { /// Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, /// along with an extra copy of the KeyRelease events. This also prevents backspace and /// arrow keys from being detected twice. + #[must_use] fn filter_event(&mut self, xev: &mut XEvent) -> bool { let wt = Self::window_target(&self.target); unsafe { @@ -145,20 +149,38 @@ 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. + let filtered = 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); + + let filtered = forward_to_ime && self.filter_event(xev); + if filtered { 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 - // when the modifiers are consumed entirely or serials are altered. - // - // Both cases shouldn't happen in well behaving clients. + // when the modifiers are consumed entirely. if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN { self.xfiltered_modifiers.pop_back(); } - self.xfiltered_modifiers.push_front(xev.serial); + self.xfiltered_modifiers.push_front(xev.keycode as u8); } } + + filtered + } else { + self.filter_event(xev) + }; + + // Don't process event if it was filtered. + if filtered { return; } @@ -434,7 +456,7 @@ impl EventProcessor { let flags = xev.data.get_long(1); let version = flags >> 24; self.dnd.version = Some(version); - let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1; + let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; if !has_more_types { let type_list = vec![ xev.data.get_long(2) as xproto::Atom, @@ -929,7 +951,7 @@ impl EventProcessor { // itself are out of sync due to XkbState being delivered before XKeyEvent, since it's // being replayed by the XIM, thus we should replay ourselves. let replay = if let Some(position) = - self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.serial) + self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.keycode as u8) { // We don't have to replay modifiers pressed before the current event if some events // were not forwarded to us, since their state is irrelevant. diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs index c75724ec83..fa0fa5c6fc 100644 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ b/src/platform_impl/linux/x11/ime/callbacks.rs @@ -123,19 +123,15 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { let is_allowed = old_context.as_ref().map(|old_context| old_context.is_allowed()).unwrap_or_default(); - // We can't use the style from the old context here, since it may change on reload, so - // pick style from the new XIM based on the old state. - let style = if is_allowed { new_im.preedit_style } else { new_im.none_style }; - let new_context = { let result = unsafe { ImeContext::new( xconn, - new_im.im, - style, + &new_im, *window, spot, (*inner).event_sender.clone(), + is_allowed, ) }; if result.is_err() { diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs index 1cc8c355b9..2c6c075e56 100644 --- a/src/platform_impl/linux/x11/ime/context.rs +++ b/src/platform_impl/linux/x11/ime/context.rs @@ -5,10 +5,9 @@ use std::{mem, ptr}; use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; -use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle}; -use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; - use super::{ffi, util, XConnection, XError}; +use crate::platform_impl::platform::x11::ime::input_method::{InputMethod, Style, XIMStyle}; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; /// IME creation error. #[derive(Debug)] @@ -82,7 +81,7 @@ extern "C" fn preedit_draw_callback( call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize; if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() { tracing::warn!( - "invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}", + "invalid chg range: buffer length={}, but chg_first={} chg_length={}", client_data.text.len(), call_data.chg_first, call_data.chg_length @@ -158,7 +157,9 @@ struct PreeditCallbacks { impl PreeditCallbacks { pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { let start_callback = create_xim_callback(client_data, unsafe { - mem::transmute(preedit_start_callback as usize) + mem::transmute::( + preedit_start_callback as *const () as usize, + ) }); let done_callback = create_xim_callback(client_data, preedit_done_callback); let caret_callback = create_xim_callback(client_data, preedit_caret_callback); @@ -182,7 +183,7 @@ struct ImeContextClientData { pub struct ImeContext { pub(crate) ic: ffi::XIC, pub(crate) ic_spot: ffi::XPoint, - pub(crate) style: Style, + pub(crate) allowed: bool, // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free // from there we keep the pointer to automatically deallocate it. _client_data: Box, @@ -191,11 +192,11 @@ pub struct ImeContext { impl ImeContext { pub(crate) unsafe fn new( xconn: &Arc, - im: ffi::XIM, - style: Style, + im: &InputMethod, window: ffi::Window, ic_spot: Option, event_sender: ImeEventSender, + allowed: bool, ) -> Result { let client_data = Box::into_raw(Box::new(ImeContextClientData { window, @@ -204,20 +205,24 @@ impl ImeContext { cursor_pos: 0, })); + let style = if allowed { im.preedit_style } else { im.none_style }; + let ic = match style as _ { Style::Preedit(style) => unsafe { ImeContext::create_preedit_ic( xconn, - im, + im.im, style, window, client_data as ffi::XPointer, ) }, Style::Nothing(style) => unsafe { - ImeContext::create_nothing_ic(xconn, im, style, window) + ImeContext::create_nothing_ic(xconn, im.im, style, window) + }, + Style::None(style) => unsafe { + ImeContext::create_none_ic(xconn, im.im, style, window) }, - Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) }, } .ok_or(ImeContextCreationError::Null)?; @@ -226,7 +231,7 @@ impl ImeContext { let mut context = ImeContext { ic, ic_spot: ffi::XPoint { x: 0, y: 0 }, - style, + allowed, _client_data: unsafe { Box::from_raw(client_data) }, }; @@ -333,7 +338,7 @@ impl ImeContext { } pub fn is_allowed(&self) -> bool { - !matches!(self.style, Style::None(_)) + self.allowed } // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks diff --git a/src/platform_impl/linux/x11/ime/inner.rs b/src/platform_impl/linux/x11/ime/inner.rs index 4d4f7cb473..da1ccf4016 100644 --- a/src/platform_impl/linux/x11/ime/inner.rs +++ b/src/platform_impl/linux/x11/ime/inner.rs @@ -51,10 +51,9 @@ impl ImeInner { } pub unsafe fn close_im_if_necessary(&self) -> Result { - if !self.is_destroyed && self.im.is_some() { - unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true) - } else { - Ok(false) + match self.im.as_ref() { + Some(im) if !self.is_destroyed => unsafe { close_im(&self.xconn, im.im).map(|_| true) }, + _ => Ok(false), } } diff --git a/src/platform_impl/linux/x11/ime/input_method.rs b/src/platform_impl/linux/x11/ime/input_method.rs index b9d3ca7101..e1f2fbd2f6 100644 --- a/src/platform_impl/linux/x11/ime/input_method.rs +++ b/src/platform_impl/linux/x11/ime/input_method.rs @@ -176,7 +176,7 @@ unsafe fn get_xim_servers(xconn: &Arc) -> Result, GetXi ) .map_err(GetXimServersError::GetPropertyError)? .into_iter() - .map(ffi::Atom::from) + .map(|atom| atom as _) .collect::>(); let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index 606e00d9f3..0a419c8d50 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -10,15 +10,13 @@ use std::sync::Arc; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tracing::debug; - -use super::{ffi, util, XConnection, XError}; use self::callbacks::*; use self::context::ImeContext; pub use self::context::ImeContextCreationError; use self::inner::{close_im, ImeInner}; -use self::input_method::{PotentialInputMethods, Style}; +use self::input_method::PotentialInputMethods; +use super::{ffi, util, XConnection, XError}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -114,39 +112,26 @@ impl Ime { pub fn create_context( &mut self, window: ffi::Window, - with_preedit: bool, + with_ime: bool, ) -> Result { let context = if self.is_destroyed() { // Create empty entry in map, so that when IME is rebuilt, this window has a context. None } else { let im = self.inner.im.as_ref().unwrap(); - let style = if with_preedit { im.preedit_style } else { im.none_style }; let context = unsafe { ImeContext::new( &self.inner.xconn, - im.im, - style, + im, window, None, self.inner.event_sender.clone(), + with_ime, )? }; - // Check the state on the context, since it could fail to enable or disable preedit. - let event = if matches!(style, Style::None(_)) { - if with_preedit { - debug!("failed to create IME context with preedit support.") - } - ImeEvent::Disabled - } else { - if !with_preedit { - debug!("failed to create IME context without preedit support.") - } - ImeEvent::Enabled - }; - + let event = if context.is_allowed() { ImeEvent::Enabled } else { ImeEvent::Disabled }; self.inner.event_sender.send((window, event)).expect("Failed to send enabled event"); Some(context) @@ -226,6 +211,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 523f4f03ca..fce1f4a26c 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -1,5 +1,3 @@ -#![cfg(x11_platform)] - use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet, VecDeque}; use std::ffi::CStr; @@ -499,6 +497,7 @@ impl EventLoop { // If we don't have any pending `_receiver` if !self.has_pending() && !matches!(&cause, StartCause::ResumeTimeReached { .. } | StartCause::Poll) + && timeout.is_none() { return; } @@ -533,7 +532,7 @@ impl EventLoop { window_id: crate::window::WindowId(window_id), event: WindowEvent::ActivationTokenDone { serial, - token: crate::window::ActivationToken::_new(token), + token: crate::window::ActivationToken::from_raw(token), }, }; callback(event, &self.event_processor.target) @@ -765,14 +764,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 { @@ -785,7 +784,7 @@ pub struct DeviceId(xinput::DeviceId); impl DeviceId { #[allow(unused)] - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { DeviceId(0) } } @@ -863,24 +862,24 @@ pub enum X11Error { impl fmt::Display for X11Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - X11Error::Xlib(e) => write!(f, "Xlib error: {}", e), - X11Error::Connect(e) => write!(f, "X11 connection error: {}", e), - X11Error::Connection(e) => write!(f, "X11 connection error: {}", e), - X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e), - X11Error::GetProperty(e) => write!(f, "Failed to get X property {}", e), - X11Error::X11(e) => write!(f, "X11 error: {:?}", e), - X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s), + X11Error::Xlib(e) => write!(f, "Xlib error: {e}"), + X11Error::Connect(e) => write!(f, "X11 connection error: {e}"), + X11Error::Connection(e) => write!(f, "X11 connection error: {e}"), + X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {e}"), + X11Error::GetProperty(e) => write!(f, "Failed to get X property {e}"), + X11Error::X11(e) => write!(f, "X11 error: {e:?}"), + X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {s}"), X11Error::InvalidActivationToken(s) => write!( f, "Invalid activation token: {}", std::str::from_utf8(s).unwrap_or("") ), - X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {}", s), + X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {s}"), X11Error::NoSuchVisual(visualid) => { - write!(f, "Could not find a matching X11 visual for ID `{:x}`", visualid) + write!(f, "Could not find a matching X11 visual for ID `{visualid:x}`") }, X11Error::XsettingsParse(err) => { - write!(f, "Failed to parse xsettings: {:?}", err) + write!(f, "Failed to parse xsettings: {err:?}") }, } } @@ -971,7 +970,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/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index 5c30293c31..55e6c2ddd8 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -51,7 +51,7 @@ where } impl XConnection { - // This is impoartant, so pay attention! + // This is important, so pay attention! // Xlib has an output buffer, and tries to hide the async nature of X from you. // This buffer contains the requests you make, and is flushed under various circumstances: // 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed" diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 6097bc517c..d10c97e5d7 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -79,7 +79,7 @@ impl XConnection { .iter() // XRROutputInfo contains an array of mode ids that correspond to // modes in the array in XRRScreenResources - .filter(|x| output_modes.iter().any(|id| x.id == *id)) + .filter(|x| output_modes.contains(&x.id)) .map(|mode| { VideoModeHandle { size: (mode.width.into(), mode.height.into()), diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index fcd21c1e8e..1acca0b1d0 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -8,9 +8,8 @@ use std::{cmp, env}; use tracing::{debug, error, info, warn}; use x11rb::connection::Connection; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; -use x11rb::protocol::shape::SK; -use x11rb::protocol::xfixes::{ConnectionExt, RegionWrapper}; -use x11rb::protocol::xproto::{self, ConnectionExt as _, Rectangle}; +use x11rb::protocol::shape::{ConnectionExt as ShapeExt, SK, SO}; +use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; use x11rb::protocol::{randr, xinput}; use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; @@ -144,12 +143,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 +220,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 +489,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 +526,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); @@ -553,7 +558,7 @@ impl UnownedWindow { // Remove the startup notification if we have one. if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() { - leap!(xconn.remove_activation_token(xwindow, &startup._token)); + leap!(xconn.remove_activation_token(xwindow, &startup.token)); } // We never want to give the user a broken window, since by then, it's too late to handle. @@ -970,8 +975,8 @@ impl UnownedWindow { let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; match state { Ok(atoms) => { - let horz_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == horz_atom); - let vert_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == vert_atom); + let horz_maximized = atoms.contains(&horz_atom); + let vert_maximized = atoms.contains(&vert_atom); horz_maximized && vert_maximized }, _ => false, @@ -1498,6 +1503,11 @@ impl UnownedWindow { #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + // We don't support the locked cursor yet, so ignore it early on. + if mode == CursorGrabMode::Locked { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); if mode == *grabbed_lock { return Ok(()); @@ -1509,40 +1519,40 @@ impl UnownedWindow { .xcb_connection() .ungrab_pointer(x11rb::CURRENT_TIME) .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`"); + *grabbed_lock = CursorGrabMode::None; let result = match mode { CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) }), CursorGrabMode::Confined => { - let result = { - self.xconn - .xcb_connection() - .grab_pointer( - true as _, - self.xwindow, - xproto::EventMask::BUTTON_PRESS - | xproto::EventMask::BUTTON_RELEASE - | xproto::EventMask::ENTER_WINDOW - | xproto::EventMask::LEAVE_WINDOW - | xproto::EventMask::POINTER_MOTION - | xproto::EventMask::POINTER_MOTION_HINT - | xproto::EventMask::BUTTON1_MOTION - | xproto::EventMask::BUTTON2_MOTION - | xproto::EventMask::BUTTON3_MOTION - | xproto::EventMask::BUTTON4_MOTION - | xproto::EventMask::BUTTON5_MOTION - | xproto::EventMask::KEYMAP_STATE, - xproto::GrabMode::ASYNC, - xproto::GrabMode::ASYNC, - self.xwindow, - 0u32, - x11rb::CURRENT_TIME, - ) - .expect("Failed to call `grab_pointer`") - .reply() - .expect("Failed to receive reply from `grab_pointer`") - }; + let result = self + .xconn + .xcb_connection() + .grab_pointer( + true as _, + self.xwindow, + xproto::EventMask::BUTTON_PRESS + | xproto::EventMask::BUTTON_RELEASE + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::POINTER_MOTION + | xproto::EventMask::POINTER_MOTION_HINT + | xproto::EventMask::BUTTON1_MOTION + | xproto::EventMask::BUTTON2_MOTION + | xproto::EventMask::BUTTON3_MOTION + | xproto::EventMask::BUTTON4_MOTION + | xproto::EventMask::BUTTON5_MOTION + | xproto::EventMask::KEYMAP_STATE, + xproto::GrabMode::ASYNC, + xproto::GrabMode::ASYNC, + self.xwindow, + 0u32, + x11rb::CURRENT_TIME, + ) + .expect("Failed to call `grab_pointer`") + .reply() + .expect("Failed to receive reply from `grab_pointer`"); match result.status { xproto::GrabStatus::SUCCESS => Ok(()), @@ -1562,9 +1572,7 @@ impl UnownedWindow { } .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) }, - CursorGrabMode::Locked => { - return Err(ExternalError::NotSupported(NotSupportedError::new())); - }, + CursorGrabMode::Locked => return Ok(()), }; if result.is_ok() { @@ -1625,6 +1633,15 @@ impl UnownedWindow { #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + // Implement cursor hittest for X11 by either setting an empty or full window input shape. + + // In X11, every window has two "shapes": + // * Bounding shape: defines the visible outline of the window. + // * Input shape: defines the region of the window that receives pointer/keyboard events. + // If the input shape is the full window rectangle, the window behaves normally. + // If the input shape is empty, the window is completely click‑through. + // Here, we implement hit test by mapping `hittest = true` to "restore a full input shape" + // and `hittest = false` to "clear the input shape" (empty list of rectangles). let mut rectangles: Vec = Vec::new(); if hittest { let size = self.inner_size(); @@ -1635,11 +1652,18 @@ impl UnownedWindow { height: size.height as u16, }) } - let region = RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles) - .map_err(|_e| ExternalError::Ignored)?; + self.xconn .xcb_connection() - .xfixes_set_window_shape_region(self.xwindow, SK::INPUT, 0, 0, region.region()) + .shape_rectangles( + SO::SET, + SK::INPUT, + ClipOrdering::UNSORTED, + self.xwindow, + 0, + 0, + &rectangles, + ) .map_err(|_e| ExternalError::Ignored)?; self.shared_state_lock().cursor_hittest = Some(hittest); Ok(()) diff --git a/src/platform_impl/linux/x11/xdisplay.rs b/src/platform_impl/linux/x11/xdisplay.rs index 0a56f290a4..79da0d1faa 100644 --- a/src/platform_impl/linux/x11/xdisplay.rs +++ b/src/platform_impl/linux/x11/xdisplay.rs @@ -145,7 +145,7 @@ impl XConnection { fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option { // Fetch the _XSETTINGS_S[screen number] atom. let xsettings_screen = xcb - .intern_atom(false, format!("_XSETTINGS_S{}", default_screen).as_bytes()) + .intern_atom(false, format!("_XSETTINGS_S{default_screen}").as_bytes()) .ok()? .reply() .ok()? @@ -234,9 +234,7 @@ impl XConnection { // Store the timestamp in the slot if it's greater than the last one. let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); loop { - let wrapping_sub = |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32); - - if wrapping_sub(timestamp, last_timestamp) <= 0 { + if (timestamp as i32).wrapping_sub(last_timestamp as i32) <= 0 { break; } diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index 38372f4297..4fb95dbd22 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -1,49 +1,104 @@ #![allow(clippy::unnecessary_cast)] +#![allow(unknown_lints)] // New lint below +#![allow(static_mut_refs)] // Uses `MainThreadBound` in new version. -use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass}; -use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder}; -use objc2_foundation::{MainThreadMarker, NSObject}; +use std::cell::Cell; +use std::mem; + +use objc2::runtime::{Imp, Sel}; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType}; +use objc2_foundation::MainThreadMarker; use super::app_state::ApplicationDelegate; use crate::event::{DeviceEvent, ElementState}; -declare_class!( - pub(super) struct WinitApplication; +type SendEvent = extern "C" fn(&NSApplication, Sel, &NSEvent); - unsafe impl ClassType for WinitApplication { - #[inherits(NSResponder, NSObject)] - type Super = NSApplication; - type Mutability = mutability::MainThreadOnly; - const NAME: &'static str = "WinitApplication"; - } +// NOTE: Only used on the main thread. Ideally, we'd use `MainThreadBound`, but that isn't +// constructible from `const` with this `objc2` version. +static mut ORIGINAL: Cell> = Cell::new(None); - impl DeclaredClass for WinitApplication {} - - unsafe impl WinitApplication { - // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. - // Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) - // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) - #[method(sendEvent:)] - fn send_event(&self, event: &NSEvent) { - // For posterity, there are some undocumented event types - // (https://github.com/servo/cocoa-rs/issues/155) - // but that doesn't really matter here. - let event_type = unsafe { event.r#type() }; - let modifier_flags = unsafe { event.modifierFlags() }; - if event_type == NSEventType::KeyUp - && modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand) - { - if let Some(key_window) = self.keyWindow() { - key_window.sendEvent(event); - } - } else { - let delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); - maybe_dispatch_device_event(&delegate, event); - unsafe { msg_send![super(self), sendEvent: event] } - } +extern "C" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) { + let mtm = MainThreadMarker::from(app); + + // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. + // Overriding `sendEvent:` fixes that. (https://stackoverflow.com/a/15294196) + // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) + // + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = unsafe { event.r#type() }; + let modifier_flags = unsafe { event.modifierFlags() }; + if event_type == NSEventType::KeyUp + && modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand) + { + if let Some(key_window) = app.keyWindow() { + key_window.sendEvent(event); } + return; + } + + // Events are generally scoped to the window level, so the best way + // to get device events is to listen for them on NSApplication. + let delegate = ApplicationDelegate::get(mtm); + maybe_dispatch_device_event(&delegate, event); + + let _ = mtm; + let original = unsafe { ORIGINAL.get().expect("no existing sendEvent: handler set") }; + original(app, sel, event) +} + +/// Override the [`sendEvent:`][NSApplication::sendEvent] method on the given application class. +/// +/// The previous implementation created a subclass of [`NSApplication`], however we would like to +/// give the user full control over their `NSApplication`, so we override the method here using +/// method swizzling instead. +/// +/// This _should_ also allow two versions of Winit to exist in the same application. +/// +/// See the following links for more info on method swizzling: +/// - +/// - +/// - +/// +/// NOTE: This function assumes that the passed in application object is the one returned from +/// [`NSApplication::sharedApplication`], i.e. the one and only global shared application object. +/// For testing though, we allow it to be a different object. +pub(crate) fn override_send_event(global_app: &NSApplication) { + let mtm = MainThreadMarker::from(global_app); + let class = global_app.class(); + + let method = + class.instance_method(sel!(sendEvent:)).expect("NSApplication must have sendEvent: method"); + + // SAFETY: Converting our `sendEvent:` implementation to an IMP. + let overridden = unsafe { mem::transmute::(send_event) }; + + // If we've already overridden the method, don't do anything. + // FIXME(madsmtm): Use `std::ptr::fn_addr_eq` (Rust 1.85) once available in MSRV. + #[allow(unknown_lints, unpredictable_function_pointer_comparisons)] + if overridden == method.implementation() { + return; } -); + + // SAFETY: Our implementation has: + // 1. The same signature as `sendEvent:`. + // 2. Does not impose extra safety requirements on callers. + let original = unsafe { method.set_implementation(overridden) }; + + // SAFETY: This is the actual signature of `sendEvent:`. + let original = unsafe { mem::transmute::(original) }; + + // NOTE: If NSApplication was safe to use from multiple threads, then this would potentially be + // a (checked) race-condition, since one could call `sendEvent:` before the original had been + // stored here. + // + // It is only usable from the main thread, however, so we're good! + let _ = mtm; + unsafe { ORIGINAL.set(Some(original)) }; +} fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) { let event_type = unsafe { event.r#type() }; @@ -85,3 +140,56 @@ fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) _ => (), } } + +#[cfg(test)] +mod tests { + use objc2::rc::Retained; + use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; + + use super::*; + + #[test] + fn test_override() { + // FIXME(madsmtm): Ensure this always runs (maybe use cargo-nextest or `--test-threads=1`?) + let Some(mtm) = MainThreadMarker::new() else { return }; + + // Create a new application, without making it the shared application. + let app = unsafe { NSApplication::new(mtm) }; + override_send_event(&app); + // Test calling twice works. + override_send_event(&app); + + // FIXME(madsmtm): Can't test this yet, need some way to mock AppState. + // unsafe { + // let event = super::super::event::dummy_event().unwrap(); + // app.sendEvent(&event) + // } + } + + #[test] + fn test_custom_class() { + let Some(_mtm) = MainThreadMarker::new() else { return }; + + declare_class!( + struct TestApplication; + + unsafe impl ClassType for TestApplication { + type Super = NSApplication; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "TestApplication"; + } + + impl DeclaredClass for TestApplication {} + + unsafe impl TestApplication { + #[method(sendEvent:)] + fn send_event(&self, _event: &NSEvent) { + todo!() + } + } + ); + + let app: Retained = unsafe { msg_send_id![TestApplication::class(), new] }; + override_send_event(&app); + } +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index ee149384b7..dd2ccd1168 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -5,11 +5,13 @@ 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; -use super::event_loop::{stop_app_immediately, ActiveEventLoop, PanicInfo}; +use super::event_loop::{notify_windows_of_exit, stop_app_immediately, ActiveEventLoop, PanicInfo}; use super::observer::{EventLoopWaker, RunLoop}; use super::{menu, WindowId, DEVICE_ID}; use crate::event::{DeviceEvent, Event, StartCause, WindowEvent}; @@ -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)] @@ -146,7 +165,9 @@ impl ApplicationDelegate { fn will_terminate(&self, _notification: &NSNotification) { trace_scope!("applicationWillTerminate:"); - // TODO: Notify every window that it will be destroyed, like done in iOS? + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + notify_windows_of_exit(&app); self.internal_exit(); } @@ -373,6 +394,7 @@ impl ApplicationDelegate { if self.exiting() { let app = NSApplication::sharedApplication(mtm); stop_app_immediately(&app); + notify_windows_of_exit(&app); } if self.ivars().stop_before_wait.get() { 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..9b7f35b02f 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); @@ -138,7 +128,7 @@ pub(crate) fn create_key_event( let logical_key = match text_with_all_modifiers.as_ref() { // Only checking for ctrl and cmd here, not checking for alt because we DO want to - // include its effect in the key. For example if -on the Germay layout- one + // include its effect in the key. For example if -on the German layout- one // presses alt+8, the logical key should be "{" // Also not checking if this is a release event because then this issue would // still affect the key release. diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 0fcd21363c..5c093d5a1d 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -16,11 +16,11 @@ use core_foundation::runloop::{ }; use objc2::rc::{autoreleasepool, Retained}; use objc2::runtime::ProtocolObject; -use objc2::{msg_send_id, ClassType}; +use objc2::sel; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow}; use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; -use super::app::WinitApplication; +use super::app::override_send_event; use super::app_state::{ApplicationDelegate, HandlePendingUserEvents}; use super::event::dummy_event; use super::monitor::{self, MonitorHandle}; @@ -33,7 +33,7 @@ use crate::event_loop::{ use crate::platform::macos::ActivationPolicy; use crate::platform::pump_events::PumpStatus; use crate::platform_impl::platform::cursor::CustomCursor; -use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, Theme}; #[derive(Default)] pub struct PanicInfo { @@ -106,6 +106,17 @@ impl ActiveEventLoop { rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) } + #[inline] + pub fn system_theme(&self) -> Option { + let app = NSApplication::sharedApplication(self.mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_display_handle_rwh_06( @@ -191,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 } } } @@ -213,20 +220,14 @@ impl EventLoop { let mtm = MainThreadMarker::new() .expect("on macOS, `EventLoop` must be created on the main thread!"); - let app: Retained = - unsafe { msg_send_id![WinitApplication::class(), sharedApplication] }; - - if !app.is_kind_of::() { - panic!( - "`winit` requires control over the principal class. You must create the event \ - loop before other parts of your application initialize NSApplication" - ); - } + // Initialize the application (if it has not already been). + let app = NSApplication::sharedApplication(mtm); 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, @@ -239,6 +240,9 @@ impl EventLoop { app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); }); + // Override `sendEvent:` on the application to forward to our application state. + override_send_event(&app); + let panic_info: Rc = Default::default(); setup_control_flow_observers(mtm, Rc::downgrade(&panic_info)); @@ -413,6 +417,22 @@ pub(super) fn stop_app_immediately(app: &NSApplication) { }); } +/// Tell all windows to close. +/// +/// This will synchronously trigger `WindowEvent::Destroyed` within +/// `windowWillClose:`, giving the application one last chance to handle +/// those events. It doesn't matter if the user also ends up closing the +/// windows in `Window`'s `Drop` impl, once a window has been closed once, it +/// stays closed. +/// +/// This ensures that no windows linger on after the event loop has exited, +/// see . +pub(super) fn notify_windows_of_exit(app: &NSApplication) { + for window in app.windows() { + window.close(); + } +} + /// Catches panics that happen inside `f` and when a panic /// happens, stops the `sharedApplication` #[inline] @@ -482,8 +502,7 @@ impl EventLoopProxy { cancel: None, perform: event_loop_proxy_handler, }; - let source = - CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::max_value() - 1, &mut context); + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); CFRunLoopWakeUp(rl); diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 92d4874e4b..1daee071e5 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -68,6 +68,8 @@ pub type CGDisplayModeRef = *mut c_void; #[link(name = "ApplicationServices", kind = "framework")] extern "C" { pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + + pub fn CGDisplayGetDisplayIDFromUUID(uuid: CFUUIDRef) -> CGDirectDisplayID; } #[link(name = "CoreGraphics", kind = "framework")] diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 2c3d3e3032..1b427a87d3 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -37,7 +37,7 @@ pub(crate) use crate::platform_impl::Fullscreen; pub struct DeviceId; impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { DeviceId } } diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index e78d84f0be..44e6316b66 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -6,6 +6,7 @@ use std::fmt; use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex}; use core_foundation::base::{CFRelease, TCFType}; use core_foundation::string::CFString; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUID}; use core_graphics::display::{ CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode, }; @@ -13,6 +14,7 @@ use objc2::rc::Retained; use objc2::runtime::AnyObject; use objc2_app_kit::NSScreen; use objc2_foundation::{ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect}; +use tracing::warn; use super::ffi; use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; @@ -97,18 +99,71 @@ impl VideoModeHandle { } } +/// `CGDirectDisplayID` is documented as: +/// > a framebuffer, a color correction (gamma) table, and possibly an attached monitor. +/// +/// That is, it doesn't actually represent the monitor itself. Instead, we use the UUID of the +/// monitor, as retrieved from `CGDisplayCreateUUIDFromDisplayID` (this makes the monitor ID stable, +/// even across reboots and video mode changes). +/// +/// NOTE: I'd be perfectly valid to store `[u8; 16]` in here instead, we only store `CFUUID` to +/// avoid having to re-create it when we want to fetch the display ID. #[derive(Clone)] -pub struct MonitorHandle(CGDirectDisplayID); +pub struct MonitorHandle(CFUUID); + +// SAFETY: CFUUID is immutable. +// FIXME(madsmtm): Upstream this into `objc2-core-foundation`. +unsafe impl Send for MonitorHandle {} +unsafe impl Sync for MonitorHandle {} + +type MonitorUuid = [u8; 16]; + +impl MonitorHandle { + /// Internal comparisons of [`MonitorHandle`]s are done first requesting a UUID for the handle. + fn uuid(&self) -> MonitorUuid { + let uuid = unsafe { CFUUIDGetUUIDBytes(self.0.as_concrete_TypeRef()) }; + MonitorUuid::from([ + uuid.byte0, + uuid.byte1, + uuid.byte2, + uuid.byte3, + uuid.byte4, + uuid.byte5, + uuid.byte6, + uuid.byte7, + uuid.byte8, + uuid.byte9, + uuid.byte10, + uuid.byte11, + uuid.byte12, + uuid.byte13, + uuid.byte14, + uuid.byte15, + ]) + } + + fn display_id(&self) -> CGDirectDisplayID { + unsafe { ffi::CGDisplayGetDisplayIDFromUUID(self.0.as_concrete_TypeRef()) } + } + + #[track_caller] + pub(crate) fn new(display_id: CGDirectDisplayID) -> Option { + // kCGNullDirectDisplay + if display_id == 0 { + // `CGDisplayCreateUUIDFromDisplayID` checks kCGNullDirectDisplay internally. + warn!("constructing monitor from invalid display ID 0; falling back to main monitor"); + } + let ptr = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(display_id) }; + if ptr.is_null() { + return None; + } + Some(Self(unsafe { CFUUID::wrap_under_create_rule(ptr) })) + } +} -// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that -// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an -// unique identifier that persists even across system reboots impl PartialEq for MonitorHandle { fn eq(&self, other: &Self) -> bool { - unsafe { - ffi::CGDisplayCreateUUIDFromDisplayID(self.0) - == ffi::CGDisplayCreateUUIDFromDisplayID(other.0) - } + self.uuid() == other.uuid() } } @@ -122,18 +177,13 @@ impl PartialOrd for MonitorHandle { impl Ord for MonitorHandle { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - unsafe { - ffi::CGDisplayCreateUUIDFromDisplayID(self.0) - .cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0)) - } + self.uuid().cmp(&other.uuid()) } } impl std::hash::Hash for MonitorHandle { fn hash(&self, state: &mut H) { - unsafe { - ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state); - } + self.uuid().hash(state); } } @@ -141,7 +191,8 @@ pub fn available_monitors() -> VecDeque { if let Ok(displays) = CGDisplay::active_displays() { let mut monitors = VecDeque::with_capacity(displays.len()); for display in displays { - monitors.push_back(MonitorHandle(display)); + // Display ID just fetched from `CGGetActiveDisplayList`, should be fine to unwrap. + monitors.push_back(MonitorHandle::new(display).expect("invalid display ID")); } monitors } else { @@ -150,7 +201,8 @@ pub fn available_monitors() -> VecDeque { } pub fn primary_monitor() -> MonitorHandle { - MonitorHandle(CGDisplay::main().id) + // Display ID just fetched from `CGMainDisplayID`, should be fine to unwrap. + MonitorHandle::new(CGDisplay::main().id).expect("invalid display ID") } impl fmt::Debug for MonitorHandle { @@ -167,26 +219,20 @@ impl fmt::Debug for MonitorHandle { } impl MonitorHandle { - pub fn new(id: CGDirectDisplayID) -> Self { - MonitorHandle(id) - } - // TODO: Be smarter about this: // pub fn name(&self) -> Option { - let MonitorHandle(display_id) = *self; - let screen_num = CGDisplay::new(display_id).model_number(); + let screen_num = CGDisplay::new(self.display_id()).model_number(); Some(format!("Monitor #{screen_num}")) } #[inline] pub fn native_identifier(&self) -> u32 { - self.0 + self.display_id() } pub fn size(&self) -> PhysicalSize { - let MonitorHandle(display_id) = *self; - let display = CGDisplay::new(display_id); + let display = CGDisplay::new(self.display_id()); let height = display.pixels_high(); let width = display.pixels_wide(); PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor()) @@ -213,14 +259,15 @@ impl MonitorHandle { pub fn refresh_rate_millihertz(&self) -> Option { unsafe { - let current_display_mode = NativeDisplayMode(CGDisplayCopyDisplayMode(self.0) as _); + let current_display_mode = + NativeDisplayMode(CGDisplayCopyDisplayMode(self.display_id()) as _); let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0); if refresh_rate > 0.0 { return Some((refresh_rate * 1000.0).round() as u32); } let mut display_link = std::ptr::null_mut(); - if ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link) + if ffi::CVDisplayLinkCreateWithCGDisplay(self.display_id(), &mut display_link) != ffi::kCVReturnSuccess { return None; @@ -243,27 +290,34 @@ impl MonitorHandle { unsafe { let modes = { - let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null()); - assert!(!array.is_null(), "failed to get list of display modes"); - let array_count = CFArrayGetCount(array); - let modes: Vec<_> = (0..array_count) - .map(move |i| { - let mode = CFArrayGetValueAtIndex(array, i) as *mut _; - ffi::CGDisplayModeRetain(mode); - mode - }) - .collect(); - CFRelease(array as *const _); - modes + let array = ffi::CGDisplayCopyAllDisplayModes(self.display_id(), std::ptr::null()); + if array.is_null() { + // Occasionally, certain CalDigit Thunderbolt Hubs report a spurious monitor + // during sleep/wake/cycling monitors. It tends to have null + // or 1 video mode only. See . + warn!(monitor = ?self, "failed to get a list of display modes"); + Vec::new() + } else { + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + } }; modes.into_iter().map(move |mode| { - let cg_refresh_rate_hertz = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; + let cg_refresh_rate_hertz = ffi::CGDisplayModeGetRefreshRate(mode); // CGDisplayModeGetRefreshRate returns 0.0 for any display that // isn't a CRT - let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0 { - (cg_refresh_rate_hertz * 1000) as u32 + let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0.0 { + (cg_refresh_rate_hertz * 1000.0).round() as u32 } else { refresh_rate_millihertz }; @@ -296,13 +350,17 @@ impl MonitorHandle { } pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option> { - let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) }; + let uuid = self.uuid(); NSScreen::screens(mtm).into_iter().find(|screen| { let other_native_id = get_display_id(screen); - let other_uuid = unsafe { - ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID) - }; - uuid == other_uuid + if let Some(other) = MonitorHandle::new(other_native_id) { + uuid == other.uuid() + } else { + // Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(other_native_id, "comparing against screen with invalid display ID"); + false + } }) } } diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index 1f960d0a6b..8339803086 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -214,13 +214,13 @@ pub fn setup_control_flow_observers(mtm: MainThreadMarker, panic_info: Weak Self { + pub const fn dummy() -> Self { Self(0) } } diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index 425d6068df..e293505ebe 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -1,6 +1,8 @@ #![allow(clippy::unnecessary_cast)] use std::cell::{Cell, RefCell}; use std::collections::VecDeque; +use std::ffi::c_void; +use std::ptr; use std::sync::{Arc, Mutex}; use core_graphics::display::{CGDisplay, CGPoint}; @@ -9,18 +11,20 @@ use objc2::rc::{autoreleasepool, Retained}; use objc2::runtime::{AnyObject, ProtocolObject}; use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; use objc2_app_kit::{ - NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication, - NSApplicationPresentationOptions, NSBackingStoreType, NSDraggingDestination, - NSFilenamesPboardType, NSPasteboard, NSRequestUserAttentionType, NSScreen, NSView, - NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel, - NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, - NSWindowTabbingMode, NSWindowTitleVisibility, + NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, + NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, + NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard, + NSRequestUserAttentionType, NSScreen, NSView, NSWindowButton, NSWindowDelegate, + NSWindowFullScreenButton, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, }; use objc2_foundation::{ - ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDistributedNotificationCenter, - NSObject, NSObjectNSDelayedPerforming, NSObjectNSThreadPerformAdditions, NSObjectProtocol, - NSPoint, NSRect, NSSize, NSString, + ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSKeyValueChangeKey, + NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSObject, + NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, + NSRect, NSSize, NSString, }; +use tracing::{trace, warn}; use super::app_state::ApplicationDelegate; use super::cursor::cursor_from_icon; @@ -51,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 { @@ -68,6 +73,7 @@ impl Default for PlatformSpecificWindowAttributes { accepts_first_mouse: true, tabbing_identifier: None, option_as_alt: Default::default(), + borderless_game: false, } } } @@ -79,12 +85,10 @@ pub(crate) struct State { window: Retained, - current_theme: Cell>, - // 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, @@ -118,6 +122,7 @@ pub(crate) struct State { standard_frame: Cell>, is_simple_fullscreen: Cell, saved_style: Cell>, + is_borderless_game: Cell, } declare_class!( @@ -368,7 +373,10 @@ declare_class!( use std::path::PathBuf; let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; - let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap(); + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; let filenames: Retained> = unsafe { Retained::cast(filenames) }; filenames.into_iter().for_each(|file| { @@ -394,7 +402,10 @@ declare_class!( use std::path::PathBuf; let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; - let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap(); + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; let filenames: Retained> = unsafe { Retained::cast(filenames) }; filenames.into_iter().for_each(|file| { @@ -419,32 +430,66 @@ declare_class!( } } + // Key-Value Observing unsafe impl WindowDelegate { - // Observe theme change - #[method(effectiveAppearanceDidChange:)] - fn effective_appearance_did_change(&self, sender: Option<&AnyObject>) { - trace_scope!("effectiveAppearanceDidChange:"); - unsafe { - self.performSelectorOnMainThread_withObject_waitUntilDone( - sel!(effectiveAppearanceDidChangedOnMainThread:), - sender, - false, - ) - }; - } + #[method(observeValueForKeyPath:ofObject:change:context:)] + fn observe_value( + &self, + key_path: Option<&NSString>, + _object: Option<&AnyObject>, + change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + trace_scope!("observeValueForKeyPath:ofObject:change:context:"); + // 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`"); + + // SAFETY: The value of `effectiveAppearance` is `NSAppearance` + let old: *const AnyObject = old; + let old: *const NSAppearance = old.cast(); + let old: &NSAppearance = unsafe { &*old }; + let new: *const AnyObject = new; + let new: *const NSAppearance = new.cast(); + let new: &NSAppearance = unsafe { &*new }; + + trace!(old = %unsafe { old.name() }, new = %unsafe { new.name() }, "effectiveAppearance changed"); + + // Ignore the change if the window's theme is customized by the user (since in that + // case the `effectiveAppearance` is only emitted upon said customization, and then + // it's triggered directly by a user action, and we don't want to emit the event). + if unsafe { self.window().appearance() }.is_some() { + return; + } - #[method(effectiveAppearanceDidChangedOnMainThread:)] - fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) { - let mtm = MainThreadMarker::from(self); - let theme = get_ns_theme(mtm); - let old_theme = self.ivars().current_theme.replace(Some(theme)); - if old_theme != Some(theme) { - self.queue_event(WindowEvent::ThemeChanged(theme)); + let old = appearance_to_theme(old); + let new = appearance_to_theme(new); + // Check that the theme changed in Winit's terms (the theme might have changed on + // other parameters, such as level of contrast, but the event should not be emitted + // in those cases). + if old == new { + return; + } + + self.queue_event(WindowEvent::ThemeChanged(new)); + } else { + panic!("unknown observed keypath {key_path:?}"); } } } ); +impl Drop for WindowDelegate { + fn drop(&mut self) { + unsafe { + self.window().removeObserver_forKeyPath(self, ns_string!("effectiveAppearance")); + } + } +} + fn new_window( app_delegate: &ApplicationDelegate, attrs: &WindowAttributes, @@ -613,6 +658,8 @@ fn new_window( if attrs.transparent { window.setOpaque(false); + // See `set_transparent` for details on why we do this. + window.setBackgroundColor(unsafe { Some(&NSColor::clearColor()) }); } // register for drag and drop operations. @@ -666,16 +713,14 @@ impl WindowDelegate { let scale_factor = window.backingScaleFactor() as _; - let current_theme = match attrs.preferred_theme { - Some(theme) => Some(theme), - None => Some(get_ns_theme(mtm)), - }; + if let Some(appearance) = theme_to_appearance(attrs.preferred_theme) { + unsafe { window.setAppearance(Some(&appearance)) }; + } let delegate = mtm.alloc().set_ivars(State { app_delegate: app_delegate.retain(), window: window.retain(), - current_theme: Cell::new(current_theme), - 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), @@ -689,6 +734,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] }; @@ -700,14 +746,16 @@ impl WindowDelegate { } window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); - // Enable theme change event - let notification_center = unsafe { NSDistributedNotificationCenter::defaultCenter() }; + // Listen for theme change event. + // + // SAFETY: The observer is un-registered in the `Drop` of the delegate. unsafe { - notification_center.addObserver_selector_name_object( + window.addObserver_forKeyPath_options_context( &delegate, - sel!(effectiveAppearanceDidChange:), - Some(ns_string!("AppleInterfaceThemeChangedNotification")), - None, + ns_string!("effectiveAppearance"), + NSKeyValueObservingOptions::NSKeyValueObservingOptionNew + | NSKeyValueObservingOptions::NSKeyValueObservingOptionOld, + ptr::null_mut(), ) }; @@ -797,13 +845,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)); @@ -821,7 +868,23 @@ impl WindowDelegate { } pub fn set_transparent(&self, transparent: bool) { - self.window().setOpaque(!transparent) + // This is just a hint for Quartz, it doesn't actually speculate with window alpha. + // Providing a wrong value here could result in visual artifacts, when the window is + // transparent. + self.window().setOpaque(!transparent); + + // AppKit draws the window with a background color by default, which is usually really + // nice, but gets in the way when we want to allow the contents of the window to be + // transparent, as in that case, the transparent contents will just be drawn on top of + // the background color. As such, to allow the window to be transparent, we must also set + // the background color to one with an empty alpha channel. + let color = if transparent { + unsafe { NSColor::clearColor() } + } else { + unsafe { NSColor::windowBackgroundColor() } + }; + + self.window().setBackgroundColor(Some(&color)); } pub fn set_blur(&self, blur: bool) { @@ -920,8 +983,8 @@ impl WindowDelegate { pub fn set_max_inner_size(&self, dimensions: Option) { let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { - width: std::f32::MAX as f64, - height: std::f32::MAX as f64, + width: f32::MAX as f64, + height: f32::MAX as f64, })); let scale_factor = self.scale_factor(); let max_size = dimensions.to_logical::(scale_factor); @@ -1106,7 +1169,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(()) } @@ -1355,7 +1419,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`. @@ -1365,6 +1429,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) => { @@ -1372,13 +1447,7 @@ impl WindowDelegate { toggle_fullscreen(self.window()); }, (Some(Fullscreen::Exclusive(ref video_mode)), None) => { - unsafe { - ffi::CGRestorePermanentDisplayConfiguration(); - assert_eq!( - ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), - ffi::kCGErrorSuccess - ); - }; + restore_and_release_display(&video_mode.monitor()); toggle_fullscreen(self.window()); }, (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { @@ -1409,13 +1478,7 @@ impl WindowDelegate { ); app.setPresentationOptions(presentation_options); - unsafe { - ffi::CGRestorePermanentDisplayConfiguration(); - assert_eq!( - ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), - ffi::kCGErrorSuccess - ); - }; + restore_and_release_display(&video_mode.monitor()); // Restore the normal window level following the Borderless fullscreen // `CGShieldingWindowLevel() + 1` hack. @@ -1535,7 +1598,14 @@ impl WindowDelegate { // Allow directly accessing the current monitor internally without unwrapping. pub(crate) fn current_monitor_inner(&self) -> Option { let display_id = get_display_id(&*self.window().screen()?); - Some(MonitorHandle::new(display_id)) + if let Some(monitor) = MonitorHandle::new(display_id) { + Some(monitor) + } else { + // NOTE: Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(display_id, "got screen with invalid display ID"); + None + } } #[inline] @@ -1559,7 +1629,7 @@ impl WindowDelegate { pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { let mut window_handle = rwh_04::AppKitHandle::empty(); window_handle.ns_window = self.window() as *const WinitWindow as *mut _; - window_handle.ns_view = Retained::as_ptr(&self.contentView().unwrap()) as *mut _; + window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _; rwh_04::RawWindowHandle::AppKit(window_handle) } @@ -1597,20 +1667,28 @@ impl WindowDelegate { } } - #[inline] - pub fn theme(&self) -> Option { - self.ivars().current_theme.get() - } - #[inline] pub fn has_focus(&self) -> bool { self.window().isKeyWindow() } + pub fn theme(&self) -> Option { + unsafe { self.window().appearance() } + .map(|appearance| appearance_to_theme(&appearance)) + .or_else(|| { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + }) + } + pub fn set_theme(&self, theme: Option) { - let mtm = MainThreadMarker::from(self); - set_ns_theme(theme, mtm); - self.ivars().current_theme.set(theme.or_else(|| Some(get_ns_theme(mtm)))); + unsafe { self.window().setAppearance(theme_to_appearance(theme).as_deref()) }; } #[inline] @@ -1631,6 +1709,21 @@ impl WindowDelegate { } } +fn restore_and_release_display(monitor: &MonitorHandle) { + let available_monitors = monitor::available_monitors(); + if available_monitors.contains(monitor) { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!(ffi::CGDisplayRelease(monitor.native_identifier()), ffi::kCGErrorSuccess); + }; + } else { + warn!( + monitor = monitor.name(), + "Tried to restore exclusive fullscreen on a monitor that is no longer available" + ); + } +} + impl WindowExtMacOS for WindowDelegate { #[inline] fn simple_fullscreen(&self) -> bool { @@ -1666,9 +1759,13 @@ impl WindowExtMacOS for WindowDelegate { self.ivars().is_simple_fullscreen.set(true); // Simulate pre-Lion fullscreen by hiding the dock and menu bar - let presentation_options = + let presentation_options = if self.is_borderless_game() { + NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar + } else { NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock - | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar; + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + }; app.setPresentationOptions(presentation_options); // Hide the titlebar @@ -1682,11 +1779,8 @@ impl WindowExtMacOS for WindowDelegate { 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); self.ivars().is_simple_fullscreen.set(false); let save_presentation_opts = self.ivars().save_presentation_opts.get(); @@ -1698,9 +1792,10 @@ impl WindowExtMacOS for WindowDelegate { self.window().setFrame_display(frame, true); self.window().setMovable(true); - - true + self.set_style_mask(new_mask); } + + true } #[inline] @@ -1764,39 +1859,52 @@ 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 = NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); -pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme { - let app = NSApplication::sharedApplication(mtm); - if !app.respondsToSelector(sel!(effectiveAppearance)) { - return Theme::Light; - } - let appearance = app.effectiveAppearance(); - let name = appearance - .bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ - NSString::from_str("NSAppearanceNameAqua"), - NSString::from_str("NSAppearanceNameDarkAqua"), - ])) - .unwrap(); - match &*name.to_string() { - "NSAppearanceNameDarkAqua" => Theme::Dark, - _ => Theme::Light, +fn dark_appearance_name() -> &'static NSString { + // Don't use the static `NSAppearanceNameDarkAqua` to allow linking on macOS < 10.14 + ns_string!("NSAppearanceNameDarkAqua") +} + +pub fn appearance_to_theme(appearance: &NSAppearance) -> Theme { + let best_match = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ + unsafe { NSAppearanceNameAqua.copy() }, + dark_appearance_name().copy(), + ])); + if let Some(best_match) = best_match { + if *best_match == *dark_appearance_name() { + Theme::Dark + } else { + Theme::Light + } + } else { + warn!(?appearance, "failed to determine the theme of the appearance"); + // Default to light in this case + Theme::Light } } -fn set_ns_theme(theme: Option, mtm: MainThreadMarker) { - let app = NSApplication::sharedApplication(mtm); - if app.respondsToSelector(sel!(effectiveAppearance)) { - let appearance = theme.map(|t| { - let name = match t { - Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"), - Theme::Light => NSString::from_str("NSAppearanceNameAqua"), - }; - NSAppearance::appearanceNamed(&name).unwrap() - }); - app.setAppearance(appearance.as_ref().map(|a| a.as_ref())); +fn theme_to_appearance(theme: Option) -> Option> { + let appearance = match theme? { + Theme::Light => unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua) }, + Theme::Dark => NSAppearance::appearanceNamed(dark_appearance_name()), + }; + if let Some(appearance) = appearance { + Some(appearance) + } else { + warn!(?theme, "could not find appearance for theme"); + // Assume system appearance in this case + None } } diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index a25fb3a209..3bfce6887e 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -1,27 +1,35 @@ use crate::monitor::{MonitorHandle as RootMonitorHandle, VideoModeHandle as RootVideoModeHandle}; use crate::window::Fullscreen as RootFullscreen; -#[cfg(windows_platform)] -#[path = "windows/mod.rs"] -mod platform; +#[cfg(android_platform)] +mod android; +#[cfg(ios_platform)] +mod ios; #[cfg(any(x11_platform, wayland_platform))] -#[path = "linux/mod.rs"] -mod platform; +mod linux; #[cfg(macos_platform)] -#[path = "macos/mod.rs"] -mod platform; +mod macos; +#[cfg(orbital_platform)] +mod orbital; +#[cfg(web_platform)] +mod web; +#[cfg(windows_platform)] +mod windows; + #[cfg(android_platform)] -#[path = "android/mod.rs"] -mod platform; +use android as platform; #[cfg(ios_platform)] -#[path = "ios/mod.rs"] -mod platform; -#[cfg(web_platform)] -#[path = "web/mod.rs"] -mod platform; +use ios as platform; +#[cfg(any(x11_platform, wayland_platform))] +use linux as platform; +#[cfg(macos_platform)] +use macos as platform; #[cfg(orbital_platform)] -#[path = "orbital/mod.rs"] -mod platform; +use orbital as platform; +#[cfg(web_platform)] +use web as platform; +#[cfg(windows_platform)] +use windows as platform; pub use self::platform::*; diff --git a/src/platform_impl/orbital/event_loop.rs b/src/platform_impl/orbital/event_loop.rs index 59ba9e522d..91f76e7591 100644 --- a/src/platform_impl/orbital/event_loop.rs +++ b/src/platform_impl/orbital/event_loop.rs @@ -20,7 +20,7 @@ use crate::keyboard::{ PhysicalKey, }; use crate::window::{ - CustomCursor as RootCustomCursor, CustomCursorSource, WindowId as RootWindowId, + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, }; use super::{ @@ -775,6 +775,11 @@ impl ActiveEventLoop { rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) } + #[inline] + pub fn system_theme(&self) -> Option { + None + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_display_handle_rwh_06( diff --git a/src/platform_impl/orbital/mod.rs b/src/platform_impl/orbital/mod.rs index 91d85c3d16..2d1525137a 100644 --- a/src/platform_impl/orbital/mod.rs +++ b/src/platform_impl/orbital/mod.rs @@ -103,7 +103,7 @@ pub struct WindowId { impl WindowId { pub const fn dummy() -> Self { - WindowId { fd: u64::max_value() } + WindowId { fd: u64::MAX } } } @@ -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/orbital/window.rs b/src/platform_impl/orbital/window.rs index d90d6a4584..3e676c3656 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -420,7 +420,7 @@ impl Window { window::ResizeDirection::West => "L", }; self.window_socket - .write(format!("D,{}", arg).as_bytes()) + .write(format!("D,{arg}").as_bytes()) .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; Ok(()) } 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/async/dispatcher.rs b/src/platform_impl/web/async/dispatcher.rs index 6623572154..10ab345e44 100644 --- a/src/platform_impl/web/async/dispatcher.rs +++ b/src/platform_impl/web/async/dispatcher.rs @@ -68,7 +68,12 @@ impl Dispatcher { // SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is // safe because this function won't return until `f` has finished executing. See // `Self::new()`. - let closure = Closure(unsafe { std::mem::transmute(closure) }); + let closure = Closure(unsafe { + std::mem::transmute::< + Box, + Box, + >(closure) + }); self.0.send(closure); diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 2844b2da50..214ff45a5e 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -324,7 +324,11 @@ impl Inner { match &self.cursor { SelectedCursor::Icon(icon) | SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => { - self.style.set("cursor", icon.name()) + if let CursorIcon::Default = icon { + self.style.remove("cursor") + } else { + self.style.set("cursor", icon.name()) + } }, SelectedCursor::Loading { previous: Previous::Image(cursor), .. } | SelectedCursor::Image(cursor) => { @@ -538,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/device.rs b/src/platform_impl/web/device.rs index 383bf3a1b8..91d08586c8 100644 --- a/src/platform_impl/web/device.rs +++ b/src/platform_impl/web/device.rs @@ -2,7 +2,7 @@ pub struct DeviceId(pub i32); impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { Self(0) } } diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 263a5fd964..23fe2e00ad 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -4,6 +4,7 @@ use std::sync::mpsc::{self, Receiver, Sender}; use crate::error::EventLoopError; use crate::event::Event; use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; +use crate::platform::web::{ActiveEventLoopExtWebSys, PollStrategy, WaitUntilStrategy}; use super::{backend, device, window}; @@ -53,7 +54,11 @@ impl EventLoop { // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe // because this function will never return and all resources not cleaned up by the point we // `throw` will leak, making this actually `'static`. - let handler = unsafe { std::mem::transmute(handler) }; + let handler = unsafe { + std::mem::transmute::)>, Box) + 'static>>( + handler, + ) + }; self.elw.p.run(handler, false); // Throw an exception to break out of Rust execution and use unreachable to tell the @@ -95,4 +100,20 @@ impl EventLoop { pub fn window_target(&self) -> &RootActiveEventLoop { &self.elw } + + pub fn set_poll_strategy(&self, strategy: PollStrategy) { + self.elw.set_poll_strategy(strategy); + } + + pub fn poll_strategy(&self) -> PollStrategy { + self.elw.poll_strategy() + } + + pub fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.elw.set_wait_until_strategy(strategy); + } + + pub fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.elw.wait_until_strategy() + } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index bd2c3bbe1f..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; @@ -8,24 +20,12 @@ use crate::event::{ WindowEvent, }; use crate::event_loop::{ControlFlow, DeviceEvents}; -use crate::platform::web::PollStrategy; +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; use crate::platform_impl::platform::backend::EventListenerHandle; use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner}; 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<()>); @@ -43,6 +43,7 @@ pub struct Execution { proxy_spawner: WakerSpawner>, control_flow: Cell, poll_strategy: Cell, + wait_until_strategy: Cell, exit: Cell, runner: RefCell, suspended: Cell, @@ -149,6 +150,7 @@ impl Shared { proxy_spawner, control_flow: Cell::new(ControlFlow::default()), poll_strategy: Cell::new(PollStrategy::default()), + wait_until_strategy: Cell::new(WaitUntilStrategy::default()), exit: Cell::new(false), runner: RefCell::new(RunnerEnum::Pending), suspended: Cell::new(false), @@ -243,21 +245,10 @@ impl Shared { return; } - let pointer_type = event.pointer_type(); - - if pointer_type != "mouse" { - return; - } - // chorded button event let device_id = RootDeviceId(DeviceId(event.pointer_id())); if let Some(button) = backend::event::mouse_button(&event) { - debug_assert_eq!( - pointer_type, "mouse", - "expect pointer type of a chorded button event to be a mouse" - ); - let state = if backend::event::mouse_buttons(&event).contains(button.into()) { ElementState::Pressed } else { @@ -321,10 +312,6 @@ impl Shared { return; } - if event.pointer_type() != "mouse" { - return; - } - let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); runner.send_event(Event::DeviceEvent { device_id: RootDeviceId(DeviceId(event.pointer_id())), @@ -344,10 +331,6 @@ impl Shared { return; } - if event.pointer_type() != "mouse" { - return; - } - let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); runner.send_event(Event::DeviceEvent { device_id: RootDeviceId(DeviceId(event.pointer_id())), @@ -368,7 +351,7 @@ impl Shared { } runner.send_event(Event::DeviceEvent { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + device_id: RootDeviceId(DeviceId::dummy()), event: DeviceEvent::Key(RawKeyEvent { physical_key: backend::event::key_code(&event), state: ElementState::Pressed, @@ -386,7 +369,7 @@ impl Shared { } runner.send_event(Event::DeviceEvent { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + device_id: RootDeviceId(DeviceId::dummy()), event: DeviceEvent::Key(RawKeyEvent { physical_key: backend::event::key_code(&event), state: ElementState::Released, @@ -479,14 +462,8 @@ impl Shared { if let Ok(RunnerEnum::Running(_)) = self.0.runner.try_borrow().as_ref().map(Deref::deref) { - #[wasm_bindgen] - extern "C" { - #[wasm_bindgen(js_name = queueMicrotask)] - fn queue_microtask(task: Function); - } - - queue_microtask( - Closure::once_into_js({ + self.window().queue_microtask( + &Closure::once_into_js({ let this = Rc::downgrade(&self.0); move || { if let Some(shared) = this.upgrade() { @@ -517,8 +494,13 @@ impl Shared { // If we can run the event processing right now, or need to queue this and wait for later let mut process_immediately = true; match self.0.runner.try_borrow().as_ref().map(Deref::deref) { - // If the runner is attached but not running, we always wake it up. - Ok(RunnerEnum::Running(_)) => (), + Ok(RunnerEnum::Running(ref runner)) => { + // If we're currently polling, queue this and wait for the poll() method to be + // called. + if let State::Poll { .. } = runner.state { + process_immediately = false; + } + }, Ok(RunnerEnum::Pending) => { // The runner still hasn't been attached: queue this event and wait for it to be process_immediately = false; @@ -688,6 +670,7 @@ impl Shared { start, end, _timeout: backend::Schedule::new_with_duration( + self.wait_until_strategy(), self.window(), move || cloned.resume_time_reached(start, end), delay, @@ -800,6 +783,14 @@ impl Shared { self.0.poll_strategy.get() } + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.0.wait_until_strategy.set(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.0.wait_until_strategy.get() + } + pub(crate) fn waker(&self) -> Waker> { self.0.proxy_spawner.waker() } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index e1bdf4b56d..79703aff88 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -18,7 +18,7 @@ use crate::event::{ }; use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::keyboard::ModifiersState; -use crate::platform::web::{CustomCursorFuture, PollStrategy}; +use crate::platform::web::{CustomCursorFuture, PollStrategy, WaitUntilStrategy}; use crate::platform_impl::platform::cursor::CustomCursor; use crate::platform_impl::platform::r#async::Waker; use crate::window::{ @@ -142,7 +142,7 @@ impl ActiveEventLoop { } }); - let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + let device_id = RootDeviceId(DeviceId::dummy()); runner.send_events( iter::once(Event::WindowEvent { @@ -178,7 +178,7 @@ impl ActiveEventLoop { } }); - let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + let device_id = RootDeviceId(DeviceId::dummy()); runner.send_events( iter::once(Event::WindowEvent { @@ -258,21 +258,6 @@ impl ActiveEventLoop { }); canvas.on_cursor_move( - { - let runner = self.runner.clone(); - let has_focus = has_focus.clone(); - let modifiers = self.modifiers.clone(); - - move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { - modifiers.set(active_modifiers); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ModifiersChanged(active_modifiers.into()), - }) - } - } - }, { let runner = self.runner.clone(); let has_focus = has_focus.clone(); @@ -372,20 +357,6 @@ impl ActiveEventLoop { ); canvas.on_mouse_press( - { - let runner = self.runner.clone(); - let modifiers = self.modifiers.clone(); - - move |active_modifiers| { - if modifiers.get() != active_modifiers { - modifiers.set(active_modifiers); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ModifiersChanged(active_modifiers.into()), - }) - } - } - }, { let runner = self.runner.clone(); let modifiers = self.modifiers.clone(); @@ -450,21 +421,6 @@ impl ActiveEventLoop { ); canvas.on_mouse_release( - { - let runner = self.runner.clone(); - let has_focus = has_focus.clone(); - let modifiers = self.modifiers.clone(); - - move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { - modifiers.set(active_modifiers); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ModifiersChanged(active_modifiers.into()), - }); - } - } - }, { let runner = self.runner.clone(); let has_focus = has_focus.clone(); @@ -614,7 +570,7 @@ impl ActiveEventLoop { window_id: RootWindowId(id), event: WindowEvent::Resized(new_size), }); - runner.request_redraw(RootWindowId(id)); + canvas.request_animation_frame(); } } }, @@ -667,6 +623,16 @@ impl ActiveEventLoop { self.runner.listen_device_events(allowed) } + pub fn system_theme(&self) -> Option { + backend::is_dark_mode(self.runner.window()).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { self.runner.set_control_flow(control_flow) } @@ -691,6 +657,14 @@ impl ActiveEventLoop { self.runner.poll_strategy() } + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.runner.set_wait_until_strategy(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.runner.wait_until_strategy() + } + pub(crate) fn waker(&self) -> Waker> { self.runner.waker() } diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 7b896b4c39..969d8bb188 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -28,11 +28,9 @@ mod event_loop; mod keyboard; mod main_thread; mod monitor; +mod web_sys; mod window; -#[path = "web_sys/mod.rs"] -mod backend; - pub use self::device::DeviceId; pub use self::error::OsError; pub(crate) use self::event_loop::{ @@ -43,6 +41,7 @@ pub use self::monitor::{MonitorHandle, VideoModeHandle}; pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; pub(crate) use self::keyboard::KeyEventExtra; +use self::web_sys as backend; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; pub(crate) use cursor::{ diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 0450ba8e0d..84dde9f7de 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -329,51 +329,29 @@ impl Canvas { self.pointer_handler.on_cursor_enter(&self.common, handler) } - pub fn on_mouse_release( - &mut self, - modifier_handler: MOD, - mouse_handler: M, - touch_handler: T, - ) where - MOD: 'static + FnMut(ModifiersState), + pub fn on_mouse_release(&mut self, mouse_handler: M, touch_handler: T) + where M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { - self.pointer_handler.on_mouse_release( - &self.common, - modifier_handler, - mouse_handler, - touch_handler, - ) + self.pointer_handler.on_mouse_release(&self.common, mouse_handler, touch_handler) } - pub fn on_mouse_press( - &mut self, - modifier_handler: MOD, - mouse_handler: M, - touch_handler: T, - ) where - MOD: 'static + FnMut(ModifiersState), + pub fn on_mouse_press(&mut self, mouse_handler: M, touch_handler: T) + where M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { self.pointer_handler.on_mouse_press( &self.common, - modifier_handler, mouse_handler, touch_handler, Rc::clone(&self.prevent_default), ) } - pub fn on_cursor_move( - &mut self, - modifier_handler: MOD, - mouse_handler: M, - touch_handler: T, - button_handler: B, - ) where - MOD: 'static + FnMut(ModifiersState), + pub fn on_cursor_move(&mut self, mouse_handler: M, touch_handler: T, button_handler: B) + where M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), T: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), @@ -381,7 +359,6 @@ impl Canvas { { self.pointer_handler.on_cursor_move( &self.common, - modifier_handler, mouse_handler, touch_handler, button_handler, @@ -427,8 +404,8 @@ impl Canvas { pub(crate) fn on_resize_scale(&mut self, scale_handler: S, size_handler: R) where - S: 'static + FnMut(PhysicalSize, f64), - R: 'static + FnMut(PhysicalSize), + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), { self.on_resize_scale = Some(ResizeScaleHandle::new( self.window().clone(), diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 51df63714b..ddd125765c 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -1,13 +1,15 @@ -use crate::dpi::LogicalPosition; use crate::event::{MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; +use dpi::{LogicalPosition, PhysicalPosition, Position}; use smol_str::SmolStr; use std::cell::OnceCell; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; +use super::Engine; + bitflags::bitflags! { // https://www.w3.org/TR/pointerevents3/#the-buttons-property #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -95,42 +97,48 @@ pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x(), y: event.offset_y() } } -// TODO: Remove this when Firefox supports correct movement values in coalesced events. +// TODO: Remove this when Firefox supports correct movement values in coalesced events and browsers +// have agreed on what coordinate space `movementX/Y` is using. // See . -pub struct MouseDelta(Option); - -pub struct MouseDeltaInner { - old_position: LogicalPosition, - old_delta: LogicalPosition, +// See . +pub enum MouseDelta { + Chromium, + Gecko { old_position: LogicalPosition, old_delta: LogicalPosition }, + Other, } impl MouseDelta { pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { - // Firefox has wrong movement values in coalesced events, we will detect that by checking - // for `pointerrawupdate` support. Presumably an implementation of `pointerrawupdate` - // should require correct movement values, otherwise uncoalesced events might be broken as - // well. - Self((!has_pointer_raw_support(window) && has_coalesced_events_support(event)).then(|| { - MouseDeltaInner { + match super::engine(window) { + Some(Engine::Chromium) => Self::Chromium, + // Firefox has wrong movement values in coalesced events. + Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { old_position: mouse_position(event), - old_delta: LogicalPosition { - x: event.movement_x() as f64, - y: event.movement_y() as f64, - }, - } - })) + old_delta: LogicalPosition::new( + event.movement_x() as f64, + event.movement_y() as f64, + ), + }, + _ => Self::Other, + } } - pub fn delta(&mut self, event: &MouseEvent) -> LogicalPosition { - if let Some(inner) = &mut self.0 { - let new_position = mouse_position(event); - let x = new_position.x - inner.old_position.x + inner.old_delta.x; - let y = new_position.y - inner.old_position.y + inner.old_delta.y; - inner.old_position = new_position; - inner.old_delta = LogicalPosition::new(0., 0.); - LogicalPosition::new(x, y) - } else { - LogicalPosition { x: event.movement_x() as f64, y: event.movement_y() as f64 } + pub fn delta(&mut self, event: &MouseEvent) -> Position { + match self { + MouseDelta::Chromium => { + PhysicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + MouseDelta::Gecko { old_position, old_delta } => { + let new_position = mouse_position(event); + let x = new_position.x - old_position.x + old_delta.x; + let y = new_position.y - old_position.y + old_delta.y; + *old_position = new_position; + *old_delta = LogicalPosition::new(0., 0.); + LogicalPosition::new(x, y).into() + }, + MouseDelta::Other => { + LogicalPosition::new(event.movement_x(), event.movement_y()).into() + }, } } } @@ -238,29 +246,6 @@ pub fn pointer_move_event(event: PointerEvent) -> impl Iterator. -pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool { - thread_local! { - static POINTER_RAW_SUPPORT: OnceCell = const { OnceCell::new() }; - } - - POINTER_RAW_SUPPORT.with(|support| { - *support.get_or_init(|| { - #[wasm_bindgen] - extern "C" { - type PointerRawSupport; - - #[wasm_bindgen(method, getter, js_name = onpointerrawupdate)] - fn has_on_pointerrawupdate(this: &PointerRawSupport) -> JsValue; - } - - let support: &PointerRawSupport = window.unchecked_ref(); - !support.has_on_pointerrawupdate().is_undefined() - }) - }) -} - // TODO: Remove when Safari supports `getCoalescedEvents`. // See . pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index b0a8fbae42..08962b49bc 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -9,6 +9,8 @@ mod pointer; mod resize_scaling; mod schedule; +use std::sync::OnceLock; + pub use self::canvas::{Canvas, Style}; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; @@ -16,8 +18,13 @@ pub use self::resize_scaling::ResizeScaleHandle; pub use self::schedule::Schedule; use crate::dpi::{LogicalPosition, LogicalSize}; +use js_sys::Array; use wasm_bindgen::closure::Closure; -use web_sys::{Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use web_sys::{ + Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window, +}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); @@ -158,3 +165,69 @@ pub fn is_visible(document: &Document) -> bool { } pub type RawCanvasType = HtmlCanvasElement; + +#[derive(Clone, Copy)] +pub enum Engine { + Chromium, + Gecko, + WebKit, +} + +pub fn engine(window: &Window) -> Option { + static ENGINE: OnceLock> = OnceLock::new(); + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = Navigator)] + type NavigatorExt; + + #[wasm_bindgen(method, getter, js_name = userAgentData)] + fn user_agent_data(this: &NavigatorExt) -> Option; + + type NavigatorUaData; + + #[wasm_bindgen(method, getter)] + fn brands(this: &NavigatorUaData) -> Array; + + type NavigatorUaBrandVersion; + + #[wasm_bindgen(method, getter)] + fn brand(this: &NavigatorUaBrandVersion) -> String; + } + + *ENGINE.get_or_init(|| { + let navigator: NavigatorExt = window.navigator().unchecked_into(); + + if let Some(data) = navigator.user_agent_data() { + for brand in data + .brands() + .iter() + .map(NavigatorUaBrandVersion::unchecked_from_js) + .map(|brand| brand.brand()) + { + match brand.as_str() { + "Chromium" => return Some(Engine::Chromium), + // TODO: verify when Firefox actually implements it. + "Gecko" => return Some(Engine::Gecko), + // TODO: verify when Safari actually implements it. + "WebKit" => return Some(Engine::WebKit), + _ => (), + } + } + + None + } else { + let data = navigator.user_agent().ok()?; + + if data.contains("Chrome/") { + Some(Engine::Chromium) + } else if data.contains("Gecko/") { + Some(Engine::Gecko) + } else if data.contains("AppleWebKit/") { + Some(Engine::WebKit) + } else { + None + } + } + }) +} diff --git a/src/platform_impl/web/web_sys/pointer.rs b/src/platform_impl/web/web_sys/pointer.rs index 90e8b27b7f..3ee168bb86 100644 --- a/src/platform_impl/web/web_sys/pointer.rs +++ b/src/platform_impl/web/web_sys/pointer.rs @@ -44,7 +44,7 @@ impl PointerHandler { // touch events are handled separately // handling them here would produce duplicate mouse events, inconsistent with // other platforms. - let pointer_id = (event.pointer_type() == "mouse").then(|| event.pointer_id()); + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); handler(modifiers, pointer_id); })); @@ -61,20 +61,18 @@ impl PointerHandler { // touch events are handled separately // handling them here would produce duplicate mouse events, inconsistent with // other platforms. - let pointer_id = (event.pointer_type() == "mouse").then(|| event.pointer_id()); + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); handler(modifiers, pointer_id); })); } - pub fn on_mouse_release( + pub fn on_mouse_release( &mut self, canvas_common: &Common, - mut modifier_handler: MOD, mut mouse_handler: M, mut touch_handler: T, ) where - MOD: 'static + FnMut(ModifiersState), M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { @@ -90,26 +88,23 @@ impl PointerHandler { event::mouse_position(&event).to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ), - "mouse" => mouse_handler( + _ => mouse_handler( modifiers, event.pointer_id(), event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::mouse_button(&event).expect("no mouse button released"), ), - _ => modifier_handler(modifiers), } })); } - pub fn on_mouse_press( + pub fn on_mouse_press( &mut self, canvas_common: &Common, - mut modifier_handler: MOD, mut mouse_handler: M, mut touch_handler: T, prevent_default: Rc>, ) where - MOD: 'static + FnMut(ModifiersState), M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { @@ -125,8 +120,9 @@ impl PointerHandler { } let modifiers = event::mouse_modifiers(&event); + let pointer_type = &event.pointer_type(); - match event.pointer_type().as_str() { + match pointer_type.as_str() { "touch" => { touch_handler( modifiers, @@ -135,7 +131,7 @@ impl PointerHandler { Force::Normalized(event.pressure() as f64), ); }, - "mouse" => { + _ => { mouse_handler( modifiers, event.pointer_id(), @@ -143,27 +139,27 @@ impl PointerHandler { event::mouse_button(&event).expect("no mouse button pressed"), ); - // Error is swallowed here since the error would occur every time the mouse - // is clicked when the cursor is grabbed, and there - // is probably not a situation where this could - // fail, that we care if it fails. - let _e = canvas.set_pointer_capture(event.pointer_id()); + if pointer_type == "mouse" { + // Error is swallowed here since the error would occur every time the + // mouse is clicked when the cursor is + // grabbed, and there is probably not a + // situation where this could fail, that we + // care if it fails. + let _e = canvas.set_pointer_capture(event.pointer_id()); + } }, - _ => modifier_handler(modifiers), } })); } - pub fn on_cursor_move( + pub fn on_cursor_move( &mut self, canvas_common: &Common, - mut modifier_handler: MOD, mut mouse_handler: M, mut touch_handler: T, mut button_handler: B, prevent_default: Rc>, ) where - MOD: 'static + FnMut(ModifiersState), M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), T: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), @@ -175,23 +171,10 @@ impl PointerHandler { Some(canvas_common.add_event("pointermove", move |event: PointerEvent| { let modifiers = event::mouse_modifiers(&event); - let pointer_type = event.pointer_type(); - - if let "touch" | "mouse" = pointer_type.as_str() { - } else { - modifier_handler(modifiers); - return; - } - let id = event.pointer_id(); // chorded button event if let Some(button) = event::mouse_button(&event) { - debug_assert_eq!( - pointer_type, "mouse", - "expect pointer type of a chorded button event to be a mouse" - ); - if prevent_default.get() { // prevent text selection event.prevent_default(); @@ -212,13 +195,7 @@ impl PointerHandler { // pointer move event let scale = super::scale_factor(&window); - match pointer_type.as_str() { - "mouse" => mouse_handler( - modifiers, - id, - &mut event::pointer_move_event(event) - .map(|event| event::mouse_position(&event).to_physical(scale)), - ), + match event.pointer_type().as_str() { "touch" => touch_handler( modifiers, id, @@ -229,7 +206,12 @@ impl PointerHandler { ) }), ), - _ => unreachable!("didn't return early before"), + _ => mouse_handler( + modifiers, + id, + &mut event::pointer_move_event(event) + .map(|event| event::mouse_position(&event).to_physical(scale)), + ), }; })); } diff --git a/src/platform_impl/web/web_sys/resize_scaling.rs b/src/platform_impl/web/web_sys/resize_scaling.rs index 18b377041f..1a323d06da 100644 --- a/src/platform_impl/web/web_sys/resize_scaling.rs +++ b/src/platform_impl/web/web_sys/resize_scaling.rs @@ -16,7 +16,7 @@ use super::media_query_handle::MediaQueryListHandle; use std::cell::{Cell, RefCell}; use std::rc::Rc; -pub struct ResizeScaleHandle(Rc>); +pub struct ResizeScaleHandle(Rc); impl ResizeScaleHandle { pub(crate) fn new( @@ -28,8 +28,8 @@ impl ResizeScaleHandle { resize_handler: R, ) -> Self where - S: 'static + FnMut(PhysicalSize, f64), - R: 'static + FnMut(PhysicalSize), + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), { Self(ResizeScaleInternal::new( window, @@ -42,7 +42,7 @@ impl ResizeScaleHandle { } pub(crate) fn notify_resize(&self) { - self.0.borrow_mut().notify() + self.0.notify() } } @@ -53,11 +53,11 @@ struct ResizeScaleInternal { document: Document, canvas: HtmlCanvasElement, style: Style, - mql: MediaQueryListHandle, + mql: RefCell, observer: ResizeObserver, _observer_closure: Closure, - scale_handler: Box, f64)>, - resize_handler: Box)>, + scale_handler: Box, f64)>, + resize_handler: Box)>, notify_scale: Cell, } @@ -69,12 +69,12 @@ impl ResizeScaleInternal { style: Style, scale_handler: S, resize_handler: R, - ) -> Rc> + ) -> Rc where - S: 'static + FnMut(PhysicalSize, f64), - R: 'static + FnMut(PhysicalSize), + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), { - Rc::>::new_cyclic(|weak_self| { + Rc::::new_cyclic(|weak_self| { let mql = Self::create_mql(&window, { let weak_self = weak_self.clone(); move |mql| { @@ -86,9 +86,7 @@ impl ResizeScaleInternal { let weak_self = weak_self.clone(); let observer_closure = Closure::new(move |entries: Array, _| { - if let Some(rc_self) = weak_self.upgrade() { - let mut this = rc_self.borrow_mut(); - + if let Some(this) = weak_self.upgrade() { let size = this.process_entry(entries); if this.notify_scale.replace(false) { @@ -101,18 +99,18 @@ impl ResizeScaleInternal { }); let observer = Self::create_observer(&canvas, observer_closure.as_ref()); - RefCell::new(Self { + Self { window, document, canvas, style, - mql, + mql: RefCell::new(mql), observer, _observer_closure: observer_closure, scale_handler: Box::new(scale_handler), resize_handler: Box::new(resize_handler), notify_scale: Cell::new(false), - }) + } }) } @@ -144,10 +142,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); } @@ -155,7 +152,7 @@ impl ResizeScaleInternal { observer } - fn notify(&mut self) { + fn notify(&self) { if !self.document.contains(Some(&self.canvas)) || self.style.get("display") == "none" { let size = PhysicalSize::new(0, 0); @@ -203,10 +200,9 @@ impl ResizeScaleInternal { } } - fn handle_scale(this: Rc>, mql: &MediaQueryList) { - let weak_self = Rc::downgrade(&this); - let mut this = this.borrow_mut(); - let scale = super::scale_factor(&this.window); + fn handle_scale(self: Rc, mql: &MediaQueryList) { + let weak_self = Rc::downgrade(&self); + let scale = super::scale_factor(&self.window); // TODO: confirm/reproduce this problem, see: // . @@ -220,15 +216,15 @@ impl ResizeScaleInternal { return; } - let new_mql = Self::create_mql(&this.window, move |mql| { + let new_mql = Self::create_mql(&self.window, move |mql| { if let Some(rc_self) = weak_self.upgrade() { Self::handle_scale(rc_self, mql); } }); - this.mql = new_mql; + self.mql.replace(new_mql); - this.notify_scale.set(true); - this.notify(); + self.notify_scale.set(true); + self.notify(); } fn process_entry(&self, entries: Array) -> PhysicalSize { diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs index c8b82b2e9f..eb9706843d 100644 --- a/src/platform_impl/web/web_sys/schedule.rs +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -1,12 +1,14 @@ -use js_sys::{Function, Object, Promise, Reflect}; +use js_sys::{Array, Function, Object, Promise, Reflect}; use std::cell::OnceCell; use std::time::Duration; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; +use web_sys::{ + AbortController, AbortSignal, Blob, BlobPropertyBag, MessageChannel, MessagePort, Url, Worker, +}; -use crate::platform::web::PollStrategy; +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; #[derive(Debug)] pub struct Schedule { @@ -29,6 +31,7 @@ enum Inner { port: MessagePort, _timeout_closure: Closure, }, + Worker(MessagePort), } impl Schedule { @@ -45,14 +48,24 @@ impl Schedule { } } - pub fn new_with_duration(window: &web_sys::Window, f: F, duration: Duration) -> Schedule + pub fn new_with_duration( + strategy: WaitUntilStrategy, + window: &web_sys::Window, + f: F, + duration: Duration, + ) -> Schedule where F: 'static + FnMut(), { - if has_scheduler_support(window) { - Self::new_scheduler(window, f, Some(duration)) - } else { - Self::new_timeout(window.clone(), f, Some(duration)) + match strategy { + WaitUntilStrategy::Scheduler => { + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) + } else { + Self::new_timeout(window.clone(), f, Some(duration)) + } + }, + WaitUntilStrategy::Worker => Self::new_worker(f, duration), } } @@ -153,6 +166,44 @@ impl Schedule { }, } } + + fn new_worker(f: F, duration: Duration) -> Schedule + where + F: 'static + FnMut(), + { + thread_local! { + static URL: ScriptUrl = ScriptUrl::new(include_str!("worker.min.js")); + static WORKER: Worker = URL.with(|url| Worker::new(&url.0)).expect("`new Worker()` is not expected to fail with a local script"); + } + + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: u32| secs.checked_mul(1000)) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration))) + .unwrap_or(u32::MAX); + + WORKER + .with(|worker| { + let port_2 = channel.port2(); + worker.post_message_with_transfer( + &Array::of2(&port_2, &duration.into()), + &Array::of1(&port_2).into(), + ) + }) + .expect("`Worker.postMessage()` is not expected to fail"); + + Schedule { _closure: closure, inner: Inner::Worker(port_1) } + } } impl Drop for Schedule { @@ -165,6 +216,10 @@ impl Drop for Schedule { port.close(); port.set_onmessage(None); }, + Inner::Worker(port) => { + port.close(); + port.set_onmessage(None); + }, } } } @@ -226,6 +281,29 @@ fn has_idle_callback_support(window: &web_sys::Window) -> bool { }) } +struct ScriptUrl(String); + +impl ScriptUrl { + fn new(script: &str) -> Self { + let sequence = Array::of1(&script.into()); + 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"); + + let url = Url::create_object_url_with_blob(&blob) + .expect("`URL.createObjectURL()` should never throw"); + + Self(url) + } +} + +impl Drop for ScriptUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0).expect("`URL.revokeObjectURL()` should never throw"); + } +} + #[wasm_bindgen] extern "C" { type WindowSupportExt; diff --git a/src/platform_impl/web/web_sys/worker.js b/src/platform_impl/web/web_sys/worker.js new file mode 100644 index 0000000000..5a8411ef83 --- /dev/null +++ b/src/platform_impl/web/web_sys/worker.js @@ -0,0 +1,10 @@ +onmessage = event => { + const [port, timeout] = event.data + const f = () => port.postMessage(undefined) + + if ('scheduler' in this) { + scheduler.postTask(f, { delay: timeout }) + } else { + setTimeout(f, timeout) + } +} diff --git a/src/platform_impl/web/web_sys/worker.min.js b/src/platform_impl/web/web_sys/worker.min.js new file mode 100644 index 0000000000..fd394a732f --- /dev/null +++ b/src/platform_impl/web/web_sys/worker.min.js @@ -0,0 +1 @@ +onmessage=e=>{let[s,t]=e.data,a=()=>s.postMessage(void 0);"scheduler"in this?scheduler.postTask(a,{delay:t}):setTimeout(a,t)}; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index acfefe3063..29ca730299 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -431,7 +431,7 @@ impl Drop for Inner { pub struct WindowId(pub(crate) u32); impl WindowId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { Self(0) } } diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs index 4f910ad5c9..366e44c685 100644 --- a/src/platform_impl/windows/dark_mode.rs +++ b/src/platform_impl/windows/dark_mode.rs @@ -123,7 +123,7 @@ fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { } } -fn should_use_dark_mode() -> bool { +pub fn should_use_dark_mode() -> bool { should_apps_use_dark_mode() && !is_high_contrast() } @@ -132,7 +132,13 @@ fn should_apps_use_dark_mode() -> bool { static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR; - let module = LoadLibraryA("uxtheme.dll\0".as_ptr()); + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + if !*DARK_MODE_SUPPORTED { + return None; + } + + let module = LoadLibraryA("uxtheme.dll\0".as_ptr().cast()); if module == 0 { return None; diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index cf7fa982bd..a5597e8f3d 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_ALLINPUT, 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, @@ -81,7 +87,7 @@ use crate::platform_impl::platform::{ raw_input, util, wrap_device_id, Fullscreen, WindowId, DEVICE_ID, }; use crate::window::{ - CustomCursor as RootCustomCursor, CustomCursorSource, WindowId as RootWindowId, + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, }; use runner::{EventLoopRunner, EventLoopRunnerShared}; @@ -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 { @@ -550,6 +491,10 @@ impl ActiveEventLoop { raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, allowed); } + pub fn system_theme(&self) -> Option { + Some(if super::dark_mode::should_use_dark_mode() { Theme::Dark } else { Theme::Light }) + } + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { self.runner_shared.set_control_flow(control_flow) } @@ -619,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] @@ -629,9 +574,11 @@ fn main_thread_id() -> u32 { // // See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute #[link_section = ".CRT$XCU"] - static INIT_MAIN_THREAD_ID: unsafe fn() = { - unsafe fn initer() { - unsafe { MAIN_THREAD_ID = GetCurrentThreadId() }; + static INIT_MAIN_THREAD_ID: unsafe extern "C" fn() = { + unsafe extern "C" fn initer() { + unsafe { + MAIN_THREAD_ID = GetCurrentThreadId(); + } } initer }; @@ -678,6 +625,144 @@ 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 indefinitely 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()]) }; + + // We must use `QS_ALLINPUT` to wake on accessibility messages. + let result = MsgWaitForMultipleObjectsEx( + num_handles, + raw_handles.as_ptr() as *const _, + wait_duration_ms, + QS_ALLINPUT, + 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, @@ -1133,6 +1218,31 @@ unsafe fn public_window_callback_inner( WM_NCLBUTTONDOWN => { if wparam == HTCAPTION as _ { + // Prevent the user event loop from pausing when left clicking the title bar. + // + // When the user interacts with the title bar, Windows enters the modal event + // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy + // mouse-move event seems to cancel the modal loop early, preventing the pause. + // The application will never see this dummy event. + // + // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE + // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event. + // One uses client-area coordinates and the other is screen-coordinates. In any + // case, passing the lparam as-is with the dummy event does not seem the cancel + // the modal loop. + // + // However, passing in a value of 0 has been observed to always cancel the pause. + // + // Other notes: + // + // For some unknown reason, the cursor will blink when clicking the title bar. + // Cancelling the modal loop early causes the blink to happen *immediately*. + // Otherwise, the blank happens *after* the pause. + // + // When right-click the title bar, the system window menu is presented to the user, + // and the modal event loop begins. This dummy event does *not* prevent the freeze + // in the main event loop caused by that popup menu. + let lparam = 0; unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) }; } result = ProcResult::DefWindowProc(wparam); @@ -1502,9 +1612,9 @@ unsafe fn public_window_callback_inner( }, WM_IME_SETCONTEXT => { - // Hide composing text drawn by IME. - let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize); - result = ProcResult::DefWindowProc(wparam); + // IME UI visibility flags are in lparam. + let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize); + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); }, // this is necessary for us to maintain minimize/restore state @@ -2538,7 +2648,7 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: RAWINPUT) { } enum PointerMoveKind { - /// Pointer enterd to the window. + /// Pointer entered to the window. Enter, /// Pointer leaved the window client area. Leave, 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/icon.rs b/src/platform_impl/windows/icon.rs index 624c8c8279..4e5fe69e1a 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -109,13 +109,28 @@ impl WinIcon { pub fn from_resource( resource_id: u16, size: Option>, + ) -> Result { + Self::from_resource_ptr(resource_id as PCWSTR, size) + } + + pub fn from_resource_name( + resource_name: &str, + size: Option>, + ) -> Result { + let wide_name = util::encode_wide(resource_name); + Self::from_resource_ptr(wide_name.as_ptr(), size) + } + + fn from_resource_ptr( + resource: PCWSTR, + size: Option>, ) -> Result { // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size let (width, height) = size.map(Into::into).unwrap_or((0, 0)); let handle = unsafe { LoadImageW( util::get_instance_handle(), - resource_id as PCWSTR, + resource, IMAGE_ICON, width, height, diff --git a/src/platform_impl/windows/ime.rs b/src/platform_impl/windows/ime.rs index 6761487351..eb65abf3aa 100644 --- a/src/platform_impl/windows/ime.rs +++ b/src/platform_impl/windows/ime.rs @@ -35,8 +35,13 @@ impl ImeContext { let mut first = None; let mut last = None; let mut boundary_before_char = 0; + let mut attr_idx = 0; + + for chr in text.chars() { + let Some(attr) = attrs.get(attr_idx).copied() else { + break; + }; - for (attr, chr) in attrs.into_iter().zip(text.chars()) { let char_is_targeted = attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED; @@ -47,6 +52,7 @@ impl ImeContext { } boundary_before_char += chr.len_utf8(); + attr_idx += chr.len_utf16(); } if first.is_some() && last.is_none() { diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs index 43dea40db6..9c109490eb 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)) @@ -213,8 +213,7 @@ impl KeyEventBuilder { .unwrap_or(false); if more_char_coming { // No need to produce an event just yet, because there are still more - // characters that need to appended to this keyobard - // event + // characters that need to be appended to this keyboard event MatchResult::TokenToRemove(pending_token) } else { let mut event_info = self.event_info.lock().unwrap(); @@ -334,11 +333,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 synthesized 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) { @@ -466,7 +465,7 @@ enum PartialText { enum PartialLogicalKey { /// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If - /// the text consists of multiple grapheme clusters (user-precieved characters) that means that + /// the text consists of multiple grapheme clusters (user-perceived characters) that means that /// dead key could not be combined with the second input, and in that case we should fall back /// to using what would have without a dead-key input. TextOr(Key), @@ -556,7 +555,7 @@ impl PartialKeyEventInfo { // We convert dead keys into their character. // The reason for this is that `key_without_modifiers` is designed for key-bindings, // but the US International layout treats `'` (apostrophe) as a dead key and the - // reguar US layout treats it a character. In order for a single binding + // regular US layout treats it a character. In order for a single binding // configuration to work with both layouts, we forward each dead key as a character. Key::Dead(k) => { if let Some(ch) = k { diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index bfb18d81cc..9bb02fd4ed 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -1,5 +1,3 @@ -#![cfg(windows_platform)] - use smol_str::SmolStr; use windows_sys::Win32::Foundation::{HANDLE, HWND}; use windows_sys::Win32::UI::WindowsAndMessaging::{HMENU, WINDOW_LONG_PTR_INDEX}; @@ -69,7 +67,7 @@ unsafe impl Sync for PlatformSpecificWindowAttributes {} pub struct DeviceId(u32); impl DeviceId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { DeviceId(0) } } @@ -105,7 +103,7 @@ unsafe impl Send for WindowId {} unsafe impl Sync for WindowId {} impl WindowId { - pub const unsafe fn dummy() -> Self { + pub const fn dummy() -> Self { WindowId(0) } } diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 44c7c95a73..f78a7bb03e 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -225,16 +225,16 @@ pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { if scancode == 0xe11d || scancode == 0xe02a { // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing // Ctrl+NumLock. - // This equvalence means that if the user presses Pause, the keyboard will emit two + // This equivalence means that if the user presses Pause, the keyboard will emit two // subsequent keypresses: // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) // 2, 0x0045 - Which on its own can be interpreted as Pause // // There's another combination which isn't quite an equivalence: - // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, pressing // PrtSc (print screen) produces the following sequence: // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) - // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on + // 2, 0xE037 - Which is a numpad multiply (0x37) with an extension flag (0xE000). This on // its own it can be interpreted as PrtSc // // For this reason, if we encounter the first keypress, we simply ignore it, trusting @@ -263,39 +263,37 @@ pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { scancode_to_physicalkey(scancode as u32) }; if keyboard.VKey == VK_SHIFT { - if let PhysicalKey::Code(code) = physical_key { - match code { - KeyCode::NumpadDecimal - | KeyCode::Numpad0 - | KeyCode::Numpad1 - | KeyCode::Numpad2 - | KeyCode::Numpad3 - | KeyCode::Numpad4 - | KeyCode::Numpad5 - | KeyCode::Numpad6 - | KeyCode::Numpad7 - | KeyCode::Numpad8 - | KeyCode::Numpad9 => { - // On Windows, holding the Shift key makes numpad keys behave as if NumLock - // wasn't active. The way this is exposed to applications by the system is that - // the application receives a fake key release event for the shift key at the - // moment when the numpad key is pressed, just before receiving the numpad key - // as well. - // - // The issue is that in the raw device event (here), the fake shift release - // event reports the numpad key as the scancode. Unfortunately, the event - // doesn't have any information to tell whether it's the - // left shift or the right shift that needs to get the fake - // release (or press) event so we don't forward this - // event to the application at all. - // - // For more on this, read the article by Raymond Chen, titled: - // "The shift key overrides NumLock" - // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 - return None; - }, - _ => (), - } + if let PhysicalKey::Code( + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9, + ) = physical_key + { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event + // doesn't have any information to tell whether it's the + // left shift or the right shift that needs to get the fake + // release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return None; } } diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index a22447328b..3bbd79b8e9 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -8,7 +8,7 @@ use std::{io, mem, ptr}; use crate::utils::Lazy; use windows_sys::core::{HRESULT, PCWSTR}; -use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT}; +use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, POINT, RECT}; use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR}; use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; use windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; @@ -18,9 +18,9 @@ use windows_sys::Win32::UI::HiDpi::{ use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow; use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO}; use windows_sys::Win32::UI::WindowsAndMessaging::{ - ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement, GetWindowRect, - IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, - IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, + ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement, + GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, + IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_MAXIMIZE, WINDOWPLACEMENT, }; @@ -103,6 +103,13 @@ pub fn set_cursor_hidden(hidden: bool) { } } +pub fn get_cursor_position() -> Result { + unsafe { + let mut point: POINT = mem::zeroed(); + win_to_err(GetCursorPos(&mut point)).map(|_| point) + } +} + pub fn get_cursor_clip() -> Result { unsafe { let mut rect: RECT = mem::zeroed(); @@ -225,31 +232,31 @@ pub type GetDpiForMonitor = unsafe extern "system" fn( pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( rect: *mut RECT, - dwStyle: u32, - bMenu: BOOL, - dwExStyle: u32, + dw_style: u32, + b_menu: BOOL, + dw_ex_style: u32, dpi: u32, ) -> BOOL; pub type GetPointerFrameInfoHistory = unsafe extern "system" fn( - pointerId: u32, - entriesCount: *mut u32, - pointerCount: *mut u32, - pointerInfo: *mut POINTER_INFO, + pointer_id: u32, + entries_count: *mut u32, + pointer_count: *mut u32, + pointer_info: *mut POINTER_INFO, ) -> BOOL; -pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL; +pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointer_id: u32) -> BOOL; pub type GetPointerDeviceRects = unsafe extern "system" fn( device: HANDLE, - pointerDeviceRect: *mut RECT, - displayRect: *mut RECT, + pointer_device_rect: *mut RECT, + display_rect: *mut RECT, ) -> BOOL; pub type GetPointerTouchInfo = - unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL; + unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL; pub type GetPointerPenInfo = - unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; + unsafe extern "system" fn(point_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL; pub(crate) static GET_DPI_FOR_WINDOW: Lazy> = Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 23fec32b34..228c6f03e5 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -428,14 +428,6 @@ impl Window { #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - let confine = match mode { - CursorGrabMode::None => false, - CursorGrabMode::Confined => true, - CursorGrabMode::Locked => { - return Err(ExternalError::NotSupported(NotSupportedError::new())) - }, - }; - let window = self.window; let window_state = Arc::clone(&self.window_state); let (tx, rx) = channel(); @@ -446,7 +438,10 @@ impl Window { .lock() .unwrap() .mouse - .set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine)) + .set_cursor_flags(window, |f| { + f.set(CursorFlags::GRABBED, mode != CursorGrabMode::None); + f.set(CursorFlags::LOCKED, mode == CursorGrabMode::Locked); + }) .map_err(|e| ExternalError::Os(os_error!(e))); let _ = tx.send(result); }); @@ -1129,7 +1124,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/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 911467b4c7..8e24a4f252 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -77,6 +77,7 @@ bitflags! { const GRABBED = 1 << 0; const HIDDEN = 1 << 1; const IN_WINDOW = 1 << 2; + const LOCKED = 1 << 3; } } bitflags! { @@ -485,7 +486,22 @@ impl CursorFlags { if util::is_focused(window) { let cursor_clip = match self.contains(CursorFlags::GRABBED) { true => { - if self.contains(CursorFlags::HIDDEN) { + if self.contains(CursorFlags::LOCKED) { + if let Ok(pos) = util::get_cursor_position() { + Some(RECT { + left: pos.x, + right: pos.x + 1, + top: pos.y, + bottom: pos.y + 1, + }) + } else { + // If lock is applied while the cursor is not available, lock it to the + // middle of the window. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 }) + } + } else if self.contains(CursorFlags::HIDDEN) { // Confine the cursor to the center of the window if the cursor is hidden. // This avoids problems with the cursor activating // the taskbar if the window borders or overlaps that. diff --git a/src/window.rs b/src/window.rs index f443a71db4..e0158eff52 100644 --- a/src/window.rs +++ b/src/window.rs @@ -71,16 +71,13 @@ pub struct WindowId(pub(crate) platform_impl::WindowId); impl WindowId { /// Returns a dummy id, useful for unit testing. /// - /// # Safety + /// # Notes /// /// The only guarantee made about the return value of this function is that /// it will always be equal to itself and to future values returned by this function. /// No other guarantees are made. This may be equal to a real [`WindowId`]. - /// - /// **Passing this into a winit function will result in undefined behavior.** - pub const unsafe fn dummy() -> Self { - #[allow(unused_unsafe)] - WindowId(unsafe { platform_impl::WindowId::dummy() }) + pub const fn dummy() -> Self { + WindowId(platform_impl::WindowId::dummy()) } } @@ -393,7 +390,6 @@ impl WindowAttributes { /// /// ## Platform-specific /// - /// - **macOS:** This is an app-wide setting. /// - **Wayland:** This controls only CSD. When using `None` it'll try to use dbus to get the /// system preference. When explicit theme is used, this will avoid dbus all together. /// - **x11:** Build window with `_GTK_THEME_VARIANT` hint set to `dark` or `light`. @@ -421,8 +417,8 @@ impl WindowAttributes { /// /// ## Platform-specific /// - /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely - /// prevent all apps from reading the window content, for instance, QuickTime. + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. /// - **iOS / Android / Web / x11 / Orbital:** Ignored. /// /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone @@ -470,8 +466,8 @@ impl WindowAttributes { /// ## Platform-specific /// /// - **Windows** : A child window has the WS_CHILD style and is confined - /// to the client area of its parent window. For more information, see - /// + /// to the client area of its parent window. For more information, see + /// /// - **X11**: A child window is confined to the client area of its parent window. /// - **Android / iOS / Wayland / Web:** Unsupported. #[cfg(feature = "rwh_06")] @@ -534,9 +530,9 @@ impl Window { /// provided by XRandR. /// /// If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use - /// the XRandR scaling method. Generally speaking, you should try to configure the - /// standard system variables to do what you want before resorting to - /// `WINIT_X11_SCALE_FACTOR`. + /// the XRandR scaling method. Generally speaking, you should try to configure the + /// standard system variables to do what you want before resorting to + /// `WINIT_X11_SCALE_FACTOR`. /// - **Wayland:** The scale factor is suggested by the compositor for each window individually /// by using the wp-fractional-scale protocol if available. Falls back to integer-scale /// factors otherwise. @@ -888,7 +884,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Wayland / Orbital:** Always returns [`None`]. + /// - **iOS / Android / Web / Orbital:** Always returns [`None`]. #[inline] pub fn resize_increments(&self) -> Option> { let _span = tracing::debug_span!("winit::Window::resize_increments",).entered(); @@ -904,7 +900,6 @@ impl Window { /// /// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole /// numbers. - /// - **Wayland:** Not implemented. /// - **iOS / Android / Web / Orbital:** Unsupported. #[inline] pub fn set_resize_increments>(&self, increments: Option) { @@ -942,8 +937,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **macOS:** If you're not drawing to the window yourself, you might have to set the - /// background color of the window to enable transparency. + /// - **macOS:** This will reset the window's background color. /// - **Web / iOS / Android:** Unsupported. /// - **X11:** Can only be set while building the window, with /// [`WindowAttributes::with_transparent`]. @@ -1279,7 +1273,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 @@ -1355,11 +1350,12 @@ impl Window { self.window.maybe_queue_on_main(move |w| w.request_user_attention(request_type)) } - /// Sets the current window theme. Use `None` to fallback to system default. + /// Set or override the window theme. + /// + /// Specify `None` to reset the theme to the system default. /// /// ## Platform-specific /// - /// - **macOS:** This is an app-wide setting. /// - **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus to /// get the system preference. /// - **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, it @@ -1377,10 +1373,12 @@ impl Window { /// Returns the current window theme. /// + /// Returns `None` if it cannot be determined on the current platform. + /// /// ## Platform-specific /// - /// - **macOS:** This is an app-wide setting. - /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. + /// - **iOS / Android / x11 / Orbital:** Unsupported. + /// - **Wayland:** Only returns theme overrides. #[inline] pub fn theme(&self) -> Option { let _span = tracing::debug_span!("winit::Window::theme",).entered(); @@ -1391,8 +1389,8 @@ impl Window { /// /// ## Platform-specific /// - /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely - /// prevent all apps from reading the window content, for instance, QuickTime. + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. /// - **iOS / Android / x11 / Wayland / Web / Orbital:** Unsupported. /// /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone @@ -1708,8 +1706,7 @@ pub enum CursorGrabMode { /// /// ## Platform-specific /// - /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for - /// now. + /// - **X11:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. Locked, } @@ -1827,10 +1824,11 @@ pub enum WindowLevel { /// ## Platform-specific /// /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum ImePurpose { /// No special hints for the IME (default). + #[default] Normal, /// The IME is used for password input. Password, @@ -1840,22 +1838,39 @@ pub enum ImePurpose { Terminal, } -impl Default for ImePurpose { - fn default() -> Self { - Self::Normal - } -} - /// An opaque token used to activate the [`Window`]. /// /// [`Window`]: crate::window::Window #[derive(Debug, PartialEq, Eq, Clone)] pub struct ActivationToken { - pub(crate) _token: String, + pub(crate) token: String, } impl ActivationToken { - pub(crate) fn _new(_token: String) -> Self { - Self { _token } + /// Make an [`ActivationToken`] from a string. + /// + /// This method should be used to wrap tokens passed by side channels to your application, like + /// dbus. + /// + /// The validity of the token is ensured by the windowing system. Using the invalid token will + /// only result in the side effect of the operation involving it being ignored (e.g. window + /// won't get focused automatically), but won't yield any errors. + /// + /// To obtain a valid token, use + #[cfg_attr( + any(x11_platform, wayland_platform, docsrs), + doc = " [`request_activation_token`](crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token)." + )] + #[cfg_attr( + not(any(x11_platform, wayland_platform, docsrs)), + doc = " `request_activation_token`." + )] + pub fn from_raw(token: String) -> Self { + Self { token } + } + + /// Convert the token to its string representation to later pass via IPC. + pub fn into_raw(self) -> String { + self.token } }