From 5a72b99ead31b9bf425d566272576d0f7bcceb81 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Wed, 20 Apr 2022 01:56:56 +0300 Subject: [PATCH] Use sctk-adwaita 0.5.1 auto theme selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify `with_app_id` and `with_class` methods Both APIs are used to set application name. This commit unifies the API between Wayland and X11, so downstream applications can remove platform specific code when using `WindowBuilderExtUnix`. Fixes #1739. Unify behavior of `resizable` across platforms This makes X11 and Wayland follow Windows and macOS, so the size of the window could be set even though it has resizable attribute set to false. Fixes #2242. Fix assigning the wrong monitor when receiving Windows move events (#2266) Fix embedded NULs in C wide strings returned from Windows API (#2264) On Wayland, fix hiding cursors on GNOME `wl_pointer::set_cursor` expects a serial number of the last `wl_pointer::enter` event. However other calls expect latest observed pointer serial, so this commit tracks both and use them as required by specification. Fixes #2273. Bump windows-sys version to 0.36 (#2277) Add new `Ime` event for desktop platforms This commit brings new Ime event to account for preedit state of input method, also adding `Window::set_ime_allowed` to toggle IME input on the particular window. This commit implements API as designed in #1497 for desktop platforms. On Wayland, provide option for better CSD While most compositors provide server side decorations, the GNOME does not, and won't provide them. Also Wayland clients must render client side decorations. Winit was already drawing some decorations, however they were bad looking and provided no text rendering, so the title was missing. However this commit makes use of the SCTK external frame similar to GTK's Adwaita theme supporting text rendering and looking similar to other GTK applications. Fixes #1967. Fix warnings on nightly rust (#2295) This was causing CI to fail: https://github.com/rust-windowing/winit/runs/6506026326 On macOS, emit resize event on `frame_did_change` When the window switches mode from normal to tabbed one, it doesn't get resized, however the frame gets resized. This commit makes winit to track resizes when frame changes instead of window. Fixes #2191. Reorganize `EventLoopBuilder::build()` platform documentation Since there's a "Platform-specific" header, it makes sense to put the Linux-specific part under it. On the other hand, "Can only be called on the main thread." is true for all platforms, not just iOS, so there is no reason to call it out for iOS specifically. [Windows] Avoid GetModuleHandle(NULL) (#2301) Use get_instance_handle() over GetModuleHandle(NULL) On Windows, fix reported cursor position. (#2311) When clicking and moving the cursor out of the window negative coordinates were not handled correctly. Revert "On Wayland, fix resize not propagating properly" This reverts commit 78e5a395dac9604e851f781b6810247ead7928e3. It was discovered that in some cases mesa will lock the back buffer, e.g. when making context current, leading to resize missing. Given that applications can restructure their rendering to account for that, and that winit isn't limited to playing nice with mesa reverting the original commit. Set `WindowBuilder` to must_use Add X11 opt-in function for device events Previously on X11, by default all global events were broadcasted to every winit application. This unnecessarily drains battery due to excessive CPU usage when moving the mouse. To resolve this, device events are now ignored by default and users must manually opt into it using `EventLoopWindowTarget::set_filter_device_events`. Fixes (#1634) on Linux. Prevent null dereference on X11 with bad locale Remove old dialog fix that is superseded by #2027 (#2292) This fixes the run_return loop never returning on macos when using multiple windows Migrate from lazy_static to once_cell macOS: Emit LoopDestroyed on CMD+Q (#2073) override applicationWillTerminate: On Android, use `HasRawWindowHandle` directly from the `ndk` crate (#2318) The `ndk` crate now implements [`HasRawWindowHandle` directly on `NativeWindow`], relieving the burden to reimplement it on `winit`. [`HasRawWindowHandle` directly on `NativeWindow`]: https://github.com/rust-windowing/android-ndk-rs/pull/274 Run clippy on CI Fixes #1402. Make `set_device_event_filter` non-mut Commit f10a984 added `EventLoopWindowTarget::set_device_event_filter` with for a mutable reference, however most winit APIs work with immutable references, so altering API to play nicely with existing APIs. This also disables device event filtering on debug example. Make `WindowAttributes` private (#2134) * Make `WindowAttributes` private, and move its documentation * Reorder WindowAttributes title and fullscreen to match method order Build docs on `docs.rs` for iOS and Android as well (#2324) Remove core-video-sys dependency (#2326) Hasn't been updated in over 2 years - many open PRs, seems abandoned. Is the cause of several duplicate dependencies in our dependency tree! Fix macOS 32bit (#2327) Documentation cleanup (#2328) * Remove redundant documentation links * Add note to README about windows not showing up on Wayland * Fix documentation links * Small documentation fixes * Add note about doing stuff after StartCause::Init on macOS Add `WindowBuilder::transparent` This is required to help hardware accelerated libraries like glutin that accept WindowBuilder instead of RawWindowHandle, since the api to access builder properties directly was removed. Follow up to 44288f6. Refine `Window::set_cursor_grab` API This commit renames `Window::set_cursor_grab` to `Window::set_cursor_grab_mode`. The new API now accepts enumeration to control the way cursor grab is performed. The value could be: `lock`, `confine`, or `none`. This commit also implements `Window::set_cursor_position` for Wayland, since it's tied to locked cursor. Implements API from #1677. examples/window_run_return: Enable on Android (#2321) Android also supports `EventLoopExtRunReturn`. The user will still have to follow the README to turn this example into a `cdylib` and add the `ndk_glue::main()` initialization attribute, though. Fix doubled device events on X11 Fixes #2332 macOS: disallow_highdpi will set explicity the value to avoid the SO value by default (#2339) ci: Disallow warnings in rustdoc and test private items (#2341) Make sure `cargo doc` runs cleanly without any warnings in the CI - some recently introduced but still allowing a PR to get merged. In case someone wishes to add docs on private items, make sure those adhere to the same standards. Bump smithay-client-toolkit to v0.16.0 Disallow multiple EventLoop creation Fix conflict in `WindowFlags` on Windows Map XK_Caps_Lock to VirtualKeyCode::Capital (#1864) This allows applications to handle events for the caps lock key under X11 Less redundancy and improve fullscreen in examples Remove examples/minimize which is redundant Implement From for WindowId and vise-versa This should help downstream applications to expose WindowId to the end users via e.g. IPC to control particular windows in multi window systems. examples/multiwindow.rs: ignore synthetic key press events Fix infinite recursion in `WindowId` conversion methods Add 'WindowEvent::Occluded(bool)' This commits and an event to track window occlusion state, which could help optimize rendering downstream. Add `refresh_rate_millihertz` for `MonitorHandle` This also alters `VideoMode::refresh_rate` to `VideoMode::refresh_rate_millihertz` which now returns monitor refresh rate in mHz. On Wayland send Focused(false) for new window On Wayland winit will always get an explicit focused event from the system and will transfer it downstream. So send focused false to enforce it. On Wayland, drop wl_surface on window close web: Manually emit focused event on mouse click (#2202) * Manually emit focused event on mouse click * Update CHANGELOG.md Co-authored-by: Markus Røyset web: Add `EventLoop::spawn` (#2208) * web: Add `EventLoop::spawn` This is the same as `EventLoop::run`, but doesn't throw an exception in order to return `!`. I decided to name it `spawn` rather than `run_web` because I think that's more descriptive, but I'm happy to change it to `run_web`. Resolves #1714 * Update src/platform/web.rs * Fix outdated names Co-authored-by: Markus Røyset Fix changelog entry for `EventLoopExtWebSys` (#2372) android: Hold `NativeWindow` lock until after notifying the user with `Event::Suspended` (#2307) This applies https://github.com/rust-windowing/android-ndk-rs/issues/117 on the `winit` side: Android destroys its window/surface as soon as the user returns from [`onNativeWindowDestroyed`], and we "fixed" this on the `ndk-glue` side by sending the `WindowDestroyed` event before locking the window and removing it: this lock has to wait for any user of `ndk-glue` - ie. `winit` - to give up its readlock on the window, which is what we utilize here to give users of `winit` "time" to destroy any resource created on top of a `RawWindowHandle`. since we can't pass the user a `RawWindowHandle` through the `HasRawWindowHandle` trait we have to document this case explicitly and keep the lock alive on the `winit` side instead. [`onNativeWindowDestroyed`]: https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onnativewindowdestroyed web: add `with_prevent_default`, `with_focusable` (#2365) * web: add `with_prevent_default`, `with_focusable` `with_prevent_default` controls whether `event.preventDefault` is called `with_focusable` controls whether `tabindex` is added Fixes #1768 * Remove extra space from CHANGELOG windows: Use correct value for mouse wheel delta (#2374) Make winit focus take activity into account on Windows (#2159) winit's notion of "focus" is very simple; you're either focused or not. However, Windows has both notions of focused window and active window and paying attention only to WM_SETFOCUS/WM_KILLFOCUS can cause a window to believe the user is interacting with it when they're not. (this manifests when a user switches to another application between when a winit application starts and it creates its first window) Fix typos (#2375) Bump sctk-adwaita to 0.4.1 This should force the use of system libraries for Fontconfig and freetype instead of building them with cmake if missing. This also fixes compilation failures on nightly. Fixes #2373. Tidy up "platform-specifc" doc sections (#2356) * Tidy up "platform-specific" doc sections * Unrelated grammatical fix * Subjective improvements Android: avoid deadlocks while handling UserEvent (#2343) Replace `Arc>` by `mpsc` Update raw-window-handle to v0.5.0 This updates raw-window-handle to v0.5.0. On macOS, fix confirmed character inserted When confirming input in e.g. Korean IME or using characters like `+` winit was sending those twice, once via `Ime::Commit` and the other one via `ReceivedCharacter`, since those events weren't generating any `Ime::Preedit` and were forwarded due to `do_command_by_selector`. Add method to hook xlib error handler This should help glutin to handle errors coming from GLX and offer multithreading support in a safe way. Fixes #2378. Windows: apply skip taskbar state when taskbar is restarted (#2380) Fix hiding a maximized window On Windows (#2336) Bump `ndk` and `ndk-glue` dependencies to stable `0.7.0` release (#2392) Fix type hint reference for xlib hook Consistently deliver a Resumed event on all platforms To be more consistent with mobile platforms this updates the Windows, macOS, Wayland, X11 and Web backends to all emit a Resumed event immediately after the initial `NewEvents(StartCause::Init)` event. The documentation for Suspended and Resumed has also been updated to provide general recommendations for how to handle Suspended and Resumed events in portable applications as well as providing Android and iOS specific details. This consistency makes it possible to write applications that lazily initialize their graphics state when the application resumes without any platform-specific knowledge. Previously, applications that wanted to run on Android and other systems would have to maintain two, mutually-exclusive, initialization paths. Note: This patch does nothing to guarantee that Suspended events will be delivered. It's still reasonable to say that most OSs without a formal lifecycle for applications will simply never "suspend" your application. There are currently no known portability issues caused by not delivering `Suspended` events consistently and technically it's not possible to guarantee the delivery of `Suspended` events if the OS doesn't define an application lifecycle. (app can always be terminated without any kind of clean up notification on most non-mobile OSs) Fixes #2185. ci: manually point ANDROID_NDK_ROOT to latest supplied version It seems the symlink to `ndk-bundle` and this environment variable pointing to it have been removed to prevent the sdkmanager from failing, when finding the SDK setup to be in an "indeterminate" state. It is now up to the users themselves to install an NDK through that tool or point the right variables to a preinstalled "latest" NDK. https://github.com/actions/virtual-environments/issues/2689 https://github.com/actions/virtual-environments/pull/5926 Fix changelog entry wrt scrolling The breaking change was put into the wrong release section. Release 0.27.0 version Explicitly specify minimum supported rust version This should help with distributing apps using winit. Fixes #1075. On X11, fix crash when can't disable IME Fixes #2402. Release 0.27.1 version Windows: respect min/max sizes when creating the window (#2393) On X11, fix window hints not persisting This commit fixes the issue with min, max, and resize increments not persisting across the dpi changes. Fix tracking of phase changes for mousewheel on trackpad (#2158) On Windows, add opt-in function for device events (#2409) Add CODEOWNERS file (#2420) * Add CODEOWNERS file This makes it very clear when you're stepping down from the post as a maintainer, and makes it clear for users who is expected to review their PR * Fix grammar * Make @kchibisov receive pings for the X11 platform * Fix typo Implement version 0.4 of the HasRawWindowHandle trait This makes Winit 0.27 compatible with crates like Wgpu 0.13 that are using the raw_window_handle v0.4 crate and aren't able to upgrade to 0.5 until they do a new release (since it requires a semver change). The change is intended to be self-contained (instead of pushing the details into all the platform_impl backends) since this is only intended to be a temporary trait implementation for backwards compatibility that will likely be removed before the next Winit release. Fixes #2415. Fix missleading breaking change on Windows The applications should not rely on not-implemented behavior and should use the right functions for that. Remove redundant steps from CI Tests are already building the entire crate, so no need for a separate builds slowing down the CI. On Wayland, fix `Window::request_redraw` being delayed On Waylnad when asking for redraw before `MainEventsCleared` would result for redraw being send on the next event loop tick, which is not expectable given that it must be delivered on the same event loop tick. Release 0.27.2 version On Windows, improve support for undecorated windows (#2419) Add touchpad magnify and rotate gestures support for macOS (#2157) * Add touchpad magnify support for macOS * Add touchpad rotate support for macOS * Add macOS rotate and magnify gesture cancelled phases * Correct docs for TouchpadRotate event * Fix tracing macros Document `WindowEvent::Moved` as unsupported on Wayland Update `sctk-adwaita` to use `ab_glyph` The crossfont will still be available under the option. Mark new events as breaking change Adding a new enum variant is a breaking change in winit. Co-Authored-By: kas Co-Authored-By: Artur Kovacs Co-Authored-By: Markus Siglreithmaier Co-Authored-By: Murarth Co-Authored-By: Yusuke Kominami Co-Authored-By: moko256 Co-Authored-By: Mads Marquart Co-Authored-By: Markus Røyset Co-Authored-By: Marijn Suijten Co-Authored-By: Kirill Chibisov --- .github/CODEOWNERS | 35 + .github/workflows/ci.yml | 26 +- CHANGELOG.md | 67 +- CONTRIBUTING.md | 6 +- Cargo.toml | 51 +- FEATURES.md | 10 +- HALL_OF_CHAMPIONS.md | 3 + README.md | 37 +- examples/control_flow.rs | 2 + examples/cursor.rs | 2 + examples/cursor_grab.rs | 26 +- examples/custom_events.rs | 2 + examples/drag_window.rs | 2 + examples/fullscreen.rs | 141 ++-- examples/handling_close.rs | 2 + examples/ime.rs | 99 +++ examples/min_max_size.rs | 30 - examples/minimize.rs | 41 - examples/monitor_list.rs | 2 + examples/mouse_wheel.rs | 2 + examples/multithreaded.rs | 20 +- examples/multiwindow.rs | 10 +- examples/request_redraw.rs | 2 + examples/request_redraw_threaded.rs | 10 +- examples/resizable.rs | 6 +- examples/set_ime_position.rs | 53 -- examples/timer.rs | 2 + examples/touchpad_gestures.rs | 43 + examples/transparent.rs | 2 + examples/video_modes.rs | 2 + examples/web.rs | 3 + examples/window.rs | 2 + examples/window_debug.rs | 108 +-- examples/window_icon.rs | 3 +- examples/window_run_return.rs | 7 +- src/dpi.rs | 49 +- src/event.rs | 321 +++++++- src/event_loop.rs | 179 ++-- src/icon.rs | 8 +- src/lib.rs | 13 +- src/monitor.rs | 44 +- src/platform/android.rs | 8 +- src/platform/macos.rs | 26 +- src/platform/mod.rs | 2 +- src/platform/run_return.rs | 11 +- src/platform/unix.rs | 141 +++- src/platform/web.rs | 66 +- src/platform/windows.rs | 22 + src/platform_impl/android/mod.rs | 182 ++-- src/platform_impl/ios/app_state.rs | 104 ++- src/platform_impl/ios/event_loop.rs | 8 +- src/platform_impl/ios/ffi.rs | 17 +- src/platform_impl/ios/mod.rs | 7 +- src/platform_impl/ios/monitor.rs | 62 +- src/platform_impl/ios/view.rs | 48 +- src/platform_impl/ios/window.rs | 91 +- src/platform_impl/linux/mod.rs | 148 ++-- src/platform_impl/linux/wayland/env.rs | 10 +- .../linux/wayland/event_loop/mod.rs | 180 ++-- .../linux/wayland/event_loop/sink.rs | 4 +- .../linux/wayland/event_loop/state.rs | 10 +- src/platform_impl/linux/wayland/mod.rs | 12 +- src/platform_impl/linux/wayland/output.rs | 20 +- .../linux/wayland/seat/keyboard/mod.rs | 25 +- .../linux/wayland/seat/pointer/data.rs | 12 +- .../linux/wayland/seat/pointer/handlers.rs | 21 +- .../linux/wayland/seat/pointer/mod.rs | 96 ++- .../linux/wayland/seat/text_input/handlers.rs | 49 +- .../linux/wayland/seat/text_input/mod.rs | 27 +- src/platform_impl/linux/wayland/window/mod.rs | 193 ++++- .../linux/wayland/window/shim.rs | 248 ++++-- .../linux/x11/event_processor.rs | 112 ++- src/platform_impl/linux/x11/events.rs | 2 +- src/platform_impl/linux/x11/ime/callbacks.rs | 13 +- src/platform_impl/linux/x11/ime/context.rs | 338 ++++++-- src/platform_impl/linux/x11/ime/inner.rs | 9 +- .../linux/x11/ime/input_method.rs | 11 +- src/platform_impl/linux/x11/ime/mod.rs | 95 ++- src/platform_impl/linux/x11/mod.rs | 152 ++-- src/platform_impl/linux/x11/monitor.rs | 46 +- src/platform_impl/linux/x11/util/atom.rs | 5 +- src/platform_impl/linux/x11/util/geometry.rs | 12 +- src/platform_impl/linux/x11/util/hint.rs | 8 +- src/platform_impl/linux/x11/util/icon.rs | 2 + src/platform_impl/linux/x11/util/randr.rs | 25 +- .../linux/x11/util/window_property.rs | 2 +- src/platform_impl/linux/x11/util/wm.rs | 12 +- src/platform_impl/linux/x11/window.rs | 191 +++-- src/platform_impl/macos/app.rs | 21 +- src/platform_impl/macos/app_delegate.rs | 52 +- src/platform_impl/macos/app_state.rs | 45 +- src/platform_impl/macos/event_loop.rs | 16 +- src/platform_impl/macos/ffi.rs | 42 + src/platform_impl/macos/mod.rs | 3 +- src/platform_impl/macos/monitor.rs | 48 +- src/platform_impl/macos/observer.rs | 3 +- src/platform_impl/macos/util/async.rs | 8 +- src/platform_impl/macos/util/mod.rs | 35 +- src/platform_impl/macos/view.rs | 775 +++++++++++------- src/platform_impl/macos/window.rs | 127 ++- src/platform_impl/macos/window_delegate.rs | 237 +++--- src/platform_impl/web/event_loop/mod.rs | 27 +- src/platform_impl/web/event_loop/runner.rs | 14 +- src/platform_impl/web/event_loop/state.rs | 5 +- .../web/event_loop/window_target.rs | 134 +-- src/platform_impl/web/monitor.rs | 8 +- src/platform_impl/web/web_sys/canvas.rs | 68 +- .../web/web_sys/canvas/mouse_handler.rs | 8 +- .../web/web_sys/media_query_handle.rs | 2 +- src/platform_impl/web/web_sys/mod.rs | 2 +- src/platform_impl/web/web_sys/scaling.rs | 4 +- src/platform_impl/web/web_sys/timeout.rs | 4 +- src/platform_impl/web/window.rs | 72 +- src/platform_impl/windows/dark_mode.rs | 103 ++- src/platform_impl/windows/drop_handler.rs | 2 +- src/platform_impl/windows/event_loop.rs | 643 +++++++++------ .../windows/event_loop/runner.rs | 22 +- src/platform_impl/windows/icon.rs | 5 +- src/platform_impl/windows/ime.rs | 149 ++++ src/platform_impl/windows/mod.rs | 29 +- src/platform_impl/windows/monitor.rs | 43 +- src/platform_impl/windows/raw_input.rs | 27 +- src/platform_impl/windows/util.rs | 201 ++--- src/platform_impl/windows/window.rs | 195 +++-- src/platform_impl/windows/window_state.rs | 195 +++-- src/window.rs | 534 ++++++++---- 126 files changed, 5546 insertions(+), 2720 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 examples/ime.rs delete mode 100644 examples/min_max_size.rs delete mode 100644 examples/minimize.rs delete mode 100644 examples/set_ime_position.rs create mode 100644 examples/touchpad_gestures.rs create mode 100644 src/platform_impl/windows/ime.rs 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,