diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..9de26e778e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,35 @@ +# Core maintainers: +# - @msiglreith +# - @kchibisov +# - @madsmtm +# - @maroider + +# Android +/src/platform/android.rs @msiglreith +/src/platform_impl/android @msiglreith + +# iOS +/src/platform/ios.rs @francesca64 +/src/platform_impl/ios @francesca64 + +# Unix in general +/src/platform/unix.rs @kchibisov +/src/platform_impl/linux/mod.rs @kchibisov + +# Wayland +/src/platform_impl/linux/wayland @kchibisov + +# X11 +/src/platform_impl/linux/x11 @kchibisov + +# macOS +/src/platform/macos.rs @madsmtm +/src/platform_impl/macos @madsmtm + +# Web (no maintainer) +/src/platform/web.rs +/src/platform_impl/web + +# Windows +/src/platform/windows.rs @msiglreith +/src/platform_impl/windows @msiglreith diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1076aa5655..d9be441966 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,14 @@ jobs: - name: Check Formatting run: cargo +stable fmt --all -- --check - Tests: + tests: + name: Tests strategy: fail-fast: false matrix: - rust_version: [stable, nightly] + rust_version: [1.57.0, stable, nightly] platform: + # Note: Make sure that we test all the `docs.rs` targets defined in Cargo.toml! - { target: x86_64-pc-windows-msvc, os: windows-latest, } - { target: i686-pc-windows-msvc, os: windows-latest, } - { target: x86_64-pc-windows-gnu, os: windows-latest, host: -x86_64-pc-windows-gnu } @@ -46,6 +48,7 @@ jobs: OPTIONS: ${{ matrix.platform.options }} FEATURES: ${{ format(',{0}', matrix.platform.features ) }} CMD: ${{ matrix.platform.cmd }} + RUSTDOCFLAGS: -Dwarnings runs-on: ${{ matrix.platform.os }} steps: @@ -61,7 +64,13 @@ jobs: with: rust-version: ${{ matrix.rust_version }}${{ matrix.platform.host }} targets: ${{ matrix.platform.target }} + components: clippy + - name: Setup NDK path + shell: bash + # "Temporary" workaround until https://github.com/actions/virtual-environments/issues/5879#issuecomment-1195156618 + # gets looked into. + run: echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> $GITHUB_ENV - name: Install GCC Multilib if: (matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686') run: sudo apt-get update && sudo apt-get install gcc-multilib @@ -71,12 +80,7 @@ jobs: - name: Check documentation shell: bash - if: matrix.platform.target != 'wasm32-unknown-unknown' - run: cargo $CMD doc --no-deps --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES - - - name: Build - shell: bash - run: cargo $CMD build --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + run: cargo $CMD doc --no-deps --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES --document-private-items - name: Build tests shell: bash @@ -89,10 +93,10 @@ jobs: !contains(matrix.platform.target, 'wasm32')) run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES - - - name: Build with serde enabled + - name: Lint with clippy shell: bash - run: cargo $CMD build --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES + if: (matrix.rust_version == '1.57.0') && !contains(matrix.platform.options, '--no-default-features') + run: cargo clippy --all-targets --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES -- -Dwarnings - name: Build tests with serde enabled shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index e34f0fbdc2..60ab4234a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,40 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- On Windows, added `WindowExtWindows::set_undecorated_shadow` and `WindowBuilderExtWindows::with_undecorated_shadow` to draw the drop shadow behind a borderless window. +- On Windows, fixed default window features (ie snap, animations, shake, etc.) when decorations are disabled. +- **Breaking:** On macOS, add support for two-finger touchpad magnification and rotation gestures with new events `WindowEvent::TouchpadMagnify` and `WindowEvent::TouchpadRotate`. +- On Wayland, `wayland-csd-adwaita` now uses `ab_glyph` instead of `crossfont` to render the title for decorations. +- On Wayland, a new `wayland-csd-adwaita-crossfont` feature was added to use `crossfont` instead of `ab_glyph` for decorations. +- On Wayland, if not otherwise specified use upstream automatic CSD theme selection. + +# 0.27.2 (2022-8-12) + +- On macOS, fixed touch phase reporting when scrolling. +- On X11, fix min, max and resize increment hints not persisting for resizable windows (e.g. on DPI change). +- On Windows, respect min/max inner sizes when creating the window. +- For backwards compatibility, `Window` now (additionally) implements the old version (`0.4`) of the `HasRawWindowHandle` trait +- On Windows, added support for `EventLoopWindowTarget::set_device_event_filter`. +- On Wayland, fix user requested `WindowEvent::RedrawRequested` being delayed by a frame. + +# 0.27.1 (2022-07-30) + +- The minimum supported Rust version was lowered to `1.57.0` and now explicitly tested. +- On X11, fix crash on start due to inability to create an IME context without any preedit. + +# 0.27.0 (2022-07-26) + +- On Windows, fix hiding a maximized window. +- On Android, `ndk-glue`'s `NativeWindow` lock is now held between `Event::Resumed` and `Event::Suspended`. +- On Web, added `EventLoopExtWebSys` with a `spawn` method to start the event loop without throwing an exception. +- Added `WindowEvent::Occluded(bool)`, currently implemented on macOS and X11. +- On X11, fix events for caps lock key not being sent +- Build docs on `docs.rs` for iOS and Android as well. +- **Breaking:** Removed the `WindowAttributes` struct, since all its functionality is accessible from `WindowBuilder`. +- Added `WindowBuilder::transparent` getter to check if the user set `transparent` attribute. +- On macOS, Fix emitting `Event::LoopDestroyed` on CMD+Q. +- On macOS, fixed an issue where having multiple windows would prevent run_return from ever returning. +- On Wayland, fix bug where the cursor wouldn't hide in GNOME. - On macOS, Windows, and Wayland, add `set_cursor_hittest` to let the window ignore mouse events. - On Windows, added `WindowExtWindows::set_skip_taskbar` and `WindowBuilderExtWindows::with_skip_taskbar`. - On Windows, added `EventLoopBuilderExtWindows::with_msg_hook`. @@ -19,6 +53,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On X11, fix for repeated event loop iteration when `ControlFlow` was `Wait` - On X11, fix scale factor calculation when the only monitor is reconnected - On Wayland, report unaccelerated mouse deltas in `DeviceEvent::MouseMotion`. +- On Web, a focused event is manually generated when a click occurs to emulate behaviour of other backends. - **Breaking:** Bump `ndk` version to 0.6, ndk-sys to `v0.3`, `ndk-glue` to `0.6`. - Remove no longer needed `WINIT_LINK_COLORSYNC` environment variable. - **Breaking:** Rename the `Exit` variant of `ControlFlow` to `ExitWithCode`, which holds a value to control the exit code after running. Add an `Exit` constant which aliases to `ExitWithCode(0)` instead to avoid major breakage. This shouldn't affect most existing programs. @@ -29,7 +64,6 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** Replaced `EventLoopExtUnix` with `EventLoopBuilderExtUnix` (which also has renamed methods). - **Breaking:** The platform specific extensions for Windows `winit::platform::windows` have changed. All `HANDLE`-like types e.g. `HWND` and `HMENU` were converted from winapi types or `*mut c_void` to `isize`. This was done to be consistent with the type definitions in windows-sys and to not expose internal dependencies. - The internal bindings to the [Windows API](https://docs.microsoft.com/en-us/windows/) were changed from the unofficial [winapi](https://github.com/retep998/winapi-rs) bindings to the official Microsoft [windows-sys](https://github.com/microsoft/windows-rs) bindings. -- On Wayland, fix resize and scale factor changes not being propagated properly. - On Wayland, fix polling during consecutive `EventLoop::run_return` invocations. - On Windows, fix race issue creating fullscreen windows with `WindowBuilder::with_fullscreen` - On Android, `virtual_keycode` for `KeyboardInput` events is now filled in where a suitable match is found. @@ -37,6 +71,36 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland, fix `TouchPhase::Ended` always reporting the location of the first touch down, unless the compositor sent a cancel or frame event. - On iOS, send `RedrawEventsCleared` even if there are no redraw events, consistent with other platforms. +- **Breaking:** Replaced `Window::with_app_id` and `Window::with_class` with `Window::with_name` on `WindowBuilderExtUnix`. +- On Wayland, fallback CSD was replaced with proper one: + - `WindowBuilderExtUnix::with_wayland_csd_theme` to set color theme in builder. + - `WindowExtUnix::wayland_set_csd_theme` to set color theme when creating a window. + - `WINIT_WAYLAND_CSD_THEME` env variable was added, it can be used to set "dark"/"light" theme in apps that don't expose theme setting. + - `wayland-csd-adwaita` feature that enables proper CSD with title rendering using FreeType system library. + - `wayland-csd-adwaita-notitle` feature that enables CSD but without title rendering. +- On Wayland and X11, fix window not resizing with `Window::set_inner_size` after calling `Window:set_resizable(false)`. +- On Windows, fix wrong fullscreen monitors being recognized when handling WM_WINDOWPOSCHANGING messages +- **Breaking:** Added new `WindowEvent::Ime` supported on desktop platforms. +- Added `Window::set_ime_allowed` supported on desktop platforms. +- **Breaking:** IME input on desktop platforms won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::Ime` events are handled. +- On macOS, `WindowEvent::Resized` is now emitted in `frameDidChange` instead of `windowDidResize`. +- **Breaking:** On X11, device events are now ignored for unfocused windows by default, use `EventLoopWindowTarget::set_device_event_filter` to set the filter level. +- Implemented `Default` on `EventLoop<()>`. +- Implemented `Eq` for `Fullscreen`, `Theme`, and `UserAttentionType`. +- **Breaking:** `Window::set_cursor_grab` now accepts `CursorGrabMode` to control grabbing behavior. +- On Wayland, add support for `Window::set_cursor_position`. +- Fix on macOS `WindowBuilder::with_disallow_hidpi`, setting true or false by the user no matter the SO default value. +- `EventLoopBuilder::build` will now panic when the `EventLoop` is being created more than once. +- Added `From` for `WindowId` and `From` for `u64`. +- Added `MonitorHandle::refresh_rate_millihertz` to get monitor's refresh rate. +- **Breaking**, Replaced `VideoMode::refresh_rate` with `VideoMode::refresh_rate_millihertz` providing better precision. +- On Web, add `with_prevent_default` and `with_focusable` to `WindowBuilderExtWebSys` to control whether events should be propagated. +- On Windows, fix focus events being sent to inactive windows. +- **Breaking**, update `raw-window-handle` to `v0.5` and implement `HasRawDisplayHandle` for `Window` and `EventLoopWindowTarget`. +- On X11, add function `register_xlib_error_hook` into `winit::platform::unix` to subscribe for errors comming from Xlib. +- On Android, upgrade `ndk` and `ndk-glue` dependencies to the recently released `0.7.0`. +- All platforms can now be relied on to emit a `Resumed` event. Applications are recommended to lazily initialize graphics state and windows on first resume for portability. +- **Breaking:**: Reverse horizontal scrolling sign in `MouseScrollDelta` to match the direction of vertical scrolling. A positive X value now means moving the content to the right. The meaning of vertical scrolling stays the same: a positive Y value means moving the content down. # 0.26.1 (2022-01-05) @@ -45,7 +109,6 @@ And please only add new entries to the top of this list, right below the `# Unre - On X11, add mappings for numpad comma, numpad enter, numlock and pause. - On macOS, fix Pinyin IME input by reverting a change that intended to improve IME. - On Windows, fix a crash with transparent windows on Windows 11. -- **Breaking:**: Reverse horizontal scrolling sign in `MouseScrollDelta` to match the direction of vertical scrolling. A positive X value now means moving the content to the right. The meaning of vertical scrolling stays the same: a positive Y value means moving the content down. # 0.26.0 (2021-12-01) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f90b74dff..5c287f14de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ your description of the issue as detailed as possible: When making a code contribution to winit, before opening your pull request, please make sure that: +- your patch builds with Winit's minimal supported rust version - Rust 1.57.0. - you tested your modifications on all the platforms impacted, or if not possible detail which platforms were not tested, and what should be tested, so that a maintainer or another contributor can test them - you updated any relevant documentation in winit @@ -43,10 +44,9 @@ Once your PR is deemed ready, the merging maintainer will take care of resolving ## Maintainers & Testers -The current [list of testers and contributors](https://github.com/rust-windowing/winit/wiki/Testers-and-Contributors) -can be found on the Wiki. +The current maintainers are listed in the [CODEOWNERS](.github/CODEOWNERS) file. -If you are interested in contributing or testing on a platform, please add yourself to that table! +If you are interested in being pinged when testing is needed for a certain platform, please add yourself to the [Testers and Contributors](https://github.com/rust-windowing/winit/wiki/Testers-and-Contributors) table! ## Making a new release diff --git a/Cargo.toml b/Cargo.toml index 0fee93fe47..487a84d52d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.26.1" +version = "0.27.2" authors = ["The winit contributors", "Pierre Krieger "] description = "Cross-platform window creation library." edition = "2021" @@ -10,24 +10,45 @@ readme = "README.md" repository = "https://github.com/rust-windowing/winit" documentation = "https://docs.rs/winit" categories = ["gui"] +rust-version = "1.57.0" [package.metadata.docs.rs] features = ["serde"] default-target = "x86_64-unknown-linux-gnu" -targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "i686-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "wasm32-unknown-unknown"] +# These are all tested in CI +targets = [ + # Windows + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + # macOS + "x86_64-apple-darwin", + # Unix (X11 & Wayland) + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # iOS + "x86_64-apple-ios", + # Android + "aarch64-linux-android", + # WebAssembly + "wasm32-unknown-unknown", +] [features] -default = ["x11", "wayland", "wayland-dlopen"] +default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"] wayland = ["wayland-client", "wayland-protocols", "sctk"] wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] +wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] [dependencies] instant = { version = "0.1", features = ["wasm-bindgen"] } -lazy_static = "1" +once_cell = "1.12" log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } -raw-window-handle = "0.4.2" +raw_window_handle = { package = "raw-window-handle", version = "0.5" } +raw_window_handle_04 = { package = "raw-window-handle", version = "0.4" } bitflags = "1" mint = { version = "0.5.6", optional = true } @@ -36,9 +57,9 @@ image = { version = "0.24.0", default-features = false, features = ["png"] } simple_logger = "2.1.0" [target.'cfg(target_os = "android")'.dependencies] -ndk = "0.6" -ndk-sys = "0.3" -ndk-glue = "0.6" +# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 +ndk = "0.7.0" +ndk-glue = "0.7.0" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] objc = "0.2.7" @@ -49,16 +70,11 @@ core-foundation = "0.9" core-graphics = "0.22" dispatch = "0.2.0" -[target.'cfg(target_os = "macos")'.dependencies.core-video-sys] -version = "0.1.4" -default_features = false -features = ["display_link"] - [target.'cfg(target_os = "windows")'.dependencies] parking_lot = "0.12" [target.'cfg(target_os = "windows")'.dependencies.windows-sys] -version = "0.33" +version = "0.36" features = [ "Win32_Devices_HumanInterfaceDevice", "Win32_Foundation", @@ -87,9 +103,10 @@ features = [ ] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -wayland-client = { version = "0.29", default_features = false, features = ["use_system_lib"], optional = true } -wayland-protocols = { version = "0.29", features = [ "staging_protocols"], optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.15.4", default_features = false, features = ["calloop"], optional = true } +wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } +wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.16.0", default_features = false, features = ["calloop"], optional = true } +sctk-adwaita = { version = "0.5.1", default_features = false, optional = true } mio = { version = "0.8", features = ["os-ext"], optional = true } x11-dl = { version = "2.18.5", optional = true } percent-encoding = { version = "2.0", optional = true } diff --git a/FEATURES.md b/FEATURES.md index 15aacb6b6b..3610f4b793 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -100,7 +100,8 @@ If your PR makes notable changes to Winit's features, please update this section ### Input Handling - **Mouse events**: Generating mouse events associated with pointer motion, click, and scrolling events. - **Mouse set location**: Forcibly changing the location of the pointer. -- **Cursor grab**: Locking the cursor so it cannot exit the client area of a window. +- **Cursor locking**: Locking the cursor inside the window so it cannot move. +- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them. - **Cursor icon**: Changing the cursor icon, or hiding the cursor. - **Cursor hittest**: Handle or ignore mouse events for a window. - **Touch events**: Single-touch events. @@ -173,7 +174,7 @@ Legend: |Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |✔️ | |Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**| |Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |**N/A**| -|Window decorations |✔️ |✔️ |✔️ |▢[#306] |**N/A**|**N/A**|**N/A**| +|Window decorations |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| |Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| |Window resizing |✔️ |▢[#219]|✔️ |▢[#306] |**N/A**|**N/A**|✔️ | |Window resize increments |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| @@ -197,8 +198,9 @@ Legend: |Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | |Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ | -|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A**| -|Cursor grab |✔️ |▢[#165] |▢[#242] |✔️ |**N/A**|**N/A**|✔️ | +|Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**| +|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ | +|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | |Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | diff --git a/HALL_OF_CHAMPIONS.md b/HALL_OF_CHAMPIONS.md index 111f368289..eab9433be4 100644 --- a/HALL_OF_CHAMPIONS.md +++ b/HALL_OF_CHAMPIONS.md @@ -15,9 +15,12 @@ future endeavors: vastly more sustainable era of winit. * [@goddessfreya]: For selflessly taking over maintainership of glutin, and her stellar dedication to improving both winit and glutin. +* [@ArturKovacs]: For consistently maintaining the macOS backend, and his + immense involvement in designing and implementing the new keyboard API. [@tomaka]: https://github.com/tomaka [@vberger]: https://github.com/vberger [@francesca64]: https://github.com/francesca64 [@Osspial]: https://github.com/Osspial [@goddessfreya]: https://github.com/goddessfreya +[@ArturKovacs]: https://github.com/ArturKovacs diff --git a/README.md b/README.md index 88e44f80f8..abef735b27 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ```toml [dependencies] -winit = "0.26.1" +winit = "0.27.2" ``` ## [Documentation](https://docs.rs/winit) @@ -69,6 +69,14 @@ Winit provides the following features, which can be enabled in your `Cargo.toml` ### Platform-specific usage +#### Wayland + +Note that windows don't appear on Wayland until you draw/present to them. + +`winit` doesn't do drawing, try the examples in [`glutin`] instead. + +[`glutin`]: https://github.com/rust-windowing/glutin + #### WebAssembly To run the web example: `cargo run-wasm --example web` @@ -93,16 +101,16 @@ book]. This library makes use of the [ndk-rs](https://github.com/rust-windowing/android-ndk-rs) crates, refer to that repo for more documentation. -The `ndk_glue` version needs to match the version used by `winit`. Otherwise, the application will not start correctly as `ndk_glue`'s internal NativeActivity static is not the same due to version mismatch. +The `ndk-glue` version needs to match the version used by `winit`. Otherwise, the application will not start correctly as `ndk-glue`'s internal `NativeActivity` static is not the same due to version mismatch. -`ndk_glue` <-> `winit` version comparison compatibility: +`winit` compatibility table with `ndk-glue`: -| winit | ndk_glue | +| winit | ndk-glue | | :---: | :------------------: | -| 0.24 | `ndk_glue = "0.2.0"` | -| 0.25 | `ndk_glue = "0.3.0"` | -| 0.26 | `ndk_glue = "0.5.0"` | -| 0.27 | `ndk_glue = "0.6.0"` | +| 0.24 | `ndk-glue = "0.2.0"` | +| 0.25 | `ndk-glue = "0.3.0"` | +| 0.26 | `ndk-glue = "0.5.0"` | +| 0.27 | `ndk-glue = "0.7.0"` | Running on an Android device needs a dynamic system library, add this to Cargo.toml: @@ -121,3 +129,16 @@ fn main() { ``` And run the application with `cargo apk run --example request_redraw_threaded` + +#### MacOS + +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::NewEvents(StartCause::Init)`. + +[#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 diff --git a/examples/control_flow.rs b/examples/control_flow.rs index 0946dbe794..94926d7764 100644 --- a/examples/control_flow.rs +++ b/examples/control_flow.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use std::{thread, time}; use simple_logger::SimpleLogger; diff --git a/examples/cursor.rs b/examples/cursor.rs index 86bed427c8..fdef7ef971 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyboardInput, WindowEvent}, diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 979fed65e1..cff77885ea 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -1,8 +1,10 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, WindowEvent}, event_loop::EventLoop, - window::WindowBuilder, + window::{CursorGrabMode, WindowBuilder}, }; fn main() { @@ -32,11 +34,23 @@ fn main() { .. } => { use winit::event::VirtualKeyCode::*; - match key { - Escape => control_flow.set_exit(), - G => window.set_cursor_grab(!modifiers.shift()).unwrap(), - H => window.set_cursor_visible(modifiers.shift()), - _ => (), + let result = match key { + Escape => { + control_flow.set_exit(); + Ok(()) + } + G => window.set_cursor_grab(CursorGrabMode::Confined), + L => window.set_cursor_grab(CursorGrabMode::Locked), + A => window.set_cursor_grab(CursorGrabMode::None), + H => { + window.set_cursor_visible(modifiers.shift()); + Ok(()) + } + _ => Ok(()), + }; + + if let Err(err) = result { + println!("error: {}", err); } } WindowEvent::ModifiersChanged(m) => modifiers = m, diff --git a/examples/custom_events.rs b/examples/custom_events.rs index 5ba5d4e341..c1b7133de6 100644 --- a/examples/custom_events.rs +++ b/examples/custom_events.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + #[cfg(not(target_arch = "wasm32"))] fn main() { use simple_logger::SimpleLogger; diff --git a/examples/drag_window.rs b/examples/drag_window.rs index 753958c8f1..813e9b00c9 100644 --- a/examples/drag_window.rs +++ b/examples/drag_window.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{ diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index b5a9ebb2de..6bfda4c2c9 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,37 +1,44 @@ -use std::io::{stdin, stdout, Write}; +#![allow(clippy::single_match)] use simple_logger::SimpleLogger; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; use winit::event_loop::EventLoop; -use winit::monitor::{MonitorHandle, VideoMode}; use winit::window::{Fullscreen, WindowBuilder}; fn main() { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); - print!("Please choose the fullscreen mode: (1) exclusive, (2) borderless: "); - stdout().flush().unwrap(); - - let mut num = String::new(); - stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().expect("Please enter a number"); - - let fullscreen = Some(match num { - 1 => Fullscreen::Exclusive(prompt_for_video_mode(&prompt_for_monitor(&event_loop))), - 2 => Fullscreen::Borderless(Some(prompt_for_monitor(&event_loop))), - _ => panic!("Please enter a valid number"), - }); - let mut decorations = true; + let mut minimized = false; let window = WindowBuilder::new() .with_title("Hello world!") - .with_fullscreen(fullscreen.clone()) .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { + let mut monitor_index = 0; + let mut monitor = event_loop + .available_monitors() + .next() + .expect("no monitor found!"); + println!("Monitor: {:?}", monitor.name()); + + let mut mode_index = 0; + let mut mode = monitor.video_modes().next().expect("no mode found"); + println!("Mode: {}", mode); + + println!("Keys:"); + println!("- Esc\tExit"); + println!("- F\tToggle exclusive fullscreen mode"); + println!("- B\tToggle borderless mode"); + println!("- S\tNext screen"); + println!("- M\tNext mode for this screen"); + println!("- D\tToggle window decorations"); + println!("- X\tMaximize window"); + println!("- Z\tMinimize window"); + + event_loop.run(move |event, elwt, control_flow| { control_flow.set_wait(); match event { @@ -41,29 +48,60 @@ fn main() { input: KeyboardInput { virtual_keycode: Some(virtual_code), - state, + state: ElementState::Pressed, .. }, .. - } => match (virtual_code, state) { - (VirtualKeyCode::Escape, _) => control_flow.set_exit(), - (VirtualKeyCode::F, ElementState::Pressed) => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); + } => match virtual_code { + VirtualKeyCode::Escape => control_flow.set_exit(), + VirtualKeyCode::F | VirtualKeyCode::B if window.fullscreen().is_some() => { + window.set_fullscreen(None); + } + VirtualKeyCode::F => { + let fullscreen = Some(Fullscreen::Exclusive(mode.clone())); + println!("Setting mode: {:?}", fullscreen); + window.set_fullscreen(fullscreen); + } + VirtualKeyCode::B => { + let fullscreen = Some(Fullscreen::Borderless(Some(monitor.clone()))); + println!("Setting mode: {:?}", fullscreen); + window.set_fullscreen(fullscreen); + } + VirtualKeyCode::S => { + monitor_index += 1; + if let Some(mon) = elwt.available_monitors().nth(monitor_index) { + monitor = mon; } else { - window.set_fullscreen(fullscreen.clone()); + monitor_index = 0; + monitor = elwt.available_monitors().next().expect("no monitor found!"); } + println!("Monitor: {:?}", monitor.name()); + + mode_index = 0; + mode = monitor.video_modes().next().expect("no mode found"); + println!("Mode: {}", mode); } - (VirtualKeyCode::S, ElementState::Pressed) => { - println!("window.fullscreen {:?}", window.fullscreen()); + VirtualKeyCode::M => { + mode_index += 1; + if let Some(m) = monitor.video_modes().nth(mode_index) { + mode = m; + } else { + mode_index = 0; + mode = monitor.video_modes().next().expect("no mode found"); + } + println!("Mode: {}", mode); } - (VirtualKeyCode::M, ElementState::Pressed) => { + VirtualKeyCode::D => { + decorations = !decorations; + window.set_decorations(decorations); + } + VirtualKeyCode::X => { let is_maximized = window.is_maximized(); window.set_maximized(!is_maximized); } - (VirtualKeyCode::D, ElementState::Pressed) => { - decorations = !decorations; - window.set_decorations(decorations); + VirtualKeyCode::Z => { + minimized = !minimized; + window.set_minimized(minimized); } _ => (), }, @@ -73,46 +111,3 @@ fn main() { } }); } - -// Enumerate monitors and prompt user to choose one -fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { - for (num, monitor) in event_loop.available_monitors().enumerate() { - println!("Monitor #{}: {:?}", num, monitor.name()); - } - - print!("Please write the number of the monitor to use: "); - stdout().flush().unwrap(); - - let mut num = String::new(); - stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().expect("Please enter a number"); - let monitor = event_loop - .available_monitors() - .nth(num) - .expect("Please enter a valid ID"); - - println!("Using {:?}", monitor.name()); - - monitor -} - -fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { - for (i, video_mode) in monitor.video_modes().enumerate() { - println!("Video mode #{}: {}", i, video_mode); - } - - print!("Please write the number of the video mode to use: "); - stdout().flush().unwrap(); - - let mut num = String::new(); - stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().expect("Please enter a number"); - let video_mode = monitor - .video_modes() - .nth(num) - .expect("Please enter a valid ID"); - - println!("Using {}", video_mode); - - video_mode -} diff --git a/examples/handling_close.rs b/examples/handling_close.rs index 2fd2d084b7..1fe4ad3708 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{Event, KeyboardInput, WindowEvent}, diff --git a/examples/ime.rs b/examples/ime.rs new file mode 100644 index 0000000000..bdf67a8624 --- /dev/null +++ b/examples/ime.rs @@ -0,0 +1,99 @@ +#![allow(clippy::single_match)] + +use log::LevelFilter; +use simple_logger::SimpleLogger; +use winit::{ + dpi::PhysicalPosition, + event::{ElementState, Event, Ime, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new() + .with_level(LevelFilter::Trace) + .init() + .unwrap(); + + println!("IME position will system default"); + println!("Click to set IME position to cursor's"); + println!("Press F2 to toggle IME. See the documentation of `set_ime_allowed` for more info"); + + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_inner_size(winit::dpi::LogicalSize::new(256f64, 128f64)) + .build(&event_loop) + .unwrap(); + + let mut ime_allowed = true; + window.set_ime_allowed(ime_allowed); + + let mut may_show_ime = false; + let mut cursor_position = PhysicalPosition::new(0.0, 0.0); + let mut ime_pos = PhysicalPosition::new(0.0, 0.0); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + cursor_position = position; + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Released, + .. + }, + .. + } => { + println!( + "Setting ime position to {}, {}", + cursor_position.x, cursor_position.y + ); + ime_pos = cursor_position; + if may_show_ime { + window.set_ime_position(ime_pos); + } + } + Event::WindowEvent { + event: WindowEvent::Ime(event), + .. + } => { + println!("{:?}", event); + may_show_ime = event != Ime::Disabled; + if may_show_ime { + window.set_ime_position(ime_pos); + } + } + Event::WindowEvent { + event: WindowEvent::ReceivedCharacter(ch), + .. + } => { + println!("ch: {:?}", ch); + } + Event::WindowEvent { + event: WindowEvent::KeyboardInput { input, .. }, + .. + } => { + println!("key: {:?}", input); + + if input.state == ElementState::Pressed + && input.virtual_keycode == Some(VirtualKeyCode::F2) + { + ime_allowed = !ime_allowed; + window.set_ime_allowed(ime_allowed); + println!("\nIME: {}\n", ime_allowed); + } + } + _ => (), + } + }); +} diff --git a/examples/min_max_size.rs b/examples/min_max_size.rs deleted file mode 100644 index 56877999e0..0000000000 --- a/examples/min_max_size.rs +++ /dev/null @@ -1,30 +0,0 @@ -use simple_logger::SimpleLogger; -use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; - -fn main() { - SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); - - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0))); - window.set_max_inner_size(Some(LogicalSize::new(800.0, 400.0))); - - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - println!("{:?}", event); - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => control_flow.set_exit(), - _ => (), - } - }); -} diff --git a/examples/minimize.rs b/examples/minimize.rs deleted file mode 100644 index 8af561eeaa..0000000000 --- a/examples/minimize.rs +++ /dev/null @@ -1,41 +0,0 @@ -extern crate winit; - -use simple_logger::SimpleLogger; -use winit::event::{Event, VirtualKeyCode, WindowEvent}; -use winit::event_loop::EventLoop; -use winit::window::WindowBuilder; - -fn main() { - SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); - - let window = WindowBuilder::new() - .with_title("A fantastic window!") - .build(&event_loop) - .unwrap(); - - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => control_flow.set_exit(), - - // Keyboard input event to handle minimize via a hotkey - Event::WindowEvent { - event: WindowEvent::KeyboardInput { input, .. }, - window_id, - } => { - if window_id == window.id() { - // Pressing the 'M' key will minimize the window - if input.virtual_keycode == Some(VirtualKeyCode::M) { - window.set_minimized(true); - } - } - } - _ => (), - } - }); -} diff --git a/examples/monitor_list.rs b/examples/monitor_list.rs index 9c8b77e61c..fc52fa5e3f 100644 --- a/examples/monitor_list.rs +++ b/examples/monitor_list.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{event_loop::EventLoop, window::WindowBuilder}; diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index d6e665b1a2..c1d8f0cb99 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 9ed28cdbd6..9dc125079b 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + #[cfg(not(target_arch = "wasm32"))] fn main() { use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; @@ -7,7 +9,7 @@ fn main() { dpi::{PhysicalPosition, PhysicalSize, Position, Size}, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::EventLoop, - window::{CursorIcon, Fullscreen, WindowBuilder}, + window::{CursorGrabMode, CursorIcon, Fullscreen, WindowBuilder}, }; const WINDOW_COUNT: usize = 3; @@ -86,7 +88,21 @@ fn main() { } (false, _) => None, }), - G => window.set_cursor_grab(state).unwrap(), + L if state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::Locked) { + println!("error: {}", err); + } + } + G if state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::Confined) { + println!("error: {}", err); + } + } + G | L if !state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::None) { + println!("error: {}", err); + } + } H => window.set_cursor_visible(!state), I => { println!("Info:"); diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index c6bff90afc..de87962d04 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -1,8 +1,10 @@ +#![allow(clippy::single_match)] + use std::collections::HashMap; use simple_logger::SimpleLogger; use winit::{ - event::{ElementState, Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::EventLoop, window::Window, }; @@ -14,9 +16,12 @@ fn main() { let mut windows = HashMap::new(); for _ in 0..3 { let window = Window::new(&event_loop).unwrap(); + println!("Opened a new window: {:?}", window.id()); windows.insert(window.id(), window); } + println!("Press N to open a new window."); + event_loop.run(move |event, event_loop, control_flow| { control_flow.set_wait(); @@ -37,11 +42,14 @@ fn main() { input: KeyboardInput { state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::N), .. }, + is_synthetic: false, .. } => { let window = Window::new(event_loop).unwrap(); + println!("Opened a new window: {:?}", window.id()); windows.insert(window.id(), window); } _ => (), diff --git a/examples/request_redraw.rs b/examples/request_redraw.rs index 28275a29f6..93bb43983e 100644 --- a/examples/request_redraw.rs +++ b/examples/request_redraw.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, WindowEvent}, diff --git a/examples/request_redraw_threaded.rs b/examples/request_redraw_threaded.rs index f6fba32afb..2500a8c02c 100644 --- a/examples/request_redraw_threaded.rs +++ b/examples/request_redraw_threaded.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + #[cfg(not(target_arch = "wasm32"))] fn main() { use std::{thread, time}; @@ -28,10 +30,10 @@ fn main() { control_flow.set_wait(); match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), - _ => (), - }, + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => control_flow.set_exit(), Event::RedrawRequested(_) => { println!("\nredrawing!\n"); } diff --git a/examples/resizable.rs b/examples/resizable.rs index 07678756b9..9e438a51d9 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ dpi::LogicalSize, @@ -14,7 +16,9 @@ fn main() { let window = WindowBuilder::new() .with_title("Hit space to toggle resizability.") - .with_inner_size(LogicalSize::new(400.0, 200.0)) + .with_inner_size(LogicalSize::new(600.0, 300.0)) + .with_min_inner_size(LogicalSize::new(400.0, 200.0)) + .with_max_inner_size(LogicalSize::new(800.0, 400.0)) .with_resizable(resizable) .build(&event_loop) .unwrap(); diff --git a/examples/set_ime_position.rs b/examples/set_ime_position.rs deleted file mode 100644 index 5f96a6fe6c..0000000000 --- a/examples/set_ime_position.rs +++ /dev/null @@ -1,53 +0,0 @@ -use simple_logger::SimpleLogger; -use winit::{ - dpi::PhysicalPosition, - event::{ElementState, Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; - -fn main() { - SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); - - let window = WindowBuilder::new().build(&event_loop).unwrap(); - window.set_title("A fantastic window!"); - - println!("Ime position will system default"); - println!("Click to set ime position to cursor's"); - - let mut cursor_position = PhysicalPosition::new(0.0, 0.0); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: WindowEvent::CursorMoved { position, .. }, - .. - } => { - cursor_position = position; - } - Event::WindowEvent { - event: - WindowEvent::MouseInput { - state: ElementState::Released, - .. - }, - .. - } => { - println!( - "Setting ime position to {}, {}", - cursor_position.x, cursor_position.y - ); - window.set_ime_position(cursor_position); - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - control_flow.set_exit(); - } - _ => (), - } - }); -} diff --git a/examples/timer.rs b/examples/timer.rs index 6ea4fc09ea..c312460b22 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use instant::Instant; use std::time::Duration; diff --git a/examples/touchpad_gestures.rs b/examples/touchpad_gestures.rs new file mode 100644 index 0000000000..85d9e967f8 --- /dev/null +++ b/examples/touchpad_gestures.rs @@ -0,0 +1,43 @@ +use simple_logger::SimpleLogger; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Touchpad gestures") + .build(&event_loop) + .unwrap(); + + println!("Only supported on macOS at the moment."); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::TouchpadMagnify { delta, .. } => { + if delta > 0.0 { + println!("Zoomed in {}", delta); + } else { + println!("Zoomed out {}", delta); + } + } + WindowEvent::TouchpadRotate { delta, .. } => { + if delta > 0.0 { + println!("Rotated counterclockwise {}", delta); + } else { + println!("Rotated clockwise {}", delta); + } + } + _ => (), + } + } + }); +} diff --git a/examples/transparent.rs b/examples/transparent.rs index bed8de72b0..9080cbef12 100644 --- a/examples/transparent.rs +++ b/examples/transparent.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, diff --git a/examples/video_modes.rs b/examples/video_modes.rs index 341f43855b..afc4ad6be6 100644 --- a/examples/video_modes.rs +++ b/examples/video_modes.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::event_loop::EventLoop; diff --git a/examples/web.rs b/examples/web.rs index 600a3d8d74..71e5dfd60a 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use winit::{ event::{Event, WindowEvent}, event_loop::EventLoop, @@ -43,6 +45,7 @@ mod wasm { pub fn run() { console_log::init_with_level(log::Level::Debug).expect("error initializing logger"); + #[allow(clippy::main_recursion)] super::main(); } diff --git a/examples/window.rs b/examples/window.rs index 0d0fd74a46..23a633d2ed 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, diff --git a/examples/window_debug.rs b/examples/window_debug.rs index 8c4f42c366..47a130178c 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -1,10 +1,12 @@ +#![allow(clippy::single_match)] + // This example is used by developers to test various window functions. use simple_logger::SimpleLogger; use winit::{ dpi::{LogicalSize, PhysicalSize}, event::{DeviceEvent, ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::EventLoop, + event_loop::{DeviceEventFilter, EventLoop}, window::{Fullscreen, WindowBuilder}, }; @@ -30,6 +32,8 @@ fn main() { let mut minimized = false; let mut visible = true; + event_loop.set_device_event_filter(DeviceEventFilter::Never); + event_loop.run(move |event, _, control_flow| { control_flow.set_wait(); @@ -58,61 +62,63 @@ fn main() { _ => (), }, Event::WindowEvent { - event: WindowEvent::KeyboardInput { input, .. }, + event: + WindowEvent::KeyboardInput { + input: + KeyboardInput { + virtual_keycode: Some(key), + state: ElementState::Pressed, + .. + }, + .. + }, .. - } => match input { - KeyboardInput { - virtual_keycode: Some(key), - state: ElementState::Pressed, - .. - } => match key { - VirtualKeyCode::E => { - fn area(size: PhysicalSize) -> u32 { - size.width * size.height - } - - let monitor = window.current_monitor().unwrap(); - if let Some(mode) = monitor - .video_modes() - .max_by(|a, b| area(a.size()).cmp(&area(b.size()))) - { - window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); - } else { - eprintln!("no video modes available"); - } - } - VirtualKeyCode::F => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - let monitor = window.current_monitor(); - window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); - } - } - VirtualKeyCode::P => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } - } - VirtualKeyCode::M => { - minimized = !minimized; - window.set_minimized(minimized); + } => match key { + VirtualKeyCode::E => { + fn area(size: PhysicalSize) -> u32 { + size.width * size.height } - VirtualKeyCode::Q => { - control_flow.set_exit(); + + let monitor = window.current_monitor().unwrap(); + if let Some(mode) = monitor + .video_modes() + .max_by(|a, b| area(a.size()).cmp(&area(b.size()))) + { + window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); + } else { + eprintln!("no video modes available"); } - VirtualKeyCode::V => { - visible = !visible; - window.set_visible(visible); + } + VirtualKeyCode::F => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); } - VirtualKeyCode::X => { - let is_maximized = window.is_maximized(); - window.set_maximized(!is_maximized); + } + VirtualKeyCode::P => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); } - _ => (), - }, + } + VirtualKeyCode::M => { + minimized = !minimized; + window.set_minimized(minimized); + } + VirtualKeyCode::Q => { + control_flow.set_exit(); + } + VirtualKeyCode::V => { + visible = !visible; + window.set_visible(visible); + } + VirtualKeyCode::X => { + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); + } _ => (), }, Event::WindowEvent { diff --git a/examples/window_icon.rs b/examples/window_icon.rs index c120ed904b..ed98b3d577 100644 --- a/examples/window_icon.rs +++ b/examples/window_icon.rs @@ -1,4 +1,5 @@ -extern crate image; +#![allow(clippy::single_match)] + use std::path::Path; use simple_logger::SimpleLogger; diff --git a/examples/window_run_return.rs b/examples/window_run_return.rs index 81ddc5a225..e6b3c66211 100644 --- a/examples/window_run_return.rs +++ b/examples/window_run_return.rs @@ -1,3 +1,5 @@ +#![allow(clippy::single_match)] + // Limit this example to only compatible platforms. #[cfg(any( target_os = "windows", @@ -6,7 +8,8 @@ target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd" + target_os = "openbsd", + target_os = "android", ))] fn main() { use std::{thread::sleep, time::Duration}; @@ -57,7 +60,7 @@ fn main() { } } -#[cfg(any(target_os = "ios", target_os = "android", target_arch = "wasm32"))] +#[cfg(any(target_os = "ios", target_arch = "wasm32"))] fn main() { println!("This platform doesn't support run_return."); } diff --git a/src/dpi.rs b/src/dpi.rs index be6d8177f8..7d372d8899 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -35,8 +35,9 @@ //! //! ### Position and Size types //! -//! Winit's `Physical(Position|Size)` types correspond with the actual pixels on the device, and the -//! `Logical(Position|Size)` types correspond to the physical pixels divided by the scale factor. +//! Winit's [`PhysicalPosition`] / [`PhysicalSize`] types correspond with the actual pixels on the +//! device, and the [`LogicalPosition`] / [`LogicalSize`] types correspond to the physical pixels +//! divided by the scale factor. //! All of Winit's functions return physical types, but can take either logical or physical //! coordinates as input, allowing you to use the most convenient coordinate system for your //! particular application. @@ -46,19 +47,18 @@ //! floating precision when necessary (e.g. logical sizes for fractional scale factors and touch //! input). If `P` is a floating-point type, please do not cast the values with `as {int}`. Doing so //! will truncate the fractional part of the float, rather than properly round to the nearest -//! integer. Use the provided `cast` function or `From`/`Into` conversions, which handle the +//! integer. Use the provided `cast` function or [`From`]/[`Into`] conversions, which handle the //! rounding properly. Note that precision loss will still occur when rounding from a float to an //! int, although rounding lessens the problem. //! //! ### Events //! -//! Winit will dispatch a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged) -//! event whenever a window's scale factor has changed. This can happen if the user drags their -//! window from a standard-resolution monitor to a high-DPI monitor, or if the user changes their -//! DPI settings. This gives you a chance to rescale your application's UI elements and adjust how -//! the platform changes the window's size to reflect the new scale factor. If a window hasn't -//! received a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged) event, -//! then its scale factor can be found by calling [window.scale_factor()]. +//! Winit will dispatch a [`ScaleFactorChanged`] event whenever a window's scale factor has changed. +//! This can happen if the user drags their window from a standard-resolution monitor to a high-DPI +//! monitor, or if the user changes their DPI settings. This gives you a chance to rescale your +//! application's UI elements and adjust how the platform changes the window's size to reflect the new +//! scale factor. If a window hasn't received a [`ScaleFactorChanged`] event, then its scale factor +//! can be found by calling [`window.scale_factor()`]. //! //! ## How is the scale factor calculated? //! @@ -77,7 +77,7 @@ //! currently uses a three-pronged approach: //! + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable, if present. //! + If not present, use the value set in `Xft.dpi` in Xresources. -//! + Otherwise, calcuate the scale factor based on the millimeter monitor dimensions provided by XRandR. +//! + Otherwise, calculate the scale factor based on the millimeter monitor dimensions 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 @@ -93,9 +93,11 @@ //! In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by //! both the screen scaling and the browser zoom level and can go below `1.0`. //! +//! //! [points]: https://en.wikipedia.org/wiki/Point_(typography) //! [picas]: https://en.wikipedia.org/wiki/Pica_(typography) -//! [window.scale_factor()]: crate::window::Window::scale_factor +//! [`ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged +//! [`window.scale_factor()`]: crate::window::Window::scale_factor //! [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows //! [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html //! [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ @@ -509,6 +511,29 @@ impl Size { Size::Logical(size) => size.to_physical(scale_factor), } } + + pub fn clamp>(input: S, min: S, max: S, scale_factor: f64) -> Size { + let (input, min, max) = ( + input.into().to_physical::(scale_factor), + min.into().to_physical::(scale_factor), + max.into().to_physical::(scale_factor), + ); + + let clamp = |input: f64, min: f64, max: f64| { + if input < min { + min + } else if input > max { + max + } else { + input + } + }; + + let width = clamp(input.width, min.width, max.width); + let height = clamp(input.height, min.height, max.height); + + PhysicalSize::new(width, height).into() + } } impl From> for Size { diff --git a/src/event.rs b/src/event.rs index 5afb0ebfbd..f52bcd8bb0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,10 +1,10 @@ -//! The `Event` enum and assorted supporting types. +//! The [`Event`] enum and assorted supporting types. //! -//! These are sent to the closure given to [`EventLoop::run(...)`][event_loop_run], where they get +//! These are sent to the closure given to [`EventLoop::run(...)`], where they get //! processed and used to modify the program state. For more details, see the root-level documentation. //! //! Some of these events represent different "parts" of a traditional event-handling loop. You could -//! approximate the basic ordering loop of [`EventLoop::run(...)`][event_loop_run] like this: +//! approximate the basic ordering loop of [`EventLoop::run(...)`] like this: //! //! ```rust,ignore //! let mut control_flow = ControlFlow::Poll; @@ -29,13 +29,16 @@ //! event_handler(LoopDestroyed, ..., &mut control_flow); //! ``` //! -//! This leaves out timing details like `ControlFlow::WaitUntil` but hopefully +//! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully //! describes what happens in what order. //! -//! [event_loop_run]: crate::event_loop::EventLoop::run +//! [`EventLoop::run(...)`]: crate::event_loop::EventLoop::run +//! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil use instant::Instant; use std::path::PathBuf; +#[cfg(doc)] +use crate::window::Window; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, platform_impl, @@ -71,9 +74,107 @@ pub enum Event<'a, T: 'static> { UserEvent(T), /// Emitted when the application has been suspended. + /// + /// # Portability + /// + /// Not all platforms support the notion of suspending applications, and there may be no + /// technical way to guarantee being able to emit a `Suspended` event if the OS has + /// no formal application lifecycle (currently only Android and iOS do). For this reason, + /// Winit does not currently try to emit pseudo `Suspended` events before the application + /// quits on platforms without an application lifecycle. + /// + /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] events. + /// + /// Also see [`Resumed`] notes. + /// + /// ## Android + /// + /// On Android, the `Suspended` event is only sent when the application's associated + /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`] + /// lifecycle event but there may technically be a discrepancy. + /// + /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause() + /// + /// Applications that need to run on Android should assume their [`SurfaceView`] has been + /// destroyed, which indirectly invalidates any existing render surfaces that may have been + /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]). + /// + /// After being `Suspended` on Android applications must drop all render surfaces before + /// the event callback completes, which may be re-created when the application is next [`Resumed`]. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Suspended` event is currently emitted in response to an + /// [`applicationWillResignActive`] callback which means that the application is + /// about to transition from the active to inactive state (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// [`Resumed`]: Self::Resumed Suspended, /// Emitted when the application has been resumed. + /// + /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a + /// formal suspend/resume lifecycle. For systems without a standard suspend/resume lifecycle + /// the `Resumed` event is always emitted after the [`NewEvents(StartCause::Init)`][StartCause::Init] + /// event. + /// + /// # Portability + /// + /// It's recommended that applications should only initialize their graphics context and create + /// a window after they have received their first `Resumed` event. Some systems + /// (specifically Android) won't allow applications to create a render surface until they are + /// resumed. + /// + /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` events. + /// + /// Also see [`Suspended`] notes. + /// + /// ## Android + /// + /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is + /// expected to closely correlate with the [`onResume`] lifecycle event but there may technically + /// be a discrepancy. + /// + /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume() + /// + /// Applications that need to run on Android must wait until they have been `Resumed` + /// before they will be able to create a render surface (such as an `EGLSurface`, + /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a + /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their + /// render surfaces are invalid and should be dropped. + /// + /// Also see [`Suspended`] notes. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`] + /// callback which means the application is "active" (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// [`Suspended`]: Self::Suspended Resumed, /// Emitted when all of the event loop's input events have been processed and redraw processing @@ -88,27 +189,30 @@ pub enum Event<'a, T: 'static> { /// can render here unconditionally for simplicity. MainEventsCleared, - /// Emitted after `MainEventsCleared` when a window should be redrawn. + /// Emitted after [`MainEventsCleared`] when a window should be redrawn. /// /// This gets triggered in two scenarios: /// - The OS has performed an operation that's invalidated the window's contents (such as /// resizing the window). - /// - The application has explicitly requested a redraw via - /// [`Window::request_redraw`](crate::window::Window::request_redraw). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. /// /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests /// into a single event, to help avoid duplicating rendering work. /// /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless /// something changes, like most non-game GUIs. + /// + /// [`MainEventsCleared`]: Self::MainEventsCleared RedrawRequested(WindowId), - /// Emitted after all `RedrawRequested` events have been processed and control flow is about to + /// Emitted after all [`RedrawRequested`] events have been processed and control flow is about to /// be taken away from the program. If there are no `RedrawRequested` events, it is emitted /// immediately after `MainEventsCleared`. /// /// This event is useful for doing any cleanup or bookkeeping work after all the rendering /// tasks have been completed. + /// + /// [`RedrawRequested`]: Self::RedrawRequested RedrawEventsCleared, /// Emitted when the event loop is being shut down. @@ -183,9 +287,11 @@ impl<'a, T> Event<'a, T> { /// Describes the reason the event loop is resuming. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StartCause { - /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the /// moment the timeout was requested and the requested resume time. The actual resume time is /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil ResumeTimeReached { start: Instant, requested_resume: Instant, @@ -199,20 +305,26 @@ pub enum StartCause { }, /// Sent if the event loop is being resumed after the loop's control flow was set to - /// `ControlFlow::Poll`. + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll Poll, /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. Init, } -/// Describes an event from a `Window`. +/// Describes an event from a [`Window`]. #[derive(Debug, PartialEq)] pub enum WindowEvent<'a> { /// The size of the window has changed. Contains the client area's new dimensions. Resized(PhysicalSize), /// The position of the window has changed. Contains the window's new position. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Unsupported. Moved(PhysicalPosition), /// The window has been requested to close. @@ -240,6 +352,8 @@ pub enum WindowEvent<'a> { HoveredFileCancelled, /// The window received a unicode character. + /// + /// See also the [`Ime`](Self::Ime) event for more complex character sequences. ReceivedCharacter(char), /// The window gained or lost focus. @@ -265,11 +379,21 @@ pub enum WindowEvent<'a> { /// The keyboard modifiers have changed. /// - /// Platform-specific behavior: - /// - **Web**: This API is currently unimplemented on the web. This isn't by design - it's an + /// ## Platform-specific + /// + /// - **Web:** This API is currently unimplemented on the web. This isn't by design - it's an /// issue, and it should get fixed - but it's the current state of the API. ModifiersChanged(ModifiersState), + /// An event from an input method. + /// + /// **Note:** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + Ime(Ime), + /// The cursor has moved on the window. CursorMoved { device_id: DeviceId, @@ -306,6 +430,34 @@ pub enum WindowEvent<'a> { modifiers: ModifiersState, }, + /// Touchpad magnification event with two-finger pinch gesture. + /// + /// Positive delta values indicate magnification (zooming in) and + /// negative delta values indicate shrinking (zooming out). + /// + /// ## Platform-specific + /// + /// - Only available on **macOS**. + TouchpadMagnify { + device_id: DeviceId, + delta: f64, + phase: TouchPhase, + }, + + /// Touchpad rotation event with two-finger rotation gesture. + /// + /// Positive delta values indicate rotation counterclockwise and + /// negative delta values indicate rotation clockwise. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS**. + TouchpadRotate { + device_id: DeviceId, + delta: f32, + phase: TouchPhase, + }, + /// Touchpad pressure event. /// /// At the moment, only supported on Apple forcetouch-capable macbooks. @@ -350,8 +502,19 @@ pub enum WindowEvent<'a> { /// Applications might wish to react to this to change the theme of the content of the window /// when the system changes the window theme. /// + /// ## Platform-specific + /// /// At the moment this is only supported on Windows. ThemeChanged(Theme), + + /// The window has been occluded (completely hidden from view). + /// + /// This is different to window visibility as it depends on whether the window is closed, + /// minimised, set invisible, or fully occluded by another window. + /// + /// Platform-specific behavior: + /// - **iOS / Android / Web / Wayland / Windows:** Unsupported. + Occluded(bool), } impl Clone for WindowEvent<'static> { @@ -376,7 +539,7 @@ impl Clone for WindowEvent<'static> { input: *input, is_synthetic: *is_synthetic, }, - + Ime(preedit_state) => Ime(preedit_state.clone()), ModifiersChanged(modifiers) => ModifiersChanged(*modifiers), #[allow(deprecated)] CursorMoved { @@ -418,6 +581,24 @@ impl Clone for WindowEvent<'static> { button: *button, modifiers: *modifiers, }, + TouchpadMagnify { + device_id, + delta, + phase, + } => TouchpadMagnify { + device_id: *device_id, + delta: *delta, + phase: *phase, + }, + TouchpadRotate { + device_id, + delta, + phase, + } => TouchpadRotate { + device_id: *device_id, + delta: *delta, + phase: *phase, + }, TouchpadPressure { device_id, pressure, @@ -441,6 +622,7 @@ impl Clone for WindowEvent<'static> { ScaleFactorChanged { .. } => { unreachable!("Static event can't be about scale factor changing") } + Occluded(occluded) => Occluded(*occluded), }; } } @@ -468,6 +650,7 @@ impl<'a> WindowEvent<'a> { is_synthetic, }), ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)), + Ime(event) => Some(Ime(event)), #[allow(deprecated)] CursorMoved { device_id, @@ -504,6 +687,24 @@ impl<'a> WindowEvent<'a> { button, modifiers, }), + TouchpadMagnify { + device_id, + delta, + phase, + } => Some(TouchpadMagnify { + device_id, + delta, + phase, + }), + TouchpadRotate { + device_id, + delta, + phase, + } => Some(TouchpadRotate { + device_id, + delta, + phase, + }), TouchpadPressure { device_id, pressure, @@ -525,6 +726,7 @@ impl<'a> WindowEvent<'a> { Touch(touch) => Some(Touch(touch)), ThemeChanged(theme) => Some(ThemeChanged(theme)), ScaleFactorChanged { .. } => None, + Occluded(occluded) => Some(Occluded(occluded)), } } } @@ -538,7 +740,7 @@ impl<'a> WindowEvent<'a> { pub struct DeviceId(pub(crate) platform_impl::DeviceId); impl DeviceId { - /// Returns a dummy `DeviceId`, useful for unit testing. + /// Returns a dummy id, useful for unit testing. /// /// # Safety /// @@ -567,7 +769,7 @@ pub enum DeviceEvent { /// Change in physical position of a pointing device. /// - /// This represents raw, unfiltered physical motion. Not to be confused with `WindowEvent::CursorMoved`. + /// This represents raw, unfiltered physical motion. Not to be confused with [`WindowEvent::CursorMoved`]. MouseMotion { /// (x, y) change in position in unspecified units. /// @@ -580,7 +782,7 @@ pub enum DeviceEvent { delta: MouseScrollDelta, }, - /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// Motion on some analog axis. This event will be reported for all arbitrary input devices /// that winit supports on this platform, including mouse devices. If the device is a mouse /// device then this will be reported alongside the MouseMotion event. Motion { @@ -627,6 +829,72 @@ pub struct KeyboardInput { pub modifiers: ModifiersState, } +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. +/// +/// This is also called a "composition event". +/// +/// Most keypresses using a latin-like keyboard layout simply generate a [`WindowEvent::ReceivedCharacter`]. +/// However, one couldn't possibly have a key for every single unicode character that the user might want to type +/// - so the solution operating systems employ is to allow the user to type these using _a sequence of keypresses_ instead. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the "accent key", and then +/// the character you want to apply the accent to. This will generate the following event sequence: +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some(0), Some(0)) +/// // Press "E" key +/// Ime::Commit("é") +/// ``` +/// +/// Additionally, certain input devices are configured to display a candidate box that allow the user to select the +/// desired character interactively. (To properly position this box, you must use [`Window::set_ime_position`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keybaord the following event +/// sequence could be obtained: +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some(1), Some(1)) +/// // Press "B" key +/// Ime::Preedit("a b", Some(3), Some(3)) +/// // Press left arrow key +/// Ime::Preedit("a b", Some(1), Some(1)) +/// // Press space key +/// Ime::Preedit("啊b", Some(3), Some(3)) +/// // Press space key +/// Ime::Commit("啊不") +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Ime { + /// Notifies when the IME was enabled. + /// + /// After getting this event you could receive [`Preedit`](Self::Preedit) and + /// [`Commit`](Self::Commit) events. You should also start performing IME related requests + /// like [`Window::set_ime_position`]. + Enabled, + + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. + /// + /// The cursor position is byte-wise indexed. + Preedit(String, Option<(usize, usize)>), + + /// Notifies when text should be inserted into the editor widget. + /// + /// Any pending [`Preedit`](Self::Preedit) must be cleared. + Commit(String), + + /// Notifies when the IME was disabled. + /// + /// After receiving this event you won't get any more [`Preedit`](Self::Preedit) or + /// [`Commit`](Self::Commit) events until the next [`Enabled`](Self::Enabled) event. You can + /// also stop issuing IME related requests like [`Window::set_ime_position`] and clear pending + /// preedit text. + Disabled, +} + /// Describes touch-screen input state. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -639,18 +907,18 @@ pub enum TouchPhase { /// Represents a touch event /// -/// Every time the user touches the screen, a new `Start` event with an unique -/// identifier for the finger is generated. When the finger is lifted, an `End` +/// Every time the user touches the screen, a new [`TouchPhase::Started`] event with an unique +/// identifier for the finger is generated. When the finger is lifted, an [`TouchPhase::Ended`] /// event is generated with the same finger id. /// -/// After a `Start` event has been emitted, there may be zero or more `Move` +/// After a `Started` event has been emitted, there may be zero or more `Move` /// events when the finger is moved or the touch pressure changes. /// -/// The finger id may be reused by the system after an `End` event. The user -/// should assume that a new `Start` event received with the same id has nothing +/// The finger id may be reused by the system after an `Ended` event. The user +/// should assume that a new `Started` event received with the same id has nothing /// to do with the old finger and is a new finger. /// -/// A `Cancelled` event is emitted when the system has canceled tracking this +/// A [`TouchPhase::Cancelled`] event is emitted when the system has canceled tracking this /// touch, such as when the window loses focus, or on iOS if the user moves the /// device against their face. #[derive(Debug, Clone, Copy, PartialEq)] @@ -704,8 +972,9 @@ pub enum Force { impl Force { /// Returns the force normalized to the range between 0.0 and 1.0 inclusive. + /// /// Instead of normalizing the force, you should prefer to handle - /// `Force::Calibrated` so that the amount of force the user has to apply is + /// [`Force::Calibrated`] so that the amount of force the user has to apply is /// consistent across devices. pub fn normalized(&self) -> f64 { match self { @@ -766,7 +1035,7 @@ pub enum MouseScrollDelta { /// Amount in pixels to scroll in the horizontal and /// vertical direction. /// - /// Scroll events are expressed as a PixelDelta if + /// Scroll events are expressed as a `PixelDelta` if /// supported by the device (eg. a touchpad) and /// platform. /// diff --git a/src/event_loop.rs b/src/event_loop.rs index 556f016e0d..f376ceab79 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -1,44 +1,46 @@ -//! The `EventLoop` struct and assorted supporting types, including `ControlFlow`. +//! The [`EventLoop`] struct and assorted supporting types, including +//! [`ControlFlow`]. //! -//! If you want to send custom events to the event loop, use [`EventLoop::create_proxy()`][create_proxy] -//! to acquire an [`EventLoopProxy`][event_loop_proxy] and call its [`send_event`][send_event] method. +//! If you want to send custom events to the event loop, use +//! [`EventLoop::create_proxy`] to acquire an [`EventLoopProxy`] and call its +//! [`send_event`](`EventLoopProxy::send_event`) method. //! //! See the root-level documentation for information on how to create and use an event loop to //! handle events. -//! -//! [create_proxy]: crate::event_loop::EventLoop::create_proxy -//! [event_loop_proxy]: crate::event_loop::EventLoopProxy -//! [send_event]: crate::event_loop::EventLoopProxy::send_event -use instant::Instant; use std::marker::PhantomData; use std::ops::Deref; use std::{error, fmt}; +use instant::Instant; +use once_cell::sync::OnceCell; +use raw_window_handle::{HasRawDisplayHandle, RawDisplayHandle}; + use crate::{event::Event, monitor::MonitorHandle, platform_impl}; /// Provides a way to retrieve events from the system and from the windows that were registered to /// the events loop. /// -/// An `EventLoop` can be seen more or less as a "context". Calling `EventLoop::new()` +/// An `EventLoop` can be seen more or less as a "context". Calling [`EventLoop::new`] /// initializes everything that will be required to create windows. For example on Linux creating /// an event loop opens a connection to the X or Wayland server. /// -/// To wake up an `EventLoop` from a another thread, see the `EventLoopProxy` docs. +/// To wake up an `EventLoop` from a another thread, see the [`EventLoopProxy`] docs. /// -/// Note that the `EventLoop` cannot be shared across threads (due to platform-dependant logic -/// forbidding it), as such it is neither `Send` nor `Sync`. If you need cross-thread access, the -/// `Window` created from this `EventLoop` _can_ be sent to an other thread, and the -/// `EventLoopProxy` allows you to wake up an `EventLoop` from another thread. +/// Note that this cannot be shared across threads (due to platform-dependant logic +/// forbidding it), as such it is neither [`Send`] nor [`Sync`]. If you need cross-thread access, the +/// [`Window`] created from this _can_ be sent to an other thread, and the +/// [`EventLoopProxy`] allows you to wake up an `EventLoop` from another thread. /// +/// [`Window`]: crate::window::Window pub struct EventLoop { pub(crate) event_loop: platform_impl::EventLoop, pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync } -/// Target that associates windows with an `EventLoop`. +/// Target that associates windows with an [`EventLoop`]. /// /// This type exists to allow you to create new windows while Winit executes -/// your callback. `EventLoop` will coerce into this type (`impl Deref for +/// your callback. [`EventLoop`] will coerce into this type (`impl Deref for /// EventLoop`), so functions that take this as a parameter can also take /// `&EventLoop`. pub struct EventLoopWindowTarget { @@ -77,23 +79,34 @@ impl EventLoopBuilder { /// Builds a new event loop. /// - /// ***For cross-platform compatibility, the `EventLoop` must be created on the main thread.*** - /// Attempting to create the event loop on a different thread will panic. This restriction isn't + /// ***For cross-platform compatibility, the [`EventLoop`] must be created on the main thread, + /// and only once per application.*** + /// + /// Attempting to create the event loop on a different thread, or multiple event loops in + /// the same application, will panic. This restriction isn't /// strictly necessary on all platforms, but is imposed to eliminate any nasty surprises when /// porting to platforms that require it. `EventLoopBuilderExt::any_thread` functions are exposed - /// in the relevant `platform` module if the target platform supports creating an event loop on + /// in the relevant [`platform`] module if the target platform supports creating an event loop on /// any thread. /// - /// Usage will result in display backend initialisation, this can be controlled on linux - /// using an environment variable `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`. - /// If it is not set, winit will try to connect to a wayland connection, and if it fails will - /// fallback on x11. If this variable is set with any other value, winit will panic. + /// Calling this function will result in display backend initialisation. /// /// ## Platform-specific /// - /// - **iOS:** Can only be called on the main thread. + /// - **Linux:** Backend type can be controlled using an environment variable + /// `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`. + /// If it is not set, winit will try to connect to a Wayland connection, and if that fails, + /// will fall back on X11. If this variable is set with any other value, winit will panic. + /// + /// [`platform`]: crate::platform #[inline] pub fn build(&mut self) -> EventLoop { + static EVENT_LOOP_CREATED: OnceCell<()> = OnceCell::new(); + if EVENT_LOOP_CREATED.set(()).is_err() { + panic!("Creating EventLoop multiple times is not supported."); + } + // Certain platforms accept a mutable reference in their API. + #[allow(clippy::unnecessary_mut_passed)] EventLoop { event_loop: platform_impl::EventLoop::new(&mut self.platform_specific), _marker: PhantomData, @@ -113,24 +126,28 @@ impl fmt::Debug for EventLoopWindowTarget { } } -/// Set by the user callback given to the `EventLoop::run` method. +/// Set by the user callback given to the [`EventLoop::run`] method. /// -/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`][events_cleared] -/// is emitted. Defaults to `Poll`. +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`] is emitted. +/// +/// Defaults to [`Poll`]. /// /// ## Persistency +/// /// Almost every change is persistent between multiple calls to the event loop closure within a -/// given run loop. The only exception to this is `ExitWithCode` which, once set, cannot be unset. +/// given run loop. The only exception to this is [`ExitWithCode`] which, once set, cannot be unset. /// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will -/// reset the control flow to `Poll`. +/// reset the control flow to [`Poll`]. /// -/// [events_cleared]: crate::event::Event::RedrawEventsCleared +/// [`ExitWithCode`]: Self::ExitWithCode +/// [`Poll`]: Self::Poll #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ControlFlow { /// When the current loop iteration finishes, immediately begin a new iteration regardless of /// whether or not new events are available to process. /// /// ## Platform-specific + /// /// - **Web:** Events are queued and usually sent when `requestAnimationFrame` fires but sometimes /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for /// example when the scaling of the page has changed. This should be treated as an implementation @@ -142,10 +159,12 @@ pub enum ControlFlow { /// arrives or the given time is reached. /// /// Useful for implementing efficient timers. Applications which want to render at the display's - /// native refresh rate should instead use `Poll` and the VSync functionality of a graphics API + /// native refresh rate should instead use [`Poll`] and the VSync functionality of a graphics API /// to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll WaitUntil(Instant), - /// Send a `LoopDestroyed` event and stop the event loop. This variant is *sticky* - once set, + /// Send a [`LoopDestroyed`] event and stop the event loop. This variant is *sticky* - once set, /// `control_flow` cannot be changed from `ExitWithCode`, and any future attempts to do so will /// result in the `control_flow` parameter being reset to `ExitWithCode`. /// @@ -154,11 +173,12 @@ pub enum ControlFlow { /// /// ## Platform-specific /// - /// - **Android / iOS / WASM**: The supplied exit code is unused. - /// - **Unix**: On most Unix-like platforms, only the 8 least significant bits will be used, + /// - **Android / iOS / WASM:** The supplied exit code is unused. + /// - **Unix:** On most Unix-like platforms, only the 8 least significant bits will be used, /// which can cause surprises with negative exit values (`-42` would end up as `214`). See /// [`std::process::exit`]. /// + /// [`LoopDestroyed`]: Event::LoopDestroyed /// [`Exit`]: ControlFlow::Exit ExitWithCode(i32), } @@ -166,41 +186,41 @@ pub enum ControlFlow { impl ControlFlow { /// Alias for [`ExitWithCode`]`(0)`. /// - /// [`ExitWithCode`]: ControlFlow::ExitWithCode + /// [`ExitWithCode`]: Self::ExitWithCode #[allow(non_upper_case_globals)] pub const Exit: Self = Self::ExitWithCode(0); /// Sets this to [`Poll`]. /// - /// [`Poll`]: ControlFlow::Poll + /// [`Poll`]: Self::Poll pub fn set_poll(&mut self) { *self = Self::Poll; } /// Sets this to [`Wait`]. /// - /// [`Wait`]: ControlFlow::Wait + /// [`Wait`]: Self::Wait pub fn set_wait(&mut self) { *self = Self::Wait; } /// Sets this to [`WaitUntil`]`(instant)`. /// - /// [`WaitUntil`]: ControlFlow::WaitUntil + /// [`WaitUntil`]: Self::WaitUntil pub fn set_wait_until(&mut self, instant: Instant) { *self = Self::WaitUntil(instant); } /// Sets this to [`ExitWithCode`]`(code)`. /// - /// [`ExitWithCode`]: ControlFlow::ExitWithCode + /// [`ExitWithCode`]: Self::ExitWithCode pub fn set_exit_with_code(&mut self, code: i32) { *self = Self::ExitWithCode(code); } /// Sets this to [`Exit`]. /// - /// [`Exit`]: ControlFlow::Exit + /// [`Exit`]: Self::Exit pub fn set_exit(&mut self) { *self = Self::Exit; } @@ -208,19 +228,27 @@ impl ControlFlow { impl Default for ControlFlow { #[inline(always)] - fn default() -> ControlFlow { - ControlFlow::Poll + fn default() -> Self { + Self::Poll } } impl EventLoop<()> { - /// Alias for `EventLoopBuilder::new().build()`. + /// Alias for [`EventLoopBuilder::new().build()`]. + /// + /// [`EventLoopBuilder::new().build()`]: EventLoopBuilder::build #[inline] pub fn new() -> EventLoop<()> { EventLoopBuilder::new().build() } } +impl Default for EventLoop<()> { + fn default() -> Self { + Self::new() + } +} + impl EventLoop { #[deprecated = "Use `EventLoopBuilder::::with_user_event().build()` instead."] pub fn with_user_event() -> EventLoop { @@ -238,7 +266,7 @@ impl EventLoop { /// /// ## Platform-specific /// - /// - **X11 / Wayland**: The program terminates with exit code 1 if the display server + /// - **X11 / Wayland:** The program terminates with exit code 1 if the display server /// disconnects. /// /// [`ControlFlow`]: crate::event_loop::ControlFlow @@ -250,7 +278,7 @@ impl EventLoop { self.event_loop.run(event_handler) } - /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. + /// Creates an [`EventLoopProxy`] that can be used to dispatch user events to the main event loop. pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { event_loop_proxy: self.event_loop.create_proxy(), @@ -286,9 +314,39 @@ impl EventLoopWindowTarget { pub fn primary_monitor(&self) -> Option { self.p.primary_monitor() } + + /// Change [`DeviceEvent`] filter mode. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, winit + /// will ignore them by default for unfocused windows on Linux/BSD. This method allows changing + /// this filter at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Wayland / macOS / iOS / Android / Web:** Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn set_device_event_filter(&self, _filter: DeviceEventFilter) { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "windows" + ))] + self.p.set_device_event_filter(_filter); + } +} + +unsafe impl HasRawDisplayHandle for EventLoopWindowTarget { + /// Returns a [`raw_window_handle::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> RawDisplayHandle { + self.p.raw_display_handle() + } } -/// Used to send custom events to `EventLoop`. +/// Used to send custom events to [`EventLoop`]. pub struct EventLoopProxy { event_loop_proxy: platform_impl::EventLoopProxy, } @@ -302,11 +360,13 @@ impl Clone for EventLoopProxy { } impl EventLoopProxy { - /// Send an event to the `EventLoop` from which this proxy was created. This emits a + /// Send an event to the [`EventLoop`] from which this proxy was created. This emits a /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this /// function. /// - /// Returns an `Err` if the associated `EventLoop` no longer exists. + /// Returns an `Err` if the associated [`EventLoop`] no longer exists. + /// + /// [`UserEvent(event)`]: Event::UserEvent pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { self.event_loop_proxy.send_event(event) } @@ -318,8 +378,10 @@ impl fmt::Debug for EventLoopProxy { } } -/// The error that is returned when an `EventLoopProxy` attempts to wake up an `EventLoop` that -/// no longer exists. Contains the original event given to `send_event`. +/// The error that is returned when an [`EventLoopProxy`] attempts to wake up an [`EventLoop`] that +/// no longer exists. +/// +/// Contains the original event given to [`EventLoopProxy::send_event`]. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct EventLoopClosed(pub T); @@ -330,3 +392,20 @@ impl fmt::Display for EventLoopClosed { } impl error::Error for EventLoopClosed {} + +/// Filter controlling the propagation of device events. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum DeviceEventFilter { + /// Always filter out device events. + Always, + /// Filter out device events while the window is not focused. + Unfocused, + /// Report all device events regardless of window focus. + Never, +} + +impl Default for DeviceEventFilter { + fn default() -> Self { + Self::Unfocused + } +} diff --git a/src/icon.rs b/src/icon.rs index 418ea03afa..982e5f5d81 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -13,7 +13,7 @@ pub(crate) struct Pixel { pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); #[derive(Debug)] -/// An error produced when using `Icon::from_rgba` with invalid arguments. +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. pub enum BadIcon { /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be /// safely interpreted as 32bpp RGBA pixels. @@ -73,10 +73,6 @@ mod constructors { use super::*; impl RgbaIcon { - /// Creates an `Icon` from 32bpp RGBA data. - /// - /// The length of `rgba` must be divisible by 4, and `width * height` must equal - /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { if rgba.len() % PIXEL_SIZE != 0 { return Err(BadIcon::ByteCountNotDivisibleBy4 { @@ -123,7 +119,7 @@ impl fmt::Debug for Icon { } impl Icon { - /// Creates an `Icon` from 32bpp RGBA data. + /// Creates an icon from 32bpp RGBA data. /// /// The length of `rgba` must be divisible by 4, and `width * height` must equal /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. diff --git a/src/lib.rs b/src/lib.rs index a74586f850..4871bb3c58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,9 +98,9 @@ //! # Drawing on the window //! //! Winit doesn't directly provide any methods for drawing on a [`Window`]. However it allows you to -//! retrieve the raw handle of the window (see the [`platform`] module and/or the -//! [`raw_window_handle`] method), which in turn allows you to create an -//! OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. +//! retrieve the raw handle of the window and display (see the [`platform`] module and/or the +//! [`raw_window_handle`] and [`raw_display_handle`] methods), which in turn allows +//! you to create an OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. //! //! Note that many platforms will display garbage data in the window's client area if the //! application doesn't render anything to the window by the time the desktop compositor is ready to @@ -129,13 +129,14 @@ //! [`LoopDestroyed`]: event::Event::LoopDestroyed //! [`platform`]: platform //! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle +//! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle #![deny(rust_2018_idioms)] #![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![cfg_attr(feature = "cargo-clippy", deny(warnings))] +#![allow(clippy::missing_safety_doc)] -#[allow(unused_imports)] -#[macro_use] -extern crate lazy_static; #[allow(unused_imports)] #[macro_use] extern crate log; diff --git a/src/monitor.rs b/src/monitor.rs index 5cf680ff21..0ee4817737 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,14 +1,10 @@ //! Types useful for interacting with a user's monitors. //! -//! If you want to get basic information about a monitor, you can use the [`MonitorHandle`][monitor_handle] -//! type. This is retrieved from one of the following methods, which return an iterator of -//! [`MonitorHandle`][monitor_handle]: -//! - [`EventLoopWindowTarget::available_monitors`][loop_get] -//! - [`Window::available_monitors`][window_get]. -//! -//! [monitor_handle]: crate::monitor::MonitorHandle -//! [loop_get]: crate::event_loop::EventLoopWindowTarget::available_monitors -//! [window_get]: crate::window::Window::available_monitors +//! If you want to get basic information about a monitor, you can use the +//! [`MonitorHandle`] type. This is retrieved from one of the following +//! methods, which return an iterator of [`MonitorHandle`]: +//! - [`EventLoopWindowTarget::available_monitors`](crate::event_loop::EventLoopWindowTarget::available_monitors). +//! - [`Window::available_monitors`](crate::window::Window::available_monitors). use crate::{ dpi::{PhysicalPosition, PhysicalSize}, platform_impl, @@ -16,10 +12,7 @@ use crate::{ /// Describes a fullscreen video mode of a monitor. /// -/// Can be acquired with: -/// - [`MonitorHandle::video_modes`][monitor_get]. -/// -/// [monitor_get]: crate::monitor::MonitorHandle::video_modes +/// Can be acquired with [`MonitorHandle::video_modes`]. #[derive(Clone, PartialEq, Eq, Hash)] pub struct VideoMode { pub(crate) video_mode: platform_impl::VideoMode, @@ -46,8 +39,8 @@ impl Ord for VideoMode { self.monitor().cmp(&other.monitor()).then( size.cmp(&other_size) .then( - self.refresh_rate() - .cmp(&other.refresh_rate()) + self.refresh_rate_millihertz() + .cmp(&other.refresh_rate_millihertz()) .then(self.bit_depth().cmp(&other.bit_depth())), ) .reverse(), @@ -75,12 +68,10 @@ impl VideoMode { self.video_mode.bit_depth() } - /// Returns the refresh rate of this video mode. **Note**: the returned - /// refresh rate is an integer approximation, and you shouldn't rely on this - /// value to be exact. + /// Returns the refresh rate of this video mode in mHz. #[inline] - pub fn refresh_rate(&self) -> u16 { - self.video_mode.refresh_rate() + pub fn refresh_rate_millihertz(&self) -> u32 { + self.video_mode.refresh_rate_millihertz() } /// Returns the monitor that this video mode is valid for. Each monitor has @@ -95,10 +86,10 @@ impl std::fmt::Display for VideoMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}x{} @ {} Hz ({} bpp)", + "{}x{} @ {} mHz ({} bpp)", self.size().width, self.size().height, - self.refresh_rate(), + self.refresh_rate_millihertz(), self.bit_depth() ) } @@ -148,6 +139,15 @@ impl MonitorHandle { self.inner.position() } + /// The monitor refresh rate used by the system. + /// + /// When using exclusive fullscreen, the refresh rate of the [`VideoMode`] that was used to + /// enter fullscreen should be used instead. + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + self.inner.refresh_rate_millihertz() + } + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. /// /// See the [`dpi`](crate::dpi) module for more information. diff --git a/src/platform/android.rs b/src/platform/android.rs index b4e9917642..af15f3efd9 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -7,15 +7,15 @@ use crate::{ use ndk::configuration::Configuration; use ndk_glue::Rect; -/// Additional methods on `EventLoop` that are specific to Android. +/// Additional methods on [`EventLoop`] that are specific to Android. pub trait EventLoopExtAndroid {} impl EventLoopExtAndroid for EventLoop {} -/// Additional methods on `EventLoopWindowTarget` that are specific to Android. +/// Additional methods on [`EventLoopWindowTarget`] that are specific to Android. pub trait EventLoopWindowTargetExtAndroid {} -/// Additional methods on `Window` that are specific to Android. +/// Additional methods on [`Window`] that are specific to Android. pub trait WindowExtAndroid { fn content_rect(&self) -> Rect; @@ -34,7 +34,7 @@ impl WindowExtAndroid for Window { impl EventLoopWindowTargetExtAndroid for EventLoopWindowTarget {} -/// Additional methods on `WindowBuilder` that are specific to Android. +/// Additional methods on [`WindowBuilder`] that are specific to Android. pub trait WindowBuilderExtAndroid {} impl WindowBuilderExtAndroid for WindowBuilder {} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 1ddab448a9..10997e5247 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -9,16 +9,16 @@ use crate::{ window::{Window, WindowBuilder}, }; -/// Additional methods on `Window` that are specific to MacOS. +/// Additional methods on [`Window`] that are specific to MacOS. pub trait WindowExtMacOS { /// Returns a pointer to the cocoa `NSWindow` that is used by this window. /// - /// The pointer will become invalid when the `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. fn ns_window(&self) -> *mut c_void; /// Returns a pointer to the cocoa `NSView` that is used by this window. /// - /// The pointer will become invalid when the `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. fn ns_view(&self) -> *mut c_void; /// Returns whether or not the window is in simple fullscreen mode. @@ -89,16 +89,14 @@ impl Default for ActivationPolicy { } } -/// Additional methods on `WindowBuilder` that are specific to MacOS. +/// Additional methods on [`WindowBuilder`] that are specific to MacOS. /// -/// **Note:** Properties dealing with the titlebar will be overwritten by the `with_decorations` method -/// on the base `WindowBuilder`: -/// -/// - `with_titlebar_transparent` -/// - `with_title_hidden` -/// - `with_titlebar_hidden` -/// - `with_titlebar_buttons_hidden` -/// - `with_fullsize_content_view` +/// **Note:** Properties dealing with the titlebar will be overwritten by the [`WindowBuilder::with_decorations`] method: +/// - `with_titlebar_transparent` +/// - `with_title_hidden` +/// - `with_titlebar_hidden` +/// - `with_titlebar_buttons_hidden` +/// - `with_fullsize_content_view` pub trait WindowBuilderExtMacOS { /// Enables click-and-drag behavior for the entire window, not just the titlebar. fn with_movable_by_window_background(self, movable_by_window_background: bool) @@ -238,7 +236,7 @@ impl EventLoopBuilderExtMacOS for EventLoopBuilder { } } -/// Additional methods on `MonitorHandle` that are specific to MacOS. +/// Additional methods on [`MonitorHandle`] that are specific to MacOS. pub trait MonitorHandleExtMacOS { /// Returns the identifier of the monitor for Cocoa. fn native_id(&self) -> u32; @@ -257,7 +255,7 @@ impl MonitorHandleExtMacOS for MonitorHandle { } } -/// Additional methods on `EventLoopWindowTarget` that are specific to macOS. +/// Additional methods on [`EventLoopWindowTarget`] that are specific to macOS. pub trait EventLoopWindowTargetExtMacOS { /// Hide the entire application. In most applications this is typically triggered with Command-H. fn hide_application(&self); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1ee5fce2bb..dde4c079cf 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -19,7 +19,7 @@ pub mod android; pub mod ios; pub mod macos; pub mod unix; +pub mod web; pub mod windows; pub mod run_return; -pub mod web; diff --git a/src/platform/run_return.rs b/src/platform/run_return.rs index 1c2fa62326..5013823bc1 100644 --- a/src/platform/run_return.rs +++ b/src/platform/run_return.rs @@ -14,17 +14,18 @@ use crate::{ event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; -/// Additional methods on `EventLoop` to return control flow to the caller. +/// Additional methods on [`EventLoop`] to return control flow to the caller. pub trait EventLoopExtRunReturn { - /// A type provided by the user that can be passed through `Event::UserEvent`. + /// A type provided by the user that can be passed through [`Event::UserEvent`]. type UserEvent; /// Initializes the `winit` event loop. /// - /// Unlike `run`, this function accepts non-`'static` (i.e. non-`move`) closures and returns - /// control flow to the caller when `control_flow` is set to `ControlFlow::Exit`. + /// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures + /// and returns control flow to the caller when `control_flow` is set to [`ControlFlow::Exit`]. /// /// # Caveats + /// /// Despite its appearance at first glance, this is *not* a perfect replacement for /// `poll_events`. For example, this function will not return on Windows or macOS while a /// window is getting resized, resulting in all application logic outside of the @@ -36,7 +37,7 @@ pub trait EventLoopExtRunReturn { /// /// ## Platform-specific /// - /// - **Unix-alikes** (**X11** or **Wayland**): This function returns `1` upon disconnection from + /// - **X11 / Wayland:** This function returns `1` upon disconnection from /// the display server. fn run_return(&mut self, event_handler: F) -> i32 where diff --git a/src/platform/unix.rs b/src/platform/unix.rs index 43c11b0051..99e8261940 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -19,9 +19,10 @@ use crate::{ #[cfg(feature = "x11")] use crate::dpi::Size; #[cfg(feature = "x11")] -use crate::platform_impl::x11::{ffi::XVisualInfo, XConnection}; +use crate::platform_impl::{x11::ffi::XVisualInfo, x11::XConnection, XLIB_ERROR_HOOKS}; use crate::platform_impl::{ - Backend, EventLoopWindowTarget as LinuxEventLoopWindowTarget, Window as LinuxWindow, + ApplicationName, Backend, EventLoopWindowTarget as LinuxEventLoopWindowTarget, + Window as LinuxWindow, }; // TODO: stupid hack so that glutin can do its work @@ -31,13 +32,41 @@ pub use crate::platform_impl::x11; #[cfg(feature = "x11")] pub use crate::platform_impl::{x11::util::WindowType as XWindowType, XNotSupported}; -/// Additional methods on `EventLoopWindowTarget` that are specific to Unix. +#[cfg(feature = "wayland")] +pub use crate::window::Theme; + +/// The first argument in the provided hook will be the pointer to `XDisplay` +/// and the second one the pointer to [`XErrorEvent`]. The returned `bool` is an +/// indicator whether the error was handled by the callback. +/// +/// [`XErrorEvent`]: https://linux.die.net/man/3/xerrorevent +#[cfg(feature = "x11")] +pub type XlibErrorHook = + Box bool + Send + Sync>; + +/// Hook to winit's xlib error handling callback. +/// +/// This method is provided as a safe way to handle the errors comming from X11 when using xlib +/// in external crates, like glutin for GLX access. Trying to handle errors by speculating with +/// `XSetErrorHandler` is [`unsafe`]. +/// +/// [`unsafe`]: https://www.remlab.net/op/xlib.shtml +#[inline] +#[cfg(feature = "x11")] +pub fn register_xlib_error_hook(hook: XlibErrorHook) { + // Append new hook. + unsafe { + XLIB_ERROR_HOOKS.lock().push(hook); + } +} + +/// Additional methods on [`EventLoopWindowTarget`] that are specific to Unix. pub trait EventLoopWindowTargetExtUnix { - /// True if the `EventLoopWindowTarget` uses Wayland. + /// True if the [`EventLoopWindowTarget`] uses Wayland. #[cfg(feature = "wayland")] fn is_wayland(&self) -> bool; - /// True if the `EventLoopWindowTarget` uses X11. + /// True if the [`EventLoopWindowTarget`] uses X11. #[cfg(feature = "x11")] fn is_x11(&self) -> bool; @@ -46,11 +75,13 @@ pub trait EventLoopWindowTargetExtUnix { fn xlib_xconnection(&self) -> Option>; /// Returns a pointer to the `wl_display` object of wayland that is used by this - /// `EventLoopWindowTarget`. + /// [`EventLoopWindowTarget`]. + /// + /// Returns `None` if the [`EventLoop`] doesn't use wayland (if it uses xlib for example). /// - /// Returns `None` if the `EventLoop` doesn't use wayland (if it uses xlib for example). + /// The pointer will become invalid when the winit [`EventLoop`] is destroyed. /// - /// The pointer will become invalid when the winit `EventLoop` is destroyed. + /// [`EventLoop`]: crate::event_loop::EventLoop #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void>; } @@ -69,7 +100,6 @@ impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { } #[inline] - #[doc(hidden)] #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option> { match self.p { @@ -131,9 +161,9 @@ impl EventLoopBuilderExtUnix for EventLoopBuilder { } } -/// Additional methods on `Window` that are specific to Unix. +/// Additional methods on [`Window`] that are specific to Unix. pub trait WindowExtUnix { - /// Returns the ID of the `Window` xlib object that is used by this window. + /// Returns the ID of the [`Window`] xlib object that is used by this window. /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). #[cfg(feature = "x11")] @@ -143,7 +173,7 @@ pub trait WindowExtUnix { /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). /// - /// The pointer will become invalid when the glutin `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. #[cfg(feature = "x11")] fn xlib_display(&self) -> Option<*mut raw::c_void>; @@ -158,7 +188,7 @@ pub trait WindowExtUnix { /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). /// - /// The pointer will become invalid when the glutin `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. #[cfg(feature = "x11")] fn xcb_connection(&self) -> Option<*mut raw::c_void>; @@ -166,7 +196,7 @@ pub trait WindowExtUnix { /// /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). /// - /// The pointer will become invalid when the glutin `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. #[cfg(feature = "wayland")] fn wayland_surface(&self) -> Option<*mut raw::c_void>; @@ -174,16 +204,25 @@ pub trait WindowExtUnix { /// /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). /// - /// The pointer will become invalid when the glutin `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void>; + /// Updates [`Theme`] of window decorations. + /// + /// You can also use `WINIT_WAYLAND_CSD_THEME` env variable to set the theme. + /// Possible values for env variable are: "dark" and light" + /// + /// When unspecified a theme is automatically selected. + #[cfg(feature = "wayland")] + fn wayland_set_csd_theme(&self, config: Theme); + /// Check if the window is ready for drawing /// /// It is a remnant of a previous implementation detail for the /// wayland backend, and is no longer relevant. /// - /// Always return true. + /// Always return `true`. #[deprecated] fn is_ready(&self) -> bool; } @@ -220,7 +259,6 @@ impl WindowExtUnix for Window { } #[inline] - #[doc(hidden)] #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option> { match self.window { @@ -260,31 +298,64 @@ impl WindowExtUnix for Window { } } + #[inline] + #[cfg(feature = "wayland")] + fn wayland_set_csd_theme(&self, theme: Theme) { + #[allow(clippy::single_match)] + match self.window { + LinuxWindow::Wayland(ref w) => w.set_csd_theme(theme), + #[cfg(feature = "x11")] + _ => (), + } + } + #[inline] fn is_ready(&self) -> bool { true } } -/// Additional methods on `WindowBuilder` that are specific to Unix. +/// Additional methods on [`WindowBuilder`] that are specific to Unix. pub trait WindowBuilderExtUnix { #[cfg(feature = "x11")] fn with_x11_visual(self, visual_infos: *const T) -> Self; + #[cfg(feature = "x11")] fn with_x11_screen(self, screen_id: i32) -> Self; - /// Build window with `WM_CLASS` hint; defaults to the name of the binary. Only relevant on X11. - #[cfg(feature = "x11")] - fn with_class(self, class: String, instance: String) -> Self; + /// Build window with the given `general` and `instance` names. + /// + /// On Wayland, the `general` name sets an application ID, which should match the `.desktop` + /// file destributed with your program. The `instance` is a `no-op`. + /// + /// On X11, the `general` sets general class of `WM_CLASS(STRING)`, while `instance` set the + /// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "general", "instance"`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; + /// Build window with override-redirect flag; defaults to false. Only relevant on X11. #[cfg(feature = "x11")] fn with_override_redirect(self, override_redirect: bool) -> Self; + /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. Only relevant on X11. #[cfg(feature = "x11")] fn with_x11_window_type(self, x11_window_type: Vec) -> Self; + /// Build window with `_GTK_THEME_VARIANT` hint set to the specified value. Currently only relevant on X11. #[cfg(feature = "x11")] fn with_gtk_theme_variant(self, variant: String) -> Self; + + /// Build window with certain decoration [`Theme`] + /// + /// You can also use `WINIT_WAYLAND_CSD_THEME` env variable to set the theme. + /// Possible values for env variable are: "dark" and light" + /// + /// When unspecified a theme is automatically selected. + #[cfg(feature = "wayland")] + fn with_wayland_csd_theme(self, theme: Theme) -> Self; + /// Build window with resize increment hint. Only implemented on X11. /// /// ``` @@ -299,6 +370,7 @@ pub trait WindowBuilderExtUnix { /// ``` #[cfg(feature = "x11")] fn with_resize_increments>(self, increments: S) -> Self; + /// Build window with base size hint. Only implemented on X11. /// /// ``` @@ -313,14 +385,6 @@ pub trait WindowBuilderExtUnix { /// ``` #[cfg(feature = "x11")] fn with_base_size>(self, base_size: S) -> Self; - - /// Build window with a given application ID. It should match the `.desktop` file distributed with - /// your program. Only relevant on Wayland. - /// - /// For details about application ID conventions, see the - /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) - #[cfg(feature = "wayland")] - fn with_app_id>(self, app_id: T) -> Self; } impl WindowBuilderExtUnix for WindowBuilder { @@ -342,9 +406,8 @@ impl WindowBuilderExtUnix for WindowBuilder { } #[inline] - #[cfg(feature = "x11")] - fn with_class(mut self, instance: String, class: String) -> Self { - self.platform_specific.class = Some((instance, class)); + fn with_name(mut self, general: impl Into, instance: impl Into) -> Self { + self.platform_specific.name = Some(ApplicationName::new(general.into(), instance.into())); self } @@ -369,6 +432,13 @@ impl WindowBuilderExtUnix for WindowBuilder { self } + #[inline] + #[cfg(feature = "wayland")] + fn with_wayland_csd_theme(mut self, theme: Theme) -> Self { + self.platform_specific.csd_theme = Some(theme); + self + } + #[inline] #[cfg(feature = "x11")] fn with_resize_increments>(mut self, increments: S) -> Self { @@ -382,13 +452,6 @@ impl WindowBuilderExtUnix for WindowBuilder { self.platform_specific.base_size = Some(base_size.into()); self } - - #[inline] - #[cfg(feature = "wayland")] - fn with_app_id>(mut self, app_id: T) -> Self { - self.platform_specific.app_id = Some(app_id.into()); - self - } } /// Additional methods on `MonitorHandle` that are specific to Linux. diff --git a/src/platform/web.rs b/src/platform/web.rs index 210f87b9d2..82e42db336 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -1,10 +1,14 @@ #![cfg(target_arch = "wasm32")] //! The web target does not automatically insert the canvas element object into the web page, to -//! allow end users to determine how the page should be laid out. Use the `WindowExtWebSys` trait -//! to retrieve the canvas from the Window. Alternatively, use the `WindowBuilderExtWebSys` trait +//! allow end users to determine how the page should be laid out. Use the [`WindowExtWebSys`] trait +//! to retrieve the canvas from the Window. Alternatively, use the [`WindowBuilderExtWebSys`] trait //! to provide your own canvas. +use crate::event::Event; +use crate::event_loop::ControlFlow; +use crate::event_loop::EventLoop; +use crate::event_loop::EventLoopWindowTarget; use crate::window::WindowBuilder; use web_sys::HtmlCanvasElement; @@ -18,6 +22,17 @@ pub trait WindowExtWebSys { pub trait WindowBuilderExtWebSys { fn with_canvas(self, canvas: Option) -> Self; + + /// Whether `event.preventDefault` should be automatically called to prevent event propagation + /// when appropriate. + /// + /// For example, mouse wheel events are only handled by the canvas by default. This avoids + /// the default behavior of scrolling the page. + fn with_prevent_default(self, prevent_default: bool) -> Self; + + /// Whether the canvas should be focusable using the tab key. This is necessary to capture + /// canvas keyboard events. + fn with_focusable(self, focusable: bool) -> Self; } impl WindowBuilderExtWebSys for WindowBuilder { @@ -26,4 +41,51 @@ impl WindowBuilderExtWebSys for WindowBuilder { self } + + fn with_prevent_default(mut self, prevent_default: bool) -> Self { + self.platform_specific.prevent_default = prevent_default; + + self + } + + fn with_focusable(mut self, focusable: bool) -> Self { + self.platform_specific.focusable = focusable; + + self + } +} + +/// Additional methods on `EventLoop` that are specific to the web. +pub trait EventLoopExtWebSys { + /// A type provided by the user that can be passed through `Event::UserEvent`. + type UserEvent; + + /// Initializes the winit event loop. + /// + /// Unlike `run`, this returns immediately, and doesn't throw an exception in order to + /// satisfy its `!` return type. + fn spawn(self, event_handler: F) + where + F: 'static + + FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ); +} + +impl EventLoopExtWebSys for EventLoop { + type UserEvent = T; + + fn spawn(self, event_handler: F) + where + F: 'static + + FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ), + { + self.event_loop.spawn(event_handler) + } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2e5d05a738..63c90ff7b8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -143,6 +143,11 @@ pub trait WindowExtWindows { /// Whether to show or hide the window icon in the taskbar. fn set_skip_taskbar(&self, skip: bool); + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn set_undecorated_shadow(&self, shadow: bool); } impl WindowExtWindows for Window { @@ -175,6 +180,11 @@ impl WindowExtWindows for Window { fn set_skip_taskbar(&self, skip: bool) { self.window.set_skip_taskbar(skip) } + + #[inline] + fn set_undecorated_shadow(&self, shadow: bool) { + self.window.set_undecorated_shadow(shadow) + } } /// Additional methods on `WindowBuilder` that are specific to Windows. @@ -229,6 +239,12 @@ pub trait WindowBuilderExtWindows { /// Whether show or hide the window icon in the taskbar. fn with_skip_taskbar(self, skip: bool) -> WindowBuilder; + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn with_undecorated_shadow(self, shadow: bool) -> WindowBuilder; } impl WindowBuilderExtWindows for WindowBuilder { @@ -279,6 +295,12 @@ impl WindowBuilderExtWindows for WindowBuilder { self.platform_specific.skip_taskbar = skip; self } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> WindowBuilder { + self.platform_specific.decoration_shadow = shadow; + self + } } /// Additional methods on `MonitorHandle` that are specific to Windows. diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 9457894e81..9c275e6ed8 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -1,38 +1,45 @@ #![cfg(target_os = "android")] -use crate::{ - dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - error, - event::{self, VirtualKeyCode}, - event_loop::{self, ControlFlow}, - monitor, window, +use std::{ + collections::VecDeque, + sync::{mpsc, RwLock}, + time::{Duration, Instant}, }; + use ndk::{ configuration::Configuration, event::{InputEvent, KeyAction, Keycode, MotionAction}, looper::{ForeignLooper, Poll, ThreadLooper}, + native_window::NativeWindow, }; -use ndk_glue::{Event, Rect}; -use raw_window_handle::{AndroidNdkHandle, RawWindowHandle}; -use std::{ - collections::VecDeque, - sync::{Arc, Mutex, RwLock}, - time::{Duration, Instant}, +use ndk_glue::{Event, LockReadGuard, Rect}; +use once_cell::sync::Lazy; +use raw_window_handle::{ + AndroidDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, +}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + error, + event::{self, VirtualKeyCode}, + event_loop::{self, ControlFlow}, + monitor, + window::{self, CursorGrabMode}, }; -lazy_static! { - static ref CONFIG: RwLock = RwLock::new(Configuration::from_asset_manager( +static CONFIG: Lazy> = Lazy::new(|| { + RwLock::new(Configuration::from_asset_manager( #[allow(deprecated)] // TODO: rust-windowing/winit#2196 - &ndk_glue::native_activity().asset_manager() - )); - // If this is `Some()` a `Poll::Wake` is considered an `EventSource::Internal` with the event - // contained in the `Option`. The event is moved outside of the `Option` replacing it with a - // `None`. - // - // This allows us to inject event into the event loop without going through `ndk-glue` and - // calling unsafe function that should only be called by Android. - static ref INTERNAL_EVENT: RwLock> = RwLock::new(None); -} + &ndk_glue::native_activity().asset_manager(), + )) +}); +// If this is `Some()` a `Poll::Wake` is considered an `EventSource::Internal` with the event +// contained in the `Option`. The event is moved outside of the `Option` replacing it with a +// `None`. +// +// This allows us to inject event into the event loop without going through `ndk-glue` and +// calling unsafe function that should only be called by Android. +static INTERNAL_EVENT: Lazy>> = Lazy::new(|| RwLock::new(None)); enum InternalEvent { RedrawRequested, @@ -230,14 +237,16 @@ fn poll(poll: Poll) -> Option { pub struct EventLoop { window_target: event_loop::EventLoopWindowTarget, - user_queue: Arc>>, + user_events_sender: mpsc::Sender, + user_events_receiver: mpsc::Receiver, first_event: Option, start_cause: event::StartCause, looper: ThreadLooper, running: bool, + window_lock: Option>, } -#[derive(Default, Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes {} macro_rules! call_event_handler { @@ -252,6 +261,7 @@ macro_rules! call_event_handler { impl EventLoop { pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { + let (user_events_sender, user_events_receiver) = mpsc::channel(); Self { window_target: event_loop::EventLoopWindowTarget { p: EventLoopWindowTarget { @@ -259,11 +269,13 @@ impl EventLoop { }, _marker: std::marker::PhantomData, }, - user_queue: Default::default(), + user_events_sender, + user_events_receiver, first_event: None, start_cause: event::StartCause::Init, looper: ThreadLooper::for_thread().unwrap(), running: false, + window_lock: None, } } @@ -296,22 +308,42 @@ impl EventLoop { match self.first_event.take() { Some(EventSource::Callback) => match ndk_glue::poll_events().unwrap() { Event::WindowCreated => { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::Resumed - ); + // Acquire a lock on the window to prevent Android from destroying + // it until we've notified and waited for the user in Event::Suspended. + // WARNING: ndk-glue is inherently racy (https://github.com/rust-windowing/winit/issues/2293) + // and may have already received onNativeWindowDestroyed while this thread hasn't yet processed + // the event, and would see a `None` lock+window in that case. + if let Some(next_window_lock) = ndk_glue::native_window() { + assert!( + self.window_lock.replace(next_window_lock).is_none(), + "Received `Event::WindowCreated` while we were already holding a lock" + ); + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Resumed + ); + } else { + warn!("Received `Event::WindowCreated` while `ndk_glue::native_window()` provides no window"); + } } Event::WindowResized => resized = true, Event::WindowRedrawNeeded => redraw = true, Event::WindowDestroyed => { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::Suspended - ); + // Release the lock, allowing Android to clean up this surface + // WARNING: See above - if ndk-glue is racy, this event may be called + // without having a `self.window_lock` in place. + if self.window_lock.take().is_some() { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Suspended + ); + } else { + warn!("Received `Event::WindowDestroyed` while we were not holding a window lock"); + } } Event::Pause => self.running = false, Event::Resume => self.running = true, @@ -365,7 +397,7 @@ impl EventLoop { }, Some(EventSource::InputQueue) => { if let Some(input_queue) = ndk_glue::input_queue().as_ref() { - while let Some(event) = input_queue.get_event() { + while let Some(event) = input_queue.get_event().expect("get_event") { if let Some(event) = input_queue.pre_dispatch(event) { let mut handled = true; let window_id = window::WindowId(WindowId); @@ -466,8 +498,9 @@ impl EventLoop { } } Some(EventSource::User) => { - let mut user_queue = self.user_queue.lock().unwrap(); - while let Some(event) = user_queue.pop_front() { + // try_recv only errors when empty (expected) or disconnect. But because Self + // contains a Sender it will never disconnect, so no error handling need. + while let Ok(event) = self.user_events_receiver.try_recv() { call_event_handler!( event_handler, self.window_target(), @@ -568,20 +601,22 @@ impl EventLoop { pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { - queue: self.user_queue.clone(), + user_events_sender: self.user_events_sender.clone(), looper: ForeignLooper::for_thread().expect("called from event loop thread"), } } } pub struct EventLoopProxy { - queue: Arc>>, + user_events_sender: mpsc::Sender, looper: ForeignLooper, } impl EventLoopProxy { pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { - self.queue.lock().unwrap().push_back(event); + self.user_events_sender + .send(event) + .map_err(|mpsc::SendError(x)| event_loop::EventLoopClosed(x))?; self.looper.wake(); Ok(()) } @@ -590,7 +625,7 @@ impl EventLoopProxy { impl Clone for EventLoopProxy { fn clone(&self) -> Self { EventLoopProxy { - queue: self.queue.clone(), + user_events_sender: self.user_events_sender.clone(), looper: self.looper.clone(), } } @@ -612,6 +647,10 @@ impl EventLoopWindowTarget { v.push_back(MonitorHandle); v } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Android(AndroidDisplayHandle::empty()) + } } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -623,6 +662,18 @@ impl WindowId { } } +impl From for u64 { + fn from(_: WindowId) -> Self { + 0 + } +} + +impl From for WindowId { + fn from(_: u64) -> Self { + Self + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct DeviceId; @@ -638,7 +689,7 @@ pub struct PlatformSpecificWindowBuilderAttributes; pub struct Window; impl Window { - pub fn new( + pub(crate) fn new( _el: &EventLoopWindowTarget, _window_attrs: window::WindowAttributes, _: PlatformSpecificWindowBuilderAttributes, @@ -748,6 +799,8 @@ impl Window { pub fn set_ime_position(&self, _position: Position) {} + pub fn set_ime_allowed(&self, _allowed: bool) {} + pub fn focus_window(&self) {} pub fn request_user_attention(&self, _request_type: Option) {} @@ -760,7 +813,7 @@ impl Window { )) } - pub fn set_cursor_grab(&self, _: bool) -> Result<(), error::ExternalError> { + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( error::NotSupportedError::new(), )) @@ -781,13 +834,15 @@ impl Window { } pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = AndroidNdkHandle::empty(); - if let Some(native_window) = ndk_glue::native_window().as_ref() { - handle.a_native_window = unsafe { native_window.ptr().as_mut() as *mut _ as *mut _ } + if let Some(native_window) = ndk_glue::native_window() { + native_window.raw_window_handle() } else { panic!("Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events."); - }; - RawWindowHandle::AndroidNdk(handle) + } + } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Android(AndroidDisplayHandle::empty()) } pub fn config(&self) -> Configuration { @@ -841,20 +896,23 @@ impl MonitorHandle { .unwrap_or(1.0) } + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + pub fn video_modes(&self) -> impl Iterator { let size = self.size().into(); - let mut v = Vec::new(); // FIXME this is not the real refresh rate - // (it is guarunteed to support 32 bit color though) - v.push(monitor::VideoMode { + // (it is guaranteed to support 32 bit color though) + std::iter::once(monitor::VideoMode { video_mode: VideoMode { size, bit_depth: 32, - refresh_rate: 60, + refresh_rate_millihertz: 60000, monitor: self.clone(), }, - }); - v.into_iter() + }) } } @@ -862,7 +920,7 @@ impl MonitorHandle { pub struct VideoMode { size: (u32, u32), bit_depth: u16, - refresh_rate: u16, + refresh_rate_millihertz: u32, monitor: MonitorHandle, } @@ -875,8 +933,8 @@ impl VideoMode { self.bit_depth } - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } pub fn monitor(&self) -> monitor::MonitorHandle { diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 6d1d206942..0586084476 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -10,6 +10,7 @@ use std::{ }; use objc::runtime::{BOOL, YES}; +use once_cell::sync::Lazy; use crate::{ dpi::LogicalSize, @@ -52,11 +53,7 @@ enum UserCallbackTransitionResult<'a> { impl Event<'static, Never> { fn is_redraw(&self) -> bool { - if let Event::RedrawRequested(_) = self { - true - } else { - false - } + matches!(self, Event::RedrawRequested(_)) } } @@ -119,7 +116,7 @@ impl Drop for AppState { } => { for &mut window in queued_windows { unsafe { - let () = msg_send![window, release]; + let _: () = msg_send![window, release]; } } } @@ -200,10 +197,10 @@ impl AppState { } fn has_launched(&self) -> bool { - match self.state() { - &AppStateImpl::NotLaunched { .. } | &AppStateImpl::Launching { .. } => false, - _ => true, - } + !matches!( + self.state(), + AppStateImpl::NotLaunched { .. } | AppStateImpl::Launching { .. } + ) } fn will_launch_transition(&mut self, queued_event_handler: Box) { @@ -528,7 +525,9 @@ pub unsafe fn queue_gl_or_metal_redraw(window: id) { | &mut AppStateImpl::InUserCallback { ref mut queued_gpu_redraws, .. - } => drop(queued_gpu_redraws.insert(window)), + } => { + let _ = queued_gpu_redraws.insert(window); + } s @ &mut AppStateImpl::ProcessingRedraws { .. } | s @ &mut AppStateImpl::Waiting { .. } | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), @@ -536,6 +535,7 @@ pub unsafe fn queue_gl_or_metal_redraw(window: id) { panic!("Attempt to create a `Window` after the app has terminated") } } + drop(this); } @@ -548,7 +548,7 @@ pub unsafe fn will_launch(queued_event_handler: Box) { pub unsafe fn did_finish_launching() { let mut this = AppState::get_mut(); let windows = match this.state_mut() { - AppStateImpl::Launching { queued_windows, .. } => mem::replace(queued_windows, Vec::new()), + AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), s => bug!("unexpected state {:?}", s), }; @@ -578,15 +578,15 @@ pub unsafe fn did_finish_launching() { // ``` let screen: id = msg_send![window, screen]; let _: id = msg_send![screen, retain]; - let () = msg_send![window, setScreen:0 as id]; - let () = msg_send![window, setScreen: screen]; - let () = msg_send![screen, release]; + let _: () = msg_send![window, setScreen:0 as id]; + let _: () = msg_send![window, setScreen: screen]; + let _: () = msg_send![screen, release]; let controller: id = msg_send![window, rootViewController]; - let () = msg_send![window, setRootViewController:ptr::null::<()>()]; - let () = msg_send![window, setRootViewController: controller]; - let () = msg_send![window, makeKeyAndVisible]; + let _: () = msg_send![window, setRootViewController:ptr::null::<()>()]; + let _: () = msg_send![window, setRootViewController: controller]; + let _: () = msg_send![window, makeKeyAndVisible]; } - let () = msg_send![window, release]; + let _: () = msg_send![window, release]; } let (windows, events) = AppState::get_mut().did_finish_launching_transition(); @@ -603,9 +603,9 @@ pub unsafe fn did_finish_launching() { let count: NSUInteger = msg_send![window, retainCount]; // make sure the window is still referenced if count > 1 { - let () = msg_send![window, makeKeyAndVisible]; + let _: () = msg_send![window, makeKeyAndVisible]; } - let () = msg_send![window, release]; + let _: () = msg_send![window, release]; } } @@ -670,7 +670,7 @@ pub unsafe fn handle_nonuser_events>(events &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _, - } => mem::replace(queued_events, Vec::new()), + } => mem::take(queued_events), s => bug!("unexpected state {:?}", s), }; if queued_events.is_empty() { @@ -751,7 +751,7 @@ unsafe fn handle_user_events() { &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _, - } => mem::replace(queued_events, Vec::new()), + } => mem::take(queued_events), s => bug!("unexpected state {:?}", s), }; if queued_events.is_empty() { @@ -793,7 +793,7 @@ pub unsafe fn handle_main_events_cleared() { return; } match this.state_mut() { - &mut AppStateImpl::ProcessingEvents { .. } => {} + AppStateImpl::ProcessingEvents { .. } => {} _ => bug!("`ProcessingRedraws` happened unexpectedly"), }; drop(this); @@ -875,7 +875,7 @@ fn handle_hidpi_proxy( let size = CGSize::new(logical_size); let new_frame: CGRect = CGRect::new(screen_frame.origin, size); unsafe { - let () = msg_send![view, setFrame: new_frame]; + let _: () = msg_send![view, setFrame: new_frame]; } } @@ -990,20 +990,20 @@ macro_rules! os_capabilities { } os_capabilities! { - /// https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + /// #[allow(unused)] // error message unused safe_area_err_msg: "-[UIView safeAreaInsets]", safe_area: 11-0, - /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc + /// home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", home_indicator_hidden: 11-0, - /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc + /// defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", defer_system_gestures: 11-0, - /// https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc + /// maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", maximum_frames_per_second: 10-3, - /// https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc + /// #[allow(unused)] // error message unused force_touch_err_msg: "-[UITouch force]", force_touch: 9-0, @@ -1016,29 +1016,27 @@ impl NSOperatingSystemVersion { } pub fn os_capabilities() -> OSCapabilities { - lazy_static! { - static ref OS_CAPABILITIES: OSCapabilities = { - let version: NSOperatingSystemVersion = unsafe { - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - let atleast_ios_8: BOOL = msg_send![ - process_info, - respondsToSelector: sel!(operatingSystemVersion) - ]; - // winit requires atleast iOS 8 because no one has put the time into supporting earlier os versions. - // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support - // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS - // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 - // has been tested to not even run on macOS 10.15 - Xcode 8 might? - // - // The minimum required iOS version is likely to grow in the future. - assert!( - atleast_ios_8 == YES, - "`winit` requires iOS version 8 or greater" - ); - msg_send![process_info, operatingSystemVersion] - }; - version.into() + static OS_CAPABILITIES: Lazy = Lazy::new(|| { + let version: NSOperatingSystemVersion = unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let atleast_ios_8: BOOL = msg_send![ + process_info, + respondsToSelector: sel!(operatingSystemVersion) + ]; + // winit requires atleast iOS 8 because no one has put the time into supporting earlier os versions. + // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support + // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS + // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 + // has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!( + atleast_ios_8 == YES, + "`winit` requires iOS version 8 or greater" + ); + msg_send![process_info, operatingSystemVersion] }; - } + version.into() + }); OS_CAPABILITIES.clone() } diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index feb6b7ea53..eeaf2e6894 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -7,6 +7,8 @@ use std::{ sync::mpsc::{self, Receiver, Sender}, }; +use raw_window_handle::{RawDisplayHandle, UiKitDisplayHandle}; + use crate::{ dpi::LogicalSize, event::Event, @@ -63,13 +65,17 @@ impl EventLoopWindowTarget { Some(RootMonitorHandle { inner: monitor }) } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::UiKit(UiKitDisplayHandle::empty()) + } } pub struct EventLoop { window_target: RootEventLoopWindowTarget, } -#[derive(Default, Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes {} impl EventLoop { diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 726723f30b..7f560514d1 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -88,7 +88,7 @@ pub enum UITouchPhase { Cancelled, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] #[allow(dead_code)] #[repr(isize)] pub enum UIForceTouchCapability { @@ -97,7 +97,7 @@ pub enum UIForceTouchCapability { Available, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] #[allow(dead_code)] #[repr(isize)] pub enum UITouchType { @@ -144,10 +144,9 @@ impl From for UIUserInterfaceIdiom { } } } - -impl Into for UIUserInterfaceIdiom { - fn into(self) -> Idiom { - match self { +impl From for Idiom { + fn from(ui_idiom: UIUserInterfaceIdiom) -> Idiom { + match ui_idiom { UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, UIUserInterfaceIdiom::Phone => Idiom::Phone, UIUserInterfaceIdiom::Pad => Idiom::Pad, @@ -230,9 +229,9 @@ impl From for UIRectEdge { } } -impl Into for UIRectEdge { - fn into(self) -> ScreenEdge { - let bits: u8 = self.0.try_into().expect("invalid `UIRectEdge`"); +impl From for ScreenEdge { + fn from(ui_rect_edge: UIRectEdge) -> ScreenEdge { + let bits: u8 = ui_rect_edge.0.try_into().expect("invalid `UIRectEdge`"); ScreenEdge::from_bits(bits).expect("invalid `ScreenEdge`") } } diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index 53c230f3f0..37677918e5 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -56,6 +56,7 @@ //! Also note that app may not receive the LoopDestroyed event if suspended; it might be SIGKILL'ed. #![cfg(target_os = "ios")] +#![allow(clippy::let_unit_value)] // TODO: (mtak-) UIKit requires main thread for virtually all function/method calls. This could be // worked around in the future by using GCD (grand central dispatch) and/or caching of values like @@ -108,9 +109,7 @@ unsafe impl Sync for DeviceId {} pub enum OsError {} impl fmt::Display for OsError { - fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - _ => unreachable!(), - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "os error") } } diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 4ba3a3dbb3..42516cb0c4 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -17,7 +17,7 @@ use crate::{ pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) refresh_rate_millihertz: u32, pub(crate) screen_mode: NativeDisplayMode, pub(crate) monitor: MonitorHandle, } @@ -30,7 +30,7 @@ unsafe impl Send for NativeDisplayMode {} impl Drop for NativeDisplayMode { fn drop(&mut self) { unsafe { - let () = msg_send![self.0, release]; + let _: () = msg_send![self.0, release]; } } } @@ -49,7 +49,7 @@ impl Clone for VideoMode { VideoMode { size: self.size, bit_depth: self.bit_depth, - refresh_rate: self.refresh_rate, + refresh_rate_millihertz: self.refresh_rate_millihertz, screen_mode: self.screen_mode.clone(), monitor: self.monitor.clone(), } @@ -59,30 +59,14 @@ impl Clone for VideoMode { impl VideoMode { unsafe fn retained_new(uiscreen: id, screen_mode: id) -> VideoMode { assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); - let os_capabilities = app_state::os_capabilities(); - let refresh_rate: NSInteger = if os_capabilities.maximum_frames_per_second { - msg_send![uiscreen, maximumFramesPerSecond] - } else { - // https://developer.apple.com/library/archive/technotes/tn2460/_index.html - // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison - // - // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not - // supported, they are all guaranteed to have 60hz refresh rates. This does not - // correctly handle external displays. ProMotion displays support 120fps, but they were - // introduced at the same time as the `maximumFramesPerSecond` API. - // - // FIXME: earlier OSs could calculate the refresh rate using - // `-[CADisplayLink duration]`. - os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); - 60 - }; + let refresh_rate_millihertz = refresh_rate_millihertz(uiscreen); let size: CGSize = msg_send![screen_mode, size]; let screen_mode: id = msg_send![screen_mode, retain]; let screen_mode = NativeDisplayMode(screen_mode); VideoMode { size: (size.width as u32, size.height as u32), bit_depth: 32, - refresh_rate: refresh_rate as u16, + refresh_rate_millihertz, screen_mode, monitor: MonitorHandle::retained_new(uiscreen), } @@ -96,8 +80,8 @@ impl VideoMode { self.bit_depth } - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } pub fn monitor(&self) -> RootMonitorHandle { @@ -115,7 +99,7 @@ pub struct Inner { impl Drop for Inner { fn drop(&mut self) { unsafe { - let () = msg_send![self.uiscreen, release]; + let _: () = msg_send![self.uiscreen, release]; } } } @@ -193,7 +177,7 @@ impl MonitorHandle { pub fn retained_new(uiscreen: id) -> MonitorHandle { unsafe { assert_main_thread!("`MonitorHandle` can only be cloned on the main thread on iOS"); - let () = msg_send![uiscreen, retain]; + let _: () = msg_send![uiscreen, retain]; } MonitorHandle { inner: Inner { uiscreen }, @@ -239,6 +223,10 @@ impl Inner { } } + pub fn refresh_rate_millihertz(&self) -> Option { + Some(refresh_rate_millihertz(self.uiscreen)) + } + pub fn video_modes(&self) -> impl Iterator { let mut modes = BTreeSet::new(); unsafe { @@ -257,6 +245,30 @@ impl Inner { } } +fn refresh_rate_millihertz(uiscreen: id) -> u32 { + let refresh_rate_millihertz: NSInteger = unsafe { + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.maximum_frames_per_second { + msg_send![uiscreen, maximumFramesPerSecond] + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + } + }; + + refresh_rate_millihertz as u32 * 1000 +} + // MonitorHandleExtIOS impl Inner { pub fn ui_screen(&self) -> id { diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index ccdec6bfbf..b4cb4225f1 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -112,14 +112,14 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { ))), ); let superclass: &'static Class = msg_send![object, superclass]; - let () = msg_send![super(object, superclass), drawRect: rect]; + let _: () = msg_send![super(object, superclass), drawRect: rect]; } } extern "C" fn layout_subviews(object: &Object, _: Sel) { unsafe { let superclass: &'static Class = msg_send![object, superclass]; - let () = msg_send![super(object, superclass), layoutSubviews]; + let _: () = msg_send![super(object, superclass), layoutSubviews]; let window: id = msg_send![object, window]; assert!(!window.is_null()); @@ -133,14 +133,14 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { width: screen_frame.size.width as f64, height: screen_frame.size.height as f64, } - .to_physical(scale_factor.into()); + .to_physical(scale_factor as f64); // If the app is started in landscape, the view frame and window bounds can be mismatched. // The view frame will be in portrait and the window bounds in landscape. So apply the // window bounds to the view frame to make it consistent. let view_frame: CGRect = msg_send![object, frame]; if view_frame != window_bounds { - let () = msg_send![object, setFrame: window_bounds]; + let _: () = msg_send![object, setFrame: window_bounds]; } app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { @@ -157,7 +157,7 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { ) { unsafe { let superclass: &'static Class = msg_send![object, superclass]; - let () = msg_send![ + let _: () = msg_send![ super(object, superclass), setContentScaleFactor: untrusted_scale_factor ]; @@ -180,7 +180,7 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { && scale_factor > 0.0, "invalid scale_factor set on UIView", ); - let scale_factor: f64 = scale_factor.into(); + let scale_factor = scale_factor as f64; let bounds: CGRect = msg_send![object, bounds]; let screen: id = msg_send![window, screen]; let screen_space: id = msg_send![screen, coordinateSpace]; @@ -340,7 +340,7 @@ unsafe fn get_view_controller_class() -> &'static Class { prefers_status_bar_hidden: BOOL, setPrefersStatusBarHidden: |object| { unsafe { - let () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; + let _: () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; } }, prefersStatusBarHidden, @@ -353,7 +353,7 @@ unsafe fn get_view_controller_class() -> &'static Class { OSCapabilities::home_indicator_hidden_err_msg; |object| { unsafe { - let () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; + let _: () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; } }, prefersHomeIndicatorAutoHidden, @@ -363,7 +363,7 @@ unsafe fn get_view_controller_class() -> &'static Class { supported_orientations: UIInterfaceOrientationMask, setSupportedInterfaceOrientations: |object| { unsafe { - let () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; + let _: () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; } }, supportedInterfaceOrientations, @@ -376,7 +376,7 @@ unsafe fn get_view_controller_class() -> &'static Class { OSCapabilities::defer_system_gestures_err_msg; |object| { unsafe { - let () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; + let _: () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; } }, preferredScreenEdgesDeferringSystemGestures, @@ -398,7 +398,7 @@ unsafe fn get_window_class() -> &'static Class { window_id: RootWindowId(object.into()), event: WindowEvent::Focused(true), })); - let () = msg_send![super(object, class!(UIWindow)), becomeKeyWindow]; + let _: () = msg_send![super(object, class!(UIWindow)), becomeKeyWindow]; } } @@ -408,7 +408,7 @@ unsafe fn get_window_class() -> &'static Class { window_id: RootWindowId(object.into()), event: WindowEvent::Focused(false), })); - let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; + let _: () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; } } @@ -429,7 +429,7 @@ unsafe fn get_window_class() -> &'static Class { } // requires main thread -pub unsafe fn create_view( +pub(crate) unsafe fn create_view( _window_attributes: &WindowAttributes, platform_attributes: &PlatformSpecificWindowBuilderAttributes, frame: CGRect, @@ -440,16 +440,16 @@ pub unsafe fn create_view( assert!(!view.is_null(), "Failed to create `UIView` instance"); let view: id = msg_send![view, initWithFrame: frame]; assert!(!view.is_null(), "Failed to initialize `UIView` instance"); - let () = msg_send![view, setMultipleTouchEnabled: YES]; + let _: () = msg_send![view, setMultipleTouchEnabled: YES]; if let Some(scale_factor) = platform_attributes.scale_factor { - let () = msg_send![view, setContentScaleFactor: scale_factor as CGFloat]; + let _: () = msg_send![view, setContentScaleFactor: scale_factor as CGFloat]; } view } // requires main thread -pub unsafe fn create_view_controller( +pub(crate) unsafe fn create_view_controller( _window_attributes: &WindowAttributes, platform_attributes: &PlatformSpecificWindowBuilderAttributes, view: id, @@ -484,28 +484,28 @@ pub unsafe fn create_view_controller( let edges: UIRectEdge = platform_attributes .preferred_screen_edges_deferring_system_gestures .into(); - let () = msg_send![ + let _: () = msg_send![ view_controller, setPrefersStatusBarHidden: status_bar_hidden ]; - let () = msg_send![ + let _: () = msg_send![ view_controller, setSupportedInterfaceOrientations: supported_orientations ]; - let () = msg_send![ + let _: () = msg_send![ view_controller, setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden ]; - let () = msg_send![ + let _: () = msg_send![ view_controller, setPreferredScreenEdgesDeferringSystemGestures: edges ]; - let () = msg_send![view_controller, setView: view]; + let _: () = msg_send![view_controller, setView: view]; view_controller } // requires main thread -pub unsafe fn create_window( +pub(crate) unsafe fn create_window( window_attributes: &WindowAttributes, _platform_attributes: &PlatformSpecificWindowBuilderAttributes, frame: CGRect, @@ -520,11 +520,11 @@ pub unsafe fn create_window( !window.is_null(), "Failed to initialize `UIWindow` instance" ); - let () = msg_send![window, setRootViewController: view_controller]; + let _: () = msg_send![window, setRootViewController: view_controller]; match window_attributes.fullscreen { Some(Fullscreen::Exclusive(ref video_mode)) => { let uiscreen = video_mode.monitor().ui_screen() as id; - let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; + let _: () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; msg_send![window, setScreen:video_mode.monitor().ui_screen()] } Some(Fullscreen::Borderless(ref monitor)) => { diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 81c225ef1b..0a4b81c243 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -1,10 +1,10 @@ -use raw_window_handle::{RawWindowHandle, UiKitHandle}; use std::{ collections::VecDeque, ops::{Deref, DerefMut}, }; use objc::runtime::{Class, Object, BOOL, NO, YES}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle, UiKitDisplayHandle, UiKitWindowHandle}; use crate::{ dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, @@ -23,7 +23,8 @@ use crate::{ monitor, view, EventLoopWindowTarget, MonitorHandle, }, window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, + WindowId as RootWindowId, }, }; @@ -37,9 +38,9 @@ pub struct Inner { impl Drop for Inner { fn drop(&mut self) { unsafe { - let () = msg_send![self.view, release]; - let () = msg_send![self.view_controller, release]; - let () = msg_send![self.window, release]; + let _: () = msg_send![self.view, release]; + let _: () = msg_send![self.view_controller, release]; + let _: () = msg_send![self.window, release]; } } } @@ -52,10 +53,10 @@ impl Inner { pub fn set_visible(&self, visible: bool) { match visible { true => unsafe { - let () = msg_send![self.window, setHidden: NO]; + let _: () = msg_send![self.window, setHidden: NO]; }, false => unsafe { - let () = msg_send![self.window, setHidden: YES]; + let _: () = msg_send![self.window, setHidden: YES]; }, } } @@ -78,7 +79,7 @@ impl Inner { // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc app_state::queue_gl_or_metal_redraw(self.window); } else { - let () = msg_send![self.view, setNeedsDisplay]; + let _: () = msg_send![self.view, setNeedsDisplay]; } } } @@ -119,8 +120,8 @@ impl Inner { }, size: screen_frame.size, }; - let bounds = self.from_screen_space(new_screen_frame); - let () = msg_send![self.window, setBounds: bounds]; + let bounds = self.rect_from_screen_space(new_screen_frame); + let _: () = msg_send![self.window, setBounds: bounds]; } } @@ -184,7 +185,7 @@ impl Inner { Err(ExternalError::NotSupported(NotSupportedError::new())) } - pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } @@ -218,7 +219,7 @@ impl Inner { let uiscreen = match monitor { Some(Fullscreen::Exclusive(video_mode)) => { let uiscreen = video_mode.video_mode.monitor.ui_screen() as id; - let () = + let _: () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; uiscreen } @@ -234,16 +235,16 @@ impl Inner { // this is pretty slow on iOS, so avoid doing it if we can let current: id = msg_send![self.window, screen]; if uiscreen != current { - let () = msg_send![self.window, setScreen: uiscreen]; + let _: () = msg_send![self.window, setScreen: uiscreen]; } let bounds: CGRect = msg_send![uiscreen, bounds]; - let () = msg_send![self.window, setFrame: bounds]; + let _: () = msg_send![self.window, setFrame: bounds]; // For external displays, we must disable overscan compensation or // the displayed image will have giant black bars surrounding it on // each side - let () = msg_send![ + let _: () = msg_send![ uiscreen, setOverscanCompensation: UIScreenOverscanCompensation::None ]; @@ -291,6 +292,10 @@ impl Inner { warn!("`Window::set_ime_position` is ignored on iOS") } + pub fn set_ime_allowed(&self, _allowed: bool) { + warn!("`Window::set_ime_allowed` is ignored on iOS") + } + pub fn focus_window(&self) { warn!("`Window::set_focus` is ignored on iOS") } @@ -327,11 +332,15 @@ impl Inner { } pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = UiKitHandle::empty(); - handle.ui_window = self.window as _; - handle.ui_view = self.view as _; - handle.ui_view_controller = self.view_controller as _; - RawWindowHandle::UiKit(handle) + let mut window_handle = UiKitWindowHandle::empty(); + window_handle.ui_window = self.window as _; + window_handle.ui_view = self.view as _; + window_handle.ui_view_controller = self.view_controller as _; + RawWindowHandle::UiKit(window_handle) + } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::UiKit(UiKitDisplayHandle::empty()) } } @@ -371,15 +380,15 @@ impl DerefMut for Window { } impl Window { - pub fn new( + pub(crate) fn new( _event_loop: &EventLoopWindowTarget, window_attributes: WindowAttributes, platform_attributes: PlatformSpecificWindowBuilderAttributes, ) -> Result { - if let Some(_) = window_attributes.min_inner_size { + if window_attributes.min_inner_size.is_some() { warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); } - if let Some(_) = window_attributes.max_inner_size { + if window_attributes.max_inner_size.is_some() { warn!("`WindowAttributes::max_inner_size` is ignored on iOS"); } if window_attributes.always_on_top { @@ -415,7 +424,7 @@ impl Window { None => screen_bounds, }; - let view = view::create_view(&window_attributes, &platform_attributes, frame.clone()); + let view = view::create_view(&window_attributes, &platform_attributes, frame); let gl_or_metal_backed = { let view_class: id = msg_send![view, class]; @@ -448,7 +457,7 @@ impl Window { // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` // event on window creation if the DPI factor != 1.0 let scale_factor: CGFloat = msg_send![view, contentScaleFactor]; - let scale_factor: f64 = scale_factor.into(); + let scale_factor = scale_factor as f64; if scale_factor != 1.0 { let bounds: CGRect = msg_send![view, bounds]; let screen: id = msg_send![window, screen]; @@ -498,7 +507,7 @@ impl Inner { "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" ); let scale_factor = scale_factor as CGFloat; - let () = msg_send![self.view, setContentScaleFactor: scale_factor]; + let _: () = msg_send![self.view, setContentScaleFactor: scale_factor]; } } @@ -519,7 +528,7 @@ impl Inner { pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { unsafe { let prefers_home_indicator_hidden = if hidden { YES } else { NO }; - let () = msg_send![ + let _: () = msg_send![ self.view_controller, setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden ]; @@ -529,7 +538,7 @@ impl Inner { pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { let edges: UIRectEdge = edges.into(); unsafe { - let () = msg_send![ + let _: () = msg_send![ self.view_controller, setPreferredScreenEdgesDeferringSystemGestures: edges ]; @@ -539,7 +548,7 @@ impl Inner { pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { unsafe { let status_bar_hidden = if hidden { YES } else { NO }; - let () = msg_send![ + let _: () = msg_send![ self.view_controller, setPrefersStatusBarHidden: status_bar_hidden ]; @@ -550,11 +559,11 @@ impl Inner { impl Inner { // requires main thread unsafe fn screen_frame(&self) -> CGRect { - self.to_screen_space(msg_send![self.window, bounds]) + self.rect_to_screen_space(msg_send![self.window, bounds]) } // requires main thread - unsafe fn to_screen_space(&self, rect: CGRect) -> CGRect { + unsafe fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { let screen: id = msg_send![self.window, screen]; if !screen.is_null() { let screen_space: id = msg_send![screen, coordinateSpace]; @@ -565,7 +574,7 @@ impl Inner { } // requires main thread - unsafe fn from_screen_space(&self, rect: CGRect) -> CGRect { + unsafe fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { let screen: id = msg_send![self.window, screen]; if !screen.is_null() { let screen_space: id = msg_send![screen, coordinateSpace]; @@ -590,9 +599,9 @@ impl Inner { height: bounds.size.height - safe_area.top - safe_area.bottom, }, }; - self.to_screen_space(safe_bounds) + self.rect_to_screen_space(safe_bounds) } else { - let screen_frame = self.to_screen_space(bounds); + let screen_frame = self.rect_to_screen_space(bounds); let status_bar_frame: CGRect = { let app: id = msg_send![class!(UIApplication), sharedApplication]; assert!( @@ -636,6 +645,20 @@ impl WindowId { } } +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.window as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self { + window: raw_id as _, + } + } +} + unsafe impl Send for WindowId {} unsafe impl Sync for WindowId {} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index ece8d34e60..f3c9e9c2e8 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -11,26 +11,35 @@ compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); #[cfg(feature = "wayland")] use std::error::Error; + use std::{collections::VecDeque, env, fmt}; #[cfg(feature = "x11")] use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Arc}; +#[cfg(feature = "x11")] +use once_cell::sync::Lazy; #[cfg(feature = "x11")] use parking_lot::Mutex; -use raw_window_handle::RawWindowHandle; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; #[cfg(feature = "x11")] pub use self::x11::XNotSupported; #[cfg(feature = "x11")] use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, XError}; +#[cfg(feature = "x11")] +use crate::platform::unix::XlibErrorHook; +#[cfg(feature = "wayland")] +use crate::window::Theme; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::Event, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + event_loop::{ + ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, + }, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, }; pub(crate) use crate::icon::RgbaIcon as PlatformIcon; @@ -49,7 +58,7 @@ pub mod x11; /// If this variable is set with any other value, winit will panic. const BACKEND_PREFERENCE_ENV_VAR: &str = "WINIT_UNIX_BACKEND"; -#[derive(Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) enum Backend { #[cfg(feature = "x11")] X, @@ -57,23 +66,27 @@ pub(crate) enum Backend { Wayland, } -#[derive(Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes { pub(crate) forced_backend: Option, pub(crate) any_thread: bool, } -impl Default for PlatformSpecificEventLoopAttributes { - fn default() -> Self { - Self { - forced_backend: None, - any_thread: false, - } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationName { + pub general: String, + pub instance: String, +} + +impl ApplicationName { + pub fn new(general: String, instance: String) -> Self { + Self { general, instance } } } #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { + pub name: Option, #[cfg(feature = "x11")] pub visual_infos: Option, #[cfg(feature = "x11")] @@ -83,20 +96,19 @@ pub struct PlatformSpecificWindowBuilderAttributes { #[cfg(feature = "x11")] pub base_size: Option, #[cfg(feature = "x11")] - pub class: Option<(String, String)>, - #[cfg(feature = "x11")] pub override_redirect: bool, #[cfg(feature = "x11")] pub x11_window_types: Vec, #[cfg(feature = "x11")] pub gtk_theme_variant: Option, #[cfg(feature = "wayland")] - pub app_id: Option, + pub csd_theme: Option, } impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { + name: None, #[cfg(feature = "x11")] visual_infos: None, #[cfg(feature = "x11")] @@ -106,24 +118,20 @@ impl Default for PlatformSpecificWindowBuilderAttributes { #[cfg(feature = "x11")] base_size: None, #[cfg(feature = "x11")] - class: None, - #[cfg(feature = "x11")] override_redirect: false, #[cfg(feature = "x11")] x11_window_types: vec![XWindowType::Normal], #[cfg(feature = "x11")] gtk_theme_variant: None, #[cfg(feature = "wayland")] - app_id: None, + csd_theme: None, } } } #[cfg(feature = "x11")] -lazy_static! { - pub static ref X11_BACKEND: Mutex, XNotSupported>> = - Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new)); -} +pub static X11_BACKEND: Lazy, XNotSupported>>> = + Lazy::new(|| Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new))); #[derive(Debug, Clone)] pub enum OsError { @@ -156,19 +164,23 @@ pub enum Window { } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum WindowId { - #[cfg(feature = "x11")] - X(x11::WindowId), - #[cfg(feature = "wayland")] - Wayland(wayland::WindowId), +pub struct WindowId(u64); + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id) + } } impl WindowId { pub const unsafe fn dummy() -> Self { - #[cfg(feature = "wayland")] - return WindowId::Wayland(wayland::WindowId::dummy()); - #[cfg(all(not(feature = "wayland"), feature = "x11"))] - return WindowId::X(x11::WindowId::dummy()); + Self(0) } } @@ -246,6 +258,11 @@ impl MonitorHandle { x11_or_wayland!(match self; MonitorHandle(m) => m.position()) } + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.refresh_rate_millihertz()) + } + #[inline] pub fn scale_factor(&self) -> f64 { x11_or_wayland!(match self; MonitorHandle(m) => m.scale_factor() as f64) @@ -277,8 +294,8 @@ impl VideoMode { } #[inline] - pub fn refresh_rate(&self) -> u16 { - x11_or_wayland!(match self; VideoMode(m) => m.refresh_rate()) + pub fn refresh_rate_millihertz(&self) -> u32 { + x11_or_wayland!(match self; VideoMode(m) => m.refresh_rate_millihertz()) } #[inline] @@ -289,7 +306,7 @@ impl VideoMode { impl Window { #[inline] - pub fn new( + pub(crate) fn new( window_target: &EventLoopWindowTarget, attribs: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, @@ -308,7 +325,12 @@ impl Window { #[inline] pub fn id(&self) -> WindowId { - x11_or_wayland!(match self; Window(w) => w.id(); as WindowId) + match self { + #[cfg(feature = "wayland")] + Self::Wayland(window) => window.id(), + #[cfg(feature = "x11")] + Self::X(window) => window.id(), + } } #[inline] @@ -382,8 +404,8 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(grab)) + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) } #[inline] @@ -471,6 +493,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.set_ime_position(position)) } + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed)) + } + #[inline] pub fn focus_window(&self) { match self { @@ -547,16 +574,22 @@ impl Window { } } + #[inline] pub fn raw_window_handle(&self) -> RawWindowHandle { - match self { - #[cfg(feature = "x11")] - Window::X(ref window) => RawWindowHandle::Xlib(window.raw_window_handle()), - #[cfg(feature = "wayland")] - Window::Wayland(ref window) => RawWindowHandle::Wayland(window.raw_window_handle()), - } + x11_or_wayland!(match self; Window(window) => window.raw_window_handle()) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle()) } } +/// Hooks for X11 errors. +#[cfg(feature = "x11")] +pub(crate) static mut XLIB_ERROR_HOOKS: Lazy>> = + Lazy::new(|| Mutex::new(Vec::new())); + #[cfg(feature = "x11")] unsafe extern "C" fn x_error_callback( display: *mut x11::ffi::Display, @@ -564,6 +597,12 @@ unsafe extern "C" fn x_error_callback( ) -> c_int { let xconn_lock = X11_BACKEND.lock(); if let Ok(ref xconn) = *xconn_lock { + // Call all the hooks. + let mut error_handled = false; + for hook in XLIB_ERROR_HOOKS.lock().iter() { + error_handled |= hook(display as *mut _, event as *mut _); + } + // `assume_init` is safe here because the array consists of `MaybeUninit` values, // which do not require initialization. let mut buf: [MaybeUninit; 1024] = MaybeUninit::uninit().assume_init(); @@ -582,7 +621,10 @@ unsafe extern "C" fn x_error_callback( minor_code: (*event).minor_code, }; - error!("X11 error: {:#?}", error); + // Don't log error. + if !error_handled { + error!("X11 error: {:#?}", error); + } *xconn.latest_error.lock() = Some(error); } @@ -592,7 +634,7 @@ unsafe extern "C" fn x_error_callback( pub enum EventLoop { #[cfg(feature = "wayland")] - Wayland(wayland::EventLoop), + Wayland(Box>), #[cfg(feature = "x11")] X(x11::EventLoop), } @@ -682,7 +724,7 @@ impl EventLoop { #[cfg(feature = "wayland")] fn new_wayland_any_thread() -> Result, Box> { - wayland::EventLoop::new().map(EventLoop::Wayland) + wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) } #[cfg(feature = "x11")] @@ -714,7 +756,7 @@ impl EventLoop { } pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget { - x11_or_wayland!(match self; EventLoop(evl) => evl.window_target()) + x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target()) } } @@ -775,6 +817,20 @@ impl EventLoopWindowTarget { } } } + + #[inline] + pub fn set_device_event_filter(&self, _filter: DeviceEventFilter) { + match *self { + #[cfg(feature = "wayland")] + EventLoopWindowTarget::Wayland(_) => (), + #[cfg(feature = "x11")] + EventLoopWindowTarget::X(ref evlp) => evlp.set_device_event_filter(_filter), + } + } + + pub fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle()) + } } fn sticky_exit_callback( diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs index ae213cb64a..f3e9b2401c 100644 --- a/src/platform_impl/linux/wayland/env.rs +++ b/src/platform_impl/linux/wayland/env.rs @@ -24,23 +24,23 @@ use sctk::shm::ShmHandler; /// Set of extra features that are supported by the compositor. #[derive(Debug, Clone, Copy)] pub struct WindowingFeatures { - cursor_grab: bool, + pointer_constraints: bool, xdg_activation: bool, } impl WindowingFeatures { /// Create `WindowingFeatures` based on the presented interfaces. pub fn new(env: &Environment) -> Self { - let cursor_grab = env.get_global::().is_some(); + let pointer_constraints = env.get_global::().is_some(); let xdg_activation = env.get_global::().is_some(); Self { - cursor_grab, + pointer_constraints, xdg_activation, } } - pub fn cursor_grab(&self) -> bool { - self.cursor_grab + pub fn pointer_constraints(&self) -> bool { + self.pointer_constraints } pub fn xdg_activation(&self) -> bool { diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index 0a69365b60..60f951fc33 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -2,10 +2,13 @@ use std::cell::RefCell; use std::collections::HashMap; use std::error::Error; use std::io::Result as IOResult; +use std::mem; use std::process; use std::rc::Rc; use std::time::{Duration, Instant}; +use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; + use sctk::reexports::client::protocol::wl_compositor::WlCompositor; use sctk::reexports::client::protocol::wl_shm::WlShm; use sctk::reexports::client::Display; @@ -24,7 +27,7 @@ use crate::platform_impl::EventLoopWindowTarget as PlatformEventLoopWindowTarget use super::env::{WindowingFeatures, WinitEnv}; use super::output::OutputManager; use super::seat::SeatManager; -use super::window::shim::{self, WindowRequest, WindowUpdate}; +use super::window::shim::{self, WindowCompositorUpdate, WindowUserRequest}; use super::{DeviceId, WindowId}; mod proxy; @@ -32,10 +35,9 @@ mod sink; mod state; pub use proxy::EventLoopProxy; +pub use sink::EventSink; pub use state::WinitState; -use sink::EventSink; - type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; pub struct EventLoopWindowTarget { @@ -65,14 +67,25 @@ pub struct EventLoopWindowTarget { /// Theme manager to manage cursors. /// - /// It's being shared amoung all windows to avoid loading + /// It's being shared between all windows to avoid loading /// multiple similar themes. pub theme_manager: ThemeManager, _marker: std::marker::PhantomData, } +impl EventLoopWindowTarget { + pub fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = WaylandDisplayHandle::empty(); + display_handle.display = self.display.get_display_ptr() as *mut _; + RawDisplayHandle::Wayland(display_handle) + } +} + pub struct EventLoop { + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WinitDispatcher, + /// Event loop. event_loop: calloop::EventLoop<'static, WinitState>, @@ -85,9 +98,6 @@ pub struct EventLoop { /// Sender of user events. user_events_sender: calloop::channel::Sender, - /// Dispatcher of Wayland events. - pub wayland_dispatcher: WinitDispatcher, - /// Window target. window_target: RootEventLoopWindowTarget, @@ -155,17 +165,19 @@ impl EventLoop { let (event_loop_awakener, event_loop_awakener_source) = calloop::ping::make_ping()?; // Handler of window requests. - event_loop.handle().insert_source( - event_loop_awakener_source, - move |_, _, winit_state| { - shim::handle_window_requests(winit_state); - }, - )?; + event_loop + .handle() + .insert_source(event_loop_awakener_source, move |_, _, state| { + // Drain events here as well to account for application doing batch event processing + // on RedrawEventsCleared. + shim::handle_window_requests(state); + })?; let event_loop_handle = event_loop.handle(); let window_map = HashMap::new(); let event_sink = EventSink::new(); - let window_updates = HashMap::new(); + let window_user_requests = HashMap::new(); + let window_compositor_updates = HashMap::new(); // Create event loop window target. let event_loop_window_target = EventLoopWindowTarget { @@ -174,7 +186,8 @@ impl EventLoop { state: RefCell::new(WinitState { window_map, event_sink, - window_updates, + window_user_requests, + window_compositor_updates, }), event_loop_handle, output_manager, @@ -223,7 +236,12 @@ impl EventLoop { &mut control_flow, ); - let mut window_updates: Vec<(WindowId, WindowUpdate)> = Vec::new(); + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + callback(Event::Resumed, &self.window_target, &mut control_flow); + + let mut window_compositor_updates: Vec<(WindowId, WindowCompositorUpdate)> = Vec::new(); + let mut window_user_requests: Vec<(WindowId, WindowUserRequest)> = Vec::new(); let mut event_sink_back_buffer = Vec::new(); // NOTE We break on errors from dispatches, since if we've got protocol error @@ -344,27 +362,26 @@ impl EventLoop { ); } - // Process 'new' pending updates. - self.with_window_target(|window_target| { - let state = window_target.state.get_mut(); - window_updates.clear(); - window_updates.extend( + // Process 'new' pending updates from compositor. + self.with_state(|state| { + window_compositor_updates.clear(); + window_compositor_updates.extend( state - .window_updates + .window_compositor_updates .iter_mut() - .map(|(wid, window_update)| (*wid, window_update.take())), + .map(|(wid, window_update)| (*wid, mem::take(window_update))), ); }); - for (window_id, window_update) in window_updates.iter_mut() { - if let Some(scale_factor) = window_update.scale_factor.map(|f| f as f64) { - let mut physical_size = self.with_window_target(|window_target| { - let state = window_target.state.get_mut(); + for (window_id, window_compositor_update) in window_compositor_updates.iter_mut() { + if let Some(scale_factor) = window_compositor_update.scale_factor.map(|f| f as f64) + { + let mut physical_size = self.with_state(|state| { let window_handle = state.window_map.get(window_id).unwrap(); let mut size = window_handle.size.lock().unwrap(); // Update the new logical size if it was changed. - let window_size = window_update.size.unwrap_or(*size); + let window_size = window_compositor_update.size.unwrap_or(*size); *size = window_size; window_size.to_physical(scale_factor) @@ -372,9 +389,7 @@ impl EventLoop { sticky_exit_callback( Event::WindowEvent { - window_id: crate::window::WindowId( - crate::platform_impl::WindowId::Wayland(*window_id), - ), + window_id: crate::window::WindowId(*window_id), event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size: &mut physical_size, @@ -388,27 +403,27 @@ impl EventLoop { // We don't update size on a window handle since we'll do that later // when handling size update. let new_logical_size = physical_size.to_logical(scale_factor); - window_update.size = Some(new_logical_size); + window_compositor_update.size = Some(new_logical_size); } - if let Some(size) = window_update.size.take() { - let physical_size = self.with_window_target(|window_target| { - let state = window_target.state.get_mut(); + if let Some(size) = window_compositor_update.size.take() { + let physical_size = self.with_state(|state| { let window_handle = state.window_map.get_mut(window_id).unwrap(); let mut window_size = window_handle.size.lock().unwrap(); // Always issue resize event on scale factor change. - let physical_size = - if window_update.scale_factor.is_none() && *window_size == size { - // The size hasn't changed, don't inform downstream about that. - None - } else { - *window_size = size; - let scale_factor = - sctk::get_surface_scale_factor(window_handle.window.surface()); - let physical_size = size.to_physical(scale_factor as f64); - Some(physical_size) - }; + let physical_size = if window_compositor_update.scale_factor.is_none() + && *window_size == size + { + // The size hasn't changed, don't inform downstream about that. + None + } else { + *window_size = size; + let scale_factor = + sctk::get_surface_scale_factor(window_handle.window.surface()); + let physical_size = size.to_physical(scale_factor as f64); + Some(physical_size) + }; // We still perform all of those resize related logic even if the size // hasn't changed, since GNOME relies on `set_geometry` calls after @@ -417,16 +432,11 @@ impl EventLoop { window_handle.window.refresh(); // Mark that refresh isn't required, since we've done it right now. - window_update.refresh_frame = false; - - // Queue request for redraw into the next iteration of the loop, since - // resize and scale factor changes are double buffered. - window_handle - .pending_window_requests - .lock() + state + .window_user_requests + .get_mut(window_id) .unwrap() - .push(WindowRequest::Redraw); - window_target.event_loop_awakener.ping(); + .refresh_frame = false; physical_size }); @@ -434,9 +444,7 @@ impl EventLoop { if let Some(physical_size) = physical_size { sticky_exit_callback( Event::WindowEvent { - window_id: crate::window::WindowId( - crate::platform_impl::WindowId::Wayland(*window_id), - ), + window_id: crate::window::WindowId(*window_id), event: WindowEvent::Resized(physical_size), }, &self.window_target, @@ -446,12 +454,11 @@ impl EventLoop { } } - if window_update.close_window { + // If the close is requested, send it here. + if window_compositor_update.close_window { sticky_exit_callback( Event::WindowEvent { - window_id: crate::window::WindowId( - crate::platform_impl::WindowId::Wayland(*window_id), - ), + window_id: crate::window::WindowId(*window_id), event: WindowEvent::CloseRequested, }, &self.window_target, @@ -464,8 +471,7 @@ impl EventLoop { // The purpose of the back buffer and that swap is to not hold borrow_mut when // we're doing callback to the user, since we can double borrow if the user decides // to create a window in one of those callbacks. - self.with_window_target(|window_target| { - let state = window_target.state.get_mut(); + self.with_state(|state| { std::mem::swap( &mut event_sink_back_buffer, &mut state.event_sink.window_events, @@ -486,26 +492,42 @@ impl EventLoop { &mut callback, ); + // Apply user requests, so every event required resize and latter surface commit will + // be applied right before drawing. This will also ensure that every `RedrawRequested` + // event will be delivered in time. + self.with_state(|state| { + shim::handle_window_requests(state); + }); + + // Process 'new' pending updates from compositor. + self.with_state(|state| { + window_user_requests.clear(); + window_user_requests.extend( + state + .window_user_requests + .iter_mut() + .map(|(wid, window_request)| (*wid, mem::take(window_request))), + ); + }); + // Handle RedrawRequested events. - for (window_id, window_update) in window_updates.iter() { + for (window_id, mut window_request) in window_user_requests.iter() { // Handle refresh of the frame. - if window_update.refresh_frame { - self.with_window_target(|window_target| { - let state = window_target.state.get_mut(); + if window_request.refresh_frame { + self.with_state(|state| { let window_handle = state.window_map.get_mut(window_id).unwrap(); window_handle.window.refresh(); - if !window_update.redraw_requested { - window_handle.window.surface().commit(); - } }); + + // In general refreshing the frame requires surface commit, those force user + // to redraw. + window_request.redraw_requested = true; } // Handle redraw request. - if window_update.redraw_requested { + if window_request.redraw_requested { sticky_exit_callback( - Event::RedrawRequested(crate::window::WindowId( - crate::platform_impl::WindowId::Wayland(*window_id), - )), + Event::RedrawRequested(crate::window::WindowId(*window_id)), &self.window_target, &mut control_flow, &mut callback, @@ -536,9 +558,9 @@ impl EventLoop { &self.window_target } - fn with_window_target) -> U>(&mut self, f: F) -> U { + fn with_state U>(&mut self, f: F) -> U { let state = match &mut self.window_target.p { - PlatformEventLoopWindowTarget::Wayland(window_target) => window_target, + PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), #[cfg(feature = "x11")] _ => unreachable!(), }; @@ -553,6 +575,8 @@ impl EventLoop { _ => unreachable!(), }; - self.event_loop.dispatch(timeout, state) + self.event_loop + .dispatch(timeout, state) + .map_err(|error| error.into()) } } diff --git a/src/platform_impl/linux/wayland/event_loop/sink.rs b/src/platform_impl/linux/wayland/event_loop/sink.rs index 303ab826e4..26a895e569 100644 --- a/src/platform_impl/linux/wayland/event_loop/sink.rs +++ b/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -1,7 +1,7 @@ //! An event loop's sink to deliver events from the Wayland event callbacks. use crate::event::{DeviceEvent, DeviceId as RootDeviceId, Event, WindowEvent}; -use crate::platform_impl::platform::{DeviceId as PlatformDeviceId, WindowId as PlatformWindowId}; +use crate::platform_impl::platform::DeviceId as PlatformDeviceId; use crate::window::WindowId as RootWindowId; use super::{DeviceId, WindowId}; @@ -30,7 +30,7 @@ impl EventSink { pub fn push_window_event(&mut self, event: WindowEvent<'static>, window_id: WindowId) { self.window_events.push(Event::WindowEvent { event, - window_id: RootWindowId(PlatformWindowId::Wayland(window_id)), + window_id: RootWindowId(window_id), }); } } diff --git a/src/platform_impl/linux/wayland/event_loop/state.rs b/src/platform_impl/linux/wayland/event_loop/state.rs index 7aad9eccd2..0cf1c6680e 100644 --- a/src/platform_impl/linux/wayland/event_loop/state.rs +++ b/src/platform_impl/linux/wayland/event_loop/state.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use super::EventSink; -use crate::platform_impl::wayland::window::shim::{WindowHandle, WindowUpdate}; +use crate::platform_impl::wayland::window::shim::{ + WindowCompositorUpdate, WindowHandle, WindowUserRequest, +}; use crate::platform_impl::wayland::WindowId; /// Wrapper to carry winit's state. @@ -12,10 +14,14 @@ pub struct WinitState { /// event loop and forwarded downstream afterwards. pub event_sink: EventSink, + /// Window updates comming from the user requests. Those are separatelly dispatched right after + /// `MainEventsCleared`. + pub window_user_requests: HashMap, + /// Window updates, which are coming from SCTK or the compositor, which require /// calling back to the winit's downstream. They are handled right in the event loop, /// unlike the ones coming from buffers on the `WindowHandle`'s. - pub window_updates: HashMap, + pub window_compositor_updates: HashMap, /// Window map containing all SCTK windows. Since those windows aren't allowed /// to be sent to other threads, they live on the event loop's thread diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index 4ed564aec0..9871b2f23e 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -8,6 +8,7 @@ use sctk::reexports::client::protocol::wl_surface::WlSurface; +pub use crate::platform_impl::platform::WindowId; pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; pub use output::{MonitorHandle, VideoMode}; pub use window::Window; @@ -27,16 +28,7 @@ impl DeviceId { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId(usize); - -impl WindowId { - pub const unsafe fn dummy() -> Self { - WindowId(0) - } -} - #[inline] fn make_wid(surface: &WlSurface) -> WindowId { - WindowId(surface.as_ref().c_ptr() as usize) + WindowId(surface.as_ref().c_ptr() as u64) } diff --git a/src/platform_impl/linux/wayland/output.rs b/src/platform_impl/linux/wayland/output.rs index 79cc523f04..e3a434781e 100644 --- a/src/platform_impl/linux/wayland/output.rs +++ b/src/platform_impl/linux/wayland/output.rs @@ -167,6 +167,16 @@ impl MonitorHandle { .into() } + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + sctk::output::with_output_info(&self.proxy, |info| { + info.modes + .iter() + .find_map(|mode| mode.is_current.then(|| mode.refresh_rate as u32)) + }) + .flatten() + } + #[inline] pub fn scale_factor(&self) -> i32 { sctk::output::with_output_info(&self.proxy, |info| info.scale_factor).unwrap_or(1) @@ -175,14 +185,14 @@ impl MonitorHandle { #[inline] pub fn video_modes(&self) -> impl Iterator { let modes = sctk::output::with_output_info(&self.proxy, |info| info.modes.clone()) - .unwrap_or_else(Vec::new); + .unwrap_or_default(); let monitor = self.clone(); modes.into_iter().map(move |mode| RootVideoMode { video_mode: PlatformVideoMode::Wayland(VideoMode { size: (mode.dimensions.0 as u32, mode.dimensions.1 as u32).into(), - refresh_rate: (mode.refresh_rate as f32 / 1000.0).round() as u16, + refresh_rate_millihertz: mode.refresh_rate as u32, bit_depth: 32, monitor: monitor.clone(), }), @@ -194,7 +204,7 @@ impl MonitorHandle { pub struct VideoMode { pub(crate) size: PhysicalSize, pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) refresh_rate_millihertz: u32, pub(crate) monitor: MonitorHandle, } @@ -210,8 +220,8 @@ impl VideoMode { } #[inline] - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } pub fn monitor(&self) -> RootMonitorHandle { diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs index c6e0ad456e..262a014bac 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -7,7 +7,7 @@ use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::Attached; -use sctk::reexports::calloop::{LoopHandle, RegistrationToken}; +use sctk::reexports::calloop::LoopHandle; use sctk::seat::keyboard; @@ -20,12 +20,6 @@ mod keymap; pub(crate) struct Keyboard { pub keyboard: WlKeyboard, - - /// The source for repeat keys. - pub repeat_token: Option, - - /// LoopHandle to drop `RepeatSource`, when dropping the keyboard. - pub loop_handle: LoopHandle<'static, WinitState>, } impl Keyboard { @@ -35,7 +29,7 @@ impl Keyboard { modifiers_state: Rc>, ) -> Option { let mut inner = KeyboardInner::new(modifiers_state); - let keyboard_data = keyboard::map_keyboard_repeat( + let keyboard = keyboard::map_keyboard_repeat( loop_handle.clone(), seat, None, @@ -44,15 +38,10 @@ impl Keyboard { let winit_state = dispatch_data.get::().unwrap(); handlers::handle_keyboard(event, &mut inner, winit_state); }, - ); - - let (keyboard, repeat_token) = keyboard_data.ok()?; + ) + .ok()?; - Some(Self { - keyboard, - loop_handle, - repeat_token: Some(repeat_token), - }) + Some(Self { keyboard }) } } @@ -61,10 +50,6 @@ impl Drop for Keyboard { if self.keyboard.as_ref().version() >= 3 { self.keyboard.release(); } - - if let Some(repeat_token) = self.repeat_token.take() { - self.loop_handle.remove(repeat_token); - } } } diff --git a/src/platform_impl/linux/wayland/seat/pointer/data.rs b/src/platform_impl/linux/wayland/seat/pointer/data.rs index 1da60d3526..17e7a57a07 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/data.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/data.rs @@ -5,8 +5,9 @@ use std::rc::Rc; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Attached; -use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1}; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; use crate::event::{ModifiersState, TouchPhase}; @@ -25,10 +26,14 @@ pub(super) struct PointerData { pub pointer_constraints: Option>, pub confined_pointer: Rc>>, + pub locked_pointer: Rc>>, - /// A latest event serial. + /// Latest observed serial in pointer events. pub latest_serial: Rc>, + /// Latest observed serial in pointer enter events. + pub latest_enter_serial: Rc>, + /// The currently accumulated axis data on a pointer. pub axis_data: AxisData, } @@ -36,13 +41,16 @@ pub(super) struct PointerData { impl PointerData { pub fn new( confined_pointer: Rc>>, + locked_pointer: Rc>>, pointer_constraints: Option>, modifiers_state: Rc>, ) -> Self { Self { surface: None, latest_serial: Rc::new(Cell::new(0)), + latest_enter_serial: Rc::new(Cell::new(0)), confined_pointer, + locked_pointer, modifiers_state, pointer_constraints, axis_data: AxisData::new(), diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs index c5e605fe59..7348cc66f1 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -42,6 +42,7 @@ pub(super) fn handle_pointer( .. } => { pointer_data.latest_serial.replace(serial); + pointer_data.latest_enter_serial.replace(serial); let window_id = wayland::make_wid(&surface); if !winit_state.window_map.contains_key(&window_id) { @@ -59,8 +60,10 @@ pub(super) fn handle_pointer( let winit_pointer = WinitPointer { pointer, confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + locked_pointer: Rc::downgrade(&pointer_data.locked_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + latest_enter_serial: pointer_data.latest_enter_serial.clone(), seat, }; window_handle.pointer_entered(winit_pointer); @@ -102,8 +105,10 @@ pub(super) fn handle_pointer( let winit_pointer = WinitPointer { pointer, confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + locked_pointer: Rc::downgrade(&pointer_data.locked_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + latest_enter_serial: pointer_data.latest_enter_serial.clone(), seat, }; window_handle.pointer_left(winit_pointer); @@ -297,17 +302,17 @@ pub(super) fn handle_pointer( #[inline] pub(super) fn handle_relative_pointer(event: RelativePointerEvent, winit_state: &mut WinitState) { - match event { - RelativePointerEvent::RelativeMotion { - dx_unaccel, - dy_unaccel, - .. - } => winit_state.event_sink.push_device_event( + if let RelativePointerEvent::RelativeMotion { + dx_unaccel, + dy_unaccel, + .. + } = event + { + winit_state.event_sink.push_device_event( DeviceEvent::MouseMotion { delta: (dx_unaccel, dy_unaccel), }, DeviceId, - ), - _ => (), + ) } } diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index c6726224c0..b43deebb38 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -11,12 +11,14 @@ use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_rela use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1, Lifetime}; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; use sctk::seat::pointer::{ThemeManager, ThemedPointer}; -use sctk::window::{FallbackFrame, Window}; +use sctk::window::Window; use crate::event::ModifiersState; use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::window::WinitFrame; use crate::window::CursorIcon; mod data; @@ -34,9 +36,17 @@ pub struct WinitPointer { /// Cursor to handle confine requests. confined_pointer: Weak>>, + /// Cursor to handle locked requests. + locked_pointer: Weak>>, + /// Latest observed serial in pointer events. + /// used by Window::start_interactive_move() latest_serial: Rc>, + /// Latest observed serial in pointer enter events. + /// used by Window::set_cursor() + latest_enter_serial: Rc>, + /// Seat. seat: WlSeat, } @@ -58,7 +68,9 @@ impl WinitPointer { Some(cursor_icon) => cursor_icon, None => { // Hide the cursor. - (*self.pointer).set_cursor(self.latest_serial.get(), None, 0, 0); + // WlPointer::set_cursor() expects the serial of the last *enter* + // event (compare to to start_interactive_move()). + (*self.pointer).set_cursor(self.latest_enter_serial.get(), None, 0, 0); return; } }; @@ -106,7 +118,7 @@ impl WinitPointer { CursorIcon::ZoomOut => &["zoom-out"], }; - let serial = Some(self.latest_serial.get()); + let serial = Some(self.latest_enter_serial.get()); for cursor in cursors { if self.pointer.set_cursor(cursor, serial).is_ok() { return; @@ -150,7 +162,55 @@ impl WinitPointer { } } - pub fn drag_window(&self, window: &Window) { + pub fn lock(&self, surface: &WlSurface) { + let pointer_constraints = match &self.pointer_constraints { + Some(pointer_constraints) => pointer_constraints, + None => return, + }; + + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + *locked_pointer.borrow_mut() = Some(init_locked_pointer( + pointer_constraints, + surface, + &*self.pointer, + )); + } + + pub fn unlock(&self) { + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + let mut locked_pointer = locked_pointer.borrow_mut(); + + if let Some(locked_pointer) = locked_pointer.take() { + locked_pointer.destroy(); + } + } + + pub fn set_cursor_position(&self, surface_x: u32, surface_y: u32) { + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + let locked_pointer = locked_pointer.borrow_mut(); + if let Some(locked_pointer) = locked_pointer.as_ref() { + locked_pointer.set_cursor_position_hint(surface_x.into(), surface_y.into()); + } + } + + pub fn drag_window(&self, window: &Window) { + // WlPointer::setart_interactive_move() expects the last serial of *any* + // pointer event (compare to set_cursor()). window.start_interactive_move(&self.seat, self.latest_serial.get()); } } @@ -165,6 +225,9 @@ pub(super) struct Pointers { /// Confined pointer. confined_pointer: Rc>>, + + /// Locked pointer. + locked_pointer: Rc>>, } impl Pointers { @@ -176,11 +239,15 @@ impl Pointers { modifiers_state: Rc>, ) -> Self { let confined_pointer = Rc::new(RefCell::new(None)); + let locked_pointer = Rc::new(RefCell::new(None)); + let pointer_data = Rc::new(RefCell::new(PointerData::new( confined_pointer.clone(), + locked_pointer.clone(), pointer_constraints.clone(), modifiers_state, ))); + let pointer_seat = seat.detach(); let pointer = theme_manager.theme_pointer_with_impl( seat, @@ -207,6 +274,7 @@ impl Pointers { pointer, relative_pointer, confined_pointer, + locked_pointer, } } } @@ -223,6 +291,11 @@ impl Drop for Pointers { confined_pointer.destroy(); } + // Drop lock ponter. + if let Some(locked_pointer) = self.locked_pointer.borrow_mut().take() { + locked_pointer.destroy(); + } + // Drop the pointer itself in case it's possible. if self.pointer.as_ref().version() >= 3 { self.pointer.release(); @@ -234,7 +307,7 @@ pub(super) fn init_relative_pointer( relative_pointer_manager: &ZwpRelativePointerManagerV1, pointer: &WlPointer, ) -> ZwpRelativePointerV1 { - let relative_pointer = relative_pointer_manager.get_relative_pointer(&*pointer); + let relative_pointer = relative_pointer_manager.get_relative_pointer(pointer); relative_pointer.quick_assign(move |_, event, mut dispatch_data| { let winit_state = dispatch_data.get::().unwrap(); handlers::handle_relative_pointer(event, winit_state); @@ -255,3 +328,16 @@ pub(super) fn init_confined_pointer( confined_pointer.detach() } + +pub(super) fn init_locked_pointer( + pointer_constraints: &Attached, + surface: &WlSurface, + pointer: &WlPointer, +) -> ZwpLockedPointerV1 { + let locked_pointer = + pointer_constraints.lock_pointer(surface, pointer, None, Lifetime::Persistent); + + locked_pointer.quick_assign(move |_, _, _| {}); + + locked_pointer.detach() +} diff --git a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs index 4ba13d6715..8f05bb60d0 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs @@ -5,11 +5,11 @@ use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input Event as TextInputEvent, ZwpTextInputV3, }; -use crate::event::WindowEvent; +use crate::event::{Ime, WindowEvent}; use crate::platform_impl::wayland; use crate::platform_impl::wayland::event_loop::WinitState; -use super::{TextInputHandler, TextInputInner}; +use super::{Preedit, TextInputHandler, TextInputInner}; #[inline] pub(super) fn handle_text_input( @@ -30,8 +30,11 @@ pub(super) fn handle_text_input( inner.target_window_id = Some(window_id); // Enable text input on that surface. - text_input.enable(); - text_input.commit(); + if window_handle.ime_allowed.get() { + text_input.enable(); + text_input.commit(); + event_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); + } // Notify a window we're currently over about text input handler. let text_input_handler = TextInputHandler { @@ -58,19 +61,45 @@ pub(super) fn handle_text_input( text_input: text_input.detach(), }; window_handle.text_input_left(text_input_handler); + event_sink.push_window_event(WindowEvent::Ime(Ime::Disabled), window_id); + } + TextInputEvent::PreeditString { + text, + cursor_begin, + cursor_end, + } => { + let cursor_begin = usize::try_from(cursor_begin).ok(); + let cursor_end = usize::try_from(cursor_end).ok(); + let text = text.unwrap_or_default(); + inner.pending_preedit = Some(Preedit { + text, + cursor_begin, + cursor_end, + }); } TextInputEvent::CommitString { text } => { - // Update currenly commited string. - inner.commit_string = text; + // Update currenly commited string and reset previous preedit. + inner.pending_preedit = None; + inner.pending_commit = Some(text.unwrap_or_default()); } TextInputEvent::Done { .. } => { - let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) { - (Some(window_id), Some(text)) => (window_id, text), + let window_id = match inner.target_window_id { + Some(window_id) => window_id, _ => return, }; - for ch in text.chars() { - event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id); + if let Some(text) = inner.pending_commit.take() { + event_sink.push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id); + } + + // Push preedit string we've got after latest commit. + if let Some(preedit) = inner.pending_preedit.take() { + let cursor_range = preedit + .cursor_begin + .map(|b| (b, preedit.cursor_end.unwrap_or(b))); + + let event = Ime::Preedit(preedit.text, cursor_range); + event_sink.push_window_event(WindowEvent::Ime(event), window_id); } } _ => (), 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 77f4ff0827..52ec94afaf 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -20,6 +20,17 @@ impl TextInputHandler { self.text_input.set_cursor_rectangle(x, y, 0, 0); self.text_input.commit(); } + + #[inline] + pub fn set_input_allowed(&self, allowed: bool) { + if allowed { + self.text_input.enable(); + } else { + self.text_input.disable(); + } + + self.text_input.commit(); + } } /// A wrapper around text input to automatically destroy the object on `Drop`. @@ -52,15 +63,25 @@ struct TextInputInner { /// Currently focused surface. target_window_id: Option, - /// Pending string to commit. - commit_string: Option, + /// Pending commit event which will be dispatched on `text_input_v3::Done`. + pending_commit: Option, + + /// Pending preedit event which will be dispatched on `text_input_v3::Done`. + pending_preedit: Option, +} + +struct Preedit { + text: String, + cursor_begin: Option, + cursor_end: Option, } impl TextInputInner { fn new() -> Self { Self { target_window_id: None, - commit_string: None, + pending_commit: None, + pending_preedit: None, } } } diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 73f2ec67db..db63781ddb 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -7,8 +7,10 @@ use sctk::reexports::client::Display; use sctk::reexports::calloop; -use raw_window_handle::WaylandHandle; -use sctk::window::{Decorations, FallbackFrame}; +use raw_window_handle::{ + RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, +}; +use sctk::window::Decorations; use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; @@ -17,7 +19,9 @@ use crate::platform_impl::{ MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes as PlatformAttributes, }; -use crate::window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}; +use crate::window::{ + CursorGrabMode, CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes, +}; use super::env::WindowingFeatures; use super::event_loop::WinitState; @@ -26,7 +30,15 @@ use super::{EventLoopWindowTarget, WindowId}; pub mod shim; -use shim::{WindowHandle, WindowRequest, WindowUpdate}; +use shim::{WindowCompositorUpdate, WindowHandle, WindowRequest, WindowUserRequest}; + +#[cfg(feature = "sctk-adwaita")] +pub type WinitFrame = sctk_adwaita::AdwaitaFrame; +#[cfg(not(feature = "sctk-adwaita"))] +pub type WinitFrame = sctk::window::FallbackFrame; + +#[cfg(feature = "sctk-adwaita")] +const WAYLAND_CSD_THEME_ENV_VAR: &str = "WINIT_WAYLAND_CSD_THEME"; pub struct Window { /// Window id. @@ -64,10 +76,13 @@ pub struct Window { /// Whether the window is decorated. decorated: AtomicBool, + + /// Grabbing mode. + cursor_grab_mode: Mutex, } impl Window { - pub fn new( + pub(crate) fn new( event_loop_window_target: &EventLoopWindowTarget, attributes: WindowAttributes, platform_attributes: PlatformAttributes, @@ -77,13 +92,22 @@ impl Window { .create_surface_with_scale_callback(move |scale, surface, mut dispatch_data| { let winit_state = dispatch_data.get::().unwrap(); - // Get the window that receiced the event. + // Get the window that received the event. let window_id = super::make_wid(&surface); - let mut window_update = winit_state.window_updates.get_mut(&window_id).unwrap(); + let mut window_compositor_update = winit_state + .window_compositor_updates + .get_mut(&window_id) + .unwrap(); + + // Mark that we need a frame refresh on the DPI change. + winit_state + .window_user_requests + .get_mut(&window_id) + .unwrap() + .refresh_frame = true; // Set pending scale factor. - window_update.scale_factor = Some(scale); - window_update.redraw_requested = true; + window_compositor_update.scale_factor = Some(scale); surface.set_buffer_scale(scale); }) @@ -105,7 +129,7 @@ impl Window { let theme_manager = event_loop_window_target.theme_manager.clone(); let mut window = event_loop_window_target .env - .create_window::( + .create_window::( surface.clone(), Some(theme_manager), (width, height), @@ -113,11 +137,19 @@ impl Window { use sctk::window::{Event, State}; let winit_state = dispatch_data.get::().unwrap(); - let mut window_update = winit_state.window_updates.get_mut(&window_id).unwrap(); + let mut window_compositor_update = winit_state + .window_compositor_updates + .get_mut(&window_id) + .unwrap(); + + let mut window_user_requests = winit_state + .window_user_requests + .get_mut(&window_id) + .unwrap(); match event { Event::Refresh => { - window_update.refresh_frame = true; + window_user_requests.refresh_frame = true; } Event::Configure { new_size, states } => { let is_maximized = states.contains(&State::Maximized); @@ -125,20 +157,30 @@ impl Window { let is_fullscreen = states.contains(&State::Fullscreen); fullscreen_clone.store(is_fullscreen, Ordering::Relaxed); - window_update.refresh_frame = true; - window_update.redraw_requested = true; + window_user_requests.refresh_frame = true; if let Some((w, h)) = new_size { - window_update.size = Some(LogicalSize::new(w, h)); + window_compositor_update.size = Some(LogicalSize::new(w, h)); } } Event::Close => { - window_update.close_window = true; + window_compositor_update.close_window = true; } } }, ) .map_err(|_| os_error!(OsError::WaylandMisc("failed to create window.")))?; + // Set CSD frame config from theme if specified, + // otherwise use upstream automatic selection. + #[cfg(feature = "sctk-adwaita")] + if let Some(theme) = platform_attributes.csd_theme.or_else(|| { + std::env::var(WAYLAND_CSD_THEME_ENV_VAR) + .ok() + .and_then(|s| s.as_str().try_into().ok()) + }) { + window.set_frame_config(theme.into()); + } + // Set decorations. if attributes.decorations { window.set_decorate(Decorations::FollowServer); @@ -159,8 +201,8 @@ impl Window { window.set_max_size(max_size); // Set Wayland specific window attributes. - if let Some(app_id) = platform_attributes.app_id { - window.set_app_id(app_id); + if let Some(name) = platform_attributes.name { + window.set_app_id(name.general); } // Set common window attributes. @@ -205,9 +247,9 @@ impl Window { let size = Arc::new(Mutex::new(LogicalSize::new(width, height))); // We should trigger redraw and commit the surface for the newly created window. - let mut window_update = WindowUpdate::new(); - window_update.refresh_frame = true; - window_update.redraw_requested = true; + let mut window_user_request = WindowUserRequest::new(); + window_user_request.refresh_frame = true; + window_user_request.redraw_requested = true; let window_id = super::make_wid(&surface); let window_requests = Arc::new(Mutex::new(Vec::with_capacity(64))); @@ -220,13 +262,26 @@ impl Window { window_requests.clone(), ); + // Set resizable state, so we can determine how to handle `Window::set_inner_size`. + window_handle.is_resizable.set(attributes.resizable); + let mut winit_state = event_loop_window_target.state.borrow_mut(); winit_state.window_map.insert(window_id, window_handle); + // On Wayland window doesn't have Focus by default and it'll get it later on. So be + // explicit here. + winit_state + .event_sink + .push_window_event(crate::event::WindowEvent::Focused(false), window_id); + + // Add state for the window. + winit_state + .window_user_requests + .insert(window_id, window_user_request); winit_state - .window_updates - .insert(window_id, WindowUpdate::new()); + .window_compositor_updates + .insert(window_id, WindowCompositorUpdate::new()); let windowing_features = event_loop_window_target.windowing_features; @@ -260,6 +315,7 @@ impl Window { windowing_features, resizeable: AtomicBool::new(attributes.resizable), decorated: AtomicBool::new(attributes.decorations), + cursor_grab_mode: Mutex::new(CursorGrabMode::None), }; Ok(window) @@ -377,6 +433,11 @@ impl Window { self.decorated.load(Ordering::Relaxed) } + #[inline] + pub fn set_csd_theme(&self, theme: Theme) { + self.send_request(WindowRequest::CsdThemeVariant(theme)); + } + #[inline] pub fn set_minimized(&self, minimized: bool) { // You can't unminimize the window on Wayland. @@ -444,12 +505,17 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - if !self.windowing_features.cursor_grab() { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + if !self.windowing_features.pointer_constraints() { + if mode == CursorGrabMode::None { + return Ok(()); + } + return Err(ExternalError::NotSupported(NotSupportedError::new())); } - self.send_request(WindowRequest::GrabCursor(grab)); + *self.cursor_grab_mode.lock().unwrap() = mode; + self.send_request(WindowRequest::SetCursorGrabMode(mode)); Ok(()) } @@ -464,15 +530,19 @@ impl Window { } #[inline] - pub fn set_cursor_position(&self, _: Position) -> Result<(), ExternalError> { - // XXX This is possible if the locked pointer is being used. We don't have any - // API for that right now, but it could be added in - // https://github.com/rust-windowing/winit/issues/1677. - // - // This function is essential for the locked pointer API. - // - // See pointer-constraints-unstable-v1.xml. - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + // Positon can be set only for locked cursor. + if *self.cursor_grab_mode.lock().unwrap() != CursorGrabMode::Locked { + return Err(ExternalError::Os(os_error!(OsError::WaylandMisc( + "cursor position can be set only for locked cursor." + )))); + } + + let scale_factor = self.scale_factor() as f64; + let position = position.to_logical(scale_factor); + self.send_request(WindowRequest::SetLockedCursorPosition(position)); + + Ok(()) } #[inline] @@ -493,7 +563,12 @@ impl Window { pub fn set_ime_position(&self, position: Position) { let scale_factor = self.scale_factor() as f64; let position = position.to_logical(scale_factor); - self.send_request(WindowRequest::IMEPosition(position)); + self.send_request(WindowRequest::ImePosition(position)); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.send_request(WindowRequest::AllowIme(allowed)); } #[inline] @@ -523,11 +598,17 @@ impl Window { } #[inline] - pub fn raw_window_handle(&self) -> WaylandHandle { - let mut handle = WaylandHandle::empty(); - handle.display = self.display.get_display_ptr() as *mut _; - handle.surface = self.surface.as_ref().c_ptr() as *mut _; - handle + pub fn raw_window_handle(&self) -> RawWindowHandle { + let mut window_handle = WaylandWindowHandle::empty(); + window_handle.surface = self.surface.as_ref().c_ptr() as *mut _; + RawWindowHandle::Wayland(window_handle) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = WaylandDisplayHandle::empty(); + display_handle.display = self.display.get_display_ptr() as *mut _; + RawDisplayHandle::Wayland(display_handle) } #[inline] @@ -542,3 +623,33 @@ impl Drop for Window { self.send_request(WindowRequest::Close); } } + +#[cfg(feature = "sctk-adwaita")] +impl From for sctk_adwaita::FrameConfig { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => sctk_adwaita::FrameConfig::light(), + Theme::Dark => sctk_adwaita::FrameConfig::dark(), + } + } +} + +impl TryFrom<&str> for Theme { + type Error = (); + + /// ``` + /// use winit::window::Theme; + /// + /// assert_eq!("dark".try_into(), Ok(Theme::Dark)); + /// assert_eq!("lIghT".try_into(), Ok(Theme::Light)); + /// ``` + fn try_from(theme: &str) -> Result { + if theme.eq_ignore_ascii_case("dark") { + Ok(Self::Dark) + } else if theme.eq_ignore_ascii_case("light") { + Ok(Self::Light) + } else { + Err(()) + } + } +} diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 60247b317e..de0e3f2c2a 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -1,4 +1,5 @@ use std::cell::Cell; +use std::mem::ManuallyDrop; use std::sync::{Arc, Mutex}; use sctk::reexports::client::protocol::wl_compositor::WlCompositor; @@ -8,18 +9,20 @@ use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activat use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_v1::XdgActivationV1; use sctk::environment::Environment; -use sctk::window::{Decorations, FallbackFrame, Window}; +use sctk::window::{Decorations, Window}; use crate::dpi::{LogicalPosition, LogicalSize}; -use crate::event::WindowEvent; +use crate::event::{Ime, WindowEvent}; use crate::platform_impl::wayland; use crate::platform_impl::wayland::env::WinitEnv; -use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::event_loop::{EventSink, WinitState}; use crate::platform_impl::wayland::seat::pointer::WinitPointer; use crate::platform_impl::wayland::seat::text_input::TextInputHandler; use crate::platform_impl::wayland::WindowId; -use crate::window::{CursorIcon, UserAttentionType}; +use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType}; + +use super::WinitFrame; /// A request to SCTK window from Winit window. #[derive(Debug, Clone)] @@ -38,8 +41,11 @@ pub enum WindowRequest { /// Change the cursor icon. NewCursorIcon(CursorIcon), - /// Grab cursor. - GrabCursor(bool), + /// Change cursor grabbing mode. + SetCursorGrabMode(CursorGrabMode), + + /// Set cursor position. + SetLockedCursorPosition(LogicalPosition), /// Drag window. DragWindow, @@ -53,6 +59,9 @@ pub enum WindowRequest { /// Request decorations change. Decorate(bool), + /// Request decorations change. + CsdThemeVariant(Theme), + /// Make the window resizeable. Resizeable(bool), @@ -69,7 +78,10 @@ pub enum WindowRequest { FrameSize(LogicalSize), /// Set IME window position. - IMEPosition(LogicalPosition), + ImePosition(LogicalPosition), + + /// Enable IME on the given window. + AllowIme(bool), /// Request Attention. /// @@ -86,56 +98,38 @@ pub enum WindowRequest { Close, } -/// Pending update to a window from SCTK window. -#[derive(Debug, Clone, Copy)] -pub struct WindowUpdate { +// The window update comming from the compositor. +#[derive(Default, Debug, Clone, Copy)] +pub struct WindowCompositorUpdate { /// New window size. pub size: Option>, /// New scale factor. pub scale_factor: Option, - /// Whether `redraw` was requested. - pub redraw_requested: bool, - - /// Wether the frame should be refreshed. - pub refresh_frame: bool, - /// Close the window. pub close_window: bool, } -impl WindowUpdate { +impl WindowCompositorUpdate { pub fn new() -> Self { - Self { - size: None, - scale_factor: None, - redraw_requested: false, - refresh_frame: false, - close_window: false, - } + Default::default() } +} - pub fn take(&mut self) -> Self { - let size = self.size.take(); - let scale_factor = self.scale_factor.take(); - - let redraw_requested = self.redraw_requested; - self.redraw_requested = false; - - let refresh_frame = self.refresh_frame; - self.refresh_frame = false; +/// Pending update to a window requested by the user. +#[derive(Default, Debug, Clone, Copy)] +pub struct WindowUserRequest { + /// Whether `redraw` was requested. + pub redraw_requested: bool, - let close_window = self.close_window; - self.close_window = false; + /// Wether the frame should be refreshed. + pub refresh_frame: bool, +} - Self { - size, - scale_factor, - redraw_requested, - refresh_frame, - close_window, - } +impl WindowUserRequest { + pub fn new() -> Self { + Default::default() } } @@ -143,7 +137,7 @@ impl WindowUpdate { /// and react to events. pub struct WindowHandle { /// An actual window. - pub window: Window, + pub window: ManuallyDrop>, /// The current size of the window. pub size: Arc>>, @@ -154,11 +148,17 @@ pub struct WindowHandle { /// Current cursor icon. pub cursor_icon: Cell, + /// Whether the window is resizable. + pub is_resizable: Cell, + + /// Allow IME events for that window. + pub ime_allowed: Cell, + /// Visible cursor or not. cursor_visible: Cell, /// Cursor confined to the surface. - confined: Cell, + cursor_grab_mode: Cell, /// Pointers over the current surface. pointers: Vec, @@ -179,7 +179,7 @@ pub struct WindowHandle { impl WindowHandle { pub fn new( env: &Environment, - window: Window, + window: Window, size: Arc>>, pending_window_requests: Arc>>, ) -> Self { @@ -189,38 +189,53 @@ impl WindowHandle { let compositor = env.get_global::().unwrap(); Self { - window, + window: ManuallyDrop::new(window), size, pending_window_requests, cursor_icon: Cell::new(CursorIcon::Default), - confined: Cell::new(false), + is_resizable: Cell::new(true), + cursor_grab_mode: Cell::new(CursorGrabMode::None), cursor_visible: Cell::new(true), pointers: Vec::new(), text_inputs: Vec::new(), xdg_activation, attention_requested: Cell::new(false), compositor, + ime_allowed: Cell::new(false), } } - pub fn set_cursor_grab(&self, grab: bool) { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) { // The new requested state matches the current confine status, return. - if self.confined.get() == grab { + let old_mode = self.cursor_grab_mode.replace(mode); + if old_mode == mode { return; } - self.confined.replace(grab); + // Clear old pointer data. + match old_mode { + CursorGrabMode::None => (), + CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.unconfine()), + CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.unlock()), + } - for pointer in self.pointers.iter() { - if self.confined.get() { - let surface = self.window.surface(); - pointer.confine(surface); - } else { - pointer.unconfine(); + let surface = self.window.surface(); + match mode { + CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.lock(surface)), + CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.confine(surface)), + CursorGrabMode::None => { + // Current lock/confine was already removed. } } } + pub fn set_locked_cursor_position(&self, position: LogicalPosition) { + // XXX the cursor locking is ensured inside `Window`. + self.pointers + .iter() + .for_each(|p| p.set_cursor_position(position.x, position.y)); + } + pub fn set_user_attention(&self, request_type: Option) { let xdg_activation = match self.xdg_activation.as_ref() { None => return, @@ -268,10 +283,13 @@ impl WindowHandle { let position = self.pointers.iter().position(|p| *p == pointer); if position.is_none() { - if self.confined.get() { - let surface = self.window.surface(); - pointer.confine(surface); + let surface = self.window.surface(); + match self.cursor_grab_mode.get() { + CursorGrabMode::None => (), + CursorGrabMode::Locked => pointer.lock(surface), + CursorGrabMode::Confined => pointer.confine(surface), } + self.pointers.push(pointer); } @@ -286,9 +304,11 @@ impl WindowHandle { if let Some(position) = position { let pointer = self.pointers.remove(position); - // Drop the confined pointer. - if self.confined.get() { - pointer.unconfine(); + // Drop the grabbing mode. + match self.cursor_grab_mode.get() { + CursorGrabMode::None => (), + CursorGrabMode::Locked => pointer.unlock(), + CursorGrabMode::Confined => pointer.unconfine(), } } } @@ -329,6 +349,27 @@ impl WindowHandle { } } + pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) { + if self.ime_allowed.get() == allowed { + return; + } + + self.ime_allowed.replace(allowed); + let window_id = wayland::make_wid(self.window.surface()); + + for text_input in self.text_inputs.iter() { + text_input.set_input_allowed(allowed); + } + + let event = if allowed { + WindowEvent::Ime(Ime::Enabled) + } else { + WindowEvent::Ime(Ime::Disabled) + }; + + event_sink.push_window_event(event, window_id); + } + pub fn set_cursor_visible(&self, visible: bool) { self.cursor_visible.replace(visible); let cursor_icon = match visible { @@ -363,13 +404,15 @@ impl WindowHandle { #[inline] pub fn handle_window_requests(winit_state: &mut WinitState) { let window_map = &mut winit_state.window_map; - let window_updates = &mut winit_state.window_updates; + let window_user_requests = &mut winit_state.window_user_requests; + let window_compositor_updates = &mut winit_state.window_compositor_updates; let mut windows_to_close: Vec = Vec::new(); // Process the rest of the events. for (window_id, window_handle) in window_map.iter_mut() { let mut requests = window_handle.pending_window_requests.lock().unwrap(); - for request in requests.drain(..) { + let requests = requests.drain(..); + for request in requests { match request { WindowRequest::Fullscreen(fullscreen) => { window_handle.window.set_fullscreen(fullscreen.as_ref()); @@ -383,11 +426,18 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { WindowRequest::NewCursorIcon(cursor_icon) => { window_handle.set_cursor_icon(cursor_icon); } - WindowRequest::IMEPosition(position) => { + WindowRequest::ImePosition(position) => { window_handle.set_ime_position(position); } - WindowRequest::GrabCursor(grab) => { - window_handle.set_cursor_grab(grab); + WindowRequest::AllowIme(allow) => { + let event_sink = &mut winit_state.event_sink; + window_handle.set_ime_allowed(allow, event_sink); + } + WindowRequest::SetCursorGrabMode(mode) => { + window_handle.set_cursor_grab(mode); + } + WindowRequest::SetLockedCursorPosition(position) => { + window_handle.set_locked_cursor_position(position); } WindowRequest::DragWindow => { window_handle.drag_window(); @@ -411,57 +461,73 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { window_handle.window.set_decorate(decorations); // We should refresh the frame to apply decorations change. - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.refresh_frame = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; + } + #[cfg(feature = "sctk-adwaita")] + WindowRequest::CsdThemeVariant(theme) => { + window_handle.window.set_frame_config(theme.into()); + + let window_requst = window_user_requests.get_mut(window_id).unwrap(); + window_requst.refresh_frame = true; } + #[cfg(not(feature = "sctk-adwaita"))] + WindowRequest::CsdThemeVariant(_) => {} WindowRequest::Resizeable(resizeable) => { window_handle.window.set_resizable(resizeable); // We should refresh the frame to update button state. - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.refresh_frame = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::Title(title) => { window_handle.window.set_title(title); // We should refresh the frame to draw new title. - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.refresh_frame = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::MinSize(size) => { let size = size.map(|size| (size.width, size.height)); window_handle.window.set_min_size(size); - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.redraw_requested = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::MaxSize(size) => { let size = size.map(|size| (size.width, size.height)); window_handle.window.set_max_size(size); - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.redraw_requested = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::FrameSize(size) => { - // Set new size. + if !window_handle.is_resizable.get() { + // On Wayland non-resizable window is achieved by setting both min and max + // size of the window to the same value. + let size = Some((size.width, size.height)); + window_handle.window.set_max_size(size); + window_handle.window.set_min_size(size); + } + window_handle.window.resize(size.width, size.height); // We should refresh the frame after resize. - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.refresh_frame = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::PassthroughMouseInput(passthrough) => { window_handle.passthrough_mouse_input(passthrough); - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.refresh_frame = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.refresh_frame = true; } WindowRequest::Attention(request_type) => { window_handle.set_user_attention(request_type); } WindowRequest::Redraw => { - let window_update = window_updates.get_mut(window_id).unwrap(); - window_update.redraw_requested = true; + let window_request = window_user_requests.get_mut(window_id).unwrap(); + window_request.redraw_requested = true; } WindowRequest::Close => { // The window was requested to be closed. @@ -478,6 +544,18 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { // Close the windows. for window in windows_to_close { let _ = window_map.remove(&window); - let _ = window_updates.remove(&window); + let _ = window_user_requests.remove(&window); + let _ = window_compositor_updates.remove(&window); + } +} + +impl Drop for WindowHandle { + fn drop(&mut self) { + unsafe { + let surface = self.window.surface().clone(); + // The window must be destroyed before wl_surface. + ManuallyDrop::drop(&mut self.window); + surface.destroy(); + } } } diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 77cc55309f..54cdc59b03 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -12,20 +12,23 @@ use super::{ use util::modifiers::{ModifierKeyState, ModifierKeymap}; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, event::{ - DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent, + DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, TouchPhase, + WindowEvent, }, event_loop::EventLoopWindowTarget as RootELW, }; -/// The X11 documentation states: "Keycodes lie in the inclusive range [8,255]". +/// The X11 documentation states: "Keycodes lie in the inclusive range `[8, 255]`". const KEYCODE_OFFSET: u8 = 8; pub(super) struct EventProcessor { pub(super) dnd: Dnd, pub(super) ime_receiver: ImeReceiver, + pub(super) ime_event_receiver: ImeEventReceiver, pub(super) randr_event_offset: c_int, pub(super) devices: RefCell>, pub(super) xi2ext: XExtension, @@ -37,6 +40,7 @@ pub(super) struct EventProcessor { pub(super) first_touch: Option, // Currently focused window belonging to this process pub(super) active_window: Option, + pub(super) is_composing: bool, } impl EventProcessor { @@ -45,7 +49,7 @@ impl EventProcessor { let mut devices = self.devices.borrow_mut(); if let Some(info) = DeviceInfo::get(&wt.xconn, device) { for info in info.iter() { - devices.insert(DeviceId(info.deviceid), Device::new(self, info)); + devices.insert(DeviceId(info.deviceid), Device::new(info)); } } } @@ -55,7 +59,7 @@ impl EventProcessor { F: Fn(&Arc) -> Ret, { let mut deleted = false; - let window_id = WindowId(window_id); + let window_id = WindowId(window_id as u64); let wt = get_xtarget(&self.target); let result = wt .windows @@ -410,7 +414,7 @@ impl EventProcessor { // resizing by dragging across monitors *without* dropping the window. let (width, height) = shared_state_lock .dpi_adjusted - .unwrap_or_else(|| (xev.width as u32, xev.height as u32)); + .unwrap_or((xev.width as u32, xev.height as u32)); let last_scale_factor = shared_state_lock.last_monitor.scale_factor; let new_scale_factor = { @@ -509,7 +513,7 @@ impl EventProcessor { // In the event that the window's been destroyed without being dropped first, we // cleanup again here. - wt.windows.borrow_mut().remove(&WindowId(window)); + wt.windows.borrow_mut().remove(&WindowId(window as u64)); // Since all XIM stuff needs to happen from the same thread, we destroy the input // context here instead of when dropping the window. @@ -527,8 +531,13 @@ impl EventProcessor { ffi::VisibilityNotify => { let xev: &ffi::XVisibilityEvent = xev.as_ref(); let xwindow = xev.window; - - self.with_window(xwindow, |window| window.visibility_notify()); + callback(Event::WindowEvent { + window_id: mkwid(xwindow), + event: WindowEvent::Occluded(xev.state == ffi::VisibilityFullyObscured), + }); + self.with_window(xwindow, |window| { + window.visibility_notify(); + }); } ffi::Expose => { @@ -567,7 +576,7 @@ impl EventProcessor { // When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with // a keycode of 0. - if keycode != 0 { + if keycode != 0 && !self.is_composing { let scancode = keycode - KEYCODE_OFFSET as u32; let keysym = wt.xconn.lookup_keysym(xkev); let virtual_keycode = events::keysym_to_element(keysym as c_uint); @@ -602,12 +611,25 @@ impl EventProcessor { return; }; - for chr in written.chars() { + // If we're composing right now, send the string we've got from X11 via + // Ime::Commit. + if self.is_composing && keycode == 0 && !written.is_empty() { let event = Event::WindowEvent { window_id, - event: WindowEvent::ReceivedCharacter(chr), + event: WindowEvent::Ime(Ime::Commit(written)), }; + + self.is_composing = false; callback(event); + } else { + for chr in written.chars() { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ReceivedCharacter(chr), + }; + + callback(event); + } } } } @@ -890,6 +912,8 @@ impl EventProcessor { if self.active_window != Some(xev.event) { self.active_window = Some(xev.event); + wt.update_device_event_filter(true); + let window_id = mkwid(xev.event); let position = PhysicalPosition::new(xev.event_x, xev.event_y); @@ -939,6 +963,7 @@ impl EventProcessor { if !self.window_exists(xev.event) { return; } + wt.ime .borrow_mut() .unfocus(xev.event) @@ -947,6 +972,8 @@ impl EventProcessor { if self.active_window.take() == Some(xev.event) { let window_id = mkwid(xev.event); + wt.update_device_event_filter(false); + // Issue key release events for all pressed keys Self::handle_pressed_keys( wt, @@ -1191,11 +1218,7 @@ impl EventProcessor { &*window.shared_state.lock(), ); - let window_id = crate::window::WindowId( - crate::platform_impl::platform::WindowId::X( - *window_id, - ), - ); + let window_id = crate::window::WindowId(*window_id); let old_inner_size = PhysicalSize::new(width, height); let mut new_inner_size = PhysicalSize::new(new_width, new_height); @@ -1223,8 +1246,61 @@ impl EventProcessor { } } - if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() { - wt.ime.borrow_mut().send_xim_spot(window_id, x, y); + // Handle IME requests. + if let Ok(request) = self.ime_receiver.try_recv() { + let mut ime = wt.ime.borrow_mut(); + match request { + ImeRequest::Position(window_id, x, y) => { + ime.send_xim_spot(window_id, x, y); + } + ImeRequest::Allow(window_id, allowed) => { + ime.set_ime_allowed(window_id, allowed); + } + } + } + + let (window, event) = match self.ime_event_receiver.try_recv() { + Ok((window, event)) => (window, event), + Err(_) => return, + }; + + match event { + ImeEvent::Enabled => { + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + ImeEvent::Start => { + self.is_composing = true; + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)), + }); + } + ImeEvent::Update(text, position) => { + if self.is_composing { + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))), + }); + } + } + ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + ImeEvent::Disabled => { + self.is_composing = false; + callback(Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::Ime(Ime::Disabled), + }); + } } } diff --git a/src/platform_impl/linux/x11/events.rs b/src/platform_impl/linux/x11/events.rs index 1c34b0d0cb..9063916025 100644 --- a/src/platform_impl/linux/x11/events.rs +++ b/src/platform_impl/linux/x11/events.rs @@ -161,7 +161,7 @@ pub fn keysym_to_element(keysym: libc::c_uint) -> Option { ffi::XK_Shift_R => VirtualKeyCode::RShift, ffi::XK_Control_L => VirtualKeyCode::LControl, ffi::XK_Control_R => VirtualKeyCode::RControl, - //ffi::XK_Caps_Lock => VirtualKeyCode::Caps_lock, + ffi::XK_Caps_Lock => VirtualKeyCode::Capital, //ffi::XK_Shift_Lock => VirtualKeyCode::Shift_lock, //ffi::XK_Meta_L => VirtualKeyCode::Meta_l, //ffi::XK_Meta_R => VirtualKeyCode::Meta_r, diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs index 7b599274a4..a957c23a62 100644 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ b/src/platform_impl/linux/x11/ime/callbacks.rs @@ -108,8 +108,19 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { let mut new_contexts = HashMap::new(); for (window, old_context) in (*inner).contexts.iter() { let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); + let is_allowed = old_context + .as_ref() + .map(|old_context| old_context.is_allowed) + .unwrap_or_default(); let new_context = { - let result = ImeContext::new(xconn, new_im.im, *window, spot); + let result = ImeContext::new( + xconn, + new_im.im, + *window, + spot, + is_allowed, + (*inner).event_sender.clone(), + ); if result.is_err() { let _ = close_im(xconn, new_im.im); } diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs index 8d58082052..a3109d57db 100644 --- a/src/platform_impl/linux/x11/ime/context.rs +++ b/src/platform_impl/linux/x11/ime/context.rs @@ -1,41 +1,202 @@ -use std::{ - os::raw::{c_short, c_void}, - ptr, - sync::Arc, -}; +use std::ffi::CStr; +use std::os::raw::c_short; +use std::sync::Arc; +use std::{mem, ptr}; + +use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; + +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; use super::{ffi, util, XConnection, XError}; +/// IME creation error. #[derive(Debug)] pub enum ImeContextCreationError { + /// Got the error from Xlib. XError(XError), + + /// Got null pointer from Xlib but without exact reason. Null, } -unsafe fn create_pre_edit_attr<'a>( - xconn: &'a Arc, - ic_spot: &'a ffi::XPoint, -) -> util::XSmartPointer<'a, c_void> { - util::XSmartPointer::new( - xconn, - (xconn.xlib.XVaCreateNestedList)( - 0, - ffi::XNSpotLocation_0.as_ptr() as *const _, - ic_spot, - ptr::null_mut::<()>(), - ), - ) - .expect("XVaCreateNestedList returned NULL") +/// The callback used by XIM preedit functions. +type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); + +/// Wrapper for creating XIM callbacks. +#[inline] +fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback { + XIMCallback { + client_data, + callback: Some(callback), + } +} + +/// The server started preedit. +extern "C" fn preedit_start_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) -> i32 { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + client_data.text.clear(); + client_data.cursor_pos = 0; + client_data + .event_sender + .send((client_data.window, ImeEvent::Start)) + .expect("failed to send preedit start event"); + -1 +} + +/// Done callback is used when the preedit should be hidden. +extern "C" fn preedit_done_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + // Drop text buffer and reset cursor position on done. + client_data.text = Vec::new(); + client_data.cursor_pos = 0; + + client_data + .event_sender + .send((client_data.window, ImeEvent::End)) + .expect("failed to send preedit end event"); +} + +fn calc_byte_position(text: &[char], pos: usize) -> usize { + text.iter() + .take(pos) + .fold(0, |byte_pos, text| byte_pos + text.len_utf8()) +} + +/// Preedit text information to be drawn inline by the client. +extern "C" fn preedit_draw_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) }; + client_data.cursor_pos = call_data.caret as usize; + + let chg_range = + 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() { + warn!( + "invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}", + client_data.text.len(), + call_data.chg_first, + call_data.chg_length + ); + return; + } + + // NULL indicate text deletion + let mut new_chars = if call_data.text.is_null() { + Vec::new() + } else { + let xim_text = unsafe { &mut *(call_data.text) }; + if xim_text.encoding_is_wchar > 0 { + return; + } + + let new_text = unsafe { xim_text.string.multi_byte }; + + if new_text.is_null() { + return; + } + + let new_text = unsafe { CStr::from_ptr(new_text) }; + + String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")) + .chars() + .collect() + }; + let mut old_text_tail = client_data.text.split_off(chg_range.end); + client_data.text.truncate(chg_range.start); + client_data.text.append(&mut new_chars); + client_data.text.append(&mut old_text_tail); + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); } -// WARNING: this struct doesn't destroy its XIC resource when dropped. +/// Handling of cursor movements in preedit text. +extern "C" fn preedit_caret_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) }; + + if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition { + client_data.cursor_pos = call_data.position as usize; + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); + } +} + +/// Struct to simplify callback creation and latter passing into Xlib XIM. +struct PreeditCallbacks { + start_callback: ffi::XIMCallback, + done_callback: ffi::XIMCallback, + draw_callback: ffi::XIMCallback, + caret_callback: ffi::XIMCallback, +} + +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) + }); + let done_callback = create_xim_callback(client_data, preedit_done_callback); + let caret_callback = create_xim_callback(client_data, preedit_caret_callback); + let draw_callback = create_xim_callback(client_data, preedit_draw_callback); + + PreeditCallbacks { + start_callback, + done_callback, + caret_callback, + draw_callback, + } + } +} + +struct ImeContextClientData { + window: ffi::Window, + event_sender: ImeEventSender, + text: Vec, + cursor_pos: usize, +} + +// XXX: this struct doesn't destroy its XIC resource when dropped. // This is intentional, as it doesn't have enough information to know whether or not the context // still exists on the server. Since `ImeInner` has that awareness, destruction must be handled // through `ImeInner`. -#[derive(Debug)] pub struct ImeContext { - pub ic: ffi::XIC, - pub ic_spot: ffi::XPoint, + pub(super) ic: ffi::XIC, + pub(super) ic_spot: ffi::XPoint, + pub(super) is_allowed: bool, + // Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from + // there we keep the pointer to automatically deallocate it. + _client_data: Box, } impl ImeContext { @@ -44,25 +205,43 @@ impl ImeContext { im: ffi::XIM, window: ffi::Window, ic_spot: Option, + is_allowed: bool, + event_sender: ImeEventSender, ) -> Result { - let ic = if let Some(ic_spot) = ic_spot { - ImeContext::create_ic_with_spot(xconn, im, window, ic_spot) + let client_data = Box::into_raw(Box::new(ImeContextClientData { + window, + event_sender, + text: Vec::new(), + cursor_pos: 0, + })); + + let ic = if is_allowed { + ImeContext::create_ic(xconn, im, window, client_data as ffi::XPointer) + .ok_or(ImeContextCreationError::Null)? } else { - ImeContext::create_ic(xconn, im, window) + ImeContext::create_none_ic(xconn, im, window).ok_or(ImeContextCreationError::Null)? }; - let ic = ic.ok_or(ImeContextCreationError::Null)?; xconn .check_errors() .map_err(ImeContextCreationError::XError)?; - Ok(ImeContext { + let mut context = ImeContext { ic, - ic_spot: ic_spot.unwrap_or(ffi::XPoint { x: 0, y: 0 }), - }) + ic_spot: ffi::XPoint { x: 0, y: 0 }, + is_allowed, + _client_data: Box::from_raw(client_data), + }; + + // Set the spot location, if it's present. + if let Some(ic_spot) = ic_spot { + context.set_spot(xconn, ic_spot.x, ic_spot.y) + } + + Ok(context) } - unsafe fn create_ic( + unsafe fn create_none_ic( xconn: &Arc, im: ffi::XIM, window: ffi::Window, @@ -70,40 +249,67 @@ impl ImeContext { let ic = (xconn.xlib.XCreateIC)( im, ffi::XNInputStyle_0.as_ptr() as *const _, - ffi::XIMPreeditNothing | ffi::XIMStatusNothing, + ffi::XIMPreeditNone | ffi::XIMStatusNone, ffi::XNClientWindow_0.as_ptr() as *const _, window, ptr::null_mut::<()>(), ); - if ic.is_null() { - None - } else { - Some(ic) - } + + (!ic.is_null()).then(|| ic) } - unsafe fn create_ic_with_spot( + unsafe fn create_ic( xconn: &Arc, im: ffi::XIM, window: ffi::Window, - ic_spot: ffi::XPoint, + client_data: ffi::XPointer, ) -> Option { - let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot); - let ic = (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - ffi::XIMPreeditNothing | ffi::XIMStatusNothing, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ffi::XNPreeditAttributes_0.as_ptr() as *const _, - pre_edit_attr.ptr, - ptr::null_mut::<()>(), - ); - if ic.is_null() { - None - } else { - Some(ic) - } + let preedit_callbacks = PreeditCallbacks::new(client_data); + let preedit_attr = util::XSmartPointer::new( + xconn, + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNPreeditStartCallback_0.as_ptr() as *const _, + &(preedit_callbacks.start_callback) as *const _, + ffi::XNPreeditDoneCallback_0.as_ptr() as *const _, + &(preedit_callbacks.done_callback) as *const _, + ffi::XNPreeditCaretCallback_0.as_ptr() as *const _, + &(preedit_callbacks.caret_callback) as *const _, + ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, + &(preedit_callbacks.draw_callback) as *const _, + ptr::null_mut::<()>(), + ), + ) + .expect("XVaCreateNestedList returned NULL"); + + let ic = { + let ic = (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ); + + // If we've failed to create IC with preedit callbacks fallback to normal one. + if ic.is_null() { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + ffi::XIMPreeditNothing | ffi::XIMStatusNothing, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + } else { + ic + } + }; + + (!ic.is_null()).then(|| ic) } pub fn focus(&self, xconn: &Arc) -> Result<(), XError> { @@ -120,18 +326,34 @@ impl ImeContext { xconn.check_errors() } + // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks + // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the + // window and couldn't be changed. + // + // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. pub fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { - if self.ic_spot.x == x && self.ic_spot.y == y { + if !self.is_allowed || self.ic_spot.x == x && self.ic_spot.y == y { return; } + self.ic_spot = ffi::XPoint { x, y }; unsafe { - let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot); + let preedit_attr = util::XSmartPointer::new( + xconn, + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNSpotLocation_0.as_ptr(), + &self.ic_spot, + ptr::null_mut::<()>(), + ), + ) + .expect("XVaCreateNestedList returned NULL"); + (xconn.xlib.XSetICValues)( self.ic, ffi::XNPreeditAttributes_0.as_ptr() as *const _, - pre_edit_attr.ptr, + preedit_attr.ptr, ptr::null_mut::<()>(), ); } diff --git a/src/platform_impl/linux/x11/ime/inner.rs b/src/platform_impl/linux/x11/ime/inner.rs index 97dcf7af6c..58b558bcaa 100644 --- a/src/platform_impl/linux/x11/ime/inner.rs +++ b/src/platform_impl/linux/x11/ime/inner.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc}; use super::{ffi, XConnection, XError}; use super::{context::ImeContext, input_method::PotentialInputMethods}; +use crate::platform_impl::platform::x11::ime::ImeEventSender; pub unsafe fn close_im(xconn: &Arc, im: ffi::XIM) -> Result<(), XError> { (xconn.xlib.XCloseIM)(im); @@ -22,6 +23,7 @@ pub struct ImeInner { pub contexts: HashMap>, // WARNING: this is initially zeroed! pub destroy_callback: ffi::XIMCallback, + pub event_sender: ImeEventSender, // Indicates whether or not the the input method was destroyed on the server end // (i.e. if ibus/fcitx/etc. was terminated/restarted) pub is_destroyed: bool, @@ -29,13 +31,18 @@ pub struct ImeInner { } impl ImeInner { - pub fn new(xconn: Arc, potential_input_methods: PotentialInputMethods) -> Self { + pub fn new( + xconn: Arc, + potential_input_methods: PotentialInputMethods, + event_sender: ImeEventSender, + ) -> Self { ImeInner { xconn, im: ptr::null_mut(), potential_input_methods, contexts: HashMap::new(), destroy_callback: unsafe { mem::zeroed() }, + event_sender, is_destroyed: false, is_fallback: false, } diff --git a/src/platform_impl/linux/x11/ime/input_method.rs b/src/platform_impl/linux/x11/ime/input_method.rs index e552f55635..a747f972a9 100644 --- a/src/platform_impl/linux/x11/ime/input_method.rs +++ b/src/platform_impl/linux/x11/ime/input_method.rs @@ -7,13 +7,12 @@ use std::{ sync::Arc, }; +use once_cell::sync::Lazy; use parking_lot::Mutex; use super::{ffi, util, XConnection, XError}; -lazy_static! { - static ref GLOBAL_LOCK: Mutex<()> = Default::default(); -} +static GLOBAL_LOCK: Lazy> = Lazy::new(Default::default); unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { let _lock = GLOBAL_LOCK.lock(); @@ -181,10 +180,10 @@ impl PotentialInputMethod { } // By logging this struct, you get a sequential listing of every locale modifier tried, where it -// came from, and if it succceeded. +// came from, and if it succeeded. #[derive(Debug, Clone)] pub struct PotentialInputMethods { - // On correctly configured systems, the XMODIFIERS environemnt variable tells us everything we + // On correctly configured systems, the XMODIFIERS environment variable tells us everything we // need to know. xmodifiers: Option, // We have some standard options at our disposal that should ostensibly always work. For users @@ -214,7 +213,7 @@ impl PotentialInputMethods { // that case, we get `None` and end up skipping ahead to the next method. xmodifiers, fallbacks: [ - // This is a standard input method that supports compose equences, which should + // This is a standard input method that supports compose sequences, which should // always be available. `@im=none` appears to mean the same thing. PotentialInputMethod::from_str("@im=local"), // This explicitly specifies to use the implementation-dependent default, though diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index b95da71101..746ba9ea90 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -19,9 +19,29 @@ use self::{ inner::{close_im, ImeInner}, input_method::PotentialInputMethods, }; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ImeEvent { + Enabled, + Start, + Update(String, usize), + End, + Disabled, +} + +pub type ImeReceiver = Receiver; +pub type ImeSender = Sender; +pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>; +pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; -pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>; -pub type ImeSender = Sender<(ffi::Window, i16, i16)>; +/// Request to control XIM handler from the window. +pub enum ImeRequest { + /// Set IME spot position for given `window_id`. + Position(ffi::Window, i16, i16), + + /// Allow IME input for the given `window_id`. + Allow(ffi::Window, bool), +} #[derive(Debug)] pub enum ImeCreationError { @@ -37,11 +57,14 @@ pub struct Ime { } impl Ime { - pub fn new(xconn: Arc) -> Result { + pub fn new( + xconn: Arc, + event_sender: ImeEventSender, + ) -> Result { let potential_input_methods = PotentialInputMethods::new(&xconn); let (mut inner, client_data) = { - let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods)); + let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender)); let inner_ptr = Box::into_raw(inner); let client_data = inner_ptr as _; let destroy_callback = ffi::XIMCallback { @@ -88,12 +111,54 @@ impl Ime { // Ok(_) indicates that nothing went wrong internally // Ok(true) indicates that the action was actually performed // Ok(false) indicates that the action is not presently applicable - pub fn create_context(&mut self, window: ffi::Window) -> Result { + pub fn create_context( + &mut self, + window: ffi::Window, + with_preedit: 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 { - Some(unsafe { ImeContext::new(&self.inner.xconn, self.inner.im, window, None) }?) + let context = unsafe { + ImeContext::new( + &self.inner.xconn, + self.inner.im, + window, + None, + with_preedit, + self.inner.event_sender.clone(), + ) + .or_else(|_| { + debug!( + "failed to create an IME context {} preedit support", + if with_preedit { "with" } else { "without" } + ); + ImeContext::new( + &self.inner.xconn, + self.inner.im, + window, + None, + !with_preedit, + self.inner.event_sender.clone(), + ) + }) + }?; + + // Check the state on the context, since it could fail to enable or disable preedit. + let event = if context.is_allowed { + ImeEvent::Enabled + } else { + // There's no IME without preedit. + ImeEvent::Disabled + }; + + self.inner + .event_sender + .send((window, event)) + .expect("Failed to send enabled event"); + + Some(context) }; self.inner.contexts.insert(window, context); Ok(!self.is_destroyed()) @@ -151,6 +216,24 @@ impl Ime { context.set_spot(&self.xconn, x as _, y as _); } } + + pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) { + if self.is_destroyed() { + return; + } + + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + if allowed == context.is_allowed { + return; + } + } + + // Remove context for that window. + let _ = self.remove_context(window); + + // Create new context supporting IME input. + let _ = self.create_context(window, allowed); + } } impl Drop for Ime { diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index e0a2995e77..c34eb2d460 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -23,7 +23,7 @@ pub use self::{ }; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, collections::{HashMap, HashSet}, ffi::CStr, mem::{self, MaybeUninit}, @@ -40,18 +40,24 @@ use std::{ use libc::{self, setlocale, LC_CTYPE}; use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker}; +use raw_window_handle::{RawDisplayHandle, XlibDisplayHandle}; use self::{ dnd::{Dnd, DndState}, event_processor::EventProcessor, - ime::{Ime, ImeCreationError, ImeReceiver, ImeSender}, + ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}, util::modifiers::ModifierKeymap, }; use crate::{ error::OsError as RootOsError, event::{Event, StartCause}, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, - platform_impl::{platform::sticky_exit_callback, PlatformSpecificWindowBuilderAttributes}, + event_loop::{ + ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, + }, + platform_impl::{ + platform::{sticky_exit_callback, WindowId}, + PlatformSpecificWindowBuilderAttributes, + }, window::WindowAttributes, }; @@ -76,15 +82,16 @@ impl PeekableReceiver { if self.first.is_some() { return true; } + match self.recv.try_recv() { Ok(v) => { self.first = Some(v); - return true; + true } - Err(TryRecvError::Empty) => return false, + Err(TryRecvError::Empty) => false, Err(TryRecvError::Disconnected) => { warn!("Channel was disconnected when checking incoming"); - return false; + false } } } @@ -105,6 +112,7 @@ pub struct EventLoopWindowTarget { ime: RefCell, windows: RefCell>>, redraw_sender: WakeSender, + device_event_filter: Cell, _marker: ::std::marker::PhantomData, } @@ -144,6 +152,7 @@ impl EventLoop { .expect("Failed to call XInternAtoms when initializing drag and drop"); let (ime_sender, ime_receiver) = mpsc::channel(); + let (ime_event_sender, ime_event_receiver) = mpsc::channel(); // Input methods will open successfully without setting the locale, but it won't be // possible to actually commit pre-edit sequences. unsafe { @@ -168,7 +177,7 @@ impl EventLoop { } } let ime = RefCell::new({ - let result = Ime::new(Arc::clone(&xconn)); + let result = Ime::new(Arc::clone(&xconn), ime_event_sender); if let Err(ImeCreationError::OpenFailure(ref state)) = result { panic!("Failed to open input method: {:#?}", state); } @@ -228,21 +237,27 @@ impl EventLoop { let (user_sender, user_channel) = std::sync::mpsc::channel(); let (redraw_sender, redraw_channel) = std::sync::mpsc::channel(); + let window_target = EventLoopWindowTarget { + ime, + root, + windows: Default::default(), + _marker: ::std::marker::PhantomData, + ime_sender, + xconn, + wm_delete_window, + net_wm_ping, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + device_event_filter: Default::default(), + }; + + // Set initial device event filter. + window_target.update_device_event_filter(true); + let target = Rc::new(RootELW { - p: super::EventLoopWindowTarget::X(EventLoopWindowTarget { - ime, - root, - windows: Default::default(), - _marker: ::std::marker::PhantomData, - ime_sender, - xconn, - wm_delete_window, - net_wm_ping, - redraw_sender: WakeSender { - sender: redraw_sender, // not used again so no clone - waker: waker.clone(), - }, - }), + p: super::EventLoopWindowTarget::X(window_target), _marker: ::std::marker::PhantomData, }); @@ -252,12 +267,14 @@ impl EventLoop { devices: Default::default(), randr_event_offset, ime_receiver, + ime_event_receiver, xi2ext, mod_keymap, device_mod_state: Default::default(), num_touch: 0, first_touch: None, active_window: None, + is_composing: false, }; // Register for device hotplug events @@ -316,6 +333,17 @@ impl EventLoop { callback, ); + // NB: For consistency all platforms must emit a 'resumed' event even though X11 + // applications don't themselves have a formal suspend/resume lifecycle. + if *cause == StartCause::Init { + sticky_exit_callback( + crate::event::Event::Resumed, + &this.target, + control_flow, + callback, + ); + } + // Process all pending events this.drain_events(callback, control_flow); @@ -348,7 +376,7 @@ impl EventLoop { } for window_id in windows { - let window_id = crate::window::WindowId(super::WindowId::X(window_id)); + let window_id = crate::window::WindowId(window_id); sticky_exit_callback( Event::RedrawRequested(window_id), &this.target, @@ -404,11 +432,12 @@ impl EventLoop { deadline = Some(*wait_deadline); } } - return IterationResult { + + IterationResult { wait_start: start, deadline, timeout, - }; + } } let mut control_flow = ControlFlow::default(); @@ -440,7 +469,7 @@ impl EventLoop { // must do this because during the execution of the iteration we sometimes wake // the mio waker, and if the waker is already awaken before we call poll(), // then poll doesn't block, but it returns immediately. This caused the event - // loop to run continously even if the control_flow was `Wait` + // loop to run continuously even if the control_flow was `Wait` continue; } } @@ -491,10 +520,7 @@ impl EventLoop { target, control_flow, &mut |event, window_target, control_flow| { - if let Event::RedrawRequested(crate::window::WindowId( - super::WindowId::X(wid), - )) = event - { + if let Event::RedrawRequested(crate::window::WindowId(wid)) = event { wt.redraw_sender.sender.send(wid).unwrap(); wt.redraw_sender.waker.wake().unwrap(); } else { @@ -521,6 +547,37 @@ impl EventLoopWindowTarget { pub fn x_connection(&self) -> &Arc { &self.xconn } + + pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { + self.device_event_filter.set(filter); + } + + /// Update the device event filter based on window focus. + pub fn update_device_event_filter(&self, focus: bool) { + let filter_events = self.device_event_filter.get() == DeviceEventFilter::Never + || (self.device_event_filter.get() == DeviceEventFilter::Unfocused && !focus); + + let mut mask = 0; + if !filter_events { + mask = ffi::XI_RawMotionMask + | ffi::XI_RawButtonPressMask + | ffi::XI_RawButtonReleaseMask + | ffi::XI_RawKeyPressMask + | ffi::XI_RawKeyReleaseMask; + } + + self.xconn + .select_xinput_events(self.root, ffi::XIAllMasterDevices, mask) + .queue(); + } + + pub fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { + let mut display_handle = XlibDisplayHandle::empty(); + display_handle.display = self.xconn.display as *mut _; + display_handle.screen = + unsafe { (self.xconn.xlib.XDefaultScreen)(self.xconn.display as *mut _) }; + RawDisplayHandle::Xlib(display_handle) + } } impl EventLoopProxy { @@ -572,15 +629,6 @@ impl<'a> Deref for DeviceInfo<'a> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId(ffi::Window); - -impl WindowId { - pub const unsafe fn dummy() -> Self { - WindowId(0) - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId(c_int); @@ -601,7 +649,7 @@ impl Deref for Window { } impl Window { - pub fn new( + pub(crate) fn new( event_loop: &EventLoopWindowTarget, attribs: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, @@ -620,7 +668,7 @@ impl Drop for Window { let window = self.deref(); let xconn = &window.xconn; unsafe { - (xconn.xlib.XDestroyWindow)(xconn.display, window.id().0); + (xconn.xlib.XDestroyWindow)(xconn.display, window.id().0 as ffi::Window); // If the window was somehow already destroyed, we'll get a `BadWindow` error, which we don't care about. let _ = xconn.check_errors(); } @@ -635,10 +683,7 @@ struct GenericEventCookie<'a> { } impl<'a> GenericEventCookie<'a> { - fn from_event<'b>( - xconn: &'b XConnection, - event: ffi::XEvent, - ) -> Option> { + fn from_event(xconn: &XConnection, event: ffi::XEvent) -> Option> { unsafe { let mut cookie: ffi::XGenericEventCookie = From::from(event); if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == ffi::True { @@ -666,7 +711,7 @@ struct XExtension { } fn mkwid(w: ffi::Window) -> crate::window::WindowId { - crate::window::WindowId(crate::platform_impl::WindowId::X(WindowId(w))) + crate::window::WindowId(crate::platform_impl::platform::WindowId(w as u64)) } fn mkdid(w: c_int) -> crate::event::DeviceId { crate::event::DeviceId(crate::platform_impl::DeviceId::X(DeviceId(w))) @@ -695,24 +740,11 @@ enum ScrollOrientation { } impl Device { - fn new(el: &EventProcessor, info: &ffi::XIDeviceInfo) -> Self { + fn new(info: &ffi::XIDeviceInfo) -> Self { let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; let mut scroll_axes = Vec::new(); - let wt = get_xtarget(&el.target); - if Device::physical_device(info) { - // Register for global raw events - let mask = ffi::XI_RawMotionMask - | ffi::XI_RawButtonPressMask - | ffi::XI_RawButtonReleaseMask - | ffi::XI_RawKeyPressMask - | ffi::XI_RawKeyReleaseMask; - // The request buffer is flushed when we poll for events - wt.xconn - .select_xinput_events(wt.root, info.deviceid, mask) - .queue(); - // Identify scroll axes for class_ptr in Device::classes(info) { let class = unsafe { &**class_ptr }; diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 2bd0ab17fb..8a696e8a05 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -1,11 +1,13 @@ use std::os::raw::*; +use std::slice; +use once_cell::sync::Lazy; use parking_lot::Mutex; use super::{ ffi::{ RRCrtc, RRCrtcChangeNotifyMask, RRMode, RROutputPropertyNotifyMask, - RRScreenChangeNotifyMask, True, Window, XRRCrtcInfo, XRRScreenResources, + RRScreenChangeNotifyMask, True, Window, XRRCrtcInfo, XRRModeInfo, XRRScreenResources, }, util, XConnection, XError, }; @@ -18,9 +20,7 @@ use crate::{ // Used for testing. This should always be committed as false. const DISABLE_MONITOR_LIST_CACHING: bool = false; -lazy_static! { - static ref MONITORS: Mutex>> = Mutex::default(); -} +static MONITORS: Lazy>>> = Lazy::new(Mutex::default); pub fn invalidate_cached_monitor_list() -> Option> { // We update this lazily. @@ -31,7 +31,7 @@ pub fn invalidate_cached_monitor_list() -> Option> { pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) refresh_rate_millihertz: u32, pub(crate) native_mode: RRMode, pub(crate) monitor: Option, } @@ -48,8 +48,8 @@ impl VideoMode { } #[inline] - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } #[inline] @@ -72,6 +72,8 @@ pub struct MonitorHandle { position: (i32, i32), /// If the monitor is the primary one primary: bool, + /// The refresh rate used by monitor. + refresh_rate_millihertz: Option, /// The DPI scale factor pub(crate) scale_factor: f64, /// Used to determine which windows are on this monitor @@ -106,6 +108,15 @@ impl std::hash::Hash for MonitorHandle { } } +#[inline] +pub fn mode_refresh_rate_millihertz(mode: &XRRModeInfo) -> Option { + if mode.dotClock > 0 && mode.hTotal > 0 && mode.vTotal > 0 { + Some((mode.dotClock as u64 * 1000 / (mode.hTotal as u64 * mode.vTotal as u64)) as u32) + } else { + None + } +} + impl MonitorHandle { fn new( xconn: &XConnection, @@ -117,10 +128,22 @@ impl MonitorHandle { let (name, scale_factor, video_modes) = unsafe { xconn.get_output_info(resources, crtc)? }; let dimensions = unsafe { ((*crtc).width as u32, (*crtc).height as u32) }; let position = unsafe { ((*crtc).x as i32, (*crtc).y as i32) }; + + // Get the refresh rate of the current video mode. + let current_mode = unsafe { (*crtc).mode }; + let screen_modes = + unsafe { slice::from_raw_parts((*resources).modes, (*resources).nmode as usize) }; + let refresh_rate_millihertz = screen_modes + .iter() + .find(|mode| mode.id == current_mode) + .and_then(mode_refresh_rate_millihertz); + let rect = util::AaRect::new(position, dimensions); + Some(MonitorHandle { id, name, + refresh_rate_millihertz, scale_factor, dimensions, position, @@ -137,6 +160,7 @@ impl MonitorHandle { scale_factor: 1.0, dimensions: (1, 1), position: (0, 0), + refresh_rate_millihertz: None, primary: true, rect: util::AaRect::new((0, 0), (1, 1)), video_modes: Vec::new(), @@ -165,6 +189,10 @@ impl MonitorHandle { self.position.into() } + pub fn refresh_rate_millihertz(&self) -> Option { + self.refresh_rate_millihertz + } + #[inline] pub fn scale_factor(&self) -> f64 { self.scale_factor @@ -230,11 +258,11 @@ impl XConnection { panic!("[winit] `XRRGetScreenResources` returned NULL. That should only happen if the root window doesn't exist."); } - let mut available; let mut has_primary = false; let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); - available = Vec::with_capacity((*resources).ncrtc as usize); + let mut available = Vec::with_capacity((*resources).ncrtc as usize); + for crtc_index in 0..(*resources).ncrtc { let crtc_id = *((*resources).crtcs.offset(crtc_index as isize)); let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); diff --git a/src/platform_impl/linux/x11/util/atom.rs b/src/platform_impl/linux/x11/util/atom.rs index 4138722483..5bfa386a49 100644 --- a/src/platform_impl/linux/x11/util/atom.rs +++ b/src/platform_impl/linux/x11/util/atom.rs @@ -5,15 +5,14 @@ use std::{ os::raw::*, }; +use once_cell::sync::Lazy; use parking_lot::Mutex; use super::*; type AtomCache = HashMap; -lazy_static! { - static ref ATOM_CACHE: Mutex = Mutex::new(HashMap::with_capacity(2048)); -} +static ATOM_CACHE: Lazy> = Lazy::new(|| Mutex::new(HashMap::with_capacity(2048))); impl XConnection { pub fn get_atom + Debug>(&self, name: T) -> ffi::Atom { diff --git a/src/platform_impl/linux/x11/util/geometry.rs b/src/platform_impl/linux/x11/util/geometry.rs index 17aeca5584..8a0c8a5b96 100644 --- a/src/platform_impl/linux/x11/util/geometry.rs +++ b/src/platform_impl/linux/x11/util/geometry.rs @@ -98,7 +98,7 @@ pub struct LogicalFrameExtents { pub bottom: f64, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum FrameExtentsHeuristicPath { Supported, UnsupportedNested, @@ -370,8 +370,12 @@ impl XConnection { let top = offset_y; let bottom = diff_y.saturating_sub(offset_y); - let frame_extents = - FrameExtents::new(left.into(), right.into(), top.into(), bottom.into()); + let frame_extents = FrameExtents::new( + left as c_ulong, + right as c_ulong, + top as c_ulong, + bottom as c_ulong, + ); FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested, @@ -379,7 +383,7 @@ impl XConnection { } else { // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a // border value. This is convenient, since we can use it to get an accurate frame. - let frame_extents = FrameExtents::from_border(border.into()); + let frame_extents = FrameExtents::from_border(border as c_ulong); FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedBordered, diff --git a/src/platform_impl/linux/x11/util/hint.rs b/src/platform_impl/linux/x11/util/hint.rs index 222d81748e..a2dbe6a170 100644 --- a/src/platform_impl/linux/x11/util/hint.rs +++ b/src/platform_impl/linux/x11/util/hint.rs @@ -173,6 +173,12 @@ impl MotifHints { } } +impl Default for MotifHints { + fn default() -> Self { + Self::new() + } +} + impl MwmHints { fn as_slice(&self) -> &[c_ulong] { unsafe { slice::from_raw_parts(self as *const _ as *const c_ulong, 5) } @@ -317,7 +323,7 @@ impl XConnection { let mut hints = MotifHints::new(); if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { - hints.hints.flags = props.get(0).cloned().unwrap_or(0); + hints.hints.flags = props.first().cloned().unwrap_or(0); hints.hints.functions = props.get(1).cloned().unwrap_or(0); hints.hints.decorations = props.get(2).cloned().unwrap_or(0); hints.hints.input_mode = props.get(3).cloned().unwrap_or(0) as c_long; diff --git a/src/platform_impl/linux/x11/util/icon.rs b/src/platform_impl/linux/x11/util/icon.rs index b0253e1b96..3240a9f896 100644 --- a/src/platform_impl/linux/x11/util/icon.rs +++ b/src/platform_impl/linux/x11/util/icon.rs @@ -1,3 +1,5 @@ +#![allow(clippy::assertions_on_constants)] + use super::*; use crate::icon::{Icon, Pixel, PIXEL_SIZE}; diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index cb380c7d41..2ce46e70e5 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -4,6 +4,7 @@ use super::{ ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, *, }; +use crate::platform_impl::platform::x11::monitor; use crate::{dpi::validate_scale_factor, platform_impl::platform::x11::VideoMode}; /// Represents values of `WINIT_HIDPI_FACTOR`. @@ -80,18 +81,13 @@ impl XConnection { // 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)) - .map(|x| { - let refresh_rate = if x.dotClock > 0 && x.hTotal > 0 && x.vTotal > 0 { - x.dotClock as u64 * 1000 / (x.hTotal as u64 * x.vTotal as u64) - } else { - 0 - }; - + .map(|mode| { VideoMode { - size: (x.width, x.height), - refresh_rate: (refresh_rate as f32 / 1000.0).round() as u16, + size: (mode.width, mode.height), + refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode) + .unwrap_or(0), bit_depth: bit_depth as u16, - native_mode: x.id, + native_mode: mode.id, // This is populated in `MonitorHandle::video_modes` as the // video mode is returned to the user monitor: None, @@ -164,7 +160,9 @@ impl XConnection { (self.xrandr.XRRFreeOutputInfo)(output_info); Some((name, scale_factor, modes)) } - pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Result<(), ()> { + + #[must_use] + pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Option<()> { unsafe { let mut major = 0; let mut minor = 0; @@ -195,12 +193,13 @@ impl XConnection { (self.xrandr.XRRFreeScreenResources)(resources); if status == Success as i32 { - Ok(()) + Some(()) } else { - Err(()) + None } } } + pub fn get_crtc_mode(&self, crtc_id: RRCrtc) -> RRMode { unsafe { let mut major = 0; diff --git a/src/platform_impl/linux/x11/util/window_property.rs b/src/platform_impl/linux/x11/util/window_property.rs index 8bf718ea8e..e93d5bb072 100644 --- a/src/platform_impl/linux/x11/util/window_property.rs +++ b/src/platform_impl/linux/x11/util/window_property.rs @@ -58,7 +58,7 @@ impl XConnection { property, // This offset is in terms of 32-bit chunks. offset, - // This is the quanity of 32-bit chunks to receive at once. + // This is the quantity of 32-bit chunks to receive at once. PROPERTY_BUFFER_SIZE, ffi::False, property_type, diff --git a/src/platform_impl/linux/x11/util/wm.rs b/src/platform_impl/linux/x11/util/wm.rs index 6fef5a3c4d..89e9a0b8ad 100644 --- a/src/platform_impl/linux/x11/util/wm.rs +++ b/src/platform_impl/linux/x11/util/wm.rs @@ -1,12 +1,12 @@ +use once_cell::sync::Lazy; use parking_lot::Mutex; use super::*; // This info is global to the window manager. -lazy_static! { - static ref SUPPORTED_HINTS: Mutex> = Mutex::new(Vec::with_capacity(0)); - static ref WM_NAME: Mutex> = Mutex::new(None); -} +static SUPPORTED_HINTS: Lazy>> = + Lazy::new(|| Mutex::new(Vec::with_capacity(0))); +static WM_NAME: Lazy>> = Lazy::new(|| Mutex::new(None)); pub fn hint_is_supported(hint: ffi::Atom) -> bool { (*SUPPORTED_HINTS.lock()).contains(&hint) @@ -60,7 +60,7 @@ impl XConnection { let root_window_wm_check = { let result = self.get_property(root, check_atom, ffi::XA_WINDOW); - let wm_check = result.ok().and_then(|wm_check| wm_check.get(0).cloned()); + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); wm_check? }; @@ -70,7 +70,7 @@ impl XConnection { let child_window_wm_check = { let result = self.get_property(root_window_wm_check, check_atom, ffi::XA_WINDOW); - let wm_check = result.ok().and_then(|wm_check| wm_check.get(0).cloned()); + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); wm_check? }; diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 4af0a76b16..ab7cddfa42 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1,4 +1,3 @@ -use raw_window_handle::XlibHandle; use std::{ cmp, env, ffi::CString, @@ -8,10 +7,11 @@ use std::{ ptr, slice, sync::Arc, }; -use x11_dl::xlib::TrueColor; use libc; use parking_lot::Mutex; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XlibDisplayHandle, XlibWindowHandle}; +use x11_dl::xlib::TrueColor; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, @@ -22,11 +22,12 @@ use crate::{ MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, - window::{CursorIcon, Fullscreen, Icon, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, Icon, UserAttentionType, WindowAttributes}, }; use super::{ - ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError, + ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection, + XError, }; #[derive(Debug)] @@ -105,7 +106,7 @@ pub struct UnownedWindow { root: ffi::Window, // never changes screen_id: i32, // never changes cursor: Mutex, - cursor_grabbed: Mutex, + cursor_grabbed_mode: Mutex, cursor_visible: Mutex, ime_sender: Mutex, pub shared_state: Mutex, @@ -113,7 +114,7 @@ pub struct UnownedWindow { } impl UnownedWindow { - pub fn new( + pub(crate) fn new( event_loop: &EventLoopWindowTarget, window_attrs: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, @@ -275,7 +276,7 @@ impl UnownedWindow { root, screen_id, cursor: Default::default(), - cursor_grabbed: Mutex::new(false), + cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), cursor_visible: Mutex::new(true), ime_sender: Mutex::new(event_loop.ime_sender.clone()), shared_state: SharedState::new(guessed_monitor, &window_attrs), @@ -310,11 +311,11 @@ impl UnownedWindow { // WM_CLASS must be set *before* mapping the window, as per ICCCM! { - let (class, instance) = if let Some((instance, class)) = pl_attribs.class { - let instance = CString::new(instance.as_str()) + let (class, instance) = if let Some(name) = pl_attribs.name { + let instance = CString::new(name.instance.as_str()) .expect("`WM_CLASS` instance contained null byte"); - let class = - CString::new(class.as_str()).expect("`WM_CLASS` class contained null byte"); + let class = CString::new(name.general.as_str()) + .expect("`WM_CLASS` class contained null byte"); (instance, class) } else { let class = env::args() @@ -370,15 +371,15 @@ impl UnownedWindow { } else { max_inner_size = Some(dimensions.into()); min_inner_size = Some(dimensions.into()); - - let mut shared_state = window.shared_state.get_mut(); - shared_state.min_inner_size = window_attrs.min_inner_size; - shared_state.max_inner_size = window_attrs.max_inner_size; - shared_state.resize_increments = pl_attribs.resize_increments; - shared_state.base_size = pl_attribs.base_size; } } + let mut shared_state = window.shared_state.get_mut(); + shared_state.min_inner_size = min_inner_size.map(Into::into); + shared_state.max_inner_size = max_inner_size.map(Into::into); + shared_state.resize_increments = pl_attribs.resize_increments; + shared_state.base_size = pl_attribs.base_size; + let mut normal_hints = util::NormalHints::new(xconn); normal_hints.set_position(position.map(|PhysicalPosition { x, y }| (x, y))); normal_hints.set_size(Some(dimensions)); @@ -453,7 +454,10 @@ impl UnownedWindow { .queue(); { - let result = event_loop.ime.borrow_mut().create_context(window.xwindow); + let result = event_loop + .ime + .borrow_mut() + .create_context(window.xwindow, false); if let Err(err) = result { let e = match err { ImeContextCreationError::XError(err) => OsError::XError(err), @@ -1115,8 +1119,15 @@ impl UnownedWindow { #[inline] pub fn set_inner_size(&self, size: Size) { let scale_factor = self.scale_factor(); - let (width, height) = size.to_physical::(scale_factor).into(); - self.set_inner_size_physical(width, height); + let size = size.to_physical::(scale_factor).into(); + if !self.shared_state.lock().is_resizable { + self.update_normal_hints(|normal_hints| { + normal_hints.set_min_size(Some(size)); + normal_hints.set_max_size(Some(size)); + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + self.set_inner_size_physical(size.0, size.1); } fn update_normal_hints(&self, callback: F) -> Result<(), XError> @@ -1261,64 +1272,75 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - let mut grabbed_lock = self.cursor_grabbed.lock(); - if grab == *grabbed_lock { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let mut grabbed_lock = self.cursor_grabbed_mode.lock(); + if mode == *grabbed_lock { return Ok(()); } + unsafe { // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. // Therefore, this is common to both codepaths. (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); } - let result = if grab { - let result = unsafe { - (self.xconn.xlib.XGrabPointer)( - self.xconn.display, - self.xwindow, - ffi::True, - (ffi::ButtonPressMask - | ffi::ButtonReleaseMask - | ffi::EnterWindowMask - | ffi::LeaveWindowMask - | ffi::PointerMotionMask - | ffi::PointerMotionHintMask - | ffi::Button1MotionMask - | ffi::Button2MotionMask - | ffi::Button3MotionMask - | ffi::Button4MotionMask - | ffi::Button5MotionMask - | ffi::ButtonMotionMask - | ffi::KeymapStateMask) as c_uint, - ffi::GrabModeAsync, - ffi::GrabModeAsync, - self.xwindow, - 0, - ffi::CurrentTime, - ) - }; - match result { - ffi::GrabSuccess => Ok(()), - ffi::AlreadyGrabbed => { - Err("Cursor could not be grabbed: already grabbed by another client") - } - ffi::GrabInvalidTime => Err("Cursor could not be grabbed: invalid time"), - ffi::GrabNotViewable => { - Err("Cursor could not be grabbed: grab location not viewable") + let result = match mode { + CursorGrabMode::None => self + .xconn + .flush_requests() + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))), + CursorGrabMode::Confined => { + let result = unsafe { + (self.xconn.xlib.XGrabPointer)( + self.xconn.display, + self.xwindow, + ffi::True, + (ffi::ButtonPressMask + | ffi::ButtonReleaseMask + | ffi::EnterWindowMask + | ffi::LeaveWindowMask + | ffi::PointerMotionMask + | ffi::PointerMotionHintMask + | ffi::Button1MotionMask + | ffi::Button2MotionMask + | ffi::Button3MotionMask + | ffi::Button4MotionMask + | ffi::Button5MotionMask + | ffi::ButtonMotionMask + | ffi::KeymapStateMask) as c_uint, + ffi::GrabModeAsync, + ffi::GrabModeAsync, + self.xwindow, + 0, + ffi::CurrentTime, + ) + }; + + match result { + ffi::GrabSuccess => Ok(()), + ffi::AlreadyGrabbed => { + Err("Cursor could not be confined: already confined by another client") + } + ffi::GrabInvalidTime => Err("Cursor could not be confined: invalid time"), + ffi::GrabNotViewable => { + Err("Cursor could not be confined: confine location not viewable") + } + ffi::GrabFrozen => { + Err("Cursor could not be confined: frozen by another client") + } + _ => unreachable!(), } - ffi::GrabFrozen => Err("Cursor could not be grabbed: frozen by another client"), - _ => unreachable!(), + .map_err(|err| ExternalError::Os(os_error!(OsError::XMisc(err)))) + } + CursorGrabMode::Locked => { + return Err(ExternalError::NotSupported(NotSupportedError::new())); } - .map_err(|err| ExternalError::Os(os_error!(OsError::XMisc(err)))) - } else { - self.xconn - .flush_requests() - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) }; + if result.is_ok() { - *grabbed_lock = grab; + *grabbed_lock = mode; } + result } @@ -1375,14 +1397,14 @@ impl UnownedWindow { // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` // if the cursor isn't currently grabbed - let mut grabbed_lock = self.cursor_grabbed.lock(); + let mut grabbed_lock = self.cursor_grabbed_mode.lock(); unsafe { (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); } self.xconn .flush_requests() .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; - *grabbed_lock = false; + *grabbed_lock = CursorGrabMode::None; // we keep the lock until we are done self.xconn @@ -1403,17 +1425,21 @@ impl UnownedWindow { .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) } - pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { + #[inline] + pub fn set_ime_position(&self, spot: Position) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); let _ = self .ime_sender .lock() - .send((self.xwindow, x as i16, y as i16)); + .send(ImeRequest::Position(self.xwindow, x, y)); } #[inline] - pub fn set_ime_position(&self, spot: Position) { - let (x, y) = spot.to_physical::(self.scale_factor()).into(); - self.set_ime_position_physical(x, y); + pub fn set_ime_allowed(&self, allowed: bool) { + let _ = self + .ime_sender + .lock() + .send(ImeRequest::Allow(self.xwindow, allowed)); } #[inline] @@ -1470,23 +1496,30 @@ impl UnownedWindow { #[inline] pub fn id(&self) -> WindowId { - WindowId(self.xwindow) + WindowId(self.xwindow as u64) } #[inline] pub fn request_redraw(&self) { self.redraw_sender .sender - .send(WindowId(self.xwindow)) + .send(WindowId(self.xwindow as u64)) .unwrap(); self.redraw_sender.waker.wake().unwrap(); } #[inline] - pub fn raw_window_handle(&self) -> XlibHandle { - let mut handle = XlibHandle::empty(); - handle.window = self.xlib_window(); - handle.display = self.xlib_display(); - handle + pub fn raw_window_handle(&self) -> RawWindowHandle { + let mut window_handle = XlibWindowHandle::empty(); + window_handle.window = self.xlib_window(); + RawWindowHandle::Xlib(window_handle) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = XlibDisplayHandle::empty(); + display_handle.display = self.xlib_display(); + display_handle.screen = self.screen_id; + RawDisplayHandle::Xlib(display_handle) } } diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index b5a5582cc3..a4a6ef0b4d 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -8,6 +8,7 @@ use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; +use once_cell::sync::Lazy; use super::{app_state::AppState, event::EventWrapper, util, DEVICE_ID}; use crate::event::{DeviceEvent, ElementState, Event}; @@ -16,19 +17,17 @@ pub struct AppClass(pub *const Class); unsafe impl Send for AppClass {} unsafe impl Sync for AppClass {} -lazy_static! { - pub static ref APP_CLASS: AppClass = unsafe { - let superclass = class!(NSApplication); - let mut decl = ClassDecl::new("WinitApp", superclass).unwrap(); +pub static APP_CLASS: Lazy = Lazy::new(|| unsafe { + let superclass = class!(NSApplication); + let mut decl = ClassDecl::new("WinitApp", superclass).unwrap(); - decl.add_method( - sel!(sendEvent:), - send_event as extern "C" fn(&Object, Sel, id), - ); + decl.add_method( + sel!(sendEvent:), + send_event as extern "C" fn(&Object, Sel, id), + ); - AppClass(decl.register()) - }; -} + AppClass(decl.register()) +}); // 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) diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 940a5538e6..fc5ae7e407 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,14 +1,16 @@ -use crate::{platform::macos::ActivationPolicy, platform_impl::platform::app_state::AppState}; +use std::{ + cell::{RefCell, RefMut}, + os::raw::c_void, +}; use cocoa::base::id; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; -use std::{ - cell::{RefCell, RefMut}, - os::raw::c_void, -}; +use once_cell::sync::Lazy; + +use crate::{platform::macos::ActivationPolicy, platform_impl::platform::app_state::AppState}; static AUX_DELEGATE_STATE_NAME: &str = "auxState"; @@ -21,23 +23,26 @@ pub struct AppDelegateClass(pub *const Class); unsafe impl Send for AppDelegateClass {} unsafe impl Sync for AppDelegateClass {} -lazy_static! { - pub static ref APP_DELEGATE_CLASS: AppDelegateClass = unsafe { - let superclass = class!(NSResponder); - let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); +pub static APP_DELEGATE_CLASS: Lazy = Lazy::new(|| unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); - decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); + decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(applicationDidFinishLaunching:), - did_finish_launching as extern "C" fn(&Object, Sel, id), - ); - decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(&Object, Sel, id), + ); - AppDelegateClass(decl.register()) - }; -} + decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); + + AppDelegateClass(decl.register()) +}); /// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS pub unsafe fn get_aux_state_mut(this: &Object) -> RefMut<'_, AuxDelegateState> { @@ -67,7 +72,7 @@ extern "C" fn dealloc(this: &Object, _: Sel) { let state_ptr: *mut c_void = *(this.get_ivar(AUX_DELEGATE_STATE_NAME)); // As soon as the box is constructed it is immediately dropped, releasing the underlying // memory - Box::from_raw(state_ptr as *mut RefCell); + drop(Box::from_raw(state_ptr as *mut RefCell)); } } @@ -75,3 +80,10 @@ extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) { trace_scope!("applicationDidFinishLaunching:"); AppState::launched(this); } + +extern "C" fn will_terminate(_this: &Object, _: Sel, _: id) { + trace!("Triggered `applicationWillTerminate`"); + // TODO: Notify every window that it will be destroyed, like done in iOS? + AppState::exit(); + trace!("Completed `applicationWillTerminate`"); +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 7b392a4124..711c99b158 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -21,6 +21,7 @@ use objc::{ rc::autoreleasepool, runtime::{Object, BOOL, NO, YES}, }; +use once_cell::sync::Lazy; use crate::{ dpi::LogicalSize, @@ -41,9 +42,7 @@ use crate::{ window::WindowId, }; -lazy_static! { - static ref HANDLER: Handler = Default::default(); -} +static HANDLER: Lazy = Lazy::new(Default::default); impl<'a, Never> Event<'a, Never> { fn userify(self) -> Event<'a, T> { @@ -127,7 +126,6 @@ impl EventHandler for EventLoopHandler { struct Handler { ready: AtomicBool, in_callback: AtomicBool, - dialog_is_closing: AtomicBool, control_flow: Mutex, control_flow_prev: Mutex, start_time: Mutex>, @@ -262,8 +260,6 @@ impl Handler { } } -pub static INTERRUPT_EVENT_LOOP_EXIT: AtomicBool = AtomicBool::new(false); - pub enum AppState {} impl AppState { @@ -306,6 +302,9 @@ impl AppState { HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents( StartCause::Init, ))); + // NB: For consistency all platforms must emit a 'resumed' event even though macOS + // applications don't themselves have a formal suspend/resume lifecycle. + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed)); HANDLER.set_in_callback(false); } @@ -403,40 +402,12 @@ impl AppState { if HANDLER.should_exit() { unsafe { let app: id = NSApp(); - let windows: id = msg_send![app, windows]; - let window_count: usize = msg_send![windows, count]; - - let dialog_open = if window_count > 1 { - let dialog: id = msg_send![windows, lastObject]; - let is_main_window: BOOL = msg_send![dialog, isMainWindow]; - let is_visible: BOOL = msg_send![dialog, isVisible]; - is_visible != NO && is_main_window == NO - } else { - false - }; - let dialog_is_closing = HANDLER.dialog_is_closing.load(Ordering::SeqCst); autoreleasepool(|| { - if !INTERRUPT_EVENT_LOOP_EXIT.load(Ordering::SeqCst) - && !dialog_open - && !dialog_is_closing - { - let () = msg_send![app, stop: nil]; - // To stop event loop immediately, we need to post some event here. - post_dummy_event(app); - } + let _: () = msg_send![app, stop: nil]; + // To stop event loop immediately, we need to post some event here. + post_dummy_event(app); }); - - if window_count > 0 { - let window: id = msg_send![windows, firstObject]; - let window_has_focus: BOOL = msg_send![window, isKeyWindow]; - if !dialog_open && window_has_focus != NO && dialog_is_closing { - HANDLER.dialog_is_closing.store(false, Ordering::SeqCst); - } - if dialog_open { - HANDLER.dialog_is_closing.store(true, Ordering::SeqCst); - } - } }; } HANDLER.update_start_time(); diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index cb069f8354..ea0315139c 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -17,6 +17,7 @@ use cocoa::{ foundation::{NSInteger, NSPoint, NSTimeInterval}, }; use objc::rc::autoreleasepool; +use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle}; use crate::{ event::Event, @@ -87,6 +88,11 @@ impl EventLoopWindowTarget { let monitor = monitor::primary_monitor(); Some(RootMonitorHandle { inner: monitor }) } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()) + } } impl EventLoopWindowTarget { @@ -120,7 +126,7 @@ pub struct EventLoop { _callback: Option>>, } -#[derive(Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes { pub(crate) activation_policy: ActivationPolicy, pub(crate) default_menu: bool, @@ -210,10 +216,10 @@ impl EventLoop { // A bit of juggling with the callback references to make sure // that `self.callback` is the only owner of the callback. let weak_cb: Weak<_> = Rc::downgrade(&callback); - mem::drop(callback); + drop(callback); AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); - let () = msg_send![app, run]; + let _: () = msg_send![app, run]; if let Some(panic) = self.panic_info.take() { drop(self._callback.take()); @@ -246,7 +252,7 @@ pub unsafe fn post_dummy_event(target: id) { data1: 0 as NSInteger data2: 0 as NSInteger ]; - let () = msg_send![target, postEvent: dummy_event atStart: YES]; + let _: () = msg_send![target, postEvent: dummy_event atStart: YES]; } /// Catches panics that happen inside `f` and when a panic @@ -270,7 +276,7 @@ pub fn stop_app_on_panic R + UnwindSafe, R>( unsafe { let app_class = class!(NSApplication); let app: id = msg_send![app_class, sharedApplication]; - let () = msg_send![app, stop: nil]; + let _: () = msg_send![app, stop: nil]; // Posting a dummy event to get `stop` to take effect immediately. // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 229bd6598d..0a23e60d41 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -221,3 +221,45 @@ extern "C" { pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); } + +mod core_video { + use super::*; + + #[link(name = "CoreVideo", kind = "framework")] + extern "C" {} + + // CVBase.h + + pub type CVTimeFlags = i32; // int32_t + pub const kCVTimeIsIndefinite: CVTimeFlags = 1 << 0; + + #[repr(C)] + #[derive(Debug, Clone)] + pub struct CVTime { + pub time_value: i64, // int64_t + pub time_scale: i32, // int32_t + pub flags: i32, // int32_t + } + + // CVReturn.h + + pub type CVReturn = i32; // int32_t + pub const kCVReturnSuccess: CVReturn = 0; + + // CVDisplayLink.h + + pub type CVDisplayLinkRef = *mut c_void; + + extern "C" { + pub fn CVDisplayLinkCreateWithCGDisplay( + displayID: CGDirectDisplayID, + displayLinkOut: *mut CVDisplayLinkRef, + ) -> CVReturn; + pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod( + displayLink: CVDisplayLinkRef, + ) -> CVTime; + pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef); + } +} + +pub use core_video::*; diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 50e438ae44..4608e2fa42 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,4 +1,5 @@ #![cfg(target_os = "macos")] +#![allow(clippy::let_unit_value)] #[macro_use] mod util; @@ -69,7 +70,7 @@ impl Deref for Window { } impl Window { - pub fn new( + pub(crate) fn new( _window_target: &EventLoopWindowTarget, attributes: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index 08dba6c9f9..2b93be6a5a 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -16,16 +16,12 @@ use core_foundation::{ string::CFString, }; use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; -use core_video_sys::{ - kCVReturnSuccess, kCVTimeIsIndefinite, CVDisplayLinkCreateWithCGDisplay, - CVDisplayLinkGetNominalOutputVideoRefreshPeriod, CVDisplayLinkRelease, -}; #[derive(Clone)] pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) refresh_rate_millihertz: u32, pub(crate) monitor: MonitorHandle, pub(crate) native_mode: NativeDisplayMode, } @@ -34,7 +30,7 @@ impl PartialEq for VideoMode { fn eq(&self, other: &Self) -> bool { self.size == other.size && self.bit_depth == other.bit_depth - && self.refresh_rate == other.refresh_rate + && self.refresh_rate_millihertz == other.refresh_rate_millihertz && self.monitor == other.monitor } } @@ -45,7 +41,7 @@ impl std::hash::Hash for VideoMode { fn hash(&self, state: &mut H) { self.size.hash(state); self.bit_depth.hash(state); - self.refresh_rate.hash(state); + self.refresh_rate_millihertz.hash(state); self.monitor.hash(state); } } @@ -55,7 +51,7 @@ impl std::fmt::Debug for VideoMode { f.debug_struct("VideoMode") .field("size", &self.size) .field("bit_depth", &self.bit_depth) - .field("refresh_rate", &self.refresh_rate) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) .field("monitor", &self.monitor) .finish() } @@ -91,8 +87,8 @@ impl VideoMode { self.bit_depth } - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } pub fn monitor(&self) -> RootMonitorHandle { @@ -224,22 +220,25 @@ impl MonitorHandle { unsafe { NSScreen::backingScaleFactor(screen) as f64 } } - pub fn video_modes(&self) -> impl Iterator { - let cv_refresh_rate = unsafe { + pub fn refresh_rate_millihertz(&self) -> Option { + unsafe { let mut display_link = std::ptr::null_mut(); assert_eq!( - CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link), - kCVReturnSuccess + ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link), + ffi::kCVReturnSuccess ); - let time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); - CVDisplayLinkRelease(display_link); + let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + ffi::CVDisplayLinkRelease(display_link); // This value is indefinite if an invalid display link was specified - assert!(time.flags & kCVTimeIsIndefinite == 0); + assert!(time.flags & ffi::kCVTimeIsIndefinite == 0); - time.timeScale as i64 / time.timeValue - }; + Some((time.time_scale as i64 / time.time_value * 1000) as u32) + } + } + pub fn video_modes(&self) -> impl Iterator { + let refresh_rate_millihertz = self.refresh_rate_millihertz().unwrap_or(0); let monitor = self.clone(); unsafe { @@ -259,14 +258,15 @@ impl MonitorHandle { }; modes.into_iter().map(move |mode| { - let cg_refresh_rate = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; + let cg_refresh_rate_millihertz = + ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; // CGDisplayModeGetRefreshRate returns 0.0 for any display that // isn't a CRT - let refresh_rate = if cg_refresh_rate > 0 { - cg_refresh_rate + let refresh_rate_millihertz = if cg_refresh_rate_millihertz > 0 { + (cg_refresh_rate_millihertz * 1000) as u32 } else { - cv_refresh_rate + refresh_rate_millihertz }; let pixel_encoding = @@ -287,7 +287,7 @@ impl MonitorHandle { ffi::CGDisplayModeGetPixelWidth(mode) as u32, ffi::CGDisplayModeGetPixelHeight(mode) as u32, ), - refresh_rate: refresh_rate as u16, + refresh_rate_millihertz, bit_depth, monitor: monitor.clone(), native_mode: NativeDisplayMode(mode), diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index 308637cb81..2ced84ef5f 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -99,7 +99,8 @@ pub type CFRunLoopTimerCallBack = extern "C" fn(timer: CFRunLoopTimerRef, info: pub enum CFRunLoopTimerContext {} /// This mirrors the struct with the same name from Core Foundation. -/// https://developer.apple.com/documentation/corefoundation/cfrunloopobservercontext?language=objc +/// +/// #[allow(non_snake_case)] #[repr(C)] pub struct CFRunLoopObserverContext { diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index afd62c2e9b..bf7e33ea72 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -167,11 +167,15 @@ pub unsafe fn set_maximized_async( shared_state_lock.maximized = maximized; - let curr_mask = ns_window.styleMask(); if shared_state_lock.fullscreen.is_some() { // Handle it in window_did_exit_fullscreen return; - } else if curr_mask.contains(NSWindowStyleMask::NSResizableWindowMask) { + } + + if ns_window + .styleMask() + .contains(NSWindowStyleMask::NSResizableWindowMask) + { // Just use the native zoom if resizable ns_window.zoom_(nil); } else { diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index 7747a57bc1..859bdb9c9d 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -4,14 +4,15 @@ mod cursor; pub use self::{cursor::*, r#async::*}; use std::ops::{BitAnd, Deref}; +use std::os::raw::c_uchar; use cocoa::{ - appkit::{NSApp, NSWindowStyleMask}, + appkit::{CGFloat, NSApp, NSWindowStyleMask}, base::{id, nil}, foundation::{NSPoint, NSRect, NSString, NSUInteger}, }; use core_graphics::display::CGDisplay; -use objc::runtime::{Class, Object}; +use objc::runtime::{Class, Object, BOOL, NO}; use crate::dpi::LogicalPosition; use crate::platform_impl::platform::ffi; @@ -32,7 +33,7 @@ pub const EMPTY_RANGE: ffi::NSRange = ffi::NSRange { length: 0, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct IdRef(id); impl IdRef { @@ -60,7 +61,7 @@ impl Drop for IdRef { fn drop(&mut self) { if self.0 != nil { unsafe { - let () = msg_send![self.0, release]; + let _: () = msg_send![self.0, release]; }; } } @@ -112,7 +113,7 @@ impl Drop for TraceGuard { // 1. translate the bottom-left window corner into the top-left window corner // 2. translate the coordinate from a bottom-left origin coordinate system to a top-left one pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { - CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) + CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) as f64 } /// Converts from winit screen-coordinates to macOS screen-coordinates. @@ -120,8 +121,8 @@ pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { /// macOS: bottom-left is (0, 0) and y increasing upwards pub fn window_position(position: LogicalPosition) -> NSPoint { NSPoint::new( - position.x, - CGDisplay::main().pixels_high() as f64 - position.y, + position.x as CGFloat, + CGDisplay::main().pixels_high() as CGFloat - position.y as CGFloat, ) } @@ -149,7 +150,7 @@ pub unsafe fn superclass(this: &Object) -> &Class { #[allow(dead_code)] pub unsafe fn open_emoji_picker() { - let () = msg_send![NSApp(), orderFrontCharacterPalette: nil]; + let _: () = msg_send![NSApp(), orderFrontCharacterPalette: nil]; } pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, on: bool) { @@ -165,3 +166,21 @@ pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, o // If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly! window.makeFirstResponder_(view); } + +/// For invalid utf8 sequences potentially returned by `UTF8String`, +/// it behaves identically to `String::from_utf8_lossy` +/// +/// Safety: Assumes that `string` is an instance of `NSAttributedString` or `NSString` +pub unsafe fn id_to_string_lossy(string: id) -> String { + let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; + let characters = if has_attr != NO { + // This is a *mut NSAttributedString + msg_send![string, string] + } else { + // This is already a *mut NSString + string + }; + let utf8_sequence = + std::slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); + String::from_utf8_lossy(utf8_sequence).into_owned() +} diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 2d879d5887..16bc457ecc 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -3,7 +3,10 @@ use std::{ collections::VecDeque, os::raw::*, ptr, slice, str, - sync::{Arc, Mutex, Weak}, + sync::{ + atomic::{compiler_fence, Ordering}, + Arc, Mutex, Weak, + }, }; use cocoa::{ @@ -15,11 +18,12 @@ use objc::{ declare::ClassDecl, runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, }; +use once_cell::sync::Lazy; use crate::{ - dpi::LogicalPosition, + dpi::{LogicalPosition, LogicalSize}, event::{ - DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton, + DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent, }, platform_impl::platform::{ @@ -29,7 +33,7 @@ use crate::{ scancode_to_keycode, EventWrapper, }, ffi::*, - util::{self, IdRef}, + util::{self, id_to_string_lossy, IdRef}, window::get_window_id, DEVICE_ID, }, @@ -50,20 +54,48 @@ impl Default for CursorState { } } +#[derive(Debug, Eq, PartialEq)] +enum ImeState { + /// The IME events are disabled, so only `ReceivedCharacter` is being sent to the user. + Disabled, + + /// The IME events are enabled. + Enabled, + + /// The IME is in preedit. + Preedit, + + /// The text was just commited, so the next input from the keyboard must be ignored. + Commited, +} + pub(super) struct ViewState { ns_window: id, pub cursor_state: Arc>, - /// The position of the candidate window. ime_position: LogicalPosition, - raw_characters: Option, pub(super) modifiers: ModifiersState, tracking_rect: Option, + ime_state: ImeState, + input_source: String, + + /// True iff the application wants IME events. + /// + /// Can be set using `set_ime_allowed` + ime_allowed: bool, + + /// True if the current key event should be forwarded + /// to the application, even during IME + forward_key_to_app: bool, } impl ViewState { fn get_scale_factor(&self) -> f64 { (unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as f64 } + + fn is_ime_enabled(&self) -> bool { + !matches!(self.ime_state, ImeState::Disabled) + } } pub fn new_view(ns_window: id) -> (IdRef, Weak>) { @@ -72,11 +104,13 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak>) { let state = ViewState { ns_window, cursor_state, - // By default, open the candidate window in the top left corner ime_position: LogicalPosition::new(0.0, 0.0), - raw_characters: None, modifiers: Default::default(), tracking_rect: None, + ime_state: ImeState::Disabled, + input_source: String::new(), + ime_allowed: false, + forward_key_to_app: false, }; unsafe { // This is free'd in `dealloc` @@ -97,179 +131,217 @@ pub unsafe fn set_ime_position(ns_view: id, position: LogicalPosition) { let _: () = msg_send![input_context, invalidateCharacterCoordinates]; } +pub unsafe fn set_ime_allowed(ns_view: id, ime_allowed: bool) { + let state_ptr: *mut c_void = *(*ns_view).get_mut_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + if state.ime_allowed == ime_allowed { + return; + } + state.ime_allowed = ime_allowed; + if state.ime_allowed { + return; + } + let marked_text_ref: &mut id = (*ns_view).get_mut_ivar("markedText"); + + // Clear markedText + let _: () = msg_send![*marked_text_ref, release]; + let marked_text = + ::init(NSMutableAttributedString::alloc(nil)); + *marked_text_ref = marked_text; + + if state.ime_state != ImeState::Disabled { + state.ime_state = ImeState::Disabled; + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Disabled), + })); + } +} + struct ViewClass(*const Class); unsafe impl Send for ViewClass {} unsafe impl Sync for ViewClass {} -lazy_static! { - static ref VIEW_CLASS: ViewClass = unsafe { - let superclass = class!(NSView); - let mut decl = ClassDecl::new("WinitView", superclass).unwrap(); - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(initWithWinit:), - init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id, - ); - decl.add_method( - sel!(viewDidMoveToWindow), - view_did_move_to_window as extern "C" fn(&Object, Sel), - ); - decl.add_method( - sel!(drawRect:), - draw_rect as extern "C" fn(&Object, Sel, NSRect), - ); - decl.add_method( - sel!(acceptsFirstResponder), - accepts_first_responder as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(touchBar), - touch_bar as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(resetCursorRects), - reset_cursor_rects as extern "C" fn(&Object, Sel), - ); - decl.add_method( - sel!(hasMarkedText), - has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(markedRange), - marked_range as extern "C" fn(&Object, Sel) -> NSRange, - ); - decl.add_method( - sel!(selectedRange), - selected_range as extern "C" fn(&Object, Sel) -> NSRange, - ); - decl.add_method( - sel!(setMarkedText:selectedRange:replacementRange:), - set_marked_text as extern "C" fn(&mut Object, Sel, id, NSRange, NSRange), - ); - decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(validAttributesForMarkedText), - valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, - ); - decl.add_method( - sel!(attributedSubstringForProposedRange:actualRange:), - attributed_substring_for_proposed_range - as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, - ); - decl.add_method( - sel!(insertText:replacementRange:), - insert_text as extern "C" fn(&Object, Sel, id, NSRange), - ); - decl.add_method( - sel!(characterIndexForPoint:), - character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> NSUInteger, - ); - decl.add_method( - sel!(firstRectForCharacterRange:actualRange:), - first_rect_for_character_range - as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> NSRect, - ); - decl.add_method( - sel!(doCommandBySelector:), - do_command_by_selector as extern "C" fn(&Object, Sel, Sel), - ); - decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id)); - decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id)); - decl.add_method( - sel!(flagsChanged:), - flags_changed as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(insertTab:), - insert_tab as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(insertBackTab:), - insert_back_tab as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseDown:), - mouse_down as extern "C" fn(&Object, Sel, id), - ); - decl.add_method(sel!(mouseUp:), mouse_up as extern "C" fn(&Object, Sel, id)); - decl.add_method( - sel!(rightMouseDown:), - right_mouse_down as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(rightMouseUp:), - right_mouse_up as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseDown:), - other_mouse_down as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseUp:), - other_mouse_up as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseMoved:), - mouse_moved as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseDragged:), - mouse_dragged as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(rightMouseDragged:), - right_mouse_dragged as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseDragged:), - other_mouse_dragged as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseEntered:), - mouse_entered as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(mouseExited:), - mouse_exited as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(scrollWheel:), - scroll_wheel as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(pressureChangeWithEvent:), - pressure_change_with_event as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(_wantsKeyDownForEvent:), - wants_key_down_for_event as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(cancelOperation:), - cancel_operation as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(frameDidChange:), - frame_did_change as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(acceptsFirstMouse:), - accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_ivar::<*mut c_void>("winitState"); - decl.add_ivar::("markedText"); - let protocol = Protocol::get("NSTextInputClient").unwrap(); - decl.add_protocol(protocol); - ViewClass(decl.register()) - }; -} +static VIEW_CLASS: Lazy = Lazy::new(|| unsafe { + let superclass = class!(NSView); + let mut decl = ClassDecl::new("WinitView", superclass).unwrap(); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(initWithWinit:), + init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id, + ); + decl.add_method( + sel!(viewDidMoveToWindow), + view_did_move_to_window as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(drawRect:), + draw_rect as extern "C" fn(&Object, Sel, NSRect), + ); + decl.add_method( + sel!(acceptsFirstResponder), + accepts_first_responder as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(touchBar), + touch_bar as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(resetCursorRects), + reset_cursor_rects as extern "C" fn(&Object, Sel), + ); + + // ------------------------------------------------------------------ + // NSTextInputClient + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&mut Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(characterIndexForPoint:), + character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> NSUInteger, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> NSRect, + ); + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); + // ------------------------------------------------------------------ + + decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id)); + decl.add_method( + sel!(flagsChanged:), + flags_changed as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(insertTab:), + insert_tab as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(insertBackTab:), + insert_back_tab as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDown:), + mouse_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method(sel!(mouseUp:), mouse_up as extern "C" fn(&Object, Sel, id)); + decl.add_method( + sel!(rightMouseDown:), + right_mouse_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + right_mouse_up as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + other_mouse_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + other_mouse_up as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + mouse_moved as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + mouse_dragged as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDragged:), + right_mouse_dragged as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDragged:), + other_mouse_dragged as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseEntered:), + mouse_entered as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseExited:), + mouse_exited as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + scroll_wheel as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(magnifyWithEvent:), + magnify_with_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rotateWithEvent:), + rotate_with_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(pressureChangeWithEvent:), + pressure_change_with_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(_wantsKeyDownForEvent:), + wants_key_down_for_event as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(cancelOperation:), + cancel_operation as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(frameDidChange:), + frame_did_change as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_ivar::<*mut c_void>("winitState"); + decl.add_ivar::("markedText"); + let protocol = Protocol::get("NSTextInputClient").unwrap(); + decl.add_protocol(protocol); + ViewClass(decl.register()) +}); extern "C" fn dealloc(this: &Object, _sel: Sel) { unsafe { - let state: *mut c_void = *this.get_ivar("winitState"); let marked_text: id = *this.get_ivar("markedText"); let _: () = msg_send![marked_text, release]; - Box::from_raw(state as *mut ViewState); + let state: *mut c_void = *this.get_ivar("winitState"); + drop(Box::from_raw(state as *mut ViewState)); } } @@ -285,15 +357,19 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i let notification_center: &Object = msg_send![class!(NSNotificationCenter), defaultCenter]; - let notification_name = + // About frame change + let frame_did_change_notification_name = IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification")); let _: () = msg_send![ notification_center, addObserver: this selector: sel!(frameDidChange:) - name: notification_name + name: frame_did_change_notification_name object: this ]; + + let winit_state = &mut *(state as *mut ViewState); + winit_state.input_source = current_input_source(this); } this } @@ -337,8 +413,17 @@ extern "C" fn frame_did_change(this: &Object, _sel: Sel, _event: id) { userData:ptr::null_mut::() assumeInside:NO ]; - state.tracking_rect = Some(tracking_rect); + + // Emit resize event here rather than from windowDidResize because: + // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. + // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical::(state.get_scale_factor()); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Resized(size), + })); } } @@ -351,7 +436,7 @@ extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { AppState::handle_redraw(WindowId(get_window_id(state.ns_window))); let superclass = util::superclass(this); - let () = msg_send![super(this, superclass), drawRect: rect]; + let _: () = msg_send![super(this, superclass), drawRect: rect]; } } @@ -402,7 +487,7 @@ extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange { let marked_text: id = *this.get_ivar("markedText"); let length = marked_text.length(); if length > 0 { - NSRange::new(0, length - 1) + NSRange::new(0, length) } else { util::EMPTY_RANGE } @@ -414,6 +499,13 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange { util::EMPTY_RANGE } +/// Safety: Assumes that `view` is an instance of `VIEW_CLASS` from winit. +unsafe fn current_input_source(view: *const Object) -> String { + let input_context: id = msg_send![view, inputContext]; + let input_source: id = msg_send![input_context, selectedKeyboardInputSource]; + id_to_string_lossy(input_source) +} + extern "C" fn set_marked_text( this: &mut Object, _sel: Sel, @@ -423,7 +515,10 @@ extern "C" fn set_marked_text( ) { trace_scope!("setMarkedText:selectedRange:replacementRange:"); unsafe { + // Get pre-edit text let marked_text_ref: &mut id = this.get_mut_ivar("markedText"); + + // Update markedText let _: () = msg_send![(*marked_text_ref), release]; let marked_text = NSMutableAttributedString::alloc(nil); let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; @@ -433,6 +528,40 @@ extern "C" fn set_marked_text( marked_text.initWithString(string); }; *marked_text_ref = marked_text; + + // Update ViewState with new marked text + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let preedit_string = id_to_string_lossy(string); + + // Notify IME is active if application still doesn't know it. + if state.ime_state == ImeState::Disabled { + state.input_source = current_input_source(this); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Enabled), + })); + } + + // Don't update state to preedit when we've just commited a string, since the following + // preedit string will be None anyway. + if state.ime_state != ImeState::Commited { + state.ime_state = ImeState::Preedit; + } + + // Empty string basically means that there's no preedit, so indicate that by sending + // `None` cursor range. + let cursor_range = if preedit_string.is_empty() { + None + } else { + Some((preedit_string.len(), preedit_string.len())) + }; + + // Send WindowEvent for updating marked text + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit(preedit_string, cursor_range)), + })); } } @@ -446,6 +575,19 @@ extern "C" fn unmark_text(this: &Object, _sel: Sel) { let _: () = msg_send![s, release]; let input_context: id = msg_send![this, inputContext]; let _: () = msg_send![input_context, discardMarkedText]; + + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + })); + if state.is_ime_enabled() { + // Leave the Preedit state + state.ime_state = ImeState::Enabled; + } else { + warn!("Expected to have IME enabled when receiving unmarkText"); + } } } @@ -499,67 +641,45 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); - let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; - let characters = if has_attr != NO { - // This is a *mut NSAttributedString - msg_send![string, string] - } else { - // This is already a *mut NSString - string - }; + let string = id_to_string_lossy(string); - let slice = - slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); - let string = str::from_utf8_unchecked(slice); + let is_control = string.chars().next().map_or(false, |c| c.is_control()); // We don't need this now, but it's here if that changes. //let event: id = msg_send![NSApp(), currentEvent]; - let mut events = VecDeque::with_capacity(characters.len()); - for character in string.chars().filter(|c| !is_corporate_character(*c)) { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + if state.is_ime_enabled() && !is_control { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), + event: WindowEvent::Ime(Ime::Commit(string)), })); + state.ime_state = ImeState::Commited; } - - AppState::queue_events(events); } } -extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { +extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _command: Sel) { trace_scope!("doCommandBySelector:"); - // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character - // happens, i.e. newlines, tabs, and Ctrl+C. + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human + // readable" character happens, i.e. newlines, tabs, and Ctrl+C. unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); - let mut events = VecDeque::with_capacity(1); - if command == sel!(insertNewline:) { - // The `else` condition would emit the same character, but I'm keeping this here both... - // 1) as a reminder for how `doCommandBySelector` works - // 2) to make our use of carriage return explicit - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter('\r'), - })); - } else { - let raw_characters = state.raw_characters.take(); - if let Some(raw_characters) = raw_characters { - for character in raw_characters - .chars() - .filter(|c| !is_corporate_character(*c)) - { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), - })); - } - } - }; + // We shouldn't forward any character from just commited text, since we'll end up sending + // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, + // which is not desired given it was used to confirm IME input. + if state.ime_state == ImeState::Commited { + return; + } - AppState::queue_events(events); + state.forward_key_to_app = true; + + let has_marked_text: BOOL = msg_send![this, hasMarkedText]; + if has_marked_text == NO && state.ime_state == ImeState::Preedit { + // Leave preedit so that we also report the keyup for this key + state.ime_state = ImeState::Enabled; + } } } @@ -637,54 +757,79 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let window_id = WindowId(get_window_id(state.ns_window)); + + let input_source = current_input_source(this); + if state.input_source != input_source && state.is_ime_enabled() { + state.ime_state = ImeState::Disabled; + state.input_source = input_source; + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Disabled), + })); + } + let was_in_preedit = state.ime_state == ImeState::Preedit; + let characters = get_characters(event, false); + state.forward_key_to_app = false; + + // The `interpretKeyEvents` function might call + // `setMarkedText`, `insertText`, and `doCommandBySelector`. + // It's important that we call this before queuing the KeyboardInput, because + // we must send the `KeyboardInput` event during IME if it triggered + // `doCommandBySelector`. (doCommandBySelector means that the keyboard input + // is not handled by IME and should be handled by the application) + let mut text_commited = false; + if state.ime_allowed { + let events_for_nsview: id = msg_send![class!(NSArray), arrayWithObject: event]; + let _: () = msg_send![this, interpretKeyEvents: events_for_nsview]; + + // Using a compiler fence because `interpretKeyEvents` might call + // into functions that modify the `ViewState`, but the compiler + // doesn't know this. Without the fence, the compiler may think that + // some of the reads (eg `state.ime_state`) that happen after this + // point are not needed. + compiler_fence(Ordering::SeqCst); + + // If the text was commited we must treat the next keyboard event as IME related. + if state.ime_state == ImeState::Commited { + state.ime_state = ImeState::Enabled; + text_commited = true; + } + } - state.raw_characters = Some(characters.clone()); + let now_in_preedit = state.ime_state == ImeState::Preedit; let scancode = get_scancode(event) as u32; let virtual_keycode = retrieve_keycode(event); - let is_repeat: BOOL = msg_send![event, isARepeat]; - update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] - let window_event = Event::WindowEvent { - window_id, - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Pressed, - scancode, - virtual_keycode, - modifiers: event_mods(event), + let ime_related = was_in_preedit || now_in_preedit || text_commited; + + if !ime_related || state.forward_key_to_app || !state.ime_allowed { + #[allow(deprecated)] + let window_event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Pressed, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }; + }; - let pass_along = { AppState::queue_event(EventWrapper::StaticEvent(window_event)); - // Emit `ReceivedCharacter` for key repeats - if is_repeat != NO { - for character in characters.chars().filter(|c| !is_corporate_character(*c)) { - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id, - event: WindowEvent::ReceivedCharacter(character), - })); - } - false - } else { - true - } - }; - if pass_along { - // Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do... - // So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some - // keys to generate twice as many characters. - let array: id = msg_send![class!(NSArray), arrayWithObject: event]; - let _: () = msg_send![this, interpretKeyEvents: array]; + for character in characters.chars().filter(|c| !is_corporate_character(*c)) { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::ReceivedCharacter(character), + })); + } } } } @@ -700,22 +845,25 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] - let window_event = Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Released, - scancode, - virtual_keycode, - modifiers: event_mods(event), + // We want to send keyboard input when we are not currently in preedit + if state.ime_state != ImeState::Preedit { + #[allow(deprecated)] + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Released, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }; + }; - AppState::queue_event(EventWrapper::StaticEvent(window_event)); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } } } @@ -786,7 +934,7 @@ extern "C" fn insert_tab(this: &Object, _sel: Sel, _sender: id) { let first_responder: id = msg_send![window, firstResponder]; let this_ptr = this as *const _ as *mut _; if first_responder == this_ptr { - let (): _ = msg_send![window, selectNextKeyView: this]; + let _: () = msg_send![window, selectNextKeyView: this]; } } } @@ -798,7 +946,7 @@ extern "C" fn insert_back_tab(this: &Object, _sel: Sel, _sender: id) { let first_responder: id = msg_send![window, firstResponder]; let this_ptr = this as *const _ as *mut _; if first_responder == this_ptr { - let (): _ = msg_send![window, selectPreviousKeyView: this]; + let _: () = msg_send![window, selectPreviousKeyView: this]; } } } @@ -1008,12 +1156,27 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { MouseScrollDelta::LineDelta(x as f32, y as f32) } }; - let phase = match event.phase() { + + // The "momentum phase," if any, has higher priority than touch phase (the two should + // be mutually exclusive anyhow, which is why the API is rather incoherent). If no momentum + // phase is recorded (or rather, the started/ended cases of the momentum phase) then we + // report the touch phase. + let phase = match event.momentumPhase() { NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { TouchPhase::Started } - NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, - _ => TouchPhase::Moved, + NSEventPhase::NSEventPhaseEnded | NSEventPhase::NSEventPhaseCancelled => { + TouchPhase::Ended + } + _ => match event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded | NSEventPhase::NSEventPhaseCancelled => { + TouchPhase::Ended + } + _ => TouchPhase::Moved, + }, }; let device_event = Event::DeviceEvent { @@ -1041,6 +1204,64 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { } } +extern "C" fn magnify_with_event(this: &Object, _sel: Sel, event: id) { + trace_scope!("magnifyWithEvent:"); + + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let delta = event.magnification(); + let phase = match event.phase() { + NSEventPhase::NSEventPhaseBegan => TouchPhase::Started, + NSEventPhase::NSEventPhaseChanged => TouchPhase::Moved, + NSEventPhase::NSEventPhaseCancelled => TouchPhase::Cancelled, + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => return, + }; + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::TouchpadMagnify { + device_id: DEVICE_ID, + delta, + phase, + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + +extern "C" fn rotate_with_event(this: &Object, _sel: Sel, event: id) { + trace_scope!("rotateWithEvent:"); + + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let delta = event.rotation(); + let phase = match event.phase() { + NSEventPhase::NSEventPhaseBegan => TouchPhase::Started, + NSEventPhase::NSEventPhaseChanged => TouchPhase::Moved, + NSEventPhase::NSEventPhaseCancelled => TouchPhase::Cancelled, + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => return, + }; + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::TouchpadRotate { + device_id: DEVICE_ID, + delta, + phase, + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { trace_scope!("pressureChangeWithEvent:"); @@ -1058,7 +1279,7 @@ extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { event: WindowEvent::TouchpadPressure { device_id: DEVICE_ID, pressure, - stage, + stage: stage as i64, }, }; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index a4b827c702..f804a275f2 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1,4 +1,3 @@ -use raw_window_handle::{AppKitHandle, RawWindowHandle}; use std::{ collections::VecDeque, convert::TryInto, @@ -10,6 +9,10 @@ use std::{ }, }; +use raw_window_handle::{ + AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle, +}; + use crate::{ dpi::{ LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, @@ -20,7 +23,6 @@ use crate::{ platform::macos::WindowExtMacOS, platform_impl::platform::{ app_state::AppState, - app_state::INTERRUPT_EVENT_LOOP_EXIT, ffi, monitor::{self, MonitorHandle, VideoMode}, util::{self, IdRef}, @@ -30,7 +32,8 @@ use crate::{ OsError, }, window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, + WindowId as RootWindowId, }, }; use cocoa::{ @@ -47,6 +50,7 @@ use objc::{ rc::autoreleasepool, runtime::{Class, Object, Sel, BOOL, NO, YES}, }; +use once_cell::sync::Lazy; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId(pub usize); @@ -57,6 +61,18 @@ impl WindowId { } } +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as usize) + } +} + // Convert the `cocoa::base::id` associated with a window to a usize to use as a unique identifier // for the window. pub fn get_window_id(window_cocoa_id: id) -> WindowId { @@ -99,8 +115,13 @@ unsafe fn create_view( ) -> Option<(IdRef, Weak>)> { let (ns_view, cursor_state) = new_view(ns_window); ns_view.non_nil().map(|ns_view| { + // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until + // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid + // always the default system value in favour of the user's code if !pl_attribs.disallow_hidpi { ns_view.setWantsBestResolutionOpenGLSurface_(YES); + } else { + ns_view.setWantsBestResolutionOpenGLSurface_(NO); } // On Mojave, views automatically become layer-backed shortly after being added to @@ -247,32 +268,30 @@ struct WindowClass(*const Class); unsafe impl Send for WindowClass {} unsafe impl Sync for WindowClass {} -lazy_static! { - static ref WINDOW_CLASS: WindowClass = unsafe { - let window_superclass = class!(NSWindow); - let mut decl = ClassDecl::new("WinitWindow", window_superclass).unwrap(); +static WINDOW_CLASS: Lazy = Lazy::new(|| unsafe { + let window_superclass = class!(NSWindow); + let mut decl = ClassDecl::new("WinitWindow", window_superclass).unwrap(); - pub extern "C" fn can_become_main_window(_: &Object, _: Sel) -> BOOL { - trace_scope!("canBecomeMainWindow"); - YES - } + pub extern "C" fn can_become_main_window(_: &Object, _: Sel) -> BOOL { + trace_scope!("canBecomeMainWindow"); + YES + } - pub extern "C" fn can_become_key_window(_: &Object, _: Sel) -> BOOL { - trace_scope!("canBecomeKeyWindow"); - YES - } + pub extern "C" fn can_become_key_window(_: &Object, _: Sel) -> BOOL { + trace_scope!("canBecomeKeyWindow"); + YES + } - decl.add_method( - sel!(canBecomeMainWindow), - can_become_main_window as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(canBecomeKeyWindow), - can_become_key_window as extern "C" fn(&Object, Sel) -> BOOL, - ); - WindowClass(decl.register()) - }; -} + decl.add_method( + sel!(canBecomeMainWindow), + can_become_main_window as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(canBecomeKeyWindow), + can_become_key_window as extern "C" fn(&Object, Sel) -> BOOL, + ); + WindowClass(decl.register()) +}); #[derive(Default)] pub struct SharedState { @@ -373,7 +392,7 @@ unsafe impl Send for UnownedWindow {} unsafe impl Sync for UnownedWindow {} impl UnownedWindow { - pub fn new( + pub(crate) fn new( mut win_attribs: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, ) -> Result<(Arc, IdRef), RootOsError> { @@ -416,7 +435,7 @@ impl UnownedWindow { use cocoa::foundation::NSArray; // register for drag and drop operations. - let () = msg_send![ + let _: () = msg_send![ *ns_window, registerForDraggedTypes: NSArray::arrayWithObject(nil, appkit::NSFilenamesPboardType) @@ -461,7 +480,7 @@ impl UnownedWindow { if maximized { window.set_maximized(maximized); } - + trace!("Done unowned window::new"); Ok((window, delegate)) } @@ -623,9 +642,17 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 - CGDisplay::associate_mouse_and_mouse_cursor_position(!grab) + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) } @@ -907,8 +934,6 @@ impl UnownedWindow { let mut shared_state_lock = self.lock_shared_state("set_fullscreen"); shared_state_lock.fullscreen = fullscreen.clone(); - INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); - match (&old_fullscreen, &fullscreen) { (&None, &Some(_)) => unsafe { util::toggle_full_screen_async( @@ -955,7 +980,7 @@ impl UnownedWindow { | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; app.setPresentationOptions_(presentation_options); - let () = msg_send![*self.ns_window, setLevel: ffi::CGShieldingWindowLevel() + 1]; + let _: () = msg_send![*self.ns_window, setLevel: ffi::CGShieldingWindowLevel() + 1]; }, ( &Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), @@ -973,12 +998,12 @@ impl UnownedWindow { // Restore the normal window level following the Borderless fullscreen // `CGShieldingWindowLevel() + 1` hack. - let () = msg_send![ + let _: () = msg_send![ *self.ns_window, setLevel: ffi::NSWindowLevel::NSNormalWindowLevel ]; }, - _ => INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst), + _ => {} }; } @@ -1054,6 +1079,13 @@ impl UnownedWindow { unsafe { view::set_ime_position(*self.ns_view, logical_spot) }; } + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + unsafe { + view::set_ime_allowed(*self.ns_view, allowed); + } + } + #[inline] pub fn focus_window(&self) { let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] }; @@ -1115,10 +1147,15 @@ impl UnownedWindow { #[inline] pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = AppKitHandle::empty(); - handle.ns_window = *self.ns_window as *mut _; - handle.ns_view = *self.ns_view as *mut _; - RawWindowHandle::AppKit(handle) + let mut window_handle = AppKitWindowHandle::empty(); + window_handle.ns_window = *self.ns_window as *mut _; + window_handle.ns_view = *self.ns_view as *mut _; + RawWindowHandle::AppKit(window_handle) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()) } } @@ -1252,10 +1289,11 @@ unsafe fn set_min_inner_size(window: V, mut min_size: Logica // Convert from client area size to window size min_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0 min_size.height += (current_rect.size.height - content_rect.size.height) as f64; - window.setMinSize_(NSSize { + let min_size = NSSize { width: min_size.width as CGFloat, height: min_size.height as CGFloat, - }); + }; + window.setMinSize_(min_size); // If necessary, resize the window to match constraint if current_rect.size.width < min_size.width { current_rect.size.width = min_size.width; @@ -1276,10 +1314,11 @@ unsafe fn set_max_inner_size(window: V, mut max_size: Logica // Convert from client area size to window size max_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0 max_size.height += (current_rect.size.height - content_rect.size.height) as f64; - window.setMaxSize_(NSSize { + let max_size = NSSize { width: max_size.width as CGFloat, height: max_size.height as CGFloat, - }); + }; + window.setMaxSize_(max_size); // If necessary, resize the window to match constraint if current_rect.size.width > max_size.width { current_rect.size.width = max_size.width; diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index 55ea8a07ec..001c89fdd3 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -1,11 +1,11 @@ use std::{ f64, os::raw::c_void, - sync::{atomic::Ordering, Arc, Weak}, + sync::{Arc, Weak}, }; use cocoa::{ - appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow}, + appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow, NSWindowOcclusionState}, base::{id, nil}, foundation::NSUInteger, }; @@ -14,13 +14,13 @@ use objc::{ rc::autoreleasepool, runtime::{Class, Object, Sel, BOOL, NO, YES}, }; +use once_cell::sync::Lazy; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{Event, ModifiersState, WindowEvent}, platform_impl::platform::{ app_state::AppState, - app_state::INTERRUPT_EVENT_LOOP_EXIT, event::{EventProxy, EventWrapper}, util::{self, IdRef}, view::ViewState, @@ -98,14 +98,6 @@ impl WindowDelegateState { AppState::queue_event(wrapper); } - pub fn emit_resize_event(&mut self) { - let rect = unsafe { NSView::frame(*self.ns_view) }; - let scale_factor = self.get_scale_factor(); - let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); - let size = logical_size.to_physical(scale_factor); - self.emit_event(WindowEvent::Resized(size)); - } - fn emit_move_event(&mut self) { let rect = unsafe { NSWindow::frame(*self.ns_window) }; let x = rect.origin.x as f64; @@ -143,97 +135,99 @@ struct WindowDelegateClass(*const Class); unsafe impl Send for WindowDelegateClass {} unsafe impl Sync for WindowDelegateClass {} -lazy_static! { - static ref WINDOW_DELEGATE_CLASS: WindowDelegateClass = unsafe { - let superclass = class!(NSResponder); - let mut decl = ClassDecl::new("WinitWindowDelegate", superclass).unwrap(); - - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); - decl.add_method( - sel!(initWithWinit:), - init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id, - ); - - decl.add_method( - sel!(windowShouldClose:), - window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(windowWillClose:), - window_will_close as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResize:), - window_did_resize as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidMove:), - window_did_move as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidChangeBackingProperties:), - window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidBecomeKey:), - window_did_become_key as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResignKey:), - window_did_resign_key as extern "C" fn(&Object, Sel, id), - ); - - decl.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(prepareForDragOperation:), - prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(concludeDragOperation:), - conclude_drag_operation as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(draggingExited:), - dragging_exited as extern "C" fn(&Object, Sel, id), - ); - - decl.add_method( - sel!(window:willUseFullScreenPresentationOptions:), - window_will_use_fullscreen_presentation_options - as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, - ); - decl.add_method( - sel!(windowDidEnterFullScreen:), - window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillEnterFullScreen:), - window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidExitFullScreen:), - window_did_exit_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillExitFullScreen:), - window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidFailToEnterFullScreen:), - window_did_fail_to_enter_fullscreen as extern "C" fn(&Object, Sel, id), - ); - - decl.add_ivar::<*mut c_void>("winitState"); - WindowDelegateClass(decl.register()) - }; -} +static WINDOW_DELEGATE_CLASS: Lazy = Lazy::new(|| unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new("WinitWindowDelegate", superclass).unwrap(); + + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(initWithWinit:), + init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id, + ); + + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(windowWillClose:), + window_will_close as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeBackingProperties:), + window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_become_key as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_resign_key as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(window:willUseFullScreenPresentationOptions:), + window_will_use_fullscreen_presentation_options + as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + ); + decl.add_method( + sel!(windowDidEnterFullScreen:), + window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidExitFullScreen:), + window_did_exit_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillExitFullScreen:), + window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidFailToEnterFullScreen:), + window_did_fail_to_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeOcclusionState:), + window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), + ); + + decl.add_ivar::<*mut c_void>("winitState"); + WindowDelegateClass(decl.register()) +}); // This function is definitely unsafe, but labeling that would increase // boilerplate and wouldn't really clarify anything... @@ -247,7 +241,7 @@ fn with_state T, T>(this: &Object, callba extern "C" fn dealloc(this: &Object, _sel: Sel) { with_state(this, |state| unsafe { - Box::from_raw(state as *mut WindowDelegateState); + drop(Box::from_raw(state as *mut WindowDelegateState)); }); } @@ -257,7 +251,7 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i if this != nil { (*this).set_ivar("winitState", state); with_state(&*this, |state| { - let () = msg_send![*state.ns_window, setDelegate: this]; + let _: () = msg_send![*state.ns_window, setDelegate: this]; }); } this @@ -277,7 +271,7 @@ extern "C" fn window_will_close(this: &Object, _: Sel, _: id) { autoreleasepool(|| { // Since El Capitan, we need to be careful that delegate methods can't // be called after the window closes. - let () = msg_send![*state.ns_window, setDelegate: nil]; + let _: () = msg_send![*state.ns_window, setDelegate: nil]; }); state.emit_event(WindowEvent::Destroyed); }); @@ -286,7 +280,7 @@ extern "C" fn window_will_close(this: &Object, _: Sel, _: id) { extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { trace_scope!("windowDidResize:"); with_state(this, |state| { - state.emit_resize_event(); + // NOTE: WindowEvent::Resized is reported in frameDidChange. state.emit_move_event(); }); } @@ -422,13 +416,12 @@ extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { trace_scope!("windowWillEnterFullscreen:"); - INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); - with_state(this, |state| { state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_will_enter_fullscreen"); shared_state.maximized = window.is_zoomed(); - match shared_state.fullscreen { + let fullscreen = shared_state.fullscreen.as_ref(); + match fullscreen { // Exclusive mode sets the state in `set_fullscreen` as the user // can't enter exclusive mode by other means (like the // fullscreen button on the window decorations) @@ -453,8 +446,6 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { trace_scope!("windowWillExitFullScreen:"); - INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); - with_state(this, |state| { state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_will_exit_fullscreen"); @@ -498,8 +489,6 @@ extern "C" fn window_will_use_fullscreen_presentation_options( /// Invoked when entered fullscreen extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { trace_scope!("windowDidEnterFullscreen:"); - INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst); - with_state(this, |state| { state.initial_fullscreen = false; state.with_window(|window| { @@ -517,7 +506,6 @@ extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { /// Invoked when exited fullscreen extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { trace_scope!("windowDidExitFullscreen:"); - INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst); with_state(this, |state| { state.with_window(|window| { @@ -558,15 +546,30 @@ extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) shared_state.target_fullscreen = None; }); if state.initial_fullscreen { - let _: () = unsafe { - msg_send![*state.ns_window, + unsafe { + let _: () = msg_send![*state.ns_window, performSelector:sel!(toggleFullScreen:) withObject:nil afterDelay: 0.5 - ] + ]; }; } else { state.with_window(|window| window.restore_state_from_fullscreen()); } }); } + +// Invoked when the occlusion state of the window changes +extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { + trace_scope!("windowDidChangeOcclusionState:"); + unsafe { + with_state(this, |state| { + state.emit_event(WindowEvent::Occluded( + !state + .ns_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible), + )) + }); + } +} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index d193094ea7..6bec70c972 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -16,7 +16,7 @@ pub struct EventLoop { elw: RootEventLoopWindowTarget, } -#[derive(Default, Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes {} impl EventLoop { @@ -29,7 +29,22 @@ impl EventLoop { } } - pub fn run(self, mut event_handler: F) -> ! + pub fn run(self, event_handler: F) -> ! + where + F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + { + self.spawn(event_handler); + + // Throw an exception to break out of Rust execution and use unreachable to tell the + // compiler this function won't return, giving it a return type of '!' + backend::throw( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + + unreachable!(); + } + + pub fn spawn(self, mut event_handler: F) where F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), { @@ -41,14 +56,6 @@ impl EventLoop { self.elw.p.run(Box::new(move |event, flow| { event_handler(event, &target, flow) })); - - // Throw an exception to break out of Rust exceution and use unreachable to tell the - // compiler this function won't return, giving it a return type of '!' - backend::throw( - "Using exceptions for control flow, don't mind me. This isn't actually an error!", - ); - - unreachable!(); } pub fn create_proxy(&self) -> EventLoopProxy { diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index d476a4eeeb..55acc1ec1c 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -65,7 +65,7 @@ impl Runner { } } - /// Returns the cooresponding `StartCause` for the current `state`, or `None` + /// Returns the corresponding `StartCause` for the current `state`, or `None` /// when in `Exit` state. fn maybe_start_cause(&self) -> Option { Some(match self.state { @@ -158,8 +158,9 @@ impl Shared { } pub fn init(&self) { - let start_cause = Event::NewEvents(StartCause::Init); - self.run_until_cleared(iter::once(start_cause)); + // NB: For consistency all platforms must emit a 'resumed' event even though web + // applications don't themselves have a formal suspend/resume lifecycle. + self.run_until_cleared([Event::NewEvents(StartCause::Init), Event::Resumed].into_iter()); } // Run the polling logic for the Poll ControlFlow, which involves clearing the queue @@ -456,11 +457,8 @@ impl Shared { ControlFlow::ExitWithCode(_) => State::Exit, }; - match *self.0.runner.borrow_mut() { - RunnerEnum::Running(ref mut runner) => { - runner.state = new_state; - } - _ => (), + if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() { + runner.state = new_state; } } diff --git a/src/platform_impl/web/event_loop/state.rs b/src/platform_impl/web/event_loop/state.rs index 16b6e6232c..d7c7fb65c6 100644 --- a/src/platform_impl/web/event_loop/state.rs +++ b/src/platform_impl/web/event_loop/state.rs @@ -22,10 +22,7 @@ pub enum State { impl State { pub fn is_exit(&self) -> bool { - match self { - State::Exit => true, - _ => false, - } + matches!(self, State::Exit) } pub fn control_flow(&self) -> ControlFlow { diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 0ed7c37ace..6b15e3dd22 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,3 +1,10 @@ +use std::cell::RefCell; +use std::clone::Clone; +use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; +use std::rc::Rc; + +use raw_window_handle::{RawDisplayHandle, WebDisplayHandle}; + use super::{ super::monitor::MonitorHandle, backend, device::DeviceId, proxy::EventLoopProxy, runner, window::WindowId, @@ -10,10 +17,6 @@ use crate::event::{ use crate::event_loop::ControlFlow; use crate::monitor::MonitorHandle as RootMH; use crate::window::{Theme, WindowId as RootWindowId}; -use std::cell::RefCell; -use std::clone::Clone; -use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; -use std::rc::Rc; pub struct EventLoopWindowTarget { pub(crate) runner: runner::Shared, @@ -50,7 +53,12 @@ impl EventLoopWindowTarget { WindowId(self.runner.generate_id()) } - pub fn register(&self, canvas: &Rc>, id: WindowId) { + pub fn register( + &self, + canvas: &Rc>, + id: WindowId, + prevent_default: bool, + ) { self.runner.add_canvas(RootWindowId(id), canvas); let mut canvas = canvas.borrow_mut(); canvas.set_attribute("data-raw-handle", &id.0.to_string()); @@ -72,48 +80,57 @@ impl EventLoopWindowTarget { }); let runner = self.runner.clone(); - canvas.on_keyboard_press(move |scancode, virtual_keycode, modifiers| { - #[allow(deprecated)] - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Pressed, - virtual_keycode, - modifiers, + canvas.on_keyboard_press( + move |scancode, virtual_keycode, modifiers| { + #[allow(deprecated)] + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + input: KeyboardInput { + scancode, + state: ElementState::Pressed, + virtual_keycode, + modifiers, + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }); - }); + }); + }, + prevent_default, + ); let runner = self.runner.clone(); - canvas.on_keyboard_release(move |scancode, virtual_keycode, modifiers| { - #[allow(deprecated)] - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Released, - virtual_keycode, - modifiers, + canvas.on_keyboard_release( + move |scancode, virtual_keycode, modifiers| { + #[allow(deprecated)] + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + input: KeyboardInput { + scancode, + state: ElementState::Released, + virtual_keycode, + modifiers, + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }); - }); + }); + }, + prevent_default, + ); let runner = self.runner.clone(); - canvas.on_received_character(move |char_code| { - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ReceivedCharacter(char_code), - }); - }); + canvas.on_received_character( + move |char_code| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ReceivedCharacter(char_code), + }); + }, + prevent_default, + ); let runner = self.runner.clone(); canvas.on_cursor_leave(move |pointer_id| { @@ -160,13 +177,17 @@ impl EventLoopWindowTarget { // user code has the correct cursor position. runner.send_events( std::iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + .chain(std::iter::once(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { device_id: RootDeviceId(DeviceId(pointer_id)), position, modifiers, }, - }) + })) .chain(std::iter::once(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseInput { @@ -193,17 +214,20 @@ impl EventLoopWindowTarget { }); let runner = self.runner.clone(); - canvas.on_mouse_wheel(move |pointer_id, delta, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::MouseWheel { - device_id: RootDeviceId(DeviceId(pointer_id)), - delta, - phase: TouchPhase::Moved, - modifiers, - }, - }); - }); + canvas.on_mouse_wheel( + move |pointer_id, delta, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseWheel { + device_id: RootDeviceId(DeviceId(pointer_id)), + delta, + phase: TouchPhase::Moved, + modifiers, + }, + }); + }, + prevent_default, + ); let runner = self.runner.clone(); let raw = canvas.raw().clone(); @@ -258,4 +282,8 @@ impl EventLoopWindowTarget { inner: MonitorHandle, }) } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Web(WebDisplayHandle::empty()) + } } diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 780ffccd0a..5bbcc44319 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -17,6 +17,10 @@ impl MonitorHandle { None } + pub fn refresh_rate_millihertz(&self) -> Option { + None + } + pub fn size(&self) -> PhysicalSize { PhysicalSize { width: 0, @@ -41,8 +45,8 @@ impl VideoMode { unimplemented!(); } - pub fn refresh_rate(&self) -> u16 { - 32 + pub fn refresh_rate_millihertz(&self) -> u32 { + 32000 } pub fn monitor(&self) -> RootMonitorHandle { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 8d68e5aaab..ebae672227 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -44,11 +44,11 @@ impl Canvas { Some(canvas) => canvas, None => { let window = web_sys::window() - .ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + .ok_or_else(|| os_error!(OsError("Failed to obtain window".to_owned())))?; let document = window .document() - .ok_or(os_error!(OsError("Failed to obtain document".to_owned())))?; + .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; document .create_element("canvas") @@ -62,9 +62,11 @@ impl Canvas { // sequential keyboard navigation, but its order is defined by the // document's source order. // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex - canvas - .set_attribute("tabindex", "0") - .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + if attr.focusable { + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + } let mouse_state = if has_pointer_event() { MouseState::HasPointerEvent(pointer_handler::PointerHandler::new()) @@ -89,15 +91,15 @@ impl Canvas { }) } - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), RootOE> { - if grab { + pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { + if lock { self.raw().request_pointer_lock(); } else { let window = web_sys::window() - .ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + .ok_or_else(|| os_error!(OsError("Failed to obtain window".to_owned())))?; let document = window .document() - .ok_or(os_error!(OsError("Failed to obtain document".to_owned())))?; + .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; document.exit_pointer_lock(); } Ok(()) @@ -107,7 +109,7 @@ impl Canvas { self.common .raw .set_attribute(attribute, value) - .expect(&format!("Set attribute: {}", attribute)); + .unwrap_or_else(|err| panic!("error: {:?}\nSet attribute: {}", err, attribute)) } pub fn position(&self) -> LogicalPosition { @@ -148,14 +150,17 @@ impl Canvas { })); } - pub fn on_keyboard_release(&mut self, mut handler: F) + pub fn on_keyboard_release(&mut self, mut handler: F, prevent_default: bool) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { self.on_keyboard_release = Some(self.common.add_user_event( "keyup", move |event: KeyboardEvent| { - event.prevent_default(); + if prevent_default { + event.prevent_default(); + } + handler( event::scan_code(&event), event::virtual_key_code(&event), @@ -165,7 +170,7 @@ impl Canvas { )); } - pub fn on_keyboard_press(&mut self, mut handler: F) + pub fn on_keyboard_press(&mut self, mut handler: F, prevent_default: bool) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { @@ -173,16 +178,19 @@ impl Canvas { "keydown", move |event: KeyboardEvent| { // event.prevent_default() would suppress subsequent on_received_character() calls. That - // supression is correct for key sequences like Tab/Shift-Tab, Ctrl+R, PgUp/Down to + // suppression is correct for key sequences like Tab/Shift-Tab, Ctrl+R, PgUp/Down to // scroll, etc. We should not do it for key sequences that result in meaningful character // input though. - let event_key = &event.key(); - let is_key_string = event_key.len() == 1 || !event_key.is_ascii(); - let is_shortcut_modifiers = - (event.ctrl_key() || event.alt_key()) && !event.get_modifier_state("AltGr"); - if !is_key_string || is_shortcut_modifiers { - event.prevent_default(); + if prevent_default { + let event_key = &event.key(); + let is_key_string = event_key.len() == 1 || !event_key.is_ascii(); + let is_shortcut_modifiers = + (event.ctrl_key() || event.alt_key()) && !event.get_modifier_state("AltGr"); + if !is_key_string || is_shortcut_modifiers { + event.prevent_default(); + } } + handler( event::scan_code(&event), event::virtual_key_code(&event), @@ -192,7 +200,7 @@ impl Canvas { )); } - pub fn on_received_character(&mut self, mut handler: F) + pub fn on_received_character(&mut self, mut handler: F, prevent_default: bool) where F: 'static + FnMut(char), { @@ -204,8 +212,11 @@ impl Canvas { self.on_received_character = Some(self.common.add_user_event( "keypress", move |event: KeyboardEvent| { - // Supress further handling to stop keys like the space key from scrolling the page. - event.prevent_default(); + // Suppress further handling to stop keys like the space key from scrolling the page. + if prevent_default { + event.prevent_default(); + } + handler(event::codepoint(&event)); }, )); @@ -261,12 +272,15 @@ impl Canvas { } } - pub fn on_mouse_wheel(&mut self, mut handler: F) + pub fn on_mouse_wheel(&mut self, mut handler: F, prevent_default: bool) where F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), { self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { - event.prevent_default(); + if prevent_default { + event.prevent_default(); + } + if let Some(delta) = event::mouse_scroll_delta(&event) { handler(0, delta, event::mouse_modifiers(&event)); } @@ -339,9 +353,7 @@ impl Common { handler(event); }) as Box); - let listener = EventListenerHandle::new(&self.raw, event_name, closure); - - listener + EventListenerHandle::new(&self.raw, event_name, closure) } // The difference between add_event and add_user_event is that the latter has a special meaning diff --git a/src/platform_impl/web/web_sys/canvas/mouse_handler.rs b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs index 81e2aead02..1effb455c7 100644 --- a/src/platform_impl/web/web_sys/canvas/mouse_handler.rs +++ b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs @@ -8,6 +8,8 @@ use std::rc::Rc; use web_sys::{EventTarget, MouseEvent}; +type MouseLeaveHandler = Rc>>>; + #[allow(dead_code)] pub(super) struct MouseHandler { on_mouse_leave: Option>, @@ -15,7 +17,7 @@ pub(super) struct MouseHandler { on_mouse_move: Option>, on_mouse_press: Option>, on_mouse_release: Option>, - on_mouse_leave_handler: Rc>>>, + on_mouse_leave_handler: MouseLeaveHandler, mouse_capture_state: Rc>, } @@ -175,8 +177,8 @@ impl MouseHandler { .map_or(false, |target| target == EventTarget::from(canvas.clone())); match &*mouse_capture_state { // Don't handle hover events outside of canvas. - MouseCaptureState::NotCaptured if !is_over_canvas => return, - MouseCaptureState::OtherElement if !is_over_canvas => return, + MouseCaptureState::NotCaptured | MouseCaptureState::OtherElement + if !is_over_canvas => {} // If hovering over the canvas, just send the cursor move event. MouseCaptureState::NotCaptured | MouseCaptureState::OtherElement diff --git a/src/platform_impl/web/web_sys/media_query_handle.rs b/src/platform_impl/web/web_sys/media_query_handle.rs index c2ab40da77..dc9595617c 100644 --- a/src/platform_impl/web/web_sys/media_query_handle.rs +++ b/src/platform_impl/web/web_sys/media_query_handle.rs @@ -17,7 +17,7 @@ impl MediaQueryListHandle { .ok() .flatten() .and_then(|mql| { - mql.add_listener_with_opt_callback(Some(&listener.as_ref().unchecked_ref())) + mql.add_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())) .map(|_| mql) .ok() }); diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 8057f7756a..5e2d6c3820 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -98,7 +98,7 @@ pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: let style = raw.style(); style .set_property(property, value) - .expect(&format!("Failed to set {}", property)); + .unwrap_or_else(|err| panic!("error: {:?}\nFailed to set {}", err, property)) } pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs index 08b52ae306..7eb82c9fb5 100644 --- a/src/platform_impl/web/web_sys/scaling.rs +++ b/src/platform_impl/web/web_sys/scaling.rs @@ -65,13 +65,13 @@ impl ScaleChangeDetectorInternal { ); let mql = MediaQueryListHandle::new(&media_query, closure); if let Some(mql) = &mql { - assert_eq!(mql.mql().matches(), true); + assert!(mql.mql().matches()); } mql } fn handler(&mut self, event: MediaQueryListEvent) { - assert_eq!(event.matches(), false); + assert!(!event.matches()); let mql = self .mql .take() diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs index e95c54ed51..1978cbb38d 100644 --- a/src/platform_impl/web/web_sys/timeout.rs +++ b/src/platform_impl/web/web_sys/timeout.rs @@ -21,7 +21,7 @@ impl Timeout { let handle = window .set_timeout_with_callback_and_timeout_and_arguments_0( - &closure.as_ref().unchecked_ref(), + closure.as_ref().unchecked_ref(), duration.as_millis() as i32, ) .expect("Failed to set timeout"); @@ -64,7 +64,7 @@ impl AnimationFrameRequest { }) as Box); let handle = window - .request_animation_frame(&closure.as_ref().unchecked_ref()) + .request_animation_frame(closure.as_ref().unchecked_ref()) .expect("Failed to request animation frame"); AnimationFrameRequest { diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 886514cab3..492baefc16 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -4,10 +4,10 @@ use crate::event; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMH; use crate::window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWI, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWI, }; -use raw_window_handle::{RawWindowHandle, WebHandle}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget}; @@ -26,7 +26,7 @@ pub struct Window { } impl Window { - pub fn new( + pub(crate) fn new( target: &EventLoopWindowTarget, attr: WindowAttributes, platform_attr: PlatformSpecificWindowBuilderAttributes, @@ -35,12 +35,14 @@ impl Window { let id = target.generate_id(); + let prevent_default = platform_attr.prevent_default; + let canvas = backend::Canvas::create(platform_attr)?; - let mut canvas = Rc::new(RefCell::new(canvas)); + let canvas = Rc::new(RefCell::new(canvas)); let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - target.register(&mut canvas, id); + target.register(&canvas, id, prevent_default); let runner = target.runner.clone(); let resize_notify_fn = Box::new(move |new_size| { @@ -77,7 +79,7 @@ impl Window { Ok(window) } - pub fn canvas<'a>(&'a self) -> Ref<'a, backend::Canvas> { + pub fn canvas(&self) -> Ref<'_, backend::Canvas> { self.canvas.borrow() } @@ -216,11 +218,19 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + self.canvas .borrow() - .set_cursor_grab(grab) - .map_err(|e| ExternalError::Os(e)) + .set_cursor_lock(lock) + .map_err(ExternalError::Os) } #[inline] @@ -302,6 +312,11 @@ impl Window { // Currently a no-op as it does not seem there is good support for this on web } + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) { + // Currently not implemented + } + #[inline] pub fn focus_window(&self) { // Currently a no-op as it does not seem there is good support for this on web @@ -339,14 +354,19 @@ impl Window { #[inline] pub fn id(&self) -> WindowId { - return self.id; + self.id } #[inline] pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = WebHandle::empty(); - handle.id = self.id.0; - RawWindowHandle::Web(handle) + let mut window_handle = WebWindowHandle::empty(); + window_handle.id = self.id.0; + RawWindowHandle::Web(window_handle) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Web(WebDisplayHandle::empty()) } } @@ -367,7 +387,31 @@ impl WindowId { } } -#[derive(Default, Clone)] +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as u32) + } +} + +#[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub(crate) canvas: Option, + pub(crate) prevent_default: bool, + pub(crate) focusable: bool, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> Self { + Self { + canvas: None, + prevent_default: true, + focusable: true, + } + } } diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs index 8e1deac3ae..6487617f2c 100644 --- a/src/platform_impl/windows/dark_mode.rs +++ b/src/platform_impl/windows/dark_mode.rs @@ -2,6 +2,7 @@ /// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode use std::{ffi::c_void, ptr}; +use once_cell::sync::Lazy; use windows_sys::{ core::PCSTR, Win32::{ @@ -22,47 +23,45 @@ use crate::window::Theme; use super::util; -lazy_static! { - static ref WIN10_BUILD_VERSION: Option = { - type RtlGetVersion = unsafe extern "system" fn (*mut OSVERSIONINFOW) -> NTSTATUS; - let handle = get_function!("ntdll.dll", RtlGetVersion); - - if let Some(rtl_get_version) = handle { - unsafe { - let mut vi = OSVERSIONINFOW { - dwOSVersionInfoSize: 0, - dwMajorVersion: 0, - dwMinorVersion: 0, - dwBuildNumber: 0, - dwPlatformId: 0, - szCSDVersion: [0; 128], - }; - - let status = (rtl_get_version)(&mut vi); - - if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { - Some(vi.dwBuildNumber) - } else { - None - } - } - } else { - None - } - }; +static WIN10_BUILD_VERSION: Lazy> = Lazy::new(|| { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS; + let handle = get_function!("ntdll.dll", RtlGetVersion); + + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi); - static ref DARK_MODE_SUPPORTED: bool = { - // We won't try to do anything for windows versions < 17763 - // (Windows 10 October 2018 update) - match *WIN10_BUILD_VERSION { - Some(v) => v >= 17763, - None => false + if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } } - }; + } else { + None + } +}); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false, + } +}); - static ref DARK_THEME_NAME: Vec = util::encode_wide("DarkMode_Explorer"); - static ref LIGHT_THEME_NAME: Vec = util::encode_wide(""); -} +static DARK_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("DarkMode_Explorer")); +static LIGHT_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("")); /// Attempt to set a theme on a window, if necessary. /// Returns the theme that was picked @@ -113,10 +112,8 @@ fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { cbData: usize, } - lazy_static! { - static ref SET_WINDOW_COMPOSITION_ATTRIBUTE: Option = - get_function!("user32.dll", SetWindowCompositionAttribute); - } + static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute)); if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { unsafe { @@ -144,23 +141,19 @@ fn should_use_dark_mode() -> bool { fn should_apps_use_dark_mode() -> bool { type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; - lazy_static! { - static ref SHOULD_APPS_USE_DARK_MODE: Option = { - unsafe { - const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR; + 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()); + let module = LoadLibraryA("uxtheme.dll\0".as_ptr()); - if module == 0 { - return None; - } + if module == 0 { + return None; + } - let handle = GetProcAddress(module, UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL); + let handle = GetProcAddress(module, UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL); - handle.map(|handle| std::mem::transmute(handle)) - } - }; - } + handle.map(|handle| std::mem::transmute(handle)) + }); SHOULD_APPS_USE_DARK_MODE .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) diff --git a/src/platform_impl/windows/drop_handler.rs b/src/platform_impl/windows/drop_handler.rs index f273ad0036..2db12a67a8 100644 --- a/src/platform_impl/windows/drop_handler.rs +++ b/src/platform_impl/windows/drop_handler.rs @@ -80,7 +80,7 @@ impl FileDropHandler { let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { // Destroy the underlying data - Box::from_raw(drop_handler as *mut FileDropHandlerData); + drop(Box::from_raw(drop_handler as *mut FileDropHandlerData)); } count as u32 } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index f24e93ecc6..06f885b63b 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -2,7 +2,6 @@ mod runner; -use parking_lot::Mutex; use std::{ cell::Cell, collections::VecDeque, @@ -18,22 +17,24 @@ use std::{ time::{Duration, Instant}, }; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use raw_window_handle::{RawDisplayHandle, WindowsDisplayHandle}; + use windows_sys::Win32::{ Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE, Foundation::{BOOL, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_TIMEOUT, WPARAM}, Graphics::Gdi::{ - ClientToScreen, GetMonitorInfoW, GetUpdateRect, MonitorFromRect, MonitorFromWindow, - RedrawWindow, ScreenToClient, ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, - RDW_INTERNALPAINT, SC_SCREENSAVE, + GetMonitorInfoW, GetUpdateRect, MonitorFromRect, MonitorFromWindow, RedrawWindow, + ScreenToClient, ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, + SC_SCREENSAVE, }, Media::{timeBeginPeriod, timeEndPeriod, timeGetDevCaps, TIMECAPS, TIMERR_NOERROR}, - System::{ - LibraryLoader::GetModuleHandleW, Ole::RevokeDragDrop, Threading::GetCurrentThreadId, - WindowsProgramming::INFINITE, - }, + System::{Ole::RevokeDragDrop, Threading::GetCurrentThreadId, WindowsProgramming::INFINITE}, UI::{ Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}, Input::{ + Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}, KeyboardAndMouse::{ MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, VK_F4, @@ -49,48 +50,55 @@ use windows_sys::Win32::{ RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }, WindowsAndMessaging::{ - CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, - GetCursorPos, GetMessageW, GetWindowLongW, LoadCursorW, MsgWaitForMultipleObjectsEx, - PeekMessageW, PostMessageW, PostThreadMessageW, RegisterClassExW, - RegisterWindowMessageA, SetCursor, SetWindowPos, TranslateMessage, CREATESTRUCTW, - GIDC_ARRIVAL, GIDC_REMOVAL, GWL_EXSTYLE, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, - MAPVK_VK_TO_VSC, MINMAXINFO, MSG, MWMO_INPUTAVAILABLE, PM_NOREMOVE, PM_QS_PAINT, - PM_REMOVE, PT_PEN, PT_TOUCH, QS_ALLEVENTS, RI_KEY_E0, RI_KEY_E1, RI_MOUSE_WHEEL, - SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, - WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DESTROY, - WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, - WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetCursorPos, + GetMessageW, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW, + PostThreadMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, + TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, + HTCAPTION, HTCLIENT, MAPVK_VK_TO_VSC, MINMAXINFO, MSG, MWMO_INPUTAVAILABLE, + NCCALCSIZE_PARAMS, PM_NOREMOVE, PM_QS_PAINT, PM_REMOVE, PT_PEN, PT_TOUCH, QS_ALLEVENTS, + RI_KEY_E0, RI_KEY_E1, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, + SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, + WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, + WM_DROPFILES, 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_MOUSEHWHEEL, WM_MOUSEMOVE, - WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, - WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, - WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, - WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, - WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, - WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, + WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, + WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, + WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, + WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, + WS_VISIBLE, }, }, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent}, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + event::{DeviceEvent, Event, Force, Ime, KeyboardInput, Touch, TouchPhase, WindowEvent}, + event_loop::{ + ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, + }, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ dark_mode::try_theme, dpi::{become_dpi_aware, dpi_to_scale_factor}, drop_handler::FileDropHandler, event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey}, + ime::ImeContext, monitor::{self, MonitorHandle}, raw_input, util, window::InitData, - window_state::{CursorFlags, WindowFlags, WindowState}, + window_state::{CursorFlags, ImeState, WindowFlags, WindowState}, wrap_device_id, WindowId, DEVICE_ID, }, window::{Fullscreen, WindowId as RootWindowId}, }; use runner::{EventLoopRunner, EventLoopRunnerShared}; +use super::window::set_skip_taskbar; + type GetPointerFrameInfoHistory = unsafe extern "system" fn( pointerId: u32, entriesCount: *mut u32, @@ -111,18 +119,17 @@ type GetPointerTouchInfo = type GetPointerPenInfo = unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; -lazy_static! { - static ref GET_POINTER_FRAME_INFO_HISTORY: Option = - get_function!("user32.dll", GetPointerFrameInfoHistory); - static ref SKIP_POINTER_FRAME_MESSAGES: Option = - get_function!("user32.dll", SkipPointerFrameMessages); - static ref GET_POINTER_DEVICE_RECTS: Option = - get_function!("user32.dll", GetPointerDeviceRects); - static ref GET_POINTER_TOUCH_INFO: Option = - get_function!("user32.dll", GetPointerTouchInfo); - static ref GET_POINTER_PEN_INFO: Option = - get_function!("user32.dll", GetPointerPenInfo); -} +static GET_POINTER_FRAME_INFO_HISTORY: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory)); +static SKIP_POINTER_FRAME_MESSAGES: Lazy> = + Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages)); +static GET_POINTER_DEVICE_RECTS: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects)); +static GET_POINTER_TOUCH_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo)); +static GET_POINTER_PEN_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo)); + pub(crate) struct WindowData { pub window_state: Arc>, pub event_loop_runner: EventLoopRunnerShared, @@ -202,7 +209,10 @@ impl EventLoop { let thread_msg_sender = insert_event_target_window_data::(thread_msg_target, runner_shared.clone()); - raw_input::register_all_mice_and_keyboards_for_raw_input(thread_msg_target); + raw_input::register_all_mice_and_keyboards_for_raw_input( + thread_msg_target, + Default::default(), + ); EventLoop { thread_msg_sender, @@ -313,6 +323,14 @@ impl EventLoopWindowTarget { let monitor = monitor::primary_monitor(); Some(RootMonitorHandle { inner: monitor }) } + + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Windows(WindowsDisplayHandle::empty()) + } + + pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { + raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, filter); + } } /// Returns the id of the main thread. @@ -332,7 +350,7 @@ impl EventLoopWindowTarget { /// entrypoint. /// /// Full details of CRT initialization can be found here: -/// https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?view=msvc-160 +/// fn main_thread_id() -> u32 { static mut MAIN_THREAD_ID: u32 = 0; @@ -374,19 +392,17 @@ fn get_wait_thread_id() -> u32 { } } -lazy_static! { - static ref WAIT_PERIOD_MIN: Option = unsafe { - let mut caps = TIMECAPS { - wPeriodMin: 0, - wPeriodMax: 0, - }; - if timeGetDevCaps(&mut caps, mem::size_of::() as u32) == TIMERR_NOERROR { - Some(caps.wPeriodMin) - } else { - None - } +static WAIT_PERIOD_MIN: Lazy> = Lazy::new(|| unsafe { + let mut caps = TIMECAPS { + wPeriodMin: 0, + wPeriodMax: 0, }; -} + if timeGetDevCaps(&mut caps, mem::size_of::() as u32) == TIMERR_NOERROR { + Some(caps.wPeriodMin) + } else { + None + } +}); fn wait_thread(parent_thread_id: u32, msg_window_id: HWND) { unsafe { @@ -581,69 +597,52 @@ impl EventLoopProxy { type WaitUntilInstantBox = Box; -lazy_static! { - // Message sent by the `EventLoopProxy` when we want to wake up the thread. - // WPARAM and LPARAM are unused. - static ref USER_EVENT_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::WakeupMsg\0".as_ptr()) - } - }; - // Message sent when we want to execute a closure in the thread. - // WPARAM contains a Box> that must be retrieved with `Box::from_raw`, - // and LPARAM is unused. - static ref EXEC_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::ExecMsg\0".as_ptr()) - } - }; - static ref PROCESS_NEW_EVENTS_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::ProcessNewEvents\0".as_ptr()) - } - }; - /// lparam is the wait thread's message id. - static ref SEND_WAIT_THREAD_ID_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::SendWaitThreadId\0".as_ptr()) - } - }; - /// lparam points to a `Box` signifying the time `PROCESS_NEW_EVENTS_MSG_ID` should - /// be sent. - static ref WAIT_UNTIL_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::WaitUntil\0".as_ptr()) - } - }; - static ref CANCEL_WAIT_UNTIL_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::CancelWaitUntil\0".as_ptr()) - } - }; - // Message sent by a `Window` when it wants to be destroyed by the main thread. - // WPARAM and LPARAM are unused. - pub static ref DESTROY_MSG_ID: u32 = { - unsafe { - RegisterWindowMessageA("Winit::DestroyMsg\0".as_ptr()) - } - }; - // WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the - // documentation in the `window_state` module for more information. - pub static ref SET_RETAIN_STATE_ON_SIZE_MSG_ID: u32 = unsafe { - RegisterWindowMessageA("Winit::SetRetainMaximized\0".as_ptr()) - }; - static ref THREAD_EVENT_TARGET_WINDOW_CLASS: Vec = util::encode_wide("Winit Thread Event Target"); -} +// Message sent by the `EventLoopProxy` when we want to wake up the thread. +// WPARAM and LPARAM are unused. +static USER_EVENT_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::WakeupMsg\0".as_ptr()) }); +// Message sent when we want to execute a closure in the thread. +// WPARAM contains a Box> that must be retrieved with `Box::from_raw`, +// and LPARAM is unused. +static EXEC_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::ExecMsg\0".as_ptr()) }); +static PROCESS_NEW_EVENTS_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::ProcessNewEvents\0".as_ptr()) }); +/// lparam is the wait thread's message id. +static SEND_WAIT_THREAD_ID_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::SendWaitThreadId\0".as_ptr()) }); +/// lparam points to a `Box` signifying the time `PROCESS_NEW_EVENTS_MSG_ID` should +/// be sent. +static WAIT_UNTIL_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::WaitUntil\0".as_ptr()) }); +static CANCEL_WAIT_UNTIL_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::CancelWaitUntil\0".as_ptr()) }); +// Message sent by a `Window` when it wants to be destroyed by the main thread. +// WPARAM and LPARAM are unused. +pub static DESTROY_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::DestroyMsg\0".as_ptr()) }); +// WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the +// documentation in the `window_state` module for more information. +pub static SET_RETAIN_STATE_ON_SIZE_MSG_ID: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("Winit::SetRetainMaximized\0".as_ptr()) }); +static THREAD_EVENT_TARGET_WINDOW_CLASS: Lazy> = + Lazy::new(|| util::encode_wide("Winit Thread Event Target")); +/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then broadcasts this message to all top-level windows +/// +pub static TASKBAR_CREATED: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA("TaskbarCreated\0".as_ptr()) }); fn create_event_target_window() -> HWND { + use windows_sys::Win32::UI::WindowsAndMessaging::CS_HREDRAW; + use windows_sys::Win32::UI::WindowsAndMessaging::CS_VREDRAW; unsafe { let class = WNDCLASSEXW { cbSize: mem::size_of::() as u32, - style: 0, + style: CS_HREDRAW | CS_VREDRAW, lpfnWndProc: Some(thread_event_target_callback::), cbClsExtra: 0, cbWndExtra: 0, - hInstance: GetModuleHandleW(ptr::null()), + hInstance: util::get_instance_handle(), hIcon: 0, hCursor: 0, // must be null in order for cursor state to work properly hbrBackground: 0, @@ -677,7 +676,7 @@ fn create_event_target_window() -> HWND { 0, 0, 0, - GetModuleHandleW(ptr::null()), + util::get_instance_handle(), ptr::null(), ); @@ -819,6 +818,74 @@ fn update_modifiers(window: HWND, userdata: &WindowData) { } } +unsafe fn gain_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::{ElementState::Released, WindowEvent::Focused}; + for windows_keycode in event::get_pressed_keys() { + let scancode = MapVirtualKeyA(windows_keycode as u32, MAPVK_VK_TO_VSC); + let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); + + update_modifiers(window, userdata); + + #[allow(deprecated)] + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + scancode, + virtual_keycode, + state: Released, + modifiers: event::get_key_mods(), + }, + is_synthetic: true, + }, + }) + } + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(true), + }); +} + +unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::{ + ElementState::Released, + ModifiersState, + WindowEvent::{Focused, ModifiersChanged}, + }; + for windows_keycode in event::get_pressed_keys() { + let scancode = MapVirtualKeyA(windows_keycode as u32, MAPVK_VK_TO_VSC); + let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); + + #[allow(deprecated)] + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + scancode, + virtual_keycode, + state: Released, + modifiers: event::get_key_mods(), + }, + is_synthetic: true, + }, + }) + } + + userdata.window_state.lock().modifiers_state = ModifiersState::empty(); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(ModifiersState::empty()), + }); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(false), + }); +} + /// Any window whose callback is configured to this function will have its events propagated /// through the events loop of the thread the window was created in. // @@ -879,7 +946,7 @@ pub(super) unsafe extern "system" fn public_window_callback( }; if userdata_removed && recurse_depth == 0 { - Box::from_raw(userdata_ptr); + drop(Box::from_raw(userdata_ptr)); } result @@ -903,6 +970,32 @@ unsafe fn public_window_callback_inner( // the closure to catch_unwind directly so that the match body indendation wouldn't change and // the git blame and history would be preserved. let callback = || match msg { + WM_NCCALCSIZE => { + let window_flags = userdata.window_state.lock().window_flags; + if wparam == 0 || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + return DefWindowProcW(window, msg, wparam, lparam); + } + + // Extend the client area to cover the whole non-client area. + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks + // + // HACK(msiglreith): To add the drop shadow we slightly tweak the non-client area. + // This leads to a small black 1px border on the top. Adding a margin manually + // on all 4 borders would result in the caption getting drawn by the DWM. + // + // Another option would be to allow the DWM to paint inside the client area. + // Unfortunately this results in janky resize behavior, where the compositor is + // ahead of the window surface. Currently, there seems no option to achieve this + // with the Windows API. + if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + let params = &mut *(lparam as *mut NCCALCSIZE_PARAMS); + params.rgrc[0].top += 1; + params.rgrc[0].bottom += 1; + } + + 0 + } + WM_ENTERSIZEMOVE => { userdata .window_state @@ -980,35 +1073,68 @@ unsafe fn public_window_callback_inner( right: window_pos.x + window_pos.cx, bottom: window_pos.y + window_pos.cy, }; - let new_monitor = MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL); - match fullscreen { - Fullscreen::Borderless(ref mut fullscreen_monitor) => { - if new_monitor != 0 - && fullscreen_monitor - .as_ref() - .map(|monitor| new_monitor != monitor.inner.hmonitor()) - .unwrap_or(true) - { - if let Ok(new_monitor_info) = monitor::get_monitor_info(new_monitor) { - let new_monitor_rect = new_monitor_info.monitorInfo.rcMonitor; - window_pos.x = new_monitor_rect.left; - window_pos.y = new_monitor_rect.top; - window_pos.cx = new_monitor_rect.right - new_monitor_rect.left; - window_pos.cy = new_monitor_rect.bottom - new_monitor_rect.top; + + const NOMOVE_OR_NOSIZE: u32 = SWP_NOMOVE | SWP_NOSIZE; + + let new_rect = if window_pos.flags & NOMOVE_OR_NOSIZE != 0 { + let cur_rect = util::WindowArea::Outer.get_rect(window) + .expect("Unexpected GetWindowRect failure; please report this error to https://github.com/rust-windowing/winit"); + + match window_pos.flags & NOMOVE_OR_NOSIZE { + NOMOVE_OR_NOSIZE => None, + + SWP_NOMOVE => Some(RECT { + left: cur_rect.left, + top: cur_rect.top, + right: cur_rect.left + window_pos.cx, + bottom: cur_rect.top + window_pos.cy, + }), + + SWP_NOSIZE => Some(RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x - cur_rect.left + cur_rect.right, + bottom: window_pos.y - cur_rect.top + cur_rect.bottom, + }), + + _ => unreachable!(), + } + } else { + Some(new_rect) + }; + + if let Some(new_rect) = new_rect { + let new_monitor = MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL); + match fullscreen { + Fullscreen::Borderless(ref mut fullscreen_monitor) => { + if new_monitor != 0 + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.inner.hmonitor()) + .unwrap_or(true) + { + if let Ok(new_monitor_info) = monitor::get_monitor_info(new_monitor) + { + let new_monitor_rect = new_monitor_info.monitorInfo.rcMonitor; + window_pos.x = new_monitor_rect.left; + window_pos.y = new_monitor_rect.top; + window_pos.cx = new_monitor_rect.right - new_monitor_rect.left; + window_pos.cy = new_monitor_rect.bottom - new_monitor_rect.top; + } + *fullscreen_monitor = Some(crate::monitor::MonitorHandle { + inner: MonitorHandle::new(new_monitor), + }); } - *fullscreen_monitor = Some(crate::monitor::MonitorHandle { - inner: MonitorHandle::new(new_monitor), - }); } - } - Fullscreen::Exclusive(ref video_mode) => { - let old_monitor = video_mode.video_mode.monitor.hmonitor(); - if let Ok(old_monitor_info) = monitor::get_monitor_info(old_monitor) { - let old_monitor_rect = old_monitor_info.monitorInfo.rcMonitor; - window_pos.x = old_monitor_rect.left; - window_pos.y = old_monitor_rect.top; - window_pos.cx = old_monitor_rect.right - old_monitor_rect.left; - window_pos.cy = old_monitor_rect.bottom - old_monitor_rect.top; + Fullscreen::Exclusive(ref video_mode) => { + let old_monitor = video_mode.video_mode.monitor.hmonitor(); + if let Ok(old_monitor_info) = monitor::get_monitor_info(old_monitor) { + let old_monitor_rect = old_monitor_info.monitorInfo.rcMonitor; + window_pos.x = old_monitor_rect.left; + window_pos.y = old_monitor_rect.top; + window_pos.cx = old_monitor_rect.right - old_monitor_rect.left; + window_pos.cy = old_monitor_rect.bottom - old_monitor_rect.top; + } } } } @@ -1095,6 +1221,104 @@ unsafe fn public_window_callback_inner( 0 } + WM_IME_STARTCOMPOSITION => { + let ime_allowed = userdata.window_state.lock().ime_allowed; + if ime_allowed { + userdata.window_state.lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + + DefWindowProcW(window, msg, wparam, lparam) + } + + WM_IME_COMPOSITION => { + let ime_allowed_and_composing = { + let w = userdata.window_state.lock(); + w.ime_allowed && w.ime_state != ImeState::Disabled + }; + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // check whether composing. + if ime_allowed_and_composing { + let ime_context = ImeContext::current(window); + + if lparam == 0 { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + + // Google Japanese Input and ATOK have both flags, so + // first, receive composing result if exist. + if (lparam as u32 & GCS_RESULTSTR) != 0 { + if let Some(text) = ime_context.get_composed_text() { + userdata.window_state.lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + // Next, receive preedit range for next composing if exist. + if (lparam as u32 & GCS_COMPSTR) != 0 { + if let Some((text, first, last)) = ime_context.get_composing_text_and_cursor() { + userdata.window_state.lock().ime_state = ImeState::Preedit; + let cursor_range = first.map(|f| (f, last.unwrap_or(f))); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)), + }); + } + } + } + + // Not calling DefWindowProc to hide composing text drawn by IME. + 0 + } + + WM_IME_ENDCOMPOSITION => { + let ime_allowed_or_composing = { + let w = userdata.window_state.lock(); + w.ime_allowed || w.ime_state != ImeState::Disabled + }; + if ime_allowed_or_composing { + if userdata.window_state.lock().ime_state == ImeState::Preedit { + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // trying receiving composing result and commit if exists. + let ime_context = ImeContext::current(window); + if let Some(text) = ime_context.get_composed_text() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + userdata.window_state.lock().ime_state = ImeState::Disabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Disabled), + }); + } + + DefWindowProcW(window, msg, wparam, lparam) + } + + WM_IME_SETCONTEXT => { + // Hide composing text drawn by IME. + let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize); + + DefWindowProcW(window, msg, wparam, lparam) + } + // this is necessary for us to maintain minimize/restore state WM_SYSCOMMAND => { if wparam == SC_RESTORE as usize { @@ -1663,74 +1887,32 @@ unsafe fn public_window_callback_inner( 0 } - WM_SETFOCUS => { - use crate::event::{ElementState::Released, WindowEvent::Focused}; - for windows_keycode in event::get_pressed_keys() { - let scancode = MapVirtualKeyA(windows_keycode as u32, MAPVK_VK_TO_VSC); - let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); - - update_modifiers(window, userdata); - - #[allow(deprecated)] - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - scancode, - virtual_keycode, - state: Released, - modifiers: event::get_key_mods(), - }, - is_synthetic: true, - }, - }) + WM_NCACTIVATE => { + let is_active = wparam == 1; + let active_focus_changed = userdata.window_state.lock().set_active(is_active); + if active_focus_changed { + if is_active { + gain_active_focus(window, userdata); + } else { + lose_active_focus(window, userdata); + } } + DefWindowProcW(window, msg, wparam, lparam) + } - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: Focused(true), - }); - + WM_SETFOCUS => { + let active_focus_changed = userdata.window_state.lock().set_focused(true); + if active_focus_changed { + gain_active_focus(window, userdata); + } 0 } WM_KILLFOCUS => { - use crate::event::{ - ElementState::Released, - ModifiersState, - WindowEvent::{Focused, ModifiersChanged}, - }; - for windows_keycode in event::get_pressed_keys() { - let scancode = MapVirtualKeyA(windows_keycode as u32, MAPVK_VK_TO_VSC); - let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); - - #[allow(deprecated)] - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - scancode, - virtual_keycode, - state: Released, - modifiers: event::get_key_mods(), - }, - is_synthetic: true, - }, - }) + let active_focus_changed = userdata.window_state.lock().set_focused(false); + if active_focus_changed { + lose_active_focus(window, userdata); } - - userdata.window_state.lock().modifiers_state = ModifiersState::empty(); - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: ModifiersChanged(ModifiersState::empty()), - }); - - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: Focused(false), - }); 0 } @@ -1767,11 +1949,13 @@ unsafe fn public_window_callback_inner( let mmi = lparam as *mut MINMAXINFO; let window_state = userdata.window_state.lock(); + let window_flags = window_state.window_flags; if window_state.min_size.is_some() || window_state.max_size.is_some() { if let Some(min_size) = window_state.min_size { let min_size = min_size.to_physical(window_state.scale_factor); - let (width, height): (u32, u32) = util::adjust_size(window, min_size).into(); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, min_size).into(); (*mmi).ptMinTrackSize = POINT { x: width as i32, y: height as i32, @@ -1779,7 +1963,8 @@ unsafe fn public_window_callback_inner( } if let Some(max_size) = window_state.max_size { let max_size = max_size.to_physical(window_state.scale_factor); - let (width, height): (u32, u32) = util::adjust_size(window, max_size).into(); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, max_size).into(); (*mmi).ptMaxTrackSize = POINT { x: width as i32, y: height as i32, @@ -1803,7 +1988,7 @@ unsafe fn public_window_callback_inner( let new_scale_factor = dpi_to_scale_factor(new_dpi_x); let old_scale_factor: f64; - let allow_resize = { + let (allow_resize, window_flags) = { let mut window_state = userdata.window_state.lock(); old_scale_factor = window_state.scale_factor; window_state.scale_factor = new_scale_factor; @@ -1812,12 +1997,11 @@ unsafe fn public_window_callback_inner( return 0; } - window_state.fullscreen.is_none() - && !window_state.window_flags().contains(WindowFlags::MAXIMIZED) - }; + let allow_resize = window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED); - let style = GetWindowLongW(window, GWL_STYLE) as u32; - let style_ex = GetWindowLongW(window, GWL_EXSTYLE) as u32; + (allow_resize, window_state.window_flags) + }; // New size as suggested by Windows. let suggested_rect = *(lparam as *const RECT); @@ -1831,28 +2015,18 @@ unsafe fn public_window_callback_inner( // let margin_right: i32; // let margin_bottom: i32; { - let adjusted_rect = - util::adjust_window_rect_with_styles(window, style, style_ex, suggested_rect) - .unwrap_or(suggested_rect); + let adjusted_rect = window_flags + .adjust_rect(window, suggested_rect) + .unwrap_or(suggested_rect); margin_left = suggested_rect.left - adjusted_rect.left; margin_top = suggested_rect.top - adjusted_rect.top; // margin_right = adjusted_rect.right - suggested_rect.right; // margin_bottom = adjusted_rect.bottom - suggested_rect.bottom; } - let old_physical_inner_rect = { - let mut old_physical_inner_rect = mem::zeroed(); - GetClientRect(window, &mut old_physical_inner_rect); - let mut origin = mem::zeroed(); - ClientToScreen(window, &mut origin); - - old_physical_inner_rect.left += origin.x; - old_physical_inner_rect.right += origin.x; - old_physical_inner_rect.top += origin.y; - old_physical_inner_rect.bottom += origin.y; - - old_physical_inner_rect - }; + let old_physical_inner_rect = util::WindowArea::Inner + .get_rect(window) + .expect("failed to query (old) inner window area"); let old_physical_inner_size = PhysicalSize::new( (old_physical_inner_rect.right - old_physical_inner_rect.left) as u32, (old_physical_inner_rect.bottom - old_physical_inner_rect.top) as u32, @@ -1869,7 +2043,7 @@ unsafe fn public_window_callback_inner( false => old_physical_inner_size, }; - let _ = userdata.send_event(Event::WindowEvent { + userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: ScaleFactorChanged { scale_factor: new_scale_factor, @@ -1906,13 +2080,9 @@ unsafe fn public_window_callback_inner( bottom: suggested_ul.1 + new_physical_inner_size.height as i32, }; - conservative_rect = util::adjust_window_rect_with_styles( - window, - style, - style_ex, - conservative_rect, - ) - .unwrap_or(conservative_rect); + conservative_rect = window_flags + .adjust_rect(window, conservative_rect) + .unwrap_or(conservative_rect); // If we're dragging the window, offset the window so that the cursor's // relative horizontal position in the title bar is preserved. @@ -2021,7 +2191,7 @@ unsafe fn public_window_callback_inner( if window_state.current_theme != new_theme { window_state.current_theme = new_theme; - mem::drop(window_state); + drop(window_state); userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: ThemeChanged(new_theme), @@ -2042,6 +2212,10 @@ unsafe fn public_window_callback_inner( f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) }); 0 + } else if msg == *TASKBAR_CREATED { + let window_state = userdata.window_state.lock(); + set_skip_taskbar(window, window_state.skip_taskbar); + DefWindowProcW(window, msg, wparam, lparam) } else { DefWindowProcW(window, msg, wparam, lparam) } @@ -2165,7 +2339,8 @@ unsafe extern "system" fn thread_event_target_callback( let mouse_button_flags = mouse.Anonymous.Anonymous.usButtonFlags; if util::has_flag(mouse_button_flags as u32, RI_MOUSE_WHEEL) { - let delta = mouse_button_flags as i16 as f32 / WHEEL_DELTA as f32; + let delta = mouse.Anonymous.Anonymous.usButtonData as i16 as f32 + / WHEEL_DELTA as f32; userdata.send_event(Event::DeviceEvent { device_id, event: MouseWheel { @@ -2279,7 +2454,7 @@ unsafe extern "system" fn thread_event_target_callback( .catch_unwind(callback) .unwrap_or(-1); if userdata_removed { - mem::drop(userdata); + drop(userdata); } else { Box::into_raw(userdata); } diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs index fab5c61360..945d306fca 100644 --- a/src/platform_impl/windows/event_loop/runner.rs +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -16,7 +16,10 @@ use crate::{ dpi::PhysicalSize, event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, - platform_impl::platform::util, + platform_impl::platform::{ + event_loop::{WindowData, GWL_USERDATA}, + get_window_long, + }, window::WindowId, }; @@ -393,6 +396,11 @@ impl EventLoopRunner { } }; self.call_event_handler(Event::NewEvents(start_cause)); + // NB: For consistency all platforms must emit a 'resumed' event even though Windows + // applications don't themselves have a formal suspend/resume lifecycle. + if init { + self.call_event_handler(Event::Resumed); + } self.dispatch_buffered_events(); RedrawWindow(self.thread_msg_target, ptr::null(), 0, RDW_INTERNALPAINT); } @@ -429,11 +437,13 @@ impl BufferedEvent { new_inner_size: &mut new_inner_size, }, }); - util::set_inner_size_physical( - (window_id.0).0, - new_inner_size.width as _, - new_inner_size.height as _, - ); + + let window_flags = unsafe { + let userdata = + get_window_long(window_id.0.into(), GWL_USERDATA) as *mut WindowData; + (*userdata).window_state.lock().window_flags + }; + window_flags.set_size((window_id.0).0, new_inner_size); } } } diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 8ef47d6a4a..a6277da1e2 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -1,10 +1,9 @@ -use std::{fmt, io, mem, path::Path, ptr, sync::Arc}; +use std::{fmt, io, mem, path::Path, sync::Arc}; use windows_sys::{ core::PCWSTR, Win32::{ Foundation::HWND, - System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::{ CreateIcon, DestroyIcon, LoadImageW, SendMessageW, HICON, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON, @@ -111,7 +110,7 @@ impl WinIcon { let (width, height) = size.map(Into::into).unwrap_or((0, 0)); let handle = unsafe { LoadImageW( - GetModuleHandleW(ptr::null()), + util::get_instance_handle(), resource_id as PCWSTR, IMAGE_ICON, width as i32, diff --git a/src/platform_impl/windows/ime.rs b/src/platform_impl/windows/ime.rs new file mode 100644 index 0000000000..6e6d31ea5e --- /dev/null +++ b/src/platform_impl/windows/ime.rs @@ -0,0 +1,149 @@ +use std::{ + ffi::{c_void, OsString}, + mem::zeroed, + os::windows::prelude::OsStringExt, + ptr::null_mut, +}; + +use windows_sys::Win32::{ + Foundation::POINT, + Globalization::HIMC, + UI::{ + Input::Ime::{ + ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext, + ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, + CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, + IACE_DEFAULT, + }, + WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}, + }, +}; + +use crate::{dpi::Position, platform::windows::HWND}; + +pub struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + pub unsafe fn current(hwnd: HWND) -> Self { + let himc = ImmGetContext(hwnd); + ImeContext { hwnd, himc } + } + + pub unsafe fn get_composing_text_and_cursor( + &self, + ) -> Option<(String, Option, Option)> { + let text = self.get_composition_string(GCS_COMPSTR)?; + let attrs = self.get_composition_data(GCS_COMPATTR).unwrap_or_default(); + + let mut first = None; + let mut last = None; + let mut boundary_before_char = 0; + + for (attr, chr) in attrs.into_iter().zip(text.chars()) { + let char_is_targetted = + attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED; + + if first.is_none() && char_is_targetted { + first = Some(boundary_before_char); + } else if first.is_some() && last.is_none() && !char_is_targetted { + last = Some(boundary_before_char); + } + + boundary_before_char += chr.len_utf8(); + } + + if first.is_some() && last.is_none() { + last = Some(text.len()); + } else if first.is_none() { + // IME haven't split words and select any clause yet, so trying to retrieve normal cursor. + let cursor = self.get_composition_cursor(&text); + first = cursor; + last = cursor; + } + + Some((text, first, last)) + } + + pub unsafe fn get_composed_text(&self) -> Option { + self.get_composition_string(GCS_RESULTSTR) + } + + unsafe fn get_composition_cursor(&self, text: &str) -> Option { + let cursor = ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0); + (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum()) + } + + unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option { + let data = self.get_composition_data(gcs_mode)?; + let (prefix, shorts, suffix) = data.align_to::(); + if prefix.is_empty() && suffix.is_empty() { + OsString::from_wide(shorts).into_string().ok() + } else { + None + } + } + + unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { + let size = match ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) { + 0 => return Some(Vec::new()), + size if size < 0 => return None, + size => size, + }; + + let mut buf = Vec::::with_capacity(size as _); + let size = ImmGetCompositionStringW( + self.himc, + gcs_mode, + buf.as_mut_ptr() as *mut c_void, + size as _, + ); + + if size < 0 { + None + } else { + buf.set_len(size as _); + Some(buf) + } + } + + pub unsafe fn set_ime_position(&self, spot: Position, scale_factor: f64) { + if !ImeContext::system_has_ime() { + return; + } + + let (x, y) = spot.to_physical::(scale_factor).into(); + let candidate_form = CANDIDATEFORM { + dwIndex: 0, + dwStyle: CFS_EXCLUDE, + ptCurrentPos: POINT { x, y }, + rcArea: zeroed(), + }; + + ImmSetCandidateWindow(self.himc, &candidate_form); + } + + pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { + if !ImeContext::system_has_ime() { + return; + } + + if allowed { + ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT); + } else { + ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN); + } + } + + unsafe fn system_has_ime() -> bool { + GetSystemMetrics(SM_IMMENABLED) != 0 + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { ImmReleaseContext(self.hwnd, self.himc) }; + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 56c83ce7a1..3a3854228f 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -36,6 +36,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub drag_and_drop: bool, pub preferred_theme: Option, pub skip_taskbar: bool, + pub decoration_shadow: bool, } impl Default for PlatformSpecificWindowBuilderAttributes { @@ -48,6 +49,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { drag_and_drop: true, preferred_theme: None, skip_taskbar: false, + decoration_shadow: false, } } } @@ -100,19 +102,37 @@ impl WindowId { } } +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for HWND { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as HWND) + } +} + #[inline(always)] const fn get_xbutton_wparam(x: u32) -> u16 { loword(x) } #[inline(always)] -const fn get_x_lparam(x: u32) -> u16 { - loword(x) +const fn get_x_lparam(x: u32) -> i16 { + loword(x) as _ } #[inline(always)] -const fn get_y_lparam(x: u32) -> u16 { - hiword(x) +const fn get_y_lparam(x: u32) -> i16 { + hiword(x) as _ } #[inline(always)] @@ -154,6 +174,7 @@ mod drop_handler; mod event; mod event_loop; mod icon; +mod ime; mod monitor; mod raw_input; mod window; diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index 4a92eea769..469059560c 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -1,10 +1,7 @@ use std::{ collections::{BTreeSet, VecDeque}, - ffi::OsString, hash::Hash, - io, mem, - os::windows::prelude::OsStringExt, - ptr, + io, mem, ptr, }; use windows_sys::Win32::{ @@ -12,11 +9,12 @@ use windows_sys::Win32::{ Graphics::Gdi::{ EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, MonitorFromPoint, MonitorFromWindow, DEVMODEW, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, - DM_PELSWIDTH, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, - MONITOR_DEFAULTTOPRIMARY, + DM_PELSWIDTH, ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, + MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTOPRIMARY, }, }; +use super::util::decode_wide; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, @@ -31,7 +29,7 @@ use crate::{ pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) refresh_rate_millihertz: u32, pub(crate) monitor: MonitorHandle, // DEVMODEW is huge so we box it to avoid blowing up the size of winit::window::Fullscreen pub(crate) native_video_mode: Box, @@ -41,7 +39,7 @@ impl PartialEq for VideoMode { fn eq(&self, other: &Self) -> bool { self.size == other.size && self.bit_depth == other.bit_depth - && self.refresh_rate == other.refresh_rate + && self.refresh_rate_millihertz == other.refresh_rate_millihertz && self.monitor == other.monitor } } @@ -52,7 +50,7 @@ impl std::hash::Hash for VideoMode { fn hash(&self, state: &mut H) { self.size.hash(state); self.bit_depth.hash(state); - self.refresh_rate.hash(state); + self.refresh_rate_millihertz.hash(state); self.monitor.hash(state); } } @@ -62,7 +60,7 @@ impl std::fmt::Debug for VideoMode { f.debug_struct("VideoMode") .field("size", &self.size) .field("bit_depth", &self.bit_depth) - .field("refresh_rate", &self.refresh_rate) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) .field("monitor", &self.monitor) .finish() } @@ -77,8 +75,8 @@ impl VideoMode { self.bit_depth } - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz } pub fn monitor(&self) -> RootMonitorHandle { @@ -169,7 +167,7 @@ impl MonitorHandle { pub fn name(&self) -> Option { let monitor_info = get_monitor_info(self.0).unwrap(); Some( - OsString::from_wide(&monitor_info.szDevice) + decode_wide(&monitor_info.szDevice) .to_string_lossy() .to_string(), ) @@ -194,6 +192,23 @@ impl MonitorHandle { } } + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let monitor_info = get_monitor_info(self.0).unwrap(); + let device_name = monitor_info.szDevice.as_ptr(); + unsafe { + let mut mode: DEVMODEW = mem::zeroed(); + mode.dmSize = mem::size_of_val(&mode) as u16; + if EnumDisplaySettingsExW(device_name, ENUM_CURRENT_SETTINGS, &mut mode, 0) + == false.into() + { + None + } else { + Some(mode.dmDisplayFrequency * 1000) + } + } + } + #[inline] pub fn position(&self) -> PhysicalPosition { let rc_monitor = get_monitor_info(self.0).unwrap().monitorInfo.rcMonitor; @@ -235,7 +250,7 @@ impl MonitorHandle { video_mode: VideoMode { size: (mode.dmPelsWidth, mode.dmPelsHeight), bit_depth: mode.dmBitsPerPel as u16, - refresh_rate: mode.dmDisplayFrequency as u16, + refresh_rate_millihertz: mode.dmDisplayFrequency as u32 * 1000, monitor: self.clone(), native_video_mode: Box::new(mode), }, diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 46a6683044..c8d9a8b2a9 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -1,7 +1,5 @@ use std::{ - ffi::OsString, mem::{self, size_of}, - os::windows::prelude::OsStringExt, ptr, }; @@ -14,9 +12,9 @@ use windows_sys::Win32::{ Input::{ GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, - RAWINPUTHEADER, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDI_DEVICEINFO, RIDI_DEVICENAME, - RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, - RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, + RAWINPUTHEADER, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, RIDI_DEVICEINFO, + RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, + RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }, WindowsAndMessaging::{ RI_MOUSE_LEFT_BUTTON_DOWN, RI_MOUSE_LEFT_BUTTON_UP, RI_MOUSE_MIDDLE_BUTTON_DOWN, @@ -25,7 +23,7 @@ use windows_sys::Win32::{ }, }; -use crate::{event::ElementState, platform_impl::platform::util}; +use crate::{event::ElementState, event_loop::DeviceEventFilter, platform_impl::platform::util}; #[allow(dead_code)] pub fn get_raw_input_device_list() -> Option> { @@ -129,7 +127,7 @@ pub fn get_raw_input_device_name(handle: HANDLE) -> Option { unsafe { name.set_len(minimum_size as _) }; - OsString::from_wide(&name).into_string().ok() + util::decode_wide(&name).into_string().ok() } pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { @@ -140,10 +138,21 @@ pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { } } -pub fn register_all_mice_and_keyboards_for_raw_input(window_handle: HWND) -> bool { +pub fn register_all_mice_and_keyboards_for_raw_input( + mut window_handle: HWND, + filter: DeviceEventFilter, +) -> bool { // RIDEV_DEVNOTIFY: receive hotplug events // RIDEV_INPUTSINK: receive events even if we're not in the foreground - let flags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; + // RIDEV_REMOVE: don't receive device events (requires NULL hwndTarget) + let flags = match filter { + DeviceEventFilter::Always => { + window_handle = 0; + RIDEV_REMOVE + } + DeviceEventFilter::Unfocused => RIDEV_DEVNOTIFY, + DeviceEventFilter::Never => RIDEV_DEVNOTIFY | RIDEV_INPUTSINK, + }; let devices: [RAWINPUTDEVICE; 2] = [ RAWINPUTDEVICE { diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index fb84243acc..5f09bee64e 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,42 +1,51 @@ use std::{ - ffi::{c_void, OsStr}, + ffi::{c_void, OsStr, OsString}, io, iter::once, mem, ops::BitAnd, - os::windows::prelude::OsStrExt, + os::windows::prelude::{OsStrExt, OsStringExt}, ptr, sync::atomic::{AtomicBool, Ordering}, }; +use once_cell::sync::Lazy; use windows_sys::{ core::{HRESULT, PCWSTR}, Win32::{ - Foundation::{BOOL, HWND, RECT}, - Graphics::Gdi::{ClientToScreen, InvalidateRgn, HMONITOR}, - System::LibraryLoader::{GetProcAddress, LoadLibraryA}, + Foundation::{BOOL, HINSTANCE, HWND, RECT}, + Graphics::Gdi::{ClientToScreen, HMONITOR}, + System::{ + LibraryLoader::{GetProcAddress, LoadLibraryA}, + SystemServices::IMAGE_DOS_HEADER, + }, UI::{ HiDpi::{DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS}, Input::KeyboardAndMouse::GetActiveWindow, WindowsAndMessaging::{ - AdjustWindowRectEx, ClipCursor, GetClientRect, GetClipCursor, GetMenu, - GetSystemMetrics, GetWindowLongW, GetWindowRect, SetWindowPos, ShowCursor, - GWL_EXSTYLE, GWL_STYLE, 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, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOREPOSITION, SWP_NOZORDER, WINDOW_EX_STYLE, WINDOW_STYLE, + ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowRect, + 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, }, }, }, }; -use crate::{dpi::PhysicalSize, window::CursorIcon}; +use crate::window::CursorIcon; pub fn encode_wide(string: impl AsRef) -> Vec { string.as_ref().encode_wide().chain(once(0)).collect() } +pub fn decode_wide(mut wide_c_string: &[u16]) -> OsString { + if let Some(null_pos) = wide_c_string.iter().position(|c| *c == 0) { + wide_c_string = &wide_c_string[..null_pos]; + } + + OsString::from_wide(wide_c_string) +} + pub fn has_flag(bitset: T, flag: T) -> bool where T: Copy + PartialEq + BitAnd, @@ -44,111 +53,40 @@ where bitset & flag == flag } -pub unsafe fn status_map BOOL>(mut fun: F) -> Option { - let mut data: T = mem::zeroed(); - if fun(&mut data) != false.into() { - Some(data) - } else { - None - } -} - -fn win_to_err BOOL>(f: F) -> Result<(), io::Error> { - if f() != false.into() { +pub(crate) fn win_to_err(result: BOOL) -> Result<(), io::Error> { + if result != false.into() { Ok(()) } else { Err(io::Error::last_os_error()) } } -pub fn get_window_rect(hwnd: HWND) -> Option { - unsafe { status_map(|rect| GetWindowRect(hwnd, rect)) } +pub enum WindowArea { + Outer, + Inner, } -pub fn get_client_rect(hwnd: HWND) -> Result { - unsafe { - let mut rect = mem::zeroed(); - let mut top_left = mem::zeroed(); - - win_to_err(|| ClientToScreen(hwnd, &mut top_left))?; - win_to_err(|| GetClientRect(hwnd, &mut rect))?; - rect.left += top_left.x; - rect.top += top_left.y; - rect.right += top_left.x; - rect.bottom += top_left.y; +impl WindowArea { + pub fn get_rect(self, hwnd: HWND) -> Result { + let mut rect = unsafe { mem::zeroed() }; - Ok(rect) - } -} - -pub fn adjust_size(hwnd: HWND, size: PhysicalSize) -> PhysicalSize { - let (width, height): (u32, u32) = size.into(); - let rect = RECT { - left: 0, - right: width as i32, - top: 0, - bottom: height as i32, - }; - let rect = adjust_window_rect(hwnd, rect).unwrap_or(rect); - PhysicalSize::new((rect.right - rect.left) as _, (rect.bottom - rect.top) as _) -} - -pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32) { - unsafe { - let rect = adjust_window_rect( - window, - RECT { - top: 0, - left: 0, - bottom: y as i32, - right: x as i32, + match self { + WindowArea::Outer => { + win_to_err(unsafe { GetWindowRect(hwnd, &mut rect) })?; + } + WindowArea::Inner => unsafe { + let mut top_left = mem::zeroed(); + + win_to_err(ClientToScreen(hwnd, &mut top_left))?; + win_to_err(GetClientRect(hwnd, &mut rect))?; + rect.left += top_left.x; + rect.top += top_left.y; + rect.right += top_left.x; + rect.bottom += top_left.y; }, - ) - .expect("adjust_window_rect failed"); - - let outer_x = (rect.right - rect.left).abs() as _; - let outer_y = (rect.top - rect.bottom).abs() as _; - SetWindowPos( - window, - 0, - 0, - 0, - outer_x, - outer_y, - SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE, - ); - InvalidateRgn(window, 0, false.into()); - } -} - -pub fn adjust_window_rect(hwnd: HWND, rect: RECT) -> Option { - unsafe { - let style = GetWindowLongW(hwnd, GWL_STYLE) as u32; - let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; - adjust_window_rect_with_styles(hwnd, style, style_ex, rect) - } -} - -pub fn adjust_window_rect_with_styles( - hwnd: HWND, - style: WINDOW_STYLE, - style_ex: WINDOW_EX_STYLE, - rect: RECT, -) -> Option { - unsafe { - status_map(|r| { - *r = rect; + } - let b_menu = GetMenu(hwnd) != 0; - if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = - (*GET_DPI_FOR_WINDOW, *ADJUST_WINDOW_RECT_EX_FOR_DPI) - { - let dpi = get_dpi_for_window(hwnd); - adjust_window_rect_ex_for_dpi(r, style, b_menu.into(), style_ex, dpi) - } else { - AdjustWindowRectEx(r, style, b_menu.into(), style_ex) - } - }) + Ok(rect) } } @@ -163,7 +101,7 @@ pub fn set_cursor_hidden(hidden: bool) { pub fn get_cursor_clip() -> Result { unsafe { let mut rect: RECT = mem::zeroed(); - win_to_err(|| GetClipCursor(&mut rect)).map(|_| rect) + win_to_err(GetClipCursor(&mut rect)).map(|_| rect) } } @@ -176,7 +114,7 @@ pub fn set_cursor_clip(rect: Option) -> Result<(), io::Error> { .as_ref() .map(|r| r as *const RECT) .unwrap_or(ptr::null()); - win_to_err(|| ClipCursor(rect_ptr)) + win_to_err(ClipCursor(rect_ptr)) } } @@ -197,6 +135,21 @@ pub fn is_focused(window: HWND) -> bool { window == unsafe { GetActiveWindow() } } +pub fn get_instance_handle() -> HINSTANCE { + // Gets the instance handle by taking the address of the + // pseudo-variable created by the microsoft linker: + // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 + + // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: + // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance + + extern "C" { + static __ImageBase: IMAGE_DOS_HEADER; + } + + unsafe { &__ImageBase as *const _ as _ } +} + impl CursorIcon { pub(crate) fn to_windows_cursor(self) -> PCWSTR { match self { @@ -272,19 +225,17 @@ pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( dpi: u32, ) -> BOOL; -lazy_static! { - pub static ref GET_DPI_FOR_WINDOW: Option = - get_function!("user32.dll", GetDpiForWindow); - pub static ref ADJUST_WINDOW_RECT_EX_FOR_DPI: Option = - get_function!("user32.dll", AdjustWindowRectExForDpi); - pub static ref GET_DPI_FOR_MONITOR: Option = - get_function!("shcore.dll", GetDpiForMonitor); - pub static ref ENABLE_NON_CLIENT_DPI_SCALING: Option = - get_function!("user32.dll", EnableNonClientDpiScaling); - pub static ref SET_PROCESS_DPI_AWARENESS_CONTEXT: Option = - get_function!("user32.dll", SetProcessDpiAwarenessContext); - pub static ref SET_PROCESS_DPI_AWARENESS: Option = - get_function!("shcore.dll", SetProcessDpiAwareness); - pub static ref SET_PROCESS_DPI_AWARE: Option = - get_function!("user32.dll", SetProcessDPIAware); -} +pub static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); +pub static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", AdjustWindowRectExForDpi)); +pub static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); +pub static ENABLE_NON_CLIENT_DPI_SCALING: Lazy> = + Lazy::new(|| get_function!("user32.dll", EnableNonClientDpiScaling)); +pub static SET_PROCESS_DPI_AWARENESS_CONTEXT: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDpiAwarenessContext)); +pub static SET_PROCESS_DPI_AWARENESS: Lazy> = + Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness)); +pub static SET_PROCESS_DPI_AWARE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware)); diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index a5febfe846..63e41dffcc 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1,7 +1,9 @@ #![cfg(target_os = "windows")] use parking_lot::Mutex; -use raw_window_handle::{RawWindowHandle, Win32Handle}; +use raw_window_handle::{ + RawDisplayHandle, RawWindowHandle, Win32WindowHandle, WindowsDisplayHandle, +}; use std::{ cell::Cell, ffi::c_void, @@ -26,15 +28,10 @@ use windows_sys::Win32::{ Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_APARTMENTTHREADED, }, - LibraryLoader::GetModuleHandleW, Ole::{OleInitialize, RegisterDragDrop}, }, UI::{ Input::{ - Ime::{ - ImmGetContext, ImmReleaseContext, ImmSetCompositionWindow, CFS_POINT, - COMPOSITIONFORM, - }, KeyboardAndMouse::{ EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, @@ -49,8 +46,8 @@ use windows_sys::Win32::{ SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER, - SM_IMMENABLED, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, - WM_NCLBUTTONDOWN, WNDCLASSEXW, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN, + WNDCLASSEXW, }, }, }; @@ -69,11 +66,12 @@ use crate::{ drop_handler::FileDropHandler, event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, icon::{self, IconType}, + ime::ImeContext, monitor, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, Parent, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, }; /// The Win32 implementation of the main `Window` object. @@ -89,7 +87,7 @@ pub struct Window { } impl Window { - pub fn new( + pub(crate) fn new( event_loop: &EventLoopWindowTarget, w_attr: WindowAttributes, pl_attr: PlatformSpecificWindowBuilderAttributes, @@ -134,7 +132,7 @@ impl Window { #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { - util::get_window_rect(self.hwnd()) + util::WindowArea::Outer.get_rect(self.hwnd()) .map(|rect| Ok(PhysicalPosition::new(rect.left as i32, rect.top as i32))) .expect("Unexpected GetWindowRect failure; please report this error to https://github.com/rust-windowing/winit") } @@ -189,7 +187,8 @@ impl Window { #[inline] pub fn outer_size(&self) -> PhysicalSize { - util::get_window_rect(self.hwnd()) + util::WindowArea::Outer + .get_rect(self.hwnd()) .map(|rect| { PhysicalSize::new( (rect.right - rect.left) as u32, @@ -202,7 +201,7 @@ impl Window { #[inline] pub fn set_inner_size(&self, size: Size) { let scale_factor = self.scale_factor(); - let (width, height) = size.to_physical::(scale_factor).into(); + let physical_size = size.to_physical::(scale_factor); let window_state = Arc::clone(&self.window_state); let window = self.window.clone(); @@ -213,7 +212,8 @@ impl Window { }); }); - util::set_inner_size_physical(self.hwnd(), width, height); + let window_flags = self.window_state.lock().window_flags; + window_flags.set_size(self.hwnd(), physical_size); } #[inline] @@ -264,10 +264,15 @@ impl Window { #[inline] pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = Win32Handle::empty(); - handle.hwnd = self.window.0 as *mut _; - handle.hinstance = self.hinstance() as *mut _; - RawWindowHandle::Win32(handle) + let mut window_handle = Win32WindowHandle::empty(); + window_handle.hwnd = self.window.0 as *mut _; + window_handle.hinstance = self.hinstance() as *mut _; + RawWindowHandle::Win32(window_handle) + } + + #[inline] + pub fn raw_display_handle(&self) -> RawDisplayHandle { + RawDisplayHandle::Windows(WindowsDisplayHandle::empty()) } #[inline] @@ -280,7 +285,15 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + 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.clone(); let window_state = Arc::clone(&self.window_state); let (tx, rx) = channel(); @@ -290,7 +303,7 @@ impl Window { let result = window_state .lock() .mouse - .set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, grab)) + .set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, confine)) .map_err(|e| ExternalError::Os(os_error!(e))); let _ = tx.send(result); }); @@ -566,7 +579,7 @@ impl Window { self.thread_executor.execute_in_thread(move || { let _ = &window; WindowState::set_window_flags(window_state.lock(), window.0, |f| { - f.set(WindowFlags::DECORATIONS, decorations) + f.set(WindowFlags::MARKER_DECORATIONS, decorations) }); }); } @@ -574,7 +587,9 @@ impl Window { #[inline] pub fn is_decorated(&self) -> bool { let window_state = self.window_state.lock(); - window_state.window_flags.contains(WindowFlags::DECORATIONS) + window_state + .window_flags + .contains(WindowFlags::MARKER_DECORATIONS) } #[inline] @@ -626,25 +641,19 @@ impl Window { self.window_state.lock().taskbar_icon = taskbar_icon; } - pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { - if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 { - let composition_form = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: POINT { x, y }, - rcArea: unsafe { mem::zeroed() }, - }; - unsafe { - let himc = ImmGetContext(self.hwnd()); - ImmSetCompositionWindow(himc, &composition_form); - ImmReleaseContext(self.hwnd(), himc); - } + #[inline] + pub fn set_ime_position(&self, spot: Position) { + unsafe { + ImeContext::current(self.hwnd()).set_ime_position(spot, self.scale_factor()); } } #[inline] - pub fn set_ime_position(&self, spot: Position) { - let (x, y) = spot.to_physical::(self.scale_factor()).into(); - self.set_ime_position_physical(x, y); + pub fn set_ime_allowed(&self, allowed: bool) { + self.window_state.lock().ime_allowed = allowed; + unsafe { + ImeContext::set_ime_allowed(self.hwnd(), allowed); + } } #[inline] @@ -682,39 +691,21 @@ impl Window { #[inline] pub fn set_skip_taskbar(&self, skip: bool) { - com_initialized(); - unsafe { - TASKBAR_LIST.with(|task_bar_list_ptr| { - let mut task_bar_list = task_bar_list_ptr.get(); - - if task_bar_list.is_null() { - let hr = CoCreateInstance( - &CLSID_TaskbarList, - ptr::null_mut(), - CLSCTX_ALL, - &IID_ITaskbarList, - &mut task_bar_list as *mut _ as *mut _, - ); - - let hr_init = (*(*task_bar_list).lpVtbl).HrInit; - - if hr != S_OK || hr_init(task_bar_list.cast()) != S_OK { - // In some old windows, the taskbar object could not be created, we just ignore it - return; - } - task_bar_list_ptr.set(task_bar_list) - } + self.window_state.lock().skip_taskbar = skip; + unsafe { set_skip_taskbar(self.hwnd(), skip) }; + } - task_bar_list = task_bar_list_ptr.get(); - if skip { - let delete_tab = (*(*task_bar_list).lpVtbl).DeleteTab; - delete_tab(task_bar_list, self.window.0); - } else { - let add_tab = (*(*task_bar_list).lpVtbl).AddTab; - add_tab(task_bar_list, self.window.0); - } + #[inline] + pub fn set_undecorated_shadow(&self, shadow: bool) { + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) }); - } + }); } #[inline] @@ -798,6 +789,8 @@ impl<'a, T: 'static> InitData<'a, T> { enable_non_client_dpi_scaling(window); + ImeContext::set_ime_allowed(window, false); + Window { window: WindowWrapper(window), window_state, @@ -903,10 +896,17 @@ impl<'a, T: 'static> InitData<'a, T> { win.set_fullscreen(attributes.fullscreen); force_window_active(win.window.0); } else { - let dimensions = attributes + let size = attributes .inner_size .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); - win.set_inner_size(dimensions); + let max_size = attributes + .max_inner_size + .unwrap_or_else(|| PhysicalSize::new(f64::MAX, f64::MAX).into()); + let min_size = attributes + .min_inner_size + .unwrap_or_else(|| PhysicalSize::new(0, 0).into()); + let clamped_size = Size::clamp(size, min_size, max_size, win.scale_factor()); + win.set_inner_size(clamped_size); if attributes.maximized { // Need to set MAXIMIZED after setting `inner_size` as @@ -915,6 +915,14 @@ impl<'a, T: 'static> InitData<'a, T> { } } + // let margins = MARGINS { + // cxLeftWidth: 1, + // cxRightWidth: 1, + // cyTopHeight: 1, + // cyBottomHeight: 1, + // }; + // dbg!(DwmExtendFrameIntoClientArea(win.hwnd(), &margins as *const _)); + if let Some(position) = attributes.position { win.set_outer_position(position); } @@ -933,7 +941,11 @@ where let class_name = register_window_class::(&attributes.window_icon, &pl_attribs.taskbar_icon); let mut window_flags = WindowFlags::empty(); - window_flags.set(WindowFlags::DECORATIONS, attributes.decorations); + window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set( + WindowFlags::MARKER_UNDECORATED_SHADOW, + pl_attribs.decoration_shadow, + ); window_flags.set(WindowFlags::ALWAYS_ON_TOP, attributes.always_on_top); window_flags.set( WindowFlags::NO_BACK_BUFFER, @@ -981,7 +993,7 @@ where CW_USEDEFAULT, parent.unwrap_or(0), pl_attribs.menu.unwrap_or(0), - GetModuleHandleW(ptr::null()), + util::get_instance_handle(), &mut initdata as *mut _ as *mut _, ); @@ -1014,16 +1026,17 @@ unsafe fn register_window_class( .map(|icon| icon.inner.as_raw_handle()) .unwrap_or(0); + use windows_sys::Win32::UI::WindowsAndMessaging::COLOR_WINDOWFRAME; let class = WNDCLASSEXW { cbSize: mem::size_of::() as u32, style: CS_HREDRAW | CS_VREDRAW, lpfnWndProc: Some(super::event_loop::public_window_callback::), cbClsExtra: 0, cbWndExtra: 0, - hInstance: GetModuleHandleW(ptr::null()), + hInstance: util::get_instance_handle(), hIcon: h_icon, hCursor: 0, // must be null in order for cursor state to work properly - hbrBackground: 0, + hbrBackground: COLOR_WINDOWFRAME as _, lpszMenuName: ptr::null(), lpszClassName: class_name.as_ptr(), hIconSm: h_icon_small, @@ -1099,6 +1112,40 @@ unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { }) } +pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { + com_initialized(); + TASKBAR_LIST.with(|task_bar_list_ptr| { + let mut task_bar_list = task_bar_list_ptr.get(); + + if task_bar_list.is_null() { + let hr = CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList, + &mut task_bar_list as *mut _ as *mut _, + ); + + let hr_init = (*(*task_bar_list).lpVtbl).HrInit; + + if hr != S_OK || hr_init(task_bar_list.cast()) != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list_ptr.set(task_bar_list) + } + + task_bar_list = task_bar_list_ptr.get(); + if skip { + let delete_tab = (*(*task_bar_list).lpVtbl).DeleteTab; + delete_tab(task_bar_list, hwnd); + } else { + let add_tab = (*(*task_bar_list).lpVtbl).AddTab; + add_tab(task_bar_list, hwnd); + } + }); +} + unsafe fn force_window_active(handle: HWND) { // In some situation, calling SetForegroundWindow could not bring up the window, // This is a little hack which can "steal" the foreground window permission diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 9122649b9d..bf0589c871 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -1,5 +1,5 @@ use crate::{ - dpi::{PhysicalPosition, Size}, + dpi::{PhysicalPosition, PhysicalSize, Size}, event::ModifiersState, icon::Icon, platform_impl::platform::{event_loop, util}, @@ -11,14 +11,15 @@ use windows_sys::Win32::{ Foundation::{HWND, RECT}, Graphics::Gdi::InvalidateRgn, UI::WindowsAndMessaging::{ - SendMessageW, SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, - HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, - SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, - SW_SHOW, WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, - WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, - WS_EX_LEFT, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, - WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPED, - WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, + AdjustWindowRectEx, GetMenu, GetWindowLongW, SendMessageW, SetWindowLongW, SetWindowPos, + ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, + SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, + SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, WINDOWPLACEMENT, WINDOW_EX_STYLE, + WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, + WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_NOREDIRECTIONBITMAP, + WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, + WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, + WS_VISIBLE, }, }; @@ -42,6 +43,15 @@ pub struct WindowState { pub preferred_theme: Option, pub high_surrogate: Option, pub window_flags: WindowFlags, + + pub ime_state: ImeState, + pub ime_allowed: bool, + + // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS + pub is_active: bool, + pub is_focused: bool, + + pub skip_taskbar: bool, } #[derive(Clone)] @@ -67,42 +77,51 @@ bitflags! { bitflags! { pub struct WindowFlags: u32 { const RESIZABLE = 1 << 0; - const DECORATIONS = 1 << 1; - const VISIBLE = 1 << 2; - const ON_TASKBAR = 1 << 3; - const ALWAYS_ON_TOP = 1 << 4; - const NO_BACK_BUFFER = 1 << 5; - const TRANSPARENT = 1 << 6; - const CHILD = 1 << 7; - const MAXIMIZED = 1 << 8; - const POPUP = 1 << 14; + const VISIBLE = 1 << 1; + const ON_TASKBAR = 1 << 2; + const ALWAYS_ON_TOP = 1 << 3; + const NO_BACK_BUFFER = 1 << 4; + const TRANSPARENT = 1 << 5; + const CHILD = 1 << 6; + const MAXIMIZED = 1 << 7; + const POPUP = 1 << 8; /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is /// included here to make masking easier. const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 9; - const MARKER_BORDERLESS_FULLSCREEN = 1 << 13; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 10; /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. /// In most cases, it's okay to let those parameters change the state. However, when we're /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to /// effect our stored state, because the purpose of `apply_diff` is to update the actual /// window's state to match our stored state. This controls whether to accept those changes. - const MARKER_RETAIN_STATE_ON_SIZE = 1 << 10; + const MARKER_RETAIN_STATE_ON_SIZE = 1 << 11; - const MARKER_IN_SIZE_MOVE = 1 << 11; + const MARKER_IN_SIZE_MOVE = 1 << 12; - const MINIMIZED = 1 << 12; + const MINIMIZED = 1 << 13; const IGNORE_CURSOR_EVENT = 1 << 14; + /// Fully decorated window (incl. caption, border and drop shadow). + const MARKER_DECORATIONS = 1 << 15; + /// Drop shadow for undecorated windows. + const MARKER_UNDECORATED_SHADOW = 1 << 16; + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; - const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; - const INVISIBLE_AND_MASK = !WindowFlags::MAXIMIZED.bits; } } +#[derive(Eq, PartialEq)] +pub enum ImeState { + Disabled, + Enabled, + Preedit, +} + impl WindowState { - pub fn new( + pub(crate) fn new( attributes: &WindowAttributes, taskbar_icon: Option, scale_factor: f64, @@ -132,6 +151,14 @@ impl WindowState { preferred_theme, high_surrogate: None, window_flags: WindowFlags::empty(), + + ime_state: ImeState::Disabled, + ime_allowed: false, + + is_active: false, + is_focused: false, + + skip_taskbar: false, } } @@ -157,6 +184,24 @@ impl WindowState { { f(&mut self.window_flags); } + + pub fn has_active_focus(&self) -> bool { + self.is_active && self.is_focused + } + + // Updates is_active and returns whether active-focus state has changed + pub fn set_active(&mut self, is_active: bool) -> bool { + let old = self.has_active_focus(); + self.is_active = is_active; + old != self.has_active_focus() + } + + // Updates is_focused and returns whether active-focus state has changed + pub fn set_focused(&mut self, is_focused: bool) -> bool { + let old = self.has_active_focus(); + self.is_focused = is_focused; + old != self.has_active_focus() + } } impl MouseProperties { @@ -187,25 +232,22 @@ impl WindowFlags { if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; } - if !self.contains(WindowFlags::VISIBLE) { - self &= WindowFlags::INVISIBLE_AND_MASK; - } - if !self.contains(WindowFlags::DECORATIONS) { - self &= WindowFlags::NO_DECORATIONS_AND_MASK; - } self } pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { - let (mut style, mut style_ex) = (WS_OVERLAPPED, WS_EX_LEFT); + // Required styles to properly support common window functionality like aero snap. + let mut style = WS_CAPTION + | WS_MINIMIZEBOX + | WS_BORDER + | WS_CLIPSIBLINGS + | WS_CLIPCHILDREN + | WS_SYSMENU; + let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; if self.contains(WindowFlags::RESIZABLE) { style |= WS_SIZEBOX | WS_MAXIMIZEBOX; } - if self.contains(WindowFlags::DECORATIONS) { - style |= WS_CAPTION | WS_MINIMIZEBOX | WS_BORDER; - style_ex = WS_EX_WINDOWEDGE; - } if self.contains(WindowFlags::VISIBLE) { style |= WS_VISIBLE; } @@ -234,9 +276,6 @@ impl WindowFlags { style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; } - style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU; - style_ex |= WS_EX_ACCEPTFILES; - if self.intersects( WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, ) { @@ -252,21 +291,17 @@ impl WindowFlags { new = new.mask(); let diff = self ^ new; + if diff == WindowFlags::empty() { return; } - if diff.contains(WindowFlags::VISIBLE) { + if new.contains(WindowFlags::VISIBLE) { unsafe { - ShowWindow( - window, - match new.contains(WindowFlags::VISIBLE) { - true => SW_SHOW, - false => SW_HIDE, - }, - ); + ShowWindow(window, SW_SHOW); } } + if diff.contains(WindowFlags::ALWAYS_ON_TOP) { unsafe { SetWindowPos( @@ -310,6 +345,12 @@ impl WindowFlags { } } + if !new.contains(WindowFlags::VISIBLE) { + unsafe { + ShowWindow(window, SW_HIDE); + } + } + if diff != WindowFlags::empty() { let (style, style_ex) = new.to_window_styles(); @@ -339,11 +380,69 @@ impl WindowFlags { } } } + + pub fn adjust_rect(self, hwnd: HWND, mut rect: RECT) -> Result { + unsafe { + let mut style = GetWindowLongW(hwnd, GWL_STYLE) as u32; + let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + + // Frameless style implemented by manually overriding the non-client area in `WM_NCCALCSIZE`. + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_SIZEBOX); + } + + util::win_to_err({ + let b_menu = GetMenu(hwnd) != 0; + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = ( + *util::GET_DPI_FOR_WINDOW, + *util::ADJUST_WINDOW_RECT_EX_FOR_DPI, + ) { + let dpi = get_dpi_for_window(hwnd); + adjust_window_rect_ex_for_dpi(&mut rect, style, b_menu.into(), style_ex, dpi) + } else { + AdjustWindowRectEx(&mut rect, style, b_menu.into(), style_ex) + } + })?; + Ok(rect) + } + } + + pub fn adjust_size(self, hwnd: HWND, size: PhysicalSize) -> PhysicalSize { + let (width, height): (u32, u32) = size.into(); + let rect = RECT { + left: 0, + right: width as i32, + top: 0, + bottom: height as i32, + }; + let rect = self.adjust_rect(hwnd, rect).unwrap_or(rect); + + let outer_x = (rect.right - rect.left).abs(); + let outer_y = (rect.top - rect.bottom).abs(); + + PhysicalSize::new(outer_x as _, outer_y as _) + } + + pub fn set_size(self, hwnd: HWND, size: PhysicalSize) { + unsafe { + let (width, height): (u32, u32) = self.adjust_size(hwnd, size).into(); + SetWindowPos( + hwnd, + 0, + 0, + 0, + width as _, + height as _, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE, + ); + InvalidateRgn(hwnd, 0, false.into()); + } + } } impl CursorFlags { fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> { - let client_rect = util::get_client_rect(window)?; + let client_rect = util::WindowArea::Inner.get_rect(window)?; if util::is_focused(window) { let cursor_clip = match self.contains(CursorFlags::GRABBED) { diff --git a/src/window.rs b/src/window.rs index f03033256b..dba514f90d 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,6 +1,10 @@ -//! The `Window` struct and associated types. +//! The [`Window`] struct and associated types. use std::fmt; +use raw_window_handle::{ + HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, +}; + use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, @@ -61,7 +65,7 @@ impl Drop for Window { /// Identifier of a window. Unique for each window. /// -/// Can be obtained with `window.id()`. +/// Can be obtained with [`window.id()`](`Window::id`). /// /// Whenever you receive an event specific to a window, this event contains a `WindowId` which you /// can then compare to the ids of your windows. @@ -69,13 +73,13 @@ impl Drop for Window { pub struct WindowId(pub(crate) platform_impl::WindowId); impl WindowId { - /// Returns a dummy `WindowId`, useful for unit testing. + /// Returns a dummy id, useful for unit testing. /// /// # Safety /// /// 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`. + /// 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 { @@ -83,11 +87,24 @@ impl WindowId { } } +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0.into() + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id.into()) + } +} + /// Object that allows building windows. #[derive(Clone, Default)] +#[must_use] pub struct WindowBuilder { /// The attributes to use to create the window. - pub window: WindowAttributes, + pub(crate) window: WindowAttributes, // Platform-specific configuration. pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes, @@ -103,92 +120,19 @@ impl fmt::Debug for WindowBuilder { /// Attributes to use when creating a window. #[derive(Debug, Clone)] -pub struct WindowAttributes { - /// The dimensions of the window. If this is `None`, some platform-specific dimensions will be - /// used. - /// - /// The default is `None`. +pub(crate) struct WindowAttributes { pub inner_size: Option, - - /// The minimum dimensions a window can be, If this is `None`, the window will have no minimum dimensions (aside from reserved). - /// - /// The default is `None`. pub min_inner_size: Option, - - /// The maximum dimensions a window can be, If this is `None`, the maximum will have no maximum or will be set to the primary monitor's dimensions by the platform. - /// - /// The default is `None`. pub max_inner_size: Option, - - /// The desired position of the window. If this is `None`, some platform-specific position - /// will be chosen. - /// - /// The default is `None`. - /// - /// ## Platform-specific - /// - /// - **macOS**: The top left corner position of the window content, the window's "inner" - /// position. The window title bar will be placed above it. - /// The window will be positioned such that it fits on screen, maintaining - /// set `inner_size` if any. - /// If you need to precisely position the top left corner of the whole window you have to - /// use [`Window::set_outer_position`] after creating the window. - /// - **Windows**: The top left corner position of the window title bar, the window's "outer" - /// position. - /// There may be a small gap between this position and the window due to the specifics of the - /// Window Manager. - /// - **X11**: The top left corner of the window, the window's "outer" position. - /// - **Others**: Ignored. - /// - /// See [`Window::set_outer_position`]. - /// - /// [`Window::set_outer_position`]: crate::window::Window::set_outer_position pub position: Option, - - /// Whether the window is resizable or not. - /// - /// The default is `true`. pub resizable: bool, - - /// Whether the window should be set as fullscreen upon creation. - /// - /// The default is `None`. - pub fullscreen: Option, - - /// The title of the window in the title bar. - /// - /// The default is `"winit window"`. pub title: String, - - /// Whether the window should be maximized upon creation. - /// - /// The default is `false`. + pub fullscreen: Option, pub maximized: bool, - - /// Whether the window should be immediately visible upon creation. - /// - /// The default is `true`. pub visible: bool, - - /// Whether the the window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - /// - /// The default is `false`. pub transparent: bool, - - /// Whether the window should have borders and bars. - /// - /// The default is `true`. pub decorations: bool, - - /// Whether the window should always be on top of other windows. - /// - /// The default is `false`. pub always_on_top: bool, - - /// The window icon. - /// - /// The default is `None`. pub window_icon: Option, } @@ -214,7 +158,7 @@ impl Default for WindowAttributes { } impl WindowBuilder { - /// Initializes a new `WindowBuilder` with default values. + /// Initializes a new builder with default values. #[inline] pub fn new() -> Self { Default::default() @@ -222,31 +166,33 @@ impl WindowBuilder { /// Requests the window to be of specific dimensions. /// - /// See [`Window::set_inner_size`] for details. + /// If this is not set, some platform-specific dimensions will be used. /// - /// [`Window::set_inner_size`]: crate::window::Window::set_inner_size + /// See [`Window::set_inner_size`] for details. #[inline] pub fn with_inner_size>(mut self, size: S) -> Self { self.window.inner_size = Some(size.into()); self } - /// Sets a minimum dimension size for the window. + /// Sets the minimum dimensions a window can have. /// - /// See [`Window::set_min_inner_size`] for details. + /// If this is not set, the window will have no minimum dimensions (aside + /// from reserved). /// - /// [`Window::set_min_inner_size`]: crate::window::Window::set_min_inner_size + /// See [`Window::set_min_inner_size`] for details. #[inline] pub fn with_min_inner_size>(mut self, min_size: S) -> Self { self.window.min_inner_size = Some(min_size.into()); self } - /// Sets a maximum dimension size for the window. + /// Sets the maximum dimensions a window can have. /// - /// See [`Window::set_max_inner_size`] for details. + /// If this is not set, the window will have no maximum or will be set to + /// the primary monitor's dimensions by the platform. /// - /// [`Window::set_max_inner_size`]: crate::window::Window::set_max_inner_size + /// See [`Window::set_max_inner_size`] for details. #[inline] pub fn with_max_inner_size>(mut self, max_size: S) -> Self { self.window.max_inner_size = Some(max_size.into()); @@ -255,9 +201,26 @@ impl WindowBuilder { /// Sets a desired initial position for the window. /// - /// See [`WindowAttributes::position`] for details. + /// If this is not set, some platform-specific position will be chosen. + /// + /// See [`Window::set_outer_position`] for details. /// - /// [`WindowAttributes::position`]: crate::window::WindowAttributes::position + /// ## Platform-specific + /// + /// - **macOS:** The top left corner position of the window content, the + /// window's "inner" position. The window title bar will be placed above + /// it. The window will be positioned such that it fits on screen, + /// maintaining set `inner_size` if any. + /// If you need to precisely position the top left corner of the whole + /// window you have to use [`Window::set_outer_position`] after creating + /// the window. + /// - **Windows:** The top left corner position of the window title bar, + /// the window's "outer" position. + /// There may be a small gap between this position and the window due to + /// the specifics of the Window Manager. + /// - **X11:** The top left corner of the window, the window's "outer" + /// position. + /// - **Others:** Ignored. #[inline] pub fn with_position>(mut self, position: P) -> Self { self.window.position = Some(position.into()); @@ -266,53 +229,53 @@ impl WindowBuilder { /// Sets whether the window is resizable or not. /// - /// See [`Window::set_resizable`] for details. + /// The default is `true`. /// - /// [`Window::set_resizable`]: crate::window::Window::set_resizable + /// See [`Window::set_resizable`] for details. #[inline] pub fn with_resizable(mut self, resizable: bool) -> Self { self.window.resizable = resizable; self } - /// Requests a specific title for the window. + /// Sets the initial title of the window in the title bar. /// - /// See [`Window::set_title`] for details. + /// The default is `"winit window"`. /// - /// [`Window::set_title`]: crate::window::Window::set_title + /// See [`Window::set_title`] for details. #[inline] pub fn with_title>(mut self, title: T) -> Self { self.window.title = title.into(); self } - /// Sets the window fullscreen state. + /// Sets whether the window should be put into fullscreen upon creation. /// - /// See [`Window::set_fullscreen`] for details. + /// The default is `None`. /// - /// [`Window::set_fullscreen`]: crate::window::Window::set_fullscreen + /// See [`Window::set_fullscreen`] for details. #[inline] pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { self.window.fullscreen = fullscreen; self } - /// Requests maximized mode. + /// Request that the window is maximized upon creation. /// - /// See [`Window::set_maximized`] for details. + /// The default is `false`. /// - /// [`Window::set_maximized`]: crate::window::Window::set_maximized + /// See [`Window::set_maximized`] for details. #[inline] pub fn with_maximized(mut self, maximized: bool) -> Self { self.window.maximized = maximized; self } - /// Sets whether the window will be initially hidden or visible. + /// Sets whether the window will be initially visible or hidden. /// - /// See [`Window::set_visible`] for details. + /// The default is to show the window. /// - /// [`Window::set_visible`]: crate::window::Window::set_visible + /// See [`Window::set_visible`] for details. #[inline] pub fn with_visible(mut self, visible: bool) -> Self { self.window.visible = visible; @@ -320,17 +283,28 @@ impl WindowBuilder { } /// Sets whether the background of the window should be transparent. + /// + /// If this is `true`, writing colors with alpha values different than + /// `1.0` will produce a transparent window. + /// + /// The default is `false`. #[inline] pub fn with_transparent(mut self, transparent: bool) -> Self { self.window.transparent = transparent; self } + /// Get whether the window will support transparency. + #[inline] + pub fn transparent(&self) -> bool { + self.window.transparent + } + /// Sets whether the window should have a border, a title bar, etc. /// - /// See [`Window::set_decorations`] for details. + /// The default is `true`. /// - /// [`Window::set_decorations`]: crate::window::Window::set_decorations + /// See [`Window::set_decorations`] for details. #[inline] pub fn with_decorations(mut self, decorations: bool) -> Self { self.window.decorations = decorations; @@ -339,9 +313,9 @@ impl WindowBuilder { /// Sets whether or not the window will always be on top of other windows. /// - /// See [`Window::set_always_on_top`] for details. + /// The default is `false`. /// - /// [`Window::set_always_on_top`]: crate::window::Window::set_always_on_top + /// See [`Window::set_always_on_top`] for details. #[inline] pub fn with_always_on_top(mut self, always_on_top: bool) -> Self { self.window.always_on_top = always_on_top; @@ -350,9 +324,9 @@ impl WindowBuilder { /// Sets the window icon. /// - /// See [`Window::set_window_icon`] for details. + /// The default is `None`. /// - /// [`Window::set_window_icon`]: crate::window::Window::set_window_icon + /// See [`Window::set_window_icon`] for details. #[inline] pub fn with_window_icon(mut self, window_icon: Option) -> Self { self.window.window_icon = window_icon; @@ -363,9 +337,10 @@ impl WindowBuilder { /// /// Possible causes of error include denied permission, incompatible system, and lack of memory. /// - /// Platform-specific behavior: - /// - **Web**: The window is created but not inserted into the web page automatically. Please - /// see the web platform module for more information. + /// ## Platform-specific + /// + /// - **Web:** The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. #[inline] pub fn build( self, @@ -389,11 +364,12 @@ impl Window { /// Error should be very rare and only occur in case of permission denied, incompatible system, /// out of memory, etc. /// - /// Platform-specific behavior: - /// - **Web**: The window is created but not inserted into the web page automatically. Please - /// see the web platform module for more information. + /// ## Platform-specific /// - /// [`WindowBuilder::new().build(event_loop)`]: crate::window::WindowBuilder::build + /// - **Web:** The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. + /// + /// [`WindowBuilder::new().build(event_loop)`]: WindowBuilder::build #[inline] pub fn new(event_loop: &EventLoopWindowTarget) -> Result { let builder = WindowBuilder::new(); @@ -411,7 +387,7 @@ impl Window { /// See the [`dpi`](crate::dpi) module for more information. /// /// Note that this value can change depending on user action (for example if the window is - /// moved to another screen); as such, tracking `WindowEvent::ScaleFactorChanged` events is + /// moved to another screen); as such, tracking [`WindowEvent::ScaleFactorChanged`] events is /// the most robust way to track the DPI you need to use to draw. /// /// ## Platform-specific @@ -421,19 +397,20 @@ impl Window { /// - **iOS:** Can only be called on the main thread. Returns the underlying `UIView`'s /// [`contentScaleFactor`]. /// + /// [`WindowEvent::ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc #[inline] pub fn scale_factor(&self) -> f64 { self.window.scale_factor() } - /// Emits a `WindowEvent::RedrawRequested` event in the associated event loop after all OS + /// Emits a [`Event::RedrawRequested`] event in the associated event loop after all OS /// events have been processed by the event loop. /// /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with /// OS-requested redraws (e.g. when a window gets resized). /// - /// This function can cause `RedrawRequested` events to be emitted after `Event::MainEventsCleared` + /// This function can cause `RedrawRequested` events to be emitted after [`Event::MainEventsCleared`] /// but before `Event::NewEvents` if called in the following circumstances: /// * While processing `MainEventsCleared`. /// * While processing a `RedrawRequested` event that was sent during `MainEventsCleared` or any @@ -443,6 +420,9 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. /// - **Android:** Subsequent calls after `MainEventsCleared` are not handled. + /// + /// [`Event::RedrawRequested`]: crate::event::Event::RedrawRequested + /// [`Event::MainEventsCleared`]: crate::event::Event::MainEventsCleared #[inline] pub fn request_redraw(&self) { self.window.request_redraw() @@ -454,14 +434,14 @@ impl Window { /// Returns the position of the top-left hand corner of the window's client area relative to the /// top-left hand corner of the desktop. /// - /// The same conditions that apply to `outer_position` apply to this method. + /// The same conditions that apply to [`Window::outer_position`] apply to this method. /// /// ## Platform-specific /// /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the /// window's [safe area] in the screen space coordinate system. /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns the - /// same value as `outer_position`._ + /// same value as [`Window::outer_position`]._ /// - **Android / Wayland:** Always returns [`NotSupportedError`]. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc @@ -471,14 +451,14 @@ impl Window { } /// Returns the position of the top-left hand corner of the window relative to the - /// top-left hand corner of the desktop. + /// top-left hand corner of the desktop. /// /// Note that the top-left hand corner of the desktop is not necessarily the same as - /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner - /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. + /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner + /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. /// /// The coordinates can be negative if the top-left hand corner of the window is outside - /// of the visible screen region. + /// of the visible screen region. /// /// ## Platform-specific /// @@ -493,8 +473,8 @@ impl Window { /// Modifies the position of the window. /// - /// See `outer_position` for more information about the coordinates. This automatically un-maximizes the - /// window if it's maximized. + /// See [`Window::outer_position`] for more information about the coordinates. + /// This automatically un-maximizes the window if it's maximized. /// /// ```no_run /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; @@ -538,8 +518,8 @@ impl Window { /// Modifies the inner size of the window. /// - /// See `inner_size` for more information about the values. This automatically un-maximizes the - /// window if it's maximized. + /// See [`Window::inner_size`] for more information about the values. + /// This automatically un-maximizes the window if it's maximized. /// /// ```no_run /// # use winit::dpi::{LogicalSize, PhysicalSize}; @@ -566,14 +546,14 @@ impl Window { /// Returns the physical size of the entire window. /// /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), - /// use `inner_size` instead. + /// use [`Window::inner_size`] instead. /// /// ## Platform-specific /// - /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window in + /// - **iOS:** Can only be called on the main thread. Returns the [`PhysicalSize`] of the window in /// screen space coordinates. /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as - /// `inner_size`._ + /// [`Window::inner_size`]._ #[inline] pub fn outer_size(&self) -> PhysicalSize { self.window.outer_size() @@ -641,6 +621,7 @@ impl Window { /// Modifies the window's visibility. /// /// If `false`, this will hide the window. If `true`, this will show the window. + /// /// ## Platform-specific /// /// - **Android / Wayland / Web:** Unsupported. @@ -650,9 +631,9 @@ impl Window { self.window.set_visible(visible) } - /// Gets the window's current vibility state. + /// Gets the window's current visibility state. /// - /// If `None` means it couldn't be determined so it is not recommended to use this to drive your rendering backend. + /// `None` means it couldn't be determined, so it is not recommended to use this to drive your rendering backend. /// /// ## Platform-specific /// @@ -665,18 +646,18 @@ impl Window { /// Sets whether the window is resizable or not. /// - /// Note that making the window unresizable doesn't exempt you from handling `Resized`, as that event can still be - /// triggered by DPI scaling, entering fullscreen mode, etc. + /// Note that making the window unresizable doesn't exempt you from handling [`WindowEvent::Resized`], as that + /// event can still be triggered by DPI scaling, entering fullscreen mode, etc. Also, the + /// window could still be resized by calling [`Window::set_inner_size`]. /// /// ## Platform-specific /// /// This only has an effect on desktop platforms. /// - /// Due to a bug in XFCE, this has no effect on Xfwm. - /// - /// ## Platform-specific - /// + /// - **X11:** Due to a bug in XFCE, this has no effect on Xfwm. /// - **iOS / Android / Web:** Unsupported. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized #[inline] pub fn set_resizable(&self, resizable: bool) { self.window.set_resizable(resizable) @@ -728,18 +709,18 @@ impl Window { /// /// ## Platform-specific /// - /// - **macOS:** `Fullscreen::Exclusive` provides true exclusive mode with a + /// - **macOS:** [`Fullscreen::Exclusive`] provides true exclusive mode with a /// video mode change. *Caveat!* macOS doesn't provide task switching (or /// spaces!) while in exclusive fullscreen mode. This mode should be used /// when a video mode change is desired, but for a better user experience, /// borderless fullscreen might be preferred. /// - /// `Fullscreen::Borderless` provides a borderless fullscreen window on a + /// [`Fullscreen::Borderless`] provides a borderless fullscreen window on a /// separate space. This is the idiomatic way for fullscreen games to work /// on macOS. See `WindowExtMacOs::set_simple_fullscreen` if /// separate spaces are not preferred. /// - /// The dock and the menu bar are always disabled in fullscreen mode. + /// The dock and the menu bar are disabled in exclusive fullscreen mode. /// - **iOS:** Can only be called on the main thread. /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. /// - **Windows:** Screen saver is disabled in fullscreen mode. @@ -766,8 +747,6 @@ impl Window { /// ## Platform-specific /// /// - **iOS / Android / Web:** Unsupported. - /// - /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc #[inline] pub fn set_decorations(&self, decorations: bool) { self.window.set_decorations(decorations) @@ -794,18 +773,20 @@ impl Window { self.window.set_always_on_top(always_on_top) } - /// Sets the window icon. On Windows and X11, this is typically the small icon in the top-left + /// Sets the window icon. + /// + /// On Windows and X11, this is typically the small icon in the top-left /// corner of the titlebar. /// /// ## Platform-specific /// /// - **iOS / Android / Web / Wayland / macOS:** Unsupported. /// - /// On Windows, this sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's - /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. /// - /// X11 has no universal guidelines for icon sizes, so you're at the whims of the WM. That - /// said, it's usually in the same ballpark as on Windows. + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That + /// said, it's usually in the same ballpark as on Windows. #[inline] pub fn set_window_icon(&self, window_icon: Option) { self.window.set_window_icon(window_icon) @@ -813,6 +794,13 @@ impl Window { /// Sets location of IME candidate box in client area coordinates relative to the top left. /// + /// This is the window / popup / overlay that allows you to select the desired characters. + /// The look of this box may differ between input devices, even on the same platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// /// ```no_run /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; /// # use winit::event_loop::EventLoop; @@ -829,11 +817,41 @@ impl Window { /// ## Platform-specific /// /// - **iOS / Android / Web:** Unsupported. + /// + /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0 + /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 #[inline] pub fn set_ime_position>(&self, position: P) { self.window.set_ime_position(position.into()) } + /// Sets whether the window should get IME events + /// + /// When IME is allowed, the window will receive [`Ime`] events, and during the + /// preedit phase the window will NOT get [`KeyboardInput`] or + /// [`ReceivedCharacter`] events. The window should allow IME while it is + /// expecting text input. + /// + /// When IME is not allowed, the window won't receive [`Ime`] events, and will + /// receive [`KeyboardInput`] events for every keypress instead. Without + /// allowing IME, the window will also get [`ReceivedCharacter`] events for + /// certain keyboard input. Not allowing IME is useful for games for example. + /// + /// IME is **not** allowed by default. + /// + /// ## Platform-specific + /// + /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are combined. + /// - **iOS / Android / Web:** Unsupported. + /// + /// [`Ime`]: crate::event::WindowEvent::Ime + /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput + /// [`ReceivedCharacter`]: crate::event::WindowEvent::ReceivedCharacter + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.window.set_ime_allowed(allowed); + } + /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// @@ -851,14 +869,14 @@ impl Window { /// Requests user attention to the window, this has no effect if the application /// is already focused. How requesting for user attention manifests is platform dependent, - /// see `UserAttentionType` for details. + /// see [`UserAttentionType`] for details. /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. /// /// ## Platform-specific /// - /// - **iOS / Android / Web :** Unsupported. + /// - **iOS / Android / Web:** Unsupported. /// - **macOS:** `None` has no effect. /// - **X11:** Requests for user attention must be manually cleared. /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. @@ -903,18 +921,24 @@ impl Window { self.window.set_cursor_position(position.into()) } - /// Grabs the cursor, preventing it from leaving the window. + /// Set grabbing [mode]([`CursorGrabMode`]) on the cursor preventing it from leaving the window. /// - /// There's no guarantee that the cursor will be hidden. You should - /// hide it by yourself if you want so. + /// # Example /// - /// ## Platform-specific + /// First try confining the cursor, and if that fails, try locking it instead. /// - /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. - /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + /// ```no_run + /// # use winit::event_loop::EventLoop; + /// # use winit::window::{CursorGrabMode, Window}; + /// # let mut event_loop = EventLoop::new(); + /// # let window = Window::new(&event_loop).unwrap(); + /// window.set_cursor_grab(CursorGrabMode::Confined) + /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) + /// .unwrap(); + /// ``` #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - self.window.set_cursor_grab(grab) + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + self.window.set_cursor_grab(mode) } /// Modifies the cursor's visibility. @@ -980,11 +1004,13 @@ impl Window { /// Returns the list of all the monitors available on the system. /// - /// This is the same as `EventLoopWindowTarget::available_monitors`, and is provided for convenience. + /// This is the same as [`EventLoopWindowTarget::available_monitors`], and is provided for convenience. /// /// ## Platform-specific /// /// **iOS:** Can only be called on the main thread. + /// + /// [`EventLoopWindowTarget::available_monitors`]: crate::event_loop::EventLoopWindowTarget::available_monitors #[inline] pub fn available_monitors(&self) -> impl Iterator { self.window @@ -997,30 +1023,185 @@ impl Window { /// /// Returns `None` if it can't identify any monitor as a primary one. /// - /// This is the same as `EventLoopWindowTarget::primary_monitor`, and is provided for convenience. + /// This is the same as [`EventLoopWindowTarget::primary_monitor`], and is provided for convenience. /// /// ## Platform-specific /// /// **iOS:** Can only be called on the main thread. /// **Wayland:** Always returns `None`. + /// + /// [`EventLoopWindowTarget::primary_monitor`]: crate::event_loop::EventLoopWindowTarget::primary_monitor #[inline] pub fn primary_monitor(&self) -> Option { self.window.primary_monitor() } } - -unsafe impl raw_window_handle::HasRawWindowHandle for Window { - /// Returns a `raw_window_handle::RawWindowHandle` for the Window +unsafe impl HasRawWindowHandle for Window { + /// Returns a [`raw_window_handle::RawWindowHandle`] for the Window /// /// ## Platform-specific /// - /// - **Android:** Only available after receiving the Resumed event and before Suspended. *If you* - /// *try to get the handle outside of that period, this function will panic*! - fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + /// ### Android + /// + /// Only available after receiving [`Event::Resumed`] and before [`Event::Suspended`]. *If you + /// try to get the handle outside of that period, this function will panic*! + /// + /// Make sure to release or destroy any resources created from this `RawWindowHandle` (ie. Vulkan + /// or OpenGL surfaces) before returning from [`Event::Suspended`], at which point Android will + /// release the underlying window/surface: any subsequent interaction is undefined behavior. + /// + /// [`Event::Resumed`]: crate::event::Event::Resumed + /// [`Event::Suspended`]: crate::event::Event::Suspended + fn raw_window_handle(&self) -> RawWindowHandle { self.window.raw_window_handle() } } +unsafe impl HasRawDisplayHandle for Window { + /// Returns a [`raw_window_handle::RawDisplayHandle`] used by the [`EventLoop`] that + /// created a window. + /// + /// [`EventLoop`]: crate::event_loop::EventLoop + fn raw_display_handle(&self) -> RawDisplayHandle { + self.window.raw_display_handle() + } +} +unsafe impl raw_window_handle_04::HasRawWindowHandle for Window { + /// Returns a [`raw_window_handle_04::RawWindowHandle`] for the Window + /// + /// This provides backwards compatibility for downstream crates that have not yet + /// upgraded to `raw_window_handle` version 0.5, such as Wgpu version 0.13. + /// + /// ## Platform-specific + /// + /// ### Android + /// + /// Only available after receiving [`Event::Resumed`] and before [`Event::Suspended`]. *If you + /// try to get the handle outside of that period, this function will panic*! + /// + /// Make sure to release or destroy any resources created from this `RawWindowHandle` (ie. Vulkan + /// or OpenGL surfaces) before returning from [`Event::Suspended`], at which point Android will + /// release the underlying window/surface: any subsequent interaction is undefined behavior. + /// + /// [`Event::Resumed`]: crate::event::Event::Resumed + /// [`Event::Suspended`]: crate::event::Event::Suspended + fn raw_window_handle(&self) -> raw_window_handle_04::RawWindowHandle { + use raw_window_handle_04::{ + AndroidNdkHandle, AppKitHandle, HaikuHandle, OrbitalHandle, UiKitHandle, WaylandHandle, + WebHandle, Win32Handle, WinRtHandle, XcbHandle, XlibHandle, + }; + + // XXX: Ideally this would be encapsulated either through a + // compatibility API from raw_window_handle_05 or else within the + // backends but since this is only to provide short-term backwards + // compatibility, we just handle the full mapping inline here. + // + // The intention is to remove this trait implementation before Winit + // 0.28, once crates have had time to upgrade to raw_window_handle 0.5 + + match (self.window.raw_window_handle(), self.window.raw_display_handle()) { + (RawWindowHandle::UiKit(window_handle), _) => { + let mut handle = UiKitHandle::empty(); + handle.ui_view = window_handle.ui_view; + handle.ui_window = window_handle.ui_window; + handle.ui_view_controller = window_handle.ui_view_controller; + raw_window_handle_04::RawWindowHandle::UiKit(handle) + }, + (RawWindowHandle::AppKit(window_handle), _) => { + let mut handle = AppKitHandle::empty(); + handle.ns_window = window_handle.ns_window; + handle.ns_view = window_handle.ns_view; + raw_window_handle_04::RawWindowHandle::AppKit(handle) + }, + (RawWindowHandle::Orbital(window_handle), _) => { + let mut handle = OrbitalHandle::empty(); + handle.window = window_handle.window; + raw_window_handle_04::RawWindowHandle::Orbital(handle) + }, + (RawWindowHandle::Xlib(window_handle), RawDisplayHandle::Xlib(display_handle)) => { + let mut handle = XlibHandle::empty(); + handle.display = display_handle.display; + handle.window = window_handle.window; + handle.visual_id = window_handle.visual_id; + raw_window_handle_04::RawWindowHandle::Xlib(handle) + }, + (RawWindowHandle::Xcb(window_handle), RawDisplayHandle::Xcb(display_handle)) => { + let mut handle = XcbHandle::empty(); + handle.connection = display_handle.connection; + handle.window = window_handle.window; + handle.visual_id = window_handle.visual_id; + raw_window_handle_04::RawWindowHandle::Xcb(handle) + }, + (RawWindowHandle::Wayland(window_handle), RawDisplayHandle::Wayland(display_handle)) => { + let mut handle = WaylandHandle::empty(); + handle.display = display_handle.display; + handle.surface = window_handle.surface; + raw_window_handle_04::RawWindowHandle::Wayland(handle) + }, + (RawWindowHandle::Win32(window_handle), _) => { + let mut handle = Win32Handle::empty(); + handle.hwnd = window_handle.hwnd; + handle.hinstance = window_handle.hinstance; + raw_window_handle_04::RawWindowHandle::Win32(handle) + }, + (RawWindowHandle::WinRt(window_handle), _) => { + let mut handle = WinRtHandle::empty(); + handle.core_window = window_handle.core_window; + raw_window_handle_04::RawWindowHandle::WinRt(handle) + }, + (RawWindowHandle::Web(window_handle), _) => { + let mut handle = WebHandle::empty(); + handle.id = window_handle.id; + raw_window_handle_04::RawWindowHandle::Web(handle) + }, + (RawWindowHandle::AndroidNdk(window_handle), _) => { + let mut handle = AndroidNdkHandle::empty(); + handle.a_native_window = window_handle.a_native_window; + raw_window_handle_04::RawWindowHandle::AndroidNdk(handle) + }, + (RawWindowHandle::Haiku(window_handle), _) => { + let mut handle = HaikuHandle::empty(); + handle.b_window = window_handle.b_window; + handle.b_direct_window = window_handle.b_direct_window; + raw_window_handle_04::RawWindowHandle::Haiku(handle) + }, + _ => panic!("No HasRawWindowHandle version 0.4 backwards compatibility for new Winit window type"), + } + } +} + +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + /// Describes the appearance of the mouse cursor. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1087,7 +1268,7 @@ impl Default for CursorIcon { } /// Fullscreen modes. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Fullscreen { Exclusive(VideoMode), @@ -1095,7 +1276,7 @@ pub enum Fullscreen { Borderless(Option), } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Theme { Light, Dark, @@ -1103,14 +1284,19 @@ pub enum Theme { /// ## Platform-specific /// -/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between `Critical` and `Informational`. -#[derive(Debug, Clone, Copy, PartialEq)] +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UserAttentionType { /// ## Platform-specific + /// /// - **macOS:** Bounces the dock icon until the application is in focus. /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. Critical, /// ## Platform-specific + /// /// - **macOS:** Bounces the dock icon once. /// - **Windows:** Flashes the taskbar button until the application is in focus. Informational,