diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efffef9d38..6cf95605c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,146 +6,182 @@ on: branches: [master] jobs: - Check_Formatting: + fmt: + name: Check formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: hecrj/setup-rust-action@v1 + - uses: dtolnay/rust-toolchain@stable with: - rust-version: stable components: rustfmt - name: Check Formatting - run: cargo +stable fmt --all -- --check - - cargo-deny: - name: cargo-deny - - # TODO: remove this matrix when https://github.com/EmbarkStudios/cargo-deny/issues/324 is resolved - strategy: - fail-fast: false - matrix: - platform: - - aarch64-apple-ios - - aarch64-linux-android - - i686-pc-windows-gnu - - i686-pc-windows-msvc - - i686-unknown-linux-gnu - - wasm32-unknown-unknown - - x86_64-apple-darwin - - x86_64-apple-ios - - x86_64-pc-windows-gnu - - x86_64-pc-windows-msvc - - x86_64-unknown-linux-gnu - - x86_64-unknown-redox - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: EmbarkStudios/cargo-deny-action@v1 - with: - command: check - log-level: error - arguments: --all-features --target ${{ matrix.platform }} + run: cargo fmt -- --check tests: - name: Tests + name: Test ${{ matrix.toolchain }} ${{ matrix.platform.name }} + runs-on: ${{ matrix.platform.os }} + strategy: fail-fast: false matrix: - rust_version: ['1.64.0', stable, nightly] + toolchain: [stable, nightly, '1.65.0'] 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 } - - { target: i686-pc-windows-gnu, os: windows-latest, host: -i686-pc-windows-gnu } - - { target: i686-unknown-linux-gnu, os: ubuntu-latest, } - - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, } - - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: x11 } - - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "wayland,wayland-dlopen" } - - { target: aarch64-linux-android, os: ubuntu-latest, options: -p winit, cmd: 'apk --', features: "android-native-activity" } - - { target: x86_64-unknown-redox, os: ubuntu-latest, } - - { target: x86_64-apple-darwin, os: macos-latest, } - - { target: x86_64-apple-ios, os: macos-latest, } - - { target: aarch64-apple-ios, os: macos-latest, } - # We're using Windows rather than Ubuntu to run the wasm tests because caching cargo-web - # doesn't currently work on Linux. - - { target: wasm32-unknown-unknown, os: windows-latest, } + - { name: 'Windows 64bit MSVC', target: x86_64-pc-windows-msvc, os: windows-latest, } + - { name: 'Windows 32bit MSVC', target: i686-pc-windows-msvc, os: windows-latest, } + - { name: 'Windows 64bit GNU', target: x86_64-pc-windows-gnu, os: windows-latest, host: -x86_64-pc-windows-gnu } + - { name: 'Windows 32bit GNU', target: i686-pc-windows-gnu, os: windows-latest, host: -i686-pc-windows-gnu } + - { name: 'Linux 32bit', target: i686-unknown-linux-gnu, os: ubuntu-latest, } + - { name: 'Linux 64bit', target: x86_64-unknown-linux-gnu, os: ubuntu-latest, } + - { name: 'X11', target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: '--no-default-features --features=x11' } + - { name: 'Wayland', target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: '--no-default-features --features=wayland,wayland-dlopen' } + - { name: 'Android', target: aarch64-linux-android, os: ubuntu-latest, options: '--package=winit --features=android-native-activity', cmd: 'apk --' } + - { name: 'Redox OS', target: x86_64-unknown-redox, os: ubuntu-latest, } + - { name: 'macOS', target: x86_64-apple-darwin, os: macos-latest, } + - { name: 'iOS x86_64', target: x86_64-apple-ios, os: macos-latest, } + - { name: 'iOS Aarch64', target: aarch64-apple-ios, os: macos-latest, } + - { name: 'web', target: wasm32-unknown-unknown, os: ubuntu-latest, } + exclude: + # Android is tested on stable-3 + - toolchain: '1.65.0' + platform: { name: 'Android', target: aarch64-linux-android, os: ubuntu-latest, options: '--package=winit --features=android-native-activity', cmd: 'apk --' } + include: + - toolchain: '1.69.0' + platform: { name: 'Android', target: aarch64-linux-android, os: ubuntu-latest, options: '--package=winit --features=android-native-activity', cmd: 'apk --' } env: + # Set more verbose terminal output + CARGO_TERM_VERBOSE: true RUST_BACKTRACE: 1 - CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-C debuginfo=0 --deny warnings" - OPTIONS: ${{ matrix.platform.options }} - FEATURES: ${{ format(',{0}', matrix.platform.features ) }} + + # Faster compilation and error on warnings + RUSTFLAGS: '--codegen=debuginfo=0 --deny=warnings' + RUSTDOCFLAGS: '--deny=warnings' + + OPTIONS: --target=${{ matrix.platform.target }} ${{ matrix.platform.options }} CMD: ${{ matrix.platform.cmd }} - RUSTDOCFLAGS: -Dwarnings - runs-on: ${{ matrix.platform.os }} steps: - uses: actions/checkout@v3 - # Used to cache cargo-web - - name: Cache cargo folder - uses: actions/cache@v1 - with: - path: ~/.cargo - key: ${{ matrix.platform.target }}-cargo-${{ matrix.rust_version }} - - uses: hecrj/setup-rust-action@v1 + - name: Restore cache of cargo folder + # We use `restore` and later `save`, so that we can create the key after + # the cache has been downloaded. + # + # This could be avoided if we added Cargo.lock to the repository. + uses: actions/cache/restore@v3 with: - rust-version: ${{ matrix.rust_version }}${{ matrix.platform.host }} - targets: ${{ matrix.platform.target }} - components: clippy + # https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-${{ matrix.toolchain }}-${{ matrix.platform.name }}-never-intended-to-be-found + restore-keys: cargo-${{ matrix.toolchain }}-${{ matrix.platform.name }} + + - name: Generate lockfile + # Also updates the crates.io index + run: cargo generate-lockfile && cargo update -p ahash --precise 0.8.7 && cargo update -p bumpalo --precise 3.14.0 - 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 - - name: Install cargo-apk + + - name: Cache cargo-apk if: contains(matrix.platform.target, 'android') - run: cargo install cargo-apk + id: cargo-apk-cache + uses: actions/cache@v3 + with: + path: ~/.cargo/bin/cargo-apk + # Change this key if we update the required cargo-apk version + key: cargo-apk-v0-9-7 + + - uses: dtolnay/rust-toolchain@master + if: contains(matrix.platform.target, 'android') && (steps.cargo-apk-cache.outputs.cache-hit != 'true') + with: + toolchain: stable + + - name: Install cargo-apk + if: contains(matrix.platform.target, 'android') && (steps.cargo-apk-cache.outputs.cache-hit != 'true') + run: cargo install cargo-apk --version=^0.9.7 --locked + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }}${{ matrix.platform.host }} + targets: ${{ matrix.platform.target }} + components: clippy - name: Check documentation - shell: bash - run: cargo doc --no-deps --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES --document-private-items + run: cargo doc --no-deps $OPTIONS --document-private-items - name: Build crate - shell: bash - run: cargo $CMD build --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + run: cargo $CMD build $OPTIONS - name: Build tests - shell: bash if: > !contains(matrix.platform.target, 'redox') && - matrix.rust_version != '1.64.0' - run: cargo $CMD test --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + matrix.toolchain != '1.65.0' + run: cargo $CMD test --no-run $OPTIONS - name: Run tests - shell: bash if: > !contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios') && !contains(matrix.platform.target, 'wasm32') && !contains(matrix.platform.target, 'redox') && - matrix.rust_version != '1.64.0' - run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + matrix.toolchain != '1.65.0' + run: cargo $CMD test $OPTIONS - name: Lint with clippy - shell: bash - if: (matrix.rust_version == 'stable') && !contains(matrix.platform.options, '--no-default-features') - run: cargo clippy --all-targets --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES -- -Dwarnings + if: (matrix.toolchain == 'stable') && !contains(matrix.platform.options, '--no-default-features') + run: cargo clippy --all-targets $OPTIONS -- -Dwarnings - name: Build tests with serde enabled - shell: bash if: > !contains(matrix.platform.target, 'redox') && - matrix.rust_version != '1.64.0' - run: cargo $CMD test --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES + matrix.toolchain != '1.65.0' + run: cargo $CMD test --no-run $OPTIONS --features serde + - name: Run tests with serde enabled - shell: bash if: > !contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios') && !contains(matrix.platform.target, 'wasm32') && !contains(matrix.platform.target, 'redox') && - matrix.rust_version != '1.64.0' - run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES + matrix.toolchain != '1.65.0' + run: cargo $CMD test $OPTIONS --features serde + + # See restore step above + - name: Save cache of cargo folder + uses: actions/cache/save@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-${{ matrix.toolchain }}-${{ matrix.platform.name }}-${{ hashFiles('Cargo.lock') }} + + cargo-deny: + name: Run cargo-deny on ${{ matrix.platform.name }} + runs-on: ubuntu-latest + + # TODO: remove this matrix when https://github.com/EmbarkStudios/cargo-deny/issues/324 is resolved + strategy: + fail-fast: false + matrix: + platform: + - { name: 'Android', target: aarch64-linux-android } + - { name: 'iOS', target: aarch64-apple-ios } + - { name: 'Linux', target: x86_64-unknown-linux-gnu } + - { name: 'macOS', target: x86_64-apple-darwin } + - { name: 'Redox OS', target: x86_64-unknown-redox } + - { name: 'web', target: wasm32-unknown-unknown } + - { name: 'Windows', target: x86_64-pc-windows-gnu } + + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check + log-level: error + arguments: --all-features --target ${{ matrix.platform.target }} diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 9656dc7882..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "deps/apk-builder"] - path = deps/apk-builder - url = https://github.com/rust-windowing/android-rs-glue diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d64788ee6..2a9288f24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,177 @@ All notable changes to this project will be documented in this file. -Please keep one empty line before and after all headers. (This is required for `git` to produce a conflict when a release is made while a PR is open and the PR's changelog entry would go into the wrong section). +Please keep one empty line before and after all headers. (This is required for +`git` to produce a conflict when a release is made while a PR is open and the +PR's changelog entry would go into the wrong section). -And please only add new entries to the top of this list, right below the `# Unreleased` header. +And please only add new entries to the top of this list, right below the `# +Unreleased` header. # Unreleased -- On Web, allow event loops to be recreated with `spawn`. -- **Breaking:** Rename `Window::set_ime_position` to `Window::set_ime_cursor_area` adding a way to set exclusive zone. -- On Android, changed default behavior of Android to ignore volume keys letting the operating system handle them. -- On Android, added `EventLoopBuilderExtAndroid::handle_volume_keys` to indicate that the application will handle the volume keys manually. +# 0.29.15 + +- On X11, fix crash due to xsettings query on systems with incomplete xsettings. + +# 0.29.14 + +- On X11/Wayland, fix `text` and `text_with_all_modifiers` not being `None` during compose. +- On Wayland, don't reapply cursor grab when unchanged. +- On X11, fix a bug where some mouse events would be unexpectedly filtered out. + +# 0.29.13 + +- On Web, fix possible crash with `ControlFlow::Wait` and `ControlFlow::WaitUntil`. + +# 0.29.12 + +- On X11, fix use after free during xinput2 handling. +- On X11, filter close to zero values in mouse device events + +# 0.29.11 + +- On Wayland, fix DeviceEvent::Motion not being sent +- On X11, don't require XIM to run. +- On X11, fix xkb state not being updated correctly sometimes leading to wrong input. +- Fix compatibility with 32-bit platforms without 64-bit atomics. +- On macOS, fix incorrect IME cursor rect origin. +- On X11, fix swapped instance and general class names. +- On Windows, fixed a race condition when sending an event through the loop proxy. +- On Wayland, disable `Occluded` event handling. +- On X11, reload dpi on `_XSETTINGS_SETTINGS` update. +- On X11, fix deadlock when adjusting DPI and resizing at the same time. +- On Wayland, fix `Focused(false)` being send when other seats still have window focused. +- On Wayland, fix `Window::set_{min,max}_inner_size` not always applied. +- On Windows, fix inconsistent resizing behavior with multi-monitor setups when repositioning outside the event loop. +- On Wayland, fix `WAYLAND_SOCKET` not used when detecting platform. +- On Orbital, fix `logical_key` and `text` not reported in `KeyEvent`. +- On Orbital, implement `KeyEventExtModifierSupplement`. +- On Orbital, map keys to `NamedKey` when possible. +- On Orbital, implement `set_cursor_grab`. +- On Orbital, implement `set_cursor_visible`. +- On Orbital, implement `drag_window`. +- On Orbital, implement `drag_resize_window`. +- On Orbital, implement `set_transparent`. +- On Orbital, implement `set_visible`. +- On Orbital, implement `is_visible`. +- On Orbital, implement `set_resizable`. +- On Orbital, implement `is_resizable`. +- On Orbital, implement `set_maximized`. +- On Orbital, implement `is_maximized`. +- On Orbital, implement `set_decorations`. +- On Orbital, implement `is_decorated`. +- On Orbital, implement `set_window_level`. +- On Orbital, emit `DeviceEvent::MouseMotion`. +- On Wayland, fix title in CSD not updated from `AboutToWait`. + +# 0.29.10 + +- On Web, account for canvas being focused already before event loop starts. +- On Web, increase cursor position accuracy. + +# 0.29.9 + +- On X11, fix `NotSupported` error not propagated when creating event loop. +- On Wayland, fix resize not issued when scale changes +- On X11 and Wayland, fix arrow up on keypad reported as `ArrowLeft`. +- On macOS, report correct logical key when Ctrl or Cmd is pressed. + +# 0.29.8 + +- On X11, fix IME input lagging behind. +- On X11, fix `ModifiersChanged` not sent from xdotool-like input +- On X11, fix keymap not updated from xmodmap. +- On X11, reduce the amount of time spent fetching screen resources. +- On Wayland, fix `Window::request_inner_size` being overwritten by resize. +- On Wayland, fix `Window::inner_size` not using the correct rounding. + +# 0.29.7 + +- On X11, fix `Xft.dpi` reload during runtime. +- On X11, fix window minimize. + +# 0.29.6 + +- On Web, fix context menu not being disabled by `with_prevent_default(true)`. +- On Wayland, fix `WindowEvent::Destroyed` not being delivered after destroying window. +- Fix `EventLoopExtRunOnDemand::run_on_demand` not working for consequent invocation + +# 0.29.5 + +- On macOS, remove spurious error logging when handling `Fn`. +- On X11, fix an issue where floating point data from the server is + misinterpreted during a drag and drop operation. +- On X11, fix a bug where focusing the window would panic. +- On macOS, fix `refresh_rate_millihertz`. +- On Wayland, disable Client Side Decorations when `wl_subcompositor` is not supported. +- On X11, fix `Xft.dpi` detection from Xresources. +- On Windows, fix consecutive calls to `window.set_fullscreen(Some(Fullscreen::Borderless(None)))` resulting in losing previous window state when eventually exiting fullscreen using `window.set_fullscreen(None)`. +- On Wayland, fix resize being sent on focus change. +- On Windows, fix `set_ime_cursor_area`. + +# 0.29.4 + +- Fix crash when running iOS app on macOS. +- On X11, check common alternative cursor names when loading cursor. +- On X11, reload the DPI after a property change event. +- On Windows, fix so `drag_window` and `drag_resize_window` can be called from another thread. +- On Windows, fix `set_control_flow` in `AboutToWait` not being taken in account. +- On macOS, send a `Resized` event after each `ScaleFactorChanged` event. +- On Wayland, fix `wl_surface` being destroyed before associated objects. +- On macOS, fix assertion when pressing `Fn` key. + +# 0.29.3 + +- On Wayland, apply correct scale to `PhysicalSize` passed in `WindowBuilder::with_inner_size` when possible. +- On Wayland, fix `RedrawRequsted` being always sent without decorations and `sctk-adwaita` feature. +- On Wayland, ignore resize requests when the window is fully tiled. +- On Wayland, use `configure_bounds` to constrain `with_inner_size` when compositor wants users to pick size. +- On Windows, fix deadlock when accessing the state during `Cursor{Enter,Leave}`. +- On Windows, add support for `Window::set_transparent`. +- On macOS, fix deadlock when entering a nested event loop from an event handler. +- On macOS, add support for `Window::set_blur`. + +# 0.29.2 + +- **Breaking:** Bump MSRV from `1.60` to `1.65`. +- **Breaking:** Add `Event::MemoryWarning`; implemented on iOS/Android. +- **Breaking:** Bump `ndk` version to `0.8.0`, ndk-sys to `0.5.0`, `android-activity` to `0.5.0`. +- **Breaking:** Change default `ControlFlow` from `Poll` to `Wait`. +- **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. +- **Breaking:** Moved `ControlFlow::Exit` to `EventLoopWindowTarget::exit()` and `EventLoopWindowTarget::exiting()` and removed `ControlFlow::ExitWithCode(_)` entirely. +- **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`. +- **Breaking:** `EventLoop::new` and `EventLoopBuilder::build` now return `Result` +- **Breaking:** `WINIT_UNIX_BACKEND` was removed in favor of standard `WAYLAND_DISPLAY` and `DISPLAY` variables. +- **Breaking:** on Wayland, dispatching user created Wayland queue won't wake up the loop unless winit has event to send back. +- **Breaking:** remove `DeviceEvent::Text`. +- **Breaking:** Remove lifetime parameter from `Event` and `WindowEvent`. +- **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. +- **Breaking:** `ActivationTokenDone` event which could be requested with the new `startup_notify` module, see its docs for more. +- **Breaking:** `ScaleFactorChanged` now contains a writer instead of a reference to update inner size. +- **Breaking** `run() -> !` has been replaced by `run() -> Result<(), EventLoopError>` for returning errors without calling `std::process::exit()` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- **Breaking** Removed `EventLoopExtRunReturn` / `run_return` in favor of `EventLoopExtPumpEvents` / `pump_events` and `EventLoopExtRunOnDemand` / `run_on_demand` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- `RedrawRequested` is no longer guaranteed to be emitted after `MainEventsCleared`, it is now platform-specific when the event is emitted after being requested via `redraw_request()`. + - On Windows, `RedrawRequested` is now driven by `WM_PAINT` messages which are requested via `redraw_request()` +- **Breaking** `LoopDestroyed` renamed to `LoopExiting` ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `RedrawEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `MainEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking:** Remove all deprecated `modifiers` fields. - **Breaking:** Rename `DeviceEventFilter` to `DeviceEvents` reversing the behavior of variants. +- **Breaking** Add `AboutToWait` event which is emitted when the event loop is about to block and wait for new events ([#2900](https://github.com/rust-windowing/winit/issues/2900)) - **Breaking:** Rename `EventLoopWindowTarget::set_device_event_filter` to `listen_device_events`. -- On X11, fix `EventLoopWindowTarget::listen_device_events` effect being reversed. -- **Breaking:** Remove all deprecated `modifiers` fields. +- **Breaking:** Rename `Window::set_ime_position` to `Window::set_ime_cursor_area` adding a way to set exclusive zone. +- **Breaking:** `with_x11_visual` now takes the visual ID instead of the bare pointer. +- **Breaking** `MouseButton` now supports `Back` and `Forward` variants, emitted from mouse events on Wayland, X11, Windows, macOS and Web. +- **Breaking:** On Web, `instant` is now replaced by `web_time`. +- **Breaking:** On Web, dropped support for Safari versions below 13.1. +- **Breaking:** On Web, the canvas output bitmap size is no longer adjusted. +- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to the canvas size will be reported through `WindowEvent::Resized`. +- **Breaking:** Updated `bitflags` crate version to `2`, which changes the API on exposed types. +- **Breaking:** `CursorIcon::Arrow` was removed. +- **Breaking:** `CursorIcon::Hand` is now named `CursorIcon::Pointer`. +- **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate. +- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. - **Breaking:** Overhaul keyboard input handling. - Replace `KeyboardInput` with `KeyEvent` and `RawKeyEvent`. - Change `WindowEvent::KeyboardInput` to contain a `KeyEvent`. @@ -25,11 +182,13 @@ And please only add new entries to the top of this list, right below the `# Unre - Replace `VirtualKeyCode` with the `Key` enum. - Replace `ScanCode` with the `KeyCode` enum. - Rename `ModifiersState::LOGO` to `SUPER` and `ModifiersState::CTRL` to `CONTROL`. + - Add `PhysicalKey` wrapping `KeyCode` and `NativeKeyCode`. - Add `KeyCode` to refer to keys (roughly) by their physical location. - Add `NativeKeyCode` to represent raw `KeyCode`s which Winit doesn't understand. - Add `Key` to represent the keys after they've been interpreted by the active (software) keyboard layout. + - Add `NamedKey` to represent the categorized keys. - Add `NativeKey` to represent raw `Key`s which Winit doesn't understand. - Add `KeyLocation` to tell apart `Key`s which usually "mean" the same thing, but can appear simultaneously in different spots on the same keyboard @@ -38,56 +197,112 @@ And please only add new entries to the top of this list, right below the `# Unre of dead key sequences. - Add `KeyEventExtModifierSupplement` to expose additional (and less portable) interpretations of a given key-press. - - Add `KeyCodeExtScancode`, which lets you convert between raw keycodes and - `KeyCode`. + - Add `PhysicalKeyExtScancode`, which lets you convert between scancodes and + `PhysicalKey`. - `ModifiersChanged` now uses dedicated `Modifiers` struct. +- Removed platform-specific extensions that should be retrieved through `raw-window-handle` trait implementations instead: + - `platform::windows::HINSTANCE`. + - `WindowExtWindows::hinstance`. + - `WindowExtWindows::hwnd`. + - `WindowExtIOS::ui_window`. + - `WindowExtIOS::ui_view_controller`. + - `WindowExtIOS::ui_view`. + - `WindowExtMacOS::ns_window`. + - `WindowExtMacOS::ns_view`. + - `EventLoopWindowTargetExtWayland::wayland_display`. + - `WindowExtWayland::wayland_surface`. + - `WindowExtWayland::wayland_display`. + - `WindowExtX11::xlib_window`. + - `WindowExtX11::xlib_display`. + - `WindowExtX11::xlib_screen_id`. + - `WindowExtX11::xcb_connection`. +- Reexport `raw-window-handle` in `window` module. +- Add `ElementState::is_pressed`. +- Add `Window::pre_present_notify` to notify winit before presenting to the windowing system. +- Add `Window::set_blur` to request a blur behind the window; implemented on Wayland for now. +- Add `Window::show_window_menu` to request a titlebar/system menu; implemented on Wayland/Windows for now. +- Implement `AsFd`/`AsRawFd` for `EventLoop` on X11 and Wayland. +- Implement `PartialOrd` and `Ord` for `MouseButton`. +- Implement `PartialOrd` and `Ord` on types in the `dpi` module. +- Make `WindowBuilder` `Send + Sync`. +- Make iOS `MonitorHandle` and `VideoMode` usable from other threads. +- Make iOS windows usable from other threads. +- On Android, add force data to touch events. +- On Android, added `EventLoopBuilderExtAndroid::handle_volume_keys` to indicate that the application will handle the volume keys manually. +- On Android, fix `DeviceId` to contain device id's. - On Orbital, fix `ModifiersChanged` not being sent. -- **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate. -- **Breaking:** `CursorIcon::Hand` is now named `CursorIcon::Pointer`. -- **Breaking:** `CursorIcon::Arrow` was removed. -- On Wayland, fix maximized startup not taking full size on GNOME. -- On Wayland, fix initial window size not restored for maximized/fullscreened on startup window. - On Wayland, `Window::outer_size` now accounts for **client side** decorations. -- On Wayland, fix window not checking that it actually got initial configure event. -- On Wayland, fix maximized window creation and window geometry handling. -- On Wayland, fix forward compatibility issues. - On Wayland, add `Window::drag_resize_window` method. -- On Wayland, drop `WINIT_WAYLAND_CSD_THEME` variable. -- Implement `PartialOrd` and `Ord` on types in the `dpi` module. -- **Breaking:** Bump MSRV from `1.60` to `1.64`. -- **Breaking:** On Web, the canvas output bitmap size is no longer adjusted. -- On Web: fix `Window::request_redraw` not waking the event loop when called from outside the loop. -- On Web: fix position of touch events to be relative to the canvas. -- On Web, fix `Window:::set_fullscreen` doing nothing when called outside the event loop but during - a transient activation. -- On Web, fix pointer button events not being processed when a buttons is already pressed. -- **Breaking:** Updated `bitflags` crate version to `2`, which changes the API on exposed types. -- On Web, handle coalesced pointer events, which increases the resolution of pointer inputs. -- **Breaking:** On Web, `instant` is now replaced by `web_time`. -- On Windows, port to `windows-sys` version 0.48.0. +- On Wayland, remove `WINIT_WAYLAND_CSD_THEME` variable. +- On Wayland, fix `TouchPhase::Canceled` being sent for moved events. +- On Wayland, fix forward compatibility issues. +- On Wayland, fix initial window size not restored for maximized/fullscreened on startup window. +- On Wayland, fix maximized startup not taking full size on GNOME. +- On Wayland, fix maximized window creation and window geometry handling. +- On Wayland, fix window not checking that it actually got initial configure event. +- On Wayland, make double clicking and moving the CSD frame more reliable. +- On Wayland, support `Occluded` event with xdg-shell v6 +- On Wayland, use frame callbacks to throttle `RedrawRequested` events so redraws will align with compositor. +- On Web, `ControlFlow::WaitUntil` now uses the Prioritized Task Scheduling API. `setTimeout()`, with a trick to circumvent throttling to 4ms, is used as a fallback. +- On Web, `EventLoopProxy` now implements `Send`. +- On Web, `Window` now implements `Send` and `Sync`. +- On Web, account for CSS `padding`, `border`, and `margin` when getting or setting the canvas position. +- On Web, add Fullscreen API compatibility for Safari. +- On Web, add `DeviceEvent::Motion`, `DeviceEvent::MouseWheel`, `DeviceEvent::Button` and `DeviceEvent::Key` support. +- On Web, add `EventLoopWindowTargetExtWebSys` and `PollStrategy`, which allows to set different strategies for `ControlFlow::Poll`. By default the Prioritized Task Scheduling API is used, but an option to use `Window.requestIdleCallback` is available as well. Both use `setTimeout()`, with a trick to circumvent throttling to 4ms, as a fallback. +- On Web, add `WindowBuilderExtWebSys::with_append()` to append the canvas element to the web page on creation. +- On Web, allow event loops to be recreated with `spawn`. +- On Web, enable event propagation. +- On Web, fix `ControlFlow::WaitUntil` to never wake up **before** the given time. +- On Web, fix `DeviceEvent::MouseMotion` only being emitted for each canvas instead of the whole window. +- On Web, fix `Window:::set_fullscreen` doing nothing when called outside the event loop but during transient activation. - On Web, fix pen treated as mouse input. -- On Web, send mouse position on button release as well. +- On Web, fix pointer button events not being processed when a buttons is already pressed. +- On Web, fix scale factor resize suggestion always overwriting the canvas size. +- On Web, fix some `WindowBuilder` methods doing nothing. +- On Web, fix some `Window` methods using incorrect HTML attributes instead of CSS properties. +- On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events. - On Web, fix touch input not gaining or loosing focus. -- **Breaking:** On Web, dropped support for Safari versions below 13.1. +- On Web, fix touch location to be as accurate as mouse position. +- On Web, handle coalesced pointer events, which increases the resolution of pointer inputs. +- On Web, implement `Window::focus_window()`. +- On Web, implement `Window::set_(min|max)_inner_size()`. +- On Web, implement `WindowEvent::Occluded`. +- On Web, never return a `MonitorHandle`. - On Web, prevent clicks on the canvas to select text. -- On Web, `EventLoopProxy` now implements `Send`. -- On Web, `Window` now implements `Send` and `Sync`. -- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. -- On Web, use the correct canvas size when calculating the new size during scale factor change, - instead of using the output bitmap size. +- On Web, remove any fullscreen requests from the queue when an external fullscreen activation was detected. +- On Web, remove unnecessary `Window::is_dark_mode()`, which was replaced with `Window::theme()`. +- On Web, respect `EventLoopWindowTarget::listen_device_events()` settings. - On Web, scale factor and dark mode detection are now more robust. -- On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events. -- On Web, fix scale factor resize suggestion always overwriting the canvas size. +- On Web, send mouse position on button release as well. +- On Web, take all transient activations on the canvas and window into account to queue a fullscreen request. +- On Web, use `Window.requestAnimationFrame()` to throttle `RedrawRequested` events. +- On Web, use the correct canvas size when calculating the new size during scale factor change, instead of using the output bitmap size. +- On Web: fix `Window::request_redraw` not waking the event loop when called from outside the loop. +- On Web: fix position of touch events to be relative to the canvas. +- On Windows, add `drag_resize_window` method support. +- On Windows, add horizontal MouseWheel `DeviceEvent`. +- On Windows, added `WindowBuilderExtWindows::with_class_name` to customize the internal class name. +- On Windows, fix IME APIs not working when from non event loop thread. +- On Windows, fix `CursorEnter/Left` not being sent when grabbing the mouse. +- On Windows, fix `RedrawRequested` not being delivered when calling `Window::request_redraw` from `RedrawRequested`. +- On Windows, port to `windows-sys` version 0.48.0. +- On X11, add a `with_embedded_parent_window` function to the window builder to allow embedding a window into another window. +- On X11, fix event loop not waking up on `ControlFlow::Poll` and `ControlFlow::WaitUntil`. +- On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. +- On X11, set `visual_id` in returned `raw-window-handle`. +- On iOS, add ability to change the status bar style. +- On iOS, add force data to touch events when using the Apple Pencil. +- On iOS, always wake the event loop when transitioning from `ControlFlow::Poll` to `ControlFlow::Poll`. +- On iOS, send events `WindowEvent::Occluded(false)`, `WindowEvent::Occluded(true)` when application enters/leaves foreground. +- On macOS, add tabbing APIs on `WindowExtMacOS` and `EventLoopWindowTargetExtMacOS`. +- On macOS, fix assertion when pressing `Globe` key. +- On macOS, fix crash in `window.set_minimized(false)`. - On macOS, fix crash when dropping `Window`. -- On Web, use `Window.requestIdleCallback()` for `ControlFlow::Poll` when available. -- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to - the canvas size will be reported through `WindowEvent::Resized`. -- On Web, respect `EventLoopWindowTarget::listen_device_events()` settings. -- On Web, fix `DeviceEvent::MouseMotion` only being emitted for each canvas instead of the whole window. -- On Web, add `DeviceEvent::Motion`, `DeviceEvent::MouseWheel`, `DeviceEvent::Button` and - `DeviceEvent::Key` support. -- **Breaking** `MouseButton` now supports `Back` and `Forward` variants, emitted from mouse events - on Wayland, X11, Windows, macOS and Web. + +# 0.28.7 + +- Fix window size sometimes being invalid when resizing on macOS 14 Sonoma. # 0.28.6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f84f57e907..07751183a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ may be worth creating a separate crate that extends Winit's API to add that func When reporting an issue, in order to help the maintainers understand what the problem is, please make your description of the issue as detailed as possible: -- if it is a bug, please provide clear explanation of what happens, what should happen, and how to +- if it is a bug, please provide a clear explanation of what happens, what should happen, and how to reproduce the issue, ideally by providing a minimal program exhibiting the problem - if it is a feature request, please provide a clear argumentation about why you believe this feature should be supported by winit @@ -20,8 +20,8 @@ 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.64. -- you tested your modifications on all the platforms impacted, or if not possible detail which platforms +- your patch builds with Winit's minimal supported rust version - Rust 1.65. +- 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 - you left comments in your code explaining any part that is not straightforward, so that the @@ -34,7 +34,7 @@ When making a code contribution to winit, before opening your pull request, plea relevant sections in [`FEATURES.md`](https://github.com/rust-windowing/winit/blob/master/FEATURES.md#features) should be updated. -Once your PR is open, you can ask for review by a maintainer of your platform. Winit's merging policy +Once your PR is open, you can ask for a review by a maintainer of your platform. Winit's merging policy is that a PR must be approved by at least two maintainers of winit before being merged, including at least a maintainer of the platform (a maintainer making a PR themselves counts as approving it). @@ -46,27 +46,26 @@ Once your PR is deemed ready, the merging maintainer will take care of resolving The current maintainers are listed in the [CODEOWNERS](.github/CODEOWNERS) file. -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! +If you are interested in being pinged when testing is needed for a specific platform, please add yourself to the [Testers and Contributors](https://github.com/rust-windowing/winit/wiki/Testers-and-Contributors) table! ## Release process -Given that winit is a widely used library we should be able to make a patch +Given that winit is a widely used library, we should be able to make a patch releases at any time we want without blocking the development of new features. -To achieve these goals, a new branch is created for every new release. Releases -and later patch releases are committed and tagged in this branch. +To achieve these goals, a new branch is created for every new release. Releases and later patch releases are committed and tagged in this branch. The exact steps for an exemplary `0.2.0` release might look like this: - 1. Initially the version on the latest master is `0.1.0` + 1. Initially, the version on the latest master is `0.1.0` 2. A new `v0.2.x` branch is created for the release 3. In the branch, the version is bumped to `v0.2.0` 4. The new commit in the branch is tagged `v0.2.0` 5. The version is pushed to crates.io 6. A GitHub release is created for the `v0.2.0` tag - 7. On master, the version is bumped to `0.2.0` and the CHANGELOG is updated + 7. On master, the version is bumped to `0.2.0`, and the CHANGELOG is updated -When doing a patch release the process is similar: - 1. Initially the version of the latest release is `0.2.0` +When doing a patch release, the process is similar: + 1. Initially, the version of the latest release is `0.2.0` 2. Checkout the `v0.2.x` branch 3. Cherry-pick the required non-breaking changes into the `v0.2.x` 4. Follow steps 3-7 of the regular release example diff --git a/Cargo.toml b/Cargo.toml index fba184fb08..5435d429b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.28.6" +version = "0.29.15" authors = ["The winit contributors", "Pierre Krieger "] description = "Cross-platform window creation library." edition = "2021" @@ -10,10 +10,17 @@ readme = "README.md" repository = "https://github.com/rust-windowing/winit" documentation = "https://docs.rs/winit" categories = ["gui"] -rust-version = "1.64.0" +rust-version = "1.65.0" [package.metadata.docs.rs] -features = ["serde"] +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + # Enabled to get docs to compile + "android-native-activity", +] default-target = "x86_64-unknown-linux-gnu" # These are all tested in CI targets = [ @@ -35,9 +42,9 @@ targets = [ rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -x11 = ["x11-dl", "percent-encoding", "xkbcommon-dl/x11"] -wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "sctk", "fnv", "memmap2"] +default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "wayland-protocols-plasma", "sctk", "ahash", "memmap2"] wayland-dlopen = ["wayland-backend/dlopen"] wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] @@ -45,40 +52,74 @@ wayland-csd-adwaita-notitle = ["sctk-adwaita"] android-native-activity = ["android-activity/native-activity"] android-game-activity = ["android-activity/game-activity"] serde = ["dep:serde", "cursor-icon/serde", "smol_str/serde"] +rwh_04 = ["dep:rwh_04", "ndk/rwh_04"] +rwh_05 = ["dep:rwh_05", "ndk/rwh_05"] +rwh_06 = ["dep:rwh_06", "ndk/rwh_06"] [build-dependencies] cfg_aliases = "0.1.1" [dependencies] bitflags = "2" -cursor-icon = "1.0.0" +cursor-icon = "1.1.0" log = "0.4" mint = { version = "0.5.6", optional = true } once_cell = "1.12" -raw_window_handle = { package = "raw-window-handle", version = "0.5" } +rwh_04 = { package = "raw-window-handle", version = "0.4", optional = true } +rwh_05 = { package = "raw-window-handle", version = "0.5.2", features = ["std"], optional = true } +rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"], optional = true } serde = { version = "1", optional = true, features = ["serde_derive"] } smol_str = "0.2.0" [dev-dependencies] image = { version = "0.24.0", default-features = false, features = ["png"] } -simple_logger = { version = "2.1.0", default_features = false } +simple_logger = { version = "4.2.0", default_features = false } +winit = { path = ".", features = ["rwh_05"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies] softbuffer = "0.3.0" [target.'cfg(target_os = "android")'.dependencies] -# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 -android-activity = "0.4.0" -ndk = "0.7.0" -ndk-sys = "0.4.0" +android-activity = "0.5.0" +ndk = { version = "0.8.0", default-features = false } +ndk-sys = "0.5.0" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] core-foundation = "0.9.3" -objc2 = ">=0.3.0-beta.3, <0.3.0-beta.4" # Allow `0.3.0-beta.3.patch-leaks` +objc2 = "0.4.1" [target.'cfg(target_os = "macos")'.dependencies] -core-graphics = "0.22.3" -dispatch = "0.2.0" +core-graphics = "0.23.1" + +[target.'cfg(target_os = "macos")'.dependencies.icrate] +version = "0.0.4" +features = [ + "dispatch", + "Foundation", + "Foundation_NSArray", + "Foundation_NSAttributedString", + "Foundation_NSMutableAttributedString", + "Foundation_NSData", + "Foundation_NSDictionary", + "Foundation_NSString", + "Foundation_NSProcessInfo", + "Foundation_NSThread", + "Foundation_NSNumber", +] + +[target.'cfg(target_os = "ios")'.dependencies.icrate] +version = "0.0.4" +features = [ + "dispatch", + "Foundation", + "Foundation_NSArray", + "Foundation_NSData", + "Foundation_NSError", + "Foundation_NSString", + "Foundation_NSProcessInfo", + "Foundation_NSThread", + "Foundation_NSSet", +] [target.'cfg(target_os = "windows")'.dependencies] unicode-segmentation = "1.7.1" @@ -113,18 +154,22 @@ features = [ ] [target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies] +ahash = { version = "0.8.3", features = ["no-rng"], optional = true } +bytemuck = { version = "1.13.1", default-features = false, optional = true } +calloop = "0.12.3" libc = "0.2.64" +memmap2 = { version = "0.9.0", optional = true } percent-encoding = { version = "2.0", optional = true } -fnv = { version = "1.0.3", optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.17.0", default-features = false, features = ["calloop"], optional = true } -sctk-adwaita = { version = "0.6.0", default_features = false, optional = true } -wayland-client = { version = "0.30.0", optional = true } -wayland-backend = { version = "0.1.0", default_features = false, features = ["client_system"], optional = true } -wayland-protocols = { version = "0.30.0", features = [ "staging"], optional = true } -calloop = "0.10.5" -x11-dl = { version = "2.18.5", optional = true } -xkbcommon-dl = "0.4.0" -memmap2 = { version = "0.5.0", optional = true } +rustix = { version = "0.38.4", default-features = false, features = ["std", "system", "thread", "process"] } +sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"], optional = true } +sctk-adwaita = { version = "0.8.0", default_features = false, optional = true } +wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"], optional = true } +wayland-client = { version = "0.31.1", optional = true } +wayland-protocols = { version = "0.31.0", features = [ "staging"], optional = true } +wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optional = true } +x11-dl = { version = "2.19.1", optional = true } +x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true } +xkbcommon-dl = "0.4.2" [target.'cfg(target_os = "redox")'.dependencies] orbclient = { version = "0.3.42", default-features = false } @@ -134,6 +179,8 @@ redox_syscall = "0.3" package = "web-sys" version = "0.3.64" features = [ + 'AbortController', + 'AbortSignal', 'console', 'CssStyleDeclaration', 'Document', @@ -145,8 +192,12 @@ features = [ 'FocusEvent', 'HtmlCanvasElement', 'HtmlElement', + 'IntersectionObserver', + 'IntersectionObserverEntry', 'KeyboardEvent', 'MediaQueryList', + 'MessageChannel', + 'MessagePort', 'Node', 'PageTransitionEvent', 'PointerEvent', @@ -155,6 +206,7 @@ features = [ 'ResizeObserverEntry', 'ResizeObserverOptions', 'ResizeObserverSize', + 'VisibilityState', 'Window', 'WheelEvent' ] diff --git a/FEATURES.md b/FEATURES.md index 04155b5dc8..97286ceb22 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,6 +1,6 @@ # Winit Scope -Winit aims to expose an interface that abstracts over window creation and input handling, and can +Winit aims to expose an interface that abstracts over window creation and input handling and can be used to create both games and applications. It supports the following main graphical platforms: - Desktop - Windows 7+ (10+ is tested regularly) @@ -13,7 +13,9 @@ be used to create both games and applications. It supports the following main gr - iOS - Android - Web - - via WASM + - Chrome + - Firefox + - Safari 13.1+ Most platforms expose capabilities that cannot be meaningfully transposed onto others. Winit does not aim to support every single feature of every platform, but rather to abstract over the common features @@ -43,10 +45,10 @@ be released and the library will enter maintenance mode. For the most part, new be added past this point. New platform features may be accepted and exposed through point releases. ### Tier upgrades -Some platform features could in theory be exposed across multiple platforms, but have not gone +Some platform features could, in theory, be exposed across multiple platforms, but have not gone through the implementation work necessary to function on all platforms. When one of these features gets implemented across all platforms, a PR can be opened to upgrade the feature to a core feature. -If that gets accepted, the platform-specific functions gets deprecated and become permanently +If that gets accepted, the platform-specific functions get deprecated and become permanently exposed through the core, cross-platform API. # Features @@ -86,7 +88,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Fullscreen toggle**: The windows created by winit can be switched to and from fullscreen after creation. - **Exclusive fullscreen**: Winit allows changing the video mode of the monitor - for fullscreen windows, and if applicable, captures the monitor for exclusive + for fullscreen windows and, if applicable, captures the monitor for exclusive use by this application. - **HiDPI support**: Winit assists developers in appropriately scaling HiDPI content. - **Popup / modal windows**: Windows can be created relative to the client area of other windows, and parent @@ -103,7 +105,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Mouse set location**: Forcibly changing the location of the pointer. - **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 icon**: Changing the cursor icon or hiding the cursor. - **Cursor hittest**: Handle or ignore mouse events for a window. - **Touch events**: Single-touch events. - **Touch pressure**: Touch events contain information about the amount of force being applied. @@ -117,6 +119,7 @@ If your PR makes notable changes to Winit's features, please update this section ## Platform ### Windows +* Setting the name of the internal window class * Setting the taskbar icon * Setting the parent window * Setting a menu bar @@ -143,20 +146,17 @@ If your PR makes notable changes to Winit's features, please update this section ### iOS * `winit` has a minimum OS requirement of iOS 8 -* Get the `UIWindow` object pointer -* Get the `UIViewController` object pointer -* Get the `UIView` object pointer * Get the `UIScreen` object pointer * Setting the `UIView` hidpi factor * Valid orientations * Home indicator visibility -* Status bar visibility -* Deferrring system gestures +* Status bar visibility and style +* Deferring system gestures * Getting the device idiom * Getting the preferred video mode ### Web -* Get if systems preferred color scheme is "dark" +* Get if the systems preferred color scheme is "dark" ## Usability * `serde`: Enables serialization/deserialization of certain types with Serde. (Maintainer: @Osspial) @@ -166,13 +166,13 @@ If your PR makes notable changes to Winit's features, please update this section Legend: - ✔️: Works as intended -- ▢: Mostly works but some bugs are known +- ▢: Mostly works, but some bugs are known - ❌: Missing feature or large bugs making it unusable - **N/A**: Not applicable for this platform - ❓: Unknown status ### Windowing -|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |WASM |Redox OS| +|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |Web |Redox OS| |-------------------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | |Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |✔️ |✔️ | |Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ | @@ -182,6 +182,7 @@ Legend: |Window resizing |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | |Window resize increments |❌ |✔️ |✔️ |❌ |**N/A**|**N/A**|**N/A**|**N/A** | |Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|N/A |✔️ | +|Window blur |❌ |❌ |❌ |✔️ |**N/A**|**N/A**|N/A |❌ | |Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | |Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | |Window minimization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | @@ -192,20 +193,20 @@ Legend: |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**|**N/A** | ### System information -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM |Redox OS| +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| |---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | ------ | |Monitor list |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | |Video mode query |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | ### Input handling -|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM |Redox OS| +|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | |Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | |Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**|**N/A** | |Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ | |Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | -|Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ |❌ | +|Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** | |Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** | |Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ |**N/A** | @@ -215,19 +216,19 @@ Legend: |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ |**N/A** | |Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ |**N/A** | |Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | -|Resize with cursor |❌ |❌ |✔️ |❌ |**N/A**|**N/A**|**N/A** |**N/A** | +|Resize with cursor |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | ### Pending API Reworks Changes in the API that have been agreed upon but aren't implemented across all platforms. -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM |Redox OS| +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| |------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | |New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |❓ | |Event Loop 2.0 ([#459]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |✔️ | |Keyboard Input 2.0 ([#753]) |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | ### Completed API Reworks -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM |Redox OS| +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| |------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | [#165]: https://github.com/rust-windowing/winit/issues/165 diff --git a/HALL_OF_CHAMPIONS.md b/HALL_OF_CHAMPIONS.md index eab9433be4..9ef10c0017 100644 --- a/HALL_OF_CHAMPIONS.md +++ b/HALL_OF_CHAMPIONS.md @@ -2,21 +2,20 @@ The winit maintainers would like to recognize the following former winit contributors, without whom winit would not exist in its current form. We thank -them deeply for their time and efforts, and wish them best of luck in their +them deeply for their time and efforts and wish them the best of luck in their future endeavors: * [@tomaka]: For creating the winit project and guiding it through its early years of existence. -* [@vberger]: For diligently creating the Wayland backend, and being its +* [@vberger]: For diligently creating the Wayland backend and being its extremely helpful and benevolent maintainer for years. * [@francesca64]: For taking over the responsibility of maintaining almost every - winit backend, and standardizing HiDPI support across all of them. -* [@Osspial]: For heroically landing EventLoop 2.0, and valiantly ushering in a + winit backend and standardizing HiDPI support across all of them. +* [@Osspial]: For heroically landing EventLoop 2.0 and valiantly ushering in a vastly more sustainable era of winit. -* [@goddessfreya]: For selflessly taking over maintainership of glutin, and her +* [@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. +* [@ArturKovacs]: For consistently maintaining the macOS backend and for his immense involvement in designing and implementing the new keyboard API. [@tomaka]: https://github.com/tomaka [@vberger]: https://github.com/vberger diff --git a/README.md b/README.md index 35f64c5c70..92684f4e97 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ ```toml [dependencies] -winit = "0.28.6" +winit = "0.29.15" ``` ## [Documentation](https://docs.rs/winit) For features _within_ the scope of winit, see [FEATURES.md](FEATURES.md). -For features _outside_ the scope of winit, see [Missing features provided by other crates](https://github.com/rust-windowing/winit/wiki/Missing-features-provided-by-other-crates) in the wiki. +For features _outside_ the scope of winit, see [Are we GUI Yet?](https://areweguiyet.com/) and [Are we game yet?](https://arewegameyet.rs/), depending on what kind of project you're looking to do. ## Contact Us @@ -26,39 +26,12 @@ Join us in any of these: Winit is a window creation and management library. It can create windows and lets you handle events (for example: the window being resized, a key being pressed, a mouse movement, etc.) -produced by window. +produced by the window. Winit is designed to be a low-level brick in a hierarchy of libraries. Consequently, in order to show something on the window you need to use the platform-specific getters provided by winit, or another library. -```rust -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; - -fn main() { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} -``` - -Winit is only officially supported on the latest stable version of the Rust compiler. - ### Cargo Features Winit provides the following features, which can be enabled in your `Cargo.toml` file: @@ -67,6 +40,34 @@ Winit provides the following features, which can be enabled in your `Cargo.toml` * `wayland` (enabled by default): On Unix platform, compiles with the Wayland backend * `mint`: Enables mint (math interoperability standard types) conversions. +## MSRV Policy + +This crate's Minimum Supported Rust Version (MSRV) is **1.65**. Changes to +the MSRV will be accompanied by a minor version bump. + +As a **tentative** policy, the upper bound of the MSRV is given by the following +formula: + +``` +min(sid, stable - 3) +``` + +Where `sid` is the current version of `rustc` provided by [Debian Sid], and +`stable` is the latest stable version of Rust. This bound may be broken in case of a major ecosystem shift or a security vulnerability. + +[Debian Sid]: https://packages.debian.org/sid/rustc + +The exception is for the Android platform, where a higher Rust version +must be used for certain Android features. In this case, the MSRV will be +capped at the latest stable version of Rust minus three. This inconsistency is +not reflected in Cargo metadata, as it is not powerful enough to expose this +restriction. + +All crates in the [`rust-windowing`] organizations have the +same MSRV policy. + +[`rust-windowing`]: https://github.com/rust-windowing + ### Platform-specific usage #### Wayland @@ -84,7 +85,7 @@ either [provide Winit with a `` element][web with_canvas], or [let Winit create a `` element which you can then retrieve][web canvas getter] and insert it into the DOM yourself. -For example code using Winit with WebAssembly, check out the [web example]. For +For the example code using Winit with WebAssembly, check out the [web example]. For information on using Rust on WebAssembly, check out the [Rust and WebAssembly book]. @@ -95,7 +96,7 @@ book]. #### Android -The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/0.7.0/ndk/) crate. +The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/latest/ndk/) crate. Native Android applications need some form of "glue" crate that is responsible for defining the main entry point for your Rust application as well as tracking @@ -107,13 +108,14 @@ glue crate (prior to `0.28` it used The version of the glue crate that your application depends on _must_ match the version that Winit depends on because the glue crate is responsible for your -application's main entrypoint. If Cargo resolves multiple versions they will +application's main entry point. If Cargo resolves multiple versions, they will clash. `winit` glue compatibility table: | winit | ndk-glue | | :---: | :--------------------------: | +| 0.29 | `android-activity = "0.5"` | | 0.28 | `android-activity = "0.4"` | | 0.27 | `ndk-glue = "0.7"` | | 0.26 | `ndk-glue = "0.5"` | @@ -124,7 +126,7 @@ The recommended way to avoid a conflict with the glue version is to avoid explic depending on the `android-activity` crate, and instead consume the API that is re-exported by Winit under `winit::platform::android::activity::*` -Running on an Android device needs a dynamic system library, add this to Cargo.toml: +Running on an Android device needs a dynamic system library. Add this to Cargo.toml: ```toml [lib] @@ -132,14 +134,14 @@ name = "main" crate-type = ["cdylib"] ``` -All Android applications are based on an `Activity` subclass and the +All Android applications are based on an `Activity` subclass, and the `android-activity` crate is designed to support different choices for this base class. Your application _must_ specify the base class it needs via a feature flag: | Base Class | Feature Flag | Notes | | :--------------: | :---------------: | :-----: | | `NativeActivity` | `android-native-activity` | Built-in to Android - it is possible to use without compiling any Java or Kotlin code. Java or Kotlin code may be needed to subclass `NativeActivity` to access some platform features. It does not derive from the [`AndroidAppCompat`] base class.| -| [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`] which is a defacto standard `Activity` base class that helps support a wider range of Android versions. Requires a build system that can compile Java or Kotlin and fetch Android dependencies from a [Maven repository][agdk_jetpack] (or link with an embedded [release][agdk_releases] of [`GameActivity`]) | +| [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`], a defacto standard `Activity` base class that helps support a wider range of Android versions. Requires a build system that can compile Java or Kotlin and fetch Android dependencies from a [Maven repository][agdk_jetpack] (or link with an embedded [release][agdk_releases] of [`GameActivity`]) | [`GameActivity`]: https://developer.android.com/games/agdk/game-activity [`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input @@ -148,40 +150,13 @@ class. Your application _must_ specify the base class it needs via a feature fla [agdk_releases]: https://developer.android.com/games/agdk/download#agdk-libraries [Gradle]: https://developer.android.com/studio/build -For example, add this to Cargo.toml: -```toml -winit = { version = "0.28", features = [ "android-native-activity" ] } - -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.11.0" -``` - -And, for example, define an entry point for your library like this: -```rust -#[cfg(target_os = "android")] -use winit::platform::android::activity::AndroidApp; - -#[cfg(target_os = "android")] -#[no_mangle] -fn android_main(app: AndroidApp) { - use winit::platform::android::EventLoopBuilderExtAndroid; - - android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Trace)); - - let event_loop = EventLoopBuilder::with_user_event() - .with_android_app(app) - .build(); - _main(event_loop); -} -``` - -For more details, refer to these `android-activity` [example applications](https://github.com/rib/android-activity/tree/main/examples). +For more details, refer to these `android-activity` [example applications](https://github.com/rust-mobile/android-activity/tree/main/examples). ##### Converting from `ndk-glue` to `android-activity` -If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk` then the minimal changes would be: +If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk`, then the minimal changes would be: 1. Remove `ndk-glue` from your `Cargo.toml` -2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.28", features = [ "android-native-activity" ] }` +2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.29.15", features = [ "android-native-activity" ] }` 3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize logging as above). 4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your event loop (as shown above). @@ -192,13 +167,13 @@ 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)`. +`Event::Resumed`. #### iOS Similar to macOS, iOS's main `UIApplicationMain` does some init work that's required -by all UI related code, see issue [#1705]. You should consider creating your windows -inside `Event::NewEvents(StartCause::Init)`. +by all UI-related code (see issue [#1705]). It would be best to consider creating your windows +inside `Event::Resumed`. [#2238]: https://github.com/rust-windowing/winit/issues/2238 @@ -208,5 +183,5 @@ inside `Event::NewEvents(StartCause::Init)`. #### Redox OS -Redox OS has some functionality not present yet, that will be implemented when +Redox OS has some functionality not yet present that will be implemented when its orbital display server provides it. diff --git a/build.rs b/build.rs index 00d706d369..7d83d771f1 100644 --- a/build.rs +++ b/build.rs @@ -8,12 +8,12 @@ fn main() { cfg_aliases! { // Systems. android_platform: { target_os = "android" }, - wasm_platform: { target_family = "wasm" }, + wasm_platform: { all(target_family = "wasm", not(target_os = "emscripten")) }, macos_platform: { target_os = "macos" }, ios_platform: { target_os = "ios" }, windows_platform: { target_os = "windows" }, apple: { any(target_os = "ios", target_os = "macos") }, - free_unix: { all(unix, not(apple), not(android_platform)) }, + free_unix: { all(unix, not(apple), not(android_platform), not(target_os = "emscripten")) }, redox: { target_os = "redox" }, // Native displays. diff --git a/clippy.toml b/clippy.toml index 70fbe7075e..cd4c241255 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,4 +4,10 @@ disallowed-methods = [ { path = "web_sys::HtmlCanvasElement::height", reason = "Winit shouldn't touch the internal canvas size" }, { path = "web_sys::HtmlCanvasElement::set_width", reason = "Winit shouldn't touch the internal canvas size" }, { path = "web_sys::HtmlCanvasElement::set_height", reason = "Winit shouldn't touch the internal canvas size" }, + { path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" }, + { path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" }, + { path = "web_sys::Element::request_fullscreen", reason = "Doesn't account for compatibility with Safari" }, + { path = "web_sys::Document::exit_fullscreen", reason = "Doesn't account for compatibility with Safari" }, + { path = "web_sys::Document::fullscreen_element", reason = "Doesn't account for compatibility with Safari" }, + { path = "icrate::AppKit::NSView::visibleRect", reason = "We expose a render target to the user, and visibility is not really relevant to that (and can break if you don't use the rectangle position as well). Use `frame` instead." }, ] diff --git a/deny.toml b/deny.toml index db4f119fbc..b566e1e263 100644 --- a/deny.toml +++ b/deny.toml @@ -31,16 +31,10 @@ multiple-versions = "deny" wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed deny = [] skip = [ - { name = "bitflags" }, # the ecosystem is in the process of migrating. - { name = "nix" }, # differing version - as of 2023-03-02 whis can be solved with `cargo update && cargo update -p calloop --precise 0.10.2` - { name = "memoffset"}, # due to different nix versions. - { name = "memmap2" }, # sctk uses a different version until the next update - { name = "libloading" }, # x11rb uses a different version until the next update - { name = "syn" }, # https://github.com/rust-mobile/ndk/issues/392 and https://github.com/rustwasm/wasm-bindgen/issues/3390 - { name = "num_enum"}, # See above ^, waiting for release - { name = "num_enum_derive"},# See above ^, waiting for release - { name = "miniz_oxide"}, # https://github.com/rust-lang/flate2-rs/issues/340 - { name = "redox_syscall" }, # https://gitlab.redox-os.org/redox-os/orbclient/-/issues/46 + { name = "raw-window-handle" }, # we intentionally have multiple versions of this + { name = "bitflags" }, # the ecosystem is in the process of migrating. + { name = "libloading" }, # x11rb uses a different version until the next update + { name = "redox_syscall" }, # https://gitlab.redox-os.org/redox-os/orbclient/-/issues/46 ] skip-tree = [] diff --git a/docs/res/ATTRIBUTION.md b/docs/res/ATTRIBUTION.md index 25f51d683e..268316f946 100644 --- a/docs/res/ATTRIBUTION.md +++ b/docs/res/ATTRIBUTION.md @@ -5,7 +5,7 @@ These images are used in the documentation of `winit`. ## keyboard_*.svg These files are a modified version of "[ANSI US QWERTY (Windows)](https://commons.wikimedia.org/wiki/File:ANSI_US_QWERTY_(Windows).svg)" -by [Tomiĉo] (https://commons.wikimedia.org/wiki/User:Tomi%C4%89o). It is +by [Tomiĉo] (https://commons.wikimedia.org/wiki/User:Tomi%C4%89o). It was originally released under the [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en) License. Minor modifications have been made by [John Nunley](https://github.com/notgull), which have been released under the same license as a derivative work. diff --git a/examples/child_window.rs b/examples/child_window.rs index 775c180c68..9234d9e4e6 100644 --- a/examples/child_window.rs +++ b/examples/child_window.rs @@ -1,16 +1,23 @@ -#[cfg(any(x11_platform, macos_platform, windows_platform))] +#[cfg(all( + feature = "rwh_06", + any(x11_platform, macos_platform, windows_platform) +))] #[path = "util/fill.rs"] mod fill; -#[cfg(any(x11_platform, macos_platform, windows_platform))] -fn main() { +#[cfg(all( + feature = "rwh_06", + any(x11_platform, macos_platform, windows_platform) +))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { use std::collections::HashMap; - use raw_window_handle::HasRawWindowHandle; use winit::{ dpi::{LogicalPosition, LogicalSize, Position}, event::{ElementState, Event, KeyEvent, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, + event_loop::{EventLoop, EventLoopWindowTarget}, + raw_window_handle::HasRawWindowHandle, window::{Window, WindowBuilder, WindowId}, }; @@ -19,7 +26,7 @@ fn main() { event_loop: &EventLoopWindowTarget<()>, windows: &mut HashMap, ) { - let parent = parent.raw_window_handle(); + let parent = parent.raw_window_handle().unwrap(); let mut builder = WindowBuilder::new() .with_title("child window") .with_inner_size(LogicalSize::new(200.0f32, 200.0f32)) @@ -36,7 +43,7 @@ fn main() { let mut windows = HashMap::new(); - let event_loop: EventLoop<()> = EventLoop::new(); + let event_loop: EventLoop<()> = EventLoop::new().unwrap(); let parent_window = WindowBuilder::new() .with_title("parent window") .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) @@ -46,14 +53,12 @@ fn main() { println!("parent window: {parent_window:?})"); - event_loop.run(move |event: Event<'_, ()>, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; - + event_loop.run(move |event: Event<()>, elwt| { if let Event::WindowEvent { event, window_id } = event { match event { WindowEvent::CloseRequested => { windows.clear(); - *control_flow = ControlFlow::Exit; + elwt.exit(); } WindowEvent::CursorEntered { device_id: _ } => { // On x11, println when the cursor entered in a window even if the child window is created @@ -70,19 +75,23 @@ fn main() { }, .. } => { - spawn_child_window(&parent_window, event_loop, &mut windows); + spawn_child_window(&parent_window, elwt, &mut windows); + } + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } } _ => (), } - } else if let Event::RedrawRequested(wid) = event { - if let Some(window) = windows.get(&wid) { - fill::fill_window(window); - } } }) } -#[cfg(not(any(x11_platform, macos_platform, windows_platform)))] +#[cfg(not(all( + feature = "rwh_06", + any(x11_platform, macos_platform, windows_platform) +)))] fn main() { - panic!("This example is supported only on x11, macOS, and Windows."); + panic!("This example is supported only on x11, macOS, and Windows, with the `rwh_06` feature enabled."); } diff --git a/examples/control_flow.rs b/examples/control_flow.rs index fd39d4761b..5032d0bffc 100644 --- a/examples/control_flow.rs +++ b/examples/control_flow.rs @@ -9,8 +9,8 @@ use web_time as time; use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyEvent, WindowEvent}, - event_loop::EventLoop, - keyboard::Key, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, NamedKey}, window::WindowBuilder, }; @@ -27,7 +27,7 @@ enum Mode { const WAIT_TIME: time::Duration = time::Duration::from_millis(100); const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); println!("Press '1' to switch to Wait mode."); @@ -36,7 +36,7 @@ fn main() { println!("Press 'R' to toggle request_redraw() calls."); println!("Press 'Esc' to close the window."); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.") .build(&event_loop) @@ -47,7 +47,7 @@ fn main() { let mut wait_cancelled = false; let mut close_requested = false; - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, elwt| { use winit::event::StartCause; println!("{event:?}"); match event { @@ -88,39 +88,41 @@ fn main() { request_redraw = !request_redraw; println!("\nrequest_redraw: {request_redraw}\n"); } - Key::Escape => { + Key::Named(NamedKey::Escape) => { close_requested = true; } _ => (), }, + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), }, - Event::MainEventsCleared => { + Event::AboutToWait => { if request_redraw && !wait_cancelled && !close_requested { window.request_redraw(); } - if close_requested { - control_flow.set_exit(); - } - } - Event::RedrawRequested(_window_id) => { - fill::fill_window(&window); - } - Event::RedrawEventsCleared => { + match mode { - Mode::Wait => control_flow.set_wait(), + Mode::Wait => elwt.set_control_flow(ControlFlow::Wait), Mode::WaitUntil => { if !wait_cancelled { - control_flow.set_wait_until(time::Instant::now() + WAIT_TIME); + elwt.set_control_flow(ControlFlow::WaitUntil( + time::Instant::now() + WAIT_TIME, + )); } } Mode::Poll => { thread::sleep(POLL_SLEEP_TIME); - control_flow.set_poll(); + elwt.set_control_flow(ControlFlow::Poll); } }; + + if close_requested { + elwt.exit(); + } } _ => (), } - }); + }) } diff --git a/examples/cursor.rs b/examples/cursor.rs index c9edf2a0cb..83ab1064ff 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -10,51 +10,44 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap(); window.set_title("A fantastic window!"); let mut cursor_idx = 0; - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - event: - KeyEvent { - state: ElementState::Pressed, - .. - }, - .. - }, - .. - } => { - println!("Setting cursor to \"{:?}\"", CURSORS[cursor_idx]); - window.set_cursor_icon(CURSORS[cursor_idx]); - if cursor_idx < CURSORS.len() - 1 { - cursor_idx += 1; - } else { - cursor_idx = 0; + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + .. + }, + .. + } => { + println!("Setting cursor to \"{:?}\"", CURSORS[cursor_idx]); + window.set_cursor_icon(CURSORS[cursor_idx]); + if cursor_idx < CURSORS.len() - 1 { + cursor_idx += 1; + } else { + cursor_idx = 0; + } } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } + WindowEvent::CloseRequested => { + elwt.exit(); + } + _ => (), } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - control_flow.set_exit(); - } - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } - _ => (), } - }); + }) } const CURSORS: &[CursorIcon] = &[ diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 72ba14e56c..27f8edfe8d 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -4,16 +4,16 @@ use simple_logger::SimpleLogger; use winit::{ event::{DeviceEvent, ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - keyboard::{Key, ModifiersState}, + keyboard::{Key, ModifiersState, NamedKey}, window::{CursorGrabMode, WindowBuilder}, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("Super Cursor Grab'n'Hide Simulator 9000") @@ -22,56 +22,52 @@ fn main() { let mut modifiers = ModifiersState::default(); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: key, - state: ElementState::Released, - .. - }, - .. - } => { - let result = match key { - Key::Escape => { - control_flow.set_exit(); + event_loop.run(move |event, elwt| match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Released, + .. + }, + .. + } => { + let result = match key { + Key::Named(NamedKey::Escape) => { + elwt.exit(); + Ok(()) + } + Key::Character(ch) => match ch.to_lowercase().as_str() { + "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_key()); Ok(()) } - Key::Character(ch) => match ch.to_lowercase().as_str() { - "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_key()); - Ok(()) - } - _ => Ok(()), - }, _ => Ok(()), - }; + }, + _ => Ok(()), + }; - if let Err(err) = result { - println!("error: {err}"); - } + if let Err(err) = result { + println!("error: {err}"); } - WindowEvent::ModifiersChanged(new) => modifiers = new.state(), - _ => (), - }, - Event::DeviceEvent { event, .. } => match event { - DeviceEvent::MouseMotion { delta } => println!("mouse moved: {delta:?}"), - DeviceEvent::Button { button, state } => match state { - ElementState::Pressed => println!("mouse button {button} pressed"), - ElementState::Released => println!("mouse button {button} released"), - }, - _ => (), + } + WindowEvent::ModifiersChanged(new) => modifiers = new.state(), + WindowEvent::RedrawRequested => fill::fill_window(&window), + _ => (), + }, + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::MouseMotion { delta } => println!("mouse moved: {delta:?}"), + DeviceEvent::Button { button, state } => match state { + ElementState::Pressed => println!("mouse button {button} pressed"), + ElementState::Released => println!("mouse button {button} released"), }, - Event::RedrawRequested(_) => fill::fill_window(&window), _ => (), - } - }); + }, + _ => (), + }) } diff --git a/examples/custom_events.rs b/examples/custom_events.rs index b3a3d2176a..50501e6758 100644 --- a/examples/custom_events.rs +++ b/examples/custom_events.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> Result<(), impl std::error::Error> { use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, @@ -18,7 +18,9 @@ fn main() { } SimpleLogger::new().init().unwrap(); - let event_loop = EventLoopBuilder::::with_user_event().build(); + let event_loop = EventLoopBuilder::::with_user_event() + .build() + .unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -38,21 +40,20 @@ fn main() { } }); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::UserEvent(event) => println!("user event: {event:?}"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => control_flow.set_exit(), - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } - _ => (), + event_loop.run(move |event, elwt| match event { + Event::UserEvent(event) => println!("user event: {event:?}"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => elwt.exit(), + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + fill::fill_window(&window); } - }); + _ => (), + }) } #[cfg(wasm_platform)] diff --git a/examples/drag_window.rs b/examples/drag_window.rs index af5c824d43..2d13ce431c 100644 --- a/examples/drag_window.rs +++ b/examples/drag_window.rs @@ -11,27 +11,25 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window_1 = WindowBuilder::new().build(&event_loop).unwrap(); let window_2 = WindowBuilder::new().build(&event_loop).unwrap(); let mut switched = false; let mut entered_id = window_2.id(); + let mut cursor_location = None; - event_loop.run(move |event, _, control_flow| match event { + event_loop.run(move |event, elwt| match event { Event::NewEvents(StartCause::Init) => { eprintln!("Switch which window is to be dragged by pressing \"x\".") } Event::WindowEvent { event, window_id } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), - WindowEvent::MouseInput { - state: ElementState::Pressed, - button: MouseButton::Left, - .. - } => { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::CursorMoved { position, .. } => cursor_location = Some(position), + WindowEvent::MouseInput { state, button, .. } => { let window = if (window_id == window_1.id() && switched) || (window_id == window_2.id() && !switched) { @@ -40,7 +38,15 @@ fn main() { &window_1 }; - window.drag_window().unwrap() + match (button, state) { + (MouseButton::Left, ElementState::Pressed) => window.drag_window().unwrap(), + (MouseButton::Right, ElementState::Released) => { + if let Some(position) = cursor_location { + window.show_window_menu(position); + } + } + _ => (), + } } WindowEvent::CursorEntered { .. } => { entered_id = window_id; @@ -54,22 +60,37 @@ fn main() { .. }, .. - } if c == "x" => { - switched = !switched; - name_windows(entered_id, switched, &window_1, &window_2); - println!("Switched!") + } => match c.as_str() { + "x" => { + switched = !switched; + name_windows(entered_id, switched, &window_1, &window_2); + println!("Switched!") + } + "d" => { + let window = if (window_id == window_1.id() && switched) + || (window_id == window_2.id() && !switched) + { + &window_2 + } else { + &window_1 + }; + + window.set_decorations(!window.is_decorated()); + } + _ => (), + }, + WindowEvent::RedrawRequested => { + if window_id == window_1.id() { + fill::fill_window(&window_1); + } else if window_id == window_2.id() { + fill::fill_window(&window_2); + } } _ => (), }, - Event::RedrawRequested(wid) => { - if wid == window_1.id() { - fill::fill_window(&window_1); - } else if wid == window_2.id() { - fill::fill_window(&window_2); - } - } + _ => (), - }); + }) } fn name_windows(window_id: WindowId, switched: bool, window_1: &Window, window_2: &Window) { diff --git a/examples/focus.rs b/examples/focus.rs new file mode 100644 index 0000000000..29d3c621db --- /dev/null +++ b/examples/focus.rs @@ -0,0 +1,56 @@ +#![allow(clippy::single_match)] + +//! Example for focusing a window. + +use simple_logger::SimpleLogger; +#[cfg(not(wasm_platform))] +use std::time; +#[cfg(wasm_platform)] +use web_time as time; +use winit::{ + event::{Event, StartCause, WindowEvent}, + event_loop::EventLoop, + window::WindowBuilder, +}; + +#[path = "util/fill.rs"] +mod fill; + +fn main() -> Result<(), impl std::error::Error> { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new().unwrap(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .build(&event_loop) + .unwrap(); + + let mut deadline = time::Instant::now() + time::Duration::from_secs(3); + event_loop.run(move |event, elwt| { + match event { + Event::NewEvents(StartCause::ResumeTimeReached { .. }) => { + // Timeout reached; focus the window. + println!("Re-focusing the window."); + deadline += time::Duration::from_secs(3); + window.focus_window(); + } + Event::WindowEvent { event, window_id } if window_id == window.id() => match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::RedrawRequested => { + // Notify the windowing system that we'll be presenting to the window. + window.pre_present_notify(); + fill::fill_window(&window); + } + _ => (), + }, + Event::AboutToWait => { + window.request_redraw(); + } + + _ => (), + } + + elwt.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline)); + }) +} diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index e38390ebc8..fca98401c9 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,9 +1,10 @@ #![allow(clippy::single_match)] use simple_logger::SimpleLogger; +use winit::dpi::PhysicalSize; use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; use winit::event_loop::EventLoop; -use winit::keyboard::Key; +use winit::keyboard::{Key, NamedKey}; use winit::window::{Fullscreen, WindowBuilder}; #[cfg(target_os = "macos")] @@ -12,12 +13,14 @@ use winit::platform::macos::WindowExtMacOS; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let mut decorations = true; let mut minimized = false; + let mut with_min_size = false; + let mut with_max_size = false; let window = WindowBuilder::new() .with_title("Hello world!") @@ -46,13 +49,13 @@ fn main() { println!("- D\tToggle window decorations"); println!("- X\tMaximize window"); println!("- Z\tMinimize window"); + println!("- I\tToggle mIn size limit"); + println!("- A\tToggle mAx size limit"); - event_loop.run(move |event, elwt, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::KeyboardInput { event: KeyEvent { @@ -62,7 +65,7 @@ fn main() { }, .. } => match key { - Key::Escape => control_flow.set_exit(), + Key::Named(NamedKey::Escape) => elwt.exit(), // WARNING: Consider using `key_without_modifers()` if available on your platform. // See the `key_binding` example Key::Character(ch) => match ch.to_lowercase().as_str() { @@ -120,16 +123,41 @@ fn main() { minimized = !minimized; window.set_minimized(minimized); } + "i" => { + with_min_size = !with_min_size; + let min_size = if with_min_size { + Some(PhysicalSize::new(100, 100)) + } else { + None + }; + window.set_min_inner_size(min_size); + eprintln!( + "Min: {with_min_size}: {min_size:?} => {:?}", + window.inner_size() + ); + } + "a" => { + with_max_size = !with_max_size; + let max_size = if with_max_size { + Some(PhysicalSize::new(200, 200)) + } else { + None + }; + window.set_max_inner_size(max_size); + eprintln!( + "Max: {with_max_size}: {max_size:?} => {:?}", + window.inner_size() + ); + } _ => (), }, _ => (), }, + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), - }, - Event::RedrawRequested(_) => { - fill::fill_window(&window); } - _ => {} } - }); + }) } diff --git a/examples/handling_close.rs b/examples/handling_close.rs index c5d95471a6..fdc671d25d 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -11,9 +11,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("Your faithful window") @@ -22,70 +22,65 @@ fn main() { let mut close_requested = false; - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => { + // `CloseRequested` is sent when the close button on the window is pressed (or + // through whatever other mechanisms the window manager provides for closing a + // window). If you don't handle this event, the close button won't actually do + // anything. - match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CloseRequested => { - // `CloseRequested` is sent when the close button on the window is pressed (or - // through whatever other mechanisms the window manager provides for closing a - // window). If you don't handle this event, the close button won't actually do - // anything. + // A common thing to do here is prompt the user if they have unsaved work. + // Creating a proper dialog box for that is far beyond the scope of this + // example, so here we'll just respond to the Y and N keys. + println!("Are you ready to bid your window farewell? [Y/N]"); + close_requested = true; - // A common thing to do here is prompt the user if they have unsaved work. - // Creating a proper dialog box for that is far beyond the scope of this - // example, so here we'll just respond to the Y and N keys. - println!("Are you ready to bid your window farewell? [Y/N]"); - close_requested = true; - - // In applications where you can safely close the window without further - // action from the user, this is generally where you'd handle cleanup before - // closing the window. How to close the window is detailed in the handler for - // the Y key. - } - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: key, - state: ElementState::Released, - .. - }, - .. - } => { - // WARNING: Consider using `key_without_modifers()` if available on your platform. - // See the `key_binding` example - match key.as_ref() { - Key::Character("y") => { - if close_requested { - // This is where you'll want to do any cleanup you need. - println!("Buh-bye!"); + // In applications where you can safely close the window without further + // action from the user, this is generally where you'd handle cleanup before + // closing the window. How to close the window is detailed in the handler for + // the Y key. + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Released, + .. + }, + .. + } => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + match key.as_ref() { + Key::Character("y") => { + if close_requested { + // This is where you'll want to do any cleanup you need. + println!("Buh-bye!"); - // For a single-window application like this, you'd normally just - // break out of the event loop here. If you wanted to keep running the - // event loop (i.e. if it's a multi-window application), you need to - // drop the window. That closes it, and results in `Destroyed` being - // sent. - control_flow.set_exit(); - } + // For a single-window application like this, you'd normally just + // break out of the event loop here. If you wanted to keep running the + // event loop (i.e. if it's a multi-window application), you need to + // drop the window. That closes it, and results in `Destroyed` being + // sent. + elwt.exit(); } - Key::Character("n") => { - if close_requested { - println!("Your window will continue to stay by your side."); - close_requested = false; - } + } + Key::Character("n") => { + if close_requested { + println!("Your window will continue to stay by your side."); + close_requested = false; } - _ => (), } + _ => (), } - _ => (), } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } + _ => (), } - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } - _ => (), } - }); + }) } diff --git a/examples/ime.rs b/examples/ime.rs index d108c8fc6d..79c2416411 100644 --- a/examples/ime.rs +++ b/examples/ime.rs @@ -5,15 +5,15 @@ use simple_logger::SimpleLogger; use winit::{ dpi::{PhysicalPosition, PhysicalSize}, event::{ElementState, Event, Ime, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - keyboard::{Key, KeyCode}, + event_loop::EventLoop, + keyboard::NamedKey, window::{ImePurpose, WindowBuilder}, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new() .with_level(LevelFilter::Trace) .init() @@ -24,7 +24,7 @@ fn main() { println!("Press F2 to toggle IME. See the documentation of `set_ime_allowed` for more info"); println!("Press F3 to cycle through IME purposes."); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_inner_size(winit::dpi::LogicalSize::new(256f64, 128f64)) @@ -39,71 +39,56 @@ fn main() { 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_cursor_area(ime_pos, PhysicalSize::new(10, 10)); + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; } - } - Event::WindowEvent { - event: WindowEvent::Ime(event), - .. - } => { - println!("{event:?}"); - may_show_ime = event != Ime::Disabled; - if may_show_ime { - window.set_ime_cursor_area(ime_pos, PhysicalSize::new(10, 10)); + 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_cursor_area(ime_pos, PhysicalSize::new(10, 10)); + } } - } - Event::WindowEvent { - event: WindowEvent::KeyboardInput { event, .. }, - .. - } => { - println!("key: {event:?}"); + WindowEvent::Ime(event) => { + println!("{event:?}"); + may_show_ime = event != Ime::Disabled; + if may_show_ime { + window.set_ime_cursor_area(ime_pos, PhysicalSize::new(10, 10)); + } + } + WindowEvent::KeyboardInput { event, .. } => { + println!("key: {event:?}"); - if event.state == ElementState::Pressed && event.physical_key == KeyCode::F2 { - ime_allowed = !ime_allowed; - window.set_ime_allowed(ime_allowed); - println!("\nIME allowed: {ime_allowed}\n"); + if event.state == ElementState::Pressed && event.logical_key == NamedKey::F2 { + ime_allowed = !ime_allowed; + window.set_ime_allowed(ime_allowed); + println!("\nIME allowed: {ime_allowed}\n"); + } + if event.state == ElementState::Pressed && event.logical_key == NamedKey::F3 { + ime_purpose = match ime_purpose { + ImePurpose::Normal => ImePurpose::Password, + ImePurpose::Password => ImePurpose::Terminal, + _ => ImePurpose::Normal, + }; + window.set_ime_purpose(ime_purpose); + println!("\nIME purpose: {ime_purpose:?}\n"); + } } - if event.state == ElementState::Pressed && event.logical_key == Key::F3 { - ime_purpose = match ime_purpose { - ImePurpose::Normal => ImePurpose::Password, - ImePurpose::Password => ImePurpose::Terminal, - _ => ImePurpose::Normal, - }; - window.set_ime_purpose(ime_purpose); - println!("\nIME purpose: {ime_purpose:?}\n"); + WindowEvent::RedrawRequested => { + fill::fill_window(&window); } + _ => (), } - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } - _ => (), } - }); + }) } diff --git a/examples/key_binding.rs b/examples/key_binding.rs index 7d4968f3a1..b640889708 100644 --- a/examples/key_binding.rs +++ b/examples/key_binding.rs @@ -4,7 +4,7 @@ use winit::{ dpi::LogicalSize, event::{ElementState, Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::EventLoop, keyboard::{Key, ModifiersState}, // WARNING: This is not available on all platforms (for example on the web). platform::modifier_supplement::KeyEventExtModifierSupplement, @@ -17,12 +17,12 @@ fn main() { } #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] -fn main() { +fn main() -> Result<(), impl std::error::Error> { #[path = "util/fill.rs"] mod fill; simple_logger::SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_inner_size(LogicalSize::new(400.0, 200.0)) @@ -31,12 +31,10 @@ fn main() { let mut modifiers = ModifiersState::default(); - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::ModifiersChanged(new) => { modifiers = new.state(); } @@ -54,12 +52,11 @@ fn main() { } } } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), - }, - Event::RedrawRequested(_) => { - fill::fill_window(&window); } - _ => (), }; - }); + }) } diff --git a/examples/monitor_list.rs b/examples/monitor_list.rs index 23081b63f9..b156028d61 100644 --- a/examples/monitor_list.rs +++ b/examples/monitor_list.rs @@ -7,7 +7,7 @@ use winit::{event_loop::EventLoop, window::WindowBuilder}; fn main() { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap(); if let Some(mon) = window.primary_monitor() { diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index ea0c962bbe..cda0673a0e 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -10,9 +10,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("Mouse Wheel events") @@ -34,12 +34,10 @@ In both cases the example window should move like the content of a scroll area i In other words, the deltas indicate the direction in which to move the content (in this case the window)." ); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::MouseWheel { delta, .. } => match delta { winit::event::MouseScrollDelta::LineDelta(x, y) => { println!("mouse wheel Line Delta: ({x},{y})"); @@ -57,12 +55,11 @@ In other words, the deltas indicate the direction in which to move the content ( window.set_outer_position(pos) } }, + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), - }, - Event::RedrawRequested(_) => { - fill::fill_window(&window); } - _ => (), } - }); + }) } diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index d1bff70f85..2b02d8fd3c 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> Result<(), impl std::error::Error> { use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; use simple_logger::SimpleLogger; @@ -9,7 +9,7 @@ fn main() { dpi::{PhysicalPosition, PhysicalSize, Position, Size}, event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - keyboard::{Key, ModifiersState}, + keyboard::{Key, ModifiersState, NamedKey}, window::{CursorGrabMode, CursorIcon, Fullscreen, WindowBuilder, WindowLevel}, }; @@ -17,7 +17,7 @@ fn main() { const WINDOW_SIZE: PhysicalSize = PhysicalSize::new(600, 400); SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let mut window_senders = HashMap::with_capacity(WINDOW_COUNT); for _ in 0..WINDOW_COUNT { let window = WindowBuilder::new() @@ -65,17 +65,17 @@ fn main() { }, .. } => { - use Key::{ArrowLeft, ArrowRight}; + use NamedKey::{ArrowLeft, ArrowRight}; window.set_title(&format!("{key:?}")); let state = !modifiers.shift_key(); match key { // Cycle through video modes - Key::ArrowRight | Key::ArrowLeft => { - video_mode_id = match key { - ArrowLeft => video_mode_id.saturating_sub(1), - ArrowRight => (video_modes.len() - 1).min(video_mode_id + 1), - _ => unreachable!(), - }; + Key::Named(ArrowRight) | Key::Named(ArrowLeft) => { + if key == ArrowLeft { + video_mode_id = video_mode_id.saturating_sub(1); + } else if key == ArrowRight { + video_mode_id = (video_modes.len() - 1).min(video_mode_id + 1); + } println!("Picking video mode: {}", video_modes[video_mode_id]); } // WARNING: Consider using `key_without_modifers()` if available on your platform. @@ -137,13 +137,15 @@ fn main() { }), "q" => window.request_redraw(), "r" => window.set_resizable(state), - "s" => window.set_inner_size(match state { - true => PhysicalSize::new( - WINDOW_SIZE.width + 100, - WINDOW_SIZE.height + 100, - ), - false => WINDOW_SIZE, - }), + "s" => { + let _ = window.request_inner_size(match state { + true => PhysicalSize::new( + WINDOW_SIZE.width + 100, + WINDOW_SIZE.height + 100, + ), + false => WINDOW_SIZE, + }); + } "w" => { if let Size::Physical(size) = WINDOW_SIZE.into() { window @@ -171,11 +173,10 @@ fn main() { } }); } - event_loop.run(move |event, _event_loop, control_flow| { - match !window_senders.is_empty() { - true => control_flow.set_wait(), - false => control_flow.set_exit(), - }; + event_loop.run(move |event, elwt| { + if window_senders.is_empty() { + elwt.exit() + } match event { Event::WindowEvent { event, window_id } => match event { WindowEvent::CloseRequested @@ -184,7 +185,7 @@ fn main() { event: KeyEvent { state: ElementState::Released, - logical_key: Key::Escape, + logical_key: Key::Named(NamedKey::Escape), .. }, .. @@ -193,9 +194,7 @@ fn main() { } _ => { if let Some(tx) = window_senders.get(&window_id) { - if let Some(event) = event.to_static() { - tx.send(event).unwrap(); - } + tx.send(event).unwrap(); } } }, diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 03812afc6f..07a9b84eef 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -4,18 +4,18 @@ use std::collections::HashMap; use simple_logger::SimpleLogger; use winit::{ - event::{ElementState, Event, KeyEvent, WindowEvent}, + event::{ElementState, Event, WindowEvent}, event_loop::EventLoop, - keyboard::Key, + keyboard::{Key, NamedKey}, window::Window, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let mut windows = HashMap::new(); for _ in 0..3 { @@ -26,45 +26,39 @@ fn main() { println!("Press N to open a new window."); - event_loop.run(move |event, event_loop, control_flow| { - control_flow.set_wait(); + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, window_id } = event { + match event { + WindowEvent::CloseRequested => { + println!("Window {window_id:?} has received the signal to close"); - match event { - Event::WindowEvent { event, window_id } => { - match event { - WindowEvent::CloseRequested => { - println!("Window {window_id:?} has received the signal to close"); + // This drops the window, causing it to close. + windows.remove(&window_id); - // This drops the window, causing it to close. - windows.remove(&window_id); - - if windows.is_empty() { - control_flow.set_exit(); - } + if windows.is_empty() { + elwt.exit(); } - WindowEvent::KeyboardInput { - event: - KeyEvent { - state: ElementState::Pressed, - logical_key: Key::Character(c), - .. - }, - is_synthetic: false, - .. - } if matches!(c.as_ref(), "n" | "N") => { - let window = Window::new(event_loop).unwrap(); + } + WindowEvent::KeyboardInput { + event, + is_synthetic: false, + .. + } if event.state == ElementState::Pressed => match event.logical_key { + Key::Named(NamedKey::Escape) => elwt.exit(), + Key::Character(c) if c == "n" || c == "N" => { + let window = Window::new(elwt).unwrap(); println!("Opened a new window: {:?}", window.id()); windows.insert(window.id(), window); } _ => (), + }, + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } } + _ => (), } - Event::RedrawRequested(window_id) => { - if let Some(window) = windows.get(&window_id) { - fill::fill_window(window); - } - } - _ => (), } }) } diff --git a/examples/request_redraw.rs b/examples/request_redraw.rs index 16ec6ca7c2..a058bd6019 100644 --- a/examples/request_redraw.rs +++ b/examples/request_redraw.rs @@ -10,36 +10,33 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, elwt| { println!("{event:?}"); - control_flow.set_wait(); - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::MouseInput { state: ElementState::Released, .. } => { window.request_redraw(); } + WindowEvent::RedrawRequested => { + println!("\nredrawing!\n"); + fill::fill_window(&window); + } _ => (), - }, - Event::RedrawRequested(_) => { - println!("\nredrawing!\n"); - fill::fill_window(&window); } - _ => (), } - }); + }) } diff --git a/examples/request_redraw_threaded.rs b/examples/request_redraw_threaded.rs index afe874978a..6070c2ae0e 100644 --- a/examples/request_redraw_threaded.rs +++ b/examples/request_redraw_threaded.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> Result<(), impl std::error::Error> { use std::{sync::Arc, thread, time}; use simple_logger::SimpleLogger; @@ -15,7 +15,7 @@ fn main() { mod fill; SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = { let window = WindowBuilder::new() @@ -33,23 +33,24 @@ fn main() { } }); - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, elwt| { println!("{event:?}"); - control_flow.set_wait(); - match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. - } => control_flow.set_exit(), - Event::RedrawRequested(_) => { + } => elwt.exit(), + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { println!("\nredrawing!\n"); fill::fill_window(&window); } _ => (), } - }); + }) } #[cfg(wasm_platform)] diff --git a/examples/resizable.rs b/examples/resizable.rs index b0c0f6ea7d..134c34388d 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -5,16 +5,16 @@ use winit::{ dpi::LogicalSize, event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - keyboard::KeyCode, + keyboard::{KeyCode, PhysicalKey}, window::WindowBuilder, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let mut resizable = false; @@ -27,16 +27,14 @@ fn main() { .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => control_flow.set_exit(), + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::KeyboardInput { event: KeyEvent { - physical_key: KeyCode::Space, + physical_key: PhysicalKey::Code(KeyCode::Space), state: ElementState::Released, .. }, @@ -46,12 +44,11 @@ fn main() { println!("Resizable: {resizable}"); window.set_resizable(resizable); } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), - }, - Event::RedrawRequested(_) => { - fill::fill_window(&window); } - _ => (), }; - }); + }) } diff --git a/examples/startup_notification.rs b/examples/startup_notification.rs new file mode 100644 index 0000000000..0cbf4d3652 --- /dev/null +++ b/examples/startup_notification.rs @@ -0,0 +1,116 @@ +//! Demonstrates the use of startup notifications on Linux. + +#[cfg(any(x11_platform, wayland_platform))] +#[path = "./util/fill.rs"] +mod fill; + +#[cfg(any(x11_platform, wayland_platform))] +mod example { + use std::collections::HashMap; + use std::rc::Rc; + + use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; + use winit::event_loop::EventLoop; + use winit::platform::startup_notify::{ + EventLoopExtStartupNotify, WindowBuilderExtStartupNotify, WindowExtStartupNotify, + }; + use winit::window::{Window, WindowBuilder, WindowId}; + + pub(super) fn main() -> Result<(), impl std::error::Error> { + // Create the event loop and get the activation token. + let event_loop = EventLoop::new().unwrap(); + let mut current_token = match event_loop.read_token_from_env() { + Some(token) => Some(token), + None => { + println!("No startup notification token found in environment."); + None + } + }; + + let mut windows: HashMap> = HashMap::new(); + let mut counter = 0; + let mut create_first_window = false; + + event_loop.run(move |event, elwt| { + match event { + Event::Resumed => create_first_window = true, + + Event::WindowEvent { window_id, event } => match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key, + state: ElementState::Pressed, + .. + }, + .. + } => { + if logical_key == "n" { + if let Some(window) = windows.get(&window_id) { + // Request a new activation token on this window. + // Once we get it we will use it to create a window. + window + .request_activation_token() + .expect("Failed to request activation token."); + } + } + } + + WindowEvent::CloseRequested => { + // Remove the window from the map. + windows.remove(&window_id); + if windows.is_empty() { + elwt.exit(); + return; + } + } + + WindowEvent::ActivationTokenDone { token, .. } => { + current_token = Some(token); + } + + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + super::fill::fill_window(window); + } + } + + _ => {} + }, + _ => (), + } + + // See if we've passed the deadline. + if current_token.is_some() || create_first_window { + // Create the initial window. + let window = { + let mut builder = + WindowBuilder::new().with_title(format!("Window {}", counter)); + + if let Some(token) = current_token.take() { + println!("Creating a window with token {token:?}"); + builder = builder.with_activation_token(token); + } + + Rc::new(builder.build(elwt).unwrap()) + }; + + // Add the window to the map. + windows.insert(window.id(), window.clone()); + + counter += 1; + create_first_window = false; + } + }) + } +} + +#[cfg(any(x11_platform, wayland_platform))] +fn main() -> Result<(), impl std::error::Error> { + example::main() +} + +#[cfg(not(any(x11_platform, wayland_platform)))] +fn main() { + println!("This example is only supported on X11 and Wayland platforms."); +} diff --git a/examples/theme.rs b/examples/theme.rs index 7b132a9d83..6b7f8a4f11 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -3,7 +3,7 @@ use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyEvent, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::EventLoop, keyboard::Key, window::{Theme, WindowBuilder}, }; @@ -11,9 +11,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -27,53 +27,42 @@ fn main() { println!(" (L) Light theme"); println!(" (D) Dark theme"); - 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::ThemeChanged(theme), - window_id, - .. - } if window_id == window.id() => { - println!("Theme is changed: {theme:?}") - } - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: key, - state: ElementState::Pressed, - .. - }, - .. - }, - .. - } => match key.as_ref() { - Key::Character("A" | "a") => { - println!("Theme was: {:?}", window.theme()); - window.set_theme(None); - } - Key::Character("L" | "l") => { - println!("Theme was: {:?}", window.theme()); - window.set_theme(Some(Theme::Light)); + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { window_id, event } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::ThemeChanged(theme) if window_id == window.id() => { + println!("Theme is changed: {theme:?}") } - Key::Character("D" | "d") => { - println!("Theme was: {:?}", window.theme()); - window.set_theme(Some(Theme::Dark)); + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Pressed, + .. + }, + .. + } => match key.as_ref() { + Key::Character("A" | "a") => { + println!("Theme was: {:?}", window.theme()); + window.set_theme(None); + } + Key::Character("L" | "l") => { + println!("Theme was: {:?}", window.theme()); + window.set_theme(Some(Theme::Light)); + } + Key::Character("D" | "d") => { + println!("Theme was: {:?}", window.theme()); + window.set_theme(Some(Theme::Dark)); + } + _ => (), + }, + WindowEvent::RedrawRequested => { + println!("\nredrawing!\n"); + fill::fill_window(&window); } _ => (), - }, - Event::RedrawRequested(_) => { - println!("\nredrawing!\n"); - fill::fill_window(&window); } - _ => (), } - }); + }) } diff --git a/examples/timer.rs b/examples/timer.rs index 273cc638e1..62c0eee4bb 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -9,16 +9,16 @@ use web_time::Instant; use simple_logger::SimpleLogger; use winit::{ event::{Event, StartCause, WindowEvent}, - event_loop::EventLoop, + event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -27,25 +27,28 @@ fn main() { let timer_length = Duration::new(1, 0); - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, elwt| { println!("{event:?}"); match event { Event::NewEvents(StartCause::Init) => { - control_flow.set_wait_until(Instant::now() + timer_length); + elwt.set_control_flow(ControlFlow::WaitUntil(Instant::now() + timer_length)); } Event::NewEvents(StartCause::ResumeTimeReached { .. }) => { - control_flow.set_wait_until(Instant::now() + timer_length); + elwt.set_control_flow(ControlFlow::WaitUntil(Instant::now() + timer_length)); println!("\nTimer\n"); } Event::WindowEvent { event: WindowEvent::CloseRequested, .. - } => control_flow.set_exit(), - Event::RedrawRequested(_) => { + } => elwt.exit(), + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { fill::fill_window(&window); } _ => (), } - }); + }) } diff --git a/examples/touchpad_gestures.rs b/examples/touchpad_gestures.rs index 2ebe83623b..6d9015bf02 100644 --- a/examples/touchpad_gestures.rs +++ b/examples/touchpad_gestures.rs @@ -1,16 +1,16 @@ use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::EventLoop, window::WindowBuilder, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("Touchpad gestures") @@ -19,12 +19,10 @@ fn main() { println!("Only supported on macOS at the moment."); - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - + event_loop.run(move |event, elwt| { if let Event::WindowEvent { event, .. } = event { match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::TouchpadMagnify { delta, .. } => { if delta > 0.0 { println!("Zoomed in {delta}"); @@ -42,10 +40,11 @@ fn main() { println!("Rotated clockwise {delta}"); } } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), } - } else if let Event::RedrawRequested(_) = event { - fill::fill_window(&window); } - }); + }) } diff --git a/examples/transparent.rs b/examples/transparent.rs index 134be3adad..ee149a87d0 100644 --- a/examples/transparent.rs +++ b/examples/transparent.rs @@ -10,9 +10,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_decorations(false) @@ -22,19 +22,17 @@ fn main() { window.set_title("A fantastic window!"); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); + event_loop.run(move |event, elwt| { println!("{event:?}"); - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => control_flow.set_exit(), - Event::RedrawRequested(_) => { - fill::fill_window(&window); + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } + _ => (), } - _ => (), } - }); + }) } diff --git a/examples/util/fill.rs b/examples/util/fill.rs index 7f1f1b1f63..4fcf67f4e5 100644 --- a/examples/util/fill.rs +++ b/examples/util/fill.rs @@ -7,17 +7,30 @@ //! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could //! also be used to fill the window buffer, but they are more complicated to use. -use winit::window::Window; +#[allow(unused_imports)] +pub use platform::cleanup_window; +pub use platform::fill_window; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub(super) fn fill_window(window: &Window) { - use softbuffer::{Context, Surface}; +#[cfg(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios"))))] +mod platform { use std::cell::RefCell; use std::collections::HashMap; use std::mem::ManuallyDrop; use std::num::NonZeroU32; + + use softbuffer::{Context, Surface}; + use winit::window::Window; use winit::window::WindowId; + thread_local! { + // NOTE: You should never do things like that, create context and drop it before + // you drop the event loop. We do this for brevity to not blow up examples. We use + // ManuallyDrop to prevent destructors from running. + // + // A static, thread-local map of graphics contexts to open windows. + static GC: ManuallyDrop>> = const { ManuallyDrop::new(RefCell::new(None)) }; + } + /// The graphics context used to draw to a window. struct GraphicsContext { /// The global softbuffer context. @@ -35,52 +48,69 @@ pub(super) fn fill_window(window: &Window) { } } - fn surface(&mut self, w: &Window) -> &mut Surface { - self.surfaces.entry(w.id()).or_insert_with(|| { - unsafe { Surface::new(&self.context, w) } + fn create_surface(&mut self, window: &Window) -> &mut Surface { + self.surfaces.entry(window.id()).or_insert_with(|| { + unsafe { Surface::new(&self.context, window) } .expect("Failed to create a softbuffer surface") }) } + + fn destroy_surface(&mut self, window: &Window) { + self.surfaces.remove(&window.id()); + } } - thread_local! { - // NOTE: You should never do things like that, create context and drop it before - // you drop the event loop. We do this for brevity to not blow up examples. We use - // ManuallyDrop to prevent destructors from running. - // - // A static, thread-local map of graphics contexts to open windows. - static GC: ManuallyDrop>> = ManuallyDrop::new(RefCell::new(None)); + pub fn fill_window(window: &Window) { + GC.with(|gc| { + let size = window.inner_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = gc + .get_or_insert_with(|| GraphicsContext::new(window)) + .create_surface(window); + + // Fill a buffer with a solid color. + const DARK_GRAY: u32 = 0xFF181818; + + surface + .resize(width, height) + .expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface + .buffer_mut() + .expect("Failed to get the softbuffer buffer"); + buffer.fill(DARK_GRAY); + buffer + .present() + .expect("Failed to present the softbuffer buffer"); + }) } - GC.with(|gc| { - // Either get the last context used or create a new one. - let mut gc = gc.borrow_mut(); - let surface = gc - .get_or_insert_with(|| GraphicsContext::new(window)) - .surface(window); - - // Fill a buffer with a solid color. - const DARK_GRAY: u32 = 0xFF181818; - let size = window.inner_size(); - - surface - .resize( - NonZeroU32::new(size.width).expect("Width must be greater than zero"), - NonZeroU32::new(size.height).expect("Height must be greater than zero"), - ) - .expect("Failed to resize the softbuffer surface"); - - let mut buffer = surface - .buffer_mut() - .expect("Failed to get the softbuffer buffer"); - buffer.fill(DARK_GRAY); - buffer - .present() - .expect("Failed to present the softbuffer buffer"); - }) + #[allow(dead_code)] + pub fn cleanup_window(window: &Window) { + GC.with(|gc| { + let mut gc = gc.borrow_mut(); + if let Some(context) = gc.as_mut() { + context.destroy_surface(window); + } + }); + } } -#[cfg(any(target_os = "android", target_os = "ios"))] -pub(super) fn fill_window(_window: &Window) { - // No-op on mobile platforms. +#[cfg(not(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios")))))] +mod platform { + pub fn fill_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } + + #[allow(dead_code)] + pub fn cleanup_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } } diff --git a/examples/video_modes.rs b/examples/video_modes.rs index 3b262c7b60..75acdbd45b 100644 --- a/examples/video_modes.rs +++ b/examples/video_modes.rs @@ -5,7 +5,7 @@ use winit::event_loop::EventLoop; fn main() { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let monitor = match event_loop.primary_monitor() { Some(monitor) => monitor, None => { diff --git a/examples/web.rs b/examples/web.rs index c1df698773..febc903a38 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -3,24 +3,25 @@ use winit::{ event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - keyboard::KeyCode, + keyboard::Key, window::{Fullscreen, WindowBuilder}, }; -pub fn main() { - let event_loop = EventLoop::new(); +pub fn main() -> Result<(), impl std::error::Error> { + let event_loop = EventLoop::new().unwrap(); - let window = WindowBuilder::new() - .with_title("A fantastic window!") - .build(&event_loop) - .unwrap(); + let builder = WindowBuilder::new().with_title("A fantastic window!"); + #[cfg(wasm_platform)] + let builder = { + use winit::platform::web::WindowBuilderExtWebSys; + builder.with_append(true) + }; + let window = builder.build(&event_loop).unwrap(); #[cfg(wasm_platform)] let log_list = wasm::insert_canvas_and_create_log_list(&window); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - + event_loop.run(move |event, elwt| { #[cfg(wasm_platform)] wasm::log_event(&log_list, &event); @@ -28,8 +29,8 @@ pub fn main() { Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::MainEventsCleared => { + } if window_id == window.id() => elwt.exit(), + Event::AboutToWait => { window.request_redraw(); } Event::WindowEvent { @@ -38,13 +39,13 @@ pub fn main() { WindowEvent::KeyboardInput { event: KeyEvent { - physical_key: KeyCode::KeyF, + logical_key: Key::Character(c), state: ElementState::Released, .. }, .. }, - } if window_id == window.id() => { + } if window_id == window.id() && c == "f" => { if window.fullscreen().is_some() { window.set_fullscreen(None); } else { @@ -53,37 +54,52 @@ pub fn main() { } _ => (), } - }); + }) } #[cfg(wasm_platform)] mod wasm { + use std::num::NonZeroU32; + + use softbuffer::{Surface, SurfaceExtWeb}; use wasm_bindgen::prelude::*; - use winit::{event::Event, window::Window}; + use winit::{ + event::{Event, WindowEvent}, + window::Window, + }; #[wasm_bindgen(start)] pub fn run() { console_log::init_with_level(log::Level::Debug).expect("error initializing logger"); #[allow(clippy::main_recursion)] - super::main(); + let _ = super::main(); } pub fn insert_canvas_and_create_log_list(window: &Window) -> web_sys::Element { use winit::platform::web::WindowExtWebSys; let canvas = window.canvas().unwrap(); + let mut surface = Surface::from_canvas(canvas.clone()).unwrap(); + surface + .resize( + NonZeroU32::new(canvas.width()).unwrap(), + NonZeroU32::new(canvas.height()).unwrap(), + ) + .unwrap(); + let mut buffer = surface.buffer_mut().unwrap(); + buffer.fill(0xFFF0000); + buffer.present().unwrap(); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); - // Set a background color for the canvas to make it easier to tell where the canvas is for debugging purposes. - canvas - .style() - .set_property("background-color", "crimson") - .unwrap(); - body.append_child(&canvas).unwrap(); + let style = &canvas.style(); + style.set_property("margin", "50px").unwrap(); + // Use to test interactions with border and padding. + //style.set_property("border", "50px solid black").unwrap(); + //style.set_property("padding", "50px").unwrap(); let log_header = document.create_element("h2").unwrap(); log_header.set_text_content(Some("Event Log")); @@ -100,11 +116,29 @@ mod wasm { // Getting access to browser logs requires a lot of setup on mobile devices. // So we implement this basic logging system into the page to give developers an easy alternative. // As a bonus its also kind of handy on desktop. - if let Event::WindowEvent { event, .. } = &event { + let event = match event { + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => None, + Event::WindowEvent { event, .. } => Some(format!("{event:?}")), + Event::Resumed | Event::Suspended => Some(format!("{event:?}")), + _ => None, + }; + if let Some(event) = event { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let log = document.create_element("li").unwrap(); - log.set_text_content(Some(&format!("{event:?}"))); + + let date = js_sys::Date::new_0(); + log.set_text_content(Some(&format!( + "{:02}:{:02}:{:02}.{:03}: {event}", + date.get_hours(), + date.get_minutes(), + date.get_seconds(), + date.get_milliseconds(), + ))); + log_list .insert_before(&log, log_list.first_child().as_ref()) .unwrap(); diff --git a/examples/web_aspect_ratio.rs b/examples/web_aspect_ratio.rs index cd832af44b..c13b456c04 100644 --- a/examples/web_aspect_ratio.rs +++ b/examples/web_aspect_ratio.rs @@ -7,12 +7,12 @@ pub fn main() { #[cfg(wasm_platform)] mod wasm { use wasm_bindgen::prelude::*; - use wasm_bindgen::JsCast; use web_sys::HtmlCanvasElement; use winit::{ dpi::PhysicalSize, event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::EventLoop, + platform::web::WindowBuilderExtWebSys, window::{Window, WindowBuilder}, }; @@ -30,13 +30,14 @@ This example demonstrates the desired future functionality which will possibly b #[wasm_bindgen(start)] pub fn run() { console_log::init_with_level(log::Level::Debug).expect("error initializing logger"); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") // When running in a non-wasm environment this would set the window size to 100x100. // However in this example it just sets a default initial size of 100x100 that is immediately overwritten due to the layout + styling of the page. .with_inner_size(PhysicalSize::new(100, 100)) + .with_append(true) .build(&event_loop) .unwrap(); @@ -45,18 +46,14 @@ This example demonstrates the desired future functionality which will possibly b // Render once with the size info we currently have render_circle(&canvas, window.inner_size()); - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { - event: WindowEvent::Resized(resize), - window_id, - } if window_id == window.id() => { - render_circle(&canvas, resize); - } - _ => (), + let _ = event_loop.run(move |event, _| match event { + Event::WindowEvent { + event: WindowEvent::Resized(resize), + window_id, + } if window_id == window.id() => { + render_circle(&canvas, resize); } + _ => (), }); } @@ -72,7 +69,6 @@ This example demonstrates the desired future functionality which will possibly b canvas .style() .set_css_text("display: block; background-color: crimson; margin: auto; width: 50%; aspect-ratio: 4 / 1;"); - body.append_child(&canvas).unwrap(); let explanation = document.create_element("pre").unwrap(); explanation.set_text_content(Some(EXPLANATION)); diff --git a/examples/window.rs b/examples/window.rs index 4a5d8b0068..488aa38af0 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -10,9 +10,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -20,22 +20,24 @@ fn main() { .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); + event_loop.run(move |event, elwt| { println!("{event:?}"); match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::MainEventsCleared => { + Event::WindowEvent { event, window_id } if window_id == window.id() => match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::RedrawRequested => { + // Notify the windowing system that we'll be presenting to the window. + window.pre_present_notify(); + fill::fill_window(&window); + } + _ => (), + }, + Event::AboutToWait => { window.request_redraw(); } - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } + _ => (), } - }); + }) } diff --git a/examples/window_buttons.rs b/examples/window_buttons.rs index 1f28412c4c..d5b8a5be98 100644 --- a/examples/window_buttons.rs +++ b/examples/window_buttons.rs @@ -14,9 +14,9 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -31,45 +31,38 @@ fn main() { event_loop.listen_device_events(DeviceEvents::Always); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: key, - state: ElementState::Pressed, - .. - }, - .. - }, - .. - } => match key.as_ref() { - Key::Character("F" | "f") => { - let buttons = window.enabled_buttons(); - window.set_enabled_buttons(buttons ^ WindowButtons::CLOSE); - } - Key::Character("G" | "g") => { - let buttons = window.enabled_buttons(); - window.set_enabled_buttons(buttons ^ WindowButtons::MAXIMIZE); - } - Key::Character("H" | "h") => { - let buttons = window.enabled_buttons(); - window.set_enabled_buttons(buttons ^ WindowButtons::MINIMIZE); + event_loop.run(move |event, elwt| { + if let Event::WindowEvent { window_id, event } = event { + match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Pressed, + .. + }, + .. + } => match key.as_ref() { + Key::Character("F" | "f") => { + let buttons = window.enabled_buttons(); + window.set_enabled_buttons(buttons ^ WindowButtons::CLOSE); + } + Key::Character("G" | "g") => { + let buttons = window.enabled_buttons(); + window.set_enabled_buttons(buttons ^ WindowButtons::MAXIMIZE); + } + Key::Character("H" | "h") => { + let buttons = window.enabled_buttons(); + window.set_enabled_buttons(buttons ^ WindowButtons::MINIMIZE); + } + _ => (), + }, + WindowEvent::CloseRequested if window_id == window.id() => elwt.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(&window); } _ => (), - }, - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::RedrawRequested(_) => { - fill::fill_window(&window); } - _ => (), } - }); + }) } diff --git a/examples/window_debug.rs b/examples/window_debug.rs index d1b3ba3d7e..c6bf5b9053 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -7,16 +7,16 @@ use winit::{ dpi::{LogicalSize, PhysicalSize}, event::{DeviceEvent, ElementState, Event, KeyEvent, RawKeyEvent, WindowEvent}, event_loop::{DeviceEvents, EventLoop}, - keyboard::{Key, KeyCode}, + keyboard::{Key, KeyCode, PhysicalKey}, window::{Fullscreen, WindowBuilder}, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -38,9 +38,7 @@ fn main() { event_loop.listen_device_events(DeviceEvents::Always); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - + event_loop.run(move |event, elwt| { match event { // This used to use the virtual key, but the new API // only provides the `physical_key` (`Code`). @@ -53,14 +51,14 @@ fn main() { }), .. } => match physical_key { - KeyCode::KeyM => { + PhysicalKey::Code(KeyCode::KeyM) => { if minimized { minimized = !minimized; window.set_minimized(minimized); window.focus_window(); } } - KeyCode::KeyV => { + PhysicalKey::Code(KeyCode::KeyV) => { if !visible { visible = !visible; window.set_visible(visible); @@ -68,76 +66,72 @@ fn main() { } _ => (), }, - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: Key::Character(key_str), - state: ElementState::Pressed, - .. - }, - .. - }, - .. - } => match key_str.as_ref() { - // WARNING: Consider using `key_without_modifers()` if available on your platform. - // See the `key_binding` example - "e" => { - fn area(size: PhysicalSize) -> u32 { - size.width * size.height - } + Event::WindowEvent { window_id, event } => match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Pressed, + .. + }, + .. + } => match key_str.as_ref() { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + "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"); + 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"); + } } - } - "f" => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - let monitor = window.current_monitor(); - window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + "f" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + } } - } - "p" => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); + "p" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } } - } - "m" => { - minimized = !minimized; - window.set_minimized(minimized); - } - "q" => { - control_flow.set_exit(); - } - "v" => { - visible = !visible; - window.set_visible(visible); - } - "x" => { - let is_maximized = window.is_maximized(); - window.set_maximized(!is_maximized); + "m" => { + minimized = !minimized; + window.set_minimized(minimized); + } + "q" => { + elwt.exit(); + } + "v" => { + visible = !visible; + window.set_visible(visible); + } + "x" => { + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); + } + _ => (), + }, + WindowEvent::CloseRequested if window_id == window.id() => elwt.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(&window); } _ => (), }, - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } _ => (), } - }); + }) } diff --git a/examples/window_drag_resize.rs b/examples/window_drag_resize.rs index 84dae6e03b..b1d3cba43d 100644 --- a/examples/window_drag_resize.rs +++ b/examples/window_drag_resize.rs @@ -3,7 +3,7 @@ use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyEvent, MouseButton, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::EventLoop, keyboard::Key, window::{CursorIcon, ResizeDirection, WindowBuilder}, }; @@ -13,9 +13,9 @@ const BORDER: f64 = 8.0; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_inner_size(winit::dpi::LogicalSize::new(600.0, 400.0)) @@ -27,12 +27,12 @@ fn main() { let mut border = false; let mut cursor_location = None; - event_loop.run(move |event, _, control_flow| match event { + event_loop.run(move |event, elwt| match event { Event::NewEvents(StartCause::Init) => { eprintln!("Press 'B' to toggle borderless") } Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::CloseRequested => elwt.exit(), WindowEvent::CursorMoved { position, .. } => { if !window.is_decorated() { let new_location = @@ -52,6 +52,8 @@ fn main() { } => { if let Some(dir) = cursor_location { let _res = window.drag_resize_window(dir); + } else if !window.is_decorated() { + let _res = window.drag_window(); } } WindowEvent::KeyboardInput { @@ -66,13 +68,14 @@ fn main() { border = !border; window.set_decorations(border); } + WindowEvent::RedrawRequested => { + fill::fill_window(&window); + } _ => (), }, - Event::RedrawRequested(_) => { - fill::fill_window(&window); - } + _ => (), - }); + }) } fn cursor_direction_icon(resize_direction: Option) -> CursorIcon { diff --git a/examples/window_icon.rs b/examples/window_icon.rs index e87372c208..0f19780106 100644 --- a/examples/window_icon.rs +++ b/examples/window_icon.rs @@ -4,7 +4,7 @@ use std::path::Path; use simple_logger::SimpleLogger; use winit::{ - event::Event, + event::{Event, WindowEvent}, event_loop::EventLoop, window::{Icon, WindowBuilder}, }; @@ -12,7 +12,7 @@ use winit::{ #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); // You'll have to choose an icon size at your own discretion. On X11, the desired size varies @@ -23,7 +23,7 @@ fn main() { let icon = load_icon(Path::new(path)); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("An iconic window!") @@ -33,22 +33,18 @@ fn main() { .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - + event_loop.run(move |event, elwt| { if let Event::WindowEvent { event, .. } = event { - use winit::event::WindowEvent::*; match event { - CloseRequested => control_flow.set_exit(), - DroppedFile(path) => { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::DroppedFile(path) => { window.set_window_icon(Some(load_icon(&path))); } + WindowEvent::RedrawRequested => fill::fill_window(&window), _ => (), } - } else if let Event::RedrawRequested(_) = event { - fill::fill_window(&window); } - }); + }) } fn load_icon(path: &Path) -> Icon { diff --git a/examples/window_on_demand.rs b/examples/window_on_demand.rs new file mode 100644 index 0000000000..bb00416da5 --- /dev/null +++ b/examples/window_on_demand.rs @@ -0,0 +1,93 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform,))] +fn main() -> Result<(), impl std::error::Error> { + use std::time::Duration; + + use simple_logger::SimpleLogger; + + use winit::{ + error::EventLoopError, + event::{Event, WindowEvent}, + event_loop::EventLoop, + platform::run_on_demand::EventLoopExtRunOnDemand, + window::{Window, WindowBuilder, WindowId}, + }; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct App { + window_id: Option, + window: Option, + } + + SimpleLogger::new().init().unwrap(); + let mut event_loop = EventLoop::new().unwrap(); + + fn run_app(event_loop: &mut EventLoop<()>, idx: usize) -> Result<(), EventLoopError> { + let mut app = App::default(); + + event_loop.run_on_demand(move |event, elwt| { + println!("Run {idx}: {:?}", event); + + if let Some(window) = &app.window { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window.id() == window_id => { + println!("--------------------------------------------------------- Window {idx} CloseRequested"); + fill::cleanup_window(window); + app.window = None; + } + Event::AboutToWait => window.request_redraw(), + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + fill::fill_window(window); + } + _ => (), + } + } else if let Some(id) = app.window_id { + match event { + Event::WindowEvent { + event: WindowEvent::Destroyed, + window_id, + } if id == window_id => { + println!("--------------------------------------------------------- Window {idx} Destroyed"); + app.window_id = None; + elwt.exit(); + } + _ => (), + } + } else if let Event::Resumed = event { + let window = WindowBuilder::new() + .with_title("Fantastic window number one!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .build(elwt) + .unwrap(); + app.window_id = Some(window.id()); + app.window = Some(window); + } + }) + } + + run_app(&mut event_loop, 1)?; + + println!("--------------------------------------------------------- Finished first loop"); + println!("--------------------------------------------------------- Waiting 5 seconds"); + std::thread::sleep(Duration::from_secs(5)); + + let ret = run_app(&mut event_loop, 2); + println!("--------------------------------------------------------- Finished second loop"); + ret +} + +#[cfg(not(any(windows_platform, macos_platform, x11_platform, wayland_platform,)))] +fn main() { + println!("This example is not supported on this platform"); +} diff --git a/examples/window_option_as_alt.rs b/examples/window_option_as_alt.rs index fc2e458285..28fb93bd99 100644 --- a/examples/window_option_as_alt.rs +++ b/examples/window_option_as_alt.rs @@ -18,8 +18,8 @@ mod fill; /// Prints the keyboard events characters received when option_is_alt is true versus false. /// A left mouse click will toggle option_is_alt. #[cfg(target_os = "macos")] -fn main() { - let event_loop = EventLoop::new(); +fn main() -> Result<(), impl std::error::Error> { + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -31,42 +31,39 @@ fn main() { let mut option_as_alt = window.option_as_alt(); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::WindowEvent { event, .. } => match event { - WindowEvent::MouseInput { - state: ElementState::Pressed, - button: MouseButton::Left, - .. - } => { - option_as_alt = match option_as_alt { - OptionAsAlt::None => OptionAsAlt::OnlyLeft, - OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, - OptionAsAlt::OnlyRight => OptionAsAlt::Both, - OptionAsAlt::Both => OptionAsAlt::None, - }; + event_loop.run(move |event, elwt| match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => elwt.exit(), + Event::WindowEvent { event, .. } => match event { + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + option_as_alt = match option_as_alt { + OptionAsAlt::None => OptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, + OptionAsAlt::OnlyRight => OptionAsAlt::Both, + OptionAsAlt::Both => OptionAsAlt::None, + }; - println!("Received Mouse click, toggling option_as_alt to: {option_as_alt:?}"); - window.set_option_as_alt(option_as_alt); - } - WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"), - _ => (), - }, - Event::MainEventsCleared => { - window.request_redraw(); + println!("Received Mouse click, toggling option_as_alt to: {option_as_alt:?}"); + window.set_option_as_alt(option_as_alt); } - Event::RedrawRequested(_) => { + WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"), + WindowEvent::RedrawRequested => { fill::fill_window(&window); } _ => (), + }, + Event::AboutToWait => { + window.request_redraw(); } - }); + + _ => (), + }) } #[cfg(not(target_os = "macos"))] diff --git a/examples/window_run_return.rs b/examples/window_pump_events.rs similarity index 50% rename from examples/window_run_return.rs rename to examples/window_pump_events.rs index 2a2758d0de..040ad7f001 100644 --- a/examples/window_run_return.rs +++ b/examples/window_pump_events.rs @@ -7,23 +7,22 @@ x11_platform, wayland_platform, android_platform, - orbital_platform, ))] -fn main() { - use std::{thread::sleep, time::Duration}; +fn main() -> std::process::ExitCode { + use std::{process::ExitCode, thread::sleep, time::Duration}; use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, event_loop::EventLoop, - platform::run_return::EventLoopExtRunReturn, + platform::pump_events::{EventLoopExtPumpEvents, PumpStatus}, window::WindowBuilder, }; #[path = "util/fill.rs"] mod fill; - let mut event_loop = EventLoop::new(); + let mut event_loop = EventLoop::new().unwrap(); SimpleLogger::new().init().unwrap(); let window = WindowBuilder::new() @@ -31,12 +30,9 @@ fn main() { .build(&event_loop) .unwrap(); - let mut quit = false; - - while !quit { - event_loop.run_return(|event, _, control_flow| { - control_flow.set_wait(); - + 'main: loop { + let timeout = Some(Duration::ZERO); + let status = event_loop.pump_events(timeout, |event, elwt| { if let Event::WindowEvent { event, .. } = &event { // Print only Window events to reduce noise println!("{event:?}"); @@ -45,27 +41,34 @@ fn main() { match event { Event::WindowEvent { event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => elwt.exit(), + Event::AboutToWait => { + window.request_redraw(); + } + Event::WindowEvent { + event: WindowEvent::RedrawRequested, .. } => { - quit = true; - } - Event::MainEventsCleared => { - control_flow.set_exit(); - } - Event::RedrawRequested(_) => { fill::fill_window(&window); } _ => (), } }); + if let PumpStatus::Exit(exit_code) = status { + break 'main ExitCode::from(exit_code as u8); + } - // Sleep for 1/60 second to simulate rendering - println!("rendering"); + // Sleep for 1/60 second to simulate application work + // + // Since `pump_events` doesn't block it will be important to + // throttle the loop in the app somehow. + println!("Update()"); sleep(Duration::from_millis(16)); } } -#[cfg(any(ios_platform, wasm_platform))] +#[cfg(any(ios_platform, wasm_platform, orbital_platform))] fn main() { - println!("This platform doesn't support run_return."); + println!("This platform doesn't support pump_events."); } diff --git a/examples/window_resize_increments.rs b/examples/window_resize_increments.rs index 1c713ba72c..834050585a 100644 --- a/examples/window_resize_increments.rs +++ b/examples/window_resize_increments.rs @@ -2,18 +2,18 @@ use log::debug; use simple_logger::SimpleLogger; use winit::{ dpi::LogicalSize, - event::{ElementState, Event, KeyEvent, WindowEvent}, + event::{ElementState, Event, WindowEvent}, event_loop::EventLoop, - keyboard::Key, + keyboard::NamedKey, window::WindowBuilder, }; #[path = "util/fill.rs"] mod fill; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_title("A fantastic window!") @@ -24,27 +24,13 @@ fn main() { let mut has_increments = true; - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - } if window_id == window.id() => control_flow.set_exit(), - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: Key::Space, - state: ElementState::Released, - .. - }, - .. - }, - window_id, - } if window_id == window.id() => { + event_loop.run(move |event, elwt| match event { + Event::WindowEvent { event, window_id } if window_id == window.id() => match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::KeyboardInput { event, .. } + if event.logical_key == NamedKey::Space + && event.state == ElementState::Released => + { has_increments = !has_increments; let new_increments = match window.resize_increments() { @@ -54,11 +40,13 @@ fn main() { debug!("Had increments: {}", new_increments.is_none()); window.set_resize_increments(new_increments); } - Event::MainEventsCleared => window.request_redraw(), - Event::RedrawRequested(_) => { + WindowEvent::RedrawRequested => { fill::fill_window(&window); } _ => (), - } - }); + }, + Event::AboutToWait => window.request_redraw(), + + _ => (), + }) } diff --git a/examples/window_tabbing.rs b/examples/window_tabbing.rs new file mode 100644 index 0000000000..9413ae9a39 --- /dev/null +++ b/examples/window_tabbing.rs @@ -0,0 +1,107 @@ +#![allow(clippy::single_match)] + +#[cfg(target_os = "macos")] +use std::{collections::HashMap, num::NonZeroUsize}; + +#[cfg(target_os = "macos")] +use simple_logger::SimpleLogger; +#[cfg(target_os = "macos")] +use winit::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::EventLoop, + keyboard::{Key, NamedKey}, + platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}, + window::{Window, WindowBuilder}, +}; + +#[cfg(target_os = "macos")] +#[path = "util/fill.rs"] +mod fill; + +#[cfg(target_os = "macos")] +fn main() -> Result<(), impl std::error::Error> { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new().unwrap(); + + let mut windows = HashMap::new(); + 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, elwt| { + if let Event::WindowEvent { event, window_id } = event { + match event { + WindowEvent::CloseRequested => { + println!("Window {window_id:?} has received the signal to close"); + + // This drops the window, causing it to close. + windows.remove(&window_id); + + if windows.is_empty() { + elwt.exit(); + } + } + WindowEvent::Resized(_) => { + if let Some(window) = windows.get(&window_id) { + window.request_redraw(); + } + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + logical_key, + .. + }, + is_synthetic: false, + .. + } => match logical_key.as_ref() { + Key::Character("t") => { + let tabbing_id = windows.get(&window_id).unwrap().tabbing_identifier(); + let window = WindowBuilder::new() + .with_tabbing_identifier(&tabbing_id) + .build(elwt) + .unwrap(); + println!("Added a new tab: {:?}", window.id()); + windows.insert(window.id(), window); + } + Key::Character("w") => { + let _ = windows.remove(&window_id); + } + Key::Named(NamedKey::ArrowRight) => { + windows.get(&window_id).unwrap().select_next_tab(); + } + Key::Named(NamedKey::ArrowLeft) => { + windows.get(&window_id).unwrap().select_previous_tab(); + } + Key::Character(ch) => { + if let Ok(index) = ch.parse::() { + let index = index.get(); + // Select the last tab when pressing `9`. + let window = windows.get(&window_id).unwrap(); + if index == 9 { + window.select_tab_at_index(window.num_tabs() - 1) + } else { + window.select_tab_at_index(index - 1); + } + } + } + _ => (), + }, + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } + } + _ => (), + } + } + }) +} + +#[cfg(not(target_os = "macos"))] +fn main() { + println!("This example is only supported on MacOS"); +} diff --git a/examples/x11_embed.rs b/examples/x11_embed.rs new file mode 100644 index 0000000000..e66482eba3 --- /dev/null +++ b/examples/x11_embed.rs @@ -0,0 +1,70 @@ +//! A demonstration of embedding a winit window in an existing X11 application. + +#[cfg(x11_platform)] +#[path = "util/fill.rs"] +mod fill; + +#[cfg(x11_platform)] +mod imple { + use super::fill; + use simple_logger::SimpleLogger; + use winit::{ + event::{Event, WindowEvent}, + event_loop::EventLoop, + platform::x11::WindowBuilderExtX11, + window::WindowBuilder, + }; + + pub(super) fn entry() -> Result<(), Box> { + // First argument should be a 32-bit X11 window ID. + let parent_window_id = std::env::args() + .nth(1) + .ok_or("Expected a 32-bit X11 window ID as the first argument.")? + .parse::()?; + + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new()?; + + let window = WindowBuilder::new() + .with_title("An embedded window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .with_embed_parent_window(parent_window_id) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, elwt| { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => elwt.exit(), + Event::AboutToWait => { + window.request_redraw(); + } + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + // Notify the windowing system that we'll be presenting to the window. + window.pre_present_notify(); + fill::fill_window(&window); + } + _ => (), + } + })?; + + Ok(()) + } +} + +#[cfg(not(x11_platform))] +mod imple { + pub(super) fn entry() -> Result<(), Box> { + println!("This example is only supported on X11 platforms."); + Ok(()) + } +} + +fn main() -> Result<(), Box> { + imple::entry() +} diff --git a/src/dpi.rs b/src/dpi.rs index 027f5d44e6..8c911c1061 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -4,13 +4,13 @@ //! //! Modern computer screens don't have a consistent relationship between resolution and size. //! 1920x1080 is a common resolution for both desktop and mobile screens, despite mobile screens -//! normally being less than a quarter the size of their desktop counterparts. What's more, neither -//! desktop nor mobile screens are consistent resolutions within their own size classes - common +//! typically being less than a quarter the size of their desktop counterparts. Moreover, neither +//! desktop nor mobile screens have consistent resolutions within their own size classes - common //! mobile screens range from below 720p to above 1440p, and desktop screens range from 720p to 5K //! and beyond. //! //! Given that, it's a mistake to assume that 2D content will only be displayed on screens with -//! a consistent pixel density. If you were to render a 96-pixel-square image on a 1080p screen, +//! a consistent pixel density. If you were to render a 96-pixel-square image on a 1080p screen and //! then render the same image on a similarly-sized 4K screen, the 4K rendition would only take up //! about a quarter of the physical space as it did on the 1080p screen. That issue is especially //! problematic with text rendering, where quarter-sized text becomes a significant legibility @@ -25,12 +25,12 @@ //! //! The solution to this problem is to account for the device's *scale factor*. The scale factor is //! the factor UI elements should be scaled by to be consistent with the rest of the user's system - -//! for example, a button that's normally 50 pixels across would be 100 pixels across on a device +//! for example, a button that's usually 50 pixels across would be 100 pixels across on a device //! with a scale factor of `2.0`, or 75 pixels across with a scale factor of `1.5`. //! //! Many UI systems, such as CSS, expose DPI-dependent units like [points] or [picas]. That's -//! usually a mistake, since there's no consistent mapping between the scale factor and the screen's -//! actual DPI. Unless you're printing to a physical medium, you should work in scaled pixels rather +//! usually a mistake since there's no consistent mapping between the scale factor and the screen's +//! actual DPI. Unless printing to a physical medium, you should work in scaled pixels rather //! than any DPI-dependent units. //! //! ### Position and Size types @@ -42,11 +42,11 @@ //! coordinates as input, allowing you to use the most convenient coordinate system for your //! particular application. //! -//! Winit's position and size types types are generic over their exact pixel type, `P`, to allow the +//! Winit's position and size types are generic over their exact pixel type, `P`, to allow the //! API to have integer precision where appropriate (e.g. most window manipulation functions) and //! 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 +//! 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 //! rounding properly. Note that precision loss will still occur when rounding from a float to an //! int, although rounding lessens the problem. @@ -55,35 +55,35 @@ //! //! 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 +//! monitor or if the user changes their DPI settings. This allows you 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, its scale factor //! can be found by calling [`window.scale_factor()`]. //! //! ## How is the scale factor calculated? //! -//! Scale factor is calculated differently on different platforms: +//! The scale factor is calculated differently on different platforms: //! //! - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from the //! display settings. While users are free to select any option they want, they're only given a -//! selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7, the scale factor is +//! selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7. The scale factor is //! global and changing it requires logging out. See [this article][windows_1] for technical //! details. -//! - **macOS:** Recent versions of macOS allow the user to change the scaling factor for certain -//! displays. When this is available, the user may pick a per-monitor scaling factor from a set -//! of pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default but -//! the specific value varies across devices. +//! - **macOS:** Recent macOS versions allow the user to change the scaling factor for specific +//! displays. When available, the user may pick a per-monitor scaling factor from a set of +//! pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default, +//! but the specific value varies across devices. //! - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. Winit //! currently uses a three-pronged approach: -//! + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable, if present. +//! + 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, 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 //! variables to do what you want before resorting to `WINIT_X11_SCALE_FACTOR`. -//! - **Wayland:** On Wayland, scale factors are set per-screen by the server, and are always -//! integers (most often 1 or 2). +//! - **Wayland:** The scale factor is suggested by the compositor for each window individually. The +//! monitor scale factor may differ from the window scale factor. //! - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range //! from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more //! information. @@ -581,3 +581,421 @@ impl From> for Position { Position::Logical(position.cast()) } } + +#[cfg(test)] +mod tests { + use crate::dpi; + use std::collections::HashSet; + + macro_rules! test_pixel_int_impl { + ($($name:ident => $ty:ty),*) => {$( + #[test] + fn $name() { + use dpi::Pixel; + + assert_eq!( + <$ty as Pixel>::from_f64(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::from_f64(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::from_f64(37.5), + 38, + ); + assert_eq!( + <$ty as Pixel>::from_f64(37.9), + 38, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37), + 37, + ); + } + )*}; + } + + test_pixel_int_impl! { + test_pixel_int_u8 => u8, + test_pixel_int_u16 => u16, + test_pixel_int_u32 => u32, + test_pixel_int_i8 => i8, + test_pixel_int_i16 => i16 + } + + macro_rules! assert_approx_eq { + ($a:expr, $b:expr $(,)?) => { + assert!( + ($a - $b).abs() < 0.001, + "{} is not approximately equal to {}", + $a, + $b + ); + }; + } + + macro_rules! test_pixel_float_impl { + ($($name:ident => $ty:ty),*) => {$( + #[test] + fn $name() { + use dpi::Pixel; + + assert_approx_eq!( + <$ty as Pixel>::from_f64(37.0), + 37.0, + ); + assert_approx_eq!( + <$ty as Pixel>::from_f64(37.4), + 37.4, + ); + assert_approx_eq!( + <$ty as Pixel>::from_f64(37.5), + 37.5, + ); + assert_approx_eq!( + <$ty as Pixel>::from_f64(37.9), + 37.9, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.5), + 38, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.5), + 38, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.5), + 38, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.5), + 38, + ); + + assert_eq!( + <$ty as Pixel>::cast::(37.0), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.4), + 37, + ); + assert_eq!( + <$ty as Pixel>::cast::(37.5), + 38, + ); + } + )*}; +} + + test_pixel_float_impl! { + test_pixel_float_f32 => f32, + test_pixel_float_f64 => f64 + } + + #[test] + fn test_validate_scale_factor() { + assert!(dpi::validate_scale_factor(1.0)); + assert!(dpi::validate_scale_factor(2.0)); + assert!(dpi::validate_scale_factor(3.0)); + assert!(dpi::validate_scale_factor(1.5)); + assert!(dpi::validate_scale_factor(0.5)); + + assert!(!dpi::validate_scale_factor(0.0)); + assert!(!dpi::validate_scale_factor(-1.0)); + assert!(!dpi::validate_scale_factor(f64::INFINITY)); + assert!(!dpi::validate_scale_factor(f64::NAN)); + assert!(!dpi::validate_scale_factor(f64::NEG_INFINITY)); + } + + #[test] + fn test_logical_position() { + let log_pos = dpi::LogicalPosition::new(1.0, 2.0); + assert_eq!( + log_pos.to_physical::(1.0), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + log_pos.to_physical::(2.0), + dpi::PhysicalPosition::new(2, 4) + ); + assert_eq!(log_pos.cast::(), dpi::LogicalPosition::new(1, 2)); + assert_eq!( + log_pos, + dpi::LogicalPosition::from_physical(dpi::PhysicalPosition::new(1.0, 2.0), 1.0) + ); + assert_eq!( + log_pos, + dpi::LogicalPosition::from_physical(dpi::PhysicalPosition::new(2.0, 4.0), 2.0) + ); + assert_eq!( + dpi::LogicalPosition::from((2.0, 2.0)), + dpi::LogicalPosition::new(2.0, 2.0) + ); + assert_eq!( + dpi::LogicalPosition::from([2.0, 3.0]), + dpi::LogicalPosition::new(2.0, 3.0) + ); + + let x: (f64, f64) = log_pos.into(); + assert_eq!(x, (1.0, 2.0)); + let x: [f64; 2] = log_pos.into(); + assert_eq!(x, [1.0, 2.0]); + } + + #[test] + fn test_physical_position() { + assert_eq!( + dpi::PhysicalPosition::from_logical(dpi::LogicalPosition::new(1.0, 2.0), 1.0), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + dpi::PhysicalPosition::from_logical(dpi::LogicalPosition::new(2.0, 4.0), 0.5), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + dpi::PhysicalPosition::from((2.0, 2.0)), + dpi::PhysicalPosition::new(2.0, 2.0) + ); + assert_eq!( + dpi::PhysicalPosition::from([2.0, 3.0]), + dpi::PhysicalPosition::new(2.0, 3.0) + ); + + let x: (f64, f64) = dpi::PhysicalPosition::new(1, 2).into(); + assert_eq!(x, (1.0, 2.0)); + let x: [f64; 2] = dpi::PhysicalPosition::new(1, 2).into(); + assert_eq!(x, [1.0, 2.0]); + } + + #[test] + fn test_logical_size() { + let log_size = dpi::LogicalSize::new(1.0, 2.0); + assert_eq!( + log_size.to_physical::(1.0), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + log_size.to_physical::(2.0), + dpi::PhysicalSize::new(2, 4) + ); + assert_eq!(log_size.cast::(), dpi::LogicalSize::new(1, 2)); + assert_eq!( + log_size, + dpi::LogicalSize::from_physical(dpi::PhysicalSize::new(1.0, 2.0), 1.0) + ); + assert_eq!( + log_size, + dpi::LogicalSize::from_physical(dpi::PhysicalSize::new(2.0, 4.0), 2.0) + ); + assert_eq!( + dpi::LogicalSize::from((2.0, 2.0)), + dpi::LogicalSize::new(2.0, 2.0) + ); + assert_eq!( + dpi::LogicalSize::from([2.0, 3.0]), + dpi::LogicalSize::new(2.0, 3.0) + ); + + let x: (f64, f64) = log_size.into(); + assert_eq!(x, (1.0, 2.0)); + let x: [f64; 2] = log_size.into(); + assert_eq!(x, [1.0, 2.0]); + } + + #[test] + fn test_physical_size() { + assert_eq!( + dpi::PhysicalSize::from_logical(dpi::LogicalSize::new(1.0, 2.0), 1.0), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + dpi::PhysicalSize::from_logical(dpi::LogicalSize::new(2.0, 4.0), 0.5), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + dpi::PhysicalSize::from((2.0, 2.0)), + dpi::PhysicalSize::new(2.0, 2.0) + ); + assert_eq!( + dpi::PhysicalSize::from([2.0, 3.0]), + dpi::PhysicalSize::new(2.0, 3.0) + ); + + let x: (f64, f64) = dpi::PhysicalSize::new(1, 2).into(); + assert_eq!(x, (1.0, 2.0)); + let x: [f64; 2] = dpi::PhysicalSize::new(1, 2).into(); + assert_eq!(x, [1.0, 2.0]); + } + + #[test] + fn test_size() { + assert_eq!( + dpi::Size::new(dpi::PhysicalSize::new(1, 2)), + dpi::Size::Physical(dpi::PhysicalSize::new(1, 2)) + ); + assert_eq!( + dpi::Size::new(dpi::LogicalSize::new(1.0, 2.0)), + dpi::Size::Logical(dpi::LogicalSize::new(1.0, 2.0)) + ); + + assert_eq!( + dpi::Size::new(dpi::PhysicalSize::new(1, 2)).to_logical::(1.0), + dpi::LogicalSize::new(1.0, 2.0) + ); + assert_eq!( + dpi::Size::new(dpi::PhysicalSize::new(1, 2)).to_logical::(2.0), + dpi::LogicalSize::new(0.5, 1.0) + ); + assert_eq!( + dpi::Size::new(dpi::LogicalSize::new(1.0, 2.0)).to_logical::(1.0), + dpi::LogicalSize::new(1.0, 2.0) + ); + + assert_eq!( + dpi::Size::new(dpi::PhysicalSize::new(1, 2)).to_physical::(1.0), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + dpi::Size::new(dpi::PhysicalSize::new(1, 2)).to_physical::(2.0), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + dpi::Size::new(dpi::LogicalSize::new(1.0, 2.0)).to_physical::(1.0), + dpi::PhysicalSize::new(1, 2) + ); + assert_eq!( + dpi::Size::new(dpi::LogicalSize::new(1.0, 2.0)).to_physical::(2.0), + dpi::PhysicalSize::new(2, 4) + ); + + let small = dpi::Size::Physical((1, 2).into()); + let medium = dpi::Size::Logical((3, 4).into()); + let medium_physical = dpi::Size::new(medium.to_physical::(1.0)); + let large = dpi::Size::Physical((5, 6).into()); + assert_eq!(dpi::Size::clamp(medium, small, large, 1.0), medium_physical); + assert_eq!(dpi::Size::clamp(small, medium, large, 1.0), medium_physical); + assert_eq!(dpi::Size::clamp(large, small, medium, 1.0), medium_physical); + } + + #[test] + fn test_position() { + assert_eq!( + dpi::Position::new(dpi::PhysicalPosition::new(1, 2)), + dpi::Position::Physical(dpi::PhysicalPosition::new(1, 2)) + ); + assert_eq!( + dpi::Position::new(dpi::LogicalPosition::new(1.0, 2.0)), + dpi::Position::Logical(dpi::LogicalPosition::new(1.0, 2.0)) + ); + + assert_eq!( + dpi::Position::new(dpi::PhysicalPosition::new(1, 2)).to_logical::(1.0), + dpi::LogicalPosition::new(1.0, 2.0) + ); + assert_eq!( + dpi::Position::new(dpi::PhysicalPosition::new(1, 2)).to_logical::(2.0), + dpi::LogicalPosition::new(0.5, 1.0) + ); + assert_eq!( + dpi::Position::new(dpi::LogicalPosition::new(1.0, 2.0)).to_logical::(1.0), + dpi::LogicalPosition::new(1.0, 2.0) + ); + + assert_eq!( + dpi::Position::new(dpi::PhysicalPosition::new(1, 2)).to_physical::(1.0), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + dpi::Position::new(dpi::PhysicalPosition::new(1, 2)).to_physical::(2.0), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + dpi::Position::new(dpi::LogicalPosition::new(1.0, 2.0)).to_physical::(1.0), + dpi::PhysicalPosition::new(1, 2) + ); + assert_eq!( + dpi::Position::new(dpi::LogicalPosition::new(1.0, 2.0)).to_physical::(2.0), + dpi::PhysicalPosition::new(2, 4) + ); + } + + // Eat coverage for the Debug impls et al + #[test] + fn ensure_attrs_do_not_panic() { + let _ = format!("{:?}", dpi::LogicalPosition::::default().clone()); + HashSet::new().insert(dpi::LogicalPosition::::default()); + + let _ = format!("{:?}", dpi::PhysicalPosition::::default().clone()); + HashSet::new().insert(dpi::PhysicalPosition::::default()); + + let _ = format!("{:?}", dpi::LogicalSize::::default().clone()); + HashSet::new().insert(dpi::LogicalSize::::default()); + + let _ = format!("{:?}", dpi::PhysicalSize::::default().clone()); + HashSet::new().insert(dpi::PhysicalSize::::default()); + + let _ = format!("{:?}", dpi::Size::Physical((1, 2).into()).clone()); + let _ = format!("{:?}", dpi::Position::Physical((1, 2).into()).clone()); + } +} diff --git a/src/error.rs b/src/error.rs index c039f3b868..92468620ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,11 +2,14 @@ use std::{error, fmt}; use crate::platform_impl; -/// An error whose cause it outside Winit's control. +// TODO: Rename +/// An error that may be generated when requesting Winit state #[derive(Debug)] pub enum ExternalError { /// The operation is not supported by the backend. NotSupported(NotSupportedError), + /// The operation was ignored. + Ignored, /// The OS cannot perform the operation. Os(OsError), } @@ -25,6 +28,27 @@ pub struct OsError { error: platform_impl::OsError, } +/// A general error that may occur while running the Winit event loop +#[derive(Debug)] +pub enum EventLoopError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The OS cannot perform the operation. + Os(OsError), + /// The event loop can't be re-run while it's already running + AlreadyRunning, + /// The event loop can't be re-created. + RecreationAttempt, + /// Application has exit with an error status. + ExitFailure(i32), +} + +impl From for EventLoopError { + fn from(value: OsError) -> Self { + Self::Os(value) + } +} + impl NotSupportedError { #[inline] #[allow(dead_code)] @@ -60,6 +84,7 @@ impl fmt::Display for ExternalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { ExternalError::NotSupported(e) => e.fmt(f), + ExternalError::Ignored => write!(f, "Operation was ignored"), ExternalError::Os(e) => e.fmt(f), } } @@ -77,6 +102,41 @@ impl fmt::Display for NotSupportedError { } } +impl fmt::Display for EventLoopError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + EventLoopError::AlreadyRunning => write!(f, "EventLoop is already running"), + EventLoopError::RecreationAttempt => write!(f, "EventLoop can't be recreated"), + EventLoopError::NotSupported(e) => e.fmt(f), + EventLoopError::Os(e) => e.fmt(f), + EventLoopError::ExitFailure(status) => write!(f, "Exit Failure: {status}"), + } + } +} + impl error::Error for OsError {} impl error::Error for ExternalError {} impl error::Error for NotSupportedError {} +impl error::Error for EventLoopError {} + +#[cfg(test)] +mod tests { + #![allow(clippy::redundant_clone)] + + use super::*; + + // Eat attributes for testing + #[test] + fn ensure_fmt_does_not_panic() { + let _ = format!( + "{:?}, {}", + NotSupportedError::new(), + NotSupportedError::new().clone() + ); + let _ = format!( + "{:?}, {}", + ExternalError::NotSupported(NotSupportedError::new()), + ExternalError::NotSupported(NotSupportedError::new()) + ); + } +} diff --git a/src/event.rs b/src/event.rs index 1a57bece98..f3cf4c3f0c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,26 +7,24 @@ //! approximate the basic ordering loop of [`EventLoop::run(...)`] like this: //! //! ```rust,ignore -//! let mut control_flow = ControlFlow::Poll; //! let mut start_cause = StartCause::Init; //! -//! while control_flow != ControlFlow::Exit { -//! event_handler(NewEvents(start_cause), ..., &mut control_flow); +//! while !elwt.exiting() { +//! event_handler(NewEvents(start_cause), elwt); //! //! for e in (window events, user events, device events) { -//! event_handler(e, ..., &mut control_flow); +//! event_handler(e, elwt); //! } -//! event_handler(MainEventsCleared, ..., &mut control_flow); //! //! for w in (redraw windows) { -//! event_handler(RedrawRequested(w), ..., &mut control_flow); +//! event_handler(RedrawRequested(w), elwt); //! } -//! event_handler(RedrawEventsCleared, ..., &mut control_flow); //! -//! start_cause = wait_if_necessary(control_flow); +//! event_handler(AboutToWait, elwt); +//! start_cause = wait_if_necessary(); //! } //! -//! event_handler(LoopDestroyed, ..., &mut control_flow); +//! event_handler(LoopExiting, elwt); //! ``` //! //! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully @@ -34,39 +32,53 @@ //! //! [`EventLoop::run(...)`]: crate::event_loop::EventLoop::run //! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil -use smol_str::SmolStr; use std::path::PathBuf; +use std::sync::{Mutex, Weak}; #[cfg(not(wasm_platform))] use std::time::Instant; + +use smol_str::SmolStr; #[cfg(wasm_platform)] use web_time::Instant; +use crate::error::ExternalError; #[cfg(doc)] use crate::window::Window; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, + event_loop::AsyncRequestSerial, keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}, platform_impl, - window::{Theme, WindowId}, + window::{ActivationToken, Theme, WindowId}, }; +/// iOS specific event sent when UIApplication delegate receives the response upon registering for remote notifications +#[derive(Debug, Clone, PartialEq)] +pub enum IosRemoteRegistration { + Failed { + code: isize, + localized_description: String, + }, + DeviceToken(Vec), +} + /// Describes a generic event. /// /// See the module-level docs for more information on the event loop manages each event. -#[derive(Debug, PartialEq)] -pub enum Event<'a, T: 'static> { +#[derive(Debug, Clone, PartialEq)] +pub enum Event { /// Emitted when new events arrive from the OS to be processed. /// /// This event type is useful as a place to put code that should be done before you start /// processing events, such as updating frame timing information for benchmarking or checking - /// the [`StartCause`][crate::event::StartCause] to see if a timer set by + /// the [`StartCause`] to see if a timer set by /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed. NewEvents(StartCause), /// Emitted when the OS sends an event to a winit window. WindowEvent { window_id: WindowId, - event: WindowEvent<'a>, + event: WindowEvent, }, /// Emitted when the OS sends an event to a device. @@ -84,7 +96,7 @@ pub enum Event<'a, T: 'static> { /// /// 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, + /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason, /// Winit does not currently try to emit pseudo `Suspended` events before the application /// quits on platforms without an application lifecycle. /// @@ -129,7 +141,7 @@ pub enum Event<'a, T: 'static> { /// /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event /// with the property [`persisted`] being true, which means that the page is being - /// put in the [´bfcache`] (back/forward cache) - an in-memory cache that stores a + /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a /// complete snapshot of a page (including the JavaScript heap) as the user is /// navigating away. /// @@ -195,7 +207,7 @@ pub enum Event<'a, T: 'static> { /// /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event /// with the property [`persisted`] being true, which means that the page is being - /// restored from the [´bfcache`] (back/forward cache) - an in-memory cache that + /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that /// stores a complete snapshot of a page (including the JavaScript heap) as the /// user is navigating away. /// @@ -206,119 +218,69 @@ pub enum Event<'a, T: 'static> { /// [`Suspended`]: Self::Suspended Resumed, - /// Emitted when all of the event loop's input events have been processed and redraw processing - /// is about to begin. + /// Emitted when the event loop is about to block and wait for new events. + /// + /// Most applications shouldn't need to hook into this event since there is no real relationship + /// between how often the event loop needs to wake up and the dispatching of any specific events. + /// + /// High frequency event sources, such as input devices could potentially lead to lots of wake + /// ups and also lots of corresponding `AboutToWait` events. /// - /// This event is useful as a place to put your code that should be run after all - /// state-changing events have been handled and you want to do stuff (updating state, performing - /// calculations, etc) that happens as the "main body" of your event loop. If your program only draws - /// graphics when something changes, it's usually better to do it in response to - /// [`Event::RedrawRequested`](crate::event::Event::RedrawRequested), which gets emitted - /// immediately after this event. Programs that draw graphics continuously, like most games, - /// can render here unconditionally for simplicity. - MainEventsCleared, + /// This is not an ideal event to drive application rendering from and instead applications + /// should render in response to [`WindowEvent::RedrawRequested`] events. + AboutToWait, - /// Emitted after [`MainEventsCleared`] when a window should be redrawn. + /// Emitted when the event loop is being shut down. /// - /// 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`]. + /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as a "do on quit" event. + LoopExiting, + + /// Emitted when the application has received a memory warning. /// - /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests - /// into a single event, to help avoid duplicating rendering work. + /// ## Platform-specific /// - /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless - /// something changes, like most non-game GUIs. + /// ### Android /// + /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The application + /// must [release memory] or risk being killed. /// - /// ## Platform-specific + /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory() + /// [release memory]: https://developer.android.com/topic/performance/memory#release /// - /// - **macOS / iOS:** Due to implementation difficulties, this will often, but not always, be - /// emitted directly inside `drawRect:`, with neither a preceding [`MainEventsCleared`] nor - /// subsequent `RedrawEventsCleared`. See [#2640] for work on this. + /// ### iOS /// - /// [`MainEventsCleared`]: Self::MainEventsCleared - /// [`RedrawEventsCleared`]: Self::RedrawEventsCleared - /// [#2640]: https://github.com/rust-windowing/winit/issues/2640 - RedrawRequested(WindowId), - - /// 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`. + /// On iOS, the `MemoryWarning` event is emitted in response to an [`applicationDidReceiveMemoryWarning`] + /// callback. The application must free as much memory as possible or risk being terminated, see + /// [how to respond to memory warnings]. /// - /// This event is useful for doing any cleanup or bookkeeping work after all the rendering - /// tasks have been completed. + /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni + /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings /// - /// [`RedrawRequested`]: Self::RedrawRequested - RedrawEventsCleared, - - /// Emitted when the event loop is being shut down. + /// ### Others /// - /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that - /// gets emitted. You generally want to treat this as a "do on quit" event. - LoopDestroyed, -} + /// - **macOS / Wayland / Windows / Orbital:** Unsupported. + MemoryWarning, -impl Clone for Event<'static, T> { - fn clone(&self) -> Self { - use self::Event::*; - match self { - WindowEvent { window_id, event } => WindowEvent { - window_id: *window_id, - event: event.clone(), - }, - UserEvent(event) => UserEvent(event.clone()), - DeviceEvent { device_id, event } => DeviceEvent { - device_id: *device_id, - event: event.clone(), - }, - NewEvents(cause) => NewEvents(*cause), - MainEventsCleared => MainEventsCleared, - RedrawRequested(wid) => RedrawRequested(*wid), - RedrawEventsCleared => RedrawEventsCleared, - LoopDestroyed => LoopDestroyed, - Suspended => Suspended, - Resumed => Resumed, - } - } + /// TODO + IosRemoteRegistration(IosRemoteRegistration), } -impl<'a, T> Event<'a, T> { +impl Event { #[allow(clippy::result_large_err)] - pub fn map_nonuser_event(self) -> Result, Event<'a, T>> { + pub fn map_nonuser_event(self) -> Result, Event> { use self::Event::*; match self { UserEvent(_) => Err(self), WindowEvent { window_id, event } => Ok(WindowEvent { window_id, event }), DeviceEvent { device_id, event } => Ok(DeviceEvent { device_id, event }), NewEvents(cause) => Ok(NewEvents(cause)), - MainEventsCleared => Ok(MainEventsCleared), - RedrawRequested(wid) => Ok(RedrawRequested(wid)), - RedrawEventsCleared => Ok(RedrawEventsCleared), - LoopDestroyed => Ok(LoopDestroyed), + AboutToWait => Ok(AboutToWait), + LoopExiting => Ok(LoopExiting), Suspended => Ok(Suspended), Resumed => Ok(Resumed), - } - } - - /// If the event doesn't contain a reference, turn it into an event with a `'static` lifetime. - /// Otherwise, return `None`. - pub fn to_static(self) -> Option> { - use self::Event::*; - match self { - WindowEvent { window_id, event } => event - .to_static() - .map(|event| WindowEvent { window_id, event }), - UserEvent(event) => Some(UserEvent(event)), - DeviceEvent { device_id, event } => Some(DeviceEvent { device_id, event }), - NewEvents(cause) => Some(NewEvents(cause)), - MainEventsCleared => Some(MainEventsCleared), - RedrawRequested(wid) => Some(RedrawRequested(wid)), - RedrawEventsCleared => Some(RedrawEventsCleared), - LoopDestroyed => Some(LoopDestroyed), - Suspended => Some(Suspended), - Resumed => Some(Resumed), + MemoryWarning => Ok(MemoryWarning), + IosRemoteRegistration(v) => Ok(IosRemoteRegistration(v)), } } } @@ -354,8 +316,22 @@ pub enum StartCause { } /// Describes an event from a [`Window`]. -#[derive(Debug, PartialEq)] -pub enum WindowEvent<'a> { +#[derive(Debug, Clone, PartialEq)] +pub enum WindowEvent { + /// The activation token was delivered back and now could be used. + /// + #[cfg_attr( + not(any(x11_platform, wayland_platfrom)), + allow(rustdoc::broken_intra_doc_links) + )] + /// Delivered in response to [`request_activation_token`]. + /// + /// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token + ActivationTokenDone { + serial: AsyncRequestSerial, + token: ActivationToken, + }, + /// The size of the window has changed. Contains the client area's new dimensions. Resized(PhysicalSize), @@ -430,6 +406,14 @@ pub enum WindowEvent<'a> { Ime(Ime), /// The cursor has moved on the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform CursorMoved { device_id: DeviceId, @@ -440,9 +424,25 @@ pub enum WindowEvent<'a> { }, /// The cursor has entered the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform CursorEntered { device_id: DeviceId }, /// The cursor has left the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform CursorLeft { device_id: DeviceId }, /// A mouse wheel movement or touchpad scroll occurred. @@ -528,7 +528,12 @@ pub enum WindowEvent<'a> { /// /// ## Platform-specific /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. /// - **macOS:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform Touch(Touch), /// The window's scale factor has changed. @@ -546,7 +551,10 @@ pub enum WindowEvent<'a> { /// For more information about DPI in general, see the [`dpi`](crate::dpi) module. ScaleFactorChanged { scale_factor: f64, - new_inner_size: &'a mut PhysicalSize, + /// Handle to update inner size during scale changes. + /// + /// See [`InnerSizeWriter`] docs for more details. + inner_size_writer: InnerSizeWriter, }, /// The system window theme has changed. @@ -564,207 +572,39 @@ pub enum WindowEvent<'a> { /// 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 / Orbital:** Unsupported. + /// ## Platform-specific + /// + /// ### iOS + /// + /// On iOS, the `Occluded(false)` event is emitted in response to an [`applicationWillEnterForeground`] + /// callback which means the application should start preparing its data. The `Occluded(true)` event is + /// emitted in response to an [`applicationDidEnterBackground`] callback which means the application + /// should free resources (according to the [iOS application lifecycle]). + /// + /// [`applicationWillEnterForeground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground + /// [`applicationDidEnterBackground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ### Others + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **Android / Wayland / Windows / Orbital:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform Occluded(bool), -} - -impl Clone for WindowEvent<'static> { - fn clone(&self) -> Self { - use self::WindowEvent::*; - return match self { - Resized(size) => Resized(*size), - Moved(pos) => Moved(*pos), - CloseRequested => CloseRequested, - Destroyed => Destroyed, - DroppedFile(file) => DroppedFile(file.clone()), - HoveredFile(file) => HoveredFile(file.clone()), - HoveredFileCancelled => HoveredFileCancelled, - Focused(f) => Focused(*f), - KeyboardInput { - device_id, - event, - is_synthetic, - } => KeyboardInput { - device_id: *device_id, - event: event.clone(), - is_synthetic: *is_synthetic, - }, - Ime(preedit_state) => Ime(preedit_state.clone()), - ModifiersChanged(modifiers) => ModifiersChanged(*modifiers), - CursorMoved { - device_id, - position, - } => CursorMoved { - device_id: *device_id, - position: *position, - }, - CursorEntered { device_id } => CursorEntered { - device_id: *device_id, - }, - CursorLeft { device_id } => CursorLeft { - device_id: *device_id, - }, - MouseWheel { - device_id, - delta, - phase, - } => MouseWheel { - device_id: *device_id, - delta: *delta, - phase: *phase, - }, - MouseInput { - device_id, - state, - button, - } => MouseInput { - device_id: *device_id, - state: *state, - button: *button, - }, - TouchpadMagnify { - device_id, - delta, - phase, - } => TouchpadMagnify { - device_id: *device_id, - delta: *delta, - phase: *phase, - }, - SmartMagnify { device_id } => SmartMagnify { - device_id: *device_id, - }, - TouchpadRotate { - device_id, - delta, - phase, - } => TouchpadRotate { - device_id: *device_id, - delta: *delta, - phase: *phase, - }, - TouchpadPressure { - device_id, - pressure, - stage, - } => TouchpadPressure { - device_id: *device_id, - pressure: *pressure, - stage: *stage, - }, - AxisMotion { - device_id, - axis, - value, - } => AxisMotion { - device_id: *device_id, - axis: *axis, - value: *value, - }, - Touch(touch) => Touch(*touch), - ThemeChanged(theme) => ThemeChanged(*theme), - ScaleFactorChanged { .. } => { - unreachable!("Static event can't be about scale factor changing") - } - Occluded(occluded) => Occluded(*occluded), - }; - } -} -impl<'a> WindowEvent<'a> { - pub fn to_static(self) -> Option> { - use self::WindowEvent::*; - match self { - Resized(size) => Some(Resized(size)), - Moved(position) => Some(Moved(position)), - CloseRequested => Some(CloseRequested), - Destroyed => Some(Destroyed), - DroppedFile(file) => Some(DroppedFile(file)), - HoveredFile(file) => Some(HoveredFile(file)), - HoveredFileCancelled => Some(HoveredFileCancelled), - Focused(focused) => Some(Focused(focused)), - KeyboardInput { - device_id, - event, - is_synthetic, - } => Some(KeyboardInput { - device_id, - event, - is_synthetic, - }), - ModifiersChanged(modifers) => Some(ModifiersChanged(modifers)), - Ime(event) => Some(Ime(event)), - CursorMoved { - device_id, - position, - } => Some(CursorMoved { - device_id, - position, - }), - CursorEntered { device_id } => Some(CursorEntered { device_id }), - CursorLeft { device_id } => Some(CursorLeft { device_id }), - MouseWheel { - device_id, - delta, - phase, - } => Some(MouseWheel { - device_id, - delta, - phase, - }), - MouseInput { - device_id, - state, - button, - } => Some(MouseInput { - device_id, - state, - button, - }), - TouchpadMagnify { - device_id, - delta, - phase, - } => Some(TouchpadMagnify { - device_id, - delta, - phase, - }), - SmartMagnify { device_id } => Some(SmartMagnify { device_id }), - TouchpadRotate { - device_id, - delta, - phase, - } => Some(TouchpadRotate { - device_id, - delta, - phase, - }), - TouchpadPressure { - device_id, - pressure, - stage, - } => Some(TouchpadPressure { - device_id, - pressure, - stage, - }), - AxisMotion { - device_id, - axis, - value, - } => Some(AxisMotion { - device_id, - axis, - value, - }), - Touch(touch) => Some(Touch(touch)), - ThemeChanged(theme) => Some(ThemeChanged(theme)), - ScaleFactorChanged { .. } => None, - Occluded(occluded) => Some(Occluded(occluded)), - } - } + /// Emitted 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`]. + /// + /// Winit will aggregate duplicate redraw requests into a single event, to + /// help avoid duplicating rendering work. + RedrawRequested, } /// Identifier of an input device. @@ -786,7 +626,8 @@ impl DeviceId { /// /// **Passing this into a winit function will result in undefined behavior.** pub const unsafe fn dummy() -> Self { - DeviceId(platform_impl::DeviceId::dummy()) + #[allow(unused_unsafe)] + DeviceId(unsafe { platform_impl::DeviceId::dummy() }) } } @@ -832,10 +673,6 @@ pub enum DeviceEvent { }, Key(RawKeyEvent), - - Text { - codepoint: char, - }, } /// Describes a keyboard input as a raw device event. @@ -848,7 +685,7 @@ pub enum DeviceEvent { #[derive(Debug, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct RawKeyEvent { - pub physical_key: keyboard::KeyCode, + pub physical_key: keyboard::PhysicalKey, pub state: ElementState, } @@ -880,12 +717,12 @@ pub struct KeyEvent { /// `Fn` and `FnLock` key events are *exceedingly unlikely* to be emitted by Winit. These keys /// are usually handled at the hardware or OS level, and aren't surfaced to applications. If /// you somehow see this in the wild, we'd like to know :) - pub physical_key: keyboard::KeyCode, + pub physical_key: keyboard::PhysicalKey, // Allowing `broken_intra_doc_links` for `logical_key`, because // `key_without_modifiers` is not available on all platforms #[cfg_attr( - not(any(target_os = "macos", target_os = "windows", target_os = "linux")), + not(any(windows_platform, macos_platform, x11_platform, wayland_platform)), allow(rustdoc::broken_intra_doc_links) )] /// This value is affected by all modifiers except Ctrl. @@ -920,7 +757,7 @@ pub struct KeyEvent { /// An additional difference from `logical_key` is that /// this field stores the text representation of any key /// that has such a representation. For example when - /// `logical_key` is `Key::Enter`, this field is `Some("\r")`. + /// `logical_key` is `Key::Named(NamedKey::Enter)`, this field is `Some("\r")`. /// /// This is `None` if the current keypress cannot /// be interpreted as text. @@ -1137,7 +974,12 @@ pub enum TouchPhase { /// /// ## Platform-specific /// +/// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. /// - **macOS:** Unsupported. +/// +/// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border +/// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding +/// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform #[derive(Debug, Clone, Copy, PartialEq)] pub struct Touch { pub device_id: DeviceId, @@ -1148,7 +990,10 @@ pub struct Touch { /// /// ## Platform-specific /// - /// - Only available on **iOS** 9.0+ and **Windows** 8+. + /// - Only available on **iOS** 9.0+, **Windows** 8+, **Web**, and **Android**. + /// - **Android**: This will never be [None]. If the device doesn't support pressure + /// sensitivity, force will either be 0.0 or 1.0. Also see the + /// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE). pub force: Option, /// Unique identifier of a finger. pub id: u64, @@ -1225,13 +1070,20 @@ pub enum ElementState { Released, } +impl ElementState { + /// True if `self == Pressed`. + pub fn is_pressed(self) -> bool { + self == ElementState::Pressed + } +} + /// Describes a button of a mouse controller. /// /// ## Platform-specific /// /// **macOS:** `Back` and `Forward` might not work with all hardware. /// **Orbital:** `Back` and `Forward` are unsupported due to orbital not supporting them. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MouseButton { Left, @@ -1268,3 +1120,237 @@ pub enum MouseScrollDelta { /// and move the content right and down (to reveal more things left and up). PixelDelta(PhysicalPosition), } + +/// Handle to synchroniously change the size of the window from the +/// [`WindowEvent`]. +#[derive(Debug, Clone)] +pub struct InnerSizeWriter { + pub(crate) new_inner_size: Weak>>, +} + +impl InnerSizeWriter { + #[cfg(not(orbital_platform))] + pub(crate) fn new(new_inner_size: Weak>>) -> Self { + Self { new_inner_size } + } + + /// Try to request inner size which will be set synchroniously on the window. + pub fn request_inner_size( + &mut self, + new_inner_size: PhysicalSize, + ) -> Result<(), ExternalError> { + if let Some(inner) = self.new_inner_size.upgrade() { + *inner.lock().unwrap() = new_inner_size; + Ok(()) + } else { + Err(ExternalError::Ignored) + } + } +} + +impl PartialEq for InnerSizeWriter { + fn eq(&self, other: &Self) -> bool { + self.new_inner_size.as_ptr() == other.new_inner_size.as_ptr() + } +} + +#[cfg(test)] +mod tests { + use crate::event; + use std::collections::{BTreeSet, HashSet}; + + macro_rules! foreach_event { + ($closure:expr) => {{ + #[allow(unused_mut)] + let mut x = $closure; + let did = unsafe { event::DeviceId::dummy() }; + + #[allow(deprecated)] + { + use crate::event::{Event::*, Ime::Enabled, WindowEvent::*}; + use crate::window::WindowId; + + // Mainline events. + let wid = unsafe { WindowId::dummy() }; + x(UserEvent(())); + x(NewEvents(event::StartCause::Init)); + x(AboutToWait); + x(LoopExiting); + x(Suspended); + x(Resumed); + + // Window events. + let with_window_event = |wev| { + x(WindowEvent { + window_id: wid, + event: wev, + }) + }; + + with_window_event(CloseRequested); + with_window_event(Destroyed); + with_window_event(Focused(true)); + with_window_event(Moved((0, 0).into())); + with_window_event(Resized((0, 0).into())); + with_window_event(DroppedFile("x.txt".into())); + with_window_event(HoveredFile("x.txt".into())); + with_window_event(HoveredFileCancelled); + with_window_event(Ime(Enabled)); + with_window_event(CursorMoved { + device_id: did, + position: (0, 0).into(), + }); + with_window_event(ModifiersChanged(event::Modifiers::default())); + with_window_event(CursorEntered { device_id: did }); + with_window_event(CursorLeft { device_id: did }); + with_window_event(MouseWheel { + device_id: did, + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(MouseInput { + device_id: did, + state: event::ElementState::Pressed, + button: event::MouseButton::Other(0), + }); + with_window_event(TouchpadMagnify { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(SmartMagnify { device_id: did }); + with_window_event(TouchpadRotate { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(TouchpadPressure { + device_id: did, + pressure: 0.0, + stage: 0, + }); + with_window_event(AxisMotion { + device_id: did, + axis: 0, + value: 0.0, + }); + with_window_event(Touch(event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + })); + with_window_event(ThemeChanged(crate::window::Theme::Light)); + with_window_event(Occluded(true)); + } + + #[allow(deprecated)] + { + use event::DeviceEvent::*; + + let with_device_event = |dev_ev| { + x(event::Event::DeviceEvent { + device_id: did, + event: dev_ev, + }) + }; + + with_device_event(Added); + with_device_event(Removed); + with_device_event(MouseMotion { + delta: (0.0, 0.0).into(), + }); + with_device_event(MouseWheel { + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + }); + with_device_event(Motion { + axis: 0, + value: 0.0, + }); + with_device_event(Button { + button: 0, + state: event::ElementState::Pressed, + }); + } + }}; + } + + #[allow(clippy::redundant_clone)] + #[test] + fn test_event_clone() { + foreach_event!(|event: event::Event<()>| { + let event2 = event.clone(); + assert_eq!(event, event2); + }) + } + + #[test] + fn test_map_nonuser_event() { + foreach_event!(|event: event::Event<()>| { + let is_user = matches!(event, event::Event::UserEvent(())); + let event2 = event.map_nonuser_event::<()>(); + if is_user { + assert_eq!(event2, Err(event::Event::UserEvent(()))); + } else { + assert!(event2.is_ok()); + } + }) + } + + #[test] + fn test_force_normalize() { + let force = event::Force::Normalized(0.0); + assert_eq!(force.normalized(), 0.0); + + let force2 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: None, + }; + assert_eq!(force2.normalized(), 2.0); + + let force3 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: Some(std::f64::consts::PI / 2.0), + }; + assert_eq!(force3.normalized(), 2.0); + } + + #[allow(clippy::clone_on_copy)] + #[test] + fn ensure_attrs_do_not_panic() { + foreach_event!(|event: event::Event<()>| { + let _ = format!("{:?}", event); + }); + let _ = event::StartCause::Init.clone(); + + let did = unsafe { crate::event::DeviceId::dummy() }.clone(); + HashSet::new().insert(did); + let mut set = [did, did, did]; + set.sort_unstable(); + let mut set2 = BTreeSet::new(); + set2.insert(did); + set2.insert(did); + + HashSet::new().insert(event::TouchPhase::Started.clone()); + HashSet::new().insert(event::MouseButton::Left.clone()); + HashSet::new().insert(event::Ime::Enabled); + + let _ = event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + } + .clone(); + let _ = event::Force::Calibrated { + force: 0.0, + max_possible_force: 0.0, + altitude_angle: None, + } + .clone(); + } +} diff --git a/src/event_loop.rs b/src/event_loop.rs index 9e59b37b91..76e797afa4 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -9,15 +9,17 @@ //! handle events. use std::marker::PhantomData; use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(any(x11_platform, wayland_platform))] +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::{error, fmt}; -use raw_window_handle::{HasRawDisplayHandle, RawDisplayHandle}; #[cfg(not(wasm_platform))] use std::time::{Duration, Instant}; #[cfg(wasm_platform)] use web_time::{Duration, Instant}; +use crate::error::EventLoopError; 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 @@ -87,23 +89,23 @@ impl EventLoopBuilder { /// ***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 - /// any thread. - /// /// Calling this function will result in display backend initialisation. /// + /// ## Panics + /// + /// Attempting to create the event loop off the main thread 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 any thread. + /// /// ## Platform-specific /// - /// - **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. - /// - **Android:** Must be configured with an `AndroidApp` from `android_main()` by calling - /// [`.with_android_app(app)`] before calling `.build()`. + /// - **Wayland/X11:** to prevent running under `Wayland` or `X11` unset `WAYLAND_DISPLAY` + /// or `DISPLAY` respectively when building the event loop. + /// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling + /// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic. /// /// [`platform`]: crate::platform #[cfg_attr( @@ -115,17 +117,17 @@ impl EventLoopBuilder { doc = "[`.with_android_app(app)`]: #only-available-on-android" )] #[inline] - pub fn build(&mut self) -> EventLoop { + pub fn build(&mut self) -> Result, EventLoopError> { if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { - panic!("Creating EventLoop multiple times is not supported."); + return Err(EventLoopError::RecreationAttempt); } // 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), + Ok(EventLoop { + event_loop: platform_impl::EventLoop::new(&mut self.platform_specific)?, _marker: PhantomData, - } + }) } #[cfg(wasm_platform)] @@ -146,28 +148,21 @@ impl fmt::Debug for EventLoopWindowTarget { } } -/// Set by the user callback given to the [`EventLoop::run`] method. -/// -/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`] is emitted. +/// Set through [`EventLoopWindowTarget::set_control_flow()`]. /// -/// Defaults to [`Poll`]. +/// Indicates the desired behavior of the event loop after [`Event::AboutToWait`] is emitted. /// -/// ## Persistency +/// Defaults to [`Wait`]. /// -/// 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. -/// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will -/// reset the control flow to [`Poll`]. -/// -/// [`ExitWithCode`]: Self::ExitWithCode -/// [`Poll`]: Self::Poll -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// [`Wait`]: Self::Wait +#[derive(Copy, Clone, Debug, Default, 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. Poll, /// When the current loop iteration finishes, suspend the thread until another event arrives. + #[default] Wait, /// When the current loop iteration finishes, suspend the thread until either another event @@ -179,88 +174,22 @@ pub enum ControlFlow { /// /// [`Poll`]: Self::Poll WaitUntil(Instant), - - /// 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`. - /// - /// The contained number will be used as exit code. The [`Exit`] constant is a shortcut for this - /// with exit code 0. - /// - /// ## 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, - /// 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), } impl ControlFlow { - /// Alias for [`ExitWithCode`]`(0)`. - /// - /// [`ExitWithCode`]: Self::ExitWithCode - #[allow(non_upper_case_globals)] - pub const Exit: Self = Self::ExitWithCode(0); - - /// Sets this to [`Poll`]. - /// - /// [`Poll`]: Self::Poll - pub fn set_poll(&mut self) { - *self = Self::Poll; - } - - /// Sets this to [`Wait`]. - /// - /// [`Wait`]: Self::Wait - pub fn set_wait(&mut self) { - *self = Self::Wait; - } - - /// Sets this to [`WaitUntil`]`(instant)`. - /// - /// [`WaitUntil`]: Self::WaitUntil - pub fn set_wait_until(&mut self, instant: Instant) { - *self = Self::WaitUntil(instant); - } - - /// Sets this to wait until a timeout has expired. + /// Creates a [`ControlFlow`] that waits until a timeout has expired. /// /// In most cases, this is set to [`WaitUntil`]. However, if the timeout overflows, it is /// instead set to [`Wait`]. /// /// [`WaitUntil`]: Self::WaitUntil /// [`Wait`]: Self::Wait - pub fn set_wait_timeout(&mut self, timeout: Duration) { + pub fn wait_duration(timeout: Duration) -> Self { match Instant::now().checked_add(timeout) { - Some(instant) => self.set_wait_until(instant), - None => self.set_wait(), + Some(instant) => Self::WaitUntil(instant), + None => Self::Wait, } } - - /// Sets this to [`ExitWithCode`]`(code)`. - /// - /// [`ExitWithCode`]: Self::ExitWithCode - pub fn set_exit_with_code(&mut self, code: i32) { - *self = Self::ExitWithCode(code); - } - - /// Sets this to [`Exit`]. - /// - /// [`Exit`]: Self::Exit - pub fn set_exit(&mut self) { - *self = Self::Exit; - } -} - -impl Default for ControlFlow { - #[inline(always)] - fn default() -> Self { - Self::Poll - } } impl EventLoop<()> { @@ -268,42 +197,54 @@ impl EventLoop<()> { /// /// [`EventLoopBuilder::new().build()`]: EventLoopBuilder::build #[inline] - pub fn new() -> EventLoop<()> { + pub fn new() -> Result, EventLoopError> { 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 { + pub fn with_user_event() -> Result, EventLoopError> { EventLoopBuilder::::with_user_event().build() } - /// Hijacks the calling thread and initializes the winit event loop with the provided - /// closure. Since the closure is `'static`, it must be a `move` closure if it needs to - /// access any data from the calling context. + /// Runs the event loop in the calling thread and calls the given `event_handler` closure + /// to dispatch any pending events. /// - /// See the [`ControlFlow`] docs for information on how changes to `&mut ControlFlow` impact the - /// event loop's behavior. + /// Since the closure is `'static`, it must be a `move` closure if it needs to + /// access any data from the calling context. /// - /// Any values not passed to this function will *not* be dropped. + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. /// /// ## Platform-specific /// - /// - **X11 / Wayland:** The program terminates with exit code 1 if the display server - /// disconnects. + /// - **iOS:** Will never return to the caller and so values not passed to this function will + /// *not* be dropped before the process exits. + /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript exception + /// (that Rust doesn't see) that will also mean that the rest of the function is never executed + /// and any values not passed to this function will *not* be dropped. + /// + /// Web applications are recommended to use + #[cfg_attr( + wasm_platform, + doc = "[`EventLoopExtWebSys::spawn()`][crate::platform::web::EventLoopExtWebSys::spawn()]" + )] + #[cfg_attr(not(wasm_platform), doc = "`EventLoopExtWebSys::spawn()`")] + /// [^1] instead of [`run()`] to avoid the need + /// for the Javascript exception trick, and to make it clearer that the event loop runs + /// asynchronously (via the browser's own, internal, event loop) and doesn't block the + /// current thread of execution like it does on other platforms. + /// + /// This function won't be available with `target_feature = "exception-handling"`. /// - /// [`ControlFlow`]: crate::event_loop::ControlFlow + /// [`set_control_flow()`]: EventLoopWindowTarget::set_control_flow() + /// [`run()`]: Self::run() + /// [^1]: `EventLoopExtWebSys::spawn()` is only available on WASM. #[inline] - pub fn run(self, event_handler: F) -> ! + #[cfg(not(all(wasm_platform, target_feature = "exception-handling")))] + pub fn run(self, event_handler: F) -> Result<(), EventLoopError> where - F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(Event, &EventLoopWindowTarget), { self.event_loop.run(event_handler) } @@ -316,10 +257,46 @@ impl EventLoop { } } -unsafe impl HasRawDisplayHandle for EventLoop { - /// Returns a [`raw_window_handle::RawDisplayHandle`] for the event loop. - fn raw_display_handle(&self) -> RawDisplayHandle { - self.event_loop.window_target().p.raw_display_handle() +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + rwh_06::HasDisplayHandle::display_handle(&**self) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoop { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + rwh_05::HasRawDisplayHandle::raw_display_handle(&**self) + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsFd for EventLoop { + /// Get the underlying [EventLoop]'s `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_events + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsRawFd for EventLoop { + /// Get the underlying [EventLoop]'s raw `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_events + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() } } @@ -347,7 +324,7 @@ impl EventLoopWindowTarget { /// /// ## Platform-specific /// - /// **Wayland:** Always returns `None`. + /// **Wayland / Web:** Always returns `None`. #[inline] pub fn primary_monitor(&self) -> Option { self.p @@ -366,16 +343,49 @@ impl EventLoopWindowTarget { /// - **Wayland / macOS / iOS / Android / Orbital:** Unsupported. /// /// [`DeviceEvent`]: crate::event::DeviceEvent - pub fn listen_device_events(&self, _allowed: DeviceEvents) { - #[cfg(any(x11_platform, wasm_platform, wayland_platform, windows))] - self.p.listen_device_events(_allowed); + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.p.listen_device_events(allowed); + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.p.set_control_flow(control_flow) + } + + /// Gets the current [`ControlFlow`]. + pub fn control_flow(&self) -> ControlFlow { + self.p.control_flow() + } + + /// This exits the event loop. + /// + /// See [`LoopExiting`](Event::LoopExiting). + pub fn exit(&self) { + self.p.exit() + } + + /// Returns if the [`EventLoop`] is about to stop. + /// + /// See [`exit()`](Self::exit). + pub fn exiting(&self) -> bool { + self.p.exiting() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoopWindowTarget { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.p.raw_display_handle_rwh_06()?; + // SAFETY: The display will never be deallocated while the event loop is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) } } -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() +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoopWindowTarget { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.p.raw_display_handle_rwh_05() } } @@ -437,3 +447,29 @@ pub enum DeviceEvents { /// Never capture device events. Never, } + +/// A unique identifier of the winit's async request. +/// +/// This could be used to identify the async request once it's done +/// and a specific action must be taken. +/// +/// One of the handling scenarious could be to maintain a working list +/// containing [`AsyncRequestSerial`] and some closure associated with it. +/// Then once event is arriving the working list is being traversed and a job +/// executed and removed from the list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsyncRequestSerial { + serial: usize, +} + +impl AsyncRequestSerial { + // TODO(kchibisov): Remove `cfg` when the clipboard will be added. + #[allow(dead_code)] + pub(crate) fn get() -> Self { + static CURRENT_SERIAL: AtomicUsize = AtomicUsize::new(0); + // NOTE: We rely on wrap around here, while the user may just request + // in the loop usize::MAX times that's issue is considered on them. + let serial = CURRENT_SERIAL.fetch_add(1, Ordering::Relaxed); + Self { serial } + } +} diff --git a/src/icon.rs b/src/icon.rs index 2927665935..b61cdd3b7f 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -49,11 +49,7 @@ impl fmt::Display for BadIcon { } } -impl Error for BadIcon { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self) - } -} +impl Error for BadIcon {} #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RgbaIcon { diff --git a/src/keyboard.rs b/src/keyboard.rs index 45a10c6d0b..d5b046151b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -69,7 +69,7 @@ // // --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- -use smol_str::SmolStr; +pub use smol_str::SmolStr; /// Contains the platform-native physical key identifier /// @@ -82,7 +82,7 @@ use smol_str::SmolStr; /// /// - Correctly match key press and release events. /// - On non-web platforms, support assigning keybinds to virtually any key through a UI. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum NativeKeyCode { Unidentified, @@ -135,7 +135,7 @@ impl std::fmt::Debug for NativeKeyCode { /// This enum is primarily used to store raw keysym when Winit doesn't map a given native logical /// key identifier to a meaningful [`Key`] variant. This lets you use [`Key`], and let the user /// define keybinds which work in the presence of identifiers we haven't mapped for you yet. -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum NativeKey { Unidentified, @@ -185,26 +185,112 @@ impl std::fmt::Debug for NativeKey { } } +impl From for NativeKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + match code { + NativeKeyCode::Unidentified => NativeKey::Unidentified, + NativeKeyCode::Android(x) => NativeKey::Android(x), + NativeKeyCode::MacOS(x) => NativeKey::MacOS(x), + NativeKeyCode::Windows(x) => NativeKey::Windows(x), + NativeKeyCode::Xkb(x) => NativeKey::Xkb(x), + } + } +} + +impl PartialEq for NativeKeyCode { + #[allow(clippy::cmp_owned)] // uses less code than direct match; target is stack allocated + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + NativeKey::from(*self) == *rhs + } +} + +impl PartialEq for NativeKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + rhs == self + } +} + /// Represents the location of a physical key. /// +/// This type is a superset of [`KeyCode`], including an [`Unidentified`](Self::Unidentified) +/// variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PhysicalKey { + /// A known key code + Code(KeyCode), + /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeKeyCode), +} + +impl From for PhysicalKey { + #[inline] + fn from(code: KeyCode) -> Self { + PhysicalKey::Code(code) + } +} + +impl From for PhysicalKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + PhysicalKey::Unidentified(code) + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &KeyCode) -> bool { + match self { + PhysicalKey::Code(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for KeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + match self { + PhysicalKey::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for NativeKeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +/// Code representing the location of a physical key +/// /// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few /// exceptions: /// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and /// "SuperRight" here. /// - The key that the specification calls "Super" is reported as `Unidentified` here. -/// - The `Unidentified` variant here, can still identify a key through it's `NativeKeyCode`. /// /// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum KeyCode { - /// This variant is used when the key cannot be translated to any other variant. - /// - /// The native keycode is provided (if available) so you're able to more reliably match - /// key-press and key-release events by hashing the [`KeyCode`]. It is also possible to use - /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. - Unidentified(NativeKeyCode), /// ` on a US keyboard. This is also called a backtick or grave. /// This is the 半角/全角/漢字 /// (hankaku/zenkaku/kanji) key on Japanese keyboards @@ -648,7 +734,7 @@ pub enum KeyCode { F35, } -/// Key represents the meaning of a keypress. +/// A [`Key::Named`] value /// /// This mostly conforms to the UI Events Specification's [`KeyboardEvent.key`] with a few /// exceptions: @@ -656,32 +742,12 @@ pub enum KeyCode { /// another key which the specification calls `Super`. That does not exist here.) /// - The `Space` variant here, can be identified by the character it generates in the /// specificaiton. -/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. -/// - The `Dead` variant here, can specify the character which is inserted when pressing the -/// dead-key twice. /// /// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ #[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Key { - /// A key string that corresponds to the character typed by the user, taking into account the - /// user’s current locale setting, and any system-level keyboard mapping overrides that are in - /// effect. - Character(Str), - - /// This variant is used when the key cannot be translated to any other variant. - /// - /// The native key is provided (if available) in order to allow the user to specify keybindings - /// for keys which are not defined by this API, mainly through some sort of UI. - Unidentified(NativeKey), - - /// Contains the text representation of the dead-key when available. - /// - /// ## Platform-specific - /// - **Web:** Always contains `None` - Dead(Option), - +pub enum NamedKey { /// The `Alt` (Alternative) key. /// /// This key enables the alternate modifier function for interpreting concurrent or subsequent @@ -1385,83 +1451,131 @@ pub enum Key { F35, } -macro_rules! map_match { - ( - $to_match:expr, - // Custom match arms - { $( $from:pat => $to:expr ),* }, - // The enum's name - $prefix:path, - // Trivial match arms for unit variants - { $( $t:tt ),* }) => { - match $to_match { - $( $from => $to, )* - $( Key::$t => Key::$t, )* +/// Key represents the meaning of a keypress. +/// +/// This is a superset of the UI Events Specification's [`KeyboardEvent.key`] with +/// additions: +/// - All simple variants are wrapped under the `Named` variant +/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. +/// - The `Dead` variant here, can specify the character which is inserted when pressing the +/// dead-key twice. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Key { + /// A simple (unparameterised) action + Named(NamedKey), + + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(Str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native key is provided (if available) in order to allow the user to specify keybindings + /// for keys which are not defined by this API, mainly through some sort of UI. + Unidentified(NativeKey), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), +} + +impl From for Key { + #[inline] + fn from(action: NamedKey) -> Self { + Key::Named(action) + } +} + +impl From for Key { + #[inline] + fn from(code: NativeKey) -> Self { + Key::Unidentified(code) + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NamedKey) -> bool { + match self { + Key::Named(ref a) => a == rhs, + _ => false, } - }; + } +} + +impl> PartialEq for Key { + #[inline] + fn eq(&self, rhs: &str) -> bool { + match self { + Key::Character(ref s) => s == rhs, + _ => false, + } + } +} + +impl> PartialEq<&str> for Key { + #[inline] + fn eq(&self, rhs: &&str) -> bool { + self == *rhs + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + match self { + Key::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq> for NativeKey { + #[inline] + fn eq(&self, rhs: &Key) -> bool { + rhs == self + } } impl Key { /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on /// `Key`. All other variants remain unchanged. pub fn as_ref(&self) -> Key<&str> { - map_match!( - self, - { - Key::Character(ch) => Key::Character(ch.as_str()), - Key::Dead(d) => Key::Dead(*d), - Key::Unidentified(u) => Key::Unidentified(u.clone()) - }, - Key, - { - Alt, AltGraph, CapsLock, Control, Fn, FnLock, NumLock, ScrollLock, Shift, Symbol, - SymbolLock, Meta, Hyper, Super, Enter, Tab, Space, ArrowDown, ArrowLeft, - ArrowRight, ArrowUp, End, Home, PageDown, PageUp, Backspace, Clear, Copy, CrSel, - Cut, Delete, EraseEof, ExSel, Insert, Paste, Redo, Undo, Accept, Again, Attn, - Cancel, ContextMenu, Escape, Execute, Find, Help, Pause, Play, Props, Select, - ZoomIn, ZoomOut, BrightnessDown, BrightnessUp, Eject, LogOff, Power, PowerOff, - PrintScreen, Hibernate, Standby, WakeUp, AllCandidates, Alphanumeric, CodeInput, - Compose, Convert, FinalMode, GroupFirst, GroupLast, GroupNext, GroupPrevious, - ModeChange, NextCandidate, NonConvert, PreviousCandidate, Process, SingleCandidate, - HangulMode, HanjaMode, JunjaMode, Eisu, Hankaku, Hiragana, HiraganaKatakana, - KanaMode, KanjiMode, Katakana, Romaji, Zenkaku, ZenkakuHankaku, Soft1, Soft2, - Soft3, Soft4, ChannelDown, ChannelUp, Close, MailForward, MailReply, MailSend, - MediaClose, MediaFastForward, MediaPause, MediaPlay, MediaPlayPause, MediaRecord, - MediaRewind, MediaStop, MediaTrackNext, MediaTrackPrevious, New, Open, Print, Save, - SpellCheck, Key11, Key12, AudioBalanceLeft, AudioBalanceRight, AudioBassBoostDown, - AudioBassBoostToggle, AudioBassBoostUp, AudioFaderFront, AudioFaderRear, - AudioSurroundModeNext, AudioTrebleDown, AudioTrebleUp, AudioVolumeDown, - AudioVolumeUp, AudioVolumeMute, MicrophoneToggle, MicrophoneVolumeDown, - MicrophoneVolumeUp, MicrophoneVolumeMute, SpeechCorrectionList, SpeechInputToggle, - LaunchApplication1, LaunchApplication2, LaunchCalendar, LaunchContacts, LaunchMail, - LaunchMediaPlayer, LaunchMusicPlayer, LaunchPhone, LaunchScreenSaver, - LaunchSpreadsheet, LaunchWebBrowser, LaunchWebCam, LaunchWordProcessor, - BrowserBack, BrowserFavorites, BrowserForward, BrowserHome, BrowserRefresh, - BrowserSearch, BrowserStop, AppSwitch, Call, Camera, CameraFocus, EndCall, GoBack, - GoHome, HeadsetHook, LastNumberRedial, Notification, MannerMode, VoiceDial, TV, - TV3DMode, TVAntennaCable, TVAudioDescription, TVAudioDescriptionMixDown, - TVAudioDescriptionMixUp, TVContentsMenu, TVDataService, TVInput, TVInputComponent1, - TVInputComponent2, TVInputComposite1, TVInputComposite2, TVInputHDMI1, - TVInputHDMI2, TVInputHDMI3, TVInputHDMI4, TVInputVGA1, TVMediaContext, TVNetwork, - TVNumberEntry, TVPower, TVRadioService, TVSatellite, TVSatelliteBS, TVSatelliteCS, - TVSatelliteToggle, TVTerrestrialAnalog, TVTerrestrialDigital, TVTimer, AVRInput, - AVRPower, ColorF0Red, ColorF1Green, ColorF2Yellow, ColorF3Blue, ColorF4Grey, - ColorF5Brown, ClosedCaptionToggle, Dimmer, DisplaySwap, DVR, Exit, FavoriteClear0, - FavoriteClear1, FavoriteClear2, FavoriteClear3, FavoriteRecall0, FavoriteRecall1, - FavoriteRecall2, FavoriteRecall3, FavoriteStore0, FavoriteStore1, FavoriteStore2, - FavoriteStore3, Guide, GuideNextDay, GuidePreviousDay, Info, InstantReplay, Link, - ListProgram, LiveContent, Lock, MediaApps, MediaAudioTrack, MediaLast, - MediaSkipBackward, MediaSkipForward, MediaStepBackward, MediaStepForward, - MediaTopMenu, NavigateIn, NavigateNext, NavigateOut, NavigatePrevious, - NextFavoriteChannel, NextUserProfile, OnDemand, Pairing, PinPDown, PinPMove, - PinPToggle, PinPUp, PlaySpeedDown, PlaySpeedReset, PlaySpeedUp, RandomToggle, - RcLowBattery, RecordSpeedNext, RfBypass, ScanChannelsToggle, ScreenModeNext, - Settings, SplitScreenToggle, STBInput, STBPower, Subtitle, Teletext, VideoModeNext, - Wink, ZoomToggle, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, - F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, - F32, F33, F34, F35 - } - ) + match self { + Key::Named(a) => Key::Named(*a), + Key::Character(ch) => Key::Character(ch.as_str()), + Key::Dead(d) => Key::Dead(*d), + Key::Unidentified(u) => Key::Unidentified(u.clone()), + } + } +} + +impl NamedKey { + /// Convert an action to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use winit::keyboard::NamedKey; + /// + /// assert_eq!(NamedKey::Enter.to_text(), Some("\r")); + /// assert_eq!(NamedKey::F20.to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + NamedKey::Enter => Some("\r"), + NamedKey::Backspace => Some("\x08"), + NamedKey::Tab => Some("\t"), + NamedKey::Space => Some(" "), + NamedKey::Escape => Some("\x1b"), + _ => None, + } } } @@ -1471,20 +1585,16 @@ impl Key { /// # Examples /// /// ``` - /// use winit::keyboard::Key; + /// use winit::keyboard::{NamedKey, Key}; /// /// assert_eq!(Key::Character("a".into()).to_text(), Some("a")); - /// assert_eq!(Key::Enter.to_text(), Some("\r")); - /// assert_eq!(Key::F20.to_text(), None); + /// assert_eq!(Key::Named(NamedKey::Enter).to_text(), Some("\r")); + /// assert_eq!(Key::Named(NamedKey::F20).to_text(), None); /// ``` pub fn to_text(&self) -> Option<&str> { match self { + Key::Named(action) => action.to_text(), Key::Character(ch) => Some(ch.as_str()), - Key::Enter => Some("\r"), - Key::Backspace => Some("\x08"), - Key::Tab => Some("\t"), - Key::Space => Some(" "), - Key::Escape => Some("\x1b"), _ => None, } } @@ -1572,7 +1682,7 @@ bitflags! { /// Represents the current state of the keyboard modifiers /// /// Each flag represents a modifier and is set if this modifier is active. - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ModifiersState: u32 { /// The "shift" key. const SHIFT = 0b100; diff --git a/src/lib.rs b/src/lib.rs index 9d2a93b82f..03af6f4898 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,15 +7,15 @@ //! //! ```no_run //! use winit::event_loop::EventLoop; -//! let event_loop = EventLoop::new(); +//! let event_loop = EventLoop::new().unwrap(); //! ``` //! -//! Once this is done there are two ways to create a [`Window`]: +//! Once this is done, there are two ways to create a [`Window`]: //! //! - Calling [`Window::new(&event_loop)`][window_new]. //! - Calling [`let builder = WindowBuilder::new()`][window_builder_new] then [`builder.build(&event_loop)`][window_builder_build]. //! -//! The first method is the simplest, and will give you default values for everything. The second +//! The first method is the simplest and will give you default values for everything. The second //! method allows you to customize the way your [`Window`] will look and behave by modifying the //! fields of the [`WindowBuilder`] object before you create the [`Window`]. //! @@ -26,64 +26,85 @@ //! window or a key getting pressed while the window is focused. Devices can generate //! [`DeviceEvent`]s, which contain unfiltered event data that isn't specific to a certain window. //! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a -//! [`DeviceEvent`]. You can also create and handle your own custom [`UserEvent`]s, if desired. +//! [`DeviceEvent`]. You can also create and handle your own custom [`Event::UserEvent`]s, if desired. //! -//! You can retrieve events by calling [`EventLoop::run`][event_loop_run]. This function will +//! You can retrieve events by calling [`EventLoop::run()`]. This function will //! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and -//! will run until the `control_flow` argument given to the closure is set to -//! [`ControlFlow`]`::`[`ExitWithCode`] (which [`ControlFlow`]`::`[`Exit`] aliases to), at which -//! point [`Event`]`::`[`LoopDestroyed`] is emitted and the entire program terminates. +//! will run until [`exit()`] is used, at which point [`Event::LoopExiting`]. //! //! Winit no longer uses a `EventLoop::poll_events() -> impl Iterator`-based event loop //! model, since that can't be implemented properly on some platforms (e.g web, iOS) and works poorly on //! most other platforms. However, this model can be re-implemented to an extent with -//! [`EventLoopExtRunReturn::run_return`]. See that method's documentation for more reasons about why -//! it's discouraged, beyond compatibility reasons. +#![cfg_attr( + any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform + ), + doc = "[`EventLoopExtPumpEvents::pump_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_events()]" +)] +#![cfg_attr( + not(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform + )), + doc = "`EventLoopExtPumpEvents::pump_events()`" +)] +//! [^1]. See that method's documentation for more reasons about why +//! it's discouraged beyond compatibility reasons. //! //! //! ```no_run //! use winit::{ //! event::{Event, WindowEvent}, -//! event_loop::EventLoop, +//! event_loop::{ControlFlow, EventLoop}, //! window::WindowBuilder, //! }; //! -//! let event_loop = EventLoop::new(); +//! let event_loop = EventLoop::new().unwrap(); //! let window = WindowBuilder::new().build(&event_loop).unwrap(); //! -//! event_loop.run(move |event, _, control_flow| { -//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't -//! // dispatched any events. This is ideal for games and similar applications. -//! control_flow.set_poll(); +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! event_loop.set_control_flow(ControlFlow::Poll); //! -//! // ControlFlow::Wait pauses the event loop if no events are available to process. -//! // This is ideal for non-game applications that only update in response to user -//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. -//! control_flow.set_wait(); +//! // ControlFlow::Wait pauses the event loop if no events are available to process. +//! // This is ideal for non-game applications that only update in response to user +//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. +//! event_loop.set_control_flow(ControlFlow::Wait); //! +//! event_loop.run(move |event, elwt| { //! match event { //! Event::WindowEvent { //! event: WindowEvent::CloseRequested, //! .. //! } => { //! println!("The close button was pressed; stopping"); -//! control_flow.set_exit(); +//! elwt.exit(); //! }, -//! Event::MainEventsCleared => { +//! Event::AboutToWait => { //! // Application update code. //! //! // Queue a RedrawRequested event. //! // -//! // You only need to call this if you've determined that you need to redraw, in +//! // You only need to call this if you've determined that you need to redraw in //! // applications which do not always need to. Applications that redraw continuously -//! // can just render here instead. +//! // can render here instead. //! window.request_redraw(); //! }, -//! Event::RedrawRequested(_) => { +//! Event::WindowEvent { +//! event: WindowEvent::RedrawRequested, +//! .. +//! } => { //! // Redraw the application. //! // //! // It's preferable for applications that do not render continuously to render in -//! // this event rather than in MainEventsCleared, since rendering in here allows +//! // this event rather than in AboutToWait, since rendering in here allows //! // the program to gracefully handle redraws requested by the OS. //! }, //! _ => () @@ -91,16 +112,16 @@ //! }); //! ``` //! -//! [`Event`]`::`[`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be -//! compared to the value returned by [`Window::id()`][window_id_fn] to determine which [`Window`] +//! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be +//! compared to the value returned by [`Window::id()`] to determine which [`Window`] //! dispatched the event. //! //! # Drawing on the window //! -//! Winit doesn't directly provide any methods for drawing on a [`Window`]. However it allows you to +//! Winit doesn't directly provide any methods for drawing on a [`Window`]. However, it allows you to //! 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. +//! 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 @@ -109,36 +130,36 @@ //! window visible only once you're ready to render into it. //! //! [`EventLoop`]: event_loop::EventLoop -//! [`EventLoopExtRunReturn::run_return`]: ./platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return //! [`EventLoop::new()`]: event_loop::EventLoop::new -//! [event_loop_run]: event_loop::EventLoop::run -//! [`ControlFlow`]: event_loop::ControlFlow -//! [`Exit`]: event_loop::ControlFlow::Exit -//! [`ExitWithCode`]: event_loop::ControlFlow::ExitWithCode +//! [`EventLoop::run()`]: event_loop::EventLoop::run +//! [`exit()`]: event_loop::EventLoopWindowTarget::exit //! [`Window`]: window::Window //! [`WindowId`]: window::WindowId //! [`WindowBuilder`]: window::WindowBuilder //! [window_new]: window::Window::new //! [window_builder_new]: window::WindowBuilder::new //! [window_builder_build]: window::WindowBuilder::build -//! [window_id_fn]: window::Window::id -//! [`Event`]: event::Event +//! [`Window::id()`]: window::Window::id //! [`WindowEvent`]: event::WindowEvent //! [`DeviceEvent`]: event::DeviceEvent -//! [`UserEvent`]: event::Event::UserEvent -//! [`LoopDestroyed`]: event::Event::LoopDestroyed -//! [`platform`]: platform +//! [`Event::UserEvent`]: event::Event::UserEvent +//! [`Event::LoopExiting`]: event::Event::LoopExiting //! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle //! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle +//! [^1]: `EventLoopExtPumpEvents::pump_events()` is only available on Windows, macOS, Android, X11 and Wayland. #![deny(rust_2018_idioms)] #![deny(rustdoc::broken_intra_doc_links)] #![deny(clippy::all)] +#![deny(unsafe_op_in_unsafe_fn)] #![cfg_attr(feature = "cargo-clippy", deny(warnings))] // Doc feature labels can be tested locally by running RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![allow(clippy::missing_safety_doc)] +#[cfg(feature = "rwh_06")] +pub use rwh_06 as raw_window_handle; + #[allow(unused_imports)] #[macro_use] extern crate log; @@ -160,3 +181,18 @@ mod platform_impl; pub mod window; pub mod platform; + +/// Wrapper for objects which winit will access on the main thread so they are effectively `Send` +/// and `Sync`, since they always execute on a single thread. +/// +/// # Safety +/// +/// Winit can run only one event loop at a time, and the event loop itself is tied to some thread. +/// The objects could be sent across the threads, but once passed to winit, they execute on the +/// main thread if the platform demands it. Thus, marking such objects as `Send + Sync` is safe. +#[doc(hidden)] +#[derive(Clone, Debug)] +pub(crate) struct SendSyncWrapper(pub(crate) T); + +unsafe impl Send for SendSyncWrapper {} +unsafe impl Sync for SendSyncWrapper {} diff --git a/src/monitor.rs b/src/monitor.rs index 6c0ae39d9b..45c355e529 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -108,20 +108,12 @@ impl MonitorHandle { /// Returns a human-readable name of the monitor. /// /// Returns `None` if the monitor doesn't exist anymore. - /// - /// ## Platform-specific - /// - /// - **Web:** Always returns None #[inline] pub fn name(&self) -> Option { self.inner.name() } /// Returns the monitor's resolution. - /// - /// ## Platform-specific - /// - /// - **Web:** Always returns (0,0) #[inline] pub fn size(&self) -> PhysicalSize { self.inner.size() @@ -129,10 +121,6 @@ impl MonitorHandle { /// Returns the top-left corner position of the monitor relative to the larger full /// screen area. - /// - /// ## Platform-specific - /// - /// - **Web:** Always returns (0,0) #[inline] pub fn position(&self) -> PhysicalPosition { self.inner.position() @@ -150,15 +138,18 @@ impl MonitorHandle { self.inner.refresh_rate_millihertz() } - /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + /// Returns the scale factor of the underlying monitor. To map logical pixels to physical + /// pixels and vice versa, use [`Window::scale_factor`]. /// /// See the [`dpi`](crate::dpi) module for more information. /// /// ## Platform-specific /// /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. + /// - **Wayland:** May differ from [`Window::scale_factor`]. /// - **Android:** Always returns 1.0. - /// - **Web:** Always returns 1.0 + /// + /// [`Window::scale_factor`]: crate::window::Window::scale_factor #[inline] pub fn scale_factor(&self) -> f64 { self.inner.scale_factor() diff --git a/src/platform/android.rs b/src/platform/android.rs index cb0528ef35..b5fde4ad49 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -84,5 +84,10 @@ impl EventLoopBuilderExtAndroid for EventLoopBuilder { /// use winit::platform::android::activity::AndroidApp; /// ``` pub mod activity { + // We enable the `"native-activity"` feature just so that we can build the + // docs, but it'll be very confusing for users to see the docs with that + // feature enabled, so we avoid inlining it so that they're forced to view + // it on the crate's own docs.rs page. + #[doc(no_inline)] pub use android_activity::*; } diff --git a/src/platform/ios.rs b/src/platform/ios.rs index cd8605ed86..3c2687e807 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -1,5 +1,6 @@ use std::os::raw::c_void; +use icrate::Foundation::MainThreadMarker; use objc2::rc::Id; use crate::{ @@ -22,27 +23,6 @@ impl EventLoopExtIOS for EventLoop { /// Additional methods on [`Window`] that are specific to iOS. pub trait WindowExtIOS { - /// Returns a pointer to the [`UIWindow`] that is used by this window. - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - /// - /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc - fn ui_window(&self) -> *mut c_void; - - /// Returns a pointer to the [`UIViewController`] that is used by this window. - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - /// - /// [`UIViewController`]: https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc - fn ui_view_controller(&self) -> *mut c_void; - - /// Returns a pointer to the [`UIView`] that is used by this window. - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - /// - /// [`UIView`]: https://developer.apple.com/documentation/uikit/uiview?language=objc - fn ui_view(&self) -> *mut c_void; - /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. /// /// The default value is device dependent, and it's recommended GLES or Metal applications set @@ -89,53 +69,63 @@ pub trait WindowExtIOS { /// /// The default is to prefer showing the status bar. /// - /// This changes the value returned by - /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc), - /// and then calls - /// [`-[UIViewController setNeedsStatusBarAppearanceUpdate]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc). + /// This sets the value of the + /// [`prefersStatusBarHidden`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the preferred status bar style for the [`Window`]. + /// + /// The default is system-defined. + /// + /// This sets the value of the + /// [`preferredStatusBarStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle); } impl WindowExtIOS for Window { - #[inline] - fn ui_window(&self) -> *mut c_void { - self.window.ui_window() - } - - #[inline] - fn ui_view_controller(&self) -> *mut c_void { - self.window.ui_view_controller() - } - - #[inline] - fn ui_view(&self) -> *mut c_void { - self.window.ui_view() - } - #[inline] fn set_scale_factor(&self, scale_factor: f64) { - self.window.set_scale_factor(scale_factor) + self.window + .maybe_queue_on_main(move |w| w.set_scale_factor(scale_factor)) } #[inline] fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { - self.window.set_valid_orientations(valid_orientations) + self.window + .maybe_queue_on_main(move |w| w.set_valid_orientations(valid_orientations)) } #[inline] fn set_prefers_home_indicator_hidden(&self, hidden: bool) { - self.window.set_prefers_home_indicator_hidden(hidden) + self.window + .maybe_queue_on_main(move |w| w.set_prefers_home_indicator_hidden(hidden)) } #[inline] fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { - self.window - .set_preferred_screen_edges_deferring_system_gestures(edges) + self.window.maybe_queue_on_main(move |w| { + w.set_preferred_screen_edges_deferring_system_gestures(edges) + }) } #[inline] fn set_prefers_status_bar_hidden(&self, hidden: bool) { - self.window.set_prefers_status_bar_hidden(hidden) + self.window + .maybe_queue_on_main(move |w| w.set_prefers_status_bar_hidden(hidden)) + } + + #[inline] + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.window + .maybe_queue_on_main(move |w| w.set_preferred_status_bar_style(status_bar_style)) } } @@ -148,7 +138,7 @@ pub trait WindowBuilderExtIOS { /// /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc - fn with_scale_factor(self, scale_factor: f64) -> WindowBuilder; + fn with_scale_factor(self, scale_factor: f64) -> Self; /// Sets the valid orientations for the [`Window`]. /// @@ -156,7 +146,7 @@ pub trait WindowBuilderExtIOS { /// /// This sets the initial value returned by /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc). - fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> WindowBuilder; + fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> Self; /// Sets whether the [`Window`] prefers the home indicator hidden. /// @@ -166,7 +156,7 @@ pub trait WindowBuilderExtIOS { /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). /// /// This only has an effect on iOS 11.0+. - fn with_prefers_home_indicator_hidden(self, hidden: bool) -> WindowBuilder; + fn with_prefers_home_indicator_hidden(self, hidden: bool) -> Self; /// Sets the screen edges for which the system gestures will take a lower priority than the /// application's touch handling. @@ -175,10 +165,7 @@ pub trait WindowBuilderExtIOS { /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). /// /// This only has an effect on iOS 11.0+. - fn with_preferred_screen_edges_deferring_system_gestures( - self, - edges: ScreenEdge, - ) -> WindowBuilder; + fn with_preferred_screen_edges_deferring_system_gestures(self, edges: ScreenEdge) -> Self; /// Sets whether the [`Window`] prefers the status bar hidden. /// @@ -186,43 +173,54 @@ pub trait WindowBuilderExtIOS { /// /// This sets the initial value returned by /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc). - fn with_prefers_status_bar_hidden(self, hidden: bool) -> WindowBuilder; + fn with_prefers_status_bar_hidden(self, hidden: bool) -> Self; + + /// Sets the style of the [`Window`]'s status bar. + /// + /// The default is system-defined. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredStatusBarStyle]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc), + fn with_preferred_status_bar_style(self, status_bar_style: StatusBarStyle) -> Self; } impl WindowBuilderExtIOS for WindowBuilder { #[inline] - fn with_scale_factor(mut self, scale_factor: f64) -> WindowBuilder { + fn with_scale_factor(mut self, scale_factor: f64) -> Self { self.platform_specific.scale_factor = Some(scale_factor); self } #[inline] - fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> WindowBuilder { + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> Self { self.platform_specific.valid_orientations = valid_orientations; self } #[inline] - fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> WindowBuilder { + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> Self { self.platform_specific.prefers_home_indicator_hidden = hidden; self } #[inline] - fn with_preferred_screen_edges_deferring_system_gestures( - mut self, - edges: ScreenEdge, - ) -> WindowBuilder { + fn with_preferred_screen_edges_deferring_system_gestures(mut self, edges: ScreenEdge) -> Self { self.platform_specific .preferred_screen_edges_deferring_system_gestures = edges; self } #[inline] - fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> WindowBuilder { + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> Self { self.platform_specific.prefers_status_bar_hidden = hidden; self } + + #[inline] + fn with_preferred_status_bar_style(mut self, status_bar_style: StatusBarStyle) -> Self { + self.platform_specific.preferred_status_bar_style = status_bar_style; + self + } } /// Additional methods on [`MonitorHandle`] that are specific to iOS. @@ -241,7 +239,9 @@ pub trait MonitorHandleExtIOS { impl MonitorHandleExtIOS for MonitorHandle { #[inline] fn ui_screen(&self) -> *mut c_void { - Id::as_ptr(self.inner.ui_screen()) as *mut c_void + // SAFETY: The marker is only used to get the pointer of the screen + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Id::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void } #[inline] @@ -298,3 +298,11 @@ bitflags! { | ScreenEdge::BOTTOM.bits() | ScreenEdge::RIGHT.bits(); } } + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StatusBarStyle { + #[default] + Default, + LightContent, + DarkContent, +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 18d4745d5e..d543c299d8 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -10,16 +10,6 @@ use crate::{ /// 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. - 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. - fn ns_view(&self) -> *mut c_void; - /// Returns whether or not the window is in simple fullscreen mode. fn simple_fullscreen(&self) -> bool; @@ -38,6 +28,28 @@ pub trait WindowExtMacOS { /// Sets whether or not the window has shadow. fn set_has_shadow(&self, has_shadow: bool); + /// Group windows together by using the same tabbing identifier. + /// + /// + fn set_tabbing_identifier(&self, identifier: &str); + + /// Returns the window's tabbing identifier. + fn tabbing_identifier(&self) -> String; + + /// Select next tab. + fn select_next_tab(&self); + + /// Select previous tab. + fn select_previous_tab(&self); + + /// Select the tab with the given index. + /// + /// Will no-op when the index is out of bounds. + fn select_tab_at_index(&self, index: usize); + + /// Get the number of tabs in the window tab group. + fn num_tabs(&self) -> usize; + /// Get the window's edit state. /// /// # Examples @@ -71,53 +83,79 @@ pub trait WindowExtMacOS { impl WindowExtMacOS for Window { #[inline] - fn ns_window(&self) -> *mut c_void { - self.window.ns_window() + fn simple_fullscreen(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.simple_fullscreen()) } #[inline] - fn ns_view(&self) -> *mut c_void { - self.window.ns_view() + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window + .maybe_wait_on_main(move |w| w.set_simple_fullscreen(fullscreen)) } #[inline] - fn simple_fullscreen(&self) -> bool { - self.window.simple_fullscreen() + fn has_shadow(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.has_shadow()) } #[inline] - fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { - self.window.set_simple_fullscreen(fullscreen) + fn set_has_shadow(&self, has_shadow: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_has_shadow(has_shadow)) } #[inline] - fn has_shadow(&self) -> bool { - self.window.has_shadow() + fn set_tabbing_identifier(&self, identifier: &str) { + self.window + .maybe_wait_on_main(|w| w.set_tabbing_identifier(identifier)) } #[inline] - fn set_has_shadow(&self, has_shadow: bool) { - self.window.set_has_shadow(has_shadow) + fn tabbing_identifier(&self) -> String { + self.window.maybe_wait_on_main(|w| w.tabbing_identifier()) + } + + #[inline] + fn select_next_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_next_tab()) + } + + #[inline] + fn select_previous_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_previous_tab()) + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + self.window + .maybe_queue_on_main(move |w| w.select_tab_at_index(index)) + } + + #[inline] + fn num_tabs(&self) -> usize { + self.window.maybe_wait_on_main(|w| w.num_tabs()) } #[inline] fn is_document_edited(&self) -> bool { - self.window.is_document_edited() + self.window.maybe_wait_on_main(|w| w.is_document_edited()) } #[inline] fn set_document_edited(&self, edited: bool) { - self.window.set_document_edited(edited) + self.window + .maybe_queue_on_main(move |w| w.set_document_edited(edited)) } #[inline] fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { - self.window.set_option_as_alt(option_as_alt) + self.window + .maybe_queue_on_main(move |w| w.set_option_as_alt(option_as_alt)) } #[inline] fn option_as_alt(&self) -> OptionAsAlt { - self.window.option_as_alt() + self.window.maybe_wait_on_main(|w| w.option_as_alt()) } } @@ -145,88 +183,96 @@ pub enum ActivationPolicy { /// - `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) - -> WindowBuilder; + fn with_movable_by_window_background(self, movable_by_window_background: bool) -> Self; /// Makes the titlebar transparent and allows the content to appear behind it. - fn with_titlebar_transparent(self, titlebar_transparent: bool) -> WindowBuilder; + fn with_titlebar_transparent(self, titlebar_transparent: bool) -> Self; /// Hides the window title. - fn with_title_hidden(self, title_hidden: bool) -> WindowBuilder; + fn with_title_hidden(self, title_hidden: bool) -> Self; /// Hides the window titlebar. - fn with_titlebar_hidden(self, titlebar_hidden: bool) -> WindowBuilder; + fn with_titlebar_hidden(self, titlebar_hidden: bool) -> Self; /// Hides the window titlebar buttons. - fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> WindowBuilder; + fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> Self; /// Makes the window content appear behind the titlebar. - fn with_fullsize_content_view(self, fullsize_content_view: bool) -> WindowBuilder; - fn with_disallow_hidpi(self, disallow_hidpi: bool) -> WindowBuilder; - fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder; + fn with_fullsize_content_view(self, fullsize_content_view: bool) -> Self; + fn with_disallow_hidpi(self, disallow_hidpi: bool) -> Self; + fn with_has_shadow(self, has_shadow: bool) -> Self; /// Window accepts click-through mouse events. - fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> WindowBuilder; + fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> Self; + /// Defines the window tabbing identifier. + /// + /// + fn with_tabbing_identifier(self, identifier: &str) -> Self; /// Set how the Option keys are interpreted. /// /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. - fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> WindowBuilder; + fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self; } impl WindowBuilderExtMacOS for WindowBuilder { #[inline] - fn with_movable_by_window_background( - mut self, - movable_by_window_background: bool, - ) -> WindowBuilder { + fn with_movable_by_window_background(mut self, movable_by_window_background: bool) -> Self { self.platform_specific.movable_by_window_background = movable_by_window_background; self } #[inline] - fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> WindowBuilder { + fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> Self { self.platform_specific.titlebar_transparent = titlebar_transparent; self } #[inline] - fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> WindowBuilder { + fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> Self { self.platform_specific.titlebar_hidden = titlebar_hidden; self } #[inline] - fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> WindowBuilder { + fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> Self { self.platform_specific.titlebar_buttons_hidden = titlebar_buttons_hidden; self } #[inline] - fn with_title_hidden(mut self, title_hidden: bool) -> WindowBuilder { + fn with_title_hidden(mut self, title_hidden: bool) -> Self { self.platform_specific.title_hidden = title_hidden; self } #[inline] - fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> WindowBuilder { + fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self { self.platform_specific.fullsize_content_view = fullsize_content_view; self } #[inline] - fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> WindowBuilder { + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> Self { self.platform_specific.disallow_hidpi = disallow_hidpi; self } #[inline] - fn with_has_shadow(mut self, has_shadow: bool) -> WindowBuilder { + fn with_has_shadow(mut self, has_shadow: bool) -> Self { self.platform_specific.has_shadow = has_shadow; self } #[inline] - fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> WindowBuilder { + fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self { self.platform_specific.accepts_first_mouse = accepts_first_mouse; self } #[inline] - fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> WindowBuilder { + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> Self { + self.platform_specific + .tabbing_identifier + .replace(tabbing_identifier.to_string()); + self + } + + #[inline] + fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> Self { self.platform_specific.option_as_alt = option_as_alt; self } @@ -329,6 +375,12 @@ pub trait EventLoopWindowTargetExtMacOS { fn hide_application(&self); /// Hide the other applications. In most applications this is typically triggered with Command+Option-H. fn hide_other_applications(&self); + /// Set whether the system can automatically organize windows into tabs. + /// + /// + fn set_allows_automatic_window_tabbing(&self, enabled: bool); + /// Returns whether the system can automatically organize windows into tabs. + fn allows_automatic_window_tabbing(&self) -> bool; } impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { @@ -339,6 +391,14 @@ impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { fn hide_other_applications(&self) { self.p.hide_other_applications() } + + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.p.set_allows_automatic_window_tabbing(enabled); + } + + fn allows_automatic_window_tabbing(&self) -> bool { + self.p.allows_automatic_window_tabbing() + } } /// Option as alt behavior. diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1985b7d7d6..6e9ee10943 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -9,9 +9,10 @@ //! - `windows` //! - `web` //! -//! And the following platform-specific module: +//! And the following platform-specific modules: //! -//! - `run_return` (available on `windows`, `unix`, `macos`, and `android`) +//! - `run_on_demand` (available on `windows`, `unix`, `macos`, `android`) +//! - `pump_events` (available on `windows`, `unix`, `macos`, `android`) //! //! However only the module corresponding to the platform you're compiling to will be available. @@ -23,6 +24,8 @@ pub mod ios; pub mod macos; #[cfg(orbital_platform)] pub mod orbital; +#[cfg(any(x11_platform, wayland_platform))] +pub mod startup_notify; #[cfg(wayland_platform)] pub mod wayland; #[cfg(wasm_platform)] @@ -32,14 +35,31 @@ pub mod windows; #[cfg(x11_platform)] pub mod x11; -pub mod modifier_supplement; #[cfg(any( windows_platform, macos_platform, android_platform, x11_platform, + wayland_platform +))] +pub mod run_on_demand; + +#[cfg(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform +))] +pub mod pump_events; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, wayland_platform, - orbital_platform + orbital_platform, + docsrs ))] -pub mod run_return; +pub mod modifier_supplement; pub mod scancode; diff --git a/src/platform/pump_events.rs b/src/platform/pump_events.rs new file mode 100644 index 0000000000..1ea11af648 --- /dev/null +++ b/src/platform/pump_events.rs @@ -0,0 +1,189 @@ +use std::time::Duration; + +use crate::{ + event::Event, + event_loop::{EventLoop, EventLoopWindowTarget}, +}; + +/// The return status for `pump_events` +pub enum PumpStatus { + /// Continue running external loop. + Continue, + /// Exit external loop. + Exit(i32), +} + +/// Additional methods on [`EventLoop`] for pumping events within an external event loop +pub trait EventLoopExtPumpEvents { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent; + + /// Pump the `EventLoop` to check for and dispatch pending events. + /// + /// This API is designed to enable applications to integrate Winit into an + /// external event loop, for platforms that can support this. + /// + /// The given `timeout` limits how long it may block waiting for new events. + /// + /// Passing a `timeout` of `Some(Duration::ZERO)` would ensure your external + /// event loop is never blocked but you would likely need to consider how + /// to throttle your own external loop. + /// + /// Passing a `timeout` of `None` means that it may wait indefinitely for new + /// events before returning control back to the external loop. + /// + /// ## Example + /// + /// ```rust,no_run + /// # // Copied from examples/window_pump_events.rs + /// # #[cfg(any( + /// # windows_platform, + /// # macos_platform, + /// # x11_platform, + /// # wayland_platform, + /// # android_platform, + /// # ))] + /// fn main() -> std::process::ExitCode { + /// # use std::{process::ExitCode, thread::sleep, time::Duration}; + /// # + /// # use simple_logger::SimpleLogger; + /// # use winit::{ + /// # event::{Event, WindowEvent}, + /// # event_loop::EventLoop, + /// # platform::pump_events::{EventLoopExtPumpEvents, PumpStatus}, + /// # window::WindowBuilder, + /// # }; + /// let mut event_loop = EventLoop::new().unwrap(); + /// # + /// # SimpleLogger::new().init().unwrap(); + /// let window = WindowBuilder::new() + /// .with_title("A fantastic window!") + /// .build(&event_loop) + /// .unwrap(); + /// + /// 'main: loop { + /// let timeout = Some(Duration::ZERO); + /// let status = event_loop.pump_events(timeout, |event, elwt| { + /// # if let Event::WindowEvent { event, .. } = &event { + /// # // Print only Window events to reduce noise + /// # println!("{event:?}"); + /// # } + /// # + /// match event { + /// Event::WindowEvent { + /// event: WindowEvent::CloseRequested, + /// window_id, + /// } if window_id == window.id() => elwt.exit(), + /// Event::AboutToWait => { + /// window.request_redraw(); + /// } + /// _ => (), + /// } + /// }); + /// if let PumpStatus::Exit(exit_code) = status { + /// break 'main ExitCode::from(exit_code as u8); + /// } + /// + /// // Sleep for 1/60 second to simulate application work + /// // + /// // Since `pump_events` doesn't block it will be important to + /// // throttle the loop in the app somehow. + /// println!("Update()"); + /// sleep(Duration::from_millis(16)); + /// } + /// } + /// ``` + /// + /// **Note:** This is not a portable API, and its usage involves a number of + /// caveats and trade offs that should be considered before using this API! + /// + /// You almost certainly shouldn't use this API, unless you absolutely know it's + /// the only practical option you have. + /// + /// ## Synchronous events + /// + /// Some events _must_ only be handled synchronously via the closure that + /// is passed to Winit so that the handler will also be synchronized with + /// the window system and operating system. + /// + /// This is because some events are driven by a window system callback + /// where the window systems expects the application to have handled the + /// event before returning. + /// + /// **These events can not be buffered and handled outside of the closure + /// passed to Winit.** + /// + /// As a general rule it is not recommended to ever buffer events to handle + /// them outside of the closure passed to Winit since it's difficult to + /// provide guarantees about which events are safe to buffer across all + /// operating systems. + /// + /// Notable events that will certainly create portability problems if + /// buffered and handled outside of Winit include: + /// - `RedrawRequested` events, used to schedule rendering. + /// + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. + /// + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. + /// - Any lifecycle events, such as `Suspended` / `Resumed`. + /// + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. + /// + /// ## Supported Platforms + /// - Windows + /// - Linux + /// - MacOS + /// - Android + /// + /// ## Unsupported Platforms + /// - **Web:** This API is fundamentally incompatible with the event-based way in which + /// Web browsers work because it's not possible to have a long-running external + /// loop that would block the browser and there is nothing that can be + /// polled to ask for new new events. Events are delivered via callbacks based + /// on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so + /// there's no way to support the same approach to polling as on MacOS. + /// + /// ## Platform-specific + /// - **Windows**: The implementation will use `PeekMessage` when checking for + /// window messages to avoid blocking your external event loop. + /// + /// - **MacOS**: The implementation works in terms of stopping the global `NSApp` + /// whenever the application `RunLoop` indicates that it is preparing to block + /// and wait for new events. + /// + /// This is very different to the polling APIs that are available on other + /// platforms (the lower level polling primitives on MacOS are private + /// implementation details for `NSApp` which aren't accessible to application + /// developers) + /// + /// It's likely this will be less efficient than polling on other OSs and + /// it also means the `NSApp` is stopped while outside of the Winit + /// event loop - and that's observable (for example to crates like `rfd`) + /// because the `NSApp` is global state. + /// + /// If you render outside of Winit you are likely to see window resizing artifacts + /// since MacOS expects applications to render synchronously during any `drawRect` + /// callback. + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &EventLoopWindowTarget); +} + +impl EventLoopExtPumpEvents for EventLoop { + type UserEvent = T; + + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &EventLoopWindowTarget), + { + self.event_loop.pump_events(timeout, event_handler) + } +} diff --git a/src/platform/run_on_demand.rs b/src/platform/run_on_demand.rs new file mode 100644 index 0000000000..f7d1d52500 --- /dev/null +++ b/src/platform/run_on_demand.rs @@ -0,0 +1,89 @@ +use crate::{ + error::EventLoopError, + event::Event, + event_loop::{EventLoop, EventLoopWindowTarget}, +}; + +#[cfg(doc)] +use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window}; + +/// Additional methods on [`EventLoop`] to return control flow to the caller. +pub trait EventLoopExtRunOnDemand { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent; + + /// Runs the event loop in the calling thread and calls the given `event_handler` closure + /// to dispatch any window system events. + /// + /// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures + /// and it is possible to return control back to the caller without + /// consuming the `EventLoop` (by using [`exit()`]) and + /// so the event loop can be re-run after it has exit. + /// + /// It's expected that each run of the loop will be for orthogonal instantiations of your + /// Winit application, but internally each instantiation may re-use some common window + /// system resources, such as a display server connection. + /// + /// This API is not designed to run an event loop in bursts that you can exit from and return + /// to while maintaining the full state of your application. (If you need something like this + /// you can look at the [`EventLoopExtPumpEvents::pump_events()`] API) + /// + /// Each time `run_on_demand` is called the `event_handler` can expect to receive a + /// `NewEvents(Init)` and `Resumed` event (even on platforms that have no suspend/resume + /// lifecycle) - which can be used to consistently initialize application state. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// # Caveats + /// - This extension isn't available on all platforms, since it's not always possible to return + /// to the caller (specifically this is impossible on iOS and Web - though with the Web + /// backend it is possible to use `EventLoopExtWebSys::spawn()`[^1] more than once instead). + /// - No [`Window`] state can be carried between separate runs of the event loop. + /// + /// You are strongly encouraged to use [`EventLoop::run()`] for portability, unless you specifically need + /// the ability to re-run a single event loop more than once + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - macOS + /// - Android + /// + /// # Unsupported Platforms + /// - **Web:** This API is fundamentally incompatible with the event-based way in which + /// Web browsers work because it's not possible to have a long-running external + /// loop that would block the browser and there is nothing that can be + /// polled to ask for new events. Events are delivered via callbacks based + /// on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS. + /// + #[cfg_attr( + not(wasm_platform), + doc = "[^1]: `spawn()` is only available on `wasm` platforms." + )] + /// + /// [`exit()`]: EventLoopWindowTarget::exit() + /// [`set_control_flow()`]: EventLoopWindowTarget::set_control_flow() + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &EventLoopWindowTarget); +} + +impl EventLoopExtRunOnDemand for EventLoop { + type UserEvent = T; + + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &EventLoopWindowTarget), + { + self.event_loop.window_target().clear_exit(); + self.event_loop.run_on_demand(event_handler) + } +} + +impl EventLoopWindowTarget { + /// Clear exit status. + pub(crate) fn clear_exit(&self) { + self.p.clear_exit() + } +} diff --git a/src/platform/run_return.rs b/src/platform/run_return.rs deleted file mode 100644 index 750b041618..0000000000 --- a/src/platform/run_return.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::{ - event::Event, - event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, -}; - -/// 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`]. - type UserEvent; - - /// Initializes the `winit` event loop. - /// - /// 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 - /// `event_handler` closure not running until the resize operation ends. Other OS operations - /// may also result in such freezes. This behavior is caused by fundamental limitations in the - /// underlying OS APIs, which cannot be hidden by `winit` without severe stability repercussions. - /// - /// You are strongly encouraged to use `run`, unless the use of this is absolutely necessary. - /// - /// ## Platform-specific - /// - /// - **X11 / Wayland:** This function returns `1` upon disconnection from - /// the display server. - fn run_return(&mut self, event_handler: F) -> i32 - where - F: FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ); -} - -impl EventLoopExtRunReturn for EventLoop { - type UserEvent = T; - - fn run_return(&mut self, event_handler: F) -> i32 - where - F: FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ), - { - self.event_loop.run_return(event_handler) - } -} diff --git a/src/platform/scancode.rs b/src/platform/scancode.rs index 3b4c6804e5..1173d98b5a 100644 --- a/src/platform/scancode.rs +++ b/src/platform/scancode.rs @@ -1,14 +1,14 @@ #![cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform))] -use crate::keyboard::KeyCode; +use crate::keyboard::{KeyCode, PhysicalKey}; // TODO: Describe what this value contains for each platform -/// Additional methods for the [`KeyCode`] type that allow the user to access the platform-specific +/// Additional methods for the [`PhysicalKey`] type that allow the user to access the platform-specific /// scancode. /// -/// [`KeyCode`]: crate::keyboard::KeyCode -pub trait KeyCodeExtScancode { +/// [`PhysicalKey`]: crate::keyboard::PhysicalKey +pub trait PhysicalKeyExtScancode { /// The raw value of the platform-specific physical key identifier. /// /// Returns `Some(key_id)` if the conversion was succesful; returns `None` otherwise. @@ -18,13 +18,28 @@ pub trait KeyCodeExtScancode { /// - **Wayland/X11**: A 32-bit linux scancode, which is X11/Wayland keycode subtracted by 8. fn to_scancode(self) -> Option; - /// Constructs a `KeyCode` from a platform-specific physical key identifier. + /// Constructs a `PhysicalKey` from a platform-specific physical key identifier. /// - /// Note that this conversion may be lossy, i.e. converting the returned `KeyCode` back + /// Note that this conversion may be lossy, i.e. converting the returned `PhysicalKey` back /// using `to_scancode` might not yield the original value. /// /// ## Platform-specific /// - **Wayland/X11**: A 32-bit linux scancode. When building from X11/Wayland keycode subtract /// `8` to get the value you wanted. - fn from_scancode(scancode: u32) -> KeyCode; + fn from_scancode(scancode: u32) -> PhysicalKey; +} + +impl PhysicalKeyExtScancode for KeyCode +where + PhysicalKey: PhysicalKeyExtScancode, +{ + #[inline] + fn from_scancode(scancode: u32) -> PhysicalKey { + ::from_scancode(scancode) + } + + #[inline] + fn to_scancode(self) -> Option { + ::to_scancode(PhysicalKey::Code(self)) + } } diff --git a/src/platform/startup_notify.rs b/src/platform/startup_notify.rs new file mode 100644 index 0000000000..323c2fe624 --- /dev/null +++ b/src/platform/startup_notify.rs @@ -0,0 +1,99 @@ +//! Window startup notification to handle window raising. +//! +//! The [`ActivationToken`] is essential to ensure that your newly +//! created window will obtain the focus, otherwise the user could +//! be requered to click on the window. +//! +//! Such token is usually delivered via the environment variable and +//! could be read from it with the [`EventLoopExtStartupNotify::read_token_from_env`]. +//! +//! Such token must also be reset after reading it from your environment with +//! [`reset_activation_token_env`] otherwise child processes could inherit it. +//! +//! When starting a new child process with a newly obtained [`ActivationToken`] from +//! [`WindowExtStartupNotify::request_activation_token`] the [`set_activation_token_env`] +//! must be used to propagate it to the child +//! +//! To ensure the delivery of such token by other processes to you, the user should +//! set `StartupNotify=true` inside the `.desktop` file of their application. +//! +//! The specification could be found [`here`]. +//! +//! [`here`]: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + +use std::env; + +use crate::error::NotSupportedError; +use crate::event_loop::{AsyncRequestSerial, EventLoopWindowTarget}; +use crate::window::{ActivationToken, Window, WindowBuilder}; + +/// The variable which is used mostly on X11. +const X11_VAR: &str = "DESKTOP_STARTUP_ID"; + +/// The variable which is used mostly on Wayland. +const WAYLAND_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +pub trait EventLoopExtStartupNotify { + /// Read the token from the environment. + /// + /// It's recommended **to unset** this environment variable for child processes. + fn read_token_from_env(&self) -> Option; +} + +pub trait WindowExtStartupNotify { + /// Request a new activation token. + /// + /// The token will be delivered inside + fn request_activation_token(&self) -> Result; +} + +pub trait WindowBuilderExtStartupNotify { + /// Use this [`ActivationToken`] during window creation. + /// + /// Not using such a token upon a window could make your window not gaining + /// focus until the user clicks on the window. + fn with_activation_token(self, token: ActivationToken) -> Self; +} + +impl EventLoopExtStartupNotify for EventLoopWindowTarget { + fn read_token_from_env(&self) -> Option { + match self.p { + #[cfg(wayland_platform)] + crate::platform_impl::EventLoopWindowTarget::Wayland(_) => env::var(WAYLAND_VAR), + #[cfg(x11_platform)] + crate::platform_impl::EventLoopWindowTarget::X(_) => env::var(X11_VAR), + } + .ok() + .map(ActivationToken::_new) + } +} + +impl WindowExtStartupNotify for Window { + fn request_activation_token(&self) -> Result { + self.window.request_activation_token() + } +} + +impl WindowBuilderExtStartupNotify for WindowBuilder { + fn with_activation_token(mut self, token: ActivationToken) -> Self { + self.platform_specific.activation_token = Some(token); + self + } +} + +/// Remove the activation environment variables from the current process. +/// +/// This is wise to do before running child processes, +/// which may not to support the activation token. +pub fn reset_activation_token_env() { + env::remove_var(X11_VAR); + env::remove_var(WAYLAND_VAR); +} + +/// Set environment variables responsible for activation token. +/// +/// This could be used before running daemon processes. +pub fn set_activation_token_env(token: ActivationToken) { + env::set_var(X11_VAR, &token._token); + env::set_var(WAYLAND_VAR, token._token); +} diff --git a/src/platform/wayland.rs b/src/platform/wayland.rs index 3e59184d70..ba87651858 100644 --- a/src/platform/wayland.rs +++ b/src/platform/wayland.rs @@ -1,17 +1,10 @@ -use std::os::raw; - -use sctk::reexports::client::Proxy; - use crate::{ event_loop::{EventLoopBuilder, EventLoopWindowTarget}, monitor::MonitorHandle, window::{Window, WindowBuilder}, }; -use crate::platform_impl::{ - ApplicationName, Backend, EventLoopWindowTarget as LinuxEventLoopWindowTarget, - Window as LinuxWindow, -}; +use crate::platform_impl::{ApplicationName, Backend}; pub use crate::window::Theme; @@ -19,16 +12,6 @@ pub use crate::window::Theme; pub trait EventLoopWindowTargetExtWayland { /// True if the [`EventLoopWindowTarget`] uses Wayland. fn is_wayland(&self) -> bool; - - /// Returns a pointer to the `wl_display` object of wayland that is used by this - /// [`EventLoopWindowTarget`]. - /// - /// 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. - /// - /// [`EventLoop`]: crate::event_loop::EventLoop - fn wayland_display(&self) -> Option<*mut raw::c_void>; } impl EventLoopWindowTargetExtWayland for EventLoopWindowTarget { @@ -36,17 +19,6 @@ impl EventLoopWindowTargetExtWayland for EventLoopWindowTarget { fn is_wayland(&self) -> bool { self.p.is_wayland() } - - #[inline] - fn wayland_display(&self) -> Option<*mut raw::c_void> { - match self.p { - LinuxEventLoopWindowTarget::Wayland(ref p) => { - Some(p.connection.display().id().as_ptr() as *mut _) - } - #[cfg(x11_platform)] - _ => None, - } - } } /// Additional methods on [`EventLoopBuilder`] that are specific to Wayland. @@ -76,41 +48,9 @@ impl EventLoopBuilderExtWayland for EventLoopBuilder { } /// Additional methods on [`Window`] that are specific to Wayland. -pub trait WindowExtWayland { - /// Returns a pointer to the `wl_surface` object of wayland that is used by this window. - /// - /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - fn wayland_surface(&self) -> Option<*mut raw::c_void>; - - /// Returns a pointer to the `wl_display` object of wayland that is used by this window. - /// - /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - fn wayland_display(&self) -> Option<*mut raw::c_void>; -} +pub trait WindowExtWayland {} -impl WindowExtWayland for Window { - #[inline] - fn wayland_surface(&self) -> Option<*mut raw::c_void> { - match self.window { - LinuxWindow::Wayland(ref w) => Some(w.surface().id().as_ptr() as *mut _), - #[cfg(x11_platform)] - _ => None, - } - } - - #[inline] - fn wayland_display(&self) -> Option<*mut raw::c_void> { - match self.window { - LinuxWindow::Wayland(ref w) => Some(w.display().id().as_ptr() as *mut _), - #[cfg(x11_platform)] - _ => None, - } - } -} +impl WindowExtWayland for Window {} /// Additional methods on [`WindowBuilder`] that are specific to Wayland. pub trait WindowBuilderExtWayland { diff --git a/src/platform/web.rs b/src/platform/web.rs index fbbba9f447..06e3f682a4 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -2,24 +2,58 @@ //! 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. +//! +//! It is recommended **not** to apply certain CSS properties to the canvas: +//! - [`transform`] +//! - [`border`] +//! - [`padding`] +//! +//! The following APIs can't take them into account and will therefore provide inaccurate results: +//! - [`WindowEvent::Resized`] and [`Window::(set_)inner_size()`] +//! - [`WindowEvent::Occluded`] +//! - [`WindowEvent::CursorMoved`], [`WindowEvent::CursorEntered`], [`WindowEvent::CursorLeft`], +//! and [`WindowEvent::Touch`]. +//! - [`Window::set_outer_position()`] +//! +//! [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized +//! [`Window::(set_)inner_size()`]: crate::window::Window::inner_size() +//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded +//! [`WindowEvent::CursorMoved`]: crate::event::WindowEvent::CursorMoved +//! [`WindowEvent::CursorEntered`]: crate::event::WindowEvent::CursorEntered +//! [`WindowEvent::CursorLeft`]: crate::event::WindowEvent::CursorLeft +//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch +//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position() +//! [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform +//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border +//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding use crate::event::Event; -use crate::event_loop::ControlFlow; use crate::event_loop::EventLoop; use crate::event_loop::EventLoopWindowTarget; -use crate::window::WindowBuilder; +use crate::window::{Window, WindowBuilder}; +use crate::SendSyncWrapper; use web_sys::HtmlCanvasElement; pub trait WindowExtWebSys { /// Only returns the canvas if called from inside the window. fn canvas(&self) -> Option; +} - /// Whether the browser reports the preferred color scheme to be "dark". - fn is_dark_mode(&self) -> bool; +impl WindowExtWebSys for Window { + #[inline] + fn canvas(&self) -> Option { + self.window.canvas() + } } pub trait WindowBuilderExtWebSys { + /// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`], + /// [`WindowBuilder::build()`] will create one. + /// + /// In any case, the canvas won't be automatically inserted into the web page. + /// + /// [`None`] by default. fn with_canvas(self, canvas: Option) -> Self; /// Whether `event.preventDefault` should be automatically called to prevent event propagation @@ -30,29 +64,40 @@ pub trait WindowBuilderExtWebSys { /// /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser /// context menu with Shift+Rightclick. + /// + /// Enabled by default. 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. + /// + /// Enabled by default. fn with_focusable(self, focusable: bool) -> Self; + + /// On window creation, append the canvas element to the web page if it isn't already. + /// + /// Disabled by default. + fn with_append(self, append: bool) -> Self; } impl WindowBuilderExtWebSys for WindowBuilder { fn with_canvas(mut self, canvas: Option) -> Self { - self.platform_specific.canvas = canvas; - + self.platform_specific.canvas = SendSyncWrapper(canvas); 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 + } + fn with_append(mut self, append: bool) -> Self { + self.platform_specific.append = append; self } } @@ -64,21 +109,31 @@ pub trait EventLoopExtWebSys { /// Initializes the winit event loop. /// - /// Unlike `run`, this returns immediately, and doesn't throw an exception in order to - /// satisfy its `!` return type. + /// Unlike + #[cfg_attr( + all(wasm_platform, target_feature = "exception-handling"), + doc = "`run()`" + )] + #[cfg_attr( + not(all(wasm_platform, target_feature = "exception-handling")), + doc = "[`run()`]" + )] + /// [^1], this returns immediately, and doesn't throw an exception in order to + /// satisfy its [`!`] return type. /// /// Once the event loop has been destroyed, it's possible to reinitialize another event loop /// by calling this function again. This can be useful if you want to recreate the event loop /// while the WebAssembly module is still loaded. For example, this can be used to recreate the /// event loop when switching between tabs on a single page application. + /// + #[cfg_attr( + not(all(wasm_platform, target_feature = "exception-handling")), + doc = "[`run()`]: EventLoop::run()" + )] + /// [^1]: `run()` is _not_ available on WASM when the target supports `exception-handling`. fn spawn(self, event_handler: F) where - F: 'static - + FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ); + F: 'static + FnMut(Event, &EventLoopWindowTarget); } impl EventLoopExtWebSys for EventLoop { @@ -86,13 +141,62 @@ impl EventLoopExtWebSys for EventLoop { fn spawn(self, event_handler: F) where - F: 'static - + FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ), + F: 'static + FnMut(Event, &EventLoopWindowTarget), { self.event_loop.spawn(event_handler) } } + +pub trait EventLoopWindowTargetExtWebSys { + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; +} + +impl EventLoopWindowTargetExtWebSys for EventLoopWindowTarget { + #[inline] + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.p.set_poll_strategy(strategy); + } + + #[inline] + fn poll_strategy(&self) -> PollStrategy { + self.p.poll_strategy() + } +} + +/// Strategy used for [`ControlFlow::Poll`](crate::event_loop::ControlFlow::Poll). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum PollStrategy { + /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will wait for the browser to enter an idle period before running and might + /// be affected by browser throttling. + /// + /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + IdleCallback, + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will run as fast as possible without disturbing users from interacting with + /// the page and is not affected by browser throttling. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 626e2fba6f..8e10157944 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -17,8 +17,6 @@ pub type HWND = isize; pub type HMENU = isize; /// Monitor Handle type used by Win32 API pub type HMONITOR = isize; -/// Instance Handle type used by Win32 API -pub type HINSTANCE = isize; /// Additional methods on `EventLoop` that are specific to Windows. pub trait EventLoopBuilderExtWindows { @@ -113,13 +111,6 @@ impl EventLoopBuilderExtWindows for EventLoopBuilder { /// Additional methods on `Window` that are specific to Windows. pub trait WindowExtWindows { - /// Returns the HINSTANCE of the window - fn hinstance(&self) -> HINSTANCE; - /// Returns the native handle that is used by this window. - /// - /// The pointer will become invalid when the native window was destroyed. - fn hwnd(&self) -> HWND; - /// Enables or disables mouse and keyboard input to the specified window. /// /// A window must be enabled before it can be activated. @@ -148,16 +139,6 @@ pub trait WindowExtWindows { } impl WindowExtWindows for Window { - #[inline] - fn hinstance(&self) -> HINSTANCE { - self.window.hinstance() - } - - #[inline] - fn hwnd(&self) -> HWND { - self.window.hwnd() - } - #[inline] fn set_enable(&self, enabled: bool) { self.window.set_enable(enabled) @@ -180,6 +161,7 @@ impl WindowExtWindows for Window { } /// Additional methods on `WindowBuilder` that are specific to Windows. +#[allow(rustdoc::broken_intra_doc_links)] pub trait WindowBuilderExtWindows { /// Set an owner to the window to be created. Can be used to create a dialog box, for example. /// This only works when [`WindowBuilder::with_parent_window`] isn't called or set to `None`. @@ -192,7 +174,7 @@ pub trait WindowBuilderExtWindows { /// - An owned window is hidden when its owner is minimized. /// /// For more information, see - fn with_owner_window(self, parent: HWND) -> WindowBuilder; + fn with_owner_window(self, parent: HWND) -> Self; /// Sets a menu on the window to be created. /// @@ -204,13 +186,13 @@ pub trait WindowBuilderExtWindows { /// If you use this, it is recommended that you combine it with `with_theme(Some(Theme::Light))` to avoid a jarring effect. /// /// [`CreateMenu`]: windows_sys::Win32::UI::WindowsAndMessaging::CreateMenu - fn with_menu(self, menu: HMENU) -> WindowBuilder; + fn with_menu(self, menu: HMENU) -> Self; /// This sets `ICON_BIG`. A good ceiling here is 256x256. - fn with_taskbar_icon(self, taskbar_icon: Option) -> WindowBuilder; + fn with_taskbar_icon(self, taskbar_icon: Option) -> Self; /// This sets `WS_EX_NOREDIRECTIONBITMAP`. - fn with_no_redirection_bitmap(self, flag: bool) -> WindowBuilder; + fn with_no_redirection_bitmap(self, flag: bool) -> Self; /// Enables or disables drag and drop support (enabled by default). Will interfere with other crates /// that use multi-threaded COM API (`CoInitializeEx` with `COINIT_MULTITHREADED` instead of @@ -218,57 +200,66 @@ pub trait WindowBuilderExtWindows { /// COM API regardless of this option. Currently only fullscreen mode does that, but there may be more in the future. /// If you need COM API with `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. /// See for more information. - fn with_drag_and_drop(self, flag: bool) -> WindowBuilder; + fn with_drag_and_drop(self, flag: bool) -> Self; /// Whether show or hide the window icon in the taskbar. - fn with_skip_taskbar(self, skip: bool) -> WindowBuilder; + fn with_skip_taskbar(self, skip: bool) -> Self; + + /// Customize the window class name. + fn with_class_name>(self, class_name: S) -> Self; /// 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; + fn with_undecorated_shadow(self, shadow: bool) -> Self; } impl WindowBuilderExtWindows for WindowBuilder { #[inline] - fn with_owner_window(mut self, parent: HWND) -> WindowBuilder { + fn with_owner_window(mut self, parent: HWND) -> Self { self.platform_specific.owner = Some(parent); self } #[inline] - fn with_menu(mut self, menu: HMENU) -> WindowBuilder { + fn with_menu(mut self, menu: HMENU) -> Self { self.platform_specific.menu = Some(menu); self } #[inline] - fn with_taskbar_icon(mut self, taskbar_icon: Option) -> WindowBuilder { + fn with_taskbar_icon(mut self, taskbar_icon: Option) -> Self { self.platform_specific.taskbar_icon = taskbar_icon; self } #[inline] - fn with_no_redirection_bitmap(mut self, flag: bool) -> WindowBuilder { + fn with_no_redirection_bitmap(mut self, flag: bool) -> Self { self.platform_specific.no_redirection_bitmap = flag; self } #[inline] - fn with_drag_and_drop(mut self, flag: bool) -> WindowBuilder { + fn with_drag_and_drop(mut self, flag: bool) -> Self { self.platform_specific.drag_and_drop = flag; self } #[inline] - fn with_skip_taskbar(mut self, skip: bool) -> WindowBuilder { + fn with_skip_taskbar(mut self, skip: bool) -> Self { self.platform_specific.skip_taskbar = skip; self } #[inline] - fn with_undecorated_shadow(mut self, shadow: bool) -> WindowBuilder { + fn with_class_name>(mut self, class_name: S) -> Self { + self.platform_specific.class_name = class_name.into(); + self + } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> Self { self.platform_specific.decoration_shadow = shadow; self } diff --git a/src/platform/x11.rs b/src/platform/x11.rs index 1dbdf3fb64..472128234e 100644 --- a/src/platform/x11.rs +++ b/src/platform/x11.rs @@ -1,6 +1,3 @@ -use std::os::raw; -use std::ptr; - use crate::{ event_loop::{EventLoopBuilder, EventLoopWindowTarget}, monitor::MonitorHandle, @@ -8,9 +5,7 @@ use crate::{ }; use crate::dpi::Size; -use crate::platform_impl::{ - x11::ffi::XVisualInfo, ApplicationName, Backend, Window as LinuxWindow, XLIB_ERROR_HOOKS, -}; +use crate::platform_impl::{ApplicationName, Backend, XLIB_ERROR_HOOKS}; pub use crate::platform_impl::{x11::util::WindowType as XWindowType, XNotSupported}; @@ -22,6 +17,12 @@ pub use crate::platform_impl::{x11::util::WindowType as XWindowType, XNotSupport pub type XlibErrorHook = Box bool + Send + Sync>; +/// A unique identifer for an X11 visual. +pub type XVisualID = u32; + +/// A unique identifier for an X11 window. +pub type XWindow = u32; + /// Hook to winit's xlib error handling callback. /// /// This method is provided as a safe way to handle the errors comming from X11 @@ -81,77 +82,21 @@ impl EventLoopBuilderExtX11 for EventLoopBuilder { } /// Additional methods on [`Window`] that are specific to X11. -pub trait WindowExtX11 { - /// 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). - fn xlib_window(&self) -> Option; +pub trait WindowExtX11 {} - /// Returns a pointer to the `Display` object of xlib that is used by this window. - /// - /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - fn xlib_display(&self) -> Option<*mut raw::c_void>; - - fn xlib_screen_id(&self) -> Option; - - /// This function returns the underlying `xcb_connection_t` of an xlib `Display`. - /// - /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). - /// - /// The pointer will become invalid when the [`Window`] is destroyed. - fn xcb_connection(&self) -> Option<*mut raw::c_void>; -} - -impl WindowExtX11 for Window { - #[inline] - fn xlib_window(&self) -> Option { - match self.window { - LinuxWindow::X(ref w) => Some(w.xlib_window()), - #[cfg(wayland_platform)] - _ => None, - } - } - - #[inline] - fn xlib_display(&self) -> Option<*mut raw::c_void> { - match self.window { - LinuxWindow::X(ref w) => Some(w.xlib_display()), - #[cfg(wayland_platform)] - _ => None, - } - } - - #[inline] - fn xlib_screen_id(&self) -> Option { - match self.window { - LinuxWindow::X(ref w) => Some(w.xlib_screen_id()), - #[cfg(wayland_platform)] - _ => None, - } - } - - #[inline] - fn xcb_connection(&self) -> Option<*mut raw::c_void> { - match self.window { - LinuxWindow::X(ref w) => Some(w.xcb_connection()), - #[cfg(wayland_platform)] - _ => None, - } - } -} +impl WindowExtX11 for Window {} /// Additional methods on [`WindowBuilder`] that are specific to X11. pub trait WindowBuilderExtX11 { - fn with_x11_visual(self, visual_infos: *const T) -> Self; + /// Create this window with a specific X11 visual. + fn with_x11_visual(self, visual_id: XVisualID) -> Self; fn with_x11_screen(self, screen_id: i32) -> Self; /// Build window with the given `general` and `instance` names. /// /// 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"`. + /// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "instance", "general"`. /// /// 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) @@ -176,21 +121,35 @@ pub trait WindowBuilderExtX11 { /// WindowBuilder::new().with_base_size(PhysicalSize::new(400, 200)); /// ``` fn with_base_size>(self, base_size: S) -> Self; + + /// Embed this window into another parent window. + /// + /// # Example + /// + /// ```no_run + /// use winit::window::WindowBuilder; + /// use winit::platform::x11::{XWindow, WindowBuilderExtX11}; + /// # fn main() -> Result<(), Box> { + /// let event_loop = winit::event_loop::EventLoop::new().unwrap(); + /// let parent_window_id = std::env::args().nth(1).unwrap().parse::()?; + /// let window = WindowBuilder::new() + /// .with_embed_parent_window(parent_window_id) + /// .build(&event_loop)?; + /// # Ok(()) } + /// ``` + fn with_embed_parent_window(self, parent_window_id: XWindow) -> Self; } impl WindowBuilderExtX11 for WindowBuilder { #[inline] - fn with_x11_visual(mut self, visual_infos: *const T) -> Self { - { - self.platform_specific.visual_infos = - Some(unsafe { ptr::read(visual_infos as *const XVisualInfo) }); - } + fn with_x11_visual(mut self, visual_id: XVisualID) -> Self { + self.platform_specific.x11.visual_id = Some(visual_id); self } #[inline] fn with_x11_screen(mut self, screen_id: i32) -> Self { - self.platform_specific.screen_id = Some(screen_id); + self.platform_specific.x11.screen_id = Some(screen_id); self } @@ -202,19 +161,25 @@ impl WindowBuilderExtX11 for WindowBuilder { #[inline] fn with_override_redirect(mut self, override_redirect: bool) -> Self { - self.platform_specific.override_redirect = override_redirect; + self.platform_specific.x11.override_redirect = override_redirect; self } #[inline] fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { - self.platform_specific.x11_window_types = x11_window_types; + self.platform_specific.x11.x11_window_types = x11_window_types; self } #[inline] fn with_base_size>(mut self, base_size: S) -> Self { - self.platform_specific.base_size = Some(base_size.into()); + self.platform_specific.x11.base_size = Some(base_size.into()); + self + } + + #[inline] + fn with_embed_parent_window(mut self, parent_window_id: XWindow) -> Self { + self.platform_specific.x11.embed_window = Some(parent_window_id); self } } diff --git a/src/platform_impl/android/keycodes.rs b/src/platform_impl/android/keycodes.rs index 8933e79665..b1b60089c0 100644 --- a/src/platform_impl/android/keycodes.rs +++ b/src/platform_impl/android/keycodes.rs @@ -1,9 +1,12 @@ -use android_activity::input::Keycode; +use android_activity::{ + input::{KeyAction, KeyEvent, KeyMapChar, Keycode}, + AndroidApp, +}; -use crate::keyboard::{Key, KeyCode, KeyLocation, NativeKey, NativeKeyCode}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; -pub fn to_physical_keycode(keycode: Keycode) -> KeyCode { - match keycode { +pub fn to_physical_key(keycode: Keycode) -> PhysicalKey { + PhysicalKey::Code(match keycode { Keycode::A => KeyCode::KeyA, Keycode::B => KeyCode::KeyB, Keycode::C => KeyCode::KeyC, @@ -152,328 +155,411 @@ pub fn to_physical_keycode(keycode: Keycode) -> KeyCode { Keycode::Sleep => KeyCode::Sleep, // what about SoftSleep? Keycode::Wakeup => KeyCode::WakeUp, - keycode => KeyCode::Unidentified(NativeKeyCode::Android(keycode.into())), + keycode => return PhysicalKey::Unidentified(NativeKeyCode::Android(keycode.into())), + }) +} + +/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent +/// +/// This takes a `KeyEvent` and looks up its corresponding `KeyCharacterMap` and +/// uses that to try and map the `key_code` + `meta_state` to a unicode +/// character or a dead key that can be combined with the next key press. +pub fn character_map_and_combine_key( + app: &AndroidApp, + key_event: &KeyEvent<'_>, + combining_accent: &mut Option, +) -> Option { + let device_id = key_event.device_id(); + + let key_map = match app.device_key_character_map(device_id) { + Ok(key_map) => key_map, + Err(err) => { + log::warn!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}"); + return None; + } + }; + + match key_map.get(key_event.key_code(), key_event.meta_state()) { + Ok(KeyMapChar::Unicode(unicode)) => { + // Only do dead key combining on key down + if key_event.action() == KeyAction::Down { + let combined_unicode = if let Some(accent) = combining_accent { + match key_map.get_dead_char(*accent, unicode) { + Ok(Some(key)) => Some(key), + Ok(None) => None, + Err(err) => { + log::warn!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}"); + None + } + } + } else { + Some(unicode) + }; + *combining_accent = None; + combined_unicode.map(KeyMapChar::Unicode) + } else { + Some(KeyMapChar::Unicode(unicode)) + } + } + Ok(KeyMapChar::CombiningAccent(accent)) => { + if key_event.action() == KeyAction::Down { + *combining_accent = Some(accent); + } + Some(KeyMapChar::CombiningAccent(accent)) + } + Ok(KeyMapChar::None) => { + // Leave any combining_accent state in tact (seems to match how other + // Android apps work) + None + } + Err(err) => { + log::warn!("KeyEvent: Failed to get key map character: {err:?}"); + *combining_accent = None; + None + } } } -// TODO: We need to expose getUnicodeChar via android-activity instead of having -// a fixed mapping from key codes -pub fn to_logical(keycode: Keycode, native: NativeKey) -> Key { +pub fn to_logical(key_char: Option, keycode: Keycode) -> Key { use android_activity::input::Keycode::*; - match keycode { - Unknown => Key::Unidentified(native), - - // Can be added on demand - SoftLeft => Key::Unidentified(native), - SoftRight => Key::Unidentified(native), - - // Using `BrowserHome` instead of `GoHome` according to - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values - Home => Key::BrowserHome, - Back => Key::BrowserBack, - Call => Key::Call, - Endcall => Key::EndCall, - - //------------------------------------------------------------------------------- - // Reporting unidentified, because the specific character is layout dependent. - // (I'm not sure though) - Keycode0 => Key::Character("0".into()), - Keycode1 => Key::Character("1".into()), - Keycode2 => Key::Character("2".into()), - Keycode3 => Key::Character("3".into()), - Keycode4 => Key::Character("4".into()), - Keycode5 => Key::Character("5".into()), - Keycode6 => Key::Character("6".into()), - Keycode7 => Key::Character("7".into()), - Keycode8 => Key::Character("8".into()), - Keycode9 => Key::Character("9".into()), - Star => Key::Character("*".into()), - Pound => Key::Character("#".into()), - A => Key::Character("a".into()), - B => Key::Character("b".into()), - C => Key::Character("c".into()), - D => Key::Character("d".into()), - E => Key::Character("e".into()), - F => Key::Character("f".into()), - G => Key::Character("g".into()), - H => Key::Character("h".into()), - I => Key::Character("i".into()), - J => Key::Character("j".into()), - K => Key::Character("k".into()), - L => Key::Character("l".into()), - M => Key::Character("m".into()), - N => Key::Character("n".into()), - O => Key::Character("o".into()), - P => Key::Character("p".into()), - Q => Key::Character("q".into()), - R => Key::Character("r".into()), - S => Key::Character("s".into()), - T => Key::Character("t".into()), - U => Key::Character("u".into()), - V => Key::Character("v".into()), - W => Key::Character("w".into()), - X => Key::Character("x".into()), - Y => Key::Character("y".into()), - Z => Key::Character("z".into()), - Comma => Key::Character(",".into()), - Period => Key::Character(".".into()), - Grave => Key::Character("`".into()), - Minus => Key::Character("-".into()), - Equals => Key::Character("=".into()), - LeftBracket => Key::Character("[".into()), - RightBracket => Key::Character("]".into()), - Backslash => Key::Character("\\".into()), - Semicolon => Key::Character(";".into()), - Apostrophe => Key::Character("'".into()), - Slash => Key::Character("/".into()), - At => Key::Character("@".into()), - Plus => Key::Character("+".into()), - //------------------------------------------------------------------------------- - DpadUp => Key::ArrowUp, - DpadDown => Key::ArrowDown, - DpadLeft => Key::ArrowLeft, - DpadRight => Key::ArrowRight, - DpadCenter => Key::Enter, - - VolumeUp => Key::AudioVolumeUp, - VolumeDown => Key::AudioVolumeDown, - Power => Key::Power, - Camera => Key::Camera, - Clear => Key::Clear, - - AltLeft => Key::Alt, - AltRight => Key::Alt, - ShiftLeft => Key::Shift, - ShiftRight => Key::Shift, - Tab => Key::Tab, - Space => Key::Space, - Sym => Key::Symbol, - Explorer => Key::LaunchWebBrowser, - Envelope => Key::LaunchMail, - Enter => Key::Enter, - Del => Key::Backspace, - - // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM - Num => Key::Alt, - - Headsethook => Key::HeadsetHook, - Focus => Key::CameraFocus, - - Menu => Key::Unidentified(native), - - Notification => Key::Notification, - Search => Key::BrowserSearch, - MediaPlayPause => Key::MediaPlayPause, - MediaStop => Key::MediaStop, - MediaNext => Key::MediaTrackNext, - MediaPrevious => Key::MediaTrackPrevious, - MediaRewind => Key::MediaRewind, - MediaFastForward => Key::MediaFastForward, - Mute => Key::MicrophoneVolumeMute, - PageUp => Key::PageUp, - PageDown => Key::PageDown, - Pictsymbols => Key::Unidentified(native), - SwitchCharset => Key::Unidentified(native), - - // ----------------------------------------------------------------- - // Gamepad events should be exposed through a separate API, not - // keyboard events - ButtonA => Key::Unidentified(native), - ButtonB => Key::Unidentified(native), - ButtonC => Key::Unidentified(native), - ButtonX => Key::Unidentified(native), - ButtonY => Key::Unidentified(native), - ButtonZ => Key::Unidentified(native), - ButtonL1 => Key::Unidentified(native), - ButtonR1 => Key::Unidentified(native), - ButtonL2 => Key::Unidentified(native), - ButtonR2 => Key::Unidentified(native), - ButtonThumbl => Key::Unidentified(native), - ButtonThumbr => Key::Unidentified(native), - ButtonStart => Key::Unidentified(native), - ButtonSelect => Key::Unidentified(native), - ButtonMode => Key::Unidentified(native), - // ----------------------------------------------------------------- - Escape => Key::Escape, - ForwardDel => Key::Delete, - CtrlLeft => Key::Control, - CtrlRight => Key::Control, - CapsLock => Key::CapsLock, - ScrollLock => Key::ScrollLock, - MetaLeft => Key::Super, - MetaRight => Key::Super, - Function => Key::Fn, - Sysrq => Key::PrintScreen, - Break => Key::Pause, - MoveHome => Key::Home, - MoveEnd => Key::End, - Insert => Key::Insert, - Forward => Key::BrowserForward, - MediaPlay => Key::MediaPlay, - MediaPause => Key::MediaPause, - MediaClose => Key::MediaClose, - MediaEject => Key::Eject, - MediaRecord => Key::MediaRecord, - F1 => Key::F1, - F2 => Key::F2, - F3 => Key::F3, - F4 => Key::F4, - F5 => Key::F5, - F6 => Key::F6, - F7 => Key::F7, - F8 => Key::F8, - F9 => Key::F9, - F10 => Key::F10, - F11 => Key::F11, - F12 => Key::F12, - NumLock => Key::NumLock, - Numpad0 => Key::Character("0".into()), - Numpad1 => Key::Character("1".into()), - Numpad2 => Key::Character("2".into()), - Numpad3 => Key::Character("3".into()), - Numpad4 => Key::Character("4".into()), - Numpad5 => Key::Character("5".into()), - Numpad6 => Key::Character("6".into()), - Numpad7 => Key::Character("7".into()), - Numpad8 => Key::Character("8".into()), - Numpad9 => Key::Character("9".into()), - NumpadDivide => Key::Character("/".into()), - NumpadMultiply => Key::Character("*".into()), - NumpadSubtract => Key::Character("-".into()), - NumpadAdd => Key::Character("+".into()), - NumpadDot => Key::Character(".".into()), - NumpadComma => Key::Character(",".into()), - NumpadEnter => Key::Enter, - NumpadEquals => Key::Character("=".into()), - NumpadLeftParen => Key::Character("(".into()), - NumpadRightParen => Key::Character(")".into()), - - VolumeMute => Key::AudioVolumeMute, - Info => Key::Info, - ChannelUp => Key::ChannelUp, - ChannelDown => Key::ChannelDown, - ZoomIn => Key::ZoomIn, - ZoomOut => Key::ZoomOut, - Tv => Key::TV, - Window => Key::Unidentified(native), - Guide => Key::Guide, - Dvr => Key::DVR, - Bookmark => Key::BrowserFavorites, - Captions => Key::ClosedCaptionToggle, - Settings => Key::Settings, - TvPower => Key::TVPower, - TvInput => Key::TVInput, - StbPower => Key::STBPower, - StbInput => Key::STBInput, - AvrPower => Key::AVRPower, - AvrInput => Key::AVRInput, - ProgRed => Key::ColorF0Red, - ProgGreen => Key::ColorF1Green, - ProgYellow => Key::ColorF2Yellow, - ProgBlue => Key::ColorF3Blue, - AppSwitch => Key::AppSwitch, - Button1 => Key::Unidentified(native), - Button2 => Key::Unidentified(native), - Button3 => Key::Unidentified(native), - Button4 => Key::Unidentified(native), - Button5 => Key::Unidentified(native), - Button6 => Key::Unidentified(native), - Button7 => Key::Unidentified(native), - Button8 => Key::Unidentified(native), - Button9 => Key::Unidentified(native), - Button10 => Key::Unidentified(native), - Button11 => Key::Unidentified(native), - Button12 => Key::Unidentified(native), - Button13 => Key::Unidentified(native), - Button14 => Key::Unidentified(native), - Button15 => Key::Unidentified(native), - Button16 => Key::Unidentified(native), - LanguageSwitch => Key::GroupNext, - MannerMode => Key::MannerMode, - Keycode3dMode => Key::TV3DMode, - Contacts => Key::LaunchContacts, - Calendar => Key::LaunchCalendar, - Music => Key::LaunchMusicPlayer, - Calculator => Key::LaunchApplication2, - ZenkakuHankaku => Key::ZenkakuHankaku, - Eisu => Key::Eisu, - Muhenkan => Key::NonConvert, - Henkan => Key::Convert, - KatakanaHiragana => Key::HiraganaKatakana, - Yen => Key::Unidentified(native), - Ro => Key::Unidentified(native), - Kana => Key::KanjiMode, - Assist => Key::Unidentified(native), - BrightnessDown => Key::BrightnessDown, - BrightnessUp => Key::BrightnessUp, - MediaAudioTrack => Key::MediaAudioTrack, - Sleep => Key::Standby, - Wakeup => Key::WakeUp, - Pairing => Key::Pairing, - MediaTopMenu => Key::MediaTopMenu, - Keycode11 => Key::Unidentified(native), - Keycode12 => Key::Unidentified(native), - LastChannel => Key::MediaLast, - TvDataService => Key::TVDataService, - VoiceAssist => Key::VoiceDial, - TvRadioService => Key::TVRadioService, - TvTeletext => Key::Teletext, - TvNumberEntry => Key::TVNumberEntry, - TvTerrestrialAnalog => Key::TVTerrestrialAnalog, - TvTerrestrialDigital => Key::TVTerrestrialDigital, - TvSatellite => Key::TVSatellite, - TvSatelliteBs => Key::TVSatelliteBS, - TvSatelliteCs => Key::TVSatelliteCS, - TvSatelliteService => Key::TVSatelliteToggle, - TvNetwork => Key::TVNetwork, - TvAntennaCable => Key::TVAntennaCable, - TvInputHdmi1 => Key::TVInputHDMI1, - TvInputHdmi2 => Key::TVInputHDMI2, - TvInputHdmi3 => Key::TVInputHDMI3, - TvInputHdmi4 => Key::TVInputHDMI4, - TvInputComposite1 => Key::TVInputComposite1, - TvInputComposite2 => Key::TVInputComposite2, - TvInputComponent1 => Key::TVInputComponent1, - TvInputComponent2 => Key::TVInputComponent2, - TvInputVga1 => Key::TVInputVGA1, - TvAudioDescription => Key::TVAudioDescription, - TvAudioDescriptionMixUp => Key::TVAudioDescriptionMixUp, - TvAudioDescriptionMixDown => Key::TVAudioDescriptionMixDown, - TvZoomMode => Key::ZoomToggle, - TvContentsMenu => Key::TVContentsMenu, - TvMediaContextMenu => Key::TVMediaContext, - TvTimerProgramming => Key::TVTimer, - Help => Key::Help, - NavigatePrevious => Key::NavigatePrevious, - NavigateNext => Key::NavigateNext, - NavigateIn => Key::NavigateIn, - NavigateOut => Key::NavigateOut, - StemPrimary => Key::Unidentified(native), - Stem1 => Key::Unidentified(native), - Stem2 => Key::Unidentified(native), - Stem3 => Key::Unidentified(native), - DpadUpLeft => Key::Unidentified(native), - DpadDownLeft => Key::Unidentified(native), - DpadUpRight => Key::Unidentified(native), - DpadDownRight => Key::Unidentified(native), - MediaSkipForward => Key::MediaSkipForward, - MediaSkipBackward => Key::MediaSkipBackward, - MediaStepForward => Key::MediaStepForward, - MediaStepBackward => Key::MediaStepBackward, - SoftSleep => Key::Unidentified(native), - Cut => Key::Cut, - Copy => Key::Copy, - Paste => Key::Paste, - SystemNavigationUp => Key::Unidentified(native), - SystemNavigationDown => Key::Unidentified(native), - SystemNavigationLeft => Key::Unidentified(native), - SystemNavigationRight => Key::Unidentified(native), - AllApps => Key::Unidentified(native), - Refresh => Key::BrowserRefresh, - ThumbsUp => Key::Unidentified(native), - ThumbsDown => Key::Unidentified(native), - ProfileSwitch => Key::Unidentified(native), + let native = NativeKey::Android(keycode.into()); + + match key_char { + Some(KeyMapChar::Unicode(c)) => Key::Character(smol_str::SmolStr::from_iter([c])), + Some(KeyMapChar::CombiningAccent(c)) => Key::Dead(Some(c)), + None | Some(KeyMapChar::None) => match keycode { + // Using `BrowserHome` instead of `GoHome` according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + Home => Key::Named(NamedKey::BrowserHome), + Back => Key::Named(NamedKey::BrowserBack), + Call => Key::Named(NamedKey::Call), + Endcall => Key::Named(NamedKey::EndCall), + + //------------------------------------------------------------------------------- + // These should be redundant because they should have already been matched + // as `KeyMapChar::Unicode`, but also matched here as a fallback + Keycode0 => Key::Character("0".into()), + Keycode1 => Key::Character("1".into()), + Keycode2 => Key::Character("2".into()), + Keycode3 => Key::Character("3".into()), + Keycode4 => Key::Character("4".into()), + Keycode5 => Key::Character("5".into()), + Keycode6 => Key::Character("6".into()), + Keycode7 => Key::Character("7".into()), + Keycode8 => Key::Character("8".into()), + Keycode9 => Key::Character("9".into()), + Star => Key::Character("*".into()), + Pound => Key::Character("#".into()), + A => Key::Character("a".into()), + B => Key::Character("b".into()), + C => Key::Character("c".into()), + D => Key::Character("d".into()), + E => Key::Character("e".into()), + F => Key::Character("f".into()), + G => Key::Character("g".into()), + H => Key::Character("h".into()), + I => Key::Character("i".into()), + J => Key::Character("j".into()), + K => Key::Character("k".into()), + L => Key::Character("l".into()), + M => Key::Character("m".into()), + N => Key::Character("n".into()), + O => Key::Character("o".into()), + P => Key::Character("p".into()), + Q => Key::Character("q".into()), + R => Key::Character("r".into()), + S => Key::Character("s".into()), + T => Key::Character("t".into()), + U => Key::Character("u".into()), + V => Key::Character("v".into()), + W => Key::Character("w".into()), + X => Key::Character("x".into()), + Y => Key::Character("y".into()), + Z => Key::Character("z".into()), + Comma => Key::Character(",".into()), + Period => Key::Character(".".into()), + Grave => Key::Character("`".into()), + Minus => Key::Character("-".into()), + Equals => Key::Character("=".into()), + LeftBracket => Key::Character("[".into()), + RightBracket => Key::Character("]".into()), + Backslash => Key::Character("\\".into()), + Semicolon => Key::Character(";".into()), + Apostrophe => Key::Character("'".into()), + Slash => Key::Character("/".into()), + At => Key::Character("@".into()), + Plus => Key::Character("+".into()), + //------------------------------------------------------------------------------- + DpadUp => Key::Named(NamedKey::ArrowUp), + DpadDown => Key::Named(NamedKey::ArrowDown), + DpadLeft => Key::Named(NamedKey::ArrowLeft), + DpadRight => Key::Named(NamedKey::ArrowRight), + DpadCenter => Key::Named(NamedKey::Enter), + + VolumeUp => Key::Named(NamedKey::AudioVolumeUp), + VolumeDown => Key::Named(NamedKey::AudioVolumeDown), + Power => Key::Named(NamedKey::Power), + Camera => Key::Named(NamedKey::Camera), + Clear => Key::Named(NamedKey::Clear), + + AltLeft => Key::Named(NamedKey::Alt), + AltRight => Key::Named(NamedKey::Alt), + ShiftLeft => Key::Named(NamedKey::Shift), + ShiftRight => Key::Named(NamedKey::Shift), + Tab => Key::Named(NamedKey::Tab), + Space => Key::Named(NamedKey::Space), + Sym => Key::Named(NamedKey::Symbol), + Explorer => Key::Named(NamedKey::LaunchWebBrowser), + Envelope => Key::Named(NamedKey::LaunchMail), + Enter => Key::Named(NamedKey::Enter), + Del => Key::Named(NamedKey::Backspace), + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => Key::Named(NamedKey::Alt), + + Headsethook => Key::Named(NamedKey::HeadsetHook), + Focus => Key::Named(NamedKey::CameraFocus), + + Notification => Key::Named(NamedKey::Notification), + Search => Key::Named(NamedKey::BrowserSearch), + MediaPlayPause => Key::Named(NamedKey::MediaPlayPause), + MediaStop => Key::Named(NamedKey::MediaStop), + MediaNext => Key::Named(NamedKey::MediaTrackNext), + MediaPrevious => Key::Named(NamedKey::MediaTrackPrevious), + MediaRewind => Key::Named(NamedKey::MediaRewind), + MediaFastForward => Key::Named(NamedKey::MediaFastForward), + Mute => Key::Named(NamedKey::MicrophoneVolumeMute), + PageUp => Key::Named(NamedKey::PageUp), + PageDown => Key::Named(NamedKey::PageDown), + + Escape => Key::Named(NamedKey::Escape), + ForwardDel => Key::Named(NamedKey::Delete), + CtrlLeft => Key::Named(NamedKey::Control), + CtrlRight => Key::Named(NamedKey::Control), + CapsLock => Key::Named(NamedKey::CapsLock), + ScrollLock => Key::Named(NamedKey::ScrollLock), + MetaLeft => Key::Named(NamedKey::Super), + MetaRight => Key::Named(NamedKey::Super), + Function => Key::Named(NamedKey::Fn), + Sysrq => Key::Named(NamedKey::PrintScreen), + Break => Key::Named(NamedKey::Pause), + MoveHome => Key::Named(NamedKey::Home), + MoveEnd => Key::Named(NamedKey::End), + Insert => Key::Named(NamedKey::Insert), + Forward => Key::Named(NamedKey::BrowserForward), + MediaPlay => Key::Named(NamedKey::MediaPlay), + MediaPause => Key::Named(NamedKey::MediaPause), + MediaClose => Key::Named(NamedKey::MediaClose), + MediaEject => Key::Named(NamedKey::Eject), + MediaRecord => Key::Named(NamedKey::MediaRecord), + F1 => Key::Named(NamedKey::F1), + F2 => Key::Named(NamedKey::F2), + F3 => Key::Named(NamedKey::F3), + F4 => Key::Named(NamedKey::F4), + F5 => Key::Named(NamedKey::F5), + F6 => Key::Named(NamedKey::F6), + F7 => Key::Named(NamedKey::F7), + F8 => Key::Named(NamedKey::F8), + F9 => Key::Named(NamedKey::F9), + F10 => Key::Named(NamedKey::F10), + F11 => Key::Named(NamedKey::F11), + F12 => Key::Named(NamedKey::F12), + NumLock => Key::Named(NamedKey::NumLock), + Numpad0 => Key::Character("0".into()), + Numpad1 => Key::Character("1".into()), + Numpad2 => Key::Character("2".into()), + Numpad3 => Key::Character("3".into()), + Numpad4 => Key::Character("4".into()), + Numpad5 => Key::Character("5".into()), + Numpad6 => Key::Character("6".into()), + Numpad7 => Key::Character("7".into()), + Numpad8 => Key::Character("8".into()), + Numpad9 => Key::Character("9".into()), + NumpadDivide => Key::Character("/".into()), + NumpadMultiply => Key::Character("*".into()), + NumpadSubtract => Key::Character("-".into()), + NumpadAdd => Key::Character("+".into()), + NumpadDot => Key::Character(".".into()), + NumpadComma => Key::Character(",".into()), + NumpadEnter => Key::Named(NamedKey::Enter), + NumpadEquals => Key::Character("=".into()), + NumpadLeftParen => Key::Character("(".into()), + NumpadRightParen => Key::Character(")".into()), + + VolumeMute => Key::Named(NamedKey::AudioVolumeMute), + Info => Key::Named(NamedKey::Info), + ChannelUp => Key::Named(NamedKey::ChannelUp), + ChannelDown => Key::Named(NamedKey::ChannelDown), + ZoomIn => Key::Named(NamedKey::ZoomIn), + ZoomOut => Key::Named(NamedKey::ZoomOut), + Tv => Key::Named(NamedKey::TV), + Guide => Key::Named(NamedKey::Guide), + Dvr => Key::Named(NamedKey::DVR), + Bookmark => Key::Named(NamedKey::BrowserFavorites), + Captions => Key::Named(NamedKey::ClosedCaptionToggle), + Settings => Key::Named(NamedKey::Settings), + TvPower => Key::Named(NamedKey::TVPower), + TvInput => Key::Named(NamedKey::TVInput), + StbPower => Key::Named(NamedKey::STBPower), + StbInput => Key::Named(NamedKey::STBInput), + AvrPower => Key::Named(NamedKey::AVRPower), + AvrInput => Key::Named(NamedKey::AVRInput), + ProgRed => Key::Named(NamedKey::ColorF0Red), + ProgGreen => Key::Named(NamedKey::ColorF1Green), + ProgYellow => Key::Named(NamedKey::ColorF2Yellow), + ProgBlue => Key::Named(NamedKey::ColorF3Blue), + AppSwitch => Key::Named(NamedKey::AppSwitch), + LanguageSwitch => Key::Named(NamedKey::GroupNext), + MannerMode => Key::Named(NamedKey::MannerMode), + Keycode3dMode => Key::Named(NamedKey::TV3DMode), + Contacts => Key::Named(NamedKey::LaunchContacts), + Calendar => Key::Named(NamedKey::LaunchCalendar), + Music => Key::Named(NamedKey::LaunchMusicPlayer), + Calculator => Key::Named(NamedKey::LaunchApplication2), + ZenkakuHankaku => Key::Named(NamedKey::ZenkakuHankaku), + Eisu => Key::Named(NamedKey::Eisu), + Muhenkan => Key::Named(NamedKey::NonConvert), + Henkan => Key::Named(NamedKey::Convert), + KatakanaHiragana => Key::Named(NamedKey::HiraganaKatakana), + Kana => Key::Named(NamedKey::KanjiMode), + BrightnessDown => Key::Named(NamedKey::BrightnessDown), + BrightnessUp => Key::Named(NamedKey::BrightnessUp), + MediaAudioTrack => Key::Named(NamedKey::MediaAudioTrack), + Sleep => Key::Named(NamedKey::Standby), + Wakeup => Key::Named(NamedKey::WakeUp), + Pairing => Key::Named(NamedKey::Pairing), + MediaTopMenu => Key::Named(NamedKey::MediaTopMenu), + LastChannel => Key::Named(NamedKey::MediaLast), + TvDataService => Key::Named(NamedKey::TVDataService), + VoiceAssist => Key::Named(NamedKey::VoiceDial), + TvRadioService => Key::Named(NamedKey::TVRadioService), + TvTeletext => Key::Named(NamedKey::Teletext), + TvNumberEntry => Key::Named(NamedKey::TVNumberEntry), + TvTerrestrialAnalog => Key::Named(NamedKey::TVTerrestrialAnalog), + TvTerrestrialDigital => Key::Named(NamedKey::TVTerrestrialDigital), + TvSatellite => Key::Named(NamedKey::TVSatellite), + TvSatelliteBs => Key::Named(NamedKey::TVSatelliteBS), + TvSatelliteCs => Key::Named(NamedKey::TVSatelliteCS), + TvSatelliteService => Key::Named(NamedKey::TVSatelliteToggle), + TvNetwork => Key::Named(NamedKey::TVNetwork), + TvAntennaCable => Key::Named(NamedKey::TVAntennaCable), + TvInputHdmi1 => Key::Named(NamedKey::TVInputHDMI1), + TvInputHdmi2 => Key::Named(NamedKey::TVInputHDMI2), + TvInputHdmi3 => Key::Named(NamedKey::TVInputHDMI3), + TvInputHdmi4 => Key::Named(NamedKey::TVInputHDMI4), + TvInputComposite1 => Key::Named(NamedKey::TVInputComposite1), + TvInputComposite2 => Key::Named(NamedKey::TVInputComposite2), + TvInputComponent1 => Key::Named(NamedKey::TVInputComponent1), + TvInputComponent2 => Key::Named(NamedKey::TVInputComponent2), + TvInputVga1 => Key::Named(NamedKey::TVInputVGA1), + TvAudioDescription => Key::Named(NamedKey::TVAudioDescription), + TvAudioDescriptionMixUp => Key::Named(NamedKey::TVAudioDescriptionMixUp), + TvAudioDescriptionMixDown => Key::Named(NamedKey::TVAudioDescriptionMixDown), + TvZoomMode => Key::Named(NamedKey::ZoomToggle), + TvContentsMenu => Key::Named(NamedKey::TVContentsMenu), + TvMediaContextMenu => Key::Named(NamedKey::TVMediaContext), + TvTimerProgramming => Key::Named(NamedKey::TVTimer), + Help => Key::Named(NamedKey::Help), + NavigatePrevious => Key::Named(NamedKey::NavigatePrevious), + NavigateNext => Key::Named(NamedKey::NavigateNext), + NavigateIn => Key::Named(NamedKey::NavigateIn), + NavigateOut => Key::Named(NamedKey::NavigateOut), + MediaSkipForward => Key::Named(NamedKey::MediaSkipForward), + MediaSkipBackward => Key::Named(NamedKey::MediaSkipBackward), + MediaStepForward => Key::Named(NamedKey::MediaStepForward), + MediaStepBackward => Key::Named(NamedKey::MediaStepBackward), + Cut => Key::Named(NamedKey::Cut), + Copy => Key::Named(NamedKey::Copy), + Paste => Key::Named(NamedKey::Paste), + Refresh => Key::Named(NamedKey::BrowserRefresh), + + // ----------------------------------------------------------------- + // Keycodes that don't have a logical Key mapping + // ----------------------------------------------------------------- + Unknown => Key::Unidentified(native), + + // Can be added on demand + SoftLeft => Key::Unidentified(native), + SoftRight => Key::Unidentified(native), + + Menu => Key::Unidentified(native), + + Pictsymbols => Key::Unidentified(native), + SwitchCharset => Key::Unidentified(native), + + // ----------------------------------------------------------------- + // Gamepad events should be exposed through a separate API, not + // keyboard events + ButtonA => Key::Unidentified(native), + ButtonB => Key::Unidentified(native), + ButtonC => Key::Unidentified(native), + ButtonX => Key::Unidentified(native), + ButtonY => Key::Unidentified(native), + ButtonZ => Key::Unidentified(native), + ButtonL1 => Key::Unidentified(native), + ButtonR1 => Key::Unidentified(native), + ButtonL2 => Key::Unidentified(native), + ButtonR2 => Key::Unidentified(native), + ButtonThumbl => Key::Unidentified(native), + ButtonThumbr => Key::Unidentified(native), + ButtonStart => Key::Unidentified(native), + ButtonSelect => Key::Unidentified(native), + ButtonMode => Key::Unidentified(native), + // ----------------------------------------------------------------- + Window => Key::Unidentified(native), + + Button1 => Key::Unidentified(native), + Button2 => Key::Unidentified(native), + Button3 => Key::Unidentified(native), + Button4 => Key::Unidentified(native), + Button5 => Key::Unidentified(native), + Button6 => Key::Unidentified(native), + Button7 => Key::Unidentified(native), + Button8 => Key::Unidentified(native), + Button9 => Key::Unidentified(native), + Button10 => Key::Unidentified(native), + Button11 => Key::Unidentified(native), + Button12 => Key::Unidentified(native), + Button13 => Key::Unidentified(native), + Button14 => Key::Unidentified(native), + Button15 => Key::Unidentified(native), + Button16 => Key::Unidentified(native), + + Yen => Key::Unidentified(native), + Ro => Key::Unidentified(native), + + Assist => Key::Unidentified(native), + + Keycode11 => Key::Unidentified(native), + Keycode12 => Key::Unidentified(native), + + StemPrimary => Key::Unidentified(native), + Stem1 => Key::Unidentified(native), + Stem2 => Key::Unidentified(native), + Stem3 => Key::Unidentified(native), + + DpadUpLeft => Key::Unidentified(native), + DpadDownLeft => Key::Unidentified(native), + DpadUpRight => Key::Unidentified(native), + DpadDownRight => Key::Unidentified(native), + + SoftSleep => Key::Unidentified(native), + + SystemNavigationUp => Key::Unidentified(native), + SystemNavigationDown => Key::Unidentified(native), + SystemNavigationLeft => Key::Unidentified(native), + SystemNavigationRight => Key::Unidentified(native), + + AllApps => Key::Unidentified(native), + ThumbsUp => Key::Unidentified(native), + ThumbsDown => Key::Unidentified(native), + ProfileSwitch => Key::Unidentified(native), + + // It's always possible that new versions of Android could introduce + // key codes we can't know about at compile time. + _ => Key::Unidentified(native), + }, } } diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index e607f62576..e080d0adbe 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -1,11 +1,12 @@ #![cfg(android_platform)] use std::{ + cell::Cell, collections::VecDeque, hash::Hash, sync::{ atomic::{AtomicBool, Ordering}, - mpsc, Arc, RwLock, + mpsc, Arc, Mutex, RwLock, }, time::{Duration, Instant}, }; @@ -15,26 +16,32 @@ use android_activity::{ AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect, }; use once_cell::sync::Lazy; -use raw_window_handle::{ - AndroidDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, -}; -use crate::platform_impl::Fullscreen; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, - event::{self, StartCause}, - event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW}, - keyboard::NativeKey, + event::{self, Force, InnerSizeWriter, StartCause}, + event_loop::{self, ControlFlow, DeviceEvents, EventLoopWindowTarget as RootELW}, + platform::pump_events::PumpStatus, window::{ self, CursorGrabMode, ImePurpose, ResizeDirection, Theme, WindowButtons, WindowLevel, }, }; +use crate::{error::EventLoopError, platform_impl::Fullscreen}; mod keycodes; static HAS_FOCUS: Lazy> = Lazy::new(|| RwLock::new(true)); +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + struct PeekableReceiver { recv: mpsc::Receiver, first: Option, @@ -135,8 +142,12 @@ pub struct EventLoop { redraw_flag: SharedFlag, user_events_sender: mpsc::Sender, user_events_receiver: PeekableReceiver, //must wake looper whenever something gets sent + loop_running: bool, // Dispatched `NewEvents` running: bool, + pending_redraw: bool, + cause: StartCause, ignore_volume_keys: bool, + combining_accent: Option, } #[derive(Debug, Clone, PartialEq)] @@ -154,41 +165,22 @@ impl Default for PlatformSpecificEventLoopAttributes { } } -fn sticky_exit_callback( - evt: event::Event<'_, T>, - target: &RootELW, - control_flow: &mut ControlFlow, - callback: &mut F, -) where - F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), -{ - // make ControlFlow::ExitWithCode sticky by providing a dummy - // control flow reference if it is already ExitWithCode. - if let ControlFlow::ExitWithCode(code) = *control_flow { - callback(evt, target, &mut ControlFlow::ExitWithCode(code)) - } else { - callback(evt, target, control_flow) - } -} - -struct IterationResult { - deadline: Option, - timeout: Option, - wait_start: Instant, -} - impl EventLoop { - pub(crate) fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Self { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { let (user_events_sender, user_events_receiver) = mpsc::channel(); let android_app = attributes.android_app.as_ref().expect("An `AndroidApp` as passed to android_main() is required to create an `EventLoop` on Android"); let redraw_flag = SharedFlag::new(); - Self { + Ok(Self { android_app: android_app.clone(), window_target: event_loop::EventLoopWindowTarget { p: EventLoopWindowTarget { app: android_app.clone(), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), redraw_requester: RedrawRequester::new( &redraw_flag, android_app.create_waker(), @@ -200,80 +192,60 @@ impl EventLoop { redraw_flag, user_events_sender, user_events_receiver: PeekableReceiver::from_recv(user_events_receiver), + loop_running: false, running: false, + pending_redraw: false, + cause: StartCause::Init, ignore_volume_keys: attributes.ignore_volume_keys, - } + combining_accent: None, + }) } - fn single_iteration( - &mut self, - control_flow: &mut ControlFlow, - main_event: Option>, - pending_redraw: &mut bool, - cause: &mut StartCause, - callback: &mut F, - ) -> IterationResult + fn single_iteration(&mut self, main_event: Option>, callback: &mut F) where - F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(event::Event, &RootELW), { trace!("Mainloop iteration"); - sticky_exit_callback( - event::Event::NewEvents(*cause), - self.window_target(), - control_flow, - callback, - ); - + let cause = self.cause; + let mut pending_redraw = self.pending_redraw; let mut resized = false; + callback(event::Event::NewEvents(cause), self.window_target()); + if let Some(event) = main_event { trace!("Handling main event {:?}", event); match event { MainEvent::InitWindow { .. } => { - sticky_exit_callback( - event::Event::Resumed, - self.window_target(), - control_flow, - callback, - ); + callback(event::Event::Resumed, self.window_target()); } MainEvent::TerminateWindow { .. } => { - sticky_exit_callback( - event::Event::Suspended, - self.window_target(), - control_flow, - callback, - ); + callback(event::Event::Suspended, self.window_target()); } MainEvent::WindowResized { .. } => resized = true, - MainEvent::RedrawNeeded { .. } => *pending_redraw = true, + MainEvent::RedrawNeeded { .. } => pending_redraw = true, MainEvent::ContentRectChanged { .. } => { warn!("TODO: find a way to notify application of content rect change"); } MainEvent::GainedFocus => { *HAS_FOCUS.write().unwrap() = true; - sticky_exit_callback( + callback( event::Event::WindowEvent { window_id: window::WindowId(WindowId), event: event::WindowEvent::Focused(true), }, self.window_target(), - control_flow, - callback, ); } MainEvent::LostFocus => { *HAS_FOCUS.write().unwrap() = false; - sticky_exit_callback( + callback( event::Event::WindowEvent { window_id: window::WindowId(WindowId), event: event::WindowEvent::Focused(false), }, self.window_target(), - control_flow, - callback, ); } MainEvent::ConfigChanged { .. } => { @@ -281,22 +253,23 @@ impl EventLoop { let old_scale_factor = monitor.scale_factor(); let scale_factor = monitor.scale_factor(); if (scale_factor - old_scale_factor).abs() < f64::EPSILON { - let mut size = MonitorHandle::new(self.android_app.clone()).size(); + let new_inner_size = Arc::new(Mutex::new( + MonitorHandle::new(self.android_app.clone()).size(), + )); let event = event::Event::WindowEvent { window_id: window::WindowId(WindowId), event: event::WindowEvent::ScaleFactorChanged { - new_inner_size: &mut size, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), scale_factor, }, }; - sticky_exit_callback(event, self.window_target(), control_flow, callback); + callback(event, self.window_target()); } } MainEvent::LowMemory => { - // XXX: how to forward this state to applications? - // It seems like ideally winit should support lifecycle and - // low-memory events, especially for mobile platforms. - warn!("TODO: handle Android LowMemory notification"); + callback(event::Event::MemoryWarning, self.window_target()); } MainEvent::Start => { // XXX: how to forward this state to applications? @@ -336,148 +309,32 @@ impl EventLoop { trace!("No main event to handle"); } - // Process input events - self.android_app.input_events(|event| { - let mut input_status = InputStatus::Handled; - match event { - InputEvent::MotionEvent(motion_event) => { - let window_id = window::WindowId(WindowId); - let device_id = event::DeviceId(DeviceId); + // temporarily decouple `android_app` from `self` so we aren't holding + // a borrow of `self` while iterating + let android_app = self.android_app.clone(); - let phase = match motion_event.action() { - MotionAction::Down | MotionAction::PointerDown => { - Some(event::TouchPhase::Started) - } - MotionAction::Up | MotionAction::PointerUp => { - Some(event::TouchPhase::Ended) - } - MotionAction::Move => Some(event::TouchPhase::Moved), - MotionAction::Cancel => { - Some(event::TouchPhase::Cancelled) - } - _ => { - None // TODO mouse events - } - }; - if let Some(phase) = phase { - let pointers: Box< - dyn Iterator>, - > = match phase { - event::TouchPhase::Started - | event::TouchPhase::Ended => { - Box::new( - std::iter::once(motion_event.pointer_at_index( - motion_event.pointer_index(), - )) - ) - }, - event::TouchPhase::Moved - | event::TouchPhase::Cancelled => { - Box::new(motion_event.pointers()) - } - }; + // Process input events + match android_app.input_events_iter() { + Ok(mut input_iter) => loop { + let read_event = + input_iter.next(|event| self.handle_input_event(&android_app, event, callback)); - for pointer in pointers { - let location = PhysicalPosition { - x: pointer.x() as _, - y: pointer.y() as _, - }; - trace!("Input event {device_id:?}, {phase:?}, loc={location:?}, pointer={pointer:?}"); - let event = event::Event::WindowEvent { - window_id, - event: event::WindowEvent::Touch( - event::Touch { - device_id, - phase, - location, - id: pointer.pointer_id() as u64, - force: None, - }, - ), - }; - sticky_exit_callback( - event, - self.window_target(), - control_flow, - callback - ); - } - } - } - InputEvent::KeyEvent(key) => { - match key.key_code() { - // Flag keys related to volume as unhandled. While winit does not have a way for applications - // to configure what keys to flag as handled, this appears to be a good default until winit - // can be configured. - Keycode::VolumeUp | - Keycode::VolumeDown | - Keycode::VolumeMute => { - if self.ignore_volume_keys { - input_status = InputStatus::Unhandled - } - }, - keycode => { - let state = match key.action() { - KeyAction::Down => event::ElementState::Pressed, - KeyAction::Up => event::ElementState::Released, - _ => event::ElementState::Released, - }; - - let native = NativeKey::Android(keycode.into()); - let logical_key = keycodes::to_logical(keycode, native); - // TODO: maybe use getUnicodeChar to get the logical key - - let event = event::Event::WindowEvent { - window_id: window::WindowId(WindowId), - event: event::WindowEvent::KeyboardInput { - device_id: event::DeviceId(DeviceId), - event: event::KeyEvent { - state, - physical_key: keycodes::to_physical_keycode(keycode), - logical_key, - location: keycodes::to_location(keycode), - repeat: key.repeat_count() > 0, - text: None, - platform_specific: KeyEventExtra {}, - }, - is_synthetic: false, - }, - }; - sticky_exit_callback( - event, - self.window_target(), - control_flow, - callback, - ); - } - } - } - _ => { - warn!("Unknown android_activity input event {event:?}") + if !read_event { + break; } + }, + Err(err) => { + log::warn!("Failed to get input events iterator: {err:?}"); } - input_status - }); + } // Empty the user event buffer { while let Ok(event) = self.user_events_receiver.try_recv() { - sticky_exit_callback( - crate::event::Event::UserEvent(event), - self.window_target(), - control_flow, - callback, - ); + callback(crate::event::Event::UserEvent(event), self.window_target()); } } - sticky_exit_callback( - event::Event::MainEventsCleared, - self.window_target(), - control_flow, - callback, - ); - if self.running { if resized { let size = if let Some(native_window) = self.android_app.native_window().as_ref() { @@ -491,163 +348,275 @@ impl EventLoop { window_id: window::WindowId(WindowId), event: event::WindowEvent::Resized(size), }; - sticky_exit_callback(event, self.window_target(), control_flow, callback); + callback(event, self.window_target()); } - *pending_redraw |= self.redraw_flag.get_and_reset(); - if *pending_redraw { - *pending_redraw = false; - let event = event::Event::RedrawRequested(window::WindowId(WindowId)); - sticky_exit_callback(event, self.window_target(), control_flow, callback); + pending_redraw |= self.redraw_flag.get_and_reset(); + if pending_redraw { + pending_redraw = false; + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::RedrawRequested, + }; + callback(event, self.window_target()); } } - sticky_exit_callback( - event::Event::RedrawEventsCleared, - self.window_target(), - control_flow, - callback, - ); + // This is always the last event we dispatch before poll again + callback(event::Event::AboutToWait, self.window_target()); - let start = Instant::now(); - let (deadline, timeout); + self.pending_redraw = pending_redraw; + } - match control_flow { - ControlFlow::ExitWithCode(_) => { - deadline = None; - timeout = None; - } - ControlFlow::Poll => { - *cause = StartCause::Poll; - deadline = None; - timeout = Some(Duration::from_millis(0)); - } - ControlFlow::Wait => { - *cause = StartCause::WaitCancelled { - start, - requested_resume: None, + fn handle_input_event( + &mut self, + android_app: &AndroidApp, + event: &InputEvent<'_>, + callback: &mut F, + ) -> InputStatus + where + F: FnMut(event::Event, &RootELW), + { + let mut input_status = InputStatus::Handled; + match event { + InputEvent::MotionEvent(motion_event) => { + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId(motion_event.device_id())); + + let phase = match motion_event.action() { + MotionAction::Down | MotionAction::PointerDown => { + Some(event::TouchPhase::Started) + } + MotionAction::Up | MotionAction::PointerUp => Some(event::TouchPhase::Ended), + MotionAction::Move => Some(event::TouchPhase::Moved), + MotionAction::Cancel => Some(event::TouchPhase::Cancelled), + _ => { + None // TODO mouse events + } }; - deadline = None; - timeout = None; + if let Some(phase) = phase { + let pointers: Box>> = + match phase { + event::TouchPhase::Started | event::TouchPhase::Ended => { + Box::new(std::iter::once( + motion_event.pointer_at_index(motion_event.pointer_index()), + )) + } + event::TouchPhase::Moved | event::TouchPhase::Cancelled => { + Box::new(motion_event.pointers()) + } + }; + + for pointer in pointers { + let location = PhysicalPosition { + x: pointer.x() as _, + y: pointer.y() as _, + }; + trace!("Input event {device_id:?}, {phase:?}, loc={location:?}, pointer={pointer:?}"); + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::Touch(event::Touch { + device_id, + phase, + location, + id: pointer.pointer_id() as u64, + force: Some(Force::Normalized(pointer.pressure() as f64)), + }), + }; + callback(event, self.window_target()); + } + } } - ControlFlow::WaitUntil(wait_deadline) => { - *cause = StartCause::ResumeTimeReached { - start, - requested_resume: *wait_deadline, - }; - timeout = if *wait_deadline > start { - Some(*wait_deadline - start) - } else { - Some(Duration::from_millis(0)) - }; - deadline = Some(*wait_deadline); + InputEvent::KeyEvent(key) => { + match key.key_code() { + // Flag keys related to volume as unhandled. While winit does not have a way for applications + // to configure what keys to flag as handled, this appears to be a good default until winit + // can be configured. + Keycode::VolumeUp | Keycode::VolumeDown | Keycode::VolumeMute => { + if self.ignore_volume_keys { + input_status = InputStatus::Unhandled + } + } + keycode => { + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + + let key_char = keycodes::character_map_and_combine_key( + android_app, + key, + &mut self.combining_accent, + ); + + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId(key.device_id())), + event: event::KeyEvent { + state, + physical_key: keycodes::to_physical_key(keycode), + logical_key: keycodes::to_logical(key_char, keycode), + location: keycodes::to_location(keycode), + repeat: key.repeat_count() > 0, + text: None, + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + }, + }; + callback(event, self.window_target()); + } + } + } + _ => { + warn!("Unknown android_activity input event {event:?}") } } - IterationResult { - wait_start: start, - deadline, - timeout, - } + input_status } - pub fn run(mut self, event_handler: F) -> ! + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> where - F: 'static - + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(event::Event, &event_loop::EventLoopWindowTarget), { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); + self.run_on_demand(event_handler) } - pub fn run_return(&mut self, mut callback: F) -> i32 + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> where - F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(event::Event, &event_loop::EventLoopWindowTarget), { - let mut control_flow = ControlFlow::default(); - let mut cause = StartCause::Init; - let mut pending_redraw = false; - - // run the initial loop iteration - let mut iter_result = self.single_iteration( - &mut control_flow, - None, - &mut pending_redraw, - &mut cause, - &mut callback, - ); - - let exit_code = loop { - if let ControlFlow::ExitWithCode(code) = control_flow { - break code; + if self.loop_running { + return Err(EventLoopError::AlreadyRunning); + } + + loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + } + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + } + _ => { + continue; + } } + } + } - let mut timeout = iter_result.timeout; + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(event::Event, &RootELW), + { + if !self.loop_running { + self.loop_running = true; - // If we already have work to do then we don't want to block on the next poll... - pending_redraw |= self.redraw_flag.get_and_reset(); - if self.running && (pending_redraw || self.user_events_receiver.has_incoming()) { - timeout = Some(Duration::from_millis(0)) - } + // Reset the internal state for the loop as we start running to + // ensure consistent behaviour in case the loop runs and exits more + // than once + self.pending_redraw = false; + self.cause = StartCause::Init; - let app = self.android_app.clone(); // Don't borrow self as part of poll expression - app.poll_events(timeout, |poll_event| { - let mut main_event = None; - - match poll_event { - android_activity::PollEvent::Wake => { - // In the X11 backend it's noted that too many false-positive wake ups - // would cause the event loop to run continuously. They handle this by re-checking - // for pending events (assuming they cover all valid reasons for a wake up). - // - // For now, user_events and redraw_requests are the only reasons to expect - // a wake up here so we can ignore the wake up if there are no events/requests. - // We also ignore wake ups while suspended. - pending_redraw |= self.redraw_flag.get_and_reset(); - if !self.running - || (!pending_redraw && !self.user_events_receiver.has_incoming()) - { - return; - } - } - android_activity::PollEvent::Timeout => {} - android_activity::PollEvent::Main(event) => { - main_event = Some(event); + // run the initial loop iteration + self.single_iteration(None, &mut callback); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if self.exiting() { + self.loop_running = false; + + callback(event::Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + } + + fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(event::Event, &RootELW), + { + let start = Instant::now(); + + self.pending_redraw |= self.redraw_flag.get_and_reset(); + + timeout = + if self.running && (self.pending_redraw || self.user_events_receiver.has_incoming()) { + // If we already have work to do then we don't want to block on the next poll + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) } - unknown_event => { - warn!("Unknown poll event {unknown_event:?} (ignored)"); + }; + + min_timeout(control_flow_timeout, timeout) + }; + + let app = self.android_app.clone(); // Don't borrow self as part of poll expression + app.poll_events(timeout, |poll_event| { + let mut main_event = None; + + match poll_event { + android_activity::PollEvent::Wake => { + // In the X11 backend it's noted that too many false-positive wake ups + // would cause the event loop to run continuously. They handle this by re-checking + // for pending events (assuming they cover all valid reasons for a wake up). + // + // For now, user_events and redraw_requests are the only reasons to expect + // a wake up here so we can ignore the wake up if there are no events/requests. + // We also ignore wake ups while suspended. + self.pending_redraw |= self.redraw_flag.get_and_reset(); + if !self.running + || (!self.pending_redraw && !self.user_events_receiver.has_incoming()) + { + return; } } - - let wait_cancelled = iter_result - .deadline - .map_or(false, |deadline| Instant::now() < deadline); - - if wait_cancelled { - cause = StartCause::WaitCancelled { - start: iter_result.wait_start, - requested_resume: iter_result.deadline, - }; + android_activity::PollEvent::Timeout => {} + android_activity::PollEvent::Main(event) => { + main_event = Some(event); } + unknown_event => { + warn!("Unknown poll event {unknown_event:?} (ignored)"); + } + } - iter_result = self.single_iteration( - &mut control_flow, - main_event, - &mut pending_redraw, - &mut cause, - &mut callback, - ); - }); - }; - - sticky_exit_callback( - event::Event::LoopDestroyed, - self.window_target(), - &mut control_flow, - &mut callback, - ); + self.cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + } + } else { + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + } + } + } + }; - exit_code + self.single_iteration(main_event, &mut callback); + }); } pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { @@ -660,6 +629,14 @@ impl EventLoop { waker: self.android_app.create_waker(), } } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } } pub struct EventLoopProxy { @@ -688,6 +665,8 @@ impl EventLoopProxy { pub struct EventLoopWindowTarget { app: AndroidApp, + control_flow: Cell, + exit: Cell, redraw_requester: RedrawRequester, _marker: std::marker::PhantomData, } @@ -703,8 +682,43 @@ impl EventLoopWindowTarget { v } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Android(AndroidDisplayHandle::empty()) + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Android( + rwh_06::AndroidDisplayHandle::new(), + )) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(false) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() } } @@ -730,11 +744,11 @@ impl From for WindowId { } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct DeviceId; +pub struct DeviceId(i32); impl DeviceId { pub const fn dummy() -> Self { - DeviceId + DeviceId(0) } } @@ -760,6 +774,14 @@ impl Window { }) } + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + pub fn id(&self) -> WindowId { WindowId } @@ -786,6 +808,8 @@ impl Window { self.redraw_requester.request_redraw() } + pub fn pre_present_notify(&self) {} + pub fn inner_position(&self) -> Result, error::NotSupportedError> { Err(error::NotSupportedError::new()) } @@ -802,8 +826,8 @@ impl Window { self.outer_size() } - pub fn set_inner_size(&self, _size: Size) { - warn!("Cannot set window size on Android"); + pub fn request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) } pub fn outer_size(&self) -> PhysicalSize { @@ -824,6 +848,8 @@ impl Window { pub fn set_transparent(&self, _transparent: bool) {} + pub fn set_blur(&self, _blur: bool) {} + pub fn set_visible(&self, _visibility: bool) {} pub fn is_visible(&self) -> Option { @@ -913,13 +939,19 @@ impl Window { )) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( error::NotSupportedError::new(), )) } - pub fn raw_window_handle(&self) -> RawWindowHandle { + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + use rwh_04::HasRawWindowHandle; + if let Some(native_window) = self.app.native_window().as_ref() { native_window.raw_window_handle() } else { @@ -927,8 +959,43 @@ impl Window { } } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Android(AndroidDisplayHandle::empty()) + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + use rwh_05::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + 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."); + } + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + // Allow the usage of HasRawWindowHandle inside this function + #[allow(deprecated)] + pub fn raw_window_handle_rwh_06(&self) -> Result { + use rwh_06::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } else { + log::error!("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."); + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Android( + rwh_06::AndroidDisplayHandle::new(), + )) } pub fn config(&self) -> ConfigurationRef { @@ -945,6 +1012,8 @@ impl Window { None } + pub fn set_content_protected(&self, _protected: bool) {} + pub fn has_focus(&self) -> bool { *HAS_FOCUS.read().unwrap() } @@ -973,8 +1042,8 @@ pub struct MonitorHandle { app: AndroidApp, } impl PartialOrd for MonitorHandle { - fn partial_cmp(&self, _other: &Self) -> Option { - Some(std::cmp::Ordering::Equal) + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } impl Ord for MonitorHandle { diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 8743a7f2a3..b486c2f624 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -6,6 +6,7 @@ use std::{ mem, os::raw::c_void, ptr, + sync::{Arc, Mutex}, time::Instant, }; @@ -15,22 +16,21 @@ use core_foundation::runloop::{ kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, }; -use objc2::foundation::{CGRect, CGSize, NSInteger, NSProcessInfo}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; +use icrate::Foundation::{ + CGRect, CGSize, MainThreadMarker, NSInteger, NSOperatingSystemVersion, NSProcessInfo, +}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; use objc2::{msg_send, sel}; use once_cell::sync::Lazy; +use super::event_loop::{EventHandler, Never}; use super::uikit::UIView; use super::view::WinitUIWindow; use crate::{ - dpi::LogicalSize, - event::{Event, StartCause, WindowEvent}, + dpi::PhysicalSize, + event::{Event, InnerSizeWriter, StartCause, WindowEvent}, event_loop::ControlFlow, - platform_impl::platform::{ - event_loop::{EventHandler, EventProxy, EventWrapper, Never}, - ffi::NSOperatingSystemVersion, - }, window::WindowId as RootWindowId, }; @@ -46,6 +46,19 @@ macro_rules! bug_assert { }; } +#[derive(Debug)] +pub enum EventWrapper { + StaticEvent(Event), + ScaleFactorChanged(ScaleFactorChanged), +} + +#[derive(Debug)] +pub struct ScaleFactorChanged { + pub(super) window: Id, + pub(super) suggested_size: PhysicalSize, + pub(super) scale_factor: f64, +} + enum UserCallbackTransitionResult<'a> { Success { event_handler: Box, @@ -57,9 +70,15 @@ enum UserCallbackTransitionResult<'a> { }, } -impl Event<'static, Never> { +impl Event { fn is_redraw(&self) -> bool { - matches!(self, Event::RedrawRequested(_)) + matches!( + self, + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } + ) } } @@ -68,25 +87,25 @@ impl Event<'static, Never> { #[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] enum AppStateImpl { NotLaunched { - queued_windows: Vec>, + queued_windows: Vec>, queued_events: Vec, - queued_gpu_redraws: HashSet>, + queued_gpu_redraws: HashSet>, }, Launching { - queued_windows: Vec>, + queued_windows: Vec>, queued_events: Vec, queued_event_handler: Box, - queued_gpu_redraws: HashSet>, + queued_gpu_redraws: HashSet>, }, ProcessingEvents { event_handler: Box, - queued_gpu_redraws: HashSet>, + queued_gpu_redraws: HashSet>, active_control_flow: ControlFlow, }, // special state to deal with reentrancy and prevent mutable aliasing. InUserCallback { queued_events: Vec, - queued_gpu_redraws: HashSet>, + queued_gpu_redraws: HashSet>, }, ProcessingRedraws { event_handler: Box, @@ -102,7 +121,7 @@ enum AppStateImpl { Terminated, } -struct AppState { +pub(crate) struct AppState { // This should never be `None`, except for briefly during a state transition. app_state: Option, control_flow: ControlFlow, @@ -110,24 +129,18 @@ struct AppState { } impl AppState { - // requires main thread - unsafe fn get_mut() -> RefMut<'static, AppState> { + pub(crate) fn get_mut(_mtm: MainThreadMarker) -> RefMut<'static, AppState> { // basically everything in UIKit requires the main thread, so it's pointless to use the // std::sync APIs. // must be mut because plain `static` requires `Sync` static mut APP_STATE: RefCell> = RefCell::new(None); - if cfg!(debug_assertions) { - assert_main_thread!( - "bug in winit: `AppState::get_mut()` can only be called on the main thread" - ); - } - let mut guard = APP_STATE.borrow_mut(); + let mut guard = unsafe { APP_STATE.borrow_mut() }; if guard.is_none() { #[inline(never)] #[cold] - unsafe fn init_guard(guard: &mut RefMut<'static, Option>) { - let waker = EventLoopWaker::new(CFRunLoopGetMain()); + fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(unsafe { CFRunLoopGetMain() }); **guard = Some(AppState { app_state: Some(AppStateImpl::NotLaunched { queued_windows: Vec::new(), @@ -138,7 +151,7 @@ impl AppState { waker, }); } - init_guard(&mut guard) + init_guard(&mut guard); } RefMut::map(guard, |state| state.as_mut().unwrap()) } @@ -187,6 +200,10 @@ impl AppState { ) } + fn has_terminated(&self) -> bool { + matches!(self.state(), AppStateImpl::Terminated) + } + fn will_launch_transition(&mut self, queued_event_handler: Box) { let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() { AppStateImpl::NotLaunched { @@ -204,9 +221,7 @@ impl AppState { }); } - fn did_finish_launching_transition( - &mut self, - ) -> (Vec>, Vec) { + fn did_finish_launching_transition(&mut self) -> (Vec>, Vec) { let (windows, events, event_handler, queued_gpu_redraws) = match self.take_state() { AppStateImpl::Launching { queued_windows, @@ -223,7 +238,7 @@ impl AppState { }; self.set_state(AppStateImpl::ProcessingEvents { event_handler, - active_control_flow: ControlFlow::Poll, + active_control_flow: self.control_flow, queued_gpu_redraws, }); (windows, events) @@ -232,7 +247,7 @@ impl AppState { fn wakeup_transition(&mut self) -> Option { // before `AppState::did_finish_launching` is called, pretend there is no running // event loop. - if !self.has_launched() { + if !self.has_launched() || self.has_terminated() { return None; } @@ -279,7 +294,6 @@ impl AppState { }; (waiting_event_handler, event) } - (ControlFlow::ExitWithCode(_), _) => bug!("unexpected `ControlFlow` `Exit`"), s => bug!("`EventHandler` unexpectedly woke up {:?}", s), }; @@ -363,7 +377,7 @@ impl AppState { } } - fn main_events_cleared_transition(&mut self) -> HashSet> { + fn main_events_cleared_transition(&mut self) -> HashSet> { let (event_handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { AppStateImpl::ProcessingEvents { event_handler, @@ -380,7 +394,7 @@ impl AppState { } fn events_cleared_transition(&mut self) { - if !self.has_launched() { + if !self.has_launched() || self.has_terminated() { return; } let (waiting_event_handler, old) = match self.take_state() { @@ -393,9 +407,6 @@ impl AppState { let new = self.control_flow; match (old, new) { - (ControlFlow::Poll, ControlFlow::Poll) => self.set_state(AppStateImpl::PollFinished { - waiting_event_handler, - }), (ControlFlow::Wait, ControlFlow::Wait) => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { @@ -428,36 +439,34 @@ impl AppState { }); self.waker.start_at(new_instant) } + // Unlike on macOS, handle Poll to Poll transition here to call the waker (_, ControlFlow::Poll) => { self.set_state(AppStateImpl::PollFinished { waiting_event_handler, }); self.waker.start() } - (_, ControlFlow::ExitWithCode(_)) => { - // https://developer.apple.com/library/archive/qa/qa1561/_index.html - // it is not possible to quit an iOS app gracefully and programatically - warn!("`ControlFlow::Exit` ignored on iOS"); - self.control_flow = old - } } } fn terminated_transition(&mut self) -> Box { match self.replace_state(AppStateImpl::Terminated) { AppStateImpl::ProcessingEvents { event_handler, .. } => event_handler, - s => bug!( - "`LoopDestroyed` happened while not processing events {:?}", - s - ), + s => bug!("`LoopExiting` happened while not processing events {:?}", s), } } + + pub(crate) fn set_control_flow(&mut self, control_flow: ControlFlow) { + self.control_flow = control_flow; + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow + } } -// requires main thread and window is a UIWindow -// retains window -pub(crate) unsafe fn set_key_window(window: &Id) { - let mut this = AppState::get_mut(); +pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Id) { + let mut this = AppState::get_mut(mtm); match this.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_windows, @@ -477,10 +486,8 @@ pub(crate) unsafe fn set_key_window(window: &Id) { window.makeKeyAndVisible(); } -// requires main thread and window is a UIWindow -// retains window -pub(crate) unsafe fn queue_gl_or_metal_redraw(window: Id) { - let mut this = AppState::get_mut(); +pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Id) { + let mut this = AppState::get_mut(mtm); match this.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_gpu_redraws, @@ -509,24 +516,17 @@ pub(crate) unsafe fn queue_gl_or_metal_redraw(window: Id) } } -// requires main thread -pub unsafe fn will_launch(queued_event_handler: Box) { - AppState::get_mut().will_launch_transition(queued_event_handler) +pub fn will_launch(mtm: MainThreadMarker, queued_event_handler: Box) { + AppState::get_mut(mtm).will_launch_transition(queued_event_handler) } -// requires main thread -pub unsafe fn did_finish_launching() { - let mut this = AppState::get_mut(); +pub fn did_finish_launching(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); let windows = match this.state_mut() { AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), s => bug!("unexpected state {:?}", s), }; - // start waking up the event loop now! - bug_assert!( - this.control_flow == ControlFlow::Poll, - "unexpectedly not setup to `Poll` on launch!" - ); this.waker.start(); // have to drop RefMut because the window setup code below can trigger new events @@ -544,7 +544,7 @@ pub unsafe fn did_finish_launching() { // completed. This may result in incorrect visual appearance. // ``` let screen = window.screen(); - let _: () = msg_send![&window, setScreen: ptr::null::()]; + let _: () = unsafe { msg_send![&window, setScreen: ptr::null::()] }; window.setScreen(&screen); let controller = window.rootViewController(); @@ -554,13 +554,13 @@ pub unsafe fn did_finish_launching() { window.makeKeyAndVisible(); } - let (windows, events) = AppState::get_mut().did_finish_launching_transition(); + let (windows, events) = AppState::get_mut(mtm).did_finish_launching_transition(); let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents( StartCause::Init, ))) .chain(events); - handle_nonuser_events(events); + handle_nonuser_events(mtm, events); // the above window dance hack, could possibly trigger new windows to be created. // we can just set those windows up normally, as they were created after didFinishLaunching @@ -569,27 +569,31 @@ pub unsafe fn did_finish_launching() { } } -// requires main thread // AppState::did_finish_launching handles the special transition `Init` -pub unsafe fn handle_wakeup_transition() { - let mut this = AppState::get_mut(); +pub fn handle_wakeup_transition(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); let wakeup_event = match this.wakeup_transition() { None => return, Some(wakeup_event) => wakeup_event, }; drop(this); - handle_nonuser_event(wakeup_event) + handle_nonuser_event(mtm, wakeup_event) } -// requires main thread -pub(crate) unsafe fn handle_nonuser_event(event: EventWrapper) { - handle_nonuser_events(std::iter::once(event)) +pub(crate) fn handle_nonuser_event(mtm: MainThreadMarker, event: EventWrapper) { + handle_nonuser_events(mtm, std::iter::once(event)) } -// requires main thread -pub(crate) unsafe fn handle_nonuser_events>(events: I) { - let mut this = AppState::get_mut(); +pub(crate) fn handle_nonuser_events>( + mtm: MainThreadMarker, + events: I, +) { + let mut this = AppState::get_mut(mtm); + if this.has_terminated() { + return; + } + let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { @@ -602,7 +606,6 @@ pub(crate) unsafe fn handle_nonuser_events> processing_redraws, } => (event_handler, active_control_flow, processing_redraws), }; - let mut control_flow = this.control_flow; drop(this); for wrapper in events { @@ -616,16 +619,16 @@ pub(crate) unsafe fn handle_nonuser_events> event ); } - event_handler.handle_nonuser_event(event, &mut control_flow) + event_handler.handle_nonuser_event(event) } - EventWrapper::EventProxy(proxy) => { - handle_event_proxy(&mut event_handler, control_flow, proxy) + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut event_handler, event) } } } loop { - let mut this = AppState::get_mut(); + let mut this = AppState::get_mut(mtm); let queued_events = match this.state_mut() { &mut AppStateImpl::InUserCallback { ref mut queued_events, @@ -657,7 +660,6 @@ pub(crate) unsafe fn handle_nonuser_events> active_control_flow, } }); - this.control_flow = control_flow; break; } drop(this); @@ -673,20 +675,18 @@ pub(crate) unsafe fn handle_nonuser_events> event ); } - event_handler.handle_nonuser_event(event, &mut control_flow) + event_handler.handle_nonuser_event(event) } - EventWrapper::EventProxy(proxy) => { - handle_event_proxy(&mut event_handler, control_flow, proxy) + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut event_handler, event) } } } } } -// requires main thread -unsafe fn handle_user_events() { - let mut this = AppState::get_mut(); - let mut control_flow = this.control_flow; +fn handle_user_events(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { UserCallbackTransitionResult::ReentrancyPrevented { .. } => { @@ -703,10 +703,10 @@ unsafe fn handle_user_events() { } drop(this); - event_handler.handle_user_events(&mut control_flow); + event_handler.handle_user_events(); loop { - let mut this = AppState::get_mut(); + let mut this = AppState::get_mut(mtm); let queued_events = match this.state_mut() { &mut AppStateImpl::InUserCallback { ref mut queued_events, @@ -727,29 +727,25 @@ unsafe fn handle_user_events() { queued_gpu_redraws, active_control_flow, }); - this.control_flow = control_flow; break; } drop(this); for wrapper in queued_events { match wrapper { - EventWrapper::StaticEvent(event) => { - event_handler.handle_nonuser_event(event, &mut control_flow) - } - EventWrapper::EventProxy(proxy) => { - handle_event_proxy(&mut event_handler, control_flow, proxy) + EventWrapper::StaticEvent(event) => event_handler.handle_nonuser_event(event), + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut event_handler, event) } } } - event_handler.handle_user_events(&mut control_flow); + event_handler.handle_user_events(); } } -// requires main thread -pub unsafe fn handle_main_events_cleared() { - let mut this = AppState::get_mut(); - if !this.has_launched() { +pub fn handle_main_events_cleared(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + if !this.has_launched() || this.has_terminated() { return; } match this.state_mut() { @@ -758,84 +754,62 @@ pub unsafe fn handle_main_events_cleared() { }; drop(this); - // User events are always sent out at the end of the "MainEventLoop" - handle_user_events(); - handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); + handle_user_events(mtm); - let mut this = AppState::get_mut(); - let mut redraw_events: Vec = this + let mut this = AppState::get_mut(mtm); + let redraw_events: Vec = this .main_events_cleared_transition() .into_iter() - .map(|window| EventWrapper::StaticEvent(Event::RedrawRequested(RootWindowId(window.id())))) + .map(|window| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }) + }) .collect(); - - redraw_events.push(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); drop(this); - handle_nonuser_events(redraw_events); + handle_nonuser_events(mtm, redraw_events); + handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::AboutToWait)); } -// requires main thread -pub unsafe fn handle_events_cleared() { - AppState::get_mut().events_cleared_transition(); +pub fn handle_events_cleared(mtm: MainThreadMarker) { + AppState::get_mut(mtm).events_cleared_transition(); } -// requires main thread -pub unsafe fn terminated() { - let mut this = AppState::get_mut(); +pub fn terminated(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); let mut event_handler = this.terminated_transition(); - let mut control_flow = this.control_flow; drop(this); - event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) + event_handler.handle_nonuser_event(Event::LoopExiting) } -fn handle_event_proxy( - event_handler: &mut Box, - control_flow: ControlFlow, - proxy: EventProxy, -) { - match proxy { - EventProxy::DpiChangedProxy { - suggested_size, - scale_factor, - window, - } => handle_hidpi_proxy( - event_handler, - control_flow, - suggested_size, - scale_factor, - window, - ), - } -} - -fn handle_hidpi_proxy( - event_handler: &mut Box, - mut control_flow: ControlFlow, - suggested_size: LogicalSize, - scale_factor: f64, - window: Id, -) { - let mut size = suggested_size.to_physical(scale_factor); - let new_inner_size = &mut size; +fn handle_hidpi_proxy(event_handler: &mut Box, event: ScaleFactorChanged) { + let ScaleFactorChanged { + suggested_size, + scale_factor, + window, + } = event; + let new_inner_size = Arc::new(Mutex::new(suggested_size)); let event = Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::ScaleFactorChanged { scale_factor, - new_inner_size, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), }, }; - event_handler.handle_nonuser_event(event, &mut control_flow); + event_handler.handle_nonuser_event(event); let (view, screen_frame) = get_view_and_screen_frame(&window); - let physical_size = *new_inner_size; + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); let logical_size = physical_size.to_logical(scale_factor); let size = CGSize::new(logical_size.width, logical_size.height); let new_frame: CGRect = CGRect::new(screen_frame.origin, size); view.setFrame(new_frame); } -fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Id, CGRect) { +fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Id, CGRect) { let view_controller = window.rootViewController().unwrap(); let view = view_controller.view().unwrap(); let bounds = window.bounds(); @@ -924,7 +898,7 @@ macro_rules! os_capabilities { impl From for OSCapabilities { fn from(os_version: NSOperatingSystemVersion) -> OSCapabilities { - $(let $name = os_version.meets_requirements($major, $minor);)* + $(let $name = meets_requirements(os_version, $major, $minor);)* OSCapabilities { $($name,)* os_version, } } } @@ -934,7 +908,7 @@ macro_rules! os_capabilities { pub fn $error_name(&self, extra_msg: &str) { log::warn!( concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), - $major, $minor, self.os_version.major, self.os_version.minor, self.os_version.patch, + $major, $minor, self.os_version.majorVersion, self.os_version.minorVersion, self.os_version.patchVersion, extra_msg ) } @@ -962,16 +936,18 @@ os_capabilities! { force_touch: 9-0, } -impl NSOperatingSystemVersion { - fn meets_requirements(&self, required_major: NSInteger, required_minor: NSInteger) -> bool { - (self.major, self.minor) >= (required_major, required_minor) - } +fn meets_requirements( + version: NSOperatingSystemVersion, + required_major: NSInteger, + required_minor: NSInteger, +) -> bool { + (version.majorVersion, version.minorVersion) >= (required_major, required_minor) } pub fn os_capabilities() -> OSCapabilities { static OS_CAPABILITIES: Lazy = Lazy::new(|| { let version: NSOperatingSystemVersion = unsafe { - let process_info = NSProcessInfo::process_info(); + let process_info = NSProcessInfo::processInfo(); let atleast_ios_8: bool = msg_send![ &process_info, respondsToSelector: sel!(operatingSystemVersion) @@ -984,7 +960,7 @@ pub fn os_capabilities() -> OSCapabilities { // // The minimum required iOS version is likely to grow in the future. assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater"); - msg_send![&process_info, operatingSystemVersion] + process_info.operatingSystemVersion() }; version.into() }); diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index 8649c8c7a8..a260f78aec 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -14,60 +14,82 @@ use core_foundation::runloop::{ CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, }; -use objc2::foundation::{MainThreadMarker, NSString}; -use objc2::rc::{Id, Shared}; +use icrate::Foundation::{MainThreadMarker, NSString}; use objc2::ClassType; -use raw_window_handle::{RawDisplayHandle, UiKitDisplayHandle}; -use super::uikit::{UIApplication, UIApplicationMain, UIDevice, UIScreen}; -use super::view::WinitUIWindow; -use super::{app_state, monitor, view, MonitorHandle}; use crate::{ - dpi::LogicalSize, + error::EventLoopError, event::Event, event_loop::{ - ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootEventLoopWindowTarget, + ControlFlow, DeviceEvents, EventLoopClosed, + EventLoopWindowTarget as RootEventLoopWindowTarget, }, platform::ios::Idiom, }; -#[derive(Debug)] -pub(crate) enum EventWrapper { - StaticEvent(Event<'static, Never>), - EventProxy(EventProxy), -} - -#[derive(Debug, PartialEq)] -pub(crate) enum EventProxy { - DpiChangedProxy { - window: Id, - suggested_size: LogicalSize, - scale_factor: f64, - }, -} +use super::{app_state, monitor, view, MonitorHandle}; +use super::{ + app_state::AppState, + uikit::{UIApplication, UIApplicationMain, UIDevice, UIScreen}, +}; +#[derive(Debug)] pub struct EventLoopWindowTarget { - receiver: Receiver, - sender_to_clone: Sender, + pub(super) mtm: MainThreadMarker, + p: PhantomData, } impl EventLoopWindowTarget { pub fn available_monitors(&self) -> VecDeque { - monitor::uiscreens(MainThreadMarker::new().unwrap()) + monitor::uiscreens(self.mtm) } pub fn primary_monitor(&self) -> Option { - Some(MonitorHandle::new(UIScreen::main( - MainThreadMarker::new().unwrap(), - ))) + Some(MonitorHandle::new(UIScreen::main(self.mtm))) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + AppState::get_mut(self.mtm).set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + AppState::get_mut(self.mtm).control_flow() + } + + pub(crate) fn exit(&self) { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programatically + warn!("`ControlFlow::Exit` ignored on iOS"); } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::UiKit(UiKitDisplayHandle::empty()) + pub(crate) fn exiting(&self) -> bool { + false } } pub struct EventLoop { + mtm: MainThreadMarker, + sender: Sender, + receiver: Receiver, window_target: RootEventLoopWindowTarget, } @@ -75,8 +97,11 @@ pub struct EventLoop { pub(crate) struct PlatformSpecificEventLoopAttributes {} impl EventLoop { - pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> EventLoop { - assert_main_thread!("`EventLoop` can only be created on the main thread on iOS"); + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result, EventLoopError> { + let mtm = MainThreadMarker::new() + .expect("On iOS, `EventLoop` must be created on the main thread"); static mut SINGLETON_INIT: bool = false; unsafe { @@ -88,38 +113,50 @@ impl EventLoop { SINGLETON_INIT = true; } - let (sender_to_clone, receiver) = mpsc::channel(); + let (sender, receiver) = mpsc::channel(); // this line sets up the main run loop before `UIApplicationMain` setup_control_flow_observers(); - EventLoop { + Ok(EventLoop { + mtm, + sender, + receiver, window_target: RootEventLoopWindowTarget { p: EventLoopWindowTarget { - receiver, - sender_to_clone, + mtm, + p: PhantomData, }, _marker: PhantomData, }, - } + }) } pub fn run(self, event_handler: F) -> ! where - F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + F: FnMut(Event, &RootEventLoopWindowTarget), { unsafe { - let application = UIApplication::shared(MainThreadMarker::new().unwrap()); + let application = UIApplication::shared(self.mtm); assert!( application.is_none(), "\ `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ Note: `EventLoop::run` calls `UIApplicationMain` on iOS", ); - app_state::will_launch(Box::new(EventLoopHandler { + + let event_handler = std::mem::transmute::< + Box, &RootEventLoopWindowTarget)>, + Box>, + >(Box::new(event_handler)); + + let handler = EventLoopHandler { f: event_handler, + receiver: self.receiver, event_loop: self.window_target, - })); + }; + + app_state::will_launch(self.mtm, Box::new(handler)); // Ensure application delegate is initialized view::WinitApplicationDelegate::class(); @@ -135,7 +172,7 @@ impl EventLoop { } pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy::new(self.window_target.p.sender_to_clone.clone()) + EventLoopProxy::new(self.sender.clone()) } pub fn window_target(&self) -> &RootEventLoopWindowTarget { @@ -146,9 +183,7 @@ impl EventLoop { // EventLoopExtIOS impl EventLoop { pub fn idiom(&self) -> Idiom { - UIDevice::current(MainThreadMarker::new().unwrap()) - .userInterfaceIdiom() - .into() + UIDevice::current(self.mtm).userInterfaceIdiom().into() } } @@ -226,21 +261,20 @@ fn setup_control_flow_observers() { activity: CFRunLoopActivity, _: *mut c_void, ) { - unsafe { - #[allow(non_upper_case_globals)] - match activity { - kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(), - _ => unreachable!(), - } + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(mtm), + _ => unreachable!(), } } // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end - // priority to be 0, in order to send MainEventsCleared before RedrawRequested. This value was + // priority to be 0, in order to send AboutToWait before RedrawRequested. This value was // chosen conservatively to guard against apple using different priorities for their redraw // observers in different OS's or on different devices. If it so happens that it's too - // conservative, the main symptom would be non-redraw events coming in after `MainEventsCleared`. + // conservative, the main symptom would be non-redraw events coming in after `AboutToWait`. // // The value of `0x1e8480` was determined by inspecting stack traces and the associated // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. @@ -251,13 +285,12 @@ fn setup_control_flow_observers() { activity: CFRunLoopActivity, _: *mut c_void, ) { - unsafe { - #[allow(non_upper_case_globals)] - match activity { - kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(), - kCFRunLoopExit => unimplemented!(), // not expected to ever happen - _ => unreachable!(), - } + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(mtm), + kCFRunLoopExit => {} // may happen when running on macOS + _ => unreachable!(), } } @@ -267,13 +300,12 @@ fn setup_control_flow_observers() { activity: CFRunLoopActivity, _: *mut c_void, ) { - unsafe { - #[allow(non_upper_case_globals)] - match activity { - kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(), - kCFRunLoopExit => unimplemented!(), // not expected to ever happen - _ => unreachable!(), - } + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(mtm), + kCFRunLoopExit => {} // may happen when running on macOS + _ => unreachable!(), } } @@ -314,17 +346,20 @@ fn setup_control_flow_observers() { #[derive(Debug)] pub enum Never {} +type EventHandlerCallback = dyn FnMut(Event, &RootEventLoopWindowTarget) + 'static; + pub trait EventHandler: Debug { - fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow); - fn handle_user_events(&mut self, control_flow: &mut ControlFlow); + fn handle_nonuser_event(&mut self, event: Event); + fn handle_user_events(&mut self); } -struct EventLoopHandler { - f: F, +struct EventLoopHandler { + f: Box>, + receiver: Receiver, event_loop: RootEventLoopWindowTarget, } -impl Debug for EventLoopHandler { +impl Debug for EventLoopHandler { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("EventLoopHandler") .field("event_loop", &self.event_loop) @@ -332,22 +367,14 @@ impl Debug for EventLoopHandler { } } -impl EventHandler for EventLoopHandler -where - F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), - T: 'static, -{ - fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { - (self.f)( - event.map_nonuser_event().unwrap(), - &self.event_loop, - control_flow, - ); +impl EventHandler for EventLoopHandler { + fn handle_nonuser_event(&mut self, event: Event) { + (self.f)(event.map_nonuser_event().unwrap(), &self.event_loop); } - fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { - for event in self.event_loop.p.receiver.try_iter() { - (self.f)(Event::UserEvent(event), &self.event_loop, control_flow); + fn handle_user_events(&mut self) { + for event in self.receiver.try_iter() { + (self.f)(Event::UserEvent(event), &self.event_loop); } } } diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 746a5d9995..8d2b6f26ae 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -1,31 +1,10 @@ #![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] -use std::convert::TryInto; - +use icrate::Foundation::{NSInteger, NSUInteger}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{NSInteger, NSUInteger}; use crate::platform::ios::{Idiom, ScreenEdge}; -#[repr(C)] -#[derive(Clone, Debug)] -pub struct NSOperatingSystemVersion { - pub major: NSInteger, - pub minor: NSInteger, - pub patch: NSInteger, -} - -unsafe impl Encode for NSOperatingSystemVersion { - const ENCODING: Encoding = Encoding::Struct( - "NSOperatingSystemVersion", - &[ - NSInteger::ENCODING, - NSInteger::ENCODING, - NSInteger::ENCODING, - ], - ); -} - #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct UIUserInterfaceIdiom(NSInteger); @@ -70,6 +49,10 @@ impl From for Idiom { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct UIRectEdge(NSUInteger); +impl UIRectEdge { + pub(crate) const NONE: Self = Self(0); +} + unsafe impl Encode for UIRectEdge { const ENCODING: Encoding = NSUInteger::ENCODING; } diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index d94681547d..dec71cdeff 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -48,27 +48,16 @@ //! //! - applicationDidBecomeActive is Resumed //! - applicationWillResignActive is Suspended -//! - applicationWillTerminate is LoopDestroyed +//! - applicationWillTerminate is LoopExiting //! -//! Keep in mind that after LoopDestroyed event is received every attempt to draw with +//! Keep in mind that after LoopExiting event is received every attempt to draw with //! opengl will result in segfault. //! -//! Also note that app may not receive the LoopDestroyed event if suspended; it might be SIGKILL'ed. +//! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed. #![cfg(ios_platform)] #![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 -// window size/position. -macro_rules! assert_main_thread { - ($($t:tt)*) => { - if !::objc2::foundation::is_main_thread() { - panic!($($t)*); - } - }; -} - mod app_state; mod event_loop; mod ffi; @@ -89,7 +78,7 @@ pub(crate) use self::{ use self::uikit::UIScreen; pub(crate) use crate::icon::NoIcon as PlatformIcon; -pub(self) use crate::platform_impl::Fullscreen; +pub(crate) use crate::platform_impl::Fullscreen; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId { diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index f5273ebdab..90ed85047d 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -2,12 +2,12 @@ use std::{ collections::{BTreeSet, VecDeque}, - fmt, - ops::{Deref, DerefMut}, + fmt, hash, ptr, }; -use objc2::foundation::{MainThreadMarker, NSInteger}; -use objc2::rc::{Id, Shared}; +use icrate::Foundation::{MainThreadBound, MainThreadMarker, NSInteger}; +use objc2::mutability::IsRetainable; +use objc2::rc::Id; use super::uikit::{UIScreen, UIScreenMode}; use crate::{ @@ -16,32 +16,59 @@ use crate::{ platform_impl::platform::app_state, }; -// TODO(madsmtm): Remove or refactor this -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub(crate) struct ScreenModeSendSync(pub(crate) Id); +// Workaround for `MainThreadBound` implementing almost no traits +#[derive(Debug)] +struct MainThreadBoundDelegateImpls(MainThreadBound>); -unsafe impl Send for ScreenModeSendSync {} -unsafe impl Sync for ScreenModeSendSync {} +impl Clone for MainThreadBoundDelegateImpls { + fn clone(&self) -> Self { + Self( + self.0 + .get_on_main(|inner, mtm| MainThreadBound::new(Id::clone(inner), mtm)), + ) + } +} + +impl hash::Hash for MainThreadBoundDelegateImpls { + fn hash(&self, state: &mut H) { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Id::as_ptr(self.0.get(mtm)).hash(state); + } +} + +impl PartialEq for MainThreadBoundDelegateImpls { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Id::as_ptr(self.0.get(mtm)) == Id::as_ptr(other.0.get(mtm)) + } +} + +impl Eq for MainThreadBoundDelegateImpls {} #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, pub(crate) refresh_rate_millihertz: u32, - pub(crate) screen_mode: ScreenModeSendSync, + screen_mode: MainThreadBoundDelegateImpls, pub(crate) monitor: MonitorHandle, } impl VideoMode { - fn new(uiscreen: Id, screen_mode: Id) -> VideoMode { - assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); + fn new( + uiscreen: Id, + screen_mode: Id, + mtm: MainThreadMarker, + ) -> VideoMode { let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); let size = screen_mode.size(); VideoMode { size: (size.width as u32, size.height as u32), bit_depth: 32, refresh_rate_millihertz, - screen_mode: ScreenModeSendSync(screen_mode), + screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)), monitor: MonitorHandle::new(uiscreen), } } @@ -61,53 +88,50 @@ impl VideoMode { pub fn monitor(&self) -> MonitorHandle { self.monitor.clone() } -} -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct Inner { - uiscreen: Id, + pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Id { + self.screen_mode.0.get(mtm) + } } -#[derive(Clone, PartialEq, Eq, Hash)] pub struct MonitorHandle { - inner: Inner, + ui_screen: MainThreadBound>, } -impl PartialOrd for MonitorHandle { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) +impl Clone for MonitorHandle { + fn clone(&self) -> Self { + Self { + ui_screen: self + .ui_screen + .get_on_main(|inner, mtm| MainThreadBound::new(inner.clone(), mtm)), + } } } -impl Ord for MonitorHandle { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // TODO: Make a better ordering - (self as *const Self).cmp(&(other as *const Self)) +impl hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + (self as *const Self).hash(state); } } -impl Deref for MonitorHandle { - type Target = Inner; - - fn deref(&self) -> &Inner { - assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); - &self.inner +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + ptr::eq(self, other) } } -impl DerefMut for MonitorHandle { - fn deref_mut(&mut self) -> &mut Inner { - assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); - &mut self.inner +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } -unsafe impl Send for MonitorHandle {} -unsafe impl Sync for MonitorHandle {} - -impl Drop for MonitorHandle { - fn drop(&mut self) { - assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS"); +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // TODO: Make a better ordering + (self as *const Self).cmp(&(other as *const Self)) } } @@ -135,64 +159,80 @@ impl fmt::Debug for MonitorHandle { } impl MonitorHandle { - pub(crate) fn new(uiscreen: Id) -> Self { - assert_main_thread!("`MonitorHandle` can only be created on the main thread on iOS"); + pub(crate) fn new(ui_screen: Id) -> Self { + // Holding `Id` implies we're on the main thread. + let mtm = MainThreadMarker::new().unwrap(); Self { - inner: Inner { uiscreen }, + ui_screen: MainThreadBound::new(ui_screen, mtm), } } -} -impl Inner { pub fn name(&self) -> Option { - let main = UIScreen::main(MainThreadMarker::new().unwrap()); - if self.uiscreen == main { - Some("Primary".to_string()) - } else if self.uiscreen == main.mirroredScreen() { - Some("Mirrored".to_string()) - } else { - UIScreen::screens(MainThreadMarker::new().unwrap()) - .iter() - .position(|rhs| rhs == &*self.uiscreen) - .map(|idx| idx.to_string()) - } + self.ui_screen.get_on_main(|ui_screen, mtm| { + let main = UIScreen::main(mtm); + if *ui_screen == main { + Some("Primary".to_string()) + } else if *ui_screen == main.mirroredScreen() { + Some("Mirrored".to_string()) + } else { + UIScreen::screens(mtm) + .iter() + .position(|rhs| rhs == &**ui_screen) + .map(|idx| idx.to_string()) + } + }) } pub fn size(&self) -> PhysicalSize { - let bounds = self.uiscreen.nativeBounds(); + let bounds = self + .ui_screen + .get_on_main(|ui_screen, _| ui_screen.nativeBounds()); PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) } pub fn position(&self) -> PhysicalPosition { - let bounds = self.uiscreen.nativeBounds(); + let bounds = self + .ui_screen + .get_on_main(|ui_screen, _| ui_screen.nativeBounds()); (bounds.origin.x as f64, bounds.origin.y as f64).into() } pub fn scale_factor(&self) -> f64 { - self.uiscreen.nativeScale() as f64 + self.ui_screen + .get_on_main(|ui_screen, _| ui_screen.nativeScale()) as f64 } pub fn refresh_rate_millihertz(&self) -> Option { - Some(refresh_rate_millihertz(&self.uiscreen)) + Some( + self.ui_screen + .get_on_main(|ui_screen, _| refresh_rate_millihertz(ui_screen)), + ) } pub fn video_modes(&self) -> impl Iterator { - // Use Ord impl of RootVideoMode - let modes: BTreeSet<_> = self - .uiscreen - .availableModes() - .into_iter() - .map(|mode| { - let mode: *const UIScreenMode = mode; - let mode = unsafe { Id::retain(mode as *mut UIScreenMode).unwrap() }; - - RootVideoMode { - video_mode: VideoMode::new(self.uiscreen.clone(), mode), - } - }) - .collect(); - - modes.into_iter().map(|mode| mode.video_mode) + self.ui_screen.get_on_main(|ui_screen, mtm| { + // Use Ord impl of RootVideoMode + + let modes: BTreeSet<_> = ui_screen + .availableModes() + .into_iter() + .map(|mode| RootVideoMode { + video_mode: VideoMode::new(ui_screen.clone(), mode, mtm), + }) + .collect(); + + modes.into_iter().map(|mode| mode.video_mode) + }) + } + + pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Id { + self.ui_screen.get(mtm) + } + + pub fn preferred_video_mode(&self) -> VideoMode { + self.ui_screen.get_on_main(|ui_screen, mtm| { + VideoMode::new(ui_screen.clone(), ui_screen.preferredMode().unwrap(), mtm) + }) } } @@ -220,27 +260,9 @@ fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 { refresh_rate_millihertz as u32 * 1000 } -// MonitorHandleExtIOS -impl Inner { - pub(crate) fn ui_screen(&self) -> &Id { - &self.uiscreen - } - - pub fn preferred_video_mode(&self) -> VideoMode { - VideoMode::new( - self.uiscreen.clone(), - self.uiscreen.preferredMode().unwrap(), - ) - } -} - pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { UIScreen::screens(mtm) .into_iter() - .map(|screen| { - let screen: *const UIScreen = screen; - let screen = unsafe { Id::retain(screen as *mut UIScreen).unwrap() }; - MonitorHandle::new(screen) - }) + .map(MonitorHandle::new) .collect() } diff --git a/src/platform_impl/ios/uikit/application.rs b/src/platform_impl/ios/uikit/application.rs index 319a6b03ba..fe64dec823 100644 --- a/src/platform_impl/ios/uikit/application.rs +++ b/src/platform_impl/ios/uikit/application.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{CGRect, MainThreadMarker, NSArray, NSObject}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{CGRect, MainThreadMarker, NSArray, NSObject}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{UIResponder, UIWindow}; @@ -11,20 +11,21 @@ extern_class!( unsafe impl ClassType for UIApplication { #[inherits(NSObject)] type Super = UIResponder; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIApplication { - pub fn shared(_mtm: MainThreadMarker) -> Option> { + pub fn shared(_mtm: MainThreadMarker) -> Option> { unsafe { msg_send_id![Self::class(), sharedApplication] } } - pub fn windows(&self) -> Id, Shared> { + pub fn windows(&self) -> Id> { unsafe { msg_send_id![self, windows] } } - #[sel(statusBarFrame)] + #[method(statusBarFrame)] pub fn statusBarFrame(&self) -> CGRect; } ); diff --git a/src/platform_impl/ios/uikit/coordinate_space.rs b/src/platform_impl/ios/uikit/coordinate_space.rs index d4d1c06eab..790c475633 100644 --- a/src/platform_impl/ios/uikit/coordinate_space.rs +++ b/src/platform_impl/ios/uikit/coordinate_space.rs @@ -1,5 +1,5 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{extern_class, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -7,5 +7,6 @@ extern_class!( unsafe impl ClassType for UICoordinateSpace { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); diff --git a/src/platform_impl/ios/uikit/device.rs b/src/platform_impl/ios/uikit/device.rs index 9910025a01..c663d65855 100644 --- a/src/platform_impl/ios/uikit/device.rs +++ b/src/platform_impl/ios/uikit/device.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{MainThreadMarker, NSObject}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{MainThreadMarker, NSObject}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::super::ffi::UIUserInterfaceIdiom; @@ -10,16 +10,17 @@ extern_class!( unsafe impl ClassType for UIDevice { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIDevice { - pub fn current(_mtm: MainThreadMarker) -> Id { + pub fn current(_mtm: MainThreadMarker) -> Id { unsafe { msg_send_id![Self::class(), currentDevice] } } - #[sel(userInterfaceIdiom)] + #[method(userInterfaceIdiom)] pub fn userInterfaceIdiom(&self) -> UIUserInterfaceIdiom; } ); diff --git a/src/platform_impl/ios/uikit/event.rs b/src/platform_impl/ios/uikit/event.rs index 9ce24261db..fbaccf6052 100644 --- a/src/platform_impl/ios/uikit/event.rs +++ b/src/platform_impl/ios/uikit/event.rs @@ -1,5 +1,5 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{extern_class, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -7,5 +7,6 @@ extern_class!( unsafe impl ClassType for UIEvent { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); diff --git a/src/platform_impl/ios/uikit/mod.rs b/src/platform_impl/ios/uikit/mod.rs index d42437488e..fcbc828650 100644 --- a/src/platform_impl/ios/uikit/mod.rs +++ b/src/platform_impl/ios/uikit/mod.rs @@ -1,10 +1,9 @@ -#![deny(unsafe_op_in_unsafe_fn)] #![allow(non_snake_case)] #![allow(non_upper_case_globals)] use std::os::raw::{c_char, c_int}; -use objc2::foundation::NSString; +use icrate::Foundation::NSString; mod application; mod coordinate_space; @@ -13,6 +12,7 @@ mod event; mod responder; mod screen; mod screen_mode; +mod status_bar_style; mod touch; mod trait_collection; mod view; @@ -26,6 +26,7 @@ pub(crate) use self::event::UIEvent; pub(crate) use self::responder::UIResponder; pub(crate) use self::screen::{UIScreen, UIScreenOverscanCompensation}; pub(crate) use self::screen_mode::UIScreenMode; +pub(crate) use self::status_bar_style::UIStatusBarStyle; pub(crate) use self::touch::{UITouch, UITouchPhase, UITouchType}; pub(crate) use self::trait_collection::{UIForceTouchCapability, UITraitCollection}; #[allow(unused_imports)] diff --git a/src/platform_impl/ios/uikit/responder.rs b/src/platform_impl/ios/uikit/responder.rs index 2a3cdb0a1c..fc5563f7ff 100644 --- a/src/platform_impl/ios/uikit/responder.rs +++ b/src/platform_impl/ios/uikit/responder.rs @@ -1,5 +1,5 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{extern_class, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -7,5 +7,6 @@ extern_class!( unsafe impl ClassType for UIResponder { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); diff --git a/src/platform_impl/ios/uikit/screen.rs b/src/platform_impl/ios/uikit/screen.rs index 770b92378a..94a55b1634 100644 --- a/src/platform_impl/ios/uikit/screen.rs +++ b/src/platform_impl/ios/uikit/screen.rs @@ -1,7 +1,7 @@ +use icrate::Foundation::{CGFloat, CGRect, MainThreadMarker, NSArray, NSInteger, NSObject}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{CGFloat, CGRect, MainThreadMarker, NSArray, NSInteger, NSObject}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{UICoordinateSpace, UIScreenMode}; @@ -11,53 +11,54 @@ extern_class!( unsafe impl ClassType for UIScreen { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIScreen { - pub fn main(_mtm: MainThreadMarker) -> Id { + pub fn main(_mtm: MainThreadMarker) -> Id { unsafe { msg_send_id![Self::class(), mainScreen] } } - pub fn screens(_mtm: MainThreadMarker) -> Id, Shared> { + pub fn screens(_mtm: MainThreadMarker) -> Id> { unsafe { msg_send_id![Self::class(), screens] } } - #[sel(bounds)] + #[method(bounds)] pub fn bounds(&self) -> CGRect; - #[sel(scale)] + #[method(scale)] pub fn scale(&self) -> CGFloat; - #[sel(nativeBounds)] + #[method(nativeBounds)] pub fn nativeBounds(&self) -> CGRect; - #[sel(nativeScale)] + #[method(nativeScale)] pub fn nativeScale(&self) -> CGFloat; - #[sel(maximumFramesPerSecond)] + #[method(maximumFramesPerSecond)] pub fn maximumFramesPerSecond(&self) -> NSInteger; - pub fn mirroredScreen(&self) -> Id { + pub fn mirroredScreen(&self) -> Id { unsafe { msg_send_id![Self::class(), mirroredScreen] } } - pub fn preferredMode(&self) -> Option> { + pub fn preferredMode(&self) -> Option> { unsafe { msg_send_id![self, preferredMode] } } - #[sel(setCurrentMode:)] + #[method(setCurrentMode:)] pub fn setCurrentMode(&self, mode: Option<&UIScreenMode>); - pub fn availableModes(&self) -> Id, Shared> { + pub fn availableModes(&self) -> Id> { unsafe { msg_send_id![self, availableModes] } } - #[sel(setOverscanCompensation:)] + #[method(setOverscanCompensation:)] pub fn setOverscanCompensation(&self, overscanCompensation: UIScreenOverscanCompensation); - pub fn coordinateSpace(&self) -> Id { + pub fn coordinateSpace(&self) -> Id { unsafe { msg_send_id![self, coordinateSpace] } } } diff --git a/src/platform_impl/ios/uikit/screen_mode.rs b/src/platform_impl/ios/uikit/screen_mode.rs index 1ef1a3dc4b..096cae49f9 100644 --- a/src/platform_impl/ios/uikit/screen_mode.rs +++ b/src/platform_impl/ios/uikit/screen_mode.rs @@ -1,5 +1,5 @@ -use objc2::foundation::{CGSize, NSObject}; -use objc2::{extern_class, extern_methods, ClassType}; +use icrate::Foundation::{CGSize, NSObject}; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -7,12 +7,13 @@ extern_class!( unsafe impl ClassType for UIScreenMode { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIScreenMode { - #[sel(size)] + #[method(size)] pub fn size(&self) -> CGSize; } ); diff --git a/src/platform_impl/ios/uikit/status_bar_style.rs b/src/platform_impl/ios/uikit/status_bar_style.rs new file mode 100644 index 0000000000..e38fd669e4 --- /dev/null +++ b/src/platform_impl/ios/uikit/status_bar_style.rs @@ -0,0 +1,27 @@ +use crate::platform::ios::StatusBarStyle; +use icrate::Foundation::NSInteger; +use objc2::encode::{Encode, Encoding}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UIStatusBarStyle { + #[default] + Default = 0, + LightContent = 1, + DarkContent = 3, +} + +impl From for UIStatusBarStyle { + fn from(value: StatusBarStyle) -> Self { + match value { + StatusBarStyle::Default => Self::Default, + StatusBarStyle::LightContent => Self::LightContent, + StatusBarStyle::DarkContent => Self::DarkContent, + } + } +} + +unsafe impl Encode for UIStatusBarStyle { + const ENCODING: Encoding = NSInteger::ENCODING; +} diff --git a/src/platform_impl/ios/uikit/touch.rs b/src/platform_impl/ios/uikit/touch.rs index d0c7c2c7d0..bff0e8fb35 100644 --- a/src/platform_impl/ios/uikit/touch.rs +++ b/src/platform_impl/ios/uikit/touch.rs @@ -1,6 +1,6 @@ +use icrate::Foundation::{CGFloat, CGPoint, NSInteger, NSObject}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{CGFloat, CGPoint, NSInteger, NSObject}; -use objc2::{extern_class, extern_methods, ClassType}; +use objc2::{extern_class, extern_methods, mutability, ClassType}; use super::UIView; @@ -10,27 +10,28 @@ extern_class!( unsafe impl ClassType for UITouch { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UITouch { - #[sel(locationInView:)] + #[method(locationInView:)] pub fn locationInView(&self, view: Option<&UIView>) -> CGPoint; - #[sel(type)] + #[method(type)] pub fn type_(&self) -> UITouchType; - #[sel(force)] + #[method(force)] pub fn force(&self) -> CGFloat; - #[sel(maximumPossibleForce)] + #[method(maximumPossibleForce)] pub fn maximumPossibleForce(&self) -> CGFloat; - #[sel(altitudeAngle)] + #[method(altitudeAngle)] pub fn altitudeAngle(&self) -> CGFloat; - #[sel(phase)] + #[method(phase)] pub fn phase(&self) -> UITouchPhase; } ); diff --git a/src/platform_impl/ios/uikit/trait_collection.rs b/src/platform_impl/ios/uikit/trait_collection.rs index 7ae58d2dd2..b6af7c9d78 100644 --- a/src/platform_impl/ios/uikit/trait_collection.rs +++ b/src/platform_impl/ios/uikit/trait_collection.rs @@ -1,6 +1,6 @@ +use icrate::Foundation::{NSInteger, NSObject}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{NSInteger, NSObject}; -use objc2::{extern_class, extern_methods, ClassType}; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -8,12 +8,13 @@ extern_class!( unsafe impl ClassType for UITraitCollection { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UITraitCollection { - #[sel(forceTouchCapability)] + #[method(forceTouchCapability)] pub fn forceTouchCapability(&self) -> UIForceTouchCapability; } ); diff --git a/src/platform_impl/ios/uikit/view.rs b/src/platform_impl/ios/uikit/view.rs index 05c229564c..216db016ed 100644 --- a/src/platform_impl/ios/uikit/view.rs +++ b/src/platform_impl/ios/uikit/view.rs @@ -1,7 +1,7 @@ +use icrate::Foundation::{CGFloat, CGRect, NSObject}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{CGFloat, CGRect, NSObject}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{UICoordinateSpace, UIResponder, UIViewController}; @@ -12,57 +12,58 @@ extern_class!( unsafe impl ClassType for UIView { #[inherits(NSObject)] type Super = UIResponder; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIView { - #[sel(bounds)] + #[method(bounds)] pub fn bounds(&self) -> CGRect; - #[sel(setBounds:)] + #[method(setBounds:)] pub fn setBounds(&self, value: CGRect); - #[sel(frame)] + #[method(frame)] pub fn frame(&self) -> CGRect; - #[sel(setFrame:)] + #[method(setFrame:)] pub fn setFrame(&self, value: CGRect); - #[sel(contentScaleFactor)] + #[method(contentScaleFactor)] pub fn contentScaleFactor(&self) -> CGFloat; - #[sel(setContentScaleFactor:)] + #[method(setContentScaleFactor:)] pub fn setContentScaleFactor(&self, val: CGFloat); - #[sel(setMultipleTouchEnabled:)] + #[method(setMultipleTouchEnabled:)] pub fn setMultipleTouchEnabled(&self, val: bool); - pub fn rootViewController(&self) -> Option> { + pub fn rootViewController(&self) -> Option> { unsafe { msg_send_id![self, rootViewController] } } - #[sel(setRootViewController:)] + #[method(setRootViewController:)] pub fn setRootViewController(&self, rootViewController: Option<&UIViewController>); - #[sel(convertRect:toCoordinateSpace:)] + #[method(convertRect:toCoordinateSpace:)] pub fn convertRect_toCoordinateSpace( &self, rect: CGRect, coordinateSpace: &UICoordinateSpace, ) -> CGRect; - #[sel(convertRect:fromCoordinateSpace:)] + #[method(convertRect:fromCoordinateSpace:)] pub fn convertRect_fromCoordinateSpace( &self, rect: CGRect, coordinateSpace: &UICoordinateSpace, ) -> CGRect; - #[sel(safeAreaInsets)] + #[method(safeAreaInsets)] pub fn safeAreaInsets(&self) -> UIEdgeInsets; - #[sel(setNeedsDisplay)] + #[method(setNeedsDisplay)] pub fn setNeedsDisplay(&self); } ); diff --git a/src/platform_impl/ios/uikit/view_controller.rs b/src/platform_impl/ios/uikit/view_controller.rs index 03f0e1662c..515d71b031 100644 --- a/src/platform_impl/ios/uikit/view_controller.rs +++ b/src/platform_impl/ios/uikit/view_controller.rs @@ -1,7 +1,7 @@ +use icrate::Foundation::{NSObject, NSUInteger}; use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{NSObject, NSUInteger}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{UIResponder, UIView}; @@ -12,28 +12,29 @@ extern_class!( unsafe impl ClassType for UIViewController { #[inherits(NSObject)] type Super = UIResponder; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIViewController { - #[sel(attemptRotationToDeviceOrientation)] + #[method(attemptRotationToDeviceOrientation)] pub fn attemptRotationToDeviceOrientation(); - #[sel(setNeedsStatusBarAppearanceUpdate)] + #[method(setNeedsStatusBarAppearanceUpdate)] pub fn setNeedsStatusBarAppearanceUpdate(&self); - #[sel(setNeedsUpdateOfHomeIndicatorAutoHidden)] + #[method(setNeedsUpdateOfHomeIndicatorAutoHidden)] pub fn setNeedsUpdateOfHomeIndicatorAutoHidden(&self); - #[sel(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)] + #[method(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)] pub fn setNeedsUpdateOfScreenEdgesDeferringSystemGestures(&self); - pub fn view(&self) -> Option> { + pub fn view(&self) -> Option> { unsafe { msg_send_id![self, view] } } - #[sel(setView:)] + #[method(setView:)] pub fn setView(&self, view: Option<&UIView>); } ); diff --git a/src/platform_impl/ios/uikit/window.rs b/src/platform_impl/ios/uikit/window.rs index ed78e56fad..b99e96d479 100644 --- a/src/platform_impl/ios/uikit/window.rs +++ b/src/platform_impl/ios/uikit/window.rs @@ -1,6 +1,6 @@ -use objc2::foundation::NSObject; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::NSObject; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{UIResponder, UIScreen, UIView}; @@ -11,25 +11,26 @@ extern_class!( unsafe impl ClassType for UIWindow { #[inherits(UIResponder, NSObject)] type Super = UIView; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl UIWindow { - pub fn screen(&self) -> Id { + pub fn screen(&self) -> Id { unsafe { msg_send_id![self, screen] } } - #[sel(setScreen:)] + #[method(setScreen:)] pub fn setScreen(&self, screen: &UIScreen); - #[sel(setHidden:)] + #[method(setHidden:)] pub fn setHidden(&self, flag: bool); - #[sel(makeKeyAndVisible)] + #[method(makeKeyAndVisible)] pub fn makeKeyAndVisible(&self); - #[sel(isKeyWindow)] + #[method(isKeyWindow)] pub fn isKeyWindow(&self) -> bool; } ); diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index 199daba7bb..a4ab15faa8 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -1,23 +1,31 @@ #![allow(clippy::unnecessary_cast)] +use std::cell::Cell; +use std::ptr::NonNull; -use objc2::foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSSet}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Class; -use objc2::{declare_class, extern_methods, msg_send, msg_send_id, ClassType}; +use icrate::Foundation::{ + CGFloat, CGRect, MainThreadMarker, NSData, NSError, NSObject, NSObjectProtocol, NSSet, +}; +use objc2::declare::{Ivar, IvarDrop}; +use objc2::rc::Id; +use objc2::runtime::AnyClass; +use objc2::{declare_class, extern_methods, msg_send, msg_send_id, mutability, ClassType}; +use super::app_state::{self, EventWrapper}; use super::uikit::{ UIApplication, UIDevice, UIEvent, UIForceTouchCapability, UIInterfaceOrientationMask, - UIResponder, UITouch, UITouchPhase, UITouchType, UITraitCollection, UIView, UIViewController, - UIWindow, + UIResponder, UIStatusBarStyle, UITouch, UITouchPhase, UITouchType, UITraitCollection, UIView, + UIViewController, UIWindow, }; use super::window::WindowId; + use crate::{ dpi::PhysicalPosition, - event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, + event::{ + DeviceId as RootDeviceId, Event, Force, IosRemoteRegistration, Touch, TouchPhase, + WindowEvent, + }, platform::ios::ValidOrientations, platform_impl::platform::{ - app_state, - event_loop::{EventProxy, EventWrapper}, ffi::{UIRectEdge, UIUserInterfaceIdiom}, window::PlatformSpecificWindowBuilderAttributes, DeviceId, Fullscreen, @@ -26,33 +34,33 @@ use crate::{ }; declare_class!( - pub(crate) struct WinitView {} + pub(crate) struct WinitView; unsafe impl ClassType for WinitView { #[inherits(UIResponder, NSObject)] type Super = UIView; + type Mutability = mutability::InteriorMutable; const NAME: &'static str = "WinitUIView"; } unsafe impl WinitView { - #[sel(drawRect:)] + #[method(drawRect:)] fn draw_rect(&self, rect: CGRect) { + let mtm = MainThreadMarker::new().unwrap(); let window = self.window().unwrap(); - unsafe { - app_state::handle_nonuser_events( - std::iter::once(EventWrapper::StaticEvent(Event::RedrawRequested( - RootWindowId(window.id()), - ))) - .chain(std::iter::once(EventWrapper::StaticEvent( - Event::RedrawEventsCleared, - ))), - ); - } + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }), + ); let _: () = unsafe { msg_send![super(self), drawRect: rect] }; } - #[sel(layoutSubviews)] + #[method(layoutSubviews)] fn layout_subviews(&self) { + let mtm = MainThreadMarker::new().unwrap(); let _: () = unsafe { msg_send![super(self), layoutSubviews] }; let window = self.window().unwrap(); @@ -75,16 +83,18 @@ declare_class!( self.setFrame(window_bounds); } - unsafe { - app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::Resized(size), - })); - } + }), + ); } - #[sel(setContentScaleFactor:)] + #[method(setContentScaleFactor:)] fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { + let mtm = MainThreadMarker::new().unwrap(); let _: () = unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; @@ -112,43 +122,44 @@ declare_class!( let screen_space = screen.coordinateSpace(); let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, }; let window_id = RootWindowId(window.id()); - unsafe { - app_state::handle_nonuser_events( - std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { window, scale_factor, - suggested_size: size, - })) - .chain(std::iter::once(EventWrapper::StaticEvent( - Event::WindowEvent { - window_id, - event: WindowEvent::Resized(size.to_physical(scale_factor)), - }, - ))), - ); - } + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); } - #[sel(touchesBegan:withEvent:)] + #[method(touchesBegan:withEvent:)] fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } - #[sel(touchesMoved:withEvent:)] + #[method(touchesMoved:withEvent:)] fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } - #[sel(touchesEnded:withEvent:)] + #[method(touchesEnded:withEvent:)] fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } - #[sel(touchesCancelled:withEvent:)] + #[method(touchesCancelled:withEvent:)] fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } @@ -158,17 +169,17 @@ declare_class!( extern_methods!( #[allow(non_snake_case)] unsafe impl WinitView { - fn window(&self) -> Option> { + fn window(&self) -> Option> { unsafe { msg_send_id![self, window] } } - unsafe fn traitCollection(&self) -> Id { + unsafe fn traitCollection(&self) -> Id { msg_send_id![self, traitCollection] } // TODO: Allow the user to customize this - #[sel(layerClass)] - pub(crate) fn layerClass() -> &'static Class; + #[method(layerClass)] + pub(crate) fn layerClass() -> &'static AnyClass; } ); @@ -178,9 +189,8 @@ impl WinitView { _window_attributes: &WindowAttributes, platform_attributes: &PlatformSpecificWindowBuilderAttributes, frame: CGRect, - ) -> Id { - let this: Id = - unsafe { msg_send_id![msg_send_id![Self::class(), alloc], initWithFrame: frame] }; + ) -> Id { + let this: Id = unsafe { msg_send_id![Self::alloc(), initWithFrame: frame] }; this.setMultipleTouchEnabled(true); @@ -203,7 +213,9 @@ impl WinitView { let trait_collection = unsafe { self.traitCollection() }; let touch_capability = trait_collection.forceTouchCapability(); // Both the OS _and_ the device need to be checked for force touch support. - if touch_capability == UIForceTouchCapability::Available { + if touch_capability == UIForceTouchCapability::Available + || touch_type == UITouchType::Pencil + { let force = touch.force(); let max_possible_force = touch.maximumPossibleForce(); let altitude_angle: Option = if touch_type == UITouchType::Pencil { @@ -254,108 +266,125 @@ impl WinitView { }), })); } - unsafe { - app_state::handle_nonuser_events(touch_events); - } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, touch_events); } } +pub struct ViewControllerState { + prefers_status_bar_hidden: Cell, + preferred_status_bar_style: Cell, + prefers_home_indicator_auto_hidden: Cell, + supported_orientations: Cell, + preferred_screen_edges_deferring_system_gestures: Cell, +} + declare_class!( pub(crate) struct WinitViewController { - _prefers_status_bar_hidden: bool, - _prefers_home_indicator_auto_hidden: bool, - _supported_orientations: UIInterfaceOrientationMask, - _preferred_screen_edges_deferring_system_gestures: UIRectEdge, + state: IvarDrop, "_state">, } + mod view_controller_ivars; + unsafe impl ClassType for WinitViewController { #[inherits(UIResponder, NSObject)] type Super = UIViewController; + type Mutability = mutability::InteriorMutable; const NAME: &'static str = "WinitUIViewController"; } unsafe impl WinitViewController { - #[sel(shouldAutorotate)] - fn should_autorotate(&self) -> bool { - true + #[method(init)] + unsafe fn init(this: *mut Self) -> Option> { + let this: Option<&mut Self> = msg_send![super(this), init]; + this.map(|this| { + // These are set in WinitViewController::new, it's just to set them + // to _something_. + Ivar::write( + &mut this.state, + Box::new(ViewControllerState { + prefers_status_bar_hidden: Cell::new(false), + preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default), + prefers_home_indicator_auto_hidden: Cell::new(false), + supported_orientations: Cell::new(UIInterfaceOrientationMask::All), + preferred_screen_edges_deferring_system_gestures: Cell::new( + UIRectEdge::NONE, + ), + }), + ); + NonNull::from(this) + }) } } unsafe impl WinitViewController { - #[sel(prefersStatusBarHidden)] - fn prefers_status_bar_hidden(&self) -> bool { - *self._prefers_status_bar_hidden + #[method(shouldAutorotate)] + fn should_autorotate(&self) -> bool { + true } - #[sel(setPrefersStatusBarHidden:)] - fn set_prefers_status_bar_hidden(&mut self, val: bool) { - *self._prefers_status_bar_hidden = val; - self.setNeedsStatusBarAppearanceUpdate(); + #[method(prefersStatusBarHidden)] + fn prefers_status_bar_hidden(&self) -> bool { + self.state.prefers_status_bar_hidden.get() } - #[sel(prefersHomeIndicatorAutoHidden)] - fn prefers_home_indicator_auto_hidden(&self) -> bool { - *self._prefers_home_indicator_auto_hidden + #[method(preferredStatusBarStyle)] + fn preferred_status_bar_style(&self) -> UIStatusBarStyle { + self.state.preferred_status_bar_style.get() } - #[sel(setPrefersHomeIndicatorAutoHidden:)] - fn set_prefers_home_indicator_auto_hidden(&mut self, val: bool) { - *self._prefers_home_indicator_auto_hidden = val; - let os_capabilities = app_state::os_capabilities(); - if os_capabilities.home_indicator_hidden { - self.setNeedsUpdateOfHomeIndicatorAutoHidden(); - } else { - os_capabilities.home_indicator_hidden_err_msg("ignoring") - } + #[method(prefersHomeIndicatorAutoHidden)] + fn prefers_home_indicator_auto_hidden(&self) -> bool { + self.state.prefers_home_indicator_auto_hidden.get() } - #[sel(supportedInterfaceOrientations)] + #[method(supportedInterfaceOrientations)] fn supported_orientations(&self) -> UIInterfaceOrientationMask { - *self._supported_orientations - } - - #[sel(setSupportedInterfaceOrientations:)] - fn set_supported_orientations(&mut self, val: UIInterfaceOrientationMask) { - *self._supported_orientations = val; - UIViewController::attemptRotationToDeviceOrientation(); + self.state.supported_orientations.get() } - #[sel(preferredScreenEdgesDeferringSystemGestures)] + #[method(preferredScreenEdgesDeferringSystemGestures)] fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge { - *self._preferred_screen_edges_deferring_system_gestures - } - - #[sel(setPreferredScreenEdgesDeferringSystemGestures:)] - fn set_preferred_screen_edges_deferring_system_gestures(&mut self, val: UIRectEdge) { - *self._preferred_screen_edges_deferring_system_gestures = val; - let os_capabilities = app_state::os_capabilities(); - if os_capabilities.defer_system_gestures { - self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); - } else { - os_capabilities.defer_system_gestures_err_msg("ignoring") - } + self.state + .preferred_screen_edges_deferring_system_gestures + .get() } } ); -extern_methods!( - #[allow(non_snake_case)] - unsafe impl WinitViewController { - #[sel(setPrefersStatusBarHidden:)] - pub(crate) fn setPrefersStatusBarHidden(&self, flag: bool); +impl WinitViewController { + pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) { + self.state.prefers_status_bar_hidden.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } - #[sel(setSupportedInterfaceOrientations:)] - pub(crate) fn setSupportedInterfaceOrientations(&self, val: UIInterfaceOrientationMask); + pub(crate) fn set_preferred_status_bar_style(&self, val: UIStatusBarStyle) { + self.state.preferred_status_bar_style.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } - #[sel(setPrefersHomeIndicatorAutoHidden:)] - pub(crate) fn setPrefersHomeIndicatorAutoHidden(&self, val: bool); + pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) { + self.state.prefers_home_indicator_auto_hidden.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.home_indicator_hidden { + self.setNeedsUpdateOfHomeIndicatorAutoHidden(); + } else { + os_capabilities.home_indicator_hidden_err_msg("ignoring") + } + } - #[sel(setPreferredScreenEdgesDeferringSystemGestures:)] - pub(crate) fn setPreferredScreenEdgesDeferringSystemGestures(&self, val: UIRectEdge); + pub(crate) fn set_preferred_screen_edges_deferring_system_gestures(&self, val: UIRectEdge) { + self.state + .preferred_screen_edges_deferring_system_gestures + .set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.defer_system_gestures { + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); + } else { + os_capabilities.defer_system_gestures_err_msg("ignoring") + } } -); -impl WinitViewController { pub(crate) fn set_supported_interface_orientations( &self, mtm: MainThreadMarker, @@ -378,7 +407,8 @@ impl WinitViewController { | UIInterfaceOrientationMask::PortraitUpsideDown } }; - self.setSupportedInterfaceOrientations(mask); + self.state.supported_orientations.set(mask); + UIViewController::attemptRotationToDeviceOrientation(); } pub(crate) fn new( @@ -386,17 +416,20 @@ impl WinitViewController { _window_attributes: &WindowAttributes, platform_attributes: &PlatformSpecificWindowBuilderAttributes, view: &UIView, - ) -> Id { - let this: Id = - unsafe { msg_send_id![msg_send_id![Self::class(), alloc], init] }; + ) -> Id { + let this: Id = unsafe { msg_send_id![Self::alloc(), init] }; - this.setPrefersStatusBarHidden(platform_attributes.prefers_status_bar_hidden); + this.set_prefers_status_bar_hidden(platform_attributes.prefers_status_bar_hidden); + + this.set_preferred_status_bar_style(platform_attributes.preferred_status_bar_style.into()); this.set_supported_interface_orientations(mtm, platform_attributes.valid_orientations); - this.setPrefersHomeIndicatorAutoHidden(platform_attributes.prefers_home_indicator_hidden); + this.set_prefers_home_indicator_auto_hidden( + platform_attributes.prefers_home_indicator_hidden, + ); - this.setPreferredScreenEdgesDeferringSystemGestures( + this.set_preferred_screen_edges_deferring_system_gestures( platform_attributes .preferred_screen_edges_deferring_system_gestures .into(), @@ -410,33 +443,39 @@ impl WinitViewController { declare_class!( #[derive(Debug, PartialEq, Eq, Hash)] - pub(crate) struct WinitUIWindow {} + pub(crate) struct WinitUIWindow; unsafe impl ClassType for WinitUIWindow { #[inherits(UIResponder, NSObject)] type Super = UIWindow; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitUIWindow"; } unsafe impl WinitUIWindow { - #[sel(becomeKeyWindow)] + #[method(becomeKeyWindow)] fn become_key_window(&self) { - unsafe { - app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(self.id()), event: WindowEvent::Focused(true), - })); - } + }), + ); let _: () = unsafe { msg_send![super(self), becomeKeyWindow] }; } - #[sel(resignKeyWindow)] + #[method(resignKeyWindow)] fn resign_key_window(&self) { - unsafe { - app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(self.id()), event: WindowEvent::Focused(false), - })); - } + }), + ); let _: () = unsafe { msg_send![super(self), resignKeyWindow] }; } } @@ -444,26 +483,25 @@ declare_class!( impl WinitUIWindow { pub(crate) fn new( - _mtm: MainThreadMarker, + mtm: MainThreadMarker, window_attributes: &WindowAttributes, _platform_attributes: &PlatformSpecificWindowBuilderAttributes, frame: CGRect, view_controller: &UIViewController, - ) -> Id { - let this: Id = - unsafe { msg_send_id![msg_send_id![Self::class(), alloc], initWithFrame: frame] }; + ) -> Id { + let this: Id = unsafe { msg_send_id![Self::alloc(), initWithFrame: frame] }; this.setRootViewController(Some(view_controller)); - match window_attributes.fullscreen.clone().map(Into::into) { + match window_attributes.fullscreen.0.clone().map(Into::into) { Some(Fullscreen::Exclusive(ref video_mode)) => { let monitor = video_mode.monitor(); - let screen = monitor.ui_screen(); - screen.setCurrentMode(Some(&video_mode.screen_mode.0)); + let screen = monitor.ui_screen(mtm); + screen.setCurrentMode(Some(video_mode.screen_mode(mtm))); this.setScreen(screen); } Some(Fullscreen::Borderless(Some(ref monitor))) => { - let screen = monitor.ui_screen(); + let screen = monitor.ui_screen(mtm); this.setScreen(screen); } _ => (), @@ -478,38 +516,86 @@ impl WinitUIWindow { } declare_class!( - pub struct WinitApplicationDelegate {} + pub struct WinitApplicationDelegate; unsafe impl ClassType for WinitApplicationDelegate { type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitApplicationDelegate"; } // UIApplicationDelegate protocol unsafe impl WinitApplicationDelegate { - #[sel(application:didFinishLaunchingWithOptions:)] + #[method(application:didFinishLaunchingWithOptions:)] fn did_finish_launching(&self, _application: &UIApplication, _: *mut NSObject) -> bool { - unsafe { - app_state::did_finish_launching(); - } + app_state::did_finish_launching(MainThreadMarker::new().unwrap()); true } - #[sel(applicationDidBecomeActive:)] + #[method(application:didRegisterForRemoteNotificationsWithDeviceToken:)] + fn did_register_for_remote_notifications_with_device_token( + &self, + _application: &UIApplication, + token_data: *mut NSData, + ) { + let slice: &[u8] = unsafe { token_data.as_ref() }.unwrap().bytes(); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::IosRemoteRegistration( + IosRemoteRegistration::DeviceToken(slice.to_vec()), + )), + ) + } + + #[method(application:didFailToRegisterForRemoteNotificationsWithError:)] + fn did_fail_to_register_for_remote_notifications( + &self, + _application: &UIApplication, + error: *mut NSError, + ) { + let code = unsafe { error.as_ref() }.unwrap().code(); + let localized_description: String = unsafe { error.as_ref() } + .unwrap() + .localizedDescription() + .to_string(); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::IosRemoteRegistration( + IosRemoteRegistration::Failed { + code, + localized_description, + }, + )), + ) + } + + #[method(applicationDidBecomeActive:)] fn did_become_active(&self, _application: &UIApplication) { - unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed)) } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)) } - #[sel(applicationWillResignActive:)] + #[method(applicationWillResignActive:)] fn will_resign_active(&self, _application: &UIApplication) { - unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Suspended)) } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)) } - #[sel(applicationWillEnterForeground:)] - fn will_enter_foreground(&self, _application: &UIApplication) {} - #[sel(applicationDidEnterBackground:)] - fn did_enter_background(&self, _application: &UIApplication) {} + #[method(applicationWillEnterForeground:)] + fn will_enter_foreground(&self, application: &UIApplication) { + self.send_occluded_event_for_all_windows(application, false); + } + + #[method(applicationDidEnterBackground:)] + fn did_enter_background(&self, application: &UIApplication) { + self.send_occluded_event_for_all_windows(application, true); + } - #[sel(applicationWillTerminate:)] + #[method(applicationWillTerminate:)] fn will_terminate(&self, application: &UIApplication) { let mut events = Vec::new(); for window in application.windows().iter() { @@ -526,10 +612,37 @@ declare_class!( })); } } - unsafe { - app_state::handle_nonuser_events(events); - app_state::terminated(); - } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, events); + app_state::terminated(mtm); + } + + #[method(applicationDidReceiveMemoryWarning:)] + fn did_receive_memory_warning(&self, _application: &UIApplication) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning)) } } ); + +impl WinitApplicationDelegate { + fn send_occluded_event_for_all_windows(&self, application: &UIApplication, occluded: bool) { + let mut events = Vec::new(); + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Occluded(occluded), + })); + } + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, events); + } +} diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 222c7a1595..28c7f318cf 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -1,17 +1,13 @@ #![allow(clippy::unnecessary_cast)] -use std::{ - collections::VecDeque, - ffi::c_void, - ops::{Deref, DerefMut}, -}; +use std::collections::VecDeque; -use objc2::foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadMarker}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; +use icrate::Foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; use objc2::{class, msg_send}; -use raw_window_handle::{RawDisplayHandle, RawWindowHandle, UiKitDisplayHandle, UiKitWindowHandle}; +use super::app_state::EventWrapper; use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation}; use super::view::{WinitUIWindow, WinitView, WinitViewController}; use crate::{ @@ -19,12 +15,9 @@ use crate::{ error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::{Event, WindowEvent}, icon::Icon, - platform::ios::{ScreenEdge, ValidOrientations}, + platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}, platform_impl::platform::{ - app_state, - event_loop::{EventProxy, EventWrapper}, - ffi::UIRectEdge, - monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, + app_state, monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, }, window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, @@ -33,9 +26,9 @@ use crate::{ }; pub struct Inner { - pub(crate) window: Id, - pub(crate) view_controller: Id, - pub(crate) view: Id, + window: Id, + view_controller: Id, + view: Id, gl_or_metal_backed: bool, } @@ -48,6 +41,10 @@ impl Inner { debug!("`Window::set_transparent` is ignored on iOS") } + pub fn set_blur(&self, _blur: bool) { + debug!("`Window::set_blur` is ignored on iOS") + } + pub fn set_visible(&self, visible: bool) { self.window.setHidden(!visible) } @@ -58,90 +55,81 @@ impl Inner { } pub fn request_redraw(&self) { - unsafe { - if self.gl_or_metal_backed { - // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or CAMetalLayer. - // Ordinarily the OS sets up a bunch of UIKit state before calling drawRect: on a UIView, but when using - // raw or gl/metal for drawing this work is completely avoided. - // - // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been confirmed via - // testing. - // - // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc - app_state::queue_gl_or_metal_redraw(self.window.clone()); - } else { - self.view.setNeedsDisplay(); - } + if self.gl_or_metal_backed { + let mtm = MainThreadMarker::new().unwrap(); + // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or CAMetalLayer. + // Ordinarily the OS sets up a bunch of UIKit state before calling drawRect: on a UIView, but when using + // raw or gl/metal for drawing this work is completely avoided. + // + // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been confirmed via + // testing. + // + // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc + app_state::queue_gl_or_metal_redraw(mtm, self.window.clone()); + } else { + self.view.setNeedsDisplay(); } } + pub fn pre_present_notify(&self) {} + pub fn inner_position(&self) -> Result, NotSupportedError> { - unsafe { - let safe_area = self.safe_area_screen_space(); - let position = LogicalPosition { - x: safe_area.origin.x as f64, - y: safe_area.origin.y as f64, - }; - let scale_factor = self.scale_factor(); - Ok(position.to_physical(scale_factor)) - } + let safe_area = self.safe_area_screen_space(); + let position = LogicalPosition { + x: safe_area.origin.x as f64, + y: safe_area.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) } pub fn outer_position(&self) -> Result, NotSupportedError> { - unsafe { - let screen_frame = self.screen_frame(); - let position = LogicalPosition { - x: screen_frame.origin.x as f64, - y: screen_frame.origin.y as f64, - }; - let scale_factor = self.scale_factor(); - Ok(position.to_physical(scale_factor)) - } + let screen_frame = self.screen_frame(); + let position = LogicalPosition { + x: screen_frame.origin.x as f64, + y: screen_frame.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) } pub fn set_outer_position(&self, physical_position: Position) { - unsafe { - let scale_factor = self.scale_factor(); - let position = physical_position.to_logical::(scale_factor); - let screen_frame = self.screen_frame(); - let new_screen_frame = CGRect { - origin: CGPoint { - x: position.x as _, - y: position.y as _, - }, - size: screen_frame.size, - }; - let bounds = self.rect_from_screen_space(new_screen_frame); - self.window.setBounds(bounds); - } + let scale_factor = self.scale_factor(); + let position = physical_position.to_logical::(scale_factor); + let screen_frame = self.screen_frame(); + let new_screen_frame = CGRect { + origin: CGPoint { + x: position.x as _, + y: position.y as _, + }, + size: screen_frame.size, + }; + let bounds = self.rect_from_screen_space(new_screen_frame); + self.window.setBounds(bounds); } pub fn inner_size(&self) -> PhysicalSize { - unsafe { - let scale_factor = self.scale_factor(); - let safe_area = self.safe_area_screen_space(); - let size = LogicalSize { - width: safe_area.size.width as f64, - height: safe_area.size.height as f64, - }; - size.to_physical(scale_factor) - } + let scale_factor = self.scale_factor(); + let safe_area = self.safe_area_screen_space(); + let size = LogicalSize { + width: safe_area.size.width as f64, + height: safe_area.size.height as f64, + }; + size.to_physical(scale_factor) } pub fn outer_size(&self) -> PhysicalSize { - unsafe { - let scale_factor = self.scale_factor(); - let screen_frame = self.screen_frame(); - let size = LogicalSize { - width: screen_frame.size.width as f64, - height: screen_frame.size.height as f64, - }; - size.to_physical(scale_factor) - } + let scale_factor = self.scale_factor(); + let screen_frame = self.screen_frame(); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + size.to_physical(scale_factor) } - pub fn set_inner_size(&self, _size: Size) { - warn!("not clear what `Window::set_inner_size` means on iOS"); + pub fn request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) } pub fn set_min_inner_size(&self, _dimensions: Option) { @@ -209,6 +197,9 @@ impl Inner { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } @@ -232,14 +223,17 @@ impl Inner { } pub(crate) fn set_fullscreen(&self, monitor: Option) { + let mtm = MainThreadMarker::new().unwrap(); let uiscreen = match &monitor { Some(Fullscreen::Exclusive(video_mode)) => { - let uiscreen = video_mode.monitor.ui_screen(); - uiscreen.setCurrentMode(Some(&video_mode.screen_mode.0)); + let uiscreen = video_mode.monitor.ui_screen(mtm); + uiscreen.setCurrentMode(Some(video_mode.screen_mode(mtm))); uiscreen.clone() } - Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen().clone(), - Some(Fullscreen::Borderless(None)) => self.current_monitor_inner().ui_screen().clone(), + Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen(mtm).clone(), + Some(Fullscreen::Borderless(None)) => { + self.current_monitor_inner().ui_screen(mtm).clone() + } None => { warn!("`Window::set_fullscreen(None)` ignored on iOS"); return; @@ -262,22 +256,21 @@ impl Inner { } pub(crate) fn fullscreen(&self) -> Option { - unsafe { - let monitor = self.current_monitor_inner(); - let uiscreen = monitor.ui_screen(); - let screen_space_bounds = self.screen_frame(); - let screen_bounds = uiscreen.bounds(); - - // TODO: track fullscreen instead of relying on brittle float comparisons - if screen_space_bounds.origin.x == screen_bounds.origin.x - && screen_space_bounds.origin.y == screen_bounds.origin.y - && screen_space_bounds.size.width == screen_bounds.size.width - && screen_space_bounds.size.height == screen_bounds.size.height - { - Some(Fullscreen::Borderless(Some(monitor))) - } else { - None - } + let mtm = MainThreadMarker::new().unwrap(); + let monitor = self.current_monitor_inner(); + let uiscreen = monitor.ui_screen(mtm); + let screen_space_bounds = self.screen_frame(); + let screen_bounds = uiscreen.bounds(); + + // TODO: track fullscreen instead of relying on brittle float comparisons + if screen_space_bounds.origin.x == screen_bounds.origin.x + && screen_space_bounds.origin.y == screen_bounds.origin.y + && screen_space_bounds.size.width == screen_bounds.size.width + && screen_space_bounds.size.height == screen_bounds.size.height + { + Some(Fullscreen::Borderless(Some(monitor))) + } else { + None } } @@ -338,16 +331,47 @@ impl Inner { self.window.id() } - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut window_handle = UiKitWindowHandle::empty(); + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::UiKitHandle::empty(); + window_handle.ui_window = Id::as_ptr(&self.window) as _; + window_handle.ui_view = Id::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Id::as_ptr(&self.view_controller) as _; + rwh_04::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::UiKitWindowHandle::empty(); window_handle.ui_window = Id::as_ptr(&self.window) as _; window_handle.ui_view = Id::as_ptr(&self.view) as _; window_handle.ui_view_controller = Id::as_ptr(&self.view_controller) as _; - RawWindowHandle::UiKit(window_handle) + rwh_05::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::UiKitWindowHandle::new({ + let ui_view = Id::as_ptr(&self.view) as _; + std::ptr::NonNull::new(ui_view).expect("Id should never be null") + }); + window_handle.ui_view_controller = + std::ptr::NonNull::new(Id::as_ptr(&self.view_controller) as _); + Ok(rwh_06::RawWindowHandle::UiKit(window_handle)) } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::UiKit(UiKitDisplayHandle::empty()) + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) } pub fn theme(&self) -> Option { @@ -355,6 +379,8 @@ impl Inner { None } + pub fn set_content_protected(&self, _protected: bool) {} + pub fn has_focus(&self) -> bool { self.window.isKeyWindow() } @@ -375,41 +401,16 @@ impl Inner { } pub struct Window { - pub inner: Inner, -} - -impl Drop for Window { - fn drop(&mut self) { - assert_main_thread!("`Window::drop` can only be run on the main thread on iOS"); - } -} - -unsafe impl Send for Window {} -unsafe impl Sync for Window {} - -impl Deref for Window { - type Target = Inner; - - fn deref(&self) -> &Inner { - assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); - &self.inner - } -} - -impl DerefMut for Window { - fn deref_mut(&mut self) -> &mut Inner { - assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); - &mut self.inner - } + inner: MainThreadBound, } impl Window { pub(crate) fn new( - _event_loop: &EventLoopWindowTarget, + event_loop: &EventLoopWindowTarget, window_attributes: WindowAttributes, platform_attributes: PlatformSpecificWindowBuilderAttributes, ) -> Result { - let mtm = MainThreadMarker::new().unwrap(); + let mtm = event_loop.mtm; if window_attributes.min_inner_size.is_some() { warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); @@ -421,10 +422,10 @@ impl Window { // TODO: transparency, visible let main_screen = UIScreen::main(mtm); - let fullscreen = window_attributes.fullscreen.clone().map(Into::into); + let fullscreen = window_attributes.fullscreen.0.clone().map(Into::into); let screen = match fullscreen { - Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(), - Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(), + Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(mtm), Some(Fullscreen::Borderless(None)) | None => &main_screen, }; @@ -464,7 +465,7 @@ impl Window { &view_controller, ); - unsafe { app_state::set_key_window(&window) }; + app_state::set_key_window(mtm, &window); // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` // event on window creation if the DPI factor != 1.0 @@ -476,50 +477,51 @@ impl Window { let screen_space = screen.coordinateSpace(); let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, }; let window_id = RootWindowId(window.id()); - unsafe { - app_state::handle_nonuser_events( - std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { window: window.clone(), scale_factor, - suggested_size: size, - })) - .chain(std::iter::once(EventWrapper::StaticEvent( - Event::WindowEvent { - window_id, - event: WindowEvent::Resized(size.to_physical(scale_factor)), - }, - ))), - ); - } + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); } + let inner = Inner { + window, + view_controller, + view, + gl_or_metal_backed, + }; Ok(Window { - inner: Inner { - window, - view_controller, - view, - gl_or_metal_backed, - }, + inner: MainThreadBound::new(inner, mtm), }) } -} -// WindowExtIOS -impl Inner { - pub fn ui_window(&self) -> *mut c_void { - Id::as_ptr(&self.window) as *mut c_void - } - pub fn ui_view_controller(&self) -> *mut c_void { - Id::as_ptr(&self.view_controller) as *mut c_void + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) } - pub fn ui_view(&self) -> *mut c_void { - Id::as_ptr(&self.view) as *mut c_void + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.get_on_main(|inner, _mtm| f(inner)) } +} +// WindowExtIOS +impl Inner { pub fn set_scale_factor(&self, scale_factor: f64) { assert!( dpi::validate_scale_factor(scale_factor), @@ -538,42 +540,42 @@ impl Inner { pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { self.view_controller - .setPrefersHomeIndicatorAutoHidden(hidden); + .set_prefers_home_indicator_auto_hidden(hidden); } pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { - let edges: UIRectEdge = edges.into(); self.view_controller - .setPreferredScreenEdgesDeferringSystemGestures(edges); + .set_preferred_screen_edges_deferring_system_gestures(edges.into()); } pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { - self.view_controller.setPrefersStatusBarHidden(hidden); + self.view_controller.set_prefers_status_bar_hidden(hidden); + } + + pub fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.view_controller + .set_preferred_status_bar_style(status_bar_style.into()); } } impl Inner { - // requires main thread - unsafe fn screen_frame(&self) -> CGRect { + fn screen_frame(&self) -> CGRect { self.rect_to_screen_space(self.window.bounds()) } - // requires main thread - unsafe fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { + fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { let screen_space = self.window.screen().coordinateSpace(); self.window .convertRect_toCoordinateSpace(rect, &screen_space) } - // requires main thread - unsafe fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { + fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { let screen_space = self.window.screen().coordinateSpace(); self.window .convertRect_fromCoordinateSpace(rect, &screen_space) } - // requires main thread - unsafe fn safe_area_screen_space(&self) -> CGRect { + fn safe_area_screen_space(&self) -> CGRect { let bounds = self.window.bounds(); if app_state::os_capabilities().safe_area { let safe_area = self.window.safeAreaInsets(); @@ -591,7 +593,7 @@ impl Inner { } else { let screen_frame = self.rect_to_screen_space(bounds); let status_bar_frame = { - let app = UIApplication::shared(MainThreadMarker::new().unwrap_unchecked()).expect( + let app = UIApplication::shared(MainThreadMarker::new().unwrap()).expect( "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS", ); app.statusBarFrame() @@ -648,8 +650,8 @@ impl From for WindowId { unsafe impl Send for WindowId {} unsafe impl Sync for WindowId {} -impl From<&Object> for WindowId { - fn from(window: &Object) -> WindowId { +impl From<&AnyObject> for WindowId { + fn from(window: &AnyObject) -> WindowId { WindowId { window: window as *const _ as _, } @@ -662,5 +664,6 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub valid_orientations: ValidOrientations, pub prefers_home_indicator_hidden: bool, pub prefers_status_bar_hidden: bool, + pub preferred_status_bar_style: StatusBarStyle, pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, } diff --git a/src/platform_impl/linux/common/keymap.rs b/src/platform_impl/linux/common/keymap.rs deleted file mode 100644 index 1527ddc473..0000000000 --- a/src/platform_impl/linux/common/keymap.rs +++ /dev/null @@ -1,887 +0,0 @@ -//! Convert XKB keys to Winit keys. - -use crate::keyboard::{Key, KeyCode, KeyLocation, NativeKey, NativeKeyCode}; - -/// Map the raw X11-style keycode to the `KeyCode` enum. -/// -/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. -pub fn raw_keycode_to_keycode(keycode: u32) -> KeyCode { - scancode_to_keycode(keycode.saturating_sub(8)) -} - -/// Map the linux scancode to Keycode. -/// -/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. -pub fn scancode_to_keycode(scancode: u32) -> KeyCode { - // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as - // libxkbcommon's documentation seems to suggest that the keycode values we're interested in - // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, - // I can only hope they agree on what the keycodes mean. - // - // Some of the keycodes are likely superfluous for our purposes, and some are ones which are - // difficult to test the correctness of, or discover the purpose of. Because of this, they've - // either been commented out here, or not included at all. - match scancode { - 0 => KeyCode::Unidentified(NativeKeyCode::Xkb(0)), - 1 => KeyCode::Escape, - 2 => KeyCode::Digit1, - 3 => KeyCode::Digit2, - 4 => KeyCode::Digit3, - 5 => KeyCode::Digit4, - 6 => KeyCode::Digit5, - 7 => KeyCode::Digit6, - 8 => KeyCode::Digit7, - 9 => KeyCode::Digit8, - 10 => KeyCode::Digit9, - 11 => KeyCode::Digit0, - 12 => KeyCode::Minus, - 13 => KeyCode::Equal, - 14 => KeyCode::Backspace, - 15 => KeyCode::Tab, - 16 => KeyCode::KeyQ, - 17 => KeyCode::KeyW, - 18 => KeyCode::KeyE, - 19 => KeyCode::KeyR, - 20 => KeyCode::KeyT, - 21 => KeyCode::KeyY, - 22 => KeyCode::KeyU, - 23 => KeyCode::KeyI, - 24 => KeyCode::KeyO, - 25 => KeyCode::KeyP, - 26 => KeyCode::BracketLeft, - 27 => KeyCode::BracketRight, - 28 => KeyCode::Enter, - 29 => KeyCode::ControlLeft, - 30 => KeyCode::KeyA, - 31 => KeyCode::KeyS, - 32 => KeyCode::KeyD, - 33 => KeyCode::KeyF, - 34 => KeyCode::KeyG, - 35 => KeyCode::KeyH, - 36 => KeyCode::KeyJ, - 37 => KeyCode::KeyK, - 38 => KeyCode::KeyL, - 39 => KeyCode::Semicolon, - 40 => KeyCode::Quote, - 41 => KeyCode::Backquote, - 42 => KeyCode::ShiftLeft, - 43 => KeyCode::Backslash, - 44 => KeyCode::KeyZ, - 45 => KeyCode::KeyX, - 46 => KeyCode::KeyC, - 47 => KeyCode::KeyV, - 48 => KeyCode::KeyB, - 49 => KeyCode::KeyN, - 50 => KeyCode::KeyM, - 51 => KeyCode::Comma, - 52 => KeyCode::Period, - 53 => KeyCode::Slash, - 54 => KeyCode::ShiftRight, - 55 => KeyCode::NumpadMultiply, - 56 => KeyCode::AltLeft, - 57 => KeyCode::Space, - 58 => KeyCode::CapsLock, - 59 => KeyCode::F1, - 60 => KeyCode::F2, - 61 => KeyCode::F3, - 62 => KeyCode::F4, - 63 => KeyCode::F5, - 64 => KeyCode::F6, - 65 => KeyCode::F7, - 66 => KeyCode::F8, - 67 => KeyCode::F9, - 68 => KeyCode::F10, - 69 => KeyCode::NumLock, - 70 => KeyCode::ScrollLock, - 71 => KeyCode::Numpad7, - 72 => KeyCode::Numpad8, - 73 => KeyCode::Numpad9, - 74 => KeyCode::NumpadSubtract, - 75 => KeyCode::Numpad4, - 76 => KeyCode::Numpad5, - 77 => KeyCode::Numpad6, - 78 => KeyCode::NumpadAdd, - 79 => KeyCode::Numpad1, - 80 => KeyCode::Numpad2, - 81 => KeyCode::Numpad3, - 82 => KeyCode::Numpad0, - 83 => KeyCode::NumpadDecimal, - 85 => KeyCode::Lang5, - 86 => KeyCode::IntlBackslash, - 87 => KeyCode::F11, - 88 => KeyCode::F12, - 89 => KeyCode::IntlRo, - 90 => KeyCode::Lang3, - 91 => KeyCode::Lang4, - 92 => KeyCode::Convert, - 93 => KeyCode::KanaMode, - 94 => KeyCode::NonConvert, - // 95 => KeyCode::KPJPCOMMA, - 96 => KeyCode::NumpadEnter, - 97 => KeyCode::ControlRight, - 98 => KeyCode::NumpadDivide, - 99 => KeyCode::PrintScreen, - 100 => KeyCode::AltRight, - // 101 => KeyCode::LINEFEED, - 102 => KeyCode::Home, - 103 => KeyCode::ArrowUp, - 104 => KeyCode::PageUp, - 105 => KeyCode::ArrowLeft, - 106 => KeyCode::ArrowRight, - 107 => KeyCode::End, - 108 => KeyCode::ArrowDown, - 109 => KeyCode::PageDown, - 110 => KeyCode::Insert, - 111 => KeyCode::Delete, - // 112 => KeyCode::MACRO, - 113 => KeyCode::AudioVolumeMute, - 114 => KeyCode::AudioVolumeDown, - 115 => KeyCode::AudioVolumeUp, - // 116 => KeyCode::POWER, - 117 => KeyCode::NumpadEqual, - // 118 => KeyCode::KPPLUSMINUS, - 119 => KeyCode::Pause, - // 120 => KeyCode::SCALE, - 121 => KeyCode::NumpadComma, - 122 => KeyCode::Lang1, - 123 => KeyCode::Lang2, - 124 => KeyCode::IntlYen, - 125 => KeyCode::SuperLeft, - 126 => KeyCode::SuperRight, - 127 => KeyCode::ContextMenu, - // 128 => KeyCode::STOP, - // 129 => KeyCode::AGAIN, - // 130 => KeyCode::PROPS, - // 131 => KeyCode::UNDO, - // 132 => KeyCode::FRONT, - // 133 => KeyCode::COPY, - // 134 => KeyCode::OPEN, - // 135 => KeyCode::PASTE, - // 136 => KeyCode::FIND, - // 137 => KeyCode::CUT, - // 138 => KeyCode::HELP, - // 139 => KeyCode::MENU, - // 140 => KeyCode::CALC, - // 141 => KeyCode::SETUP, - // 142 => KeyCode::SLEEP, - // 143 => KeyCode::WAKEUP, - // 144 => KeyCode::FILE, - // 145 => KeyCode::SENDFILE, - // 146 => KeyCode::DELETEFILE, - // 147 => KeyCode::XFER, - // 148 => KeyCode::PROG1, - // 149 => KeyCode::PROG2, - // 150 => KeyCode::WWW, - // 151 => KeyCode::MSDOS, - // 152 => KeyCode::COFFEE, - // 153 => KeyCode::ROTATE_DISPLAY, - // 154 => KeyCode::CYCLEWINDOWS, - // 155 => KeyCode::MAIL, - // 156 => KeyCode::BOOKMARKS, - // 157 => KeyCode::COMPUTER, - // 158 => KeyCode::BACK, - // 159 => KeyCode::FORWARD, - // 160 => KeyCode::CLOSECD, - // 161 => KeyCode::EJECTCD, - // 162 => KeyCode::EJECTCLOSECD, - 163 => KeyCode::MediaTrackNext, - 164 => KeyCode::MediaPlayPause, - 165 => KeyCode::MediaTrackPrevious, - 166 => KeyCode::MediaStop, - // 167 => KeyCode::RECORD, - // 168 => KeyCode::REWIND, - // 169 => KeyCode::PHONE, - // 170 => KeyCode::ISO, - // 171 => KeyCode::CONFIG, - // 172 => KeyCode::HOMEPAGE, - // 173 => KeyCode::REFRESH, - // 174 => KeyCode::EXIT, - // 175 => KeyCode::MOVE, - // 176 => KeyCode::EDIT, - // 177 => KeyCode::SCROLLUP, - // 178 => KeyCode::SCROLLDOWN, - // 179 => KeyCode::KPLEFTPAREN, - // 180 => KeyCode::KPRIGHTPAREN, - // 181 => KeyCode::NEW, - // 182 => KeyCode::REDO, - 183 => KeyCode::F13, - 184 => KeyCode::F14, - 185 => KeyCode::F15, - 186 => KeyCode::F16, - 187 => KeyCode::F17, - 188 => KeyCode::F18, - 189 => KeyCode::F19, - 190 => KeyCode::F20, - 191 => KeyCode::F21, - 192 => KeyCode::F22, - 193 => KeyCode::F23, - 194 => KeyCode::F24, - // 200 => KeyCode::PLAYCD, - // 201 => KeyCode::PAUSECD, - // 202 => KeyCode::PROG3, - // 203 => KeyCode::PROG4, - // 204 => KeyCode::DASHBOARD, - // 205 => KeyCode::SUSPEND, - // 206 => KeyCode::CLOSE, - // 207 => KeyCode::PLAY, - // 208 => KeyCode::FASTFORWARD, - // 209 => KeyCode::BASSBOOST, - // 210 => KeyCode::PRINT, - // 211 => KeyCode::HP, - // 212 => KeyCode::CAMERA, - // 213 => KeyCode::SOUND, - // 214 => KeyCode::QUESTION, - // 215 => KeyCode::EMAIL, - // 216 => KeyCode::CHAT, - // 217 => KeyCode::SEARCH, - // 218 => KeyCode::CONNECT, - // 219 => KeyCode::FINANCE, - // 220 => KeyCode::SPORT, - // 221 => KeyCode::SHOP, - // 222 => KeyCode::ALTERASE, - // 223 => KeyCode::CANCEL, - // 224 => KeyCode::BRIGHTNESSDOW, - // 225 => KeyCode::BRIGHTNESSU, - // 226 => KeyCode::MEDIA, - // 227 => KeyCode::SWITCHVIDEOMODE, - // 228 => KeyCode::KBDILLUMTOGGLE, - // 229 => KeyCode::KBDILLUMDOWN, - // 230 => KeyCode::KBDILLUMUP, - // 231 => KeyCode::SEND, - // 232 => KeyCode::REPLY, - // 233 => KeyCode::FORWARDMAIL, - // 234 => KeyCode::SAVE, - // 235 => KeyCode::DOCUMENTS, - // 236 => KeyCode::BATTERY, - // 237 => KeyCode::BLUETOOTH, - // 238 => KeyCode::WLAN, - // 239 => KeyCode::UWB, - 240 => KeyCode::Unidentified(NativeKeyCode::Unidentified), - // 241 => KeyCode::VIDEO_NEXT, - // 242 => KeyCode::VIDEO_PREV, - // 243 => KeyCode::BRIGHTNESS_CYCLE, - // 244 => KeyCode::BRIGHTNESS_AUTO, - // 245 => KeyCode::DISPLAY_OFF, - // 246 => KeyCode::WWAN, - // 247 => KeyCode::RFKILL, - // 248 => KeyCode::KEY_MICMUTE, - _ => KeyCode::Unidentified(NativeKeyCode::Xkb(scancode)), - } -} - -pub fn keycode_to_scancode(keycode: KeyCode) -> Option { - match keycode { - KeyCode::Unidentified(NativeKeyCode::Unidentified) => Some(240), - KeyCode::Unidentified(NativeKeyCode::Xkb(raw)) => Some(raw), - KeyCode::Escape => Some(1), - KeyCode::Digit1 => Some(2), - KeyCode::Digit2 => Some(3), - KeyCode::Digit3 => Some(4), - KeyCode::Digit4 => Some(5), - KeyCode::Digit5 => Some(6), - KeyCode::Digit6 => Some(7), - KeyCode::Digit7 => Some(8), - KeyCode::Digit8 => Some(9), - KeyCode::Digit9 => Some(10), - KeyCode::Digit0 => Some(11), - KeyCode::Minus => Some(12), - KeyCode::Equal => Some(13), - KeyCode::Backspace => Some(14), - KeyCode::Tab => Some(15), - KeyCode::KeyQ => Some(16), - KeyCode::KeyW => Some(17), - KeyCode::KeyE => Some(18), - KeyCode::KeyR => Some(19), - KeyCode::KeyT => Some(20), - KeyCode::KeyY => Some(21), - KeyCode::KeyU => Some(22), - KeyCode::KeyI => Some(23), - KeyCode::KeyO => Some(24), - KeyCode::KeyP => Some(25), - KeyCode::BracketLeft => Some(26), - KeyCode::BracketRight => Some(27), - KeyCode::Enter => Some(28), - KeyCode::ControlLeft => Some(29), - KeyCode::KeyA => Some(30), - KeyCode::KeyS => Some(31), - KeyCode::KeyD => Some(32), - KeyCode::KeyF => Some(33), - KeyCode::KeyG => Some(34), - KeyCode::KeyH => Some(35), - KeyCode::KeyJ => Some(36), - KeyCode::KeyK => Some(37), - KeyCode::KeyL => Some(38), - KeyCode::Semicolon => Some(39), - KeyCode::Quote => Some(40), - KeyCode::Backquote => Some(41), - KeyCode::ShiftLeft => Some(42), - KeyCode::Backslash => Some(43), - KeyCode::KeyZ => Some(44), - KeyCode::KeyX => Some(45), - KeyCode::KeyC => Some(46), - KeyCode::KeyV => Some(47), - KeyCode::KeyB => Some(48), - KeyCode::KeyN => Some(49), - KeyCode::KeyM => Some(50), - KeyCode::Comma => Some(51), - KeyCode::Period => Some(52), - KeyCode::Slash => Some(53), - KeyCode::ShiftRight => Some(54), - KeyCode::NumpadMultiply => Some(55), - KeyCode::AltLeft => Some(56), - KeyCode::Space => Some(57), - KeyCode::CapsLock => Some(58), - KeyCode::F1 => Some(59), - KeyCode::F2 => Some(60), - KeyCode::F3 => Some(61), - KeyCode::F4 => Some(62), - KeyCode::F5 => Some(63), - KeyCode::F6 => Some(64), - KeyCode::F7 => Some(65), - KeyCode::F8 => Some(66), - KeyCode::F9 => Some(67), - KeyCode::F10 => Some(68), - KeyCode::NumLock => Some(69), - KeyCode::ScrollLock => Some(70), - KeyCode::Numpad7 => Some(71), - KeyCode::Numpad8 => Some(72), - KeyCode::Numpad9 => Some(73), - KeyCode::NumpadSubtract => Some(74), - KeyCode::Numpad4 => Some(75), - KeyCode::Numpad5 => Some(76), - KeyCode::Numpad6 => Some(77), - KeyCode::NumpadAdd => Some(78), - KeyCode::Numpad1 => Some(79), - KeyCode::Numpad2 => Some(80), - KeyCode::Numpad3 => Some(81), - KeyCode::Numpad0 => Some(82), - KeyCode::NumpadDecimal => Some(83), - KeyCode::Lang5 => Some(85), - KeyCode::IntlBackslash => Some(86), - KeyCode::F11 => Some(87), - KeyCode::F12 => Some(88), - KeyCode::IntlRo => Some(89), - KeyCode::Lang3 => Some(90), - KeyCode::Lang4 => Some(91), - KeyCode::Convert => Some(92), - KeyCode::KanaMode => Some(93), - KeyCode::NonConvert => Some(94), - KeyCode::NumpadEnter => Some(96), - KeyCode::ControlRight => Some(97), - KeyCode::NumpadDivide => Some(98), - KeyCode::PrintScreen => Some(99), - KeyCode::AltRight => Some(100), - KeyCode::Home => Some(102), - KeyCode::ArrowUp => Some(103), - KeyCode::PageUp => Some(104), - KeyCode::ArrowLeft => Some(105), - KeyCode::ArrowRight => Some(106), - KeyCode::End => Some(107), - KeyCode::ArrowDown => Some(108), - KeyCode::PageDown => Some(109), - KeyCode::Insert => Some(110), - KeyCode::Delete => Some(111), - KeyCode::AudioVolumeMute => Some(113), - KeyCode::AudioVolumeDown => Some(114), - KeyCode::AudioVolumeUp => Some(115), - KeyCode::NumpadEqual => Some(117), - KeyCode::Pause => Some(119), - KeyCode::NumpadComma => Some(121), - KeyCode::Lang1 => Some(122), - KeyCode::Lang2 => Some(123), - KeyCode::IntlYen => Some(124), - KeyCode::SuperLeft => Some(125), - KeyCode::SuperRight => Some(126), - KeyCode::ContextMenu => Some(127), - KeyCode::MediaTrackNext => Some(163), - KeyCode::MediaPlayPause => Some(164), - KeyCode::MediaTrackPrevious => Some(165), - KeyCode::MediaStop => Some(166), - KeyCode::F13 => Some(183), - KeyCode::F14 => Some(184), - KeyCode::F15 => Some(185), - KeyCode::F16 => Some(186), - KeyCode::F17 => Some(187), - KeyCode::F18 => Some(188), - KeyCode::F19 => Some(189), - KeyCode::F20 => Some(190), - KeyCode::F21 => Some(191), - KeyCode::F22 => Some(192), - KeyCode::F23 => Some(193), - KeyCode::F24 => Some(194), - _ => None, - } -} - -pub fn keysym_to_key(keysym: u32) -> Key { - use xkbcommon_dl::keysyms; - match keysym { - // TTY function keys - keysyms::BackSpace => Key::Backspace, - keysyms::Tab => Key::Tab, - // keysyms::Linefeed => Key::Linefeed, - keysyms::Clear => Key::Clear, - keysyms::Return => Key::Enter, - keysyms::Pause => Key::Pause, - keysyms::Scroll_Lock => Key::ScrollLock, - keysyms::Sys_Req => Key::PrintScreen, - keysyms::Escape => Key::Escape, - keysyms::Delete => Key::Delete, - - // IME keys - keysyms::Multi_key => Key::Compose, - keysyms::Codeinput => Key::CodeInput, - keysyms::SingleCandidate => Key::SingleCandidate, - keysyms::MultipleCandidate => Key::AllCandidates, - keysyms::PreviousCandidate => Key::PreviousCandidate, - - // Japanese keys - keysyms::Kanji => Key::KanjiMode, - keysyms::Muhenkan => Key::NonConvert, - keysyms::Henkan_Mode => Key::Convert, - keysyms::Romaji => Key::Romaji, - keysyms::Hiragana => Key::Hiragana, - keysyms::Hiragana_Katakana => Key::HiraganaKatakana, - keysyms::Zenkaku => Key::Zenkaku, - keysyms::Hankaku => Key::Hankaku, - keysyms::Zenkaku_Hankaku => Key::ZenkakuHankaku, - // keysyms::Touroku => Key::Touroku, - // keysyms::Massyo => Key::Massyo, - keysyms::Kana_Lock => Key::KanaMode, - keysyms::Kana_Shift => Key::KanaMode, - keysyms::Eisu_Shift => Key::Alphanumeric, - keysyms::Eisu_toggle => Key::Alphanumeric, - // NOTE: The next three items are aliases for values we've already mapped. - // keysyms::Kanji_Bangou => Key::CodeInput, - // keysyms::Zen_Koho => Key::AllCandidates, - // keysyms::Mae_Koho => Key::PreviousCandidate, - - // Cursor control & motion - keysyms::Home => Key::Home, - keysyms::Left => Key::ArrowLeft, - keysyms::Up => Key::ArrowUp, - keysyms::Right => Key::ArrowRight, - keysyms::Down => Key::ArrowDown, - // keysyms::Prior => Key::PageUp, - keysyms::Page_Up => Key::PageUp, - // keysyms::Next => Key::PageDown, - keysyms::Page_Down => Key::PageDown, - keysyms::End => Key::End, - // keysyms::Begin => Key::Begin, - - // Misc. functions - keysyms::Select => Key::Select, - keysyms::Print => Key::PrintScreen, - keysyms::Execute => Key::Execute, - keysyms::Insert => Key::Insert, - keysyms::Undo => Key::Undo, - keysyms::Redo => Key::Redo, - keysyms::Menu => Key::ContextMenu, - keysyms::Find => Key::Find, - keysyms::Cancel => Key::Cancel, - keysyms::Help => Key::Help, - keysyms::Break => Key::Pause, - keysyms::Mode_switch => Key::ModeChange, - // keysyms::script_switch => Key::ModeChange, - keysyms::Num_Lock => Key::NumLock, - - // Keypad keys - // keysyms::KP_Space => Key::Character(" "), - keysyms::KP_Tab => Key::Tab, - keysyms::KP_Enter => Key::Enter, - keysyms::KP_F1 => Key::F1, - keysyms::KP_F2 => Key::F2, - keysyms::KP_F3 => Key::F3, - keysyms::KP_F4 => Key::F4, - keysyms::KP_Home => Key::Home, - keysyms::KP_Left => Key::ArrowLeft, - keysyms::KP_Up => Key::ArrowLeft, - keysyms::KP_Right => Key::ArrowRight, - keysyms::KP_Down => Key::ArrowDown, - // keysyms::KP_Prior => Key::PageUp, - keysyms::KP_Page_Up => Key::PageUp, - // keysyms::KP_Next => Key::PageDown, - keysyms::KP_Page_Down => Key::PageDown, - keysyms::KP_End => Key::End, - // This is the key labeled "5" on the numpad when NumLock is off. - // keysyms::KP_Begin => Key::Begin, - keysyms::KP_Insert => Key::Insert, - keysyms::KP_Delete => Key::Delete, - // keysyms::KP_Equal => Key::Equal, - // keysyms::KP_Multiply => Key::Multiply, - // keysyms::KP_Add => Key::Add, - // keysyms::KP_Separator => Key::Separator, - // keysyms::KP_Subtract => Key::Subtract, - // keysyms::KP_Decimal => Key::Decimal, - // keysyms::KP_Divide => Key::Divide, - - // keysyms::KP_0 => Key::Character("0"), - // keysyms::KP_1 => Key::Character("1"), - // keysyms::KP_2 => Key::Character("2"), - // keysyms::KP_3 => Key::Character("3"), - // keysyms::KP_4 => Key::Character("4"), - // keysyms::KP_5 => Key::Character("5"), - // keysyms::KP_6 => Key::Character("6"), - // keysyms::KP_7 => Key::Character("7"), - // keysyms::KP_8 => Key::Character("8"), - // keysyms::KP_9 => Key::Character("9"), - - // Function keys - keysyms::F1 => Key::F1, - keysyms::F2 => Key::F2, - keysyms::F3 => Key::F3, - keysyms::F4 => Key::F4, - keysyms::F5 => Key::F5, - keysyms::F6 => Key::F6, - keysyms::F7 => Key::F7, - keysyms::F8 => Key::F8, - keysyms::F9 => Key::F9, - keysyms::F10 => Key::F10, - keysyms::F11 => Key::F11, - keysyms::F12 => Key::F12, - keysyms::F13 => Key::F13, - keysyms::F14 => Key::F14, - keysyms::F15 => Key::F15, - keysyms::F16 => Key::F16, - keysyms::F17 => Key::F17, - keysyms::F18 => Key::F18, - keysyms::F19 => Key::F19, - keysyms::F20 => Key::F20, - keysyms::F21 => Key::F21, - keysyms::F22 => Key::F22, - keysyms::F23 => Key::F23, - keysyms::F24 => Key::F24, - keysyms::F25 => Key::F25, - keysyms::F26 => Key::F26, - keysyms::F27 => Key::F27, - keysyms::F28 => Key::F28, - keysyms::F29 => Key::F29, - keysyms::F30 => Key::F30, - keysyms::F31 => Key::F31, - keysyms::F32 => Key::F32, - keysyms::F33 => Key::F33, - keysyms::F34 => Key::F34, - keysyms::F35 => Key::F35, - - // Modifiers - keysyms::Shift_L => Key::Shift, - keysyms::Shift_R => Key::Shift, - keysyms::Control_L => Key::Control, - keysyms::Control_R => Key::Control, - keysyms::Caps_Lock => Key::CapsLock, - // keysyms::Shift_Lock => Key::ShiftLock, - - // keysyms::Meta_L => Key::Meta, - // keysyms::Meta_R => Key::Meta, - keysyms::Alt_L => Key::Alt, - keysyms::Alt_R => Key::Alt, - keysyms::Super_L => Key::Super, - keysyms::Super_R => Key::Super, - keysyms::Hyper_L => Key::Hyper, - keysyms::Hyper_R => Key::Hyper, - - // XKB function and modifier keys - // keysyms::ISO_Lock => Key::IsoLock, - // keysyms::ISO_Level2_Latch => Key::IsoLevel2Latch, - keysyms::ISO_Level3_Shift => Key::AltGraph, - keysyms::ISO_Level3_Latch => Key::AltGraph, - keysyms::ISO_Level3_Lock => Key::AltGraph, - // keysyms::ISO_Level5_Shift => Key::IsoLevel5Shift, - // keysyms::ISO_Level5_Latch => Key::IsoLevel5Latch, - // keysyms::ISO_Level5_Lock => Key::IsoLevel5Lock, - // keysyms::ISO_Group_Shift => Key::IsoGroupShift, - // keysyms::ISO_Group_Latch => Key::IsoGroupLatch, - // keysyms::ISO_Group_Lock => Key::IsoGroupLock, - keysyms::ISO_Next_Group => Key::GroupNext, - // keysyms::ISO_Next_Group_Lock => Key::GroupNextLock, - keysyms::ISO_Prev_Group => Key::GroupPrevious, - // keysyms::ISO_Prev_Group_Lock => Key::GroupPreviousLock, - keysyms::ISO_First_Group => Key::GroupFirst, - // keysyms::ISO_First_Group_Lock => Key::GroupFirstLock, - keysyms::ISO_Last_Group => Key::GroupLast, - // keysyms::ISO_Last_Group_Lock => Key::GroupLastLock, - // - keysyms::ISO_Left_Tab => Key::Tab, - // keysyms::ISO_Move_Line_Up => Key::IsoMoveLineUp, - // keysyms::ISO_Move_Line_Down => Key::IsoMoveLineDown, - // keysyms::ISO_Partial_Line_Up => Key::IsoPartialLineUp, - // keysyms::ISO_Partial_Line_Down => Key::IsoPartialLineDown, - // keysyms::ISO_Partial_Space_Left => Key::IsoPartialSpaceLeft, - // keysyms::ISO_Partial_Space_Right => Key::IsoPartialSpaceRight, - // keysyms::ISO_Set_Margin_Left => Key::IsoSetMarginLeft, - // keysyms::ISO_Set_Margin_Right => Key::IsoSetMarginRight, - // keysyms::ISO_Release_Margin_Left => Key::IsoReleaseMarginLeft, - // keysyms::ISO_Release_Margin_Right => Key::IsoReleaseMarginRight, - // keysyms::ISO_Release_Both_Margins => Key::IsoReleaseBothMargins, - // keysyms::ISO_Fast_Cursor_Left => Key::IsoFastCursorLeft, - // keysyms::ISO_Fast_Cursor_Right => Key::IsoFastCursorRight, - // keysyms::ISO_Fast_Cursor_Up => Key::IsoFastCursorUp, - // keysyms::ISO_Fast_Cursor_Down => Key::IsoFastCursorDown, - // keysyms::ISO_Continuous_Underline => Key::IsoContinuousUnderline, - // keysyms::ISO_Discontinuous_Underline => Key::IsoDiscontinuousUnderline, - // keysyms::ISO_Emphasize => Key::IsoEmphasize, - // keysyms::ISO_Center_Object => Key::IsoCenterObject, - keysyms::ISO_Enter => Key::Enter, - - // dead_grave..dead_currency - - // dead_lowline..dead_longsolidusoverlay - - // dead_a..dead_capital_schwa - - // dead_greek - - // First_Virtual_Screen..Terminate_Server - - // AccessX_Enable..AudibleBell_Enable - - // Pointer_Left..Pointer_Drag5 - - // Pointer_EnableKeys..Pointer_DfltBtnPrev - - // ch..C_H - - // 3270 terminal keys - // keysyms::3270_Duplicate => Key::Duplicate, - // keysyms::3270_FieldMark => Key::FieldMark, - // keysyms::3270_Right2 => Key::Right2, - // keysyms::3270_Left2 => Key::Left2, - // keysyms::3270_BackTab => Key::BackTab, - keysyms::_3270_EraseEOF => Key::EraseEof, - // keysyms::3270_EraseInput => Key::EraseInput, - // keysyms::3270_Reset => Key::Reset, - // keysyms::3270_Quit => Key::Quit, - // keysyms::3270_PA1 => Key::Pa1, - // keysyms::3270_PA2 => Key::Pa2, - // keysyms::3270_PA3 => Key::Pa3, - // keysyms::3270_Test => Key::Test, - keysyms::_3270_Attn => Key::Attn, - // keysyms::3270_CursorBlink => Key::CursorBlink, - // keysyms::3270_AltCursor => Key::AltCursor, - // keysyms::3270_KeyClick => Key::KeyClick, - // keysyms::3270_Jump => Key::Jump, - // keysyms::3270_Ident => Key::Ident, - // keysyms::3270_Rule => Key::Rule, - // keysyms::3270_Copy => Key::Copy, - keysyms::_3270_Play => Key::Play, - // keysyms::3270_Setup => Key::Setup, - // keysyms::3270_Record => Key::Record, - // keysyms::3270_ChangeScreen => Key::ChangeScreen, - // keysyms::3270_DeleteWord => Key::DeleteWord, - keysyms::_3270_ExSelect => Key::ExSel, - keysyms::_3270_CursorSelect => Key::CrSel, - keysyms::_3270_PrintScreen => Key::PrintScreen, - keysyms::_3270_Enter => Key::Enter, - - keysyms::space => Key::Space, - // exclam..Sinh_kunddaliya - - // XFree86 - // keysyms::XF86_ModeLock => Key::ModeLock, - - // XFree86 - Backlight controls - keysyms::XF86_MonBrightnessUp => Key::BrightnessUp, - keysyms::XF86_MonBrightnessDown => Key::BrightnessDown, - // keysyms::XF86_KbdLightOnOff => Key::LightOnOff, - // keysyms::XF86_KbdBrightnessUp => Key::KeyboardBrightnessUp, - // keysyms::XF86_KbdBrightnessDown => Key::KeyboardBrightnessDown, - - // XFree86 - "Internet" - keysyms::XF86_Standby => Key::Standby, - keysyms::XF86_AudioLowerVolume => Key::AudioVolumeDown, - keysyms::XF86_AudioRaiseVolume => Key::AudioVolumeUp, - keysyms::XF86_AudioPlay => Key::MediaPlay, - keysyms::XF86_AudioStop => Key::MediaStop, - keysyms::XF86_AudioPrev => Key::MediaTrackPrevious, - keysyms::XF86_AudioNext => Key::MediaTrackNext, - keysyms::XF86_HomePage => Key::BrowserHome, - keysyms::XF86_Mail => Key::LaunchMail, - // keysyms::XF86_Start => Key::Start, - keysyms::XF86_Search => Key::BrowserSearch, - keysyms::XF86_AudioRecord => Key::MediaRecord, - - // XFree86 - PDA - keysyms::XF86_Calculator => Key::LaunchApplication2, - // keysyms::XF86_Memo => Key::Memo, - // keysyms::XF86_ToDoList => Key::ToDoList, - keysyms::XF86_Calendar => Key::LaunchCalendar, - keysyms::XF86_PowerDown => Key::Power, - // keysyms::XF86_ContrastAdjust => Key::AdjustContrast, - // keysyms::XF86_RockerUp => Key::RockerUp, - // keysyms::XF86_RockerDown => Key::RockerDown, - // keysyms::XF86_RockerEnter => Key::RockerEnter, - - // XFree86 - More "Internet" - keysyms::XF86_Back => Key::BrowserBack, - keysyms::XF86_Forward => Key::BrowserForward, - // keysyms::XF86_Stop => Key::Stop, - keysyms::XF86_Refresh => Key::BrowserRefresh, - keysyms::XF86_PowerOff => Key::Power, - keysyms::XF86_WakeUp => Key::WakeUp, - keysyms::XF86_Eject => Key::Eject, - keysyms::XF86_ScreenSaver => Key::LaunchScreenSaver, - keysyms::XF86_WWW => Key::LaunchWebBrowser, - keysyms::XF86_Sleep => Key::Standby, - keysyms::XF86_Favorites => Key::BrowserFavorites, - keysyms::XF86_AudioPause => Key::MediaPause, - // keysyms::XF86_AudioMedia => Key::AudioMedia, - keysyms::XF86_MyComputer => Key::LaunchApplication1, - // keysyms::XF86_VendorHome => Key::VendorHome, - // keysyms::XF86_LightBulb => Key::LightBulb, - // keysyms::XF86_Shop => Key::BrowserShop, - // keysyms::XF86_History => Key::BrowserHistory, - // keysyms::XF86_OpenURL => Key::OpenUrl, - // keysyms::XF86_AddFavorite => Key::AddFavorite, - // keysyms::XF86_HotLinks => Key::HotLinks, - // keysyms::XF86_BrightnessAdjust => Key::BrightnessAdjust, - // keysyms::XF86_Finance => Key::BrowserFinance, - // keysyms::XF86_Community => Key::BrowserCommunity, - keysyms::XF86_AudioRewind => Key::MediaRewind, - // keysyms::XF86_BackForward => Key::???, - // XF86_Launch0..XF86_LaunchF - - // XF86_ApplicationLeft..XF86_CD - keysyms::XF86_Calculater => Key::LaunchApplication2, // Nice typo, libxkbcommon :) - // XF86_Clear - keysyms::XF86_Close => Key::Close, - keysyms::XF86_Copy => Key::Copy, - keysyms::XF86_Cut => Key::Cut, - // XF86_Display..XF86_Documents - keysyms::XF86_Excel => Key::LaunchSpreadsheet, - // XF86_Explorer..XF86iTouch - keysyms::XF86_LogOff => Key::LogOff, - // XF86_Market..XF86_MenuPB - keysyms::XF86_MySites => Key::BrowserFavorites, - keysyms::XF86_New => Key::New, - // XF86_News..XF86_OfficeHome - keysyms::XF86_Open => Key::Open, - // XF86_Option - keysyms::XF86_Paste => Key::Paste, - keysyms::XF86_Phone => Key::LaunchPhone, - // XF86_Q - keysyms::XF86_Reply => Key::MailReply, - keysyms::XF86_Reload => Key::BrowserRefresh, - // XF86_RotateWindows..XF86_RotationKB - keysyms::XF86_Save => Key::Save, - // XF86_ScrollUp..XF86_ScrollClick - keysyms::XF86_Send => Key::MailSend, - keysyms::XF86_Spell => Key::SpellCheck, - keysyms::XF86_SplitScreen => Key::SplitScreenToggle, - // XF86_Support..XF86_User2KB - keysyms::XF86_Video => Key::LaunchMediaPlayer, - // XF86_WheelButton - keysyms::XF86_Word => Key::LaunchWordProcessor, - // XF86_Xfer - keysyms::XF86_ZoomIn => Key::ZoomIn, - keysyms::XF86_ZoomOut => Key::ZoomOut, - - // XF86_Away..XF86_Messenger - keysyms::XF86_WebCam => Key::LaunchWebCam, - keysyms::XF86_MailForward => Key::MailForward, - // XF86_Pictures - keysyms::XF86_Music => Key::LaunchMusicPlayer, - - // XF86_Battery..XF86_UWB - // - keysyms::XF86_AudioForward => Key::MediaFastForward, - // XF86_AudioRepeat - keysyms::XF86_AudioRandomPlay => Key::RandomToggle, - keysyms::XF86_Subtitle => Key::Subtitle, - keysyms::XF86_AudioCycleTrack => Key::MediaAudioTrack, - // XF86_CycleAngle..XF86_Blue - // - keysyms::XF86_Suspend => Key::Standby, - keysyms::XF86_Hibernate => Key::Hibernate, - // XF86_TouchpadToggle..XF86_TouchpadOff - // - keysyms::XF86_AudioMute => Key::AudioVolumeMute, - - // XF86_Switch_VT_1..XF86_Switch_VT_12 - - // XF86_Ungrab..XF86_ClearGrab - keysyms::XF86_Next_VMode => Key::VideoModeNext, - // keysyms::XF86_Prev_VMode => Key::VideoModePrevious, - // XF86_LogWindowTree..XF86_LogGrabInfo - - // SunFA_Grave..SunFA_Cedilla - - // keysyms::SunF36 => Key::F36 | Key::F11, - // keysyms::SunF37 => Key::F37 | Key::F12, - - // keysyms::SunSys_Req => Key::PrintScreen, - // The next couple of xkb (until SunStop) are already handled. - // SunPrint_Screen..SunPageDown - - // SunUndo..SunFront - keysyms::SUN_Copy => Key::Copy, - keysyms::SUN_Open => Key::Open, - keysyms::SUN_Paste => Key::Paste, - keysyms::SUN_Cut => Key::Cut, - - // SunPowerSwitch - keysyms::SUN_AudioLowerVolume => Key::AudioVolumeDown, - keysyms::SUN_AudioMute => Key::AudioVolumeMute, - keysyms::SUN_AudioRaiseVolume => Key::AudioVolumeUp, - // SUN_VideoDegauss - keysyms::SUN_VideoLowerBrightness => Key::BrightnessDown, - keysyms::SUN_VideoRaiseBrightness => Key::BrightnessUp, - // SunPowerSwitchShift - // - 0 => Key::Unidentified(NativeKey::Unidentified), - _ => Key::Unidentified(NativeKey::Xkb(keysym)), - } -} - -pub fn keysym_location(keysym: u32) -> KeyLocation { - use xkbcommon_dl::keysyms; - match keysym { - keysyms::Shift_L - | keysyms::Control_L - | keysyms::Meta_L - | keysyms::Alt_L - | keysyms::Super_L - | keysyms::Hyper_L => KeyLocation::Left, - keysyms::Shift_R - | keysyms::Control_R - | keysyms::Meta_R - | keysyms::Alt_R - | keysyms::Super_R - | keysyms::Hyper_R => KeyLocation::Right, - keysyms::KP_0 - | keysyms::KP_1 - | keysyms::KP_2 - | keysyms::KP_3 - | keysyms::KP_4 - | keysyms::KP_5 - | keysyms::KP_6 - | keysyms::KP_7 - | keysyms::KP_8 - | keysyms::KP_9 - | keysyms::KP_Space - | keysyms::KP_Tab - | keysyms::KP_Enter - | keysyms::KP_F1 - | keysyms::KP_F2 - | keysyms::KP_F3 - | keysyms::KP_F4 - | keysyms::KP_Home - | keysyms::KP_Left - | keysyms::KP_Up - | keysyms::KP_Right - | keysyms::KP_Down - | keysyms::KP_Page_Up - | keysyms::KP_Page_Down - | keysyms::KP_End - | keysyms::KP_Begin - | keysyms::KP_Insert - | keysyms::KP_Delete - | keysyms::KP_Equal - | keysyms::KP_Multiply - | keysyms::KP_Add - | keysyms::KP_Separator - | keysyms::KP_Subtract - | keysyms::KP_Decimal - | keysyms::KP_Divide => KeyLocation::Numpad, - _ => KeyLocation::Standard, - } -} diff --git a/src/platform_impl/linux/common/mod.rs b/src/platform_impl/linux/common/mod.rs index fa23919676..ed7f82cde9 100644 --- a/src/platform_impl/linux/common/mod.rs +++ b/src/platform_impl/linux/common/mod.rs @@ -1,2 +1 @@ -pub mod keymap; -pub mod xkb_state; +pub mod xkb; diff --git a/src/platform_impl/linux/common/xkb/compose.rs b/src/platform_impl/linux/common/xkb/compose.rs new file mode 100644 index 0000000000..65b6514ba9 --- /dev/null +++ b/src/platform_impl/linux/common/xkb/compose.rs @@ -0,0 +1,124 @@ +//! XKB compose handling. + +use std::env; +use std::ffi::CString; +use std::ops::Deref; +use std::os::unix::ffi::OsStringExt; +use std::ptr::NonNull; + +use super::XkbContext; +use super::XKBCH; +use smol_str::SmolStr; +use xkbcommon_dl::{ + xkb_compose_compile_flags, xkb_compose_feed_result, xkb_compose_state, xkb_compose_state_flags, + xkb_compose_status, xkb_compose_table, xkb_keysym_t, +}; + +#[derive(Debug)] +pub struct XkbComposeTable { + table: NonNull, +} + +impl XkbComposeTable { + pub fn new(context: &XkbContext) -> Option { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + let locale = CString::new(locale.into_vec()).unwrap(); + + let table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + context.as_ptr(), + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + + let table = NonNull::new(table)?; + Some(Self { table }) + } + + /// Create new state with the given compose table. + pub fn new_state(&self) -> Option { + let state = unsafe { + (XKBCH.xkb_compose_state_new)( + self.table.as_ptr(), + xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; + + let state = NonNull::new(state)?; + Some(XkbComposeState { state }) + } +} + +impl Deref for XkbComposeTable { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl Drop for XkbComposeTable { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_table_unref)(self.table.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct XkbComposeState { + state: NonNull, +} + +impl XkbComposeState { + pub fn get_string(&mut self, scratch_buffer: &mut Vec) -> Option { + super::make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBCH.xkb_compose_state_get_utf8)(self.state.as_ptr(), ptr, len) + }) + } + + #[inline] + pub fn feed(&mut self, keysym: xkb_keysym_t) -> ComposeStatus { + let feed_result = unsafe { (XKBCH.xkb_compose_state_feed)(self.state.as_ptr(), keysym) }; + match feed_result { + xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED => ComposeStatus::Ignored, + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED => { + ComposeStatus::Accepted(self.status()) + } + } + } + + #[inline] + pub fn reset(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_reset)(self.state.as_ptr()); + } + } + + #[inline] + pub fn status(&mut self) -> xkb_compose_status { + unsafe { (XKBCH.xkb_compose_state_get_status)(self.state.as_ptr()) } + } +} + +impl Drop for XkbComposeState { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_unref)(self.state.as_ptr()); + }; + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ComposeStatus { + Accepted(xkb_compose_status), + Ignored, + None, +} diff --git a/src/platform_impl/linux/common/xkb/keymap.rs b/src/platform_impl/linux/common/xkb/keymap.rs new file mode 100644 index 0000000000..0fd15042a2 --- /dev/null +++ b/src/platform_impl/linux/common/xkb/keymap.rs @@ -0,0 +1,1051 @@ +//! XKB keymap. + +use std::ffi::c_char; +use std::ops::Deref; +use std::ptr::{self, NonNull}; + +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +#[cfg(wayland_platform)] +use {memmap2::MmapOptions, std::os::unix::io::OwnedFd}; + +use xkb::XKB_MOD_INVALID; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keymap, xkb_keymap_compile_flags, xkb_keysym_t, + xkb_layout_index_t, xkb_mod_index_t, +}; + +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{XkbContext, XKBH}; + +/// Map the raw X11-style keycode to the `KeyCode` enum. +/// +/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. +pub fn raw_keycode_to_physicalkey(keycode: u32) -> PhysicalKey { + scancode_to_keycode(keycode.saturating_sub(8)) +} + +/// Map the linux scancode to Keycode. +/// +/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. +pub fn scancode_to_keycode(scancode: u32) -> PhysicalKey { + // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as + // libxkbcommon's documentation seems to suggest that the keycode values we're interested in + // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, + // I can only hope they agree on what the keycodes mean. + // + // Some of the keycodes are likely superfluous for our purposes, and some are ones which are + // difficult to test the correctness of, or discover the purpose of. Because of this, they've + // either been commented out here, or not included at all. + PhysicalKey::Code(match scancode { + 0 => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), + 1 => KeyCode::Escape, + 2 => KeyCode::Digit1, + 3 => KeyCode::Digit2, + 4 => KeyCode::Digit3, + 5 => KeyCode::Digit4, + 6 => KeyCode::Digit5, + 7 => KeyCode::Digit6, + 8 => KeyCode::Digit7, + 9 => KeyCode::Digit8, + 10 => KeyCode::Digit9, + 11 => KeyCode::Digit0, + 12 => KeyCode::Minus, + 13 => KeyCode::Equal, + 14 => KeyCode::Backspace, + 15 => KeyCode::Tab, + 16 => KeyCode::KeyQ, + 17 => KeyCode::KeyW, + 18 => KeyCode::KeyE, + 19 => KeyCode::KeyR, + 20 => KeyCode::KeyT, + 21 => KeyCode::KeyY, + 22 => KeyCode::KeyU, + 23 => KeyCode::KeyI, + 24 => KeyCode::KeyO, + 25 => KeyCode::KeyP, + 26 => KeyCode::BracketLeft, + 27 => KeyCode::BracketRight, + 28 => KeyCode::Enter, + 29 => KeyCode::ControlLeft, + 30 => KeyCode::KeyA, + 31 => KeyCode::KeyS, + 32 => KeyCode::KeyD, + 33 => KeyCode::KeyF, + 34 => KeyCode::KeyG, + 35 => KeyCode::KeyH, + 36 => KeyCode::KeyJ, + 37 => KeyCode::KeyK, + 38 => KeyCode::KeyL, + 39 => KeyCode::Semicolon, + 40 => KeyCode::Quote, + 41 => KeyCode::Backquote, + 42 => KeyCode::ShiftLeft, + 43 => KeyCode::Backslash, + 44 => KeyCode::KeyZ, + 45 => KeyCode::KeyX, + 46 => KeyCode::KeyC, + 47 => KeyCode::KeyV, + 48 => KeyCode::KeyB, + 49 => KeyCode::KeyN, + 50 => KeyCode::KeyM, + 51 => KeyCode::Comma, + 52 => KeyCode::Period, + 53 => KeyCode::Slash, + 54 => KeyCode::ShiftRight, + 55 => KeyCode::NumpadMultiply, + 56 => KeyCode::AltLeft, + 57 => KeyCode::Space, + 58 => KeyCode::CapsLock, + 59 => KeyCode::F1, + 60 => KeyCode::F2, + 61 => KeyCode::F3, + 62 => KeyCode::F4, + 63 => KeyCode::F5, + 64 => KeyCode::F6, + 65 => KeyCode::F7, + 66 => KeyCode::F8, + 67 => KeyCode::F9, + 68 => KeyCode::F10, + 69 => KeyCode::NumLock, + 70 => KeyCode::ScrollLock, + 71 => KeyCode::Numpad7, + 72 => KeyCode::Numpad8, + 73 => KeyCode::Numpad9, + 74 => KeyCode::NumpadSubtract, + 75 => KeyCode::Numpad4, + 76 => KeyCode::Numpad5, + 77 => KeyCode::Numpad6, + 78 => KeyCode::NumpadAdd, + 79 => KeyCode::Numpad1, + 80 => KeyCode::Numpad2, + 81 => KeyCode::Numpad3, + 82 => KeyCode::Numpad0, + 83 => KeyCode::NumpadDecimal, + 85 => KeyCode::Lang5, + 86 => KeyCode::IntlBackslash, + 87 => KeyCode::F11, + 88 => KeyCode::F12, + 89 => KeyCode::IntlRo, + 90 => KeyCode::Lang3, + 91 => KeyCode::Lang4, + 92 => KeyCode::Convert, + 93 => KeyCode::KanaMode, + 94 => KeyCode::NonConvert, + // 95 => KeyCode::KPJPCOMMA, + 96 => KeyCode::NumpadEnter, + 97 => KeyCode::ControlRight, + 98 => KeyCode::NumpadDivide, + 99 => KeyCode::PrintScreen, + 100 => KeyCode::AltRight, + // 101 => KeyCode::LINEFEED, + 102 => KeyCode::Home, + 103 => KeyCode::ArrowUp, + 104 => KeyCode::PageUp, + 105 => KeyCode::ArrowLeft, + 106 => KeyCode::ArrowRight, + 107 => KeyCode::End, + 108 => KeyCode::ArrowDown, + 109 => KeyCode::PageDown, + 110 => KeyCode::Insert, + 111 => KeyCode::Delete, + // 112 => KeyCode::MACRO, + 113 => KeyCode::AudioVolumeMute, + 114 => KeyCode::AudioVolumeDown, + 115 => KeyCode::AudioVolumeUp, + // 116 => KeyCode::POWER, + 117 => KeyCode::NumpadEqual, + // 118 => KeyCode::KPPLUSMINUS, + 119 => KeyCode::Pause, + // 120 => KeyCode::SCALE, + 121 => KeyCode::NumpadComma, + 122 => KeyCode::Lang1, + 123 => KeyCode::Lang2, + 124 => KeyCode::IntlYen, + 125 => KeyCode::SuperLeft, + 126 => KeyCode::SuperRight, + 127 => KeyCode::ContextMenu, + // 128 => KeyCode::STOP, + // 129 => KeyCode::AGAIN, + // 130 => KeyCode::PROPS, + // 131 => KeyCode::UNDO, + // 132 => KeyCode::FRONT, + // 133 => KeyCode::COPY, + // 134 => KeyCode::OPEN, + // 135 => KeyCode::PASTE, + // 136 => KeyCode::FIND, + // 137 => KeyCode::CUT, + // 138 => KeyCode::HELP, + // 139 => KeyCode::MENU, + // 140 => KeyCode::CALC, + // 141 => KeyCode::SETUP, + // 142 => KeyCode::SLEEP, + // 143 => KeyCode::WAKEUP, + // 144 => KeyCode::FILE, + // 145 => KeyCode::SENDFILE, + // 146 => KeyCode::DELETEFILE, + // 147 => KeyCode::XFER, + // 148 => KeyCode::PROG1, + // 149 => KeyCode::PROG2, + // 150 => KeyCode::WWW, + // 151 => KeyCode::MSDOS, + // 152 => KeyCode::COFFEE, + // 153 => KeyCode::ROTATE_DISPLAY, + // 154 => KeyCode::CYCLEWINDOWS, + // 155 => KeyCode::MAIL, + // 156 => KeyCode::BOOKMARKS, + // 157 => KeyCode::COMPUTER, + // 158 => KeyCode::BACK, + // 159 => KeyCode::FORWARD, + // 160 => KeyCode::CLOSECD, + // 161 => KeyCode::EJECTCD, + // 162 => KeyCode::EJECTCLOSECD, + 163 => KeyCode::MediaTrackNext, + 164 => KeyCode::MediaPlayPause, + 165 => KeyCode::MediaTrackPrevious, + 166 => KeyCode::MediaStop, + // 167 => KeyCode::RECORD, + // 168 => KeyCode::REWIND, + // 169 => KeyCode::PHONE, + // 170 => KeyCode::ISO, + // 171 => KeyCode::CONFIG, + // 172 => KeyCode::HOMEPAGE, + // 173 => KeyCode::REFRESH, + // 174 => KeyCode::EXIT, + // 175 => KeyCode::MOVE, + // 176 => KeyCode::EDIT, + // 177 => KeyCode::SCROLLUP, + // 178 => KeyCode::SCROLLDOWN, + // 179 => KeyCode::KPLEFTPAREN, + // 180 => KeyCode::KPRIGHTPAREN, + // 181 => KeyCode::NEW, + // 182 => KeyCode::REDO, + 183 => KeyCode::F13, + 184 => KeyCode::F14, + 185 => KeyCode::F15, + 186 => KeyCode::F16, + 187 => KeyCode::F17, + 188 => KeyCode::F18, + 189 => KeyCode::F19, + 190 => KeyCode::F20, + 191 => KeyCode::F21, + 192 => KeyCode::F22, + 193 => KeyCode::F23, + 194 => KeyCode::F24, + // 200 => KeyCode::PLAYCD, + // 201 => KeyCode::PAUSECD, + // 202 => KeyCode::PROG3, + // 203 => KeyCode::PROG4, + // 204 => KeyCode::DASHBOARD, + // 205 => KeyCode::SUSPEND, + // 206 => KeyCode::CLOSE, + // 207 => KeyCode::PLAY, + // 208 => KeyCode::FASTFORWARD, + // 209 => KeyCode::BASSBOOST, + // 210 => KeyCode::PRINT, + // 211 => KeyCode::HP, + // 212 => KeyCode::CAMERA, + // 213 => KeyCode::SOUND, + // 214 => KeyCode::QUESTION, + // 215 => KeyCode::EMAIL, + // 216 => KeyCode::CHAT, + // 217 => KeyCode::SEARCH, + // 218 => KeyCode::CONNECT, + // 219 => KeyCode::FINANCE, + // 220 => KeyCode::SPORT, + // 221 => KeyCode::SHOP, + // 222 => KeyCode::ALTERASE, + // 223 => KeyCode::CANCEL, + // 224 => KeyCode::BRIGHTNESSDOW, + // 225 => KeyCode::BRIGHTNESSU, + // 226 => KeyCode::MEDIA, + // 227 => KeyCode::SWITCHVIDEOMODE, + // 228 => KeyCode::KBDILLUMTOGGLE, + // 229 => KeyCode::KBDILLUMDOWN, + // 230 => KeyCode::KBDILLUMUP, + // 231 => KeyCode::SEND, + // 232 => KeyCode::REPLY, + // 233 => KeyCode::FORWARDMAIL, + // 234 => KeyCode::SAVE, + // 235 => KeyCode::DOCUMENTS, + // 236 => KeyCode::BATTERY, + // 237 => KeyCode::BLUETOOTH, + // 238 => KeyCode::WLAN, + // 239 => KeyCode::UWB, + 240 => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + // 241 => KeyCode::VIDEO_NEXT, + // 242 => KeyCode::VIDEO_PREV, + // 243 => KeyCode::BRIGHTNESS_CYCLE, + // 244 => KeyCode::BRIGHTNESS_AUTO, + // 245 => KeyCode::DISPLAY_OFF, + // 246 => KeyCode::WWAN, + // 247 => KeyCode::RFKILL, + // 248 => KeyCode::KEY_MICMUTE, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(scancode)), + }) +} + +pub fn physicalkey_to_scancode(key: PhysicalKey) -> Option { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Unidentified => Some(240), + NativeKeyCode::Xkb(raw) => Some(raw), + _ => None, + }; + } + }; + + match code { + KeyCode::Escape => Some(1), + KeyCode::Digit1 => Some(2), + KeyCode::Digit2 => Some(3), + KeyCode::Digit3 => Some(4), + KeyCode::Digit4 => Some(5), + KeyCode::Digit5 => Some(6), + KeyCode::Digit6 => Some(7), + KeyCode::Digit7 => Some(8), + KeyCode::Digit8 => Some(9), + KeyCode::Digit9 => Some(10), + KeyCode::Digit0 => Some(11), + KeyCode::Minus => Some(12), + KeyCode::Equal => Some(13), + KeyCode::Backspace => Some(14), + KeyCode::Tab => Some(15), + KeyCode::KeyQ => Some(16), + KeyCode::KeyW => Some(17), + KeyCode::KeyE => Some(18), + KeyCode::KeyR => Some(19), + KeyCode::KeyT => Some(20), + KeyCode::KeyY => Some(21), + KeyCode::KeyU => Some(22), + KeyCode::KeyI => Some(23), + KeyCode::KeyO => Some(24), + KeyCode::KeyP => Some(25), + KeyCode::BracketLeft => Some(26), + KeyCode::BracketRight => Some(27), + KeyCode::Enter => Some(28), + KeyCode::ControlLeft => Some(29), + KeyCode::KeyA => Some(30), + KeyCode::KeyS => Some(31), + KeyCode::KeyD => Some(32), + KeyCode::KeyF => Some(33), + KeyCode::KeyG => Some(34), + KeyCode::KeyH => Some(35), + KeyCode::KeyJ => Some(36), + KeyCode::KeyK => Some(37), + KeyCode::KeyL => Some(38), + KeyCode::Semicolon => Some(39), + KeyCode::Quote => Some(40), + KeyCode::Backquote => Some(41), + KeyCode::ShiftLeft => Some(42), + KeyCode::Backslash => Some(43), + KeyCode::KeyZ => Some(44), + KeyCode::KeyX => Some(45), + KeyCode::KeyC => Some(46), + KeyCode::KeyV => Some(47), + KeyCode::KeyB => Some(48), + KeyCode::KeyN => Some(49), + KeyCode::KeyM => Some(50), + KeyCode::Comma => Some(51), + KeyCode::Period => Some(52), + KeyCode::Slash => Some(53), + KeyCode::ShiftRight => Some(54), + KeyCode::NumpadMultiply => Some(55), + KeyCode::AltLeft => Some(56), + KeyCode::Space => Some(57), + KeyCode::CapsLock => Some(58), + KeyCode::F1 => Some(59), + KeyCode::F2 => Some(60), + KeyCode::F3 => Some(61), + KeyCode::F4 => Some(62), + KeyCode::F5 => Some(63), + KeyCode::F6 => Some(64), + KeyCode::F7 => Some(65), + KeyCode::F8 => Some(66), + KeyCode::F9 => Some(67), + KeyCode::F10 => Some(68), + KeyCode::NumLock => Some(69), + KeyCode::ScrollLock => Some(70), + KeyCode::Numpad7 => Some(71), + KeyCode::Numpad8 => Some(72), + KeyCode::Numpad9 => Some(73), + KeyCode::NumpadSubtract => Some(74), + KeyCode::Numpad4 => Some(75), + KeyCode::Numpad5 => Some(76), + KeyCode::Numpad6 => Some(77), + KeyCode::NumpadAdd => Some(78), + KeyCode::Numpad1 => Some(79), + KeyCode::Numpad2 => Some(80), + KeyCode::Numpad3 => Some(81), + KeyCode::Numpad0 => Some(82), + KeyCode::NumpadDecimal => Some(83), + KeyCode::Lang5 => Some(85), + KeyCode::IntlBackslash => Some(86), + KeyCode::F11 => Some(87), + KeyCode::F12 => Some(88), + KeyCode::IntlRo => Some(89), + KeyCode::Lang3 => Some(90), + KeyCode::Lang4 => Some(91), + KeyCode::Convert => Some(92), + KeyCode::KanaMode => Some(93), + KeyCode::NonConvert => Some(94), + KeyCode::NumpadEnter => Some(96), + KeyCode::ControlRight => Some(97), + KeyCode::NumpadDivide => Some(98), + KeyCode::PrintScreen => Some(99), + KeyCode::AltRight => Some(100), + KeyCode::Home => Some(102), + KeyCode::ArrowUp => Some(103), + KeyCode::PageUp => Some(104), + KeyCode::ArrowLeft => Some(105), + KeyCode::ArrowRight => Some(106), + KeyCode::End => Some(107), + KeyCode::ArrowDown => Some(108), + KeyCode::PageDown => Some(109), + KeyCode::Insert => Some(110), + KeyCode::Delete => Some(111), + KeyCode::AudioVolumeMute => Some(113), + KeyCode::AudioVolumeDown => Some(114), + KeyCode::AudioVolumeUp => Some(115), + KeyCode::NumpadEqual => Some(117), + KeyCode::Pause => Some(119), + KeyCode::NumpadComma => Some(121), + KeyCode::Lang1 => Some(122), + KeyCode::Lang2 => Some(123), + KeyCode::IntlYen => Some(124), + KeyCode::SuperLeft => Some(125), + KeyCode::SuperRight => Some(126), + KeyCode::ContextMenu => Some(127), + KeyCode::MediaTrackNext => Some(163), + KeyCode::MediaPlayPause => Some(164), + KeyCode::MediaTrackPrevious => Some(165), + KeyCode::MediaStop => Some(166), + KeyCode::F13 => Some(183), + KeyCode::F14 => Some(184), + KeyCode::F15 => Some(185), + KeyCode::F16 => Some(186), + KeyCode::F17 => Some(187), + KeyCode::F18 => Some(188), + KeyCode::F19 => Some(189), + KeyCode::F20 => Some(190), + KeyCode::F21 => Some(191), + KeyCode::F22 => Some(192), + KeyCode::F23 => Some(193), + KeyCode::F24 => Some(194), + _ => None, + } +} + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => NamedKey::Backspace, + keysyms::Tab => NamedKey::Tab, + // keysyms::Linefeed => NamedKey::Linefeed, + keysyms::Clear => NamedKey::Clear, + keysyms::Return => NamedKey::Enter, + keysyms::Pause => NamedKey::Pause, + keysyms::Scroll_Lock => NamedKey::ScrollLock, + keysyms::Sys_Req => NamedKey::PrintScreen, + keysyms::Escape => NamedKey::Escape, + keysyms::Delete => NamedKey::Delete, + + // IME keys + keysyms::Multi_key => NamedKey::Compose, + keysyms::Codeinput => NamedKey::CodeInput, + keysyms::SingleCandidate => NamedKey::SingleCandidate, + keysyms::MultipleCandidate => NamedKey::AllCandidates, + keysyms::PreviousCandidate => NamedKey::PreviousCandidate, + + // Japanese keys + keysyms::Kanji => NamedKey::KanjiMode, + keysyms::Muhenkan => NamedKey::NonConvert, + keysyms::Henkan_Mode => NamedKey::Convert, + keysyms::Romaji => NamedKey::Romaji, + keysyms::Hiragana => NamedKey::Hiragana, + keysyms::Hiragana_Katakana => NamedKey::HiraganaKatakana, + keysyms::Zenkaku => NamedKey::Zenkaku, + keysyms::Hankaku => NamedKey::Hankaku, + keysyms::Zenkaku_Hankaku => NamedKey::ZenkakuHankaku, + // keysyms::Touroku => NamedKey::Touroku, + // keysyms::Massyo => NamedKey::Massyo, + keysyms::Kana_Lock => NamedKey::KanaMode, + keysyms::Kana_Shift => NamedKey::KanaMode, + keysyms::Eisu_Shift => NamedKey::Alphanumeric, + keysyms::Eisu_toggle => NamedKey::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => NamedKey::CodeInput, + // keysyms::Zen_Koho => NamedKey::AllCandidates, + // keysyms::Mae_Koho => NamedKey::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => NamedKey::Home, + keysyms::Left => NamedKey::ArrowLeft, + keysyms::Up => NamedKey::ArrowUp, + keysyms::Right => NamedKey::ArrowRight, + keysyms::Down => NamedKey::ArrowDown, + // keysyms::Prior => NamedKey::PageUp, + keysyms::Page_Up => NamedKey::PageUp, + // keysyms::Next => NamedKey::PageDown, + keysyms::Page_Down => NamedKey::PageDown, + keysyms::End => NamedKey::End, + // keysyms::Begin => NamedKey::Begin, + + // Misc. functions + keysyms::Select => NamedKey::Select, + keysyms::Print => NamedKey::PrintScreen, + keysyms::Execute => NamedKey::Execute, + keysyms::Insert => NamedKey::Insert, + keysyms::Undo => NamedKey::Undo, + keysyms::Redo => NamedKey::Redo, + keysyms::Menu => NamedKey::ContextMenu, + keysyms::Find => NamedKey::Find, + keysyms::Cancel => NamedKey::Cancel, + keysyms::Help => NamedKey::Help, + keysyms::Break => NamedKey::Pause, + keysyms::Mode_switch => NamedKey::ModeChange, + // keysyms::script_switch => NamedKey::ModeChange, + keysyms::Num_Lock => NamedKey::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => NamedKey::Tab, + keysyms::KP_Enter => NamedKey::Enter, + keysyms::KP_F1 => NamedKey::F1, + keysyms::KP_F2 => NamedKey::F2, + keysyms::KP_F3 => NamedKey::F3, + keysyms::KP_F4 => NamedKey::F4, + keysyms::KP_Home => NamedKey::Home, + keysyms::KP_Left => NamedKey::ArrowLeft, + keysyms::KP_Up => NamedKey::ArrowUp, + keysyms::KP_Right => NamedKey::ArrowRight, + keysyms::KP_Down => NamedKey::ArrowDown, + // keysyms::KP_Prior => NamedKey::PageUp, + keysyms::KP_Page_Up => NamedKey::PageUp, + // keysyms::KP_Next => NamedKey::PageDown, + keysyms::KP_Page_Down => NamedKey::PageDown, + keysyms::KP_End => NamedKey::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => NamedKey::Begin, + keysyms::KP_Insert => NamedKey::Insert, + keysyms::KP_Delete => NamedKey::Delete, + // keysyms::KP_Equal => NamedKey::Equal, + // keysyms::KP_Multiply => NamedKey::Multiply, + // keysyms::KP_Add => NamedKey::Add, + // keysyms::KP_Separator => NamedKey::Separator, + // keysyms::KP_Subtract => NamedKey::Subtract, + // keysyms::KP_Decimal => NamedKey::Decimal, + // keysyms::KP_Divide => NamedKey::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => NamedKey::F1, + keysyms::F2 => NamedKey::F2, + keysyms::F3 => NamedKey::F3, + keysyms::F4 => NamedKey::F4, + keysyms::F5 => NamedKey::F5, + keysyms::F6 => NamedKey::F6, + keysyms::F7 => NamedKey::F7, + keysyms::F8 => NamedKey::F8, + keysyms::F9 => NamedKey::F9, + keysyms::F10 => NamedKey::F10, + keysyms::F11 => NamedKey::F11, + keysyms::F12 => NamedKey::F12, + keysyms::F13 => NamedKey::F13, + keysyms::F14 => NamedKey::F14, + keysyms::F15 => NamedKey::F15, + keysyms::F16 => NamedKey::F16, + keysyms::F17 => NamedKey::F17, + keysyms::F18 => NamedKey::F18, + keysyms::F19 => NamedKey::F19, + keysyms::F20 => NamedKey::F20, + keysyms::F21 => NamedKey::F21, + keysyms::F22 => NamedKey::F22, + keysyms::F23 => NamedKey::F23, + keysyms::F24 => NamedKey::F24, + keysyms::F25 => NamedKey::F25, + keysyms::F26 => NamedKey::F26, + keysyms::F27 => NamedKey::F27, + keysyms::F28 => NamedKey::F28, + keysyms::F29 => NamedKey::F29, + keysyms::F30 => NamedKey::F30, + keysyms::F31 => NamedKey::F31, + keysyms::F32 => NamedKey::F32, + keysyms::F33 => NamedKey::F33, + keysyms::F34 => NamedKey::F34, + keysyms::F35 => NamedKey::F35, + + // Modifiers + keysyms::Shift_L => NamedKey::Shift, + keysyms::Shift_R => NamedKey::Shift, + keysyms::Control_L => NamedKey::Control, + keysyms::Control_R => NamedKey::Control, + keysyms::Caps_Lock => NamedKey::CapsLock, + // keysyms::Shift_Lock => NamedKey::ShiftLock, + + // keysyms::Meta_L => NamedKey::Meta, + // keysyms::Meta_R => NamedKey::Meta, + keysyms::Alt_L => NamedKey::Alt, + keysyms::Alt_R => NamedKey::Alt, + keysyms::Super_L => NamedKey::Super, + keysyms::Super_R => NamedKey::Super, + keysyms::Hyper_L => NamedKey::Hyper, + keysyms::Hyper_R => NamedKey::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => NamedKey::IsoLock, + // keysyms::ISO_Level2_Latch => NamedKey::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => NamedKey::AltGraph, + keysyms::ISO_Level3_Latch => NamedKey::AltGraph, + keysyms::ISO_Level3_Lock => NamedKey::AltGraph, + // keysyms::ISO_Level5_Shift => NamedKey::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => NamedKey::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => NamedKey::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => NamedKey::IsoGroupShift, + // keysyms::ISO_Group_Latch => NamedKey::IsoGroupLatch, + // keysyms::ISO_Group_Lock => NamedKey::IsoGroupLock, + keysyms::ISO_Next_Group => NamedKey::GroupNext, + // keysyms::ISO_Next_Group_Lock => NamedKey::GroupNextLock, + keysyms::ISO_Prev_Group => NamedKey::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => NamedKey::GroupPreviousLock, + keysyms::ISO_First_Group => NamedKey::GroupFirst, + // keysyms::ISO_First_Group_Lock => NamedKey::GroupFirstLock, + keysyms::ISO_Last_Group => NamedKey::GroupLast, + // keysyms::ISO_Last_Group_Lock => NamedKey::GroupLastLock, + // + keysyms::ISO_Left_Tab => NamedKey::Tab, + // keysyms::ISO_Move_Line_Up => NamedKey::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => NamedKey::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => NamedKey::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => NamedKey::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => NamedKey::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => NamedKey::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => NamedKey::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => NamedKey::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => NamedKey::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => NamedKey::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => NamedKey::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => NamedKey::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => NamedKey::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => NamedKey::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => NamedKey::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => NamedKey::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => NamedKey::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => NamedKey::IsoEmphasize, + // keysyms::ISO_Center_Object => NamedKey::IsoCenterObject, + keysyms::ISO_Enter => NamedKey::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => NamedKey::Duplicate, + // keysyms::3270_FieldMark => NamedKey::FieldMark, + // keysyms::3270_Right2 => NamedKey::Right2, + // keysyms::3270_Left2 => NamedKey::Left2, + // keysyms::3270_BackTab => NamedKey::BackTab, + keysyms::_3270_EraseEOF => NamedKey::EraseEof, + // keysyms::3270_EraseInput => NamedKey::EraseInput, + // keysyms::3270_Reset => NamedKey::Reset, + // keysyms::3270_Quit => NamedKey::Quit, + // keysyms::3270_PA1 => NamedKey::Pa1, + // keysyms::3270_PA2 => NamedKey::Pa2, + // keysyms::3270_PA3 => NamedKey::Pa3, + // keysyms::3270_Test => NamedKey::Test, + keysyms::_3270_Attn => NamedKey::Attn, + // keysyms::3270_CursorBlink => NamedKey::CursorBlink, + // keysyms::3270_AltCursor => NamedKey::AltCursor, + // keysyms::3270_KeyClick => NamedKey::KeyClick, + // keysyms::3270_Jump => NamedKey::Jump, + // keysyms::3270_Ident => NamedKey::Ident, + // keysyms::3270_Rule => NamedKey::Rule, + // keysyms::3270_Copy => NamedKey::Copy, + keysyms::_3270_Play => NamedKey::Play, + // keysyms::3270_Setup => NamedKey::Setup, + // keysyms::3270_Record => NamedKey::Record, + // keysyms::3270_ChangeScreen => NamedKey::ChangeScreen, + // keysyms::3270_DeleteWord => NamedKey::DeleteWord, + keysyms::_3270_ExSelect => NamedKey::ExSel, + keysyms::_3270_CursorSelect => NamedKey::CrSel, + keysyms::_3270_PrintScreen => NamedKey::PrintScreen, + keysyms::_3270_Enter => NamedKey::Enter, + + keysyms::space => NamedKey::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => NamedKey::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => NamedKey::BrightnessUp, + keysyms::XF86_MonBrightnessDown => NamedKey::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => NamedKey::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => NamedKey::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => NamedKey::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => NamedKey::Standby, + keysyms::XF86_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => NamedKey::AudioVolumeUp, + keysyms::XF86_AudioPlay => NamedKey::MediaPlay, + keysyms::XF86_AudioStop => NamedKey::MediaStop, + keysyms::XF86_AudioPrev => NamedKey::MediaTrackPrevious, + keysyms::XF86_AudioNext => NamedKey::MediaTrackNext, + keysyms::XF86_HomePage => NamedKey::BrowserHome, + keysyms::XF86_Mail => NamedKey::LaunchMail, + // keysyms::XF86_Start => NamedKey::Start, + keysyms::XF86_Search => NamedKey::BrowserSearch, + keysyms::XF86_AudioRecord => NamedKey::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => NamedKey::LaunchApplication2, + // keysyms::XF86_Memo => NamedKey::Memo, + // keysyms::XF86_ToDoList => NamedKey::ToDoList, + keysyms::XF86_Calendar => NamedKey::LaunchCalendar, + keysyms::XF86_PowerDown => NamedKey::Power, + // keysyms::XF86_ContrastAdjust => NamedKey::AdjustContrast, + // keysyms::XF86_RockerUp => NamedKey::RockerUp, + // keysyms::XF86_RockerDown => NamedKey::RockerDown, + // keysyms::XF86_RockerEnter => NamedKey::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => NamedKey::BrowserBack, + keysyms::XF86_Forward => NamedKey::BrowserForward, + // keysyms::XF86_Stop => NamedKey::Stop, + keysyms::XF86_Refresh => NamedKey::BrowserRefresh, + keysyms::XF86_PowerOff => NamedKey::Power, + keysyms::XF86_WakeUp => NamedKey::WakeUp, + keysyms::XF86_Eject => NamedKey::Eject, + keysyms::XF86_ScreenSaver => NamedKey::LaunchScreenSaver, + keysyms::XF86_WWW => NamedKey::LaunchWebBrowser, + keysyms::XF86_Sleep => NamedKey::Standby, + keysyms::XF86_Favorites => NamedKey::BrowserFavorites, + keysyms::XF86_AudioPause => NamedKey::MediaPause, + // keysyms::XF86_AudioMedia => NamedKey::AudioMedia, + keysyms::XF86_MyComputer => NamedKey::LaunchApplication1, + // keysyms::XF86_VendorHome => NamedKey::VendorHome, + // keysyms::XF86_LightBulb => NamedKey::LightBulb, + // keysyms::XF86_Shop => NamedKey::BrowserShop, + // keysyms::XF86_History => NamedKey::BrowserHistory, + // keysyms::XF86_OpenURL => NamedKey::OpenUrl, + // keysyms::XF86_AddFavorite => NamedKey::AddFavorite, + // keysyms::XF86_HotLinks => NamedKey::HotLinks, + // keysyms::XF86_BrightnessAdjust => NamedKey::BrightnessAdjust, + // keysyms::XF86_Finance => NamedKey::BrowserFinance, + // keysyms::XF86_Community => NamedKey::BrowserCommunity, + keysyms::XF86_AudioRewind => NamedKey::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => NamedKey::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => NamedKey::Close, + keysyms::XF86_Copy => NamedKey::Copy, + keysyms::XF86_Cut => NamedKey::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => NamedKey::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => NamedKey::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => NamedKey::BrowserFavorites, + keysyms::XF86_New => NamedKey::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => NamedKey::Open, + // XF86_Option + keysyms::XF86_Paste => NamedKey::Paste, + keysyms::XF86_Phone => NamedKey::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => NamedKey::MailReply, + keysyms::XF86_Reload => NamedKey::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => NamedKey::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => NamedKey::MailSend, + keysyms::XF86_Spell => NamedKey::SpellCheck, + keysyms::XF86_SplitScreen => NamedKey::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => NamedKey::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => NamedKey::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => NamedKey::ZoomIn, + keysyms::XF86_ZoomOut => NamedKey::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => NamedKey::LaunchWebCam, + keysyms::XF86_MailForward => NamedKey::MailForward, + // XF86_Pictures + keysyms::XF86_Music => NamedKey::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + // + keysyms::XF86_AudioForward => NamedKey::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => NamedKey::RandomToggle, + keysyms::XF86_Subtitle => NamedKey::Subtitle, + keysyms::XF86_AudioCycleTrack => NamedKey::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + // + keysyms::XF86_Suspend => NamedKey::Standby, + keysyms::XF86_Hibernate => NamedKey::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + // + keysyms::XF86_AudioMute => NamedKey::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => NamedKey::VideoModeNext, + // keysyms::XF86_Prev_VMode => NamedKey::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => NamedKey::F36 | NamedKey::F11, + // keysyms::SunF37 => NamedKey::F37 | NamedKey::F12, + + // keysyms::SunSys_Req => NamedKey::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => NamedKey::Copy, + keysyms::SUN_Open => NamedKey::Open, + keysyms::SUN_Paste => NamedKey::Paste, + keysyms::SUN_Cut => NamedKey::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::SUN_AudioMute => NamedKey::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => NamedKey::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => NamedKey::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => NamedKey::BrightnessUp, + // SunPowerSwitchShift + // + 0 => return Key::Unidentified(NativeKey::Unidentified), + _ => return Key::Unidentified(NativeKey::Xkb(keysym)), + }) +} + +pub fn keysym_location(keysym: u32) -> KeyLocation { + use xkbcommon_dl::keysyms; + match keysym { + keysyms::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => KeyLocation::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => KeyLocation::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +#[derive(Debug)] +pub struct XkbKeymap { + keymap: NonNull, + _mods_indices: ModsIndices, + pub _core_keyboard_id: i32, +} + +impl XkbKeymap { + #[cfg(wayland_platform)] + pub fn from_fd(context: &XkbContext, fd: OwnedFd, size: usize) -> Option { + let map = unsafe { MmapOptions::new().len(size).map_copy_read_only(&fd).ok()? }; + + let keymap = unsafe { + let keymap = (XKBH.xkb_keymap_new_from_string)( + (*context).as_ptr(), + map.as_ptr() as *const _, + xkb::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + NonNull::new(keymap)? + }; + + Some(Self::new_inner(keymap, 0)) + } + + #[cfg(x11_platform)] + pub fn from_x11_keymap( + context: &XkbContext, + xcb: *mut xcb_connection_t, + core_keyboard_id: i32, + ) -> Option { + let keymap = unsafe { + (XKBXH.xkb_x11_keymap_new_from_device)( + context.as_ptr(), + xcb, + core_keyboard_id, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + let keymap = NonNull::new(keymap)?; + Some(Self::new_inner(keymap, core_keyboard_id)) + } + + fn new_inner(keymap: NonNull, _core_keyboard_id: i32) -> Self { + let mods_indices = ModsIndices { + shift: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_SHIFT), + caps: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CAPS), + ctrl: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CTRL), + alt: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_ALT), + num: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_NUM), + mod3: mod_index_for_name(keymap, b"Mod3\0"), + logo: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_LOGO), + mod5: mod_index_for_name(keymap, b"Mod5\0"), + }; + + Self { + keymap, + _mods_indices: mods_indices, + _core_keyboard_id, + } + } + + #[cfg(x11_platform)] + pub fn mods_indices(&self) -> ModsIndices { + self._mods_indices + } + + pub fn first_keysym_by_level( + &mut self, + layout: xkb_layout_index_t, + keycode: xkb_keycode_t, + ) -> xkb_keysym_t { + unsafe { + let mut keysyms = ptr::null(); + let count = (XKBH.xkb_keymap_key_get_syms_by_level)( + self.keymap.as_ptr(), + keycode, + layout, + // NOTE: The level should be zero to ignore modifiers. + 0, + &mut keysyms, + ); + + if count == 1 { + *keysyms + } else { + 0 + } + } + } + + /// Check whether the given key repeats. + pub fn key_repeats(&mut self, keycode: xkb_keycode_t) -> bool { + unsafe { (XKBH.xkb_keymap_key_repeats)(self.keymap.as_ptr(), keycode) == 1 } + } +} + +impl Drop for XkbKeymap { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_keymap_unref)(self.keymap.as_ptr()); + }; + } +} + +impl Deref for XkbKeymap { + type Target = NonNull; + fn deref(&self) -> &Self::Target { + &self.keymap + } +} + +/// Modifier index in the keymap. +#[derive(Default, Debug, Clone, Copy)] +pub struct ModsIndices { + pub shift: Option, + pub caps: Option, + pub ctrl: Option, + pub alt: Option, + pub num: Option, + pub mod3: Option, + pub logo: Option, + pub mod5: Option, +} + +fn mod_index_for_name(keymap: NonNull, name: &[u8]) -> Option { + unsafe { + let mod_index = + (XKBH.xkb_keymap_mod_get_index)(keymap.as_ptr(), name.as_ptr() as *const c_char); + if mod_index == XKB_MOD_INVALID { + None + } else { + Some(mod_index) + } + } +} diff --git a/src/platform_impl/linux/common/xkb/mod.rs b/src/platform_impl/linux/common/xkb/mod.rs new file mode 100644 index 0000000000..fe8bb93e72 --- /dev/null +++ b/src/platform_impl/linux/common/xkb/mod.rs @@ -0,0 +1,465 @@ +use std::ops::Deref; +use std::os::raw::c_char; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use log::warn; +use once_cell::sync::Lazy; +use smol_str::SmolStr; +#[cfg(wayland_platform)] +use std::os::unix::io::OwnedFd; +use xkbcommon_dl::{ + self as xkb, xkb_compose_status, xkb_context, xkb_context_flags, xkbcommon_compose_handle, + xkbcommon_handle, XkbCommon, XkbCommonCompose, +}; +#[cfg(x11_platform)] +use {x11_dl::xlib_xcb::xcb_connection_t, xkbcommon_dl::x11::xkbcommon_x11_handle}; + +use crate::event::ElementState; +use crate::event::KeyEvent; +use crate::keyboard::{Key, KeyLocation}; +use crate::platform_impl::KeyEventExtra; + +mod compose; +mod keymap; +mod state; + +use compose::{ComposeStatus, XkbComposeState, XkbComposeTable}; +use keymap::XkbKeymap; + +#[cfg(x11_platform)] +pub use keymap::raw_keycode_to_physicalkey; +pub use keymap::{physicalkey_to_scancode, scancode_to_keycode}; +pub use state::XkbState; + +// TODO: Wire this up without using a static `AtomicBool`. +static RESET_DEAD_KEYS: AtomicBool = AtomicBool::new(false); + +static XKBH: Lazy<&'static XkbCommon> = Lazy::new(xkbcommon_handle); +static XKBCH: Lazy<&'static XkbCommonCompose> = Lazy::new(xkbcommon_compose_handle); +#[cfg(feature = "x11")] +static XKBXH: Lazy<&'static xkb::x11::XkbCommonX11> = Lazy::new(xkbcommon_x11_handle); + +#[inline(always)] +pub fn reset_dead_keys() { + RESET_DEAD_KEYS.store(true, Ordering::SeqCst); +} + +#[derive(Debug)] +pub enum Error { + /// libxkbcommon is not available + XKBNotFound, +} + +#[derive(Debug)] +pub struct Context { + // NOTE: field order matters. + #[cfg(x11_platform)] + pub core_keyboard_id: i32, + state: Option, + keymap: Option, + compose_state1: Option, + compose_state2: Option, + _compose_table: Option, + context: XkbContext, + scratch_buffer: Vec, +} + +impl Context { + pub fn new() -> Result { + if xkb::xkbcommon_option().is_none() { + return Err(Error::XKBNotFound); + } + + let context = XkbContext::new()?; + let mut compose_table = XkbComposeTable::new(&context); + let mut compose_state1 = compose_table.as_ref().and_then(|table| table.new_state()); + let mut compose_state2 = compose_table.as_ref().and_then(|table| table.new_state()); + + // Disable compose if anything compose related failed to initialize. + if compose_table.is_none() || compose_state1.is_none() || compose_state2.is_none() { + compose_state2 = None; + compose_state1 = None; + compose_table = None; + } + + Ok(Self { + state: None, + keymap: None, + compose_state1, + compose_state2, + #[cfg(x11_platform)] + core_keyboard_id: 0, + _compose_table: compose_table, + context, + scratch_buffer: Vec::with_capacity(8), + }) + } + + #[cfg(feature = "x11")] + pub fn from_x11_xkb(xcb: *mut xcb_connection_t) -> Result { + let result = unsafe { + (XKBXH.xkb_x11_setup_xkb_extension)( + xcb, + 1, + 2, + xkbcommon_dl::x11::xkb_x11_setup_xkb_extension_flags::XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if result != 1 { + return Err(Error::XKBNotFound); + } + + let mut this = Self::new()?; + this.core_keyboard_id = unsafe { (XKBXH.xkb_x11_get_core_keyboard_device_id)(xcb) }; + this.set_keymap_from_x11(xcb); + Ok(this) + } + + pub fn state_mut(&mut self) -> Option<&mut XkbState> { + self.state.as_mut() + } + + pub fn keymap_mut(&mut self) -> Option<&mut XkbKeymap> { + self.keymap.as_mut() + } + + #[cfg(wayland_platform)] + pub fn set_keymap_from_fd(&mut self, fd: OwnedFd, size: usize) { + let keymap = XkbKeymap::from_fd(&self.context, fd, size); + let state = keymap.as_ref().and_then(XkbState::new_wayland); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + #[cfg(x11_platform)] + pub fn set_keymap_from_x11(&mut self, xcb: *mut xcb_connection_t) { + let keymap = XkbKeymap::from_x11_keymap(&self.context, xcb, self.core_keyboard_id); + let state = keymap + .as_ref() + .and_then(|keymap| XkbState::new_x11(xcb, keymap)); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + /// Key builder context with the user provided xkb state. + pub fn key_context(&mut self) -> Option> { + let state = self.state.as_mut()?; + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { + state, + keymap, + compose_state1, + compose_state2, + scratch_buffer, + }) + } + + /// Key builder context with the user provided xkb state. + /// + /// Should be used when the original context must not be altered. + #[cfg(x11_platform)] + pub fn key_context_with_state<'a>( + &'a mut self, + state: &'a mut XkbState, + ) -> Option> { + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { + state, + keymap, + compose_state1, + compose_state2, + scratch_buffer, + }) + } +} + +pub struct KeyContext<'a> { + pub state: &'a mut XkbState, + pub keymap: &'a mut XkbKeymap, + compose_state1: Option<&'a mut XkbComposeState>, + compose_state2: Option<&'a mut XkbComposeState>, + scratch_buffer: &'a mut Vec, +} + +impl<'a> KeyContext<'a> { + pub fn process_key_event( + &mut self, + keycode: u32, + state: ElementState, + repeat: bool, + ) -> KeyEvent { + let mut event = + KeyEventResults::new(self, keycode, !repeat && state == ElementState::Pressed); + let physical_key = keymap::raw_keycode_to_physicalkey(keycode); + let (logical_key, location) = event.key(); + let text = event.text(); + let (key_without_modifiers, _) = event.key_without_modifiers(); + let text_with_all_modifiers = event.text_with_all_modifiers(); + + let platform_specific = KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }; + + KeyEvent { + physical_key, + logical_key, + text, + location, + state, + repeat, + platform_specific, + } + } + + fn keysym_to_utf8_raw(&mut self, keysym: u32) -> Option { + self.scratch_buffer.clear(); + self.scratch_buffer.reserve(8); + loop { + let bytes_written = unsafe { + (XKBH.xkb_keysym_to_utf8)( + keysym, + self.scratch_buffer.as_mut_ptr().cast(), + self.scratch_buffer.capacity(), + ) + }; + if bytes_written == 0 { + return None; + } else if bytes_written == -1 { + self.scratch_buffer.reserve(8); + } else { + unsafe { + self.scratch_buffer + .set_len(bytes_written.try_into().unwrap()) + }; + break; + } + } + + // Remove the null-terminator + self.scratch_buffer.pop(); + byte_slice_to_smol_str(self.scratch_buffer) + } +} + +struct KeyEventResults<'a, 'b> { + context: &'a mut KeyContext<'b>, + keycode: u32, + keysym: u32, + compose: ComposeStatus, +} + +impl<'a, 'b> KeyEventResults<'a, 'b> { + fn new(context: &'a mut KeyContext<'b>, keycode: u32, compose: bool) -> Self { + let keysym = context.state.get_one_sym_raw(keycode); + + let compose = if let Some(state) = context.compose_state1.as_mut().filter(|_| compose) { + if RESET_DEAD_KEYS.swap(false, Ordering::SeqCst) { + state.reset(); + context.compose_state2.as_mut().unwrap().reset(); + } + state.feed(keysym) + } else { + ComposeStatus::None + }; + + KeyEventResults { + context, + keycode, + keysym, + compose, + } + } + + pub fn key(&mut self) -> (Key, KeyLocation) { + let (key, location) = match self.keysym_to_key(self.keysym) { + Ok(known) => return known, + Err(undefined) => undefined, + }; + + if let ComposeStatus::Accepted(xkb_compose_status::XKB_COMPOSE_COMPOSING) = self.compose { + let compose_state = self.context.compose_state2.as_mut().unwrap(); + // When pressing a dead key twice, the non-combining variant of that character will + // be produced. Since this function only concerns itself with a single keypress, we + // simulate this double press here by feeding the keysym to the compose state + // twice. + + compose_state.feed(self.keysym); + if matches!(compose_state.feed(self.keysym), ComposeStatus::Accepted(_)) { + // Extracting only a single `char` here *should* be fine, assuming that no + // dead key's non-combining variant ever occupies more than one `char`. + let text = compose_state.get_string(self.context.scratch_buffer); + let key = Key::Dead(text.and_then(|s| s.chars().next())); + (key, location) + } else { + (key, location) + } + } else { + let key = self + .composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + + pub fn key_without_modifiers(&mut self) -> (Key, KeyLocation) { + // This will become a pointer to an array which libxkbcommon owns, so we don't need to deallocate it. + let layout = self.context.state.layout(self.keycode); + let keysym = self + .context + .keymap + .first_keysym_by_level(layout, self.keycode); + + match self.keysym_to_key(keysym) { + Ok((key, location)) => (key, location), + Err((key, location)) => { + let key = self + .context + .keysym_to_utf8_raw(keysym) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + } + + fn keysym_to_key(&self, keysym: u32) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { + let location = keymap::keysym_location(keysym); + let key = keymap::keysym_to_key(keysym); + if matches!(key, Key::Unidentified(_)) { + Err((key, location)) + } else { + Ok((key, location)) + } + } + + pub fn text(&mut self) -> Option { + self.composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + } + + // The current behaviour makes it so composing a character overrides attempts to input a + // control character with the `Ctrl` key. We can potentially add a configuration option + // if someone specifically wants the oppsite behaviour. + pub fn text_with_all_modifiers(&mut self) -> Option { + match self.composed_text() { + Ok(text) => text, + Err(_) => self + .context + .state + .get_utf8_raw(self.keycode, self.context.scratch_buffer), + } + } + + fn composed_text(&mut self) -> Result, ()> { + match self.compose { + ComposeStatus::Accepted(status) => match status { + xkb_compose_status::XKB_COMPOSE_COMPOSED => { + let state = self.context.compose_state1.as_mut().unwrap(); + Ok(state.get_string(self.context.scratch_buffer)) + } + xkb_compose_status::XKB_COMPOSE_COMPOSING + | xkb_compose_status::XKB_COMPOSE_CANCELLED => Ok(None), + xkb_compose_status::XKB_COMPOSE_NOTHING => Err(()), + }, + _ => Err(()), + } + } +} + +#[derive(Debug)] +pub struct XkbContext { + context: NonNull, +} + +impl XkbContext { + pub fn new() -> Result { + let context = unsafe { (XKBH.xkb_context_new)(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + + let context = match NonNull::new(context) { + Some(context) => context, + None => return Err(Error::XKBNotFound), + }; + + Ok(Self { context }) + } +} + +impl Drop for XkbContext { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_context_unref)(self.context.as_ptr()); + } + } +} + +impl Deref for XkbContext { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.context + } +} + +/// Shared logic for constructing a string with `xkb_compose_state_get_utf8` and +/// `xkb_state_key_get_utf8`. +fn make_string_with(scratch_buffer: &mut Vec, mut f: F) -> Option +where + F: FnMut(*mut c_char, usize) -> i32, +{ + let size = f(ptr::null_mut(), 0); + if size == 0 { + return None; + } + let size = usize::try_from(size).unwrap(); + scratch_buffer.clear(); + // The allocated buffer must include space for the null-terminator. + scratch_buffer.reserve(size + 1); + unsafe { + let written = f( + scratch_buffer.as_mut_ptr().cast(), + scratch_buffer.capacity(), + ); + if usize::try_from(written).unwrap() != size { + // This will likely never happen. + return None; + } + scratch_buffer.set_len(size); + }; + + byte_slice_to_smol_str(scratch_buffer) +} + +// NOTE: This is track_caller so we can have more informative line numbers when logging +#[track_caller] +fn byte_slice_to_smol_str(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .map(SmolStr::new) + .map_err(|e| { + log::warn!( + "UTF-8 received from libxkbcommon ({:?}) was invalid: {e}", + bytes + ) + }) + .ok() +} diff --git a/src/platform_impl/linux/common/xkb/state.rs b/src/platform_impl/linux/common/xkb/state.rs new file mode 100644 index 0000000000..27c055aa20 --- /dev/null +++ b/src/platform_impl/linux/common/xkb/state.rs @@ -0,0 +1,189 @@ +//! XKB state. + +use std::os::raw::c_char; +use std::ptr::NonNull; + +use smol_str::SmolStr; +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keysym_t, xkb_layout_index_t, xkb_state, xkb_state_component, +}; + +use crate::platform_impl::common::xkb::keymap::XkbKeymap; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{make_string_with, XKBH}; + +#[derive(Debug)] +pub struct XkbState { + state: NonNull, + modifiers: ModifiersState, +} + +impl XkbState { + #[cfg(wayland_platform)] + pub fn new_wayland(keymap: &XkbKeymap) -> Option { + let state = NonNull::new(unsafe { (XKBH.xkb_state_new)(keymap.as_ptr()) })?; + Some(Self::new_inner(state)) + } + + #[cfg(x11_platform)] + pub fn new_x11(xcb: *mut xcb_connection_t, keymap: &XkbKeymap) -> Option { + let state = unsafe { + (XKBXH.xkb_x11_state_new_from_device)(keymap.as_ptr(), xcb, keymap._core_keyboard_id) + }; + let state = NonNull::new(state)?; + Some(Self::new_inner(state)) + } + + fn new_inner(state: NonNull) -> Self { + let modifiers = ModifiersState::default(); + let mut this = Self { state, modifiers }; + this.reload_modifiers(); + this + } + + pub fn get_one_sym_raw(&mut self, keycode: xkb_keycode_t) -> xkb_keysym_t { + unsafe { (XKBH.xkb_state_key_get_one_sym)(self.state.as_ptr(), keycode) } + } + + pub fn layout(&mut self, key: xkb_keycode_t) -> xkb_layout_index_t { + unsafe { (XKBH.xkb_state_key_get_layout)(self.state.as_ptr(), key) } + } + + #[cfg(x11_platform)] + pub fn depressed_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_DEPRESSED, + ) + } + } + + #[cfg(x11_platform)] + pub fn latched_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LATCHED, + ) + } + } + + #[cfg(x11_platform)] + pub fn locked_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LOCKED, + ) + } + } + + pub fn get_utf8_raw( + &mut self, + keycode: xkb_keycode_t, + scratch_buffer: &mut Vec, + ) -> Option { + make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBH.xkb_state_key_get_utf8)(self.state.as_ptr(), keycode, ptr, len) + }) + } + + pub fn modifiers(&self) -> ModifiersState { + self.modifiers + } + + pub fn update_modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + depressed_group: u32, + latched_group: u32, + locked_group: u32, + ) { + let mask = unsafe { + (XKBH.xkb_state_update_mask)( + self.state.as_ptr(), + mods_depressed, + mods_latched, + mods_locked, + depressed_group, + latched_group, + locked_group, + ) + }; + + if mask.contains(xkb_state_component::XKB_STATE_MODS_EFFECTIVE) { + // Effective value of mods have changed, we need to update our state. + self.reload_modifiers(); + } + } + + /// Reload the modifiers. + fn reload_modifiers(&mut self) { + self.modifiers.ctrl = self.mod_name_is_active(xkb::XKB_MOD_NAME_CTRL); + self.modifiers.alt = self.mod_name_is_active(xkb::XKB_MOD_NAME_ALT); + self.modifiers.shift = self.mod_name_is_active(xkb::XKB_MOD_NAME_SHIFT); + self.modifiers.caps_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_CAPS); + self.modifiers.logo = self.mod_name_is_active(xkb::XKB_MOD_NAME_LOGO); + self.modifiers.num_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_NUM); + } + + /// Check if the modifier is active within xkb. + fn mod_name_is_active(&mut self, name: &[u8]) -> bool { + unsafe { + (XKBH.xkb_state_mod_name_is_active)( + self.state.as_ptr(), + name.as_ptr() as *const c_char, + xkb_state_component::XKB_STATE_MODS_EFFECTIVE, + ) > 0 + } + } +} + +impl Drop for XkbState { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_state_unref)(self.state.as_ptr()); + } + } +} + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl From for crate::keyboard::ModifiersState { + fn from(mods: ModifiersState) -> crate::keyboard::ModifiersState { + let mut to_mods = crate::keyboard::ModifiersState::empty(); + to_mods.set(crate::keyboard::ModifiersState::SHIFT, mods.shift); + to_mods.set(crate::keyboard::ModifiersState::CONTROL, mods.ctrl); + to_mods.set(crate::keyboard::ModifiersState::ALT, mods.alt); + to_mods.set(crate::keyboard::ModifiersState::SUPER, mods.logo); + to_mods + } +} diff --git a/src/platform_impl/linux/common/xkb_state.rs b/src/platform_impl/linux/common/xkb_state.rs index 541c186da5..48447c92e3 100644 --- a/src/platform_impl/linux/common/xkb_state.rs +++ b/src/platform_impl/linux/common/xkb_state.rs @@ -13,7 +13,7 @@ use xkbcommon_dl::{ XkbCommonCompose, }; #[cfg(feature = "wayland")] -use {memmap2::MmapOptions, wayland_backend::io_lifetimes::OwnedFd}; +use {memmap2::MmapOptions, std::os::unix::io::OwnedFd}; #[cfg(feature = "x11")] use {x11_dl::xlib_xcb::xcb_connection_t, xkbcommon_dl::x11::xkbcommon_x11_handle}; @@ -22,7 +22,7 @@ use crate::platform_impl::common::keymap; use crate::platform_impl::KeyEventExtra; use crate::{ event::ElementState, - keyboard::{Key, KeyCode, KeyLocation}, + keyboard::{Key, KeyLocation, PhysicalKey}, }; // TODO: Wire this up without using a static `AtomicBool`. @@ -250,37 +250,43 @@ impl KbdState { .unwrap_or_else(|| "C".into()); let locale = CString::new(locale.into_vec()).unwrap(); - let compose_table = (XKBCH.xkb_compose_table_new_from_locale)( - self.xkb_context, - locale.as_ptr(), - ffi::xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, - ); + let compose_table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + self.xkb_context, + locale.as_ptr(), + ffi::xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; if compose_table.is_null() { // init of compose table failed, continue without compose return; } - let compose_state = (XKBCH.xkb_compose_state_new)( - compose_table, - ffi::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, - ); + let compose_state = unsafe { + (XKBCH.xkb_compose_state_new)( + compose_table, + ffi::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; if compose_state.is_null() { // init of compose state failed, continue without compose - (XKBCH.xkb_compose_table_unref)(compose_table); + unsafe { (XKBCH.xkb_compose_table_unref)(compose_table) }; return; } - let compose_state_2 = (XKBCH.xkb_compose_state_new)( - compose_table, - ffi::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, - ); + let compose_state_2 = unsafe { + (XKBCH.xkb_compose_state_new)( + compose_table, + ffi::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; if compose_state_2.is_null() { // init of compose state failed, continue without compose - (XKBCH.xkb_compose_table_unref)(compose_table); - (XKBCH.xkb_compose_state_unref)(compose_state); + unsafe { (XKBCH.xkb_compose_table_unref)(compose_table) }; + unsafe { (XKBCH.xkb_compose_state_unref)(compose_state) }; return; } @@ -296,65 +302,73 @@ impl KbdState { } unsafe fn de_init(&mut self) { - (XKBH.xkb_state_unref)(self.xkb_state); + unsafe { (XKBH.xkb_state_unref)(self.xkb_state) }; self.xkb_state = ptr::null_mut(); - (XKBH.xkb_keymap_unref)(self.xkb_keymap); + unsafe { (XKBH.xkb_keymap_unref)(self.xkb_keymap) }; self.xkb_keymap = ptr::null_mut(); } #[cfg(feature = "x11")] pub unsafe fn init_with_x11_keymap(&mut self) { if !self.xkb_keymap.is_null() { - self.de_init(); + unsafe { self.de_init() }; } // TODO: Support keyboards other than the "virtual core keyboard device". - self.core_keyboard_id = (XKBXH.xkb_x11_get_core_keyboard_device_id)(self.xcb_connection); - let keymap = (XKBXH.xkb_x11_keymap_new_from_device)( - self.xkb_context, - self.xcb_connection, - self.core_keyboard_id, - xkbcommon_dl::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, - ); + self.core_keyboard_id = + unsafe { (XKBXH.xkb_x11_get_core_keyboard_device_id)(self.xcb_connection) }; + let keymap = unsafe { + (XKBXH.xkb_x11_keymap_new_from_device)( + self.xkb_context, + self.xcb_connection, + self.core_keyboard_id, + xkbcommon_dl::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; if keymap.is_null() { panic!("Failed to get keymap from X11 server."); } - let state = (XKBXH.xkb_x11_state_new_from_device)( - keymap, - self.xcb_connection, - self.core_keyboard_id, - ); - self.post_init(state, keymap); + let state = unsafe { + (XKBXH.xkb_x11_state_new_from_device)( + keymap, + self.xcb_connection, + self.core_keyboard_id, + ) + }; + unsafe { self.post_init(state, keymap) }; } #[cfg(feature = "wayland")] pub unsafe fn init_with_fd(&mut self, fd: OwnedFd, size: usize) { if !self.xkb_keymap.is_null() { - self.de_init(); + unsafe { self.de_init() }; } - let map = MmapOptions::new() - .len(size) - .map_copy_read_only(&fd) - .unwrap(); + let map = unsafe { + MmapOptions::new() + .len(size) + .map_copy_read_only(&fd) + .unwrap() + }; - let keymap = (XKBH.xkb_keymap_new_from_string)( - self.xkb_context, - map.as_ptr() as *const _, - ffi::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, - ffi::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, - ); + let keymap = unsafe { + (XKBH.xkb_keymap_new_from_string)( + self.xkb_context, + map.as_ptr() as *const _, + ffi::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, + ffi::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; if keymap.is_null() { panic!("Received invalid keymap from compositor."); } - let state = (XKBH.xkb_state_new)(keymap); - self.post_init(state, keymap); + let state = unsafe { (XKBH.xkb_state_new)(keymap) }; + unsafe { self.post_init(state, keymap) }; } - #[cfg(feature = "wayland")] pub fn key_repeats(&mut self, keycode: ffi::xkb_keycode_t) -> bool { unsafe { (XKBH.xkb_keymap_key_repeats)(self.xkb_keymap, keycode) == 1 } } @@ -377,7 +391,7 @@ impl KbdState { ) -> KeyEvent { let mut event = KeyEventResults::new(self, keycode, !repeat && state == ElementState::Pressed); - let physical_key = event.keycode(); + let physical_key = event.physical_key(); let (logical_key, location) = event.key(); let text = event.text(); let (key_without_modifiers, _) = event.key_without_modifiers(); @@ -484,8 +498,8 @@ impl<'a> KeyEventResults<'a> { } } - fn keycode(&mut self) -> KeyCode { - keymap::raw_keycode_to_keycode(self.keycode) + fn physical_key(&self) -> PhysicalKey { + keymap::raw_keycode_to_physicalkey(self.keycode) } pub fn key(&mut self) -> (Key, KeyLocation) { @@ -539,6 +553,7 @@ impl<'a> KeyEventResults<'a> { } else { 0 }; + self.keysym_to_key(keysym) .unwrap_or_else(|(key, location)| { ( @@ -551,7 +566,7 @@ impl<'a> KeyEventResults<'a> { }) } - fn keysym_to_key(&mut self, keysym: u32) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { + fn keysym_to_key(&self, keysym: u32) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { let location = super::keymap::keysym_location(keysym); let key = super::keymap::keysym_to_key(keysym); if matches!(key, Key::Unidentified(_)) { diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 5df360e844..321bc32dd3 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -3,45 +3,45 @@ #[cfg(all(not(x11_platform), not(wayland_platform)))] compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); -#[cfg(wayland_platform)] -use std::error::Error; - +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::Arc; +use std::time::Duration; use std::{collections::VecDeque, env, fmt}; #[cfg(x11_platform)] -use std::{ - ffi::CStr, - mem::MaybeUninit, - os::raw::*, - sync::{Arc, Mutex}, -}; +use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex}; #[cfg(x11_platform)] use once_cell::sync::Lazy; -use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use smol_str::SmolStr; -#[cfg(x11_platform)] -pub use self::x11::XNotSupported; -#[cfg(x11_platform)] -use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, XError}; #[cfg(x11_platform)] use crate::platform::x11::XlibErrorHook; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - event::{Event, KeyEvent}, - event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, + error::{EventLoopError, ExternalError, NotSupportedError, OsError as RootOsError}, + event::KeyEvent, + event_loop::{ + AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopClosed, + EventLoopWindowTarget as RootELW, + }, icon::Icon, - keyboard::{Key, KeyCode}, - platform::{modifier_supplement::KeyEventExtModifierSupplement, scancode::KeyCodeExtScancode}, + keyboard::{Key, PhysicalKey}, + platform::{ + modifier_supplement::KeyEventExtModifierSupplement, pump_events::PumpStatus, + scancode::PhysicalKeyExtScancode, + }, window::{ - CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, - WindowAttributes, WindowButtons, WindowLevel, + ActivationToken, CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, + UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, }, }; +#[cfg(x11_platform)] +pub use x11::XNotSupported; +#[cfg(x11_platform)] +use x11::{util::WindowType as XWindowType, X11Error, XConnection, XError}; pub(crate) use crate::icon::RgbaIcon as PlatformIcon; -pub(self) use crate::platform_impl::Fullscreen; +pub(crate) use crate::platform_impl::Fullscreen; pub mod common; #[cfg(wayland_platform)] @@ -49,15 +49,6 @@ pub mod wayland; #[cfg(x11_platform)] pub mod x11; -/// Environment variable specifying which backend should be used on unix platform. -/// -/// Legal values are x11 and wayland. If this variable is set only the named backend -/// will be tried by winit. 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. -const BACKEND_PREFERENCE_ENV_VAR: &str = "WINIT_UNIX_BACKEND"; - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) enum Backend { #[cfg(x11_platform)] @@ -87,32 +78,38 @@ impl ApplicationName { #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub name: Option, + pub activation_token: Option, #[cfg(x11_platform)] - pub visual_infos: Option, - #[cfg(x11_platform)] + pub x11: X11WindowBuilderAttributes, +} + +#[derive(Clone)] +#[cfg(x11_platform)] +pub struct X11WindowBuilderAttributes { + pub visual_id: Option, pub screen_id: Option, - #[cfg(x11_platform)] pub base_size: Option, - #[cfg(x11_platform)] pub override_redirect: bool, - #[cfg(x11_platform)] pub x11_window_types: Vec, + + /// The parent window to embed this window into. + pub embed_window: Option, } impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { name: None, + activation_token: None, #[cfg(x11_platform)] - visual_infos: None, - #[cfg(x11_platform)] - screen_id: None, - #[cfg(x11_platform)] - base_size: None, - #[cfg(x11_platform)] - override_redirect: false, - #[cfg(x11_platform)] - x11_window_types: vec![XWindowType::Normal], + x11: X11WindowBuilderAttributes { + visual_id: None, + screen_id: None, + base_size: None, + override_redirect: false, + x11_window_types: vec![XWindowType::Normal], + embed_window: None, + }, } } } @@ -123,23 +120,21 @@ pub(crate) static X11_BACKEND: Lazy, XNotSupported #[derive(Debug, Clone)] pub enum OsError { + Misc(&'static str), #[cfg(x11_platform)] - XError(XError), - #[cfg(x11_platform)] - XMisc(&'static str), + XError(Arc), #[cfg(wayland_platform)] - WaylandMisc(&'static str), + WaylandError(Arc), } impl fmt::Display for OsError { fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match *self { + OsError::Misc(e) => _f.pad(e), #[cfg(x11_platform)] - OsError::XError(ref e) => _f.pad(&e.description), - #[cfg(x11_platform)] - OsError::XMisc(e) => _f.pad(e), + OsError::XError(ref e) => fmt::Display::fmt(e, _f), #[cfg(wayland_platform)] - OsError::WaylandMisc(e) => _f.pad(e), + OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f), } } } @@ -183,9 +178,9 @@ pub enum DeviceId { impl DeviceId { pub const unsafe fn dummy() -> Self { #[cfg(wayland_platform)] - return DeviceId::Wayland(wayland::DeviceId::dummy()); + return DeviceId::Wayland(unsafe { wayland::DeviceId::dummy() }); #[cfg(all(not(wayland_platform), x11_platform))] - return DeviceId::X(x11::DeviceId::dummy()); + return DeviceId::X(unsafe { x11::DeviceId::dummy() }); } } @@ -288,7 +283,7 @@ impl VideoMode { #[inline] pub fn monitor(&self) -> MonitorHandle { - x11_or_wayland!(match self; VideoMode(m) => m.monitor()) + x11_or_wayland!(match self; VideoMode(m) => m.monitor(); as MonitorHandle) } } @@ -311,14 +306,17 @@ impl Window { } } + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + #[inline] pub fn id(&self) -> WindowId { - match self { - #[cfg(wayland_platform)] - Self::Wayland(window) => window.id(), - #[cfg(x11_platform)] - Self::X(window) => window.id(), - } + x11_or_wayland!(match self; Window(w) => w.id()) } #[inline] @@ -331,6 +329,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.set_transparent(transparent)); } + #[inline] + pub fn set_blur(&self, blur: bool) { + x11_or_wayland!(match self; Window(w) => w.set_blur(blur)); + } + #[inline] pub fn set_visible(&self, visible: bool) { x11_or_wayland!(match self; Window(w) => w.set_visible(visible)) @@ -367,8 +370,13 @@ impl Window { } #[inline] - pub fn set_inner_size(&self, size: Size) { - x11_or_wayland!(match self; Window(w) => w.set_inner_size(size)) + pub fn request_inner_size(&self, size: Size) -> Option> { + x11_or_wayland!(match self; Window(w) => w.request_inner_size(size)) + } + + #[inline] + pub(crate) fn request_activation_token(&self) -> Result { + x11_or_wayland!(match self; Window(w) => w.request_activation_token()) } #[inline] @@ -436,6 +444,11 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.drag_resize_window(direction)) } + #[inline] + pub fn show_window_menu(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.show_window_menu(position)) + } + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) @@ -492,23 +505,13 @@ impl Window { } #[inline] - pub fn set_window_level(&self, _level: WindowLevel) { - match self { - #[cfg(x11_platform)] - Window::X(ref w) => w.set_window_level(_level), - #[cfg(wayland_platform)] - Window::Wayland(_) => (), - } + pub fn set_window_level(&self, level: WindowLevel) { + x11_or_wayland!(match self; Window(w) => w.set_window_level(level)) } #[inline] - pub fn set_window_icon(&self, _window_icon: Option) { - match self { - #[cfg(x11_platform)] - Window::X(ref w) => w.set_window_icon(_window_icon), - #[cfg(wayland_platform)] - Window::Wayland(_) => (), - } + pub fn set_window_icon(&self, window_icon: Option) { + x11_or_wayland!(match self; Window(w) => w.set_window_icon(window_icon.map(|icon| icon.inner))) } #[inline] @@ -518,7 +521,7 @@ impl Window { #[inline] pub fn reset_dead_keys(&self) { - common::xkb_state::reset_dead_keys() + common::xkb::reset_dead_keys() } #[inline] @@ -533,20 +536,10 @@ impl Window { #[inline] pub fn focus_window(&self) { - match self { - #[cfg(x11_platform)] - Window::X(ref w) => w.focus_window(), - #[cfg(wayland_platform)] - Window::Wayland(_) => (), - } + x11_or_wayland!(match self; Window(w) => w.focus_window()) } pub fn request_user_attention(&self, request_type: Option) { - match self { - #[cfg(x11_platform)] - Window::X(ref w) => w.request_user_attention(request_type), - #[cfg(wayland_platform)] - Window::Wayland(ref w) => w.request_user_attention(request_type), - } + x11_or_wayland!(match self; Window(w) => w.request_user_attention(request_type)) } #[inline] @@ -554,20 +547,14 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.request_redraw()) } + #[inline] + pub fn pre_present_notify(&self) { + x11_or_wayland!(match self; Window(w) => w.pre_present_notify()) + } + #[inline] pub fn current_monitor(&self) -> Option { - match self { - #[cfg(x11_platform)] - Window::X(ref window) => { - let current_monitor = MonitorHandle::X(window.current_monitor()); - Some(current_monitor) - } - #[cfg(wayland_platform)] - Window::Wayland(ref window) => { - let current_monitor = MonitorHandle::Wayland(window.current_monitor()?); - Some(current_monitor) - } - } + Some(x11_or_wayland!(match self; Window(w) => w.current_monitor()?; as MonitorHandle)) } #[inline] @@ -590,25 +577,39 @@ impl Window { #[inline] pub fn primary_monitor(&self) -> Option { - match self { - #[cfg(x11_platform)] - Window::X(ref window) => { - let primary_monitor = MonitorHandle::X(window.primary_monitor()); - Some(primary_monitor) - } - #[cfg(wayland_platform)] - Window::Wayland(ref window) => window.primary_monitor(), - } + Some(x11_or_wayland!(match self; Window(w) => w.primary_monitor()?; as MonitorHandle)) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_04()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_05()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_05()) } + #[cfg(feature = "rwh_06")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - x11_or_wayland!(match self; Window(window) => window.raw_window_handle()) + pub fn raw_window_handle_rwh_06(&self) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_06()) } + #[cfg(feature = "rwh_06")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - x11_or_wayland!(match self; Window(window) => window.raw_display_handle()) + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_06()) } #[inline] @@ -621,6 +622,10 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.theme()) } + pub fn set_content_protected(&self, protected: bool) { + x11_or_wayland!(match self; Window(window) => window.set_content_protected(protected)) + } + #[inline] pub fn has_focus(&self) -> bool { x11_or_wayland!(match self; Window(window) => window.has_focus()) @@ -652,13 +657,13 @@ impl KeyEventExtModifierSupplement for KeyEvent { } } -impl KeyCodeExtScancode for KeyCode { - fn from_scancode(scancode: u32) -> KeyCode { - common::keymap::scancode_to_keycode(scancode) +impl PhysicalKeyExtScancode for PhysicalKey { + fn from_scancode(scancode: u32) -> PhysicalKey { + common::xkb::scancode_to_keycode(scancode) } fn to_scancode(self) -> Option { - common::keymap::keycode_to_scancode(self) + common::xkb::physicalkey_to_scancode(self) } } @@ -676,26 +681,31 @@ unsafe extern "C" fn x_error_callback( if let Ok(ref xconn) = *xconn_lock { // Call all the hooks. let mut error_handled = false; - for hook in XLIB_ERROR_HOOKS.lock().unwrap().iter() { + for hook in unsafe { XLIB_ERROR_HOOKS.lock() }.unwrap().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(); - (xconn.xlib.XGetErrorText)( - display, - (*event).error_code as c_int, - buf.as_mut_ptr() as *mut c_char, - buf.len() as c_int, - ); - let description = CStr::from_ptr(buf.as_ptr() as *const c_char).to_string_lossy(); - - let error = XError { - description: description.into_owned(), - error_code: (*event).error_code, - request_code: (*event).request_code, - minor_code: (*event).minor_code, + let mut buf: [MaybeUninit; 1024] = unsafe { MaybeUninit::uninit().assume_init() }; + unsafe { + (xconn.xlib.XGetErrorText)( + display, + (*event).error_code as c_int, + buf.as_mut_ptr() as *mut c_char, + buf.len() as c_int, + ) + }; + let description = + unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(); + + let error = unsafe { + XError { + description: description.into_owned(), + error_code: (*event).error_code, + request_code: (*event).request_code, + minor_code: (*event).minor_code, + } }; // Don't log error. @@ -730,7 +740,9 @@ impl Clone for EventLoopProxy { } impl EventLoop { - pub(crate) fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Self { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { if !attributes.any_thread && !is_main_thread() { panic!( "Initializing the event loop outside of the main thread is a significant \ @@ -740,73 +752,60 @@ impl EventLoop { ); } - #[cfg(x11_platform)] - if attributes.forced_backend == Some(Backend::X) { - // TODO: Propagate - return EventLoop::new_x11_any_thread().unwrap(); - } - - #[cfg(wayland_platform)] - if attributes.forced_backend == Some(Backend::Wayland) { - // TODO: Propagate - return EventLoop::new_wayland_any_thread().expect("failed to open Wayland connection"); - } - - if let Ok(env_var) = env::var(BACKEND_PREFERENCE_ENV_VAR) { - match env_var.as_str() { - "x11" => { - // TODO: propagate - #[cfg(x11_platform)] - return EventLoop::new_x11_any_thread() - .expect("Failed to initialize X11 backend"); - #[cfg(not(x11_platform))] - panic!("x11 feature is not enabled") - } - "wayland" => { - #[cfg(wayland_platform)] - return EventLoop::new_wayland_any_thread() - .expect("Failed to initialize Wayland backend"); - #[cfg(not(wayland_platform))] - panic!("wayland feature is not enabled"); - } - _ => panic!( - "Unknown environment variable value for {BACKEND_PREFERENCE_ENV_VAR}, try one of `x11`,`wayland`", - ), + // NOTE: Wayland first because of X11 could be present under Wayland as well. Empty + // variables are also treated as not set. + let backend = match ( + attributes.forced_backend, + env::var("WAYLAND_DISPLAY") + .ok() + .filter(|var| !var.is_empty()) + .or_else(|| env::var("WAYLAND_SOCKET").ok()) + .filter(|var| !var.is_empty()) + .is_some(), + env::var("DISPLAY") + .map(|var| !var.is_empty()) + .unwrap_or(false), + ) { + // User is forcing a backend. + (Some(backend), _, _) => backend, + // Wayland is present. + #[cfg(wayland_platform)] + (None, true, _) => Backend::Wayland, + // X11 is present. + #[cfg(x11_platform)] + (None, _, true) => Backend::X, + // No backend is present. + (_, wayland_display, x11_display) => { + let msg = if wayland_display && !cfg!(wayland_platform) { + "DISPLAY is not set; note: enable the `winit/wayland` feature to support Wayland" + } else if x11_display && !cfg!(x11_platform) { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET is set; note: enable the `winit/x11` feature to support X11" + } else { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET nor DISPLAY is set." + }; + return Err(EventLoopError::Os(os_error!(OsError::Misc(msg)))); } - } - - #[cfg(wayland_platform)] - let wayland_err = match EventLoop::new_wayland_any_thread() { - Ok(event_loop) => return event_loop, - Err(err) => err, }; - #[cfg(x11_platform)] - let x11_err = match EventLoop::new_x11_any_thread() { - Ok(event_loop) => return event_loop, - Err(err) => err, - }; - - #[cfg(not(wayland_platform))] - let wayland_err = "backend disabled"; - #[cfg(not(x11_platform))] - let x11_err = "backend disabled"; - - panic!( - "Failed to initialize any backend! Wayland status: {wayland_err:?} X11 status: {x11_err:?}", - ); + // Create the display based on the backend. + match backend { + #[cfg(wayland_platform)] + Backend::Wayland => EventLoop::new_wayland_any_thread().map_err(Into::into), + #[cfg(x11_platform)] + Backend::X => EventLoop::new_x11_any_thread().map_err(Into::into), + } } #[cfg(wayland_platform)] - fn new_wayland_any_thread() -> Result, Box> { + fn new_wayland_any_thread() -> Result, EventLoopError> { wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) } #[cfg(x11_platform)] - fn new_x11_any_thread() -> Result, XNotSupported> { + fn new_x11_any_thread() -> Result, EventLoopError> { let xconn = match X11_BACKEND.lock().unwrap().as_ref() { Ok(xconn) => xconn.clone(), - Err(err) => return Err(err.clone()), + Err(_) => return Err(EventLoopError::NotSupported(NotSupportedError::new())), }; Ok(EventLoop::X(x11::EventLoop::new(xconn))) @@ -816,18 +815,25 @@ impl EventLoop { x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy) } - pub fn run_return(&mut self, callback: F) -> i32 + pub fn run(mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + self.run_on_demand(callback) + } + + pub fn run_on_demand(&mut self, callback: F) -> Result<(), EventLoopError> where - F: FnMut(crate::event::Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(crate::event::Event, &RootELW), { - x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_return(callback)) + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_on_demand(callback)) } - pub fn run(self, callback: F) -> ! + pub fn pump_events(&mut self, timeout: Option, callback: F) -> PumpStatus where - F: 'static + FnMut(crate::event::Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(crate::event::Event, &RootELW), { - x11_or_wayland!(match self; EventLoop(evlp) => evlp.run(callback)) + x11_or_wayland!(match self; EventLoop(evlp) => evlp.pump_events(timeout, callback)) } pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget { @@ -835,6 +841,18 @@ impl EventLoop { } } +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_fd()) + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_raw_fd()) + } +} + impl EventLoopProxy { pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.send_event(event)) @@ -865,69 +883,84 @@ impl EventLoopWindowTarget { #[cfg(wayland_platform)] EventLoopWindowTarget::Wayland(ref evlp) => evlp .available_monitors() - .into_iter() .map(MonitorHandle::Wayland) .collect(), #[cfg(x11_platform)] - EventLoopWindowTarget::X(ref evlp) => evlp - .x_connection() - .available_monitors() - .into_iter() - .map(MonitorHandle::X) - .collect(), + EventLoopWindowTarget::X(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::X).collect() + } } } #[inline] pub fn primary_monitor(&self) -> Option { - match *self { - #[cfg(wayland_platform)] - EventLoopWindowTarget::Wayland(ref evlp) => evlp.primary_monitor(), - #[cfg(x11_platform)] - EventLoopWindowTarget::X(ref evlp) => { - let primary_monitor = MonitorHandle::X(evlp.x_connection().primary_monitor()); - Some(primary_monitor) - } - } + Some( + x11_or_wayland!(match self; EventLoopWindowTarget(evlp) => evlp.primary_monitor()?; as MonitorHandle), + ) } #[inline] - pub fn listen_device_events(&self, _allowed: DeviceEvents) { - match *self { - #[cfg(wayland_platform)] - EventLoopWindowTarget::Wayland(_) => (), - #[cfg(x11_platform)] - EventLoopWindowTarget::X(ref evlp) => evlp.set_listen_device_events(_allowed), - } + pub fn listen_device_events(&self, allowed: DeviceEvents) { + x11_or_wayland!(match self; Self(evlp) => evlp.listen_device_events(allowed)) } - pub fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { - x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle()) + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_05()) } -} -fn sticky_exit_callback( - evt: Event<'_, T>, - target: &RootELW, - control_flow: &mut ControlFlow, - callback: &mut F, -) where - F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), -{ - // make ControlFlow::ExitWithCode sticky by providing a dummy - // control flow reference if it is already ExitWithCode. - if let ControlFlow::ExitWithCode(code) = *control_flow { - callback(evt, target, &mut ControlFlow::ExitWithCode(code)) - } else { - callback(evt, target, control_flow) + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_06()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_control_flow(control_flow)) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + x11_or_wayland!(match self; Self(evlp) => evlp.control_flow()) + } + + pub(crate) fn clear_exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.clear_exit()) + } + + pub(crate) fn exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.exit()) } + + pub(crate) fn exiting(&self) -> bool { + x11_or_wayland!(match self; Self(evlp) => evlp.exiting()) + } + + #[allow(dead_code)] + fn set_exit_code(&self, code: i32) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_exit_code(code)) + } + + #[allow(dead_code)] + fn exit_code(&self) -> Option { + x11_or_wayland!(match self; Self(evlp) => evlp.exit_code()) + } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) } #[cfg(target_os = "linux")] fn is_main_thread() -> bool { - use libc::{c_long, getpid, syscall, SYS_gettid}; - - unsafe { syscall(SYS_gettid) == getpid() as c_long } + rustix::thread::gettid() == rustix::process::getpid() } #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index 6ddb457115..6a42547265 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -1,26 +1,29 @@ //! The event-loop routines. -use std::cell::RefCell; -use std::error::Error; +use std::cell::{Cell, RefCell}; use std::io::Result as IOResult; use std::marker::PhantomData; use std::mem; -use std::process; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::rc::Rc; use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; - -use sctk::reexports::calloop; +use sctk::reexports::calloop::Error as CalloopError; +use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::globals; -use sctk::reexports::client::{Connection, Proxy, QueueHandle, WaylandSource}; - -use crate::dpi::{LogicalSize, PhysicalSize}; -use crate::event::{Event, StartCause, WindowEvent}; -use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; -use crate::platform_impl::platform::sticky_exit_callback; -use crate::platform_impl::EventLoopWindowTarget as PlatformEventLoopWindowTarget; +use sctk::reexports::client::{Connection, QueueHandle}; + +use crate::dpi::LogicalSize; +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ + ControlFlow, DeviceEvents, EventLoopWindowTarget as RootEventLoopWindowTarget, +}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::min_timeout; +use crate::platform_impl::{EventLoopWindowTarget as PlatformEventLoopWindowTarget, OsError}; mod proxy; pub mod sink; @@ -29,12 +32,20 @@ pub use proxy::EventLoopProxy; use sink::EventSink; use super::state::{WindowCompositorUpdate, WinitState}; -use super::{DeviceId, WindowId}; +use super::window::state::FrameCallbackState; +use super::{logical_to_physical_rounded, DeviceId, WaylandError, WindowId}; type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; /// The Wayland event loop. pub struct EventLoop { + /// Has `run` or `run_on_demand` been called or a call to `pump_events` that starts the loop + loop_running: bool, + + buffer_sink: EventSink, + compositor_updates: Vec, + window_ids: Vec, + /// Sender of user events. user_events_sender: calloop::channel::Sender, @@ -59,61 +70,110 @@ pub struct EventLoop { } impl EventLoop { - pub fn new() -> Result, Box> { - let connection = Connection::connect_to_env()?; + pub fn new() -> Result, EventLoopError> { + macro_rules! map_err { + ($e:expr, $err:expr) => { + $e.map_err(|error| os_error!($err(error).into())) + }; + } - let (globals, mut event_queue) = globals::registry_queue_init(&connection)?; + let connection = map_err!(Connection::connect_to_env(), WaylandError::Connection)?; + + let (globals, mut event_queue) = map_err!( + globals::registry_queue_init(&connection), + WaylandError::Global + )?; let queue_handle = event_queue.handle(); - let event_loop = calloop::EventLoop::::try_new()?; + let event_loop = map_err!( + calloop::EventLoop::::try_new(), + WaylandError::Calloop + )?; - let mut winit_state = WinitState::new(&globals, &queue_handle, event_loop.handle())?; + let mut winit_state = WinitState::new(&globals, &queue_handle, event_loop.handle()) + .map_err(|error| os_error!(error))?; // NOTE: do a roundtrip after binding the globals to prevent potential // races with the server. - event_queue.roundtrip(&mut winit_state)?; + map_err!( + event_queue.roundtrip(&mut winit_state), + WaylandError::Dispatch + )?; // Register Wayland source. - let wayland_source = WaylandSource::new(event_queue)?; + let wayland_source = WaylandSource::new(connection.clone(), event_queue); let wayland_dispatcher = - calloop::Dispatcher::new(wayland_source, |_, queue, winit_state| { - queue.dispatch_pending(winit_state) + calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| { + let result = queue.dispatch_pending(winit_state); + if result.is_ok() + && (!winit_state.events_sink.is_empty() + || !winit_state.window_compositor_updates.is_empty()) + { + winit_state.dispatched_events = true; + } + result }); - event_loop - .handle() - .register_dispatcher(wayland_dispatcher.clone())?; + map_err!( + event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone()), + WaylandError::Calloop + )?; // Setup the user proxy. let pending_user_events = Rc::new(RefCell::new(Vec::new())); let pending_user_events_clone = pending_user_events.clone(); let (user_events_sender, user_events_channel) = calloop::channel::channel(); - event_loop + let result = event_loop .handle() - .insert_source(user_events_channel, move |event, _, _| { - if let calloop::channel::Event::Msg(msg) = event { - pending_user_events_clone.borrow_mut().push(msg); - } - })?; + .insert_source( + user_events_channel, + move |event, _, winit_state: &mut WinitState| { + if let calloop::channel::Event::Msg(msg) = event { + winit_state.dispatched_events = true; + pending_user_events_clone.borrow_mut().push(msg); + } + }, + ) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; // An event's loop awakener to wake up for window events from winit's windows. - let (event_loop_awakener, event_loop_awakener_source) = calloop::ping::make_ping()?; - event_loop + let (event_loop_awakener, event_loop_awakener_source) = map_err!( + calloop::ping::make_ping() + .map_err(|error| CalloopError::OtherError(Box::new(error).into())), + WaylandError::Calloop + )?; + + let result = event_loop .handle() - .insert_source(event_loop_awakener_source, move |_, _, _| { - // No extra handling is required, we just need to wake-up. - })?; + .insert_source( + event_loop_awakener_source, + move |_, _, winit_state: &mut WinitState| { + // Mark that we have something to dispatch. + winit_state.dispatched_events = true; + }, + ) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; let window_target = EventLoopWindowTarget { connection: connection.clone(), wayland_dispatcher: wayland_dispatcher.clone(), event_loop_awakener, queue_handle, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), state: RefCell::new(winit_state), _marker: PhantomData, }; let event_loop = Self { + loop_running: false, + compositor_updates: Vec::new(), + buffer_sink: EventSink::default(), + window_ids: Vec::new(), connection, wayland_dispatcher, user_events_sender, @@ -128,330 +188,375 @@ impl EventLoop { Ok(event_loop) } - pub fn run(mut self, callback: F) -> ! + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> where - F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow) + 'static, + F: FnMut(Event, &RootEventLoopWindowTarget), { - let exit_code = self.run_return(callback); - process::exit(exit_code); + if self.loop_running { + return Err(EventLoopError::AlreadyRunning); + } + + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + } + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + } + _ => { + continue; + } + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // compositor. + let _ = self.roundtrip().map_err(EventLoopError::Os); + + exit } - pub fn run_return(&mut self, mut callback: F) -> i32 + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus where - F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + F: FnMut(Event, &RootEventLoopWindowTarget), { - let mut control_flow = ControlFlow::Poll; - - // XXX preallocate certian structures to avoid allocating on each loop iteration. - let mut window_ids = Vec::::new(); - let mut compositor_updates = Vec::::new(); - let mut buffer_sink = EventSink::new(); - - callback( - Event::NewEvents(StartCause::Init), - &self.window_target, - &mut control_flow, - ); + if !self.loop_running { + self.loop_running = true; + + // Run the initial loop iteration. + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } - // XXX 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); - - // XXX We break on errors from dispatches, since if we've got protocol error - // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not - // really an option. Instead we inform that the event loop got destroyed. We may - // communicate an error that something was terminated, but winit doesn't provide us - // with an API to do that via some event. - // Still, we set the exit code to the error's OS error code, or to 1 if not possible. - let exit_code = loop { - // Flush the connection. - let _ = self.connection.flush(); - - // During the run of the user callback, some other code monitoring and reading the - // Wayland socket may have been run (mesa for example does this with vsync), if that - // is the case, some events may have been enqueued in our event queue. - // - // If some messages are there, the event loop needs to behave as if it was instantly - // woken up by messages arriving from the Wayland socket, to avoid delaying the - // dispatch of these events until we're woken up again. - let instant_wakeup = { - let mut wayland_source = self.wayland_dispatcher.as_source_mut(); - let queue = wayland_source.queue(); - let state = match &mut self.window_target.p { - PlatformEventLoopWindowTarget::Wayland(window_target) => { - window_target.state.get_mut() + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootEventLoopWindowTarget), + { + let cause = loop { + let start = Instant::now(); + + timeout = { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) } - #[cfg(x11_platform)] - _ => unreachable!(), }; + min_timeout(control_flow_timeout, timeout) + }; + + // NOTE Ideally we should flush as the last thing we do before polling + // to wait for events, and this should be done by the calloop + // WaylandSource but we currently need to flush writes manually. + // + // Checking for flush error is essential to perform an exit with error, since + // once we have a protocol error, we could get stuck retrying... + if self.connection.flush().is_err() { + self.set_exit_code(1); + return; + } - match queue.dispatch_pending(state) { - Ok(dispatched) => dispatched > 0, - Err(error) => { - error!("Error dispatching wayland queue: {}", error); - break 1; + if let Err(error) = self.loop_dispatch(timeout) { + // NOTE We exit on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not + // really an option. Instead we inform that the event loop got destroyed. We may + // communicate an error that something was terminated, but winit doesn't provide us + // with an API to do that via some event. + // Still, we set the exit code to the error's OS error code, or to 1 if not possible. + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + } + } else { + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + } } } }; - match control_flow { - ControlFlow::ExitWithCode(code) => break code, - ControlFlow::Poll => { - // Non-blocking dispatch. - let timeout = Duration::ZERO; - if let Err(error) = self.loop_dispatch(Some(timeout)) { - break error.raw_os_error().unwrap_or(1); - } + // Reduce spurious wake-ups. + let dispatched_events = self.with_state(|state| state.dispatched_events); + if matches!(cause, StartCause::WaitCancelled { .. }) && !dispatched_events { + continue; + } - callback( - Event::NewEvents(StartCause::Poll), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::Wait => { - let timeout = if instant_wakeup { - Some(Duration::ZERO) - } else { - None - }; + break cause; + }; - if let Err(error) = self.loop_dispatch(timeout) { - break error.raw_os_error().unwrap_or(1); - } + self.single_iteration(&mut callback, cause); + } - callback( - Event::NewEvents(StartCause::WaitCancelled { - start: Instant::now(), - requested_resume: None, - }), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::WaitUntil(deadline) => { - let start = Instant::now(); + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootEventLoopWindowTarget), + { + // NOTE currently just indented to simplify the diff - // Compute the amount of time we'll block for. - let duration = if deadline > start && !instant_wakeup { - deadline - start - } else { - Duration::ZERO - }; + // We retain these grow-only scratch buffers as part of the EventLoop + // for the sake of avoiding lots of reallocs. We take them here to avoid + // trying to mutably borrow `self` more than once and we swap them back + // when finished. + let mut compositor_updates = std::mem::take(&mut self.compositor_updates); + let mut buffer_sink = std::mem::take(&mut self.buffer_sink); + let mut window_ids = std::mem::take(&mut self.window_ids); - if let Err(error) = self.loop_dispatch(Some(duration)) { - break error.raw_os_error().unwrap_or(1); - } + callback(Event::NewEvents(cause), &self.window_target); - let now = Instant::now(); - - if now < deadline { - callback( - Event::NewEvents(StartCause::WaitCancelled { - start, - requested_resume: Some(deadline), - }), - &self.window_target, - &mut control_flow, - ) - } else { - callback( - Event::NewEvents(StartCause::ResumeTimeReached { - start, - requested_resume: deadline, - }), - &self.window_target, - &mut control_flow, - ) - } - } - } + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.window_target); + } + + // Handle pending user events. We don't need back buffer, since we can't dispatch + // user events indirectly via callback to the user. + for user_event in self.pending_user_events.borrow_mut().drain(..) { + callback(Event::UserEvent(user_event), &self.window_target); + } + + // Drain the pending compositor updates. + self.with_state(|state| compositor_updates.append(&mut state.window_compositor_updates)); + + for mut compositor_update in compositor_updates.drain(..) { + let window_id = compositor_update.window_id; + if compositor_update.scale_changed { + let (physical_size, scale_factor) = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + (size, scale_factor) + }); - // Handle pending user events. We don't need back buffer, since we can't dispatch - // user events indirectly via callback to the user. - for user_event in self.pending_user_events.borrow_mut().drain(..) { - sticky_exit_callback( - Event::UserEvent(user_event), + // Stash the old window size. + let old_physical_size = physical_size; + + let new_inner_size = Arc::new(Mutex::new(physical_size)); + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }, &self.window_target, - &mut control_flow, - &mut callback, ); - } - // Drain the pending compositor updates. - self.with_state(|state| { - compositor_updates.append(&mut state.window_compositor_updates) - }); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); - for mut compositor_update in compositor_updates.drain(..) { - let window_id = compositor_update.window_id; - if let Some(scale_factor) = compositor_update.scale_factor { - let mut physical_size = self.with_state(|state| { + // Resize the window when user altered the size. + if old_physical_size != physical_size { + self.with_state(|state| { let windows = state.windows.get_mut(); let mut window = windows.get(&window_id).unwrap().lock().unwrap(); - // Set the new scale factor. - window.set_scale_factor(scale_factor); - let window_size = compositor_update.size.unwrap_or(window.inner_size()); - logical_to_physical_rounded(window_size, scale_factor) + let new_logical_size: LogicalSize = + physical_size.to_logical(scale_factor); + window.request_inner_size(new_logical_size.into()); }); - // Stash the old window size. - let old_physical_size = physical_size; - - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(window_id), - event: WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size: &mut physical_size, - }, - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); - - let new_logical_size = physical_size.to_logical(scale_factor); - - // Resize the window when user altered the size. - if old_physical_size != physical_size { - self.with_state(|state| { - let windows = state.windows.get_mut(); - let mut window = windows.get(&window_id).unwrap().lock().unwrap(); - window.resize(new_logical_size); - }); - } - // Make it queue resize. - compositor_update.size = Some(new_logical_size); + compositor_update.resized = true; } + } - if let Some(size) = compositor_update.size.take() { - let physical_size = self.with_state(|state| { - let windows = state.windows.get_mut(); - let window = windows.get(&window_id).unwrap().lock().unwrap(); - - let scale_factor = window.scale_factor(); - let physical_size = logical_to_physical_rounded(size, scale_factor); - - // TODO could probably bring back size reporting optimization. - - // Mark the window as needed a redraw. - state - .window_requests - .get_mut() - .get_mut(&window_id) - .unwrap() - .redraw_requested - .store(true, Ordering::Relaxed); + // NOTE: Rescale changed the physical size which winit operates in, thus we should + // resize. + if compositor_update.resized || compositor_update.scale_changed { + let physical_size = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + + // Mark the window as needed a redraw. + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + size + }); - physical_size - }); + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + ); + } - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(window_id), - event: WindowEvent::Resized(physical_size), - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); + if compositor_update.close_window { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + ); + } + } + + // Push the events directly from the window. + self.with_state(|state| { + buffer_sink.append(&mut state.window_events_sink.lock().unwrap()); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Handle non-synthetic events. + self.with_state(|state| { + buffer_sink.append(&mut state.events_sink); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Collect the window ids + self.with_state(|state| { + window_ids.extend(state.window_requests.get_mut().keys()); + }); + + for window_id in window_ids.iter() { + let event = self.with_state(|state| { + let window_requests = state.window_requests.get_mut(); + if window_requests.get(window_id).unwrap().take_closed() { + mem::drop(window_requests.remove(window_id)); + mem::drop(state.windows.get_mut().remove(window_id)); + return Some(WindowEvent::Destroyed); } - if compositor_update.close_window { - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(window_id), - event: WindowEvent::CloseRequested, - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); + let mut window = state + .windows + .get_mut() + .get_mut(window_id) + .unwrap() + .lock() + .unwrap(); + + if window.frame_callback_state() == FrameCallbackState::Requested { + return None; } - } - // Push the events directly from the window. - self.with_state(|state| { - buffer_sink.append(&mut state.window_events_sink.lock().unwrap()); - }); - for event in buffer_sink.drain() { - let event = event.map_nonuser_event().unwrap(); - sticky_exit_callback(event, &self.window_target, &mut control_flow, &mut callback); - } + // Reset the frame callbacks state. + window.frame_callback_reset(); + let mut redraw_requested = window_requests + .get(window_id) + .unwrap() + .take_redraw_requested(); - // Handle non-synthetic events. - self.with_state(|state| { - buffer_sink.append(&mut state.events_sink); - }); - for event in buffer_sink.drain() { - let event = event.map_nonuser_event().unwrap(); - sticky_exit_callback(event, &self.window_target, &mut control_flow, &mut callback); - } + // Redraw the frame while at it. + redraw_requested |= window.refresh_frame(); - // Send events cleared. - sticky_exit_callback( - Event::MainEventsCleared, - &self.window_target, - &mut control_flow, - &mut callback, - ); - - // Collect the window ids - self.with_state(|state| { - window_ids.extend(state.window_requests.get_mut().keys()); + redraw_requested.then_some(WindowEvent::RedrawRequested) }); - for window_id in window_ids.drain(..) { - let request_redraw = self.with_state(|state| { - let window_requests = state.window_requests.get_mut(); - if window_requests.get(&window_id).unwrap().take_closed() { - mem::drop(window_requests.remove(&window_id)); - mem::drop(state.windows.get_mut().remove(&window_id)); - false - } else { - let mut redraw_requested = window_requests - .get(&window_id) - .unwrap() - .take_redraw_requested(); - - // Redraw the frames while at it. - redraw_requested |= state - .windows + if let Some(event) = event { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(*window_id), + event, + }, + &self.window_target, + ); + } + } + + // Reset the hint that we've dispatched events. + self.with_state(|state| { + state.dispatched_events = false; + }); + + // This is always the last event we dispatch before poll again + callback(Event::AboutToWait, &self.window_target); + + // Update the window frames and schedule redraws. + let mut wake_up = false; + for window_id in window_ids.drain(..) { + wake_up |= self.with_state(|state| match state.windows.get_mut().get_mut(&window_id) { + Some(window) => { + let refresh = window.lock().unwrap().refresh_frame(); + if refresh { + state + .window_requests .get_mut() .get_mut(&window_id) .unwrap() - .lock() - .unwrap() - .refresh_frame(); - - redraw_requested + .redraw_requested + .store(true, Ordering::Relaxed); } - }); - if request_redraw { - sticky_exit_callback( - Event::RedrawRequested(crate::window::WindowId(window_id)), - &self.window_target, - &mut control_flow, - &mut callback, - ); + refresh } + None => false, + }); + } + + // Wakeup event loop if needed. + // + // If the user draws from the `AboutToWait` this is likely not required, however + // we can't do much about it. + if wake_up { + match &self.window_target.p { + PlatformEventLoopWindowTarget::Wayland(window_target) => { + window_target.event_loop_awakener.ping(); + } + #[cfg(x11_platform)] + PlatformEventLoopWindowTarget::X(_) => unreachable!(), } + } - // Send RedrawEventCleared. - sticky_exit_callback( - Event::RedrawEventsCleared, - &self.window_target, - &mut control_flow, - &mut callback, - ); - }; - - callback(Event::LoopDestroyed, &self.window_target, &mut control_flow); - exit_code + std::mem::swap(&mut self.compositor_updates, &mut compositor_updates); + std::mem::swap(&mut self.buffer_sink, &mut buffer_sink); + std::mem::swap(&mut self.window_ids, &mut window_ids); } #[inline] @@ -486,6 +591,50 @@ impl EventLoop { error.into() }) } + + fn roundtrip(&mut self) -> Result { + let state = match &mut self.window_target.p { + PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + let mut wayland_source = self.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + event_queue.roundtrip(state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) + }) + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } + + fn set_exit_code(&self, code: i32) { + self.window_target.p.set_exit_code(code) + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } } pub struct EventLoopWindowTarget { @@ -495,6 +644,12 @@ pub struct EventLoopWindowTarget { /// The main queue used by the event loop. pub queue_handle: QueueHandle, + /// The application's latest control_flow state + pub(crate) control_flow: Cell, + + /// The application's exit state. + pub(crate) exit: Cell>, + // TODO remove that RefCell once we can pass `&mut` in `Window::new`. /// Winit state. pub state: RefCell, @@ -509,16 +664,58 @@ pub struct EventLoopWindowTarget { } impl EventLoopWindowTarget { - pub fn raw_display_handle(&self) -> RawDisplayHandle { - let mut display_handle = WaylandDisplayHandle::empty(); + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + use sctk::reexports::client::Proxy; + + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); display_handle.display = self.connection.display().id().as_ptr() as *mut _; - RawDisplayHandle::Wayland(display_handle) + rwh_05::RawDisplayHandle::Wayland(display_handle) } -} -// The default routine does floor, but we need round on Wayland. -fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { - let width = size.width as f64 * scale_factor; - let height = size.height as f64 * scale_factor; - (width.round(), height.round()).into() + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.connection.display().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_display should never be null") + }) + .into()) + } } diff --git a/src/platform_impl/linux/wayland/event_loop/sink.rs b/src/platform_impl/linux/wayland/event_loop/sink.rs index 0bf2ae5833..13daeee357 100644 --- a/src/platform_impl/linux/wayland/event_loop/sink.rs +++ b/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -12,7 +12,7 @@ use super::{DeviceId, WindowId}; /// to the winit's user. #[derive(Default)] pub struct EventSink { - pub window_events: Vec>, + pub window_events: Vec>, } impl EventSink { @@ -20,6 +20,12 @@ impl EventSink { Default::default() } + /// Return `true` if there're pending events. + #[inline] + pub fn is_empty(&self) -> bool { + self.window_events.is_empty() + } + /// Add new device event to a queue. #[inline] pub fn push_device_event(&mut self, event: DeviceEvent, device_id: DeviceId) { @@ -31,7 +37,7 @@ impl EventSink { /// Add new window event to a queue. #[inline] - pub fn push_window_event(&mut self, event: WindowEvent<'static>, window_id: WindowId) { + pub fn push_window_event(&mut self, event: WindowEvent, window_id: WindowId) { self.window_events.push(Event::WindowEvent { event, window_id: RootWindowId(window_id), @@ -44,7 +50,7 @@ impl EventSink { } #[inline] - pub fn drain(&mut self) -> Drain<'_, Event<'static, ()>> { + pub fn drain(&mut self) -> Drain<'_, Event<()>> { self.window_events.drain(..) } } diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index 3643982164..44c6d2e3f4 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -2,10 +2,15 @@ //! Winit's Wayland backend. +use std::fmt::Display; +use std::sync::Arc; + +use sctk::reexports::client::globals::{BindError, GlobalError}; use sctk::reexports::client::protocol::wl_surface::WlSurface; -use sctk::reexports::client::Proxy; +use sctk::reexports::client::{self, ConnectError, DispatchError, Proxy}; -pub use crate::platform_impl::platform::WindowId; +use crate::dpi::{LogicalSize, PhysicalSize}; +pub use crate::platform_impl::platform::{OsError, WindowId}; pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; pub use output::{MonitorHandle, VideoMode}; pub use window::Window; @@ -17,6 +22,46 @@ mod state; mod types; mod window; +#[derive(Debug)] +pub enum WaylandError { + /// Error connecting to the socket. + Connection(ConnectError), + + /// Error binding the global. + Global(GlobalError), + + // Bind error. + Bind(BindError), + + /// Error during the dispatching the event queue. + Dispatch(DispatchError), + + /// Calloop error. + Calloop(calloop::Error), + + /// Wayland + Wire(client::backend::WaylandError), +} + +impl Display for WaylandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WaylandError::Connection(error) => error.fmt(f), + WaylandError::Global(error) => error.fmt(f), + WaylandError::Bind(error) => error.fmt(f), + WaylandError::Dispatch(error) => error.fmt(f), + WaylandError::Calloop(error) => error.fmt(f), + WaylandError::Wire(error) => error.fmt(f), + } + } +} + +impl From for OsError { + fn from(value: WaylandError) -> Self { + Self::WaylandError(Arc::new(value)) + } +} + /// Dummy device id, since Wayland doesn't have device events. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId; @@ -32,3 +77,10 @@ impl DeviceId { fn make_wid(surface: &WlSurface) -> WindowId { WindowId(surface.id().as_ptr() as u64) } + +/// The default routine does floor, but we need round on Wayland. +fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { + let width = size.width as f64 * scale_factor; + let height = size.height as f64 * scale_factor; + (width.round(), height.round()).into() +} diff --git a/src/platform_impl/linux/wayland/output.rs b/src/platform_impl/linux/wayland/output.rs index a7878511dd..d6fe61bc78 100644 --- a/src/platform_impl/linux/wayland/output.rs +++ b/src/platform_impl/linux/wayland/output.rs @@ -3,26 +3,23 @@ use sctk::reexports::client::Proxy; use sctk::output::OutputData; -use crate::dpi::{PhysicalPosition, PhysicalSize}; -use crate::platform_impl::platform::{ - MonitorHandle as PlatformMonitorHandle, VideoMode as PlatformVideoMode, -}; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::platform_impl::platform::VideoMode as PlatformVideoMode; use super::event_loop::EventLoopWindowTarget; impl EventLoopWindowTarget { #[inline] - pub fn available_monitors(&self) -> Vec { + pub fn available_monitors(&self) -> impl Iterator { self.state .borrow() .output_state .outputs() .map(MonitorHandle::new) - .collect() } #[inline] - pub fn primary_monitor(&self) -> Option { + pub fn primary_monitor(&self) -> Option { // There's no primary monitor on Wayland. None } @@ -70,7 +67,18 @@ impl MonitorHandle { #[inline] pub fn position(&self) -> PhysicalPosition { let output_data = self.proxy.data::().unwrap(); - output_data.with_output_info(|info| info.location).into() + output_data.with_output_info(|info| { + info.logical_position.map_or_else( + || { + LogicalPosition::::from(info.location) + .to_physical(info.scale_factor as f64) + }, + |logical_position| { + LogicalPosition::::from(logical_position) + .to_physical(info.scale_factor as f64) + }, + ) + }) } #[inline] @@ -157,7 +165,7 @@ impl VideoMode { self.refresh_rate_millihertz } - pub fn monitor(&self) -> PlatformMonitorHandle { - PlatformMonitorHandle::Wayland(self.monitor.clone()) + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() } } diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs index d9569a779e..e3e28666ab 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -17,7 +17,7 @@ use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, WEnum}; use crate::event::{ElementState, WindowEvent}; use crate::keyboard::ModifiersState; -use crate::platform_impl::common::xkb_state::KbdState; +use crate::platform_impl::common::xkb::Context; use crate::platform_impl::wayland::event_loop::sink::EventSink; use crate::platform_impl::wayland::seat::WinitSeatState; use crate::platform_impl::wayland::state::WinitState; @@ -43,14 +43,10 @@ impl Dispatch for WinitState { WlKeymapFormat::NoKeymap => { warn!("non-xkb compatible keymap") } - WlKeymapFormat::XkbV1 => unsafe { - seat_state - .keyboard_state - .as_mut() - .unwrap() - .xkb_state - .init_with_fd(fd, size as usize); - }, + WlKeymapFormat::XkbV1 => { + let context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context; + context.set_keymap_from_fd(fd, size as usize); + } _ => unreachable!(), }, WEnum::Unknown(value) => { @@ -61,21 +57,32 @@ impl Dispatch for WinitState { let window_id = wayland::make_wid(&surface); // Mark the window as focused. - match state.windows.get_mut().get(&window_id) { - Some(window) => window.lock().unwrap().set_has_focus(true), + let was_unfocused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + let was_unfocused = !window.has_focus(); + window.add_seat_focus(data.seat.id()); + was_unfocused + } None => return, }; // Drop the repeat, if there were any. - seat_state.keyboard_state.as_mut().unwrap().current_repeat = None; - - // The keyboard focus is considered as general focus. - state - .events_sink - .push_window_event(WindowEvent::Focused(true), window_id); + let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } *data.window_id.lock().unwrap() = Some(window_id); + // The keyboard focus is considered as general focus. + if was_unfocused { + state + .events_sink + .push_window_event(WindowEvent::Focused(true), window_id); + } + // HACK: this is just for GNOME not fixing their ordering issue of modifiers. if std::mem::take(&mut seat_state.modifiers_pending) { state.events_sink.push_window_event( @@ -89,34 +96,44 @@ impl Dispatch for WinitState { // NOTE: we should drop the repeat regardless whethere it was for the present // window of for the window which just went gone. - seat_state.keyboard_state.as_mut().unwrap().current_repeat = None; + let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } // NOTE: The check whether the window exists is essential as we might get a // nil surface, regardless of what protocol says. - match state.windows.get_mut().get(&window_id) { - Some(window) => window.lock().unwrap().set_has_focus(false), + let focused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + window.remove_seat_focus(&data.seat.id()); + window.has_focus() + } None => return, }; - // Notify that no modifiers are being pressed. - state.events_sink.push_window_event( - WindowEvent::ModifiersChanged(ModifiersState::empty().into()), - window_id, - ); - // We don't need to update it above, because the next `Enter` will overwrite // anyway. *data.window_id.lock().unwrap() = None; - state - .events_sink - .push_window_event(WindowEvent::Focused(false), window_id); + if !focused { + // Notify that no modifiers are being pressed. + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + window_id, + ); + + state + .events_sink + .push_window_event(WindowEvent::Focused(false), window_id); + } } WlKeyboardEvent::Key { key, - state: key_state, + state: WEnum::Value(WlKeyState::Pressed), .. - } if key_state == WEnum::Value(WlKeyState::Pressed) => { + } => { let key = key + 8; key_input( @@ -134,7 +151,12 @@ impl Dispatch for WinitState { RepeatInfo::Disable => return, }; - if !keyboard_state.xkb_state.key_repeats(key) { + if !keyboard_state + .xkb_context + .keymap_mut() + .unwrap() + .key_repeats(key) + { return; } @@ -151,6 +173,9 @@ impl Dispatch for WinitState { keyboard_state.repeat_token = keyboard_state .loop_handle .insert_source(timer, move |_, _, state| { + // Required to handle the wakeups from the repeat sources. + state.dispatched_events = true; + let data = wl_keyboard.data::().unwrap(); let seat_state = state.seats.get_mut(&data.seat.id()).unwrap(); @@ -181,9 +206,9 @@ impl Dispatch for WinitState { } WlKeyboardEvent::Key { key, - state: key_state, + state: WEnum::Value(WlKeyState::Released), .. - } if key_state == WEnum::Value(WlKeyState::Released) => { + } => { let key = key + 8; key_input( @@ -197,10 +222,17 @@ impl Dispatch for WinitState { let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); if keyboard_state.repeat_info != RepeatInfo::Disable - && keyboard_state.xkb_state.key_repeats(key) + && keyboard_state + .xkb_context + .keymap_mut() + .unwrap() + .key_repeats(key) && Some(key) == keyboard_state.current_repeat { keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } } } WlKeyboardEvent::Modifiers { @@ -210,9 +242,14 @@ impl Dispatch for WinitState { group, .. } => { - let xkb_state = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_state; + let xkb_context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context; + let xkb_state = match xkb_context.state_mut() { + Some(state) => state, + None => return, + }; + xkb_state.update_modifiers(mods_depressed, mods_latched, mods_locked, 0, 0, group); - seat_state.modifiers = xkb_state.mods_state().into(); + seat_state.modifiers = xkb_state.modifiers().into(); // HACK: part of the workaround from `WlKeyboardEvent::Enter`. let window_id = match *data.window_id.lock().unwrap() { @@ -258,7 +295,7 @@ pub struct KeyboardState { pub loop_handle: LoopHandle<'static, WinitState>, /// The state of the keyboard. - pub xkb_state: KbdState, + pub xkb_context: Context, /// The information about the repeat rate obtained from the compositor. pub repeat_info: RepeatInfo, @@ -275,7 +312,7 @@ impl KeyboardState { Self { keyboard, loop_handle, - xkb_state: KbdState::new().unwrap(), + xkb_context: Context::new().unwrap(), repeat_info: RepeatInfo::default(), repeat_token: None, current_repeat: None, @@ -358,16 +395,13 @@ fn key_input( let keyboard_state = seat_state.keyboard_state.as_mut().unwrap(); let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); - let event = keyboard_state - .xkb_state - .process_key_event(keycode, state, repeat); - - event_sink.push_window_event( - WindowEvent::KeyboardInput { + if let Some(mut key_context) = keyboard_state.xkb_context.key_context() { + let event = key_context.process_key_event(keycode, state, repeat); + let event = WindowEvent::KeyboardInput { device_id, event, is_synthetic: false, - }, - window_id, - ); + }; + event_sink.push_window_event(event, window_id); + } } diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index 1832e2af6a..2e2fa43874 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -2,8 +2,9 @@ use std::sync::Arc; -use fnv::FnvHashMap; +use ahash::AHashMap; +use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_touch::WlTouch; use sctk::reexports::client::{Connection, Proxy, QueueHandle}; @@ -13,6 +14,7 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3:: use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; +use crate::event::WindowEvent; use crate::keyboard::ModifiersState; use crate::platform_impl::wayland::state::WinitState; @@ -29,7 +31,7 @@ use keyboard::{KeyboardData, KeyboardState}; use text_input::TextInputData; use touch::TouchPoint; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct WinitSeatState { /// The pointer bound on the seat. pointer: Option>>, @@ -38,7 +40,7 @@ pub struct WinitSeatState { touch: Option, /// The mapping from touched points to the surfaces they're present. - touch_map: FnvHashMap, + touch_map: AHashMap, /// The text input bound on the seat. text_input: Option>, @@ -58,16 +60,7 @@ pub struct WinitSeatState { impl WinitSeatState { pub fn new() -> Self { - Self { - pointer: None, - touch: None, - relative_pointer: None, - text_input: None, - touch_map: Default::default(), - keyboard_state: None, - modifiers: ModifiersState::empty(), - modifiers_pending: false, - } + Default::default() } } @@ -97,12 +90,14 @@ impl SeatHandler for WinitState { SeatCapability::Pointer if seat_state.pointer.is_none() => { let surface = self.compositor_state.create_surface(queue_handle); let surface_id = surface.id(); - let pointer_data = WinitPointerData::new(seat.clone(), surface); + let pointer_data = WinitPointerData::new(seat.clone()); let themed_pointer = self .seat_state .get_pointer_with_theme_and_data( queue_handle, &seat, + self.shm.wl_shm(), + surface, ThemeSpec::System, pointer_data, ) @@ -150,6 +145,10 @@ impl SeatHandler for WinitState { ) { let seat_state = self.seats.get_mut(&seat.id()).unwrap(); + if let Some(text_input) = seat_state.text_input.take() { + text_input.destroy(); + } + match capability { SeatCapability::Touch => { if let Some(touch) = seat_state.touch.take() { @@ -167,7 +166,7 @@ impl SeatHandler for WinitState { let pointer_data = pointer.pointer().winit_data(); // Remove the cursor from the mapping. - let surface_id = pointer_data.cursor_surface().id(); + let surface_id = pointer.surface().id(); let _ = self.pointer_surfaces.remove(&surface_id); // Remove the inner locks/confines before dropping the pointer. @@ -181,13 +180,10 @@ impl SeatHandler for WinitState { } SeatCapability::Keyboard => { seat_state.keyboard_state = None; + self.on_keyboard_destroy(&seat.id()); } _ => (), } - - if let Some(text_input) = seat_state.text_input.take() { - text_input.destroy(); - } } fn new_seat( @@ -206,6 +202,21 @@ impl SeatHandler for WinitState { seat: WlSeat, ) { let _ = self.seats.remove(&seat.id()); + self.on_keyboard_destroy(&seat.id()); + } +} + +impl WinitState { + fn on_keyboard_destroy(&mut self, seat: &ObjectId) { + for (window_id, window) in self.windows.get_mut() { + let mut window = window.lock().unwrap(); + let had_focus = window.has_focus(); + window.remove_seat_focus(seat); + if had_focus != window.has_focus() { + self.events_sink + .push_window_event(WindowEvent::Focused(false), *window_id); + } + } } } diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index 607b0e1b12..3b62bae10d 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::time::Duration; use sctk::reexports::client::delegate_dispatch; use sctk::reexports::client::protocol::wl_pointer::WlPointer; @@ -10,15 +11,17 @@ use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::{Connection, Proxy, QueueHandle, Dispatch}; use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1; use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}; use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::csd_frame::FrameClick; use sctk::compositor::SurfaceData; use sctk::globals::GlobalData; use sctk::seat::pointer::{PointerData, PointerDataExt}; use sctk::seat::pointer::{PointerEvent, PointerEventKind, PointerHandler}; use sctk::seat::SeatState; -use sctk::shell::xdg::frame::FrameClick; use crate::dpi::{LogicalPosition, PhysicalPosition}; use crate::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent}; @@ -67,35 +70,31 @@ impl PointerHandler for WinitState { PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } if parent_surface != surface => { - if let Some(icon) = - window.frame_point_moved(surface, event.position.0, event.position.1) - { + if let Some(icon) = window.frame_point_moved( + seat, + surface, + Duration::ZERO, + event.position.0, + event.position.1, + ) { if let Some(pointer) = seat_state.pointer.as_ref() { - let surface = pointer - .pointer() - .data::() - .unwrap() - .cursor_surface(); - let scale_factor = - surface.data::().unwrap().scale_factor(); - - let _ = pointer.set_cursor( - connection, - icon, - self.shm.wl_shm(), - surface, - scale_factor, - ); + let _ = pointer.set_cursor(connection, icon); } } } PointerEventKind::Leave { .. } if parent_surface != surface => { window.frame_point_left(); } - ref kind @ PointerEventKind::Press { button, serial, .. } - | ref kind @ PointerEventKind::Release { button, serial, .. } - if parent_surface != surface => - { + ref kind @ PointerEventKind::Press { + button, + serial, + time, + } + | ref kind @ PointerEventKind::Release { + button, + serial, + time, + } if parent_surface != surface => { let click = match wayland_button_to_winit(button) { MouseButton::Left => FrameClick::Normal, MouseButton::Right => FrameClick::Alternate, @@ -109,6 +108,7 @@ impl PointerHandler for WinitState { pressed, seat, serial, + Duration::from_millis(time as u64), window_id, &mut self.window_compositor_updates, ); @@ -238,9 +238,6 @@ impl PointerHandler for WinitState { #[derive(Debug)] pub struct WinitPointerData { - /// The surface associated with this pointer, which is used for icons. - cursor_surface: WlSurface, - /// The inner winit data associated with the pointer. inner: Mutex, @@ -249,9 +246,8 @@ pub struct WinitPointerData { } impl WinitPointerData { - pub fn new(seat: WlSeat, surface: WlSurface) -> Self { + pub fn new(seat: WlSeat) -> Self { Self { - cursor_surface: surface, inner: Mutex::new(WinitPointerDataInner::default()), sctk_data: PointerData::new(seat), } @@ -313,11 +309,6 @@ impl WinitPointerData { self.sctk_data.seat() } - /// The WlSurface used to set cursor theme. - pub fn cursor_surface(&self) -> &WlSurface { - &self.cursor_surface - } - /// Active window. pub fn focused_window(&self) -> Option { self.inner.lock().unwrap().surface @@ -325,7 +316,7 @@ impl WinitPointerData { /// Last button serial. pub fn latest_button_serial(&self) -> u32 { - self.inner.lock().unwrap().latest_button_serial + self.sctk_data.latest_button_serial().unwrap_or_default() } /// Last enter serial. @@ -341,12 +332,6 @@ impl WinitPointerData { } } -impl Drop for WinitPointerData { - fn drop(&mut self) { - self.cursor_surface.destroy(); - } -} - impl PointerDataExt for WinitPointerData { fn pointer_data(&self) -> &PointerData { &self.sctk_data @@ -486,7 +471,35 @@ impl Dispatch for PointerConstrain } } +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeDeviceV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_shape_manager has no events") + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_device_manager has no events") + } +} + delegate_dispatch!(WinitState: [ WlPointer: WinitPointerData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeManagerV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeDeviceV1: GlobalData] => SeatState); delegate_dispatch!(WinitState: [ZwpPointerConstraintsV1: GlobalData] => PointerConstraintsState); delegate_dispatch!(WinitState: [ZwpLockedPointerV1: GlobalData] => PointerConstraintsState); delegate_dispatch!(WinitState: [ZwpConfinedPointerV1: GlobalData] => PointerConstraintsState); diff --git a/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs b/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs index 23a6e21992..f7364e3771 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs @@ -60,19 +60,34 @@ impl Dispatch for RelativePointerS _conn: &Connection, _qhandle: &QueueHandle, ) { - if let zwp_relative_pointer_v1::Event::RelativeMotion { - dx_unaccel, - dy_unaccel, - .. - } = event - { - state.events_sink.push_device_event( - DeviceEvent::MouseMotion { - delta: (dx_unaccel, dy_unaccel), - }, - super::DeviceId, - ); - } + let (dx_unaccel, dy_unaccel) = match event { + zwp_relative_pointer_v1::Event::RelativeMotion { + dx_unaccel, + dy_unaccel, + .. + } => (dx_unaccel, dy_unaccel), + _ => return, + }; + state.events_sink.push_device_event( + DeviceEvent::Motion { + axis: 0, + value: dx_unaccel, + }, + super::DeviceId, + ); + state.events_sink.push_device_event( + DeviceEvent::Motion { + axis: 1, + value: dy_unaccel, + }, + super::DeviceId, + ); + state.events_sink.push_device_event( + DeviceEvent::MouseMotion { + delta: (dx_unaccel, dy_unaccel), + }, + super::DeviceId, + ); } } diff --git a/src/platform_impl/linux/wayland/seat/touch/mod.rs b/src/platform_impl/linux/wayland/seat/touch/mod.rs index e5b838689a..7232b2279f 100644 --- a/src/platform_impl/linux/wayland/seat/touch/mod.rs +++ b/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -121,7 +121,7 @@ impl TouchHandler for WinitState { device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( DeviceId, )), - phase: TouchPhase::Cancelled, + phase: TouchPhase::Moved, location: touch_point.location.to_physical(scale_factor), force: None, id: id as u64, diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index f9b7c4afca..f0c54c01e6 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -1,8 +1,8 @@ use std::cell::RefCell; -use std::error::Error; +use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; -use fnv::FnvHashMap; +use ahash::AHashMap; use sctk::reexports::calloop::LoopHandle; use sctk::reexports::client::backend::ObjectId; @@ -22,19 +22,19 @@ use sctk::shell::WaylandSurface; use sctk::shm::{Shm, ShmHandler}; use sctk::subcompositor::SubcompositorState; -use crate::dpi::LogicalSize; - -use super::event_loop::sink::EventSink; -use super::output::MonitorHandle; -use super::seat::{ +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::output::MonitorHandle; +use crate::platform_impl::wayland::seat::{ PointerConstraintsState, RelativePointerState, TextInputState, WinitPointerData, WinitPointerDataExt, WinitSeatState, }; -use super::types::wp_fractional_scaling::FractionalScalingManager; -use super::types::wp_viewporter::ViewporterState; -use super::types::xdg_activation::XdgActivationState; -use super::window::{WindowRequests, WindowState}; -use super::WindowId; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::wayland::types::wp_fractional_scaling::FractionalScalingManager; +use crate::platform_impl::wayland::types::wp_viewporter::ViewporterState; +use crate::platform_impl::wayland::types::xdg_activation::XdgActivationState; +use crate::platform_impl::wayland::window::{WindowRequests, WindowState}; +use crate::platform_impl::wayland::{WaylandError, WindowId}; +use crate::platform_impl::OsError; /// Winit's Wayland state. pub struct WinitState { @@ -48,7 +48,7 @@ pub struct WinitState { pub compositor_state: Arc, /// The state of the subcompositor. - pub subcompositor_state: Arc, + pub subcompositor_state: Option>, /// The seat state responsible for all sorts of input. pub seat_state: SeatState, @@ -60,10 +60,10 @@ pub struct WinitState { pub xdg_shell: XdgShell, /// The currently present windows. - pub windows: RefCell>>>, + pub windows: RefCell>>>, /// The requests from the `Window` to EventLoop, such as close operations and redraw requests. - pub window_requests: RefCell>>, + pub window_requests: RefCell>>, /// The events that were generated directly from the window. pub window_events_sink: Arc>, @@ -72,10 +72,10 @@ pub struct WinitState { pub window_compositor_updates: Vec, /// Currently handled seats. - pub seats: FnvHashMap, + pub seats: AHashMap, /// Currently present cursor surfaces. - pub pointer_surfaces: FnvHashMap>>, + pub pointer_surfaces: AHashMap>>, /// The state of the text input on the client. pub text_input_state: Option, @@ -102,8 +102,15 @@ pub struct WinitState { /// Fractional scaling manager. pub fractional_scaling_manager: Option, + /// KWin blur manager. + pub kwin_blur_manager: Option, + /// Loop handle to re-register event sources, such as keyboard repeat. pub loop_handle: LoopHandle<'static, Self>, + + /// Whether we have dispatched events to the user thus we want to + /// send `AboutToWait` and normally wakeup the user. + pub dispatched_events: bool, } impl WinitState { @@ -111,21 +118,28 @@ impl WinitState { globals: &GlobalList, queue_handle: &QueueHandle, loop_handle: LoopHandle<'static, WinitState>, - ) -> Result> { + ) -> Result { let registry_state = RegistryState::new(globals); - let compositor_state = CompositorState::bind(globals, queue_handle)?; - let subcompositor_state = SubcompositorState::bind( + let compositor_state = + CompositorState::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let subcompositor_state = match SubcompositorState::bind( compositor_state.wl_compositor().clone(), globals, queue_handle, - )?; + ) { + Ok(c) => Some(c), + Err(e) => { + warn!("Subcompositor protocol not available, ignoring CSD: {e:?}"); + None + } + }; let output_state = OutputState::new(globals, queue_handle); let monitors = output_state.outputs().map(MonitorHandle::new).collect(); let seat_state = SeatState::new(globals, queue_handle); - let mut seats = FnvHashMap::default(); + let mut seats = AHashMap::default(); for seat in seat_state.seats() { seats.insert(seat.id(), WinitSeatState::new()); } @@ -140,12 +154,12 @@ impl WinitState { Ok(Self { registry_state, compositor_state: Arc::new(compositor_state), - subcompositor_state: Arc::new(subcompositor_state), + subcompositor_state: subcompositor_state.map(Arc::new), output_state, seat_state, - shm: Shm::bind(globals, queue_handle)?, + shm: Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?, - xdg_shell: XdgShell::bind(globals, queue_handle)?, + xdg_shell: XdgShell::bind(globals, queue_handle).map_err(WaylandError::Bind)?, xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), windows: Default::default(), @@ -154,6 +168,7 @@ impl WinitState { window_events_sink: Default::default(), viewporter_state, fractional_scaling_manager, + kwin_blur_manager: KWinBlurManager::new(globals, queue_handle).ok(), seats, text_input_state: TextInputState::new(globals, queue_handle).ok(), @@ -167,6 +182,8 @@ impl WinitState { monitors: Arc::new(Mutex::new(monitors)), events_sink: EventSink::new(), loop_handle, + // Make it true by default. + dispatched_events: true, }) } @@ -200,7 +217,7 @@ impl WinitState { // Update the scale factor right away. window.lock().unwrap().set_scale_factor(scale_factor); - self.window_compositor_updates[pos].scale_factor = Some(scale_factor); + self.window_compositor_updates[pos].scale_changed = true; } else if let Some(pointer) = self.pointer_surfaces.get(&surface.id()) { // Get the window, where the pointer resides right now. let focused_window = match pointer.pointer().winit_data().focused_window() { @@ -264,9 +281,7 @@ impl WindowHandler for WinitState { }; // Populate the configure to the window. - // - // XXX the size on the window will be updated right before dispatching the size to the user. - let new_size = self + self.window_compositor_updates[pos].resized |= self .windows .get_mut() .get_mut(&window_id) @@ -275,7 +290,17 @@ impl WindowHandler for WinitState { .unwrap() .configure(configure, &self.shm, &self.subcompositor_state); - self.window_compositor_updates[pos].size = Some(new_size); + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the + // users, since it can break a lot of things, thus it'll ask users to redraw instead. + self.window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + // Manually mark that we've got an event, since configure may not generate a resize. + self.dispatched_events = true; } } @@ -311,6 +336,16 @@ impl OutputHandler for WinitState { } impl CompositorHandler for WinitState { + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wayland_client::protocol::wl_surface::WlSurface, + _: wayland_client::protocol::wl_output::Transform, + ) { + // TODO(kchibisov) we need to expose it somehow in winit. + } + fn scale_factor_changed( &mut self, _: &Connection, @@ -321,7 +356,27 @@ impl CompositorHandler for WinitState { self.scale_factor_changed(surface, scale_factor as f64, true) } - fn frame(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: u32) {} + fn frame(&mut self, _: &Connection, _: &QueueHandle, surface: &WlSurface, _: u32) { + let window_id = super::make_wid(surface); + let window = match self.windows.get_mut().get(&window_id) { + Some(window) => window, + None => return, + }; + + // In case we have a redraw requested we must indicate the wake up. + if self + .window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .load(Ordering::Relaxed) + { + self.dispatched_events = true; + } + + window.lock().unwrap().frame_callback_received(); + } } impl ProvidesRegistryState for WinitState { @@ -339,10 +394,10 @@ pub struct WindowCompositorUpdate { pub window_id: WindowId, /// New window size. - pub size: Option>, + pub resized: bool, /// New scale factor. - pub scale_factor: Option, + pub scale_changed: bool, /// Close the window. pub close_window: bool, @@ -352,8 +407,8 @@ impl WindowCompositorUpdate { fn new(window_id: WindowId) -> Self { Self { window_id, - size: None, - scale_factor: None, + resized: false, + scale_changed: false, close_window: false, } } diff --git a/src/platform_impl/linux/wayland/types/kwin_blur.rs b/src/platform_impl/linux/wayland/types/kwin_blur.rs new file mode 100644 index 0000000000..159f0fbaf0 --- /dev/null +++ b/src/platform_impl/linux/wayland/types/kwin_blur.rs @@ -0,0 +1,70 @@ +//! Handling of KDE-compatible blur. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{delegate_dispatch, Connection, Proxy, QueueHandle}; +use wayland_protocols_plasma::blur::client::{ + org_kde_kwin_blur::OrgKdeKwinBlur, org_kde_kwin_blur_manager::OrgKdeKwinBlurManager, +}; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// KWin blur manager. +#[derive(Debug, Clone)] +pub struct KWinBlurManager { + manager: OrgKdeKwinBlurManager, +} + +impl KWinBlurManager { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn blur( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> OrgKdeKwinBlur { + self.manager.create(surface, queue_handle, ()) + } + + pub fn unset(&self, surface: &WlSurface) { + self.manager.unset(surface) + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlurManager, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur_manager"); + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlur, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur"); + } +} + +delegate_dispatch!(WinitState: [OrgKdeKwinBlurManager: GlobalData] => KWinBlurManager); +delegate_dispatch!(WinitState: [OrgKdeKwinBlur: ()] => KWinBlurManager); diff --git a/src/platform_impl/linux/wayland/types/mod.rs b/src/platform_impl/linux/wayland/types/mod.rs index 8e92eb1d03..ea74588823 100644 --- a/src/platform_impl/linux/wayland/types/mod.rs +++ b/src/platform_impl/linux/wayland/types/mod.rs @@ -1,5 +1,6 @@ //! Wayland protocol implementation boilerplate. +pub mod kwin_blur; pub mod wp_fractional_scaling; pub mod wp_viewporter; pub mod xdg_activation; diff --git a/src/platform_impl/linux/wayland/types/xdg_activation.rs b/src/platform_impl/linux/wayland/types/xdg_activation.rs index 1befb5ff0a..be546d15bf 100644 --- a/src/platform_impl/linux/wayland/types/xdg_activation.rs +++ b/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -16,7 +16,10 @@ use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1:: use sctk::globals::GlobalData; +use crate::event_loop::AsyncRequestSerial; use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::WindowId; +use crate::window::ActivationToken; pub struct XdgActivationState { xdg_activation: XdgActivationV1, @@ -62,16 +65,29 @@ impl Dispatch for XdgA _ => return, }; - state + let global = state .xdg_activation .as_ref() .expect("got xdg_activation event without global.") - .global() - .activate(token, &data.surface); - - // Mark that no request attention is in process. - if let Some(attention_requested) = data.attention_requested.upgrade() { - attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + .global(); + + match data { + XdgActivationTokenData::Attention((surface, fence)) => { + global.activate(token, surface); + // Mark that no request attention is in process. + if let Some(attention_requested) = fence.upgrade() { + attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + XdgActivationTokenData::Obtain((window_id, serial)) => { + state.events_sink.push_window_event( + crate::event::WindowEvent::ActivationTokenDone { + serial: *serial, + token: ActivationToken::_new(token), + }, + *window_id, + ); + } } proxy.destroy(); @@ -79,24 +95,11 @@ impl Dispatch for XdgA } /// The data associated with the activation request. -pub struct XdgActivationTokenData { - /// The surface we're raising. - surface: WlSurface, - - /// Flag to throttle attention requests. - attention_requested: Weak, -} - -impl XdgActivationTokenData { - /// Create a new data. - /// - /// The `attenteion_requested` is marked as `false` on complition. - pub fn new(surface: WlSurface, attention_requested: Weak) -> Self { - Self { - surface, - attention_requested, - } - } +pub enum XdgActivationTokenData { + /// Request user attention for the given surface. + Attention((WlSurface, Weak)), + /// Get a token to be passed outside of the winit. + Obtain((WindowId, AsyncRequestSerial)), } delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 74f6370d1e..8127595ae1 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -3,11 +3,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use raw_window_handle::{ - RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, -}; - -use sctk::reexports::calloop; use sctk::reexports::client::protocol::wl_display::WlDisplay; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Proxy; @@ -22,22 +17,23 @@ use sctk::shell::WaylandSurface; use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; use crate::event::{Ime, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; use crate::platform_impl::{ - Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon, PlatformSpecificWindowBuilderAttributes as PlatformAttributes, }; use crate::window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, - WindowAttributes, WindowButtons, + WindowAttributes, WindowButtons, WindowLevel, }; use super::event_loop::sink::EventSink; use super::output::MonitorHandle; use super::state::WinitState; use super::types::xdg_activation::XdgActivationTokenData; -use super::{EventLoopWindowTarget, WindowId}; +use super::{EventLoopWindowTarget, WaylandError, WindowId}; -mod state; +pub(crate) mod state; pub use state::WindowState; @@ -56,6 +52,7 @@ pub struct Window { compositor: Arc, /// The wayland display used solely for raw window handle. + #[allow(dead_code)] display: WlDisplay, /// Xdg activation to request user attention. @@ -99,11 +96,9 @@ impl Window { .map(|activation_state| activation_state.global().clone()); let display = event_loop_window_target.connection.display(); - // XXX The initial scale factor must be 1, but it might cause sizing issues on HiDPI. - let size: LogicalSize = attributes + let size: Size = attributes .inner_size - .map(|size| size.to_logical::(1.)) - .unwrap_or((800, 600).into()); + .unwrap_or(LogicalSize::new(800., 600.).into()); // We prefer server side decorations, however to not have decorations we ask for client // side decorations instead. @@ -130,6 +125,8 @@ impl Window { // Set transparency hint. window_state.set_transparent(attributes.transparent); + window_state.set_blur(attributes.blur); + // Set the decorations hint. window_state.set_decorate(attributes.decorations); @@ -141,7 +138,8 @@ impl Window { // Set the window title. window_state.set_title(attributes.title); - // Set the min and max sizes. + // Set the min and max sizes. We must set the hints upon creating a window, so + // we use the default `1.` scaling... let min_size = attributes.min_inner_size.map(|size| size.to_logical(1.)); let max_size = attributes.max_inner_size.map(|size| size.to_logical(1.)); window_state.set_min_inner_size(min_size); @@ -151,7 +149,7 @@ impl Window { window_state.set_resizable(attributes.resizable); // Set startup mode. - match attributes.fullscreen.map(Into::into) { + match attributes.fullscreen.0.map(Into::into) { Some(Fullscreen::Exclusive(_)) => { warn!("`Fullscreen::Exclusive` is ignored on Wayland"); } @@ -168,6 +166,14 @@ impl Window { _ => (), }; + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = ( + xdg_activation.as_ref(), + platform_attributes.activation_token, + ) { + xdg_activation.activate(token._token, &surface); + } + // XXX Do initial commit. window.commit(); @@ -196,18 +202,18 @@ impl Window { let event_queue = wayland_source.queue(); // Do a roundtrip. - event_queue.roundtrip(&mut state).map_err(|_| { - os_error!(OsError::WaylandMisc( - "failed to do initial roundtrip for the window." - )) + event_queue.roundtrip(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) })?; // XXX Wait for the initial configure to arrive. while !window_state.lock().unwrap().is_configured() { - event_queue.blocking_dispatch(&mut state).map_err(|_| { - os_error!(OsError::WaylandMisc( - "failed to dispatch queue while waiting for initial configure." - )) + event_queue.blocking_dispatch(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) })?; } @@ -273,32 +279,43 @@ impl Window { pub fn inner_size(&self) -> PhysicalSize { let window_state = self.window_state.lock().unwrap(); let scale_factor = window_state.scale_factor(); - window_state.inner_size().to_physical(scale_factor) + super::logical_to_physical_rounded(window_state.inner_size(), scale_factor) } #[inline] pub fn request_redraw(&self) { - self.window_requests + // NOTE: try to not wake up the loop when the event was already scheduled and not yet + // processed by the loop, because if at this point the value was `true` it could only + // mean that the loop still haven't dispatched the value to the client and will do + // eventually, resetting it to `false`. + if self + .window_requests .redraw_requested - .store(true, Ordering::Relaxed); - self.event_loop_awakener.ping(); + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn pre_present_notify(&self) { + self.window_state.lock().unwrap().request_frame_callback(); } #[inline] pub fn outer_size(&self) -> PhysicalSize { let window_state = self.window_state.lock().unwrap(); let scale_factor = window_state.scale_factor(); - window_state.outer_size().to_physical(scale_factor) + super::logical_to_physical_rounded(window_state.outer_size(), scale_factor) } #[inline] - pub fn set_inner_size(&self, size: Size) { - // TODO should we issue the resize event? I don't think other platforms do so. + pub fn request_inner_size(&self, size: Size) -> Option> { let mut window_state = self.window_state.lock().unwrap(); - let scale_factor = window_state.scale_factor(); - window_state.resize(size.to_logical::(scale_factor)); - + let new_size = window_state.request_inner_size(size); self.request_redraw(); + Some(new_size) } /// Set the minimum inner size for the window. @@ -309,7 +326,9 @@ impl Window { self.window_state .lock() .unwrap() - .set_min_inner_size(min_size) + .set_min_inner_size(min_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); } /// Set the maximum inner size for the window. @@ -320,7 +339,9 @@ impl Window { self.window_state .lock() .unwrap() - .set_max_inner_size(max_size) + .set_max_inner_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); } #[inline] @@ -352,6 +373,13 @@ impl Window { None } + #[inline] + pub fn show_window_menu(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state.lock().unwrap().show_window_menu(position); + } + #[inline] pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { self.window_state @@ -362,7 +390,10 @@ impl Window { #[inline] pub fn set_resizable(&self, resizable: bool) { - self.window_state.lock().unwrap().set_resizable(resizable); + if self.window_state.lock().unwrap().set_resizable(resizable) { + // NOTE: Requires commit to be applied. + self.request_redraw(); + } } #[inline] @@ -386,6 +417,11 @@ impl Window { self.window_state.lock().unwrap().scale_factor() } + #[inline] + pub fn set_blur(&self, blur: bool) { + self.window_state.lock().unwrap().set_blur(blur); + } + #[inline] pub fn set_decorations(&self, decorate: bool) { self.window_state.lock().unwrap().set_decorate(decorate) @@ -396,6 +432,12 @@ impl Window { self.window_state.lock().unwrap().is_decorated() } + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) {} + + #[inline] + pub(crate) fn set_window_icon(&self, _window_icon: Option) {} + #[inline] pub fn set_minimized(&self, minimized: bool) { // You can't unminimize the window on Wayland. @@ -495,13 +537,31 @@ impl Window { self.attention_requested.store(true, Ordering::Relaxed); let surface = self.surface().clone(); - let data = - XdgActivationTokenData::new(surface.clone(), Arc::downgrade(&self.attention_requested)); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); xdg_activation_token.set_surface(&surface); xdg_activation_token.commit(); } + pub fn request_activation_token(&self) -> Result { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => return Err(NotSupportedError::new()), + }; + + let serial = AsyncRequestSerial::get(); + + let data = XdgActivationTokenData::Obtain((self.window_id, serial)); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(self.surface()); + xdg_activation_token.commit(); + + Ok(serial) + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { self.window_state.lock().unwrap().set_cursor_grab(mode) @@ -533,9 +593,7 @@ impl Window { Ok(()) } else { let region = Region::new(&*self.compositor).map_err(|_| { - ExternalError::Os(os_error!(OsError::WaylandMisc( - "failed to set input region." - ))) + ExternalError::Os(os_error!(OsError::Misc("failed to set input region."))) })?; region.add(0, 0, 0, 0); surface.set_input_region(Some(region.wl_region())); @@ -574,9 +632,7 @@ impl Window { } #[inline] - pub fn display(&self) -> &WlDisplay { - &self.display - } + pub fn focus_window(&self) {} #[inline] pub fn surface(&self) -> &WlSurface { @@ -595,23 +651,56 @@ impl Window { } #[inline] - pub fn primary_monitor(&self) -> Option { + pub fn primary_monitor(&self) -> Option { // XXX there's no such concept on Wayland. None } + #[cfg(feature = "rwh_04")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut window_handle = WaylandWindowHandle::empty(); + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WaylandHandle::empty(); window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; - RawWindowHandle::Wayland(window_handle) + window_handle.display = self.display.id().as_ptr() as *mut _; + rwh_04::RawWindowHandle::Wayland(window_handle) } + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - let mut display_handle = WaylandDisplayHandle::empty(); + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WaylandWindowHandle::empty(); + window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; + rwh_05::RawWindowHandle::Wayland(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); display_handle.display = self.display.id().as_ptr() as *mut _; - RawDisplayHandle::Wayland(display_handle) + rwh_05::RawDisplayHandle::Wayland(display_handle) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + Ok(rwh_06::WaylandWindowHandle::new({ + let ptr = self.window.wl_surface().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") + }) + .into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_proxy should never be null") + }) + .into()) } #[inline] @@ -624,6 +713,8 @@ impl Window { self.window_state.lock().unwrap().theme() } + pub fn set_content_protected(&self, _protected: bool) {} + #[inline] pub fn title(&self) -> String { self.window_state.lock().unwrap().title().to_owned() diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index e67e16528f..1d36b12915 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -1,31 +1,38 @@ //! The state of the window, which is shared with the event-loop. -use std::mem::ManuallyDrop; use std::num::NonZeroU32; use std::sync::{Arc, Weak}; +use std::time::Duration; -use log::warn; +use ahash::HashSet; +use log::{info, warn}; +use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_shm::WlShm; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::csd_frame::{ + DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState, +}; use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; -use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; -use sctk::compositor::{CompositorState, Region, SurfaceData}; +use sctk::compositor::{CompositorState, Region}; use sctk::seat::pointer::ThemedPointer; -use sctk::shell::xdg::frame::{DecorationsFrame, FrameAction, FrameClick}; use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; use sctk::shell::xdg::XdgSurface; use sctk::shell::WaylandSurface; use sctk::shm::Shm; use sctk::subcompositor::SubcompositorState; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size}; use crate::error::{ExternalError, NotSupportedError}; +use crate::platform_impl::wayland::logical_to_physical_rounded; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; use crate::platform_impl::WindowId; use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; @@ -37,7 +44,7 @@ use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState}; #[cfg(feature = "sctk-adwaita")] pub type WinitFrame = sctk_adwaita::AdwaitaFrame; #[cfg(not(feature = "sctk-adwaita"))] -pub type WinitFrame = sctk::shell::xdg::frame::fallback_frame::FallbackFrame; +pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame; // Minimum window inner size. const MIN_WINDOW_SIZE: LogicalSize = LogicalSize::new(2, 1); @@ -47,9 +54,6 @@ pub struct WindowState { /// The connection to Wayland server. pub connection: Connection, - /// The underlying SCTK window. - pub window: ManuallyDrop, - /// The window frame, which is created from the configure request. frame: Option, @@ -83,8 +87,10 @@ pub struct WindowState { /// Whether the frame is resizable. resizable: bool, - /// Whether the window has focus. - has_focus: bool, + // NOTE: we can't use simple counter, since it's racy when seat getting destroyed and new + // is created, since add/removed stuff could be delivered a bit out of order. + /// Seats that has keyboard focus on that window. + seat_focus: HashSet, /// The scale factor of the window. scale_factor: f64, @@ -125,30 +131,86 @@ pub struct WindowState { /// sends `None` for the new size in the configure. stateless_size: LogicalSize, + /// Initial window size provided by the user. Removed on the first + /// configure. + initial_size: Option, + + /// The state of the frame callback. + frame_callback_state: FrameCallbackState, + viewport: Option, fractional_scale: Option, -} + blur: Option, + blur_manager: Option, -/// The state of the cursor grabs. -#[derive(Clone, Copy)] -struct GrabState { - /// The grab mode requested by the user. - user_grab_mode: CursorGrabMode, + /// Whether the client side decorations have pending move operations. + /// + /// The value is the serial of the event triggered moved. + has_pending_move: Option, - /// The current grab mode. - current_grab_mode: CursorGrabMode, + /// The underlying SCTK window. + pub window: Window, } -impl GrabState { - fn new() -> Self { +impl WindowState { + /// Create new window state. + pub fn new( + connection: Connection, + queue_handle: &QueueHandle, + winit_state: &WinitState, + initial_size: Size, + window: Window, + theme: Option, + ) -> Self { + let compositor = winit_state.compositor_state.clone(); + let pointer_constraints = winit_state.pointer_constraints.clone(); + let viewport = winit_state + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); + let fractional_scale = winit_state + .fractional_scaling_manager + .as_ref() + .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); + Self { - user_grab_mode: CursorGrabMode::None, - current_grab_mode: CursorGrabMode::None, + blur: None, + blur_manager: winit_state.kwin_blur_manager.clone(), + compositor, + connection, + csd_fails: false, + cursor_grab_mode: GrabState::new(), + cursor_icon: CursorIcon::Default, + cursor_visible: true, + decorate: true, + fractional_scale, + frame: None, + frame_callback_state: FrameCallbackState::None, + seat_focus: Default::default(), + has_pending_move: None, + ime_allowed: false, + ime_purpose: ImePurpose::Normal, + last_configure: None, + max_inner_size: None, + min_inner_size: MIN_WINDOW_SIZE, + pointer_constraints, + pointers: Default::default(), + queue_handle: queue_handle.clone(), + resizable: true, + scale_factor: 1., + shm: winit_state.shm.wl_shm().clone(), + size: initial_size.to_logical(1.), + stateless_size: initial_size.to_logical(1.), + initial_size: Some(initial_size), + text_inputs: Vec::new(), + theme, + title: String::default(), + transparent: false, + viewport, + window, } } -} -impl WindowState { /// Apply closure on the given pointer. fn apply_on_poiner, &WinitPointerData)>( &self, @@ -163,19 +225,57 @@ impl WindowState { }) } + /// Get the current state of the frame callback. + pub fn frame_callback_state(&self) -> FrameCallbackState { + self.frame_callback_state + } + + /// The frame callback was received, but not yet sent to the user. + pub fn frame_callback_received(&mut self) { + self.frame_callback_state = FrameCallbackState::Received; + } + + /// Reset the frame callbacks state. + pub fn frame_callback_reset(&mut self) { + self.frame_callback_state = FrameCallbackState::None; + } + + /// Request a frame callback if we don't have one for this window in flight. + pub fn request_frame_callback(&mut self) { + let surface = self.window.wl_surface(); + match self.frame_callback_state { + FrameCallbackState::None | FrameCallbackState::Received => { + self.frame_callback_state = FrameCallbackState::Requested; + surface.frame(&self.queue_handle, surface.clone()); + } + FrameCallbackState::Requested => (), + } + } + pub fn configure( &mut self, configure: WindowConfigure, shm: &Shm, - subcompositor: &Arc, - ) -> LogicalSize { - if configure.decoration_mode == DecorationMode::Client - && self.frame.is_none() - && !self.csd_fails - { + subcompositor: &Option>, + ) -> bool { + // NOTE: when using fractional scaling or wl_compositor@v6 the scaling + // should be delivered before the first configure, thus apply it to + // properly scale the physical sizes provided by the users. + if let Some(initial_size) = self.initial_size.take() { + self.size = initial_size.to_logical(self.scale_factor()); + self.stateless_size = self.size; + } + + if let Some(subcompositor) = subcompositor.as_ref().filter(|_| { + configure.decoration_mode == DecorationMode::Client + && self.frame.is_none() + && !self.csd_fails + }) { match WinitFrame::new( - &*self.window, + &self.window, shm, + #[cfg(feature = "sctk-adwaita")] + self.compositor.clone(), subcompositor.clone(), self.queue_handle.clone(), #[cfg(feature = "sctk-adwaita")] @@ -183,6 +283,7 @@ impl WindowState { ) { Ok(mut frame) => { frame.set_title(&self.title); + frame.set_scaling_factor(self.scale_factor); // Hide the frame if we were asked to not decorate. frame.set_hidden(!self.decorate); self.frame = Some(frame); @@ -199,37 +300,90 @@ impl WindowState { let stateless = Self::is_stateless(&configure); - let new_size = if let Some(frame) = self.frame.as_mut() { + let (mut new_size, constrain) = if let Some(frame) = self.frame.as_mut() { // Configure the window states. frame.update_state(configure.state); match configure.new_size { (Some(width), Some(height)) => { let (width, height) = frame.subtract_borders(width, height); - ( - width.map(|w| w.get()).unwrap_or(1), - height.map(|h| h.get()).unwrap_or(1), - ) - .into() + let width = width.map(|w| w.get()).unwrap_or(1); + let height = height.map(|h| h.get()).unwrap_or(1); + ((width, height).into(), false) } - (_, _) if stateless => self.stateless_size, - _ => self.size, + (_, _) if stateless => (self.stateless_size, true), + _ => (self.size, true), } } else { match configure.new_size { - (Some(width), Some(height)) => (width.get(), height.get()).into(), - _ if stateless => self.stateless_size, - _ => self.size, + (Some(width), Some(height)) => ((width.get(), height.get()).into(), false), + _ if stateless => (self.stateless_size, true), + _ => (self.size, true), } }; - // XXX Set the configure before doing a resize. + // Apply configure bounds only when compositor let the user decide what size to pick. + if constrain { + let bounds = self.inner_size_bounds(&configure); + new_size.width = bounds + .0 + .map(|bound_w| new_size.width.min(bound_w.get())) + .unwrap_or(new_size.width); + new_size.height = bounds + .1 + .map(|bound_h| new_size.height.min(bound_h.get())) + .unwrap_or(new_size.height); + } + + let new_state = configure.state; + let old_state = self + .last_configure + .as_ref() + .map(|configure| configure.state); + + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); + + // NOTE: Set the configure before doing a resize, since we query it during it. self.last_configure = Some(configure); - // XXX Update the new size right away. - self.resize(new_size); + if state_change_requires_resize || new_size != self.inner_size() { + self.resize(new_size); + true + } else { + false + } + } + + /// Compute the bounds for the inner size of the surface. + fn inner_size_bounds( + &self, + configure: &WindowConfigure, + ) -> (Option, Option) { + let configure_bounds = match configure.suggested_bounds { + Some((width, height)) => (NonZeroU32::new(width), NonZeroU32::new(height)), + None => (None, None), + }; - new_size + if let Some(frame) = self.frame.as_ref() { + let (width, height) = frame.subtract_borders( + configure_bounds.0.unwrap_or(NonZeroU32::new(1).unwrap()), + configure_bounds.1.unwrap_or(NonZeroU32::new(1).unwrap()), + ); + ( + configure_bounds.0.and(width), + configure_bounds.1.and(height), + ) + } else { + configure_bounds + } } #[inline] @@ -265,23 +419,40 @@ impl WindowState { } /// Tells whether the window should be closed. + #[allow(clippy::too_many_arguments)] pub fn frame_click( &mut self, click: FrameClick, pressed: bool, seat: &WlSeat, serial: u32, + timestamp: Duration, window_id: WindowId, updates: &mut Vec, ) -> Option { - match self.frame.as_mut()?.on_click(click, pressed)? { + match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { FrameAction::Minimize => self.window.set_minimized(), FrameAction::Maximize => self.window.set_maximized(), FrameAction::UnMaximize => self.window.unset_maximized(), FrameAction::Close => WinitState::queue_close(updates, window_id), - FrameAction::Move => self.window.move_(seat, serial), - FrameAction::Resize(edge) => self.window.resize(seat, serial, edge), + FrameAction::Move => self.has_pending_move = Some(serial), + FrameAction::Resize(edge) => { + let edge = match edge { + ResizeEdge::None => XdgResizeEdge::None, + ResizeEdge::Top => XdgResizeEdge::Top, + ResizeEdge::Bottom => XdgResizeEdge::Bottom, + ResizeEdge::Left => XdgResizeEdge::Left, + ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, + ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, + ResizeEdge::Right => XdgResizeEdge::Right, + ResizeEdge::TopRight => XdgResizeEdge::TopRight, + ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, + _ => return None, + }; + self.window.resize(seat, serial, edge); + } FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), + _ => (), }; Some(false) @@ -294,9 +465,27 @@ impl WindowState { } // Move the point over decorations. - pub fn frame_point_moved(&mut self, surface: &WlSurface, x: f64, y: f64) -> Option<&str> { + pub fn frame_point_moved( + &mut self, + seat: &WlSeat, + surface: &WlSurface, + timestamp: Duration, + x: f64, + y: f64, + ) -> Option { + // Take the serial if we had any, so it doesn't stick around. + let serial = self.has_pending_move.take(); + if let Some(frame) = self.frame.as_mut() { - frame.click_point_moved(surface, x, y) + let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); + // If we have a cursor change, that means that cursor is over the decorations, + // so try to apply move. + if let Some(serial) = cursor.is_some().then_some(serial).flatten() { + self.window.move_(seat, serial); + None + } else { + cursor + } } else { None } @@ -309,10 +498,12 @@ impl WindowState { } /// Set the resizable state on the window. + /// + /// Returns `true` when the state was applied. #[inline] - pub fn set_resizable(&mut self, resizable: bool) { + pub fn set_resizable(&mut self, resizable: bool) -> bool { if self.resizable == resizable { - return; + return false; } self.resizable = resizable; @@ -328,12 +519,14 @@ impl WindowState { if let Some(frame) = self.frame.as_mut() { frame.set_resizable(resizable); } + + true } - /// Whether the window is focused. + /// Whether the window is focused by any seat. #[inline] pub fn has_focus(&self) -> bool { - self.has_focus + !self.seat_focus.is_empty() } /// Whether the IME is allowed. @@ -362,66 +555,13 @@ impl WindowState { .map(|configure| configure.decoration_mode == DecorationMode::Client) .unwrap_or(false); if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { - frame.is_hidden() + !frame.is_hidden() } else { // Server side decorations. true } } - /// Create new window state. - pub fn new( - connection: Connection, - queue_handle: &QueueHandle, - winit_state: &WinitState, - size: LogicalSize, - window: Window, - theme: Option, - ) -> Self { - let compositor = winit_state.compositor_state.clone(); - let pointer_constraints = winit_state.pointer_constraints.clone(); - let viewport = winit_state - .viewporter_state - .as_ref() - .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); - let fractional_scale = winit_state - .fractional_scaling_manager - .as_ref() - .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); - - Self { - compositor, - connection, - theme, - csd_fails: false, - decorate: true, - cursor_grab_mode: GrabState::new(), - cursor_icon: CursorIcon::Default, - cursor_visible: true, - fractional_scale, - frame: None, - has_focus: false, - ime_allowed: false, - ime_purpose: ImePurpose::Normal, - last_configure: None, - max_inner_size: None, - min_inner_size: MIN_WINDOW_SIZE, - pointer_constraints, - pointers: Default::default(), - queue_handle: queue_handle.clone(), - scale_factor: 1., - shm: winit_state.shm.wl_shm().clone(), - size, - stateless_size: size, - text_inputs: Vec::new(), - title: String::default(), - transparent: false, - resizable: true, - viewport, - window: ManuallyDrop::new(window), - } - } - /// Get the outer size of the window. #[inline] pub fn outer_size(&self) -> LogicalSize { @@ -457,14 +597,12 @@ impl WindowState { /// Refresh the decorations frame if it's present returning whether the client should redraw. pub fn refresh_frame(&mut self) -> bool { if let Some(frame) = self.frame.as_mut() { - let dirty = frame.is_dirty(); - if dirty { - frame.draw(); + if !frame.is_hidden() && frame.is_dirty() { + return frame.draw(); } - dirty - } else { - false } + + false } /// Reload the cursor style on the given window. @@ -490,8 +628,22 @@ impl WindowState { } } + /// Try to resize the window when the user can do so. + pub fn request_inner_size(&mut self, inner_size: Size) -> PhysicalSize { + if self + .last_configure + .as_ref() + .map(Self::is_stateless) + .unwrap_or(true) + { + self.resize(inner_size.to_logical(self.scale_factor())) + } + + logical_to_physical_rounded(self.inner_size(), self.scale_factor()) + } + /// Resize the window to the new inner size. - pub fn resize(&mut self, inner_size: LogicalSize) { + fn resize(&mut self, inner_size: LogicalSize) { self.size = inner_size; // Update the stateless size. @@ -551,20 +703,8 @@ impl WindowState { return; } - self.apply_on_poiner(|pointer, data| { - let surface = data.cursor_surface(); - let scale_factor = surface.data::().unwrap().scale_factor(); - - if pointer - .set_cursor( - &self.connection, - cursor_icon.name(), - &self.shm, - surface, - scale_factor, - ) - .is_err() - { + self.apply_on_poiner(|pointer, _| { + if pointer.set_cursor(&self.connection, cursor_icon).is_err() { warn!("Failed to set cursor to {:?}", cursor_icon); } }) @@ -618,9 +758,14 @@ impl WindowState { /// Set the cursor grabbing state on the top-level. pub fn set_cursor_grab(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { - // Replace the user grabbing mode. + if self.cursor_grab_mode.user_grab_mode == mode { + return Ok(()); + } + + self.set_cursor_grab_inner(mode)?; + // Update user grab on success. self.cursor_grab_mode.user_grab_mode = mode; - self.set_cursor_grab_inner(mode) + Ok(()) } /// Reload the hints for minimum and maximum sizes. @@ -668,6 +813,15 @@ impl WindowState { Ok(()) } + pub fn show_window_menu(&self, position: LogicalPosition) { + // TODO(kchibisov) handle touch serials. + self.apply_on_poiner(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + self.window.show_window_menu(seat, serial, position.into()); + }); + } + /// Set the position of the cursor. pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { if self.pointer_constraints.is_none() { @@ -677,7 +831,7 @@ impl WindowState { // Positon can be set only for locked cursor. if self.cursor_grab_mode.current_grab_mode != CursorGrabMode::Locked { return Err(ExternalError::Os(os_error!( - crate::platform_impl::OsError::WaylandMisc( + crate::platform_impl::OsError::Misc( "cursor position can be set only for locked cursor." ) ))); @@ -739,12 +893,16 @@ impl WindowState { } } - /// Mark that the window has focus. - /// - /// Should be used from routine that sends focused event. + /// Add seat focus for the window. #[inline] - pub fn set_has_focus(&mut self, has_focus: bool) { - self.has_focus = has_focus; + pub fn add_seat_focus(&mut self, seat: ObjectId) { + self.seat_focus.insert(seat); + } + + /// Remove seat focus from the window. + #[inline] + pub fn remove_seat_focus(&mut self, seat: &ObjectId) { + self.seat_focus.remove(seat); } /// Returns `true` if the requested state was applied. @@ -768,7 +926,7 @@ impl WindowState { /// Set the IME position. pub fn set_ime_cursor_area(&self, position: LogicalPosition, size: LogicalSize) { - // XXX This won't fly unless user will have a way to request IME window per seat, since + // FIXME: This won't fly unless user will have a way to request IME window per seat, since // the ime windows will be overlapping, but winit doesn't expose API to specify for // which seat we're setting IME position. let (x, y) = (position.x as i32, position.y as i32); @@ -799,10 +957,34 @@ impl WindowState { pub fn set_scale_factor(&mut self, scale_factor: f64) { self.scale_factor = scale_factor; - // XXX when fractional scaling is not used update the buffer scale. + // NOTE: When fractional scaling is not used update the buffer scale. if self.fractional_scale.is_none() { let _ = self.window.set_buffer_scale(self.scale_factor as _); } + + if let Some(frame) = self.frame.as_mut() { + frame.set_scaling_factor(scale_factor); + } + } + + /// Make window background blurred + #[inline] + pub fn set_blur(&mut self, blurred: bool) { + if blurred && self.blur.is_none() { + if let Some(blur_manager) = self.blur_manager.as_ref() { + let blur = blur_manager.blur(self.window.wl_surface(), &self.queue_handle); + blur.commit(); + self.blur = Some(blur); + } else { + info!("Blur manager unavailable, unable to change blur") + } + } else if !blurred && self.blur.is_some() { + self.blur_manager + .as_ref() + .unwrap() + .unset(self.window.wl_surface()); + self.blur.take().unwrap().release(); + } } /// Set the window title to a new value. @@ -860,31 +1042,70 @@ impl WindowState { impl Drop for WindowState { fn drop(&mut self) { - let surface = self.window.wl_surface().clone(); - unsafe { - ManuallyDrop::drop(&mut self.window); + if let Some(blur) = self.blur.take() { + blur.release(); + } + + if let Some(fs) = self.fractional_scale.take() { + fs.destroy(); } - surface.destroy(); + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + + // NOTE: the wl_surface used by the window is being cleaned up when + // dropping SCTK `Window`. } } -impl From for ResizeEdge { +/// The state of the cursor grabs. +#[derive(Clone, Copy)] +struct GrabState { + /// The grab mode requested by the user. + user_grab_mode: CursorGrabMode, + + /// The current grab mode. + current_grab_mode: CursorGrabMode, +} + +impl GrabState { + fn new() -> Self { + Self { + user_grab_mode: CursorGrabMode::None, + current_grab_mode: CursorGrabMode::None, + } + } +} + +/// The state of the frame callback. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameCallbackState { + /// No frame callback was requsted. + #[default] + None, + /// The frame callback was requested, but not yet arrived, the redraw events are throttled. + Requested, + /// The callback was marked as done, and user could receive redraw requested + Received, +} + +impl From for XdgResizeEdge { fn from(value: ResizeDirection) -> Self { match value { - ResizeDirection::North => ResizeEdge::Top, - ResizeDirection::West => ResizeEdge::Left, - ResizeDirection::NorthWest => ResizeEdge::TopLeft, - ResizeDirection::NorthEast => ResizeEdge::TopRight, - ResizeDirection::East => ResizeEdge::Right, - ResizeDirection::SouthWest => ResizeEdge::BottomLeft, - ResizeDirection::SouthEast => ResizeEdge::BottomRight, - ResizeDirection::South => ResizeEdge::Bottom, + ResizeDirection::North => XdgResizeEdge::Top, + ResizeDirection::West => XdgResizeEdge::Left, + ResizeDirection::NorthWest => XdgResizeEdge::TopLeft, + ResizeDirection::NorthEast => XdgResizeEdge::TopRight, + ResizeDirection::East => XdgResizeEdge::Right, + ResizeDirection::SouthWest => XdgResizeEdge::BottomLeft, + ResizeDirection::SouthEast => XdgResizeEdge::BottomRight, + ResizeDirection::South => XdgResizeEdge::Bottom, } } } -// XXX rust doesn't allow from `Option`. +// NOTE: Rust doesn't allow `From>`. #[cfg(feature = "sctk-adwaita")] fn into_sctk_adwaita_config(theme: Option) -> sctk_adwaita::FrameConfig { match theme { diff --git a/src/platform_impl/linux/x11/activation.rs b/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 0000000000..7ae910d506 --- /dev/null +++ b/src/platform_impl/linux/x11/activation.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! X11 activation handling. +//! +//! X11 has a "startup notification" specification similar to Wayland's, see this URL: +//! + +use super::{atoms::*, VoidCookie, X11Error, XConnection}; + +use std::ffi::CString; +use std::fmt::Write; + +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +impl XConnection { + /// "Request" a new activation token from the server. + pub(crate) fn request_activation_token(&self, window_title: &str) -> Result { + // The specification recommends the format "hostname+pid+"_TIME"+current time" + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + let time = self.timestamp(); + + let activation_token = format!( + "{}{}_TIME{}", + uname.nodename().to_str().unwrap_or("winit"), + pid.as_raw_nonzero(), + time + ); + + // Set up the new startup notification. + let notification = { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"new: ID="); + quote_string(&activation_token, &mut buffer); + buffer.extend_from_slice(b" NAME="); + quote_string(window_title, &mut buffer); + buffer.extend_from_slice(b" SCREEN="); + push_display(&mut buffer, &self.default_screen_index()); + + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + self.send_message(¬ification)?; + + Ok(activation_token) + } + + /// Finish launching a window with the given startup ID. + pub(crate) fn remove_activation_token( + &self, + window: xproto::Window, + startup_id: &str, + ) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Set the _NET_STARTUP_ID property on the window. + self.xcb_connection() + .change_property( + xproto::PropMode::REPLACE, + window, + atoms[_NET_STARTUP_ID], + xproto::AtomEnum::STRING, + 8, + startup_id.len().try_into().unwrap(), + startup_id.as_bytes(), + )? + .check()?; + + // Send the message indicating that the startup is over. + let message = { + const MESSAGE_ROOT: &str = "remove: ID="; + + let mut buffer = Vec::with_capacity( + MESSAGE_ROOT + .len() + .checked_add(startup_id.len()) + .and_then(|x| x.checked_add(1)) + .unwrap(), + ); + buffer.extend_from_slice(MESSAGE_ROOT.as_bytes()); + quote_string(startup_id, &mut buffer); + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + + self.send_message(&message) + } + + /// Send a startup notification message to the window manager. + fn send_message(&self, message: &[u8]) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Create a new window to send the message over. + let screen = self.default_root(); + let window = xproto::WindowWrapper::create_window( + self.xcb_connection(), + screen.root_depth, + screen.root, + -100, + -100, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new() + .override_redirect(1) + .event_mask( + xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + // Serialize the messages in 20-byte chunks. + let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN]; + message + .chunks(20) + .map(|chunk| { + let mut buffer = [0u8; 20]; + buffer[..chunk.len()].copy_from_slice(chunk); + let event = + xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer); + + // Set the message type to the continuation atom for the next chunk. + message_type = atoms[_NET_STARTUP_INFO]; + + event + }) + .try_for_each(|event| { + // Send each event in order. + self.xcb_connection() + .send_event( + false, + screen.root, + xproto::EventMask::PROPERTY_CHANGE, + event, + ) + .map(VoidCookie::ignore_error) + })?; + + Ok(()) + } +} + +/// Quote a literal string as per the startup notification specification. +fn quote_string(s: &str, target: &mut Vec) { + let total_len = s.len().checked_add(3).expect("quote string overflow"); + target.reserve(total_len); + + // Add the opening quote. + target.push(b'"'); + + // Iterate over the string split by literal quotes. + s.as_bytes().split(|&b| b == b'"').for_each(|part| { + // Add the part. + target.extend_from_slice(part); + + // Escape the quote. + target.push(b'\\'); + target.push(b'"'); + }); + + // Un-escape the last quote. + target.remove(target.len() - 2); +} + +/// Push a `Display` implementation to the buffer. +fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { + struct Writer<'a> { + buffer: &'a mut Vec, + } + + impl<'a> std::fmt::Write for Writer<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + write!(Writer { buffer }, "{}", display).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn properly_escapes_x11_messages() { + let assert_eq = |input: &str, output: &[u8]| { + let mut buf = vec![]; + quote_string(input, &mut buf); + assert_eq!(buf, output); + }; + + assert_eq("", b"\"\""); + assert_eq("foo", b"\"foo\""); + assert_eq("foo\"bar", b"\"foo\\\"bar\""); + } +} diff --git a/src/platform_impl/linux/x11/atoms.rs b/src/platform_impl/linux/x11/atoms.rs new file mode 100644 index 0000000000..c6d96c8fe0 --- /dev/null +++ b/src/platform_impl/linux/x11/atoms.rs @@ -0,0 +1,117 @@ +//! Collects every atom used by the platform implementation. + +use core::ops::Index; + +macro_rules! atom_manager { + ($($name:ident $(:$lit:literal)?),*) => { + x11rb::atom_manager! { + /// The atoms used by `winit` + pub Atoms: AtomsCookie { + $($name $(:$lit)?,)* + } + } + + /// Indices into the `Atoms` struct. + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub enum AtomName { + $($name,)* + } + + impl AtomName { + pub(crate) fn atom_from( + self, + atoms: &Atoms + ) -> &x11rb::protocol::xproto::Atom { + match self { + $(AtomName::$name => &atoms.$name,)* + } + } + } + }; +} + +atom_manager! { + // General Use Atoms + CARD32, + UTF8_STRING, + WM_CHANGE_STATE, + WM_CLIENT_MACHINE, + WM_DELETE_WINDOW, + WM_PROTOCOLS, + WM_STATE, + XIM_SERVERS, + + // Assorted ICCCM Atoms + _NET_WM_ICON, + _NET_WM_MOVERESIZE, + _NET_WM_NAME, + _NET_WM_PID, + _NET_WM_PING, + _NET_WM_STATE, + _NET_WM_STATE_ABOVE, + _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_WINDOW_TYPE, + + // Activation atoms. + _NET_STARTUP_INFO_BEGIN, + _NET_STARTUP_INFO, + _NET_STARTUP_ID, + + // WM window types. + _NET_WM_WINDOW_TYPE_DESKTOP, + _NET_WM_WINDOW_TYPE_DOCK, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_COMBO, + _NET_WM_WINDOW_TYPE_DND, + _NET_WM_WINDOW_TYPE_NORMAL, + + // Drag-N-Drop Atoms + XdndAware, + XdndEnter, + XdndLeave, + XdndDrop, + XdndPosition, + XdndStatus, + XdndActionPrivate, + XdndSelection, + XdndFinished, + XdndTypeList, + TextUriList: b"text/uri-list", + None: b"None", + + // Miscellaneous Atoms + _GTK_THEME_VARIANT, + _MOTIF_WM_HINTS, + _NET_ACTIVE_WINDOW, + _NET_CLIENT_LIST, + _NET_FRAME_EXTENTS, + _NET_SUPPORTED, + _NET_SUPPORTING_WM_CHECK, + _XEMBED, + _XSETTINGS_SETTINGS +} + +impl Index for Atoms { + type Output = x11rb::protocol::xproto::Atom; + + fn index(&self, index: AtomName) -> &Self::Output { + index.atom_from(self) + } +} + +pub(crate) use AtomName::*; +// Make sure `None` is still defined. +pub(crate) use core::option::Option::None; diff --git a/src/platform_impl/linux/x11/dnd.rs b/src/platform_impl/linux/x11/dnd.rs index 198494e184..de5ad50108 100644 --- a/src/platform_impl/linux/x11/dnd.rs +++ b/src/platform_impl/linux/x11/dnd.rs @@ -7,55 +7,12 @@ use std::{ }; use percent_encoding::percent_decode; +use x11rb::protocol::xproto::{self, ConnectionExt}; -use super::{ffi, util, XConnection, XError}; - -#[derive(Debug)] -pub(crate) struct DndAtoms { - pub enter: ffi::Atom, - pub leave: ffi::Atom, - pub drop: ffi::Atom, - pub position: ffi::Atom, - pub status: ffi::Atom, - pub action_private: ffi::Atom, - pub selection: ffi::Atom, - pub finished: ffi::Atom, - pub type_list: ffi::Atom, - pub uri_list: ffi::Atom, - pub none: ffi::Atom, -} - -impl DndAtoms { - pub fn new(xconn: &Arc) -> Result { - let names = [ - b"XdndEnter\0".as_ptr() as *mut c_char, - b"XdndLeave\0".as_ptr() as *mut c_char, - b"XdndDrop\0".as_ptr() as *mut c_char, - b"XdndPosition\0".as_ptr() as *mut c_char, - b"XdndStatus\0".as_ptr() as *mut c_char, - b"XdndActionPrivate\0".as_ptr() as *mut c_char, - b"XdndSelection\0".as_ptr() as *mut c_char, - b"XdndFinished\0".as_ptr() as *mut c_char, - b"XdndTypeList\0".as_ptr() as *mut c_char, - b"text/uri-list\0".as_ptr() as *mut c_char, - b"None\0".as_ptr() as *mut c_char, - ]; - let atoms = unsafe { xconn.get_atoms(&names) }?; - Ok(DndAtoms { - enter: atoms[0], - leave: atoms[1], - drop: atoms[2], - position: atoms[3], - status: atoms[4], - action_private: atoms[5], - selection: atoms[6], - finished: atoms[7], - type_list: atoms[8], - uri_list: atoms[9], - none: atoms[10], - }) - } -} +use super::{ + atoms::{AtomName::None as DndNone, *}, + util, CookieResultExt, X11Error, XConnection, +}; #[derive(Debug, Clone, Copy)] pub enum DndState { @@ -84,24 +41,21 @@ impl From for DndDataParseError { } } -pub(crate) struct Dnd { +pub struct Dnd { xconn: Arc, - pub atoms: DndAtoms, // Populated by XdndEnter event handler pub version: Option, - pub type_list: Option>, + pub type_list: Option>, // Populated by XdndPosition event handler - pub source_window: Option, + pub source_window: Option, // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) pub result: Option, DndDataParseError>>, } impl Dnd { - pub fn new(xconn: Arc) -> Result { - let atoms = DndAtoms::new(&xconn)?; + pub fn new(xconn: Arc) -> Result { Ok(Dnd { xconn, - atoms, version: None, type_list: None, source_window: None, @@ -118,71 +72,85 @@ impl Dnd { pub unsafe fn send_status( &self, - this_window: c_ulong, - target_window: c_ulong, + this_window: xproto::Window, + target_window: xproto::Window, state: DndState, - ) -> Result<(), XError> { + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); let (accepted, action) = match state { - DndState::Accepted => (1, self.atoms.action_private as c_long), - DndState::Rejected => (0, self.atoms.none as c_long), + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), }; self.xconn .send_client_msg( target_window, target_window, - self.atoms.status, + atoms[XdndStatus] as _, None, - [this_window as c_long, accepted, 0, 0, action], - ) - .flush() + [this_window, accepted, 0, 0, action as _], + )? + .ignore_error(); + + Ok(()) } pub unsafe fn send_finished( &self, - this_window: c_ulong, - target_window: c_ulong, + this_window: xproto::Window, + target_window: xproto::Window, state: DndState, - ) -> Result<(), XError> { + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); let (accepted, action) = match state { - DndState::Accepted => (1, self.atoms.action_private as c_long), - DndState::Rejected => (0, self.atoms.none as c_long), + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), }; self.xconn .send_client_msg( target_window, target_window, - self.atoms.finished, + atoms[XdndFinished] as _, None, - [this_window as c_long, accepted, action, 0, 0], - ) - .flush() + [this_window, accepted, action as _, 0, 0], + )? + .ignore_error(); + + Ok(()) } pub unsafe fn get_type_list( &self, - source_window: c_ulong, - ) -> Result, util::GetPropertyError> { - self.xconn - .get_property(source_window, self.atoms.type_list, ffi::XA_ATOM) + source_window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property( + source_window, + atoms[XdndTypeList], + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) } - pub unsafe fn convert_selection(&self, window: c_ulong, time: c_ulong) { - (self.xconn.xlib.XConvertSelection)( - self.xconn.display, - self.atoms.selection, - self.atoms.uri_list, - self.atoms.selection, - window, - time, - ); + pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + let atoms = self.xconn.atoms(); + self.xconn + .xcb_connection() + .convert_selection( + window, + atoms[XdndSelection], + atoms[TextUriList], + atoms[XdndSelection], + time, + ) + .expect_then_ignore_error("Failed to send XdndSelection event") } pub unsafe fn read_data( &self, - window: c_ulong, + window: xproto::Window, ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); self.xconn - .get_property(window, self.atoms.selection, self.atoms.uri_list) + .get_property(window, atoms[XdndSelection], atoms[TextUriList]) } pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index d8f076be01..92a71ecaba 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -1,92 +1,311 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc, slice, sync::Arc}; - -use libc::{c_char, c_int, c_long, c_ulong}; - -use super::{ - ffi, get_xtarget, mkdid, mkwid, monitor, util, Device, DeviceId, DeviceInfo, Dnd, DndState, - GenericEventCookie, ImeReceiver, ScrollOrientation, UnownedWindow, WindowId, XExtension, +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::os::raw::{c_char, c_int, c_long, c_ulong}; +use std::slice; +use std::sync::{Arc, Mutex}; + +use x11_dl::xinput2::{ + self, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, XIHierarchyEvent, + XILeaveEvent, XIModifierState, XIRawEvent, }; - +use x11_dl::xlib::{ + self, Display as XDisplay, Window as XWindow, XAnyEvent, XClientMessageEvent, XConfigureEvent, + XDestroyWindowEvent, XEvent, XExposeEvent, XKeyEvent, XMapEvent, XPropertyEvent, + XReparentEvent, XSelectionEvent, XVisibilityEvent, XkbAnyEvent, XkbStateRec, +}; +use x11rb::protocol::xinput; +use x11rb::protocol::xkb::ID as XkbId; +use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; +use x11rb::x11_utils::ExtensionInformation; +use x11rb::x11_utils::Serialize; +use xkbcommon_dl::xkb_mod_mask_t; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Event, Ime, MouseScrollDelta, RawKeyEvent, Touch, TouchPhase, + WindowEvent, +}; +use crate::event::{InnerSizeWriter, MouseButton}; +use crate::event_loop::EventLoopWindowTarget as RootELW; +use crate::keyboard::ModifiersState; +use crate::platform_impl::common::xkb::{self, XkbState}; +use crate::platform_impl::platform::common::xkb::Context; use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}; -use crate::{ - dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, ElementState, Event, Ime, RawKeyEvent, TouchPhase, WindowEvent}, - event_loop::EventLoopWindowTarget as RootELW, - keyboard::ModifiersState, - platform_impl::platform::common::{keymap, xkb_state::KbdState}, +use crate::platform_impl::platform::x11::EventLoopWindowTarget; +use crate::platform_impl::platform::EventLoopWindowTarget as PlatformEventLoopWindowTarget; +use crate::platform_impl::x11::util::cookie::GenericEventCookie; +use crate::platform_impl::x11::{ + atoms::*, mkdid, mkwid, util, CookieResultExt, Device, DeviceId, DeviceInfo, Dnd, DndState, + ImeReceiver, ScrollOrientation, UnownedWindow, WindowId, }; +/// The maximum amount of X modifiers to replay. +pub const MAX_MOD_REPLAY_LEN: usize = 32; + /// 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, - pub(super) xkbext: XExtension, - pub(super) target: Rc>, - pub(super) kb_state: KbdState, +pub struct EventProcessor { + pub dnd: Dnd, + pub ime_receiver: ImeReceiver, + pub ime_event_receiver: ImeEventReceiver, + pub randr_event_offset: u8, + pub devices: RefCell>, + pub xi2ext: ExtensionInformation, + pub xkbext: ExtensionInformation, + pub target: RootELW, + pub xkb_context: Context, // Number of touch events currently in progress - pub(super) num_touch: u32, - // Whether we've got a key release for the key press. - pub(super) got_key_release: bool, - pub(super) first_touch: Option, + pub num_touch: u32, + // This is the last pressed key that is repeatable (if it hasn't been + // released). + // + // Used to detect key repeats. + pub held_key_press: Option, + pub first_touch: Option, // Currently focused window belonging to this process - pub(super) active_window: Option, - pub(super) is_composing: bool, + pub active_window: Option, + /// Latest modifiers we've sent for the user to trigger change in event. + pub modifiers: Cell, + pub xfiltered_modifiers: VecDeque, + pub xmodmap: util::ModifierKeymap, + pub is_composing: bool, } impl EventProcessor { - pub(super) fn init_device(&self, device: c_int) { - let wt = get_xtarget(&self.target); - 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(info)); + pub fn process_event(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + self.process_xevent(xev, &mut callback); + + let window_target = Self::window_target_mut(&mut self.target); + + // Handle IME requests. + while let Ok(request) = self.ime_receiver.try_recv() { + let ime = match window_target.ime.as_mut() { + Some(ime) => ime, + None => continue, + }; + let ime = ime.get_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); + } } } + + // Drain IME events. + while let Ok((window, event)) = self.ime_event_receiver.try_recv() { + let window_id = mkwid(window as xproto::Window); + let event = match event { + ImeEvent::Enabled => WindowEvent::Ime(Ime::Enabled), + ImeEvent::Start => { + self.is_composing = true; + WindowEvent::Ime(Ime::Preedit("".to_owned(), None)) + } + ImeEvent::Update(text, position) if self.is_composing => { + WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))) + } + ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + WindowEvent::Ime(Ime::Preedit(String::new(), None)) + } + ImeEvent::Disabled => { + self.is_composing = false; + WindowEvent::Ime(Ime::Disabled) + } + _ => continue, + }; + + callback(&self.target, Event::WindowEvent { window_id, event }); + } + } + + /// XFilterEvent tells us when an event has been discarded by the input method. + /// Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, + /// along with an extra copy of the KeyRelease events. This also prevents backspace and + /// arrow keys from being detected twice. + fn filter_event(&mut self, xev: &mut XEvent) -> bool { + let wt = Self::window_target(&self.target); + unsafe { + (wt.xconn.xlib.XFilterEvent)(xev, { + let xev: &XAnyEvent = xev.as_ref(); + xev.window + }) == xlib::True + } } - fn with_window(&self, window_id: ffi::Window, callback: F) -> Option + fn process_xevent(&mut self, xev: &mut XEvent, mut callback: F) where - F: Fn(&Arc) -> Ret, + F: FnMut(&RootELW, Event), { - let mut deleted = false; - let window_id = WindowId(window_id as _); - let wt = get_xtarget(&self.target); - let result = wt - .windows - .borrow() - .get(&window_id) - .and_then(|window| { - let arc = window.upgrade(); - deleted = arc.is_none(); - arc - }) - .map(|window| callback(&window)); - if deleted { - // Garbage collection - wt.windows.borrow_mut().remove(&window_id); + let event_type = xev.get_type(); + + if self.filter_event(xev) { + if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + let xev: &XKeyEvent = xev.as_ref(); + if self.xmodmap.is_modifier(xev.keycode as u8) { + // Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen + // when the modifiers are consumed entirely or serials are altered. + // + // Both cases shouldn't happen in well behaving clients. + if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN { + self.xfiltered_modifiers.pop_back(); + } + self.xfiltered_modifiers.push_front(xev.serial); + } + } + return; } - result - } - fn window_exists(&self, window_id: ffi::Window) -> bool { - self.with_window(window_id, |_| ()).is_some() + match event_type { + xlib::ClientMessage => self.client_message(xev.as_ref(), &mut callback), + xlib::SelectionNotify => self.selection_notify(xev.as_ref(), &mut callback), + xlib::ConfigureNotify => self.configure_notify(xev.as_ref(), &mut callback), + xlib::ReparentNotify => self.reparent_notify(xev.as_ref()), + xlib::MapNotify => self.map_notify(xev.as_ref(), &mut callback), + xlib::DestroyNotify => self.destroy_notify(xev.as_ref(), &mut callback), + xlib::PropertyNotify => self.property_notify(xev.as_ref(), &mut callback), + xlib::VisibilityNotify => self.visibility_notify(xev.as_ref(), &mut callback), + xlib::Expose => self.expose(xev.as_ref(), &mut callback), + // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events. + ty @ xlib::KeyPress | ty @ xlib::KeyRelease => { + let state = if ty == xlib::KeyPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + self.xinput_key_input(xev.as_mut(), state, &mut callback); + } + xlib::GenericEvent => { + let wt = Self::window_target(&self.target); + let xev: GenericEventCookie = + match GenericEventCookie::from_event(wt.xconn.clone(), *xev) { + Some(xev) if xev.extension() == self.xi2ext.major_opcode => xev, + _ => return, + }; + + let evtype = xev.evtype(); + + match evtype { + ty @ xinput2::XI_ButtonPress | ty @ xinput2::XI_ButtonRelease => { + let state = if ty == xinput2::XI_ButtonPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_button_input(xev, state, &mut callback); + } + xinput2::XI_Motion => { + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_motion(xev, &mut callback); + } + xinput2::XI_Enter => { + let xev: &XIEnterEvent = unsafe { xev.as_event() }; + self.xinput2_mouse_enter(xev, &mut callback); + } + xinput2::XI_Leave => { + let xev: &XILeaveEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_left(xev, &mut callback); + } + xinput2::XI_FocusIn => { + let xev: &XIFocusInEvent = unsafe { xev.as_event() }; + self.xinput2_focused(xev, &mut callback); + } + xinput2::XI_FocusOut => { + let xev: &XIFocusOutEvent = unsafe { xev.as_event() }; + self.xinput2_unfocused(xev, &mut callback); + } + xinput2::XI_TouchBegin | xinput2::XI_TouchUpdate | xinput2::XI_TouchEnd => { + let phase = match evtype { + xinput2::XI_TouchBegin => TouchPhase::Started, + xinput2::XI_TouchUpdate => TouchPhase::Moved, + xinput2::XI_TouchEnd => TouchPhase::Ended, + _ => unreachable!(), + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.xinput2_touch(xev, phase, &mut callback); + } + xinput2::XI_RawButtonPress | xinput2::XI_RawButtonRelease => { + let state = match evtype { + xinput2::XI_RawButtonPress => ElementState::Pressed, + xinput2::XI_RawButtonRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_button_input(xev, state, &mut callback); + } + xinput2::XI_RawMotion => { + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_mouse_motion(xev, &mut callback); + } + xinput2::XI_RawKeyPress | xinput2::XI_RawKeyRelease => { + let state = match evtype { + xinput2::XI_RawKeyPress => ElementState::Pressed, + xinput2::XI_RawKeyRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &xinput2::XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_key_input(xev, state, &mut callback); + } + + xinput2::XI_HierarchyChanged => { + let xev: &XIHierarchyEvent = unsafe { xev.as_event() }; + self.xinput2_hierarchy_changed(xev, &mut callback); + } + _ => {} + } + } + _ => { + if event_type == self.xkbext.first_event as _ { + let xev: &XkbAnyEvent = unsafe { &*(xev as *const _ as *const XkbAnyEvent) }; + self.xkb_event(xev, &mut callback); + } + if event_type == self.randr_event_offset as c_int { + self.process_dpi_change(&mut callback); + } + } + } } - pub(super) fn poll(&self) -> bool { - let wt = get_xtarget(&self.target); - let result = unsafe { (wt.xconn.xlib.XPending)(wt.xconn.display) }; + pub fn poll(&self) -> bool { + let window_target = Self::window_target(&self.target); + let result = unsafe { (window_target.xconn.xlib.XPending)(window_target.xconn.display) }; result != 0 } - pub(super) unsafe fn poll_one_event(&mut self, event_ptr: *mut ffi::XEvent) -> bool { - let wt = get_xtarget(&self.target); + pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + let window_target = Self::window_target(&self.target); // This function is used to poll and remove a single event // from the Xlib event queue in a non-blocking, atomic way. // XCheckIfEvent is non-blocking and removes events from queue. @@ -94,1213 +313,1649 @@ impl EventProcessor { // global Xlib mutex. // XPeekEvent does not remove events from the queue. unsafe extern "C" fn predicate( - _display: *mut ffi::Display, - _event: *mut ffi::XEvent, + _display: *mut XDisplay, + _event: *mut XEvent, _arg: *mut c_char, ) -> c_int { // This predicate always returns "true" (1) to accept all events 1 } - let result = (wt.xconn.xlib.XCheckIfEvent)( - wt.xconn.display, - event_ptr, - Some(predicate), - std::ptr::null_mut(), - ); + let result = unsafe { + (window_target.xconn.xlib.XCheckIfEvent)( + window_target.xconn.display, + event_ptr, + Some(predicate), + std::ptr::null_mut(), + ) + }; result != 0 } - pub(super) fn process_event(&mut self, xev: &mut ffi::XEvent, mut callback: F) + pub fn init_device(&self, device: xinput::DeviceId) { + let window_target = Self::window_target(&self.target); + let mut devices = self.devices.borrow_mut(); + if let Some(info) = DeviceInfo::get(&window_target.xconn, device as _) { + for info in info.iter() { + devices.insert(DeviceId(info.deviceid as _), Device::new(info)); + } + } + } + + pub fn with_window(&self, window_id: xproto::Window, callback: F) -> Option where - F: FnMut(Event<'_, T>), + F: Fn(&Arc) -> Ret, { - let wt = get_xtarget(&self.target); - // XFilterEvent tells us when an event has been discarded by the input method. - // Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, - // along with an extra copy of the KeyRelease events. This also prevents backspace and - // arrow keys from being detected twice. - if ffi::True - == unsafe { - (wt.xconn.xlib.XFilterEvent)(xev, { - let xev: &ffi::XAnyEvent = xev.as_ref(); - xev.window - }) - } - { - return; + let mut deleted = false; + let window_id = WindowId(window_id as _); + let window_target = Self::window_target(&self.target); + let result = window_target + .windows + .borrow() + .get(&window_id) + .and_then(|window| { + let arc = window.upgrade(); + deleted = arc.is_none(); + arc + }) + .map(|window| callback(&window)); + + if deleted { + // Garbage collection + window_target.windows.borrow_mut().remove(&window_id); } - let event_type = xev.get_type(); - match event_type { - ffi::ClientMessage => { - let client_msg: &ffi::XClientMessageEvent = xev.as_ref(); + result + } - let window = client_msg.window; - let window_id = mkwid(window); + // NOTE: we avoid `self` to not borrow the entire `self` as not mut. + /// Get the platform window target. + pub fn window_target(window_target: &RootELW) -> &EventLoopWindowTarget { + match &window_target.p { + PlatformEventLoopWindowTarget::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } - if client_msg.data.get_long(0) as ffi::Atom == wt.wm_delete_window { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::CloseRequested, - }); - } else if client_msg.data.get_long(0) as ffi::Atom == wt.net_wm_ping { - let response_msg: &mut ffi::XClientMessageEvent = xev.as_mut(); - response_msg.window = wt.root; - wt.xconn - .send_event( - wt.root, - Some(ffi::SubstructureNotifyMask | ffi::SubstructureRedirectMask), - *response_msg, - ) - .queue(); - } else if client_msg.message_type == self.dnd.atoms.enter { - let source_window = client_msg.data.get_long(0) as c_ulong; - let flags = client_msg.data.get_long(1); - let version = flags >> 24; - self.dnd.version = Some(version); - let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1; - if !has_more_types { - let type_list = vec![ - client_msg.data.get_long(2) as c_ulong, - client_msg.data.get_long(3) as c_ulong, - client_msg.data.get_long(4) as c_ulong, - ]; - self.dnd.type_list = Some(type_list); - } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } - { - self.dnd.type_list = Some(more_types); - } - } else if client_msg.message_type == self.dnd.atoms.position { - // This event occurs every time the mouse moves while a file's being dragged - // over our window. We emit HoveredFile in response; while the macOS backend - // does that upon a drag entering, XDND doesn't have access to the actual drop - // data until this event. For parity with other platforms, we only emit - // `HoveredFile` the first time, though if winit's API is later extended to - // supply position updates with `HoveredFile` or another event, implementing - // that here would be trivial. - - let source_window = client_msg.data.get_long(0) as c_ulong; - - // Equivalent to `(x << shift) | y` - // where `shift = mem::size_of::() * 8` - // Note that coordinates are in "desktop space", not "window space" - // (in X11 parlance, they're root window coordinates) - //let packed_coordinates = client_msg.data.get_long(2); - //let shift = mem::size_of::() * 8; - //let x = packed_coordinates >> shift; - //let y = packed_coordinates & !(x << shift); - - // By our own state flow, `version` should never be `None` at this point. - let version = self.dnd.version.unwrap_or(5); - - // Action is specified in versions 2 and up, though we don't need it anyway. - //let action = client_msg.data.get_long(4); - - let accepted = if let Some(ref type_list) = self.dnd.type_list { - type_list.contains(&self.dnd.atoms.uri_list) - } else { - false - }; + /// Get the platform window target. + pub fn window_target_mut(window_target: &mut RootELW) -> &mut EventLoopWindowTarget { + match &mut window_target.p { + PlatformEventLoopWindowTarget::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } - if accepted { - self.dnd.source_window = Some(source_window); - unsafe { - if self.dnd.result.is_none() { - let time = if version >= 1 { - client_msg.data.get_long(3) as c_ulong - } else { - // In version 0, time isn't specified - ffi::CurrentTime - }; - // This results in the `SelectionNotify` event below - self.dnd.convert_selection(window, time); - } - self.dnd - .send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } - } else { - unsafe { - self.dnd - .send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); - } - self.dnd.reset(); - } - } else if client_msg.message_type == self.dnd.atoms.drop { - let (source_window, state) = if let Some(source_window) = self.dnd.source_window - { - if let Some(Ok(ref path_list)) = self.dnd.result { - for path in path_list { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::DroppedFile(path.clone()), - }); - } - } - (source_window, DndState::Accepted) - } else { - // `source_window` won't be part of our DND state if we already rejected the drop in our - // `XdndPosition` handler. - let source_window = client_msg.data.get_long(0) as c_ulong; - (source_window, DndState::Rejected) - }; - unsafe { - self.dnd - .send_finished(window, source_window, state) - .expect("Failed to send `XdndFinished` message."); - } - self.dnd.reset(); - } else if client_msg.message_type == self.dnd.atoms.leave { - self.dnd.reset(); - callback(Event::WindowEvent { - window_id, - event: WindowEvent::HoveredFileCancelled, - }); - } - } + fn client_message(&mut self, xev: &XClientMessageEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); - ffi::SelectionNotify => { - let xsel: &ffi::XSelectionEvent = xev.as_ref(); - - let window = xsel.requestor; - let window_id = mkwid(window); - - if xsel.property == self.dnd.atoms.selection { - let mut result = None; - - // This is where we receive data from drag and drop - if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { - let parse_result = self.dnd.parse_data(&mut data); - if let Ok(ref path_list) = parse_result { - for path in path_list { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::HoveredFile(path.clone()), - }); - } - } - result = Some(parse_result); - } + let window = xev.window as xproto::Window; + let window_id = mkwid(window); - self.dnd.result = result; - } - } + if xev.data.get_long(0) as xproto::Atom == wt.wm_delete_window { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CloseRequested, + }; + callback(&self.target, event); + return; + } - ffi::ConfigureNotify => { - let xev: &ffi::XConfigureEvent = xev.as_ref(); - let xwindow = xev.window; - let window_id = mkwid(xwindow); - - if let Some(window) = self.with_window(xwindow, Arc::clone) { - // So apparently... - // `XSendEvent` (synthetic `ConfigureNotify`) -> position relative to root - // `XConfigureNotify` (real `ConfigureNotify`) -> position relative to parent - // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.5 - // We don't want to send `Moved` when this is false, since then every `Resized` - // (whether the window moved or not) is accompanied by an extraneous `Moved` event - // that has a position relative to the parent window. - let is_synthetic = xev.send_event == ffi::True; - - // These are both in physical space. - let new_inner_size = (xev.width as u32, xev.height as u32); - let new_inner_position = (xev.x, xev.y); - - let (mut resized, moved) = { - let mut shared_state_lock = window.shared_state_lock(); - - let resized = - util::maybe_change(&mut shared_state_lock.size, new_inner_size); - let moved = if is_synthetic { - util::maybe_change( - &mut shared_state_lock.inner_position, - new_inner_position, - ) - } else { - // Detect when frame extents change. - // Since this isn't synthetic, as per the notes above, this position is relative to the - // parent window. - let rel_parent = new_inner_position; - if util::maybe_change( - &mut shared_state_lock.inner_position_rel_parent, - rel_parent, - ) { - // This ensures we process the next `Moved`. - shared_state_lock.inner_position = None; - // Extra insurance against stale frame extents. - shared_state_lock.frame_extents = None; - } - false - }; - (resized, moved) - }; + if xev.data.get_long(0) as xproto::Atom == wt.net_wm_ping { + let client_msg = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + format: xev.format as _, + sequence: xev.serial as _, + window: wt.root, + type_: xev.message_type as _, + data: xproto::ClientMessageData::from({ + let [a, b, c, d, e]: [c_long; 5] = xev.data.as_longs().try_into().unwrap(); + [a as u32, b as u32, c as u32, d as u32, e as u32] + }), + }; + + wt.xconn + .xcb_connection() + .send_event( + false, + wt.root, + xproto::EventMask::SUBSTRUCTURE_NOTIFY + | xproto::EventMask::SUBSTRUCTURE_REDIRECT, + client_msg.serialize(), + ) + .expect_then_ignore_error("Failed to send `ClientMessage` event."); + return; + } - let position = window.shared_state_lock().position; - - let new_outer_position = if let (Some(position), false) = (position, moved) { - position - } else { - let mut shared_state_lock = window.shared_state_lock(); - - // We need to convert client area position to window position. - let frame_extents = shared_state_lock - .frame_extents - .as_ref() - .cloned() - .unwrap_or_else(|| { - let frame_extents = - wt.xconn.get_frame_extents_heuristic(xwindow, wt.root); - shared_state_lock.frame_extents = Some(frame_extents.clone()); - frame_extents - }); - let outer = frame_extents - .inner_pos_to_outer(new_inner_position.0, new_inner_position.1); - shared_state_lock.position = Some(outer); - - // Unlock shared state to prevent deadlock in callback below - drop(shared_state_lock); - - if moved { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::Moved(outer.into()), - }); - } - outer - }; + if xev.message_type == atoms[XdndEnter] as c_ulong { + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + let version = flags >> 24; + self.dnd.version = Some(version); + let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1; + if !has_more_types { + let type_list = vec![ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ]; + self.dnd.type_list = Some(type_list); + } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { + self.dnd.type_list = Some(more_types); + } + return; + } - if is_synthetic { - let mut shared_state_lock = window.shared_state_lock(); - // If we don't use the existing adjusted value when available, then the user can screw up the - // resizing by dragging across monitors *without* dropping the window. - let (width, height) = shared_state_lock - .dpi_adjusted - .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 = { - let window_rect = util::AaRect::new(new_outer_position, new_inner_size); - let monitor = wt.xconn.get_monitor_for_window(Some(window_rect)); - - if monitor.is_dummy() { - // Avoid updating monitor using a dummy monitor handle - last_scale_factor - } else { - shared_state_lock.last_monitor = monitor.clone(); - monitor.scale_factor - } - }; - if last_scale_factor != new_scale_factor { - let (new_width, new_height) = window.adjust_for_dpi( - last_scale_factor, - new_scale_factor, - width, - height, - &shared_state_lock, - ); - - let old_inner_size = PhysicalSize::new(width, height); - let mut new_inner_size = PhysicalSize::new(new_width, new_height); - - // Unlock shared state to prevent deadlock in callback below - drop(shared_state_lock); - - callback(Event::WindowEvent { - window_id, - event: WindowEvent::ScaleFactorChanged { - scale_factor: new_scale_factor, - new_inner_size: &mut new_inner_size, - }, - }); - - if new_inner_size != old_inner_size { - window.set_inner_size_physical( - new_inner_size.width, - new_inner_size.height, - ); - window.shared_state_lock().dpi_adjusted = - Some(new_inner_size.into()); - // if the DPI factor changed, force a resize event to ensure the logical - // size is computed with the right DPI factor - resized = true; - } - } - } + if xev.message_type == atoms[XdndPosition] as c_ulong { + // This event occurs every time the mouse moves while a file's being dragged + // over our window. We emit HoveredFile in response; while the macOS backend + // does that upon a drag entering, XDND doesn't have access to the actual drop + // data until this event. For parity with other platforms, we only emit + // `HoveredFile` the first time, though if winit's API is later extended to + // supply position updates with `HoveredFile` or another event, implementing + // that here would be trivial. + + let source_window = xev.data.get_long(0) as xproto::Window; + + // Equivalent to `(x << shift) | y` + // where `shift = mem::size_of::() * 8` + // Note that coordinates are in "desktop space", not "window space" + // (in X11 parlance, they're root window coordinates) + //let packed_coordinates = xev.data.get_long(2); + //let shift = mem::size_of::() * 8; + //let x = packed_coordinates >> shift; + //let y = packed_coordinates & !(x << shift); + + // By our own state flow, `version` should never be `None` at this point. + let version = self.dnd.version.unwrap_or(5); + + // Action is specified in versions 2 and up, though we don't need it anyway. + //let action = xev.data.get_long(4); + + let accepted = if let Some(ref type_list) = self.dnd.type_list { + type_list.contains(&atoms[TextUriList]) + } else { + false + }; + + if !accepted { + unsafe { + self.dnd + .send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + self.dnd.reset(); + return; + } + + self.dnd.source_window = Some(source_window); + if self.dnd.result.is_none() { + let time = if version >= 1 { + xev.data.get_long(3) as xproto::Timestamp + } else { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + }; - let mut shared_state_lock = window.shared_state_lock(); + // Log this timestamp. + wt.xconn.set_timestamp(time); - // This is a hack to ensure that the DPI adjusted resize is actually applied on all WMs. KWin - // doesn't need this, but Xfwm does. The hack should not be run on other WMs, since tiling - // WMs constrain the window size, making the resize fail. This would cause an endless stream of - // XResizeWindow requests, making Xorg, the winit client, and the WM consume 100% of CPU. - if let Some(adjusted_size) = shared_state_lock.dpi_adjusted { - if new_inner_size == adjusted_size || !util::wm_name_is_one_of(&["Xfwm4"]) { - // When this finally happens, the event will not be synthetic. - shared_state_lock.dpi_adjusted = None; - } else { - window.set_inner_size_physical(adjusted_size.0, adjusted_size.1); - } - } + // This results in the `SelectionNotify` event below + unsafe { + self.dnd.convert_selection(window, time); + } + } - // Unlock shared state to prevent deadlock in callback below - drop(shared_state_lock); + unsafe { + self.dnd + .send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + return; + } - if resized { - callback(Event::WindowEvent { + if xev.message_type == atoms[XdndDrop] as c_ulong { + let (source_window, state) = if let Some(source_window) = self.dnd.source_window { + if let Some(Ok(ref path_list)) = self.dnd.result { + for path in path_list { + let event = Event::WindowEvent { window_id, - event: WindowEvent::Resized(new_inner_size.into()), - }); + event: WindowEvent::DroppedFile(path.clone()), + }; + callback(&self.target, event); } } + (source_window, DndState::Accepted) + } else { + // `source_window` won't be part of our DND state if we already rejected the drop in our + // `XdndPosition` handler. + let source_window = xev.data.get_long(0) as xproto::Window; + (source_window, DndState::Rejected) + }; + + unsafe { + self.dnd + .send_finished(window, source_window, state) + .expect("Failed to send `XdndFinished` message."); } - ffi::ReparentNotify => { - let xev: &ffi::XReparentEvent = xev.as_ref(); + self.dnd.reset(); + return; + } - // This is generally a reliable way to detect when the window manager's been - // replaced, though this event is only fired by reparenting window managers - // (which is almost all of them). Failing to correctly update WM info doesn't - // really have much impact, since on the WMs affected (xmonad, dwm, etc.) the only - // effect is that we waste some time trying to query unsupported properties. - wt.xconn.update_cached_wm_info(wt.root); + if xev.message_type == atoms[XdndLeave] as c_ulong { + self.dnd.reset(); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFileCancelled, + }; + callback(&self.target, event); + } + } - self.with_window(xev.window, |window| { - window.invalidate_cached_frame_extents(); - }); - } - ffi::MapNotify => { - let xev: &ffi::XMapEvent = xev.as_ref(); - let window = xev.window; - let window_id = mkwid(window); - - // XXX re-issue the focus state when mapping the window. - // - // The purpose of it is to deliver initial focused state of the newly created - // window, given that we can't rely on `CreateNotify`, due to it being not - // sent. - let focus = self - .with_window(window, |window| window.has_focus()) - .unwrap_or_default(); - callback(Event::WindowEvent { - window_id, - event: WindowEvent::Focused(focus), - }); - } - ffi::DestroyNotify => { - let xev: &ffi::XDestroyWindowEvent = xev.as_ref(); + fn selection_notify(&mut self, xev: &XSelectionEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); - let window = xev.window; - let window_id = mkwid(window); + let window = xev.requestor as xproto::Window; + let window_id = mkwid(window); - // In the event that the window's been destroyed without being dropped first, we - // cleanup again here. - wt.windows.borrow_mut().remove(&WindowId(window as _)); + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); - // Since all XIM stuff needs to happen from the same thread, we destroy the input - // context here instead of when dropping the window. - wt.ime - .borrow_mut() - .remove_context(window) - .expect("Failed to destroy input context"); + if xev.property != atoms[XdndSelection] as c_ulong { + return; + } - callback(Event::WindowEvent { - window_id, - event: WindowEvent::Destroyed, - }); + // This is where we receive data from drag and drop + self.dnd.result = None; + if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { + let parse_result = self.dnd.parse_data(&mut data); + if let Ok(ref path_list) = parse_result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFile(path.clone()), + }; + callback(&self.target, event); + } } + self.dnd.result = Some(parse_result); + } + } - ffi::VisibilityNotify => { - let xev: &ffi::XVisibilityEvent = xev.as_ref(); - let xwindow = xev.window; - callback(Event::WindowEvent { - window_id: mkwid(xwindow), - event: WindowEvent::Occluded(xev.state == ffi::VisibilityFullyObscured), - }); - self.with_window(xwindow, |window| { - window.visibility_notify(); - }); - } + fn configure_notify(&self, xev: &XConfigureEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); - ffi::Expose => { - let xev: &ffi::XExposeEvent = xev.as_ref(); + let xwindow = xev.window as xproto::Window; + let window_id = mkwid(xwindow); - // Multiple Expose events may be received for subareas of a window. - // We issue `RedrawRequested` only for the last event of such a series. - if xev.count == 0 { - let window = xev.window; - let window_id = mkwid(window); + let window = match self.with_window(xwindow, Arc::clone) { + Some(window) => window, + None => return, + }; - callback(Event::RedrawRequested(window_id)); + // So apparently... + // `XSendEvent` (synthetic `ConfigureNotify`) -> position relative to root + // `XConfigureNotify` (real `ConfigureNotify`) -> position relative to parent + // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.5 + // We don't want to send `Moved` when this is false, since then every `Resized` + // (whether the window moved or not) is accompanied by an extraneous `Moved` event + // that has a position relative to the parent window. + let is_synthetic = xev.send_event == xlib::True; + + // These are both in physical space. + let new_inner_size = (xev.width as u32, xev.height as u32); + let new_inner_position = (xev.x, xev.y); + + let (mut resized, moved) = { + let mut shared_state_lock = window.shared_state_lock(); + + let resized = util::maybe_change(&mut shared_state_lock.size, new_inner_size); + let moved = if is_synthetic { + util::maybe_change(&mut shared_state_lock.inner_position, new_inner_position) + } else { + // Detect when frame extents change. + // Since this isn't synthetic, as per the notes above, this position is relative to the + // parent window. + let rel_parent = new_inner_position; + if util::maybe_change(&mut shared_state_lock.inner_position_rel_parent, rel_parent) + { + // This ensures we process the next `Moved`. + shared_state_lock.inner_position = None; + // Extra insurance against stale frame extents. + shared_state_lock.frame_extents = None; } - } + false + }; + (resized, moved) + }; - // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events - ty @ ffi::KeyPress | ty @ ffi::KeyRelease => { - let xkev: &mut ffi::XKeyEvent = xev.as_mut(); - let window = match self.active_window { - Some(window) => window, - None => return, - }; + let position = window.shared_state_lock().position; + + let new_outer_position = if let (Some(position), false) = (position, moved) { + position + } else { + let mut shared_state_lock = window.shared_state_lock(); + + // We need to convert client area position to window position. + let frame_extents = shared_state_lock + .frame_extents + .as_ref() + .cloned() + .unwrap_or_else(|| { + let frame_extents = wt.xconn.get_frame_extents_heuristic(xwindow, wt.root); + shared_state_lock.frame_extents = Some(frame_extents.clone()); + frame_extents + }); + let outer = + frame_extents.inner_pos_to_outer(new_inner_position.0, new_inner_position.1); + shared_state_lock.position = Some(outer); - let window_id = mkwid(window); - let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); - let keycode = xkev.keycode as _; - let repeat = ty == ffi::KeyPress && !self.got_key_release; - // Update state after the repeat setting. - let state = if ty == ffi::KeyPress { - self.got_key_release = false; - ElementState::Pressed - } else { - self.got_key_release = true; - ElementState::Released - }; + if moved { + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Moved(outer.into()), + }, + ); + } + outer + }; - if keycode != 0 && !self.is_composing { - let event = self.kb_state.process_key_event(keycode, state, repeat); - callback(Event::WindowEvent { + if is_synthetic { + let mut shared_state_lock = window.shared_state_lock(); + // If we don't use the existing adjusted value when available, then the user can screw up the + // resizing by dragging across monitors *without* dropping the window. + let (width, height) = shared_state_lock + .dpi_adjusted + .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 = { + let window_rect = util::AaRect::new(new_outer_position, new_inner_size); + let monitor = wt + .xconn + .get_monitor_for_window(Some(window_rect)) + .expect("Failed to find monitor for window"); + + if monitor.is_dummy() { + // Avoid updating monitor using a dummy monitor handle + last_scale_factor + } else { + shared_state_lock.last_monitor = monitor.clone(); + monitor.scale_factor + } + }; + if last_scale_factor != new_scale_factor { + let (new_width, new_height) = window.adjust_for_dpi( + last_scale_factor, + new_scale_factor, + width, + height, + &shared_state_lock, + ); + + let old_inner_size = PhysicalSize::new(width, height); + let new_inner_size = PhysicalSize::new(new_width, new_height); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + let inner_size = Arc::new(Mutex::new(new_inner_size)); + callback( + &self.target, + Event::WindowEvent { window_id, - event: WindowEvent::KeyboardInput { - device_id, - event, - is_synthetic: false, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), }, - }); - } else if let Some(ic) = wt.ime.borrow().get_context(window) { - let written = wt.xconn.lookup_utf8(ic, xkev); - if !written.is_empty() { - let event = Event::WindowEvent { - window_id, - event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), - }; - callback(event); + }, + ); - let event = Event::WindowEvent { - window_id, - event: WindowEvent::Ime(Ime::Commit(written)), - }; + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); - self.is_composing = false; - callback(event); - } + if new_inner_size != old_inner_size { + window.request_inner_size_physical(new_inner_size.width, new_inner_size.height); + window.shared_state_lock().dpi_adjusted = Some(new_inner_size.into()); + // if the DPI factor changed, force a resize event to ensure the logical + // size is computed with the right DPI factor + resized = true; } } + } - ffi::GenericEvent => { - let guard = if let Some(e) = GenericEventCookie::from_event(&wt.xconn, *xev) { - e + // NOTE: Ensure that the lock is dropped before handling the resized and + // sending the event back to user. + let hittest = { + let mut shared_state_lock = window.shared_state_lock(); + let hittest = shared_state_lock.cursor_hittest; + + // This is a hack to ensure that the DPI adjusted resize is actually + // applied on all WMs. KWin doesn't need this, but Xfwm does. The hack + // should not be run on other WMs, since tiling WMs constrain the window + // size, making the resize fail. This would cause an endless stream of + // XResizeWindow requests, making Xorg, the winit client, and the WM + // consume 100% of CPU. + if let Some(adjusted_size) = shared_state_lock.dpi_adjusted { + if new_inner_size == adjusted_size || !util::wm_name_is_one_of(&["Xfwm4"]) { + // When this finally happens, the event will not be synthetic. + shared_state_lock.dpi_adjusted = None; } else { - return; - }; - let xev = &guard.cookie; - if self.xi2ext.opcode != xev.extension { - return; + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + window.request_inner_size_physical(adjusted_size.0, adjusted_size.1); } + } - use crate::event::{ - ElementState::{Pressed, Released}, - MouseButton::{Back, Forward, Left, Middle, Other, Right}, - MouseScrollDelta::LineDelta, - Touch, - WindowEvent::{ - AxisMotion, CursorEntered, CursorLeft, CursorMoved, Focused, MouseInput, - MouseWheel, - }, - }; + hittest + }; - match xev.evtype { - ffi::XI_ButtonPress | ffi::XI_ButtonRelease => { - let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; - let window_id = mkwid(xev.event); - let device_id = mkdid(xev.deviceid); - if (xev.flags & ffi::XIPointerEmulated) != 0 { - // Deliver multi-touch events instead of emulated mouse events. - return; - } - - let state = if xev.evtype == ffi::XI_ButtonPress { - Pressed - } else { - Released - }; - match xev.detail as u32 { - ffi::Button1 => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Left, - }, - }), - ffi::Button2 => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Middle, - }, - }), - ffi::Button3 => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Right, - }, - }), - - // Suppress emulated scroll wheel clicks, since we handle the real motion events for those. - // In practice, even clicky scroll wheels appear to be reported by evdev (and XInput2 in - // turn) as axis motion, so we don't otherwise special-case these button presses. - 4 | 5 | 6 | 7 => { - if xev.flags & ffi::XIPointerEmulated == 0 { - callback(Event::WindowEvent { - window_id, - event: MouseWheel { - device_id, - delta: match xev.detail { - 4 => LineDelta(0.0, 1.0), - 5 => LineDelta(0.0, -1.0), - 6 => LineDelta(1.0, 0.0), - 7 => LineDelta(-1.0, 0.0), - _ => unreachable!(), - }, - phase: TouchPhase::Moved, - }, - }); - } - } - - 8 => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Back, - }, - }), - 9 => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Forward, - }, - }), - - x => callback(Event::WindowEvent { - window_id, - event: MouseInput { - device_id, - state, - button: Other(x as u16), - }, - }), - } - } - ffi::XI_Motion => { - let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; - let device_id = mkdid(xev.deviceid); - let window_id = mkwid(xev.event); - let new_cursor_pos = (xev.event_x, xev.event_y); - - let cursor_moved = self.with_window(xev.event, |window| { - let mut shared_state_lock = window.shared_state_lock(); - util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) - }); - if cursor_moved == Some(true) { - let position = PhysicalPosition::new(xev.event_x, xev.event_y); - - callback(Event::WindowEvent { - window_id, - event: CursorMoved { - device_id, - position, - }, - }); - } else if cursor_moved.is_none() { - return; - } - - // More gymnastics, for self.devices - let mut events = Vec::new(); - { - let mask = unsafe { - slice::from_raw_parts( - xev.valuators.mask, - xev.valuators.mask_len as usize, - ) - }; - let mut devices = self.devices.borrow_mut(); - let physical_device = match devices.get_mut(&DeviceId(xev.sourceid)) { - Some(device) => device, - None => return, - }; - - let mut value = xev.valuators.values; - for i in 0..xev.valuators.mask_len * 8 { - if ffi::XIMaskIsSet(mask, i) { - let x = unsafe { *value }; - if let Some(&mut (_, ref mut info)) = physical_device - .scroll_axes - .iter_mut() - .find(|&&mut (axis, _)| axis == i) - { - let delta = (x - info.position) / info.increment; - info.position = x; - events.push(Event::WindowEvent { - window_id, - event: MouseWheel { - device_id, - delta: match info.orientation { - // X11 vertical scroll coordinates are opposite to winit's - ScrollOrientation::Horizontal => { - LineDelta(-delta as f32, 0.0) - } - ScrollOrientation::Vertical => { - LineDelta(0.0, -delta as f32) - } - }, - phase: TouchPhase::Moved, - }, - }); - } else { - events.push(Event::WindowEvent { - window_id, - event: AxisMotion { - device_id, - axis: i as u32, - value: unsafe { *value }, - }, - }); - } - value = unsafe { value.offset(1) }; - } - } - } - for event in events { - callback(event); - } - } + // Reload hittest. + if hittest.unwrap_or(false) { + let _ = window.set_cursor_hittest(true); + } - ffi::XI_Enter => { - let xev: &ffi::XIEnterEvent = unsafe { &*(xev.data as *const _) }; + if resized { + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(new_inner_size.into()), + }, + ); + } + } - let window_id = mkwid(xev.event); - let device_id = mkdid(xev.deviceid); + /// This is generally a reliable way to detect when the window manager's been + /// replaced, though this event is only fired by reparenting window managers + /// (which is almost all of them). Failing to correctly update WM info doesn't + /// really have much impact, since on the WMs affected (xmonad, dwm, etc.) the only + /// effect is that we waste some time trying to query unsupported properties. + fn reparent_notify(&self, xev: &XReparentEvent) { + let wt = Self::window_target(&self.target); - if let Some(all_info) = DeviceInfo::get(&wt.xconn, ffi::XIAllDevices) { - let mut devices = self.devices.borrow_mut(); - for device_info in all_info.iter() { - if device_info.deviceid == xev.sourceid - // This is needed for resetting to work correctly on i3, and - // presumably some other WMs. On those, `XI_Enter` doesn't include - // the physical device ID, so both `sourceid` and `deviceid` are - // the virtual device. - || device_info.attachment == xev.sourceid - { - let device_id = DeviceId(device_info.deviceid); - if let Some(device) = devices.get_mut(&device_id) { - device.reset_scroll_position(device_info); - } - } - } - } - - if self.window_exists(xev.event) { - callback(Event::WindowEvent { - window_id, - event: CursorEntered { device_id }, - }); - - let position = PhysicalPosition::new(xev.event_x, xev.event_y); - - callback(Event::WindowEvent { - window_id, - event: CursorMoved { - device_id, - position, - }, - }); - } - } - ffi::XI_Leave => { - let xev: &ffi::XILeaveEvent = unsafe { &*(xev.data as *const _) }; - - // Leave, FocusIn, and FocusOut can be received by a window that's already - // been destroyed, which the user presumably doesn't want to deal with. - let window_closed = !self.window_exists(xev.event); - if !window_closed { - callback(Event::WindowEvent { - window_id: mkwid(xev.event), - event: CursorLeft { - device_id: mkdid(xev.deviceid), - }, - }); - } - } - ffi::XI_FocusIn => { - let xev: &ffi::XIFocusInEvent = unsafe { &*(xev.data as *const _) }; - - wt.ime - .borrow_mut() - .focus(xev.event) - .expect("Failed to focus input context"); - - if self.active_window != Some(xev.event) { - self.active_window = Some(xev.event); - - wt.update_listen_device_events(true); - - let window_id = mkwid(xev.event); - let position = PhysicalPosition::new(xev.event_x, xev.event_y); - - if let Some(window) = self.with_window(xev.event, Arc::clone) { - window.shared_state_lock().has_focus = true; - } - - callback(Event::WindowEvent { - window_id, - event: Focused(true), - }); - - let modifiers: crate::keyboard::ModifiersState = - self.kb_state.mods_state().into(); - if !modifiers.is_empty() { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::ModifiersChanged(modifiers.into()), - }); - } - - // The deviceid for this event is for a keyboard instead of a pointer, - // so we have to do a little extra work. - let pointer_id = self - .devices - .borrow() - .get(&DeviceId(xev.deviceid)) - .map(|device| device.attachment) - .unwrap_or(2); - - callback(Event::WindowEvent { - window_id, - event: CursorMoved { - device_id: mkdid(pointer_id), - position, - }, - }); - - // Issue key press events for all pressed keys - Self::handle_pressed_keys( - wt, - window_id, - ElementState::Pressed, - &mut self.kb_state, - &mut callback, - ); - } - } - ffi::XI_FocusOut => { - let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) }; - if !self.window_exists(xev.event) { - return; - } - - wt.ime - .borrow_mut() - .unfocus(xev.event) - .expect("Failed to unfocus input context"); - - if self.active_window.take() == Some(xev.event) { - let window_id = mkwid(xev.event); - - wt.update_listen_device_events(false); - - // Issue key release events for all pressed keys - Self::handle_pressed_keys( - wt, - window_id, - ElementState::Released, - &mut self.kb_state, - &mut callback, - ); - - callback(Event::WindowEvent { - window_id, - event: WindowEvent::ModifiersChanged( - ModifiersState::empty().into(), - ), - }); - - if let Some(window) = self.with_window(xev.event, Arc::clone) { - window.shared_state_lock().has_focus = false; - } - - callback(Event::WindowEvent { - window_id, - event: Focused(false), - }) - } - } + wt.xconn.update_cached_wm_info(wt.root); - ffi::XI_TouchBegin | ffi::XI_TouchUpdate | ffi::XI_TouchEnd => { - let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; - let window_id = mkwid(xev.event); - let phase = match xev.evtype { - ffi::XI_TouchBegin => TouchPhase::Started, - ffi::XI_TouchUpdate => TouchPhase::Moved, - ffi::XI_TouchEnd => TouchPhase::Ended, - _ => unreachable!(), - }; - if self.window_exists(xev.event) { - let id = xev.detail as u64; - let location = PhysicalPosition::new(xev.event_x, xev.event_y); - - // Mouse cursor position changes when touch events are received. - // Only the first concurrently active touch ID moves the mouse cursor. - if is_first_touch(&mut self.first_touch, &mut self.num_touch, id, phase) - { - callback(Event::WindowEvent { - window_id, - event: WindowEvent::CursorMoved { - device_id: mkdid(util::VIRTUAL_CORE_POINTER), - position: location.cast(), - }, - }); - } - - callback(Event::WindowEvent { - window_id, - event: WindowEvent::Touch(Touch { - device_id: mkdid(xev.deviceid), - phase, - location, - force: None, // TODO - id, - }), - }) - } - } + self.with_window(xev.window as xproto::Window, |window| { + window.invalidate_cached_frame_extents(); + }); + } - ffi::XI_RawButtonPress | ffi::XI_RawButtonRelease => { - let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; - if xev.flags & ffi::XIPointerEmulated == 0 { - callback(Event::DeviceEvent { - device_id: mkdid(xev.deviceid), - event: DeviceEvent::Button { - button: xev.detail as u32, - state: match xev.evtype { - ffi::XI_RawButtonPress => Pressed, - ffi::XI_RawButtonRelease => Released, - _ => unreachable!(), - }, - }, - }); - } - } + fn map_notify(&self, xev: &XMapEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // NOTE: Re-issue the focus state when mapping the window. + // + // The purpose of it is to deliver initial focused state of the newly created + // window, given that we can't rely on `CreateNotify`, due to it being not + // sent. + let focus = self + .with_window(window, |window| window.has_focus()) + .unwrap_or_default(); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(focus), + }; - ffi::XI_RawMotion => { - let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; - let did = mkdid(xev.deviceid); + callback(&self.target, event); + } - let mask = unsafe { - slice::from_raw_parts( - xev.valuators.mask, - xev.valuators.mask_len as usize, - ) - }; - let mut value = xev.raw_values; - let mut mouse_delta = (0.0, 0.0); - let mut scroll_delta = (0.0, 0.0); - for i in 0..xev.valuators.mask_len * 8 { - if ffi::XIMaskIsSet(mask, i) { - let x = unsafe { *value }; - // We assume that every XInput2 device with analog axes is a pointing device emitting - // relative coordinates. - match i { - 0 => mouse_delta.0 = x, - 1 => mouse_delta.1 = x, - 2 => scroll_delta.0 = x as f32, - 3 => scroll_delta.1 = x as f32, - _ => {} - } - callback(Event::DeviceEvent { - device_id: did, - event: DeviceEvent::Motion { - axis: i as u32, - value: x, - }, - }); - value = unsafe { value.offset(1) }; - } - } - if mouse_delta != (0.0, 0.0) { - callback(Event::DeviceEvent { - device_id: did, - event: DeviceEvent::MouseMotion { delta: mouse_delta }, - }); - } - if scroll_delta != (0.0, 0.0) { - callback(Event::DeviceEvent { - device_id: did, - event: DeviceEvent::MouseWheel { - delta: LineDelta(scroll_delta.0, scroll_delta.1), - }, - }); - } - } - ffi::XI_RawKeyPress | ffi::XI_RawKeyRelease => { - let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; + fn destroy_notify(&self, xev: &XDestroyWindowEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); - let state = match xev.evtype { - ffi::XI_RawKeyPress => Pressed, - ffi::XI_RawKeyRelease => Released, - _ => unreachable!(), - }; + let window = xev.window as xproto::Window; + let window_id = mkwid(window); - let device_id = mkdid(xev.sourceid); - let keycode = xev.detail as u32; - if keycode < KEYCODE_OFFSET as u32 { - return; - } - let physical_key = keymap::raw_keycode_to_keycode(keycode); - - callback(Event::DeviceEvent { - device_id, - event: DeviceEvent::Key(RawKeyEvent { - physical_key, - state, - }), - }); - } + // In the event that the window's been destroyed without being dropped first, we + // cleanup again here. + wt.windows.borrow_mut().remove(&WindowId(window as _)); - ffi::XI_HierarchyChanged => { - let xev: &ffi::XIHierarchyEvent = unsafe { &*(xev.data as *const _) }; - for info in - unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) } - { - if 0 != info.flags & (ffi::XISlaveAdded | ffi::XIMasterAdded) { - self.init_device(info.deviceid); - callback(Event::DeviceEvent { - device_id: mkdid(info.deviceid), - event: DeviceEvent::Added, - }); - } else if 0 != info.flags & (ffi::XISlaveRemoved | ffi::XIMasterRemoved) - { - callback(Event::DeviceEvent { - device_id: mkdid(info.deviceid), - event: DeviceEvent::Removed, - }); - let mut devices = self.devices.borrow_mut(); - devices.remove(&DeviceId(info.deviceid)); - } - } - } + // Since all XIM stuff needs to happen from the same thread, we destroy the input + // context here instead of when dropping the window. + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .remove_context(window as XWindow) + .expect("Failed to destroy input context"); + } - _ => {} + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Destroyed, + }, + ); + } + + fn property_notify(&mut self, xev: &XPropertyEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.x_connection().atoms(); + let atom = xev.atom as xproto::Atom; + + if atom == xproto::Atom::from(xproto::AtomEnum::RESOURCE_MANAGER) + || atom == atoms[_XSETTINGS_SETTINGS] + { + self.process_dpi_change(&mut callback); + } + } + + fn visibility_notify(&self, xev: &XVisibilityEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let xwindow = xev.window as xproto::Window; + + let event = Event::WindowEvent { + window_id: mkwid(xwindow), + event: WindowEvent::Occluded(xev.state == xlib::VisibilityFullyObscured), + }; + callback(&self.target, event); + + self.with_window(xwindow, |window| { + window.visibility_notify(); + }); + } + + fn expose(&self, xev: &XExposeEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + // Multiple Expose events may be received for subareas of a window. + // We issue `RedrawRequested` only for the last event of such a series. + if xev.count == 0 { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }; + + callback(&self.target, event); + } + } + + fn xinput_key_input(&mut self, xev: &mut XKeyEvent, state: ElementState, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = match self.active_window { + Some(window) => window, + None => return, + }; + + let window_id = mkwid(window); + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + let keycode = xev.keycode as _; + + // Update state to track key repeats and determine whether this key was a repeat. + // + // Note, when a key is held before focusing on this window the first + // (non-synthetic) event will not be flagged as a repeat (also note that the + // synthetic press event that is generated before this when the window gains focus + // will also not be flagged as a repeat). + // + // Only keys that can repeat should change the held_key_press state since a + // continuously held repeatable key may continue repeating after the press of a + // non-repeatable key. + let key_repeats = self + .xkb_context + .keymap_mut() + .map(|k| k.key_repeats(keycode)) + .unwrap_or(false); + let repeat = if key_repeats { + let is_latest_held = self.held_key_press == Some(keycode); + + if state == ElementState::Pressed { + self.held_key_press = Some(keycode); + is_latest_held + } else { + // Check that the released key is the latest repeatable key that has been + // pressed, since repeats will continue for the latest key press if a + // different previously pressed key is released. + if is_latest_held { + self.held_key_press = None; } + false } - _ => { - if event_type == self.xkbext.first_event_id { - let xev = unsafe { &*(xev as *const _ as *const ffi::XkbAnyEvent) }; - match xev.xkb_type { - ffi::XkbNewKeyboardNotify => { - let xev = unsafe { - &*(xev as *const _ as *const ffi::XkbNewKeyboardNotifyEvent) - }; - let keycodes_changed_flag = 0x1; - let geometry_changed_flag = 0x1 << 1; - - let keycodes_changed = - util::has_flag(xev.changed, keycodes_changed_flag); - let geometry_changed = - util::has_flag(xev.changed, geometry_changed_flag); - - if xev.device == self.kb_state.core_keyboard_id - && (keycodes_changed || geometry_changed) - { - unsafe { self.kb_state.init_with_x11_keymap() }; - } - } - ffi::XkbStateNotify => { - let xev = - unsafe { &*(xev as *const _ as *const ffi::XkbStateNotifyEvent) }; - - let prev_mods = self.kb_state.mods_state(); - self.kb_state.update_modifiers( - xev.base_mods, - xev.latched_mods, - xev.locked_mods, - xev.base_group as u32, - xev.latched_group as u32, - xev.locked_group as u32, - ); - let new_mods = self.kb_state.mods_state(); - if prev_mods != new_mods { - if let Some(window) = self.active_window { - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::ModifiersChanged( - Into::::into(new_mods).into(), - ), - }); - } - } - } - _ => {} + } else { + false + }; + + // NOTE: When the modifier was captured by the XFilterEvents the modifiers for the modifier + // itself are out of sync due to XkbState being delivered before XKeyEvent, since it's + // being replayed by the XIM, thus we should replay ourselves. + let replay = if let Some(position) = self + .xfiltered_modifiers + .iter() + .rev() + .position(|&s| s == xev.serial) + { + // We don't have to replay modifiers pressed before the current event if some events + // were not forwarded to us, since their state is irrelevant. + self.xfiltered_modifiers + .resize(self.xfiltered_modifiers.len() - 1 - position, 0); + true + } else { + false + }; + + // Always update the modifiers when we're not replaying. + if !replay { + self.udpate_mods_from_core_event(window_id, xev.state as u16, &mut callback); + } + + if keycode != 0 && !self.is_composing { + // Don't alter the modifiers state from replaying. + if replay { + self.send_synthic_modifier_from_core(window_id, xev.state as u16, &mut callback); + } + + if let Some(mut key_processor) = self.xkb_context.key_context() { + let event = key_processor.process_key_event(keycode, state, repeat); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id, + event, + is_synthetic: false, + }, + }; + callback(&self.target, event); + } + + // Restore the client's modifiers state after replay. + if replay { + self.send_modifiers(window_id, self.modifiers.get(), true, &mut callback); + } + + return; + } + + let wt = Self::window_target(&self.target); + + if let Some(ic) = wt + .ime + .as_ref() + .and_then(|ime| ime.borrow().get_context(window as XWindow)) + { + let written = wt.xconn.lookup_utf8(ic, xev); + if !written.is_empty() { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Commit(written)), + }; + + self.is_composing = false; + callback(&self.target, event); + } + } + } + + fn send_synthic_modifier_from_core( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootELW, Event), + { + let keymap = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + let wt = Self::window_target(&self.target); + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + + // Use synthetic state since we're replaying the modifier. The user modifier state + // will be restored later. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + + let mask = self.xkb_mod_mask_from_core(state); + xkb_state.update_modifiers(mask, 0, 0, 0, 0, Self::core_keyboard_group(state)); + let mods: ModifiersState = xkb_state.modifiers().into(); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(mods.into()), + }; + + callback(&self.target, event); + } + + fn xinput2_button_input(&self, event: &XIDeviceEvent, state: ElementState, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let window_id = mkwid(event.event as xproto::Window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Deliver multi-touch events instead of emulated mouse events. + if (event.flags & xinput2::XIPointerEmulated) != 0 { + return; + } + + let event = match event.detail as u32 { + xlib::Button1 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Left, + }, + xlib::Button2 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Middle, + }, + + xlib::Button3 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Right, + }, + + // Suppress emulated scroll wheel clicks, since we handle the real motion events for those. + // In practice, even clicky scroll wheels appear to be reported by evdev (and XInput2 in + // turn) as axis motion, so we don't otherwise special-case these button presses. + 4..=7 => WindowEvent::MouseWheel { + device_id, + delta: match event.detail { + 4 => MouseScrollDelta::LineDelta(0.0, 1.0), + 5 => MouseScrollDelta::LineDelta(0.0, -1.0), + 6 => MouseScrollDelta::LineDelta(1.0, 0.0), + 7 => MouseScrollDelta::LineDelta(-1.0, 0.0), + _ => unreachable!(), + }, + phase: TouchPhase::Moved, + }, + 8 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Back, + }, + + 9 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Forward, + }, + x => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Other(x as u16), + }, + }; + + let event = Event::WindowEvent { window_id, event }; + callback(&self.target, event); + } + + fn xinput2_mouse_motion(&self, event: &XIDeviceEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let device_id = mkdid(event.deviceid as xinput::DeviceId); + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let new_cursor_pos = (event.event_x, event.event_y); + + let cursor_moved = self.with_window(window, |window| { + let mut shared_state_lock = window.shared_state_lock(); + util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) + }); + + if cursor_moved == Some(true) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }; + callback(&self.target, event); + } else if cursor_moved.is_none() { + return; + } + + // More gymnastics, for self.devices + let mask = unsafe { + slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + }; + let mut devices = self.devices.borrow_mut(); + let physical_device = match devices.get_mut(&DeviceId(event.sourceid as xinput::DeviceId)) { + Some(device) => device, + None => return, + }; + + let mut events = Vec::new(); + let mut value = event.valuators.values; + for i in 0..event.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + + let x = unsafe { *value }; + + let event = if let Some(&mut (_, ref mut info)) = physical_device + .scroll_axes + .iter_mut() + .find(|&&mut (axis, _)| axis == i as _) + { + let delta = (x - info.position) / info.increment; + info.position = x; + // X11 vertical scroll coordinates are opposite to winit's + let delta = match info.orientation { + ScrollOrientation::Horizontal => { + MouseScrollDelta::LineDelta(-delta as f32, 0.0) } + ScrollOrientation::Vertical => MouseScrollDelta::LineDelta(0.0, -delta as f32), + }; + + WindowEvent::MouseWheel { + device_id, + delta, + phase: TouchPhase::Moved, } - if event_type == self.randr_event_offset { - // In the future, it would be quite easy to emit monitor hotplug events. - let prev_list = monitor::invalidate_cached_monitor_list(); - if let Some(prev_list) = prev_list { - let new_list = wt.xconn.available_monitors(); - for new_monitor in new_list { - // Previous list may be empty, in case of disconnecting and - // reconnecting the only one monitor. We still need to emit events in - // this case. - let maybe_prev_scale_factor = prev_list - .iter() - .find(|prev_monitor| prev_monitor.name == new_monitor.name) - .map(|prev_monitor| prev_monitor.scale_factor); - if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { - for (window_id, window) in wt.windows.borrow().iter() { - if let Some(window) = window.upgrade() { - // Check if the window is on this monitor - let monitor = window.current_monitor(); - if monitor.name == new_monitor.name { - let (width, height) = window.inner_size_physical(); - let (new_width, new_height) = window.adjust_for_dpi( - // If we couldn't determine the previous scale - // factor (e.g., because all monitors were closed - // before), just pick whatever the current monitor - // has set as a baseline. - maybe_prev_scale_factor - .unwrap_or(monitor.scale_factor), - new_monitor.scale_factor, - width, - height, - &window.shared_state_lock(), - ); - - 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); - - callback(Event::WindowEvent { - window_id, - event: WindowEvent::ScaleFactorChanged { - scale_factor: new_monitor.scale_factor, - new_inner_size: &mut new_inner_size, - }, - }); - - if new_inner_size != old_inner_size { - let (new_width, new_height) = new_inner_size.into(); - window - .set_inner_size_physical(new_width, new_height); - } - } - } - } - } - } - } + } else { + WindowEvent::AxisMotion { + device_id, + axis: i as u32, + value: unsafe { *value }, } - } + }; + + events.push(Event::WindowEvent { window_id, event }); + + value = unsafe { value.offset(1) }; } - // 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); + for event in events { + callback(&self.target, event); + } + } + + fn xinput2_mouse_enter(&self, event: &XIEnterEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + if let Some(all_info) = DeviceInfo::get(&wt.xconn, super::ALL_DEVICES.into()) { + let mut devices = self.devices.borrow_mut(); + for device_info in all_info.iter() { + if device_info.deviceid == event.sourceid + // This is needed for resetting to work correctly on i3, and + // presumably some other WMs. On those, `XI_Enter` doesn't include + // the physical device ID, so both `sourceid` and `deviceid` are + // the virtual device. + || device_info.attachment == event.sourceid + { + let device_id = DeviceId(device_info.deviceid as _); + if let Some(device) = devices.get_mut(&device_id) { + device.reset_scroll_position(device_info); + } } } } - let (window, event) = match self.ime_event_receiver.try_recv() { - Ok((window, event)) => (window, event), - Err(_) => return, + if self.window_exists(window) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorEntered { device_id }, + }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_mouse_left(&self, event: &XILeaveEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let window = event.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Leave, FocusIn, and FocusOut can be received by a window that's already + // been destroyed, which the user presumably doesn't want to deal with. + if self.window_exists(window) { + let event = Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::CursorLeft { + device_id: mkdid(event.deviceid as xinput::DeviceId), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_focused(&mut self, xev: &XIFocusInEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .focus(xev.event) + .expect("Failed to focus input context"); + } + + if self.active_window == Some(window) { + return; + } + + self.active_window = Some(window); + + wt.update_listen_device_events(true); + + let window_id = mkwid(window); + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = true; + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(true), }; + callback(&self.target, event); + + // Issue key press events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Pressed, + &mut self.xkb_context, + &mut callback, + ); - match event { - ImeEvent::Enabled => { - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Enabled), - }); + self.update_mods_from_query(window_id, &mut callback); + + // The deviceid for this event is for a keyboard instead of a pointer, + // so we have to do a little extra work. + let pointer_id = self + .devices + .borrow() + .get(&DeviceId(xev.deviceid as xinput::DeviceId)) + .map(|device| device.attachment) + .unwrap_or(2); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(pointer_id as _), + position, + }, + }; + callback(&self.target, event); + } + + fn xinput2_unfocused(&mut self, xev: &XIFocusOutEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if !self.window_exists(window) { + return; + } + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .unfocus(xev.event) + .expect("Failed to unfocus input context"); + } + + if self.active_window.take() == Some(window) { + let window_id = mkwid(window); + + wt.update_listen_device_events(false); + + // Clear the modifiers when unfocusing the window. + if let Some(xkb_state) = self.xkb_context.state_mut() { + xkb_state.update_modifiers(0, 0, 0, 0, 0, 0); + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback); } - ImeEvent::Start => { - self.is_composing = true; - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)), - }); + + // Issue key release events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Released, + &mut self.xkb_context, + &mut callback, + ); + + // Clear this so detecting key repeats is consistently handled when the + // window regains focus. + self.held_key_press = None; + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = false; + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(false), + }; + callback(&self.target, event) + } + } + + fn xinput2_touch(&mut self, xev: &XIDeviceEvent, phase: TouchPhase, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = xev.event as xproto::Window; + if self.window_exists(window) { + let window_id = mkwid(window); + let id = xev.detail as u64; + let location = PhysicalPosition::new(xev.event_x, xev.event_y); + + // Mouse cursor position changes when touch events are received. + // Only the first concurrently active touch ID moves the mouse cursor. + if is_first_touch(&mut self.first_touch, &mut self.num_touch, id, phase) { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(util::VIRTUAL_CORE_POINTER), + position: location.cast(), + }, + }; + callback(&self.target, event); + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Touch(Touch { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + phase, + location, + force: None, // TODO + id, + }), + }; + callback(&self.target, event) + } + } + + fn xinput2_raw_button_input(&self, xev: &XIRawEvent, state: ElementState, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.flags & xinput2::XIPointerEmulated == 0 { + let event = Event::DeviceEvent { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + event: DeviceEvent::Button { + state, + button: xev.detail as u32, + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_mouse_motion(&self, xev: &XIRawEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let did = mkdid(xev.deviceid as xinput::DeviceId); + + let mask = + unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) }; + let mut value = xev.raw_values; + let mut mouse_delta = util::Delta::default(); + let mut scroll_delta = util::Delta::default(); + for i in 0..xev.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + let x = unsafe { *value }; + + // We assume that every XInput2 device with analog axes is a pointing device emitting + // relative coordinates. + match i { + 0 => mouse_delta.set_x(x), + 1 => mouse_delta.set_y(x), + 2 => scroll_delta.set_x(x as f32), + 3 => scroll_delta.set_y(x as f32), + _ => {} + } + + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::Motion { + axis: i as u32, + value: x, + }, + }; + callback(&self.target, event); + + value = unsafe { value.offset(1) }; + } + + if let Some(mouse_delta) = mouse_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseMotion { delta: mouse_delta }, + }; + callback(&self.target, event); + } + + if let Some(scroll_delta) = scroll_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseWheel { + delta: MouseScrollDelta::LineDelta(scroll_delta.0, scroll_delta.1), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_key_input(&mut self, xev: &XIRawEvent, state: ElementState, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let device_id = mkdid(xev.sourceid as xinput::DeviceId); + let keycode = xev.detail as u32; + if keycode < KEYCODE_OFFSET as u32 { + return; + } + let physical_key = xkb::raw_keycode_to_physicalkey(keycode); + + callback( + &self.target, + Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { + physical_key, + state, + }), + }, + ); + } + + fn xinput2_hierarchy_changed(&mut self, xev: &XIHierarchyEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let infos = unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) }; + for info in infos { + if 0 != info.flags & (xinput2::XISlaveAdded | xinput2::XIMasterAdded) { + self.init_device(info.deviceid as xinput::DeviceId); + callback( + &self.target, + Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Added, + }, + ); + } else if 0 != info.flags & (xinput2::XISlaveRemoved | xinput2::XIMasterRemoved) { + callback( + &self.target, + Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Removed, + }, + ); + let mut devices = self.devices.borrow_mut(); + devices.remove(&DeviceId(info.deviceid as xinput::DeviceId)); } - ImeEvent::Update(text, position) => { - if self.is_composing { - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))), - }); + } + } + + fn xkb_event(&mut self, xev: &XkbAnyEvent, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + match xev.xkb_type { + xlib::XkbNewKeyboardNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbNewKeyboardNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let keycodes_changed_flag = 0x1; + let geometry_changed_flag = 0x1 << 1; + + let keycodes_changed = util::has_flag(xev.changed, keycodes_changed_flag); + let geometry_changed = util::has_flag(xev.changed, geometry_changed_flag); + + if xev.device == self.xkb_context.core_keyboard_id + && (keycodes_changed || geometry_changed) + { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } } } - 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)), - }); + xlib::XkbMapNotify => { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } } - ImeEvent::Disabled => { - self.is_composing = false; - callback(Event::WindowEvent { - window_id: mkwid(window), - event: WindowEvent::Ime(Ime::Disabled), - }); + xlib::XkbStateNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbStateNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + xev.base_mods, + xev.latched_mods, + xev.locked_mods, + xev.base_group as u32, + xev.latched_group as u32, + xev.locked_group as u32, + ); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + _ => {} + } + } + + pub fn update_mods_from_xinput2_event( + &mut self, + mods: &XIModifierState, + group: &XIModifierState, + force: bool, + mut callback: F, + ) where + F: FnMut(&RootELW, Event), + { + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + mods.base as u32, + mods.latched as u32, + mods.locked as u32, + group.base as u32, + group.latched as u32, + group.locked as u32, + ); + + // NOTE: we use active window since generally sub windows don't have keyboard input, + // and winit assumes that unfocused window doesn't have modifiers. + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers(); + self.send_modifiers(window_id, mods.into(), force, &mut callback); + } + } + + fn update_mods_from_query(&mut self, window_id: crate::window::WindowId, mut callback: F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + unsafe { + let mut state: XkbStateRec = std::mem::zeroed(); + if (wt.xconn.xlib.XkbGetState)(wt.xconn.display, XkbId::USE_CORE_KBD.into(), &mut state) + == xlib::True + { + xkb_state.update_modifiers( + state.base_mods as u32, + state.latched_mods as u32, + state.locked_mods as u32, + state.base_group as u32, + state.latched_group as u32, + state.locked_group as u32, + ); } } + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback) + } + + pub fn udpate_mods_from_core_event( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootELW, Event), + { + let xkb_mask = self.xkb_mod_mask_from_core(state); + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + // NOTE: this is inspired by Qt impl. + let mut depressed = xkb_state.depressed_modifiers() & xkb_mask; + let latched = xkb_state.latched_modifiers() & xkb_mask; + let locked = xkb_state.locked_modifiers() & xkb_mask; + // Set modifiers in depressed if they don't appear in any of the final masks. + depressed |= !(depressed | latched | locked) & xkb_mask; + + xkb_state.update_modifiers( + depressed, + latched, + locked, + 0, + 0, + Self::core_keyboard_group(state), + ); + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), false, &mut callback); + } + + // Bits 13 and 14 report the state keyboard group. + pub fn core_keyboard_group(state: u16) -> u32 { + ((state >> 13) & 3) as u32 + } + + pub fn xkb_mod_mask_from_core(&mut self, state: u16) -> xkb_mod_mask_t { + let mods_indices = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap.mods_indices(), + None => return 0, + }; + + // Build the XKB modifiers from the regular state. + let mut depressed = 0u32; + if let Some(shift) = mods_indices + .shift + .filter(|_| ModMask::SHIFT.intersects(state)) + { + depressed |= 1 << shift; + } + if let Some(caps) = mods_indices + .caps + .filter(|_| ModMask::LOCK.intersects(state)) + { + depressed |= 1 << caps; + } + if let Some(ctrl) = mods_indices + .ctrl + .filter(|_| ModMask::CONTROL.intersects(state)) + { + depressed |= 1 << ctrl; + } + if let Some(alt) = mods_indices.alt.filter(|_| ModMask::M1.intersects(state)) { + depressed |= 1 << alt; + } + if let Some(num) = mods_indices.num.filter(|_| ModMask::M2.intersects(state)) { + depressed |= 1 << num; + } + if let Some(mod3) = mods_indices.mod3.filter(|_| ModMask::M3.intersects(state)) { + depressed |= 1 << mod3; + } + if let Some(logo) = mods_indices.logo.filter(|_| ModMask::M4.intersects(state)) { + depressed |= 1 << logo; + } + if let Some(mod5) = mods_indices.mod5.filter(|_| ModMask::M5.intersects(state)) { + depressed |= 1 << mod5; + } + + depressed + } + + /// Send modifiers for the active window. + /// + /// The event won't be sent when the `modifiers` match the previously `sent` modifiers value, + /// unless `force` is passed. The `force` should be passed when the active window changes. + fn send_modifiers, Event)>( + &self, + window_id: crate::window::WindowId, + modifiers: ModifiersState, + force: bool, + callback: &mut F, + ) { + // NOTE: Always update the modifiers to account for case when they've changed + // and forced was `true`. + if self.modifiers.replace(modifiers) != modifiers || force { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(self.modifiers.get().into()), + }; + callback(&self.target, event); + } } fn handle_pressed_keys( - wt: &super::EventLoopWindowTarget, + target: &RootELW, window_id: crate::window::WindowId, state: ElementState, - kb_state: &mut KbdState, + xkb_context: &mut Context, callback: &mut F, ) where - F: FnMut(Event<'_, T>), + F: FnMut(&RootELW, Event), { let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); // Update modifiers state and emit key events based on which keys are currently pressed. - for keycode in wt + let window_target = Self::window_target(target); + let xcb = window_target + .xconn + .xcb_connection() + .get_raw_xcb_connection(); + + let keymap = match xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + // Send the keys using the sythetic state to not alter the main state. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + let mut key_processor = match xkb_context.key_context_with_state(&mut xkb_state) { + Some(key_processor) => key_processor, + None => return, + }; + + for keycode in window_target .xconn .query_keymap() .into_iter() .filter(|k| *k >= KEYCODE_OFFSET) { - let keycode = keycode as u32; - let event = kb_state.process_key_event(keycode, state, false); - callback(Event::WindowEvent { + let event = key_processor.process_key_event(keycode as u32, state, false); + let event = Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { device_id, event, is_synthetic: true, }, - }); + }; + callback(target, event); } } + + fn process_dpi_change(&self, callback: &mut F) + where + F: FnMut(&RootELW, Event), + { + let wt = Self::window_target(&self.target); + wt.xconn + .reload_database() + .expect("failed to reload Xft database"); + + // In the future, it would be quite easy to emit monitor hotplug events. + let prev_list = { + let prev_list = wt.xconn.invalidate_cached_monitor_list(); + match prev_list { + Some(prev_list) => prev_list, + None => return, + } + }; + + let new_list = wt + .xconn + .available_monitors() + .expect("Failed to get monitor list"); + for new_monitor in new_list { + // Previous list may be empty, in case of disconnecting and + // reconnecting the only one monitor. We still need to emit events in + // this case. + let maybe_prev_scale_factor = prev_list + .iter() + .find(|prev_monitor| prev_monitor.name == new_monitor.name) + .map(|prev_monitor| prev_monitor.scale_factor); + if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { + for window in wt.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) { + window.refresh_dpi_for_monitor(&new_monitor, maybe_prev_scale_factor, |event| { + callback(&self.target, event); + }) + } + } + } + } + + fn window_exists(&self, window_id: xproto::Window) -> bool { + self.with_window(window_id, |_| ()).is_some() + } } fn is_first_touch(first: &mut Option, num: &mut u32, id: u64, phase: TouchPhase) -> bool { diff --git a/src/platform_impl/linux/x11/events.rs b/src/platform_impl/linux/x11/events.rs deleted file mode 100644 index 9063916025..0000000000 --- a/src/platform_impl/linux/x11/events.rs +++ /dev/null @@ -1,1008 +0,0 @@ -use super::ffi; -use crate::event::VirtualKeyCode; -use libc; - -pub fn keysym_to_element(keysym: libc::c_uint) -> Option { - Some(match keysym { - ffi::XK_BackSpace => VirtualKeyCode::Back, - ffi::XK_Tab => VirtualKeyCode::Tab, - //ffi::XK_Linefeed => VirtualKeyCode::Linefeed, - //ffi::XK_Clear => VirtualKeyCode::Clear, - ffi::XK_Return => VirtualKeyCode::Return, - ffi::XK_Pause => VirtualKeyCode::Pause, - //ffi::XK_Scroll_Lock => VirtualKeyCode::Scroll_lock, - //ffi::XK_Sys_Req => VirtualKeyCode::Sys_req, - ffi::XK_Escape => VirtualKeyCode::Escape, - ffi::XK_Delete => VirtualKeyCode::Delete, - ffi::XK_Multi_key => VirtualKeyCode::Compose, - //ffi::XK_Kanji => VirtualKeyCode::Kanji, - //ffi::XK_Muhenkan => VirtualKeyCode::Muhenkan, - //ffi::XK_Henkan_Mode => VirtualKeyCode::Henkan_mode, - //ffi::XK_Henkan => VirtualKeyCode::Henkan, - //ffi::XK_Romaji => VirtualKeyCode::Romaji, - //ffi::XK_Hiragana => VirtualKeyCode::Hiragana, - //ffi::XK_Katakana => VirtualKeyCode::Katakana, - //ffi::XK_Hiragana_Katakana => VirtualKeyCode::Hiragana_katakana, - //ffi::XK_Zenkaku => VirtualKeyCode::Zenkaku, - //ffi::XK_Hankaku => VirtualKeyCode::Hankaku, - //ffi::XK_Zenkaku_Hankaku => VirtualKeyCode::Zenkaku_hankaku, - //ffi::XK_Touroku => VirtualKeyCode::Touroku, - //ffi::XK_Massyo => VirtualKeyCode::Massyo, - //ffi::XK_Kana_Lock => VirtualKeyCode::Kana_lock, - //ffi::XK_Kana_Shift => VirtualKeyCode::Kana_shift, - //ffi::XK_Eisu_Shift => VirtualKeyCode::Eisu_shift, - //ffi::XK_Eisu_toggle => VirtualKeyCode::Eisu_toggle, - ffi::XK_Home => VirtualKeyCode::Home, - ffi::XK_Left => VirtualKeyCode::Left, - ffi::XK_Up => VirtualKeyCode::Up, - ffi::XK_Right => VirtualKeyCode::Right, - ffi::XK_Down => VirtualKeyCode::Down, - //ffi::XK_Prior => VirtualKeyCode::Prior, - ffi::XK_Page_Up => VirtualKeyCode::PageUp, - //ffi::XK_Next => VirtualKeyCode::Next, - ffi::XK_Page_Down => VirtualKeyCode::PageDown, - ffi::XK_End => VirtualKeyCode::End, - //ffi::XK_Begin => VirtualKeyCode::Begin, - //ffi::XK_Win_L => VirtualKeyCode::Win_l, - //ffi::XK_Win_R => VirtualKeyCode::Win_r, - //ffi::XK_App => VirtualKeyCode::App, - //ffi::XK_Select => VirtualKeyCode::Select, - //ffi::XK_Print => VirtualKeyCode::Print, - //ffi::XK_Execute => VirtualKeyCode::Execute, - ffi::XK_Insert => VirtualKeyCode::Insert, - //ffi::XK_Undo => VirtualKeyCode::Undo, - //ffi::XK_Redo => VirtualKeyCode::Redo, - //ffi::XK_Menu => VirtualKeyCode::Menu, - //ffi::XK_Find => VirtualKeyCode::Find, - //ffi::XK_Cancel => VirtualKeyCode::Cancel, - //ffi::XK_Help => VirtualKeyCode::Help, - //ffi::XK_Break => VirtualKeyCode::Break, - //ffi::XK_Mode_switch => VirtualKeyCode::Mode_switch, - //ffi::XK_script_switch => VirtualKeyCode::Script_switch, - ffi::XK_Num_Lock => VirtualKeyCode::Numlock, - //ffi::XK_KP_Space => VirtualKeyCode::Kp_space, - //ffi::XK_KP_Tab => VirtualKeyCode::Kp_tab, - ffi::XK_KP_Enter => VirtualKeyCode::NumpadEnter, - //ffi::XK_KP_F1 => VirtualKeyCode::Kp_f1, - //ffi::XK_KP_F2 => VirtualKeyCode::Kp_f2, - //ffi::XK_KP_F3 => VirtualKeyCode::Kp_f3, - //ffi::XK_KP_F4 => VirtualKeyCode::Kp_f4, - ffi::XK_KP_Home => VirtualKeyCode::Home, - ffi::XK_KP_Left => VirtualKeyCode::Left, - ffi::XK_KP_Up => VirtualKeyCode::Up, - ffi::XK_KP_Right => VirtualKeyCode::Right, - ffi::XK_KP_Down => VirtualKeyCode::Down, - //ffi::XK_KP_Prior => VirtualKeyCode::Kp_prior, - ffi::XK_KP_Page_Up => VirtualKeyCode::PageUp, - //ffi::XK_KP_Next => VirtualKeyCode::Kp_next, - ffi::XK_KP_Page_Down => VirtualKeyCode::PageDown, - ffi::XK_KP_End => VirtualKeyCode::End, - //ffi::XK_KP_Begin => VirtualKeyCode::Kp_begin, - ffi::XK_KP_Insert => VirtualKeyCode::Insert, - ffi::XK_KP_Delete => VirtualKeyCode::Delete, - ffi::XK_KP_Equal => VirtualKeyCode::NumpadEquals, - ffi::XK_KP_Multiply => VirtualKeyCode::NumpadMultiply, - ffi::XK_KP_Add => VirtualKeyCode::NumpadAdd, - ffi::XK_KP_Separator => VirtualKeyCode::NumpadComma, - ffi::XK_KP_Subtract => VirtualKeyCode::NumpadSubtract, - ffi::XK_KP_Decimal => VirtualKeyCode::NumpadDecimal, - ffi::XK_KP_Divide => VirtualKeyCode::NumpadDivide, - ffi::XK_KP_0 => VirtualKeyCode::Numpad0, - ffi::XK_KP_1 => VirtualKeyCode::Numpad1, - ffi::XK_KP_2 => VirtualKeyCode::Numpad2, - ffi::XK_KP_3 => VirtualKeyCode::Numpad3, - ffi::XK_KP_4 => VirtualKeyCode::Numpad4, - ffi::XK_KP_5 => VirtualKeyCode::Numpad5, - ffi::XK_KP_6 => VirtualKeyCode::Numpad6, - ffi::XK_KP_7 => VirtualKeyCode::Numpad7, - ffi::XK_KP_8 => VirtualKeyCode::Numpad8, - ffi::XK_KP_9 => VirtualKeyCode::Numpad9, - ffi::XK_F1 => VirtualKeyCode::F1, - ffi::XK_F2 => VirtualKeyCode::F2, - ffi::XK_F3 => VirtualKeyCode::F3, - ffi::XK_F4 => VirtualKeyCode::F4, - ffi::XK_F5 => VirtualKeyCode::F5, - ffi::XK_F6 => VirtualKeyCode::F6, - ffi::XK_F7 => VirtualKeyCode::F7, - ffi::XK_F8 => VirtualKeyCode::F8, - ffi::XK_F9 => VirtualKeyCode::F9, - ffi::XK_F10 => VirtualKeyCode::F10, - ffi::XK_F11 => VirtualKeyCode::F11, - //ffi::XK_L1 => VirtualKeyCode::L1, - ffi::XK_F12 => VirtualKeyCode::F12, - //ffi::XK_L2 => VirtualKeyCode::L2, - ffi::XK_F13 => VirtualKeyCode::F13, - //ffi::XK_L3 => VirtualKeyCode::L3, - ffi::XK_F14 => VirtualKeyCode::F14, - //ffi::XK_L4 => VirtualKeyCode::L4, - ffi::XK_F15 => VirtualKeyCode::F15, - //ffi::XK_L5 => VirtualKeyCode::L5, - ffi::XK_F16 => VirtualKeyCode::F16, - //ffi::XK_L6 => VirtualKeyCode::L6, - ffi::XK_F17 => VirtualKeyCode::F17, - //ffi::XK_L7 => VirtualKeyCode::L7, - ffi::XK_F18 => VirtualKeyCode::F18, - //ffi::XK_L8 => VirtualKeyCode::L8, - ffi::XK_F19 => VirtualKeyCode::F19, - //ffi::XK_L9 => VirtualKeyCode::L9, - ffi::XK_F20 => VirtualKeyCode::F20, - //ffi::XK_L10 => VirtualKeyCode::L10, - ffi::XK_F21 => VirtualKeyCode::F21, - //ffi::XK_R1 => VirtualKeyCode::R1, - ffi::XK_F22 => VirtualKeyCode::F22, - //ffi::XK_R2 => VirtualKeyCode::R2, - ffi::XK_F23 => VirtualKeyCode::F23, - //ffi::XK_R3 => VirtualKeyCode::R3, - ffi::XK_F24 => VirtualKeyCode::F24, - //ffi::XK_R4 => VirtualKeyCode::R4, - //ffi::XK_F25 => VirtualKeyCode::F25, - //ffi::XK_R5 => VirtualKeyCode::R5, - //ffi::XK_F26 => VirtualKeyCode::F26, - //ffi::XK_R6 => VirtualKeyCode::R6, - //ffi::XK_F27 => VirtualKeyCode::F27, - //ffi::XK_R7 => VirtualKeyCode::R7, - //ffi::XK_F28 => VirtualKeyCode::F28, - //ffi::XK_R8 => VirtualKeyCode::R8, - //ffi::XK_F29 => VirtualKeyCode::F29, - //ffi::XK_R9 => VirtualKeyCode::R9, - //ffi::XK_F30 => VirtualKeyCode::F30, - //ffi::XK_R10 => VirtualKeyCode::R10, - //ffi::XK_F31 => VirtualKeyCode::F31, - //ffi::XK_R11 => VirtualKeyCode::R11, - //ffi::XK_F32 => VirtualKeyCode::F32, - //ffi::XK_R12 => VirtualKeyCode::R12, - //ffi::XK_F33 => VirtualKeyCode::F33, - //ffi::XK_R13 => VirtualKeyCode::R13, - //ffi::XK_F34 => VirtualKeyCode::F34, - //ffi::XK_R14 => VirtualKeyCode::R14, - //ffi::XK_F35 => VirtualKeyCode::F35, - //ffi::XK_R15 => VirtualKeyCode::R15, - ffi::XK_Shift_L => VirtualKeyCode::LShift, - ffi::XK_Shift_R => VirtualKeyCode::RShift, - ffi::XK_Control_L => VirtualKeyCode::LControl, - ffi::XK_Control_R => VirtualKeyCode::RControl, - 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, - ffi::XK_Alt_L => VirtualKeyCode::LAlt, - ffi::XK_Alt_R => VirtualKeyCode::RAlt, - //ffi::XK_Super_L => VirtualKeyCode::Super_l, - //ffi::XK_Super_R => VirtualKeyCode::Super_r, - //ffi::XK_Hyper_L => VirtualKeyCode::Hyper_l, - //ffi::XK_Hyper_R => VirtualKeyCode::Hyper_r, - ffi::XK_ISO_Left_Tab => VirtualKeyCode::Tab, - ffi::XK_space => VirtualKeyCode::Space, - //ffi::XK_exclam => VirtualKeyCode::Exclam, - //ffi::XK_quotedbl => VirtualKeyCode::Quotedbl, - //ffi::XK_numbersign => VirtualKeyCode::Numbersign, - //ffi::XK_dollar => VirtualKeyCode::Dollar, - //ffi::XK_percent => VirtualKeyCode::Percent, - //ffi::XK_ampersand => VirtualKeyCode::Ampersand, - ffi::XK_apostrophe => VirtualKeyCode::Apostrophe, - //ffi::XK_quoteright => VirtualKeyCode::Quoteright, - //ffi::XK_parenleft => VirtualKeyCode::Parenleft, - //ffi::XK_parenright => VirtualKeyCode::Parenright, - ffi::XK_asterisk => VirtualKeyCode::Asterisk, - ffi::XK_plus => VirtualKeyCode::Plus, - ffi::XK_comma => VirtualKeyCode::Comma, - ffi::XK_minus => VirtualKeyCode::Minus, - ffi::XK_period => VirtualKeyCode::Period, - ffi::XK_slash => VirtualKeyCode::Slash, - ffi::XK_0 => VirtualKeyCode::Key0, - ffi::XK_1 => VirtualKeyCode::Key1, - ffi::XK_2 => VirtualKeyCode::Key2, - ffi::XK_3 => VirtualKeyCode::Key3, - ffi::XK_4 => VirtualKeyCode::Key4, - ffi::XK_5 => VirtualKeyCode::Key5, - ffi::XK_6 => VirtualKeyCode::Key6, - ffi::XK_7 => VirtualKeyCode::Key7, - ffi::XK_8 => VirtualKeyCode::Key8, - ffi::XK_9 => VirtualKeyCode::Key9, - ffi::XK_colon => VirtualKeyCode::Colon, - ffi::XK_semicolon => VirtualKeyCode::Semicolon, - //ffi::XK_less => VirtualKeyCode::Less, - ffi::XK_equal => VirtualKeyCode::Equals, - //ffi::XK_greater => VirtualKeyCode::Greater, - //ffi::XK_question => VirtualKeyCode::Question, - ffi::XK_at => VirtualKeyCode::At, - ffi::XK_A => VirtualKeyCode::A, - ffi::XK_B => VirtualKeyCode::B, - ffi::XK_C => VirtualKeyCode::C, - ffi::XK_D => VirtualKeyCode::D, - ffi::XK_E => VirtualKeyCode::E, - ffi::XK_F => VirtualKeyCode::F, - ffi::XK_G => VirtualKeyCode::G, - ffi::XK_H => VirtualKeyCode::H, - ffi::XK_I => VirtualKeyCode::I, - ffi::XK_J => VirtualKeyCode::J, - ffi::XK_K => VirtualKeyCode::K, - ffi::XK_L => VirtualKeyCode::L, - ffi::XK_M => VirtualKeyCode::M, - ffi::XK_N => VirtualKeyCode::N, - ffi::XK_O => VirtualKeyCode::O, - ffi::XK_P => VirtualKeyCode::P, - ffi::XK_Q => VirtualKeyCode::Q, - ffi::XK_R => VirtualKeyCode::R, - ffi::XK_S => VirtualKeyCode::S, - ffi::XK_T => VirtualKeyCode::T, - ffi::XK_U => VirtualKeyCode::U, - ffi::XK_V => VirtualKeyCode::V, - ffi::XK_W => VirtualKeyCode::W, - ffi::XK_X => VirtualKeyCode::X, - ffi::XK_Y => VirtualKeyCode::Y, - ffi::XK_Z => VirtualKeyCode::Z, - ffi::XK_bracketleft => VirtualKeyCode::LBracket, - ffi::XK_backslash => VirtualKeyCode::Backslash, - ffi::XK_bracketright => VirtualKeyCode::RBracket, - //ffi::XK_asciicircum => VirtualKeyCode::Asciicircum, - //ffi::XK_underscore => VirtualKeyCode::Underscore, - ffi::XK_grave => VirtualKeyCode::Grave, - //ffi::XK_quoteleft => VirtualKeyCode::Quoteleft, - ffi::XK_a => VirtualKeyCode::A, - ffi::XK_b => VirtualKeyCode::B, - ffi::XK_c => VirtualKeyCode::C, - ffi::XK_d => VirtualKeyCode::D, - ffi::XK_e => VirtualKeyCode::E, - ffi::XK_f => VirtualKeyCode::F, - ffi::XK_g => VirtualKeyCode::G, - ffi::XK_h => VirtualKeyCode::H, - ffi::XK_i => VirtualKeyCode::I, - ffi::XK_j => VirtualKeyCode::J, - ffi::XK_k => VirtualKeyCode::K, - ffi::XK_l => VirtualKeyCode::L, - ffi::XK_m => VirtualKeyCode::M, - ffi::XK_n => VirtualKeyCode::N, - ffi::XK_o => VirtualKeyCode::O, - ffi::XK_p => VirtualKeyCode::P, - ffi::XK_q => VirtualKeyCode::Q, - ffi::XK_r => VirtualKeyCode::R, - ffi::XK_s => VirtualKeyCode::S, - ffi::XK_t => VirtualKeyCode::T, - ffi::XK_u => VirtualKeyCode::U, - ffi::XK_v => VirtualKeyCode::V, - ffi::XK_w => VirtualKeyCode::W, - ffi::XK_x => VirtualKeyCode::X, - ffi::XK_y => VirtualKeyCode::Y, - ffi::XK_z => VirtualKeyCode::Z, - //ffi::XK_braceleft => VirtualKeyCode::Braceleft, - //ffi::XK_bar => VirtualKeyCode::Bar, - //ffi::XK_braceright => VirtualKeyCode::Braceright, - //ffi::XK_asciitilde => VirtualKeyCode::Asciitilde, - //ffi::XK_nobreakspace => VirtualKeyCode::Nobreakspace, - //ffi::XK_exclamdown => VirtualKeyCode::Exclamdown, - //ffi::XK_cent => VirtualKeyCode::Cent, - //ffi::XK_sterling => VirtualKeyCode::Sterling, - //ffi::XK_currency => VirtualKeyCode::Currency, - //ffi::XK_yen => VirtualKeyCode::Yen, - //ffi::XK_brokenbar => VirtualKeyCode::Brokenbar, - //ffi::XK_section => VirtualKeyCode::Section, - //ffi::XK_diaeresis => VirtualKeyCode::Diaeresis, - //ffi::XK_copyright => VirtualKeyCode::Copyright, - //ffi::XK_ordfeminine => VirtualKeyCode::Ordfeminine, - //ffi::XK_guillemotleft => VirtualKeyCode::Guillemotleft, - //ffi::XK_notsign => VirtualKeyCode::Notsign, - //ffi::XK_hyphen => VirtualKeyCode::Hyphen, - //ffi::XK_registered => VirtualKeyCode::Registered, - //ffi::XK_macron => VirtualKeyCode::Macron, - //ffi::XK_degree => VirtualKeyCode::Degree, - //ffi::XK_plusminus => VirtualKeyCode::Plusminus, - //ffi::XK_twosuperior => VirtualKeyCode::Twosuperior, - //ffi::XK_threesuperior => VirtualKeyCode::Threesuperior, - //ffi::XK_acute => VirtualKeyCode::Acute, - //ffi::XK_mu => VirtualKeyCode::Mu, - //ffi::XK_paragraph => VirtualKeyCode::Paragraph, - //ffi::XK_periodcentered => VirtualKeyCode::Periodcentered, - //ffi::XK_cedilla => VirtualKeyCode::Cedilla, - //ffi::XK_onesuperior => VirtualKeyCode::Onesuperior, - //ffi::XK_masculine => VirtualKeyCode::Masculine, - //ffi::XK_guillemotright => VirtualKeyCode::Guillemotright, - //ffi::XK_onequarter => VirtualKeyCode::Onequarter, - //ffi::XK_onehalf => VirtualKeyCode::Onehalf, - //ffi::XK_threequarters => VirtualKeyCode::Threequarters, - //ffi::XK_questiondown => VirtualKeyCode::Questiondown, - //ffi::XK_Agrave => VirtualKeyCode::Agrave, - //ffi::XK_Aacute => VirtualKeyCode::Aacute, - //ffi::XK_Acircumflex => VirtualKeyCode::Acircumflex, - //ffi::XK_Atilde => VirtualKeyCode::Atilde, - //ffi::XK_Adiaeresis => VirtualKeyCode::Adiaeresis, - //ffi::XK_Aring => VirtualKeyCode::Aring, - //ffi::XK_AE => VirtualKeyCode::Ae, - //ffi::XK_Ccedilla => VirtualKeyCode::Ccedilla, - //ffi::XK_Egrave => VirtualKeyCode::Egrave, - //ffi::XK_Eacute => VirtualKeyCode::Eacute, - //ffi::XK_Ecircumflex => VirtualKeyCode::Ecircumflex, - //ffi::XK_Ediaeresis => VirtualKeyCode::Ediaeresis, - //ffi::XK_Igrave => VirtualKeyCode::Igrave, - //ffi::XK_Iacute => VirtualKeyCode::Iacute, - //ffi::XK_Icircumflex => VirtualKeyCode::Icircumflex, - //ffi::XK_Idiaeresis => VirtualKeyCode::Idiaeresis, - //ffi::XK_ETH => VirtualKeyCode::Eth, - //ffi::XK_Eth => VirtualKeyCode::Eth, - //ffi::XK_Ntilde => VirtualKeyCode::Ntilde, - //ffi::XK_Ograve => VirtualKeyCode::Ograve, - //ffi::XK_Oacute => VirtualKeyCode::Oacute, - //ffi::XK_Ocircumflex => VirtualKeyCode::Ocircumflex, - //ffi::XK_Otilde => VirtualKeyCode::Otilde, - //ffi::XK_Odiaeresis => VirtualKeyCode::Odiaeresis, - //ffi::XK_multiply => VirtualKeyCode::Multiply, - //ffi::XK_Ooblique => VirtualKeyCode::Ooblique, - //ffi::XK_Ugrave => VirtualKeyCode::Ugrave, - //ffi::XK_Uacute => VirtualKeyCode::Uacute, - //ffi::XK_Ucircumflex => VirtualKeyCode::Ucircumflex, - //ffi::XK_Udiaeresis => VirtualKeyCode::Udiaeresis, - //ffi::XK_Yacute => VirtualKeyCode::Yacute, - //ffi::XK_THORN => VirtualKeyCode::Thorn, - //ffi::XK_Thorn => VirtualKeyCode::Thorn, - //ffi::XK_ssharp => VirtualKeyCode::Ssharp, - //ffi::XK_agrave => VirtualKeyCode::Agrave, - //ffi::XK_aacute => VirtualKeyCode::Aacute, - //ffi::XK_acircumflex => VirtualKeyCode::Acircumflex, - //ffi::XK_atilde => VirtualKeyCode::Atilde, - //ffi::XK_adiaeresis => VirtualKeyCode::Adiaeresis, - //ffi::XK_aring => VirtualKeyCode::Aring, - //ffi::XK_ae => VirtualKeyCode::Ae, - //ffi::XK_ccedilla => VirtualKeyCode::Ccedilla, - //ffi::XK_egrave => VirtualKeyCode::Egrave, - //ffi::XK_eacute => VirtualKeyCode::Eacute, - //ffi::XK_ecircumflex => VirtualKeyCode::Ecircumflex, - //ffi::XK_ediaeresis => VirtualKeyCode::Ediaeresis, - //ffi::XK_igrave => VirtualKeyCode::Igrave, - //ffi::XK_iacute => VirtualKeyCode::Iacute, - //ffi::XK_icircumflex => VirtualKeyCode::Icircumflex, - //ffi::XK_idiaeresis => VirtualKeyCode::Idiaeresis, - //ffi::XK_eth => VirtualKeyCode::Eth, - //ffi::XK_ntilde => VirtualKeyCode::Ntilde, - //ffi::XK_ograve => VirtualKeyCode::Ograve, - //ffi::XK_oacute => VirtualKeyCode::Oacute, - //ffi::XK_ocircumflex => VirtualKeyCode::Ocircumflex, - //ffi::XK_otilde => VirtualKeyCode::Otilde, - //ffi::XK_odiaeresis => VirtualKeyCode::Odiaeresis, - //ffi::XK_division => VirtualKeyCode::Division, - //ffi::XK_oslash => VirtualKeyCode::Oslash, - //ffi::XK_ugrave => VirtualKeyCode::Ugrave, - //ffi::XK_uacute => VirtualKeyCode::Uacute, - //ffi::XK_ucircumflex => VirtualKeyCode::Ucircumflex, - //ffi::XK_udiaeresis => VirtualKeyCode::Udiaeresis, - //ffi::XK_yacute => VirtualKeyCode::Yacute, - //ffi::XK_thorn => VirtualKeyCode::Thorn, - //ffi::XK_ydiaeresis => VirtualKeyCode::Ydiaeresis, - //ffi::XK_Aogonek => VirtualKeyCode::Aogonek, - //ffi::XK_breve => VirtualKeyCode::Breve, - //ffi::XK_Lstroke => VirtualKeyCode::Lstroke, - //ffi::XK_Lcaron => VirtualKeyCode::Lcaron, - //ffi::XK_Sacute => VirtualKeyCode::Sacute, - //ffi::XK_Scaron => VirtualKeyCode::Scaron, - //ffi::XK_Scedilla => VirtualKeyCode::Scedilla, - //ffi::XK_Tcaron => VirtualKeyCode::Tcaron, - //ffi::XK_Zacute => VirtualKeyCode::Zacute, - //ffi::XK_Zcaron => VirtualKeyCode::Zcaron, - //ffi::XK_Zabovedot => VirtualKeyCode::Zabovedot, - //ffi::XK_aogonek => VirtualKeyCode::Aogonek, - //ffi::XK_ogonek => VirtualKeyCode::Ogonek, - //ffi::XK_lstroke => VirtualKeyCode::Lstroke, - //ffi::XK_lcaron => VirtualKeyCode::Lcaron, - //ffi::XK_sacute => VirtualKeyCode::Sacute, - //ffi::XK_caron => VirtualKeyCode::Caron, - //ffi::XK_scaron => VirtualKeyCode::Scaron, - //ffi::XK_scedilla => VirtualKeyCode::Scedilla, - //ffi::XK_tcaron => VirtualKeyCode::Tcaron, - //ffi::XK_zacute => VirtualKeyCode::Zacute, - //ffi::XK_doubleacute => VirtualKeyCode::Doubleacute, - //ffi::XK_zcaron => VirtualKeyCode::Zcaron, - //ffi::XK_zabovedot => VirtualKeyCode::Zabovedot, - //ffi::XK_Racute => VirtualKeyCode::Racute, - //ffi::XK_Abreve => VirtualKeyCode::Abreve, - //ffi::XK_Lacute => VirtualKeyCode::Lacute, - //ffi::XK_Cacute => VirtualKeyCode::Cacute, - //ffi::XK_Ccaron => VirtualKeyCode::Ccaron, - //ffi::XK_Eogonek => VirtualKeyCode::Eogonek, - //ffi::XK_Ecaron => VirtualKeyCode::Ecaron, - //ffi::XK_Dcaron => VirtualKeyCode::Dcaron, - //ffi::XK_Dstroke => VirtualKeyCode::Dstroke, - //ffi::XK_Nacute => VirtualKeyCode::Nacute, - //ffi::XK_Ncaron => VirtualKeyCode::Ncaron, - //ffi::XK_Odoubleacute => VirtualKeyCode::Odoubleacute, - //ffi::XK_Rcaron => VirtualKeyCode::Rcaron, - //ffi::XK_Uring => VirtualKeyCode::Uring, - //ffi::XK_Udoubleacute => VirtualKeyCode::Udoubleacute, - //ffi::XK_Tcedilla => VirtualKeyCode::Tcedilla, - //ffi::XK_racute => VirtualKeyCode::Racute, - //ffi::XK_abreve => VirtualKeyCode::Abreve, - //ffi::XK_lacute => VirtualKeyCode::Lacute, - //ffi::XK_cacute => VirtualKeyCode::Cacute, - //ffi::XK_ccaron => VirtualKeyCode::Ccaron, - //ffi::XK_eogonek => VirtualKeyCode::Eogonek, - //ffi::XK_ecaron => VirtualKeyCode::Ecaron, - //ffi::XK_dcaron => VirtualKeyCode::Dcaron, - //ffi::XK_dstroke => VirtualKeyCode::Dstroke, - //ffi::XK_nacute => VirtualKeyCode::Nacute, - //ffi::XK_ncaron => VirtualKeyCode::Ncaron, - //ffi::XK_odoubleacute => VirtualKeyCode::Odoubleacute, - //ffi::XK_udoubleacute => VirtualKeyCode::Udoubleacute, - //ffi::XK_rcaron => VirtualKeyCode::Rcaron, - //ffi::XK_uring => VirtualKeyCode::Uring, - //ffi::XK_tcedilla => VirtualKeyCode::Tcedilla, - //ffi::XK_abovedot => VirtualKeyCode::Abovedot, - //ffi::XK_Hstroke => VirtualKeyCode::Hstroke, - //ffi::XK_Hcircumflex => VirtualKeyCode::Hcircumflex, - //ffi::XK_Iabovedot => VirtualKeyCode::Iabovedot, - //ffi::XK_Gbreve => VirtualKeyCode::Gbreve, - //ffi::XK_Jcircumflex => VirtualKeyCode::Jcircumflex, - //ffi::XK_hstroke => VirtualKeyCode::Hstroke, - //ffi::XK_hcircumflex => VirtualKeyCode::Hcircumflex, - //ffi::XK_idotless => VirtualKeyCode::Idotless, - //ffi::XK_gbreve => VirtualKeyCode::Gbreve, - //ffi::XK_jcircumflex => VirtualKeyCode::Jcircumflex, - //ffi::XK_Cabovedot => VirtualKeyCode::Cabovedot, - //ffi::XK_Ccircumflex => VirtualKeyCode::Ccircumflex, - //ffi::XK_Gabovedot => VirtualKeyCode::Gabovedot, - //ffi::XK_Gcircumflex => VirtualKeyCode::Gcircumflex, - //ffi::XK_Ubreve => VirtualKeyCode::Ubreve, - //ffi::XK_Scircumflex => VirtualKeyCode::Scircumflex, - //ffi::XK_cabovedot => VirtualKeyCode::Cabovedot, - //ffi::XK_ccircumflex => VirtualKeyCode::Ccircumflex, - //ffi::XK_gabovedot => VirtualKeyCode::Gabovedot, - //ffi::XK_gcircumflex => VirtualKeyCode::Gcircumflex, - //ffi::XK_ubreve => VirtualKeyCode::Ubreve, - //ffi::XK_scircumflex => VirtualKeyCode::Scircumflex, - //ffi::XK_kra => VirtualKeyCode::Kra, - //ffi::XK_kappa => VirtualKeyCode::Kappa, - //ffi::XK_Rcedilla => VirtualKeyCode::Rcedilla, - //ffi::XK_Itilde => VirtualKeyCode::Itilde, - //ffi::XK_Lcedilla => VirtualKeyCode::Lcedilla, - //ffi::XK_Emacron => VirtualKeyCode::Emacron, - //ffi::XK_Gcedilla => VirtualKeyCode::Gcedilla, - //ffi::XK_Tslash => VirtualKeyCode::Tslash, - //ffi::XK_rcedilla => VirtualKeyCode::Rcedilla, - //ffi::XK_itilde => VirtualKeyCode::Itilde, - //ffi::XK_lcedilla => VirtualKeyCode::Lcedilla, - //ffi::XK_emacron => VirtualKeyCode::Emacron, - //ffi::XK_gcedilla => VirtualKeyCode::Gcedilla, - //ffi::XK_tslash => VirtualKeyCode::Tslash, - //ffi::XK_ENG => VirtualKeyCode::Eng, - //ffi::XK_eng => VirtualKeyCode::Eng, - //ffi::XK_Amacron => VirtualKeyCode::Amacron, - //ffi::XK_Iogonek => VirtualKeyCode::Iogonek, - //ffi::XK_Eabovedot => VirtualKeyCode::Eabovedot, - //ffi::XK_Imacron => VirtualKeyCode::Imacron, - //ffi::XK_Ncedilla => VirtualKeyCode::Ncedilla, - //ffi::XK_Omacron => VirtualKeyCode::Omacron, - //ffi::XK_Kcedilla => VirtualKeyCode::Kcedilla, - //ffi::XK_Uogonek => VirtualKeyCode::Uogonek, - //ffi::XK_Utilde => VirtualKeyCode::Utilde, - //ffi::XK_Umacron => VirtualKeyCode::Umacron, - //ffi::XK_amacron => VirtualKeyCode::Amacron, - //ffi::XK_iogonek => VirtualKeyCode::Iogonek, - //ffi::XK_eabovedot => VirtualKeyCode::Eabovedot, - //ffi::XK_imacron => VirtualKeyCode::Imacron, - //ffi::XK_ncedilla => VirtualKeyCode::Ncedilla, - //ffi::XK_omacron => VirtualKeyCode::Omacron, - //ffi::XK_kcedilla => VirtualKeyCode::Kcedilla, - //ffi::XK_uogonek => VirtualKeyCode::Uogonek, - //ffi::XK_utilde => VirtualKeyCode::Utilde, - //ffi::XK_umacron => VirtualKeyCode::Umacron, - //ffi::XK_overline => VirtualKeyCode::Overline, - //ffi::XK_kana_fullstop => VirtualKeyCode::Kana_fullstop, - //ffi::XK_kana_openingbracket => VirtualKeyCode::Kana_openingbracket, - //ffi::XK_kana_closingbracket => VirtualKeyCode::Kana_closingbracket, - //ffi::XK_kana_comma => VirtualKeyCode::Kana_comma, - //ffi::XK_kana_conjunctive => VirtualKeyCode::Kana_conjunctive, - //ffi::XK_kana_middledot => VirtualKeyCode::Kana_middledot, - //ffi::XK_kana_WO => VirtualKeyCode::Kana_wo, - //ffi::XK_kana_a => VirtualKeyCode::Kana_a, - //ffi::XK_kana_i => VirtualKeyCode::Kana_i, - //ffi::XK_kana_u => VirtualKeyCode::Kana_u, - //ffi::XK_kana_e => VirtualKeyCode::Kana_e, - //ffi::XK_kana_o => VirtualKeyCode::Kana_o, - //ffi::XK_kana_ya => VirtualKeyCode::Kana_ya, - //ffi::XK_kana_yu => VirtualKeyCode::Kana_yu, - //ffi::XK_kana_yo => VirtualKeyCode::Kana_yo, - //ffi::XK_kana_tsu => VirtualKeyCode::Kana_tsu, - //ffi::XK_kana_tu => VirtualKeyCode::Kana_tu, - //ffi::XK_prolongedsound => VirtualKeyCode::Prolongedsound, - //ffi::XK_kana_A => VirtualKeyCode::Kana_a, - //ffi::XK_kana_I => VirtualKeyCode::Kana_i, - //ffi::XK_kana_U => VirtualKeyCode::Kana_u, - //ffi::XK_kana_E => VirtualKeyCode::Kana_e, - //ffi::XK_kana_O => VirtualKeyCode::Kana_o, - //ffi::XK_kana_KA => VirtualKeyCode::Kana_ka, - //ffi::XK_kana_KI => VirtualKeyCode::Kana_ki, - //ffi::XK_kana_KU => VirtualKeyCode::Kana_ku, - //ffi::XK_kana_KE => VirtualKeyCode::Kana_ke, - //ffi::XK_kana_KO => VirtualKeyCode::Kana_ko, - //ffi::XK_kana_SA => VirtualKeyCode::Kana_sa, - //ffi::XK_kana_SHI => VirtualKeyCode::Kana_shi, - //ffi::XK_kana_SU => VirtualKeyCode::Kana_su, - //ffi::XK_kana_SE => VirtualKeyCode::Kana_se, - //ffi::XK_kana_SO => VirtualKeyCode::Kana_so, - //ffi::XK_kana_TA => VirtualKeyCode::Kana_ta, - //ffi::XK_kana_CHI => VirtualKeyCode::Kana_chi, - //ffi::XK_kana_TI => VirtualKeyCode::Kana_ti, - //ffi::XK_kana_TSU => VirtualKeyCode::Kana_tsu, - //ffi::XK_kana_TU => VirtualKeyCode::Kana_tu, - //ffi::XK_kana_TE => VirtualKeyCode::Kana_te, - //ffi::XK_kana_TO => VirtualKeyCode::Kana_to, - //ffi::XK_kana_NA => VirtualKeyCode::Kana_na, - //ffi::XK_kana_NI => VirtualKeyCode::Kana_ni, - //ffi::XK_kana_NU => VirtualKeyCode::Kana_nu, - //ffi::XK_kana_NE => VirtualKeyCode::Kana_ne, - //ffi::XK_kana_NO => VirtualKeyCode::Kana_no, - //ffi::XK_kana_HA => VirtualKeyCode::Kana_ha, - //ffi::XK_kana_HI => VirtualKeyCode::Kana_hi, - //ffi::XK_kana_FU => VirtualKeyCode::Kana_fu, - //ffi::XK_kana_HU => VirtualKeyCode::Kana_hu, - //ffi::XK_kana_HE => VirtualKeyCode::Kana_he, - //ffi::XK_kana_HO => VirtualKeyCode::Kana_ho, - //ffi::XK_kana_MA => VirtualKeyCode::Kana_ma, - //ffi::XK_kana_MI => VirtualKeyCode::Kana_mi, - //ffi::XK_kana_MU => VirtualKeyCode::Kana_mu, - //ffi::XK_kana_ME => VirtualKeyCode::Kana_me, - //ffi::XK_kana_MO => VirtualKeyCode::Kana_mo, - //ffi::XK_kana_YA => VirtualKeyCode::Kana_ya, - //ffi::XK_kana_YU => VirtualKeyCode::Kana_yu, - //ffi::XK_kana_YO => VirtualKeyCode::Kana_yo, - //ffi::XK_kana_RA => VirtualKeyCode::Kana_ra, - //ffi::XK_kana_RI => VirtualKeyCode::Kana_ri, - //ffi::XK_kana_RU => VirtualKeyCode::Kana_ru, - //ffi::XK_kana_RE => VirtualKeyCode::Kana_re, - //ffi::XK_kana_RO => VirtualKeyCode::Kana_ro, - //ffi::XK_kana_WA => VirtualKeyCode::Kana_wa, - //ffi::XK_kana_N => VirtualKeyCode::Kana_n, - //ffi::XK_voicedsound => VirtualKeyCode::Voicedsound, - //ffi::XK_semivoicedsound => VirtualKeyCode::Semivoicedsound, - //ffi::XK_kana_switch => VirtualKeyCode::Kana_switch, - //ffi::XK_Arabic_comma => VirtualKeyCode::Arabic_comma, - //ffi::XK_Arabic_semicolon => VirtualKeyCode::Arabic_semicolon, - //ffi::XK_Arabic_question_mark => VirtualKeyCode::Arabic_question_mark, - //ffi::XK_Arabic_hamza => VirtualKeyCode::Arabic_hamza, - //ffi::XK_Arabic_maddaonalef => VirtualKeyCode::Arabic_maddaonalef, - //ffi::XK_Arabic_hamzaonalef => VirtualKeyCode::Arabic_hamzaonalef, - //ffi::XK_Arabic_hamzaonwaw => VirtualKeyCode::Arabic_hamzaonwaw, - //ffi::XK_Arabic_hamzaunderalef => VirtualKeyCode::Arabic_hamzaunderalef, - //ffi::XK_Arabic_hamzaonyeh => VirtualKeyCode::Arabic_hamzaonyeh, - //ffi::XK_Arabic_alef => VirtualKeyCode::Arabic_alef, - //ffi::XK_Arabic_beh => VirtualKeyCode::Arabic_beh, - //ffi::XK_Arabic_tehmarbuta => VirtualKeyCode::Arabic_tehmarbuta, - //ffi::XK_Arabic_teh => VirtualKeyCode::Arabic_teh, - //ffi::XK_Arabic_theh => VirtualKeyCode::Arabic_theh, - //ffi::XK_Arabic_jeem => VirtualKeyCode::Arabic_jeem, - //ffi::XK_Arabic_hah => VirtualKeyCode::Arabic_hah, - //ffi::XK_Arabic_khah => VirtualKeyCode::Arabic_khah, - //ffi::XK_Arabic_dal => VirtualKeyCode::Arabic_dal, - //ffi::XK_Arabic_thal => VirtualKeyCode::Arabic_thal, - //ffi::XK_Arabic_ra => VirtualKeyCode::Arabic_ra, - //ffi::XK_Arabic_zain => VirtualKeyCode::Arabic_zain, - //ffi::XK_Arabic_seen => VirtualKeyCode::Arabic_seen, - //ffi::XK_Arabic_sheen => VirtualKeyCode::Arabic_sheen, - //ffi::XK_Arabic_sad => VirtualKeyCode::Arabic_sad, - //ffi::XK_Arabic_dad => VirtualKeyCode::Arabic_dad, - //ffi::XK_Arabic_tah => VirtualKeyCode::Arabic_tah, - //ffi::XK_Arabic_zah => VirtualKeyCode::Arabic_zah, - //ffi::XK_Arabic_ain => VirtualKeyCode::Arabic_ain, - //ffi::XK_Arabic_ghain => VirtualKeyCode::Arabic_ghain, - //ffi::XK_Arabic_tatweel => VirtualKeyCode::Arabic_tatweel, - //ffi::XK_Arabic_feh => VirtualKeyCode::Arabic_feh, - //ffi::XK_Arabic_qaf => VirtualKeyCode::Arabic_qaf, - //ffi::XK_Arabic_kaf => VirtualKeyCode::Arabic_kaf, - //ffi::XK_Arabic_lam => VirtualKeyCode::Arabic_lam, - //ffi::XK_Arabic_meem => VirtualKeyCode::Arabic_meem, - //ffi::XK_Arabic_noon => VirtualKeyCode::Arabic_noon, - //ffi::XK_Arabic_ha => VirtualKeyCode::Arabic_ha, - //ffi::XK_Arabic_heh => VirtualKeyCode::Arabic_heh, - //ffi::XK_Arabic_waw => VirtualKeyCode::Arabic_waw, - //ffi::XK_Arabic_alefmaksura => VirtualKeyCode::Arabic_alefmaksura, - //ffi::XK_Arabic_yeh => VirtualKeyCode::Arabic_yeh, - //ffi::XK_Arabic_fathatan => VirtualKeyCode::Arabic_fathatan, - //ffi::XK_Arabic_dammatan => VirtualKeyCode::Arabic_dammatan, - //ffi::XK_Arabic_kasratan => VirtualKeyCode::Arabic_kasratan, - //ffi::XK_Arabic_fatha => VirtualKeyCode::Arabic_fatha, - //ffi::XK_Arabic_damma => VirtualKeyCode::Arabic_damma, - //ffi::XK_Arabic_kasra => VirtualKeyCode::Arabic_kasra, - //ffi::XK_Arabic_shadda => VirtualKeyCode::Arabic_shadda, - //ffi::XK_Arabic_sukun => VirtualKeyCode::Arabic_sukun, - //ffi::XK_Arabic_switch => VirtualKeyCode::Arabic_switch, - //ffi::XK_Serbian_dje => VirtualKeyCode::Serbian_dje, - //ffi::XK_Macedonia_gje => VirtualKeyCode::Macedonia_gje, - //ffi::XK_Cyrillic_io => VirtualKeyCode::Cyrillic_io, - //ffi::XK_Ukrainian_ie => VirtualKeyCode::Ukrainian_ie, - //ffi::XK_Ukranian_je => VirtualKeyCode::Ukranian_je, - //ffi::XK_Macedonia_dse => VirtualKeyCode::Macedonia_dse, - //ffi::XK_Ukrainian_i => VirtualKeyCode::Ukrainian_i, - //ffi::XK_Ukranian_i => VirtualKeyCode::Ukranian_i, - //ffi::XK_Ukrainian_yi => VirtualKeyCode::Ukrainian_yi, - //ffi::XK_Ukranian_yi => VirtualKeyCode::Ukranian_yi, - //ffi::XK_Cyrillic_je => VirtualKeyCode::Cyrillic_je, - //ffi::XK_Serbian_je => VirtualKeyCode::Serbian_je, - //ffi::XK_Cyrillic_lje => VirtualKeyCode::Cyrillic_lje, - //ffi::XK_Serbian_lje => VirtualKeyCode::Serbian_lje, - //ffi::XK_Cyrillic_nje => VirtualKeyCode::Cyrillic_nje, - //ffi::XK_Serbian_nje => VirtualKeyCode::Serbian_nje, - //ffi::XK_Serbian_tshe => VirtualKeyCode::Serbian_tshe, - //ffi::XK_Macedonia_kje => VirtualKeyCode::Macedonia_kje, - //ffi::XK_Byelorussian_shortu => VirtualKeyCode::Byelorussian_shortu, - //ffi::XK_Cyrillic_dzhe => VirtualKeyCode::Cyrillic_dzhe, - //ffi::XK_Serbian_dze => VirtualKeyCode::Serbian_dze, - //ffi::XK_numerosign => VirtualKeyCode::Numerosign, - //ffi::XK_Serbian_DJE => VirtualKeyCode::Serbian_dje, - //ffi::XK_Macedonia_GJE => VirtualKeyCode::Macedonia_gje, - //ffi::XK_Cyrillic_IO => VirtualKeyCode::Cyrillic_io, - //ffi::XK_Ukrainian_IE => VirtualKeyCode::Ukrainian_ie, - //ffi::XK_Ukranian_JE => VirtualKeyCode::Ukranian_je, - //ffi::XK_Macedonia_DSE => VirtualKeyCode::Macedonia_dse, - //ffi::XK_Ukrainian_I => VirtualKeyCode::Ukrainian_i, - //ffi::XK_Ukranian_I => VirtualKeyCode::Ukranian_i, - //ffi::XK_Ukrainian_YI => VirtualKeyCode::Ukrainian_yi, - //ffi::XK_Ukranian_YI => VirtualKeyCode::Ukranian_yi, - //ffi::XK_Cyrillic_JE => VirtualKeyCode::Cyrillic_je, - //ffi::XK_Serbian_JE => VirtualKeyCode::Serbian_je, - //ffi::XK_Cyrillic_LJE => VirtualKeyCode::Cyrillic_lje, - //ffi::XK_Serbian_LJE => VirtualKeyCode::Serbian_lje, - //ffi::XK_Cyrillic_NJE => VirtualKeyCode::Cyrillic_nje, - //ffi::XK_Serbian_NJE => VirtualKeyCode::Serbian_nje, - //ffi::XK_Serbian_TSHE => VirtualKeyCode::Serbian_tshe, - //ffi::XK_Macedonia_KJE => VirtualKeyCode::Macedonia_kje, - //ffi::XK_Byelorussian_SHORTU => VirtualKeyCode::Byelorussian_shortu, - //ffi::XK_Cyrillic_DZHE => VirtualKeyCode::Cyrillic_dzhe, - //ffi::XK_Serbian_DZE => VirtualKeyCode::Serbian_dze, - //ffi::XK_Cyrillic_yu => VirtualKeyCode::Cyrillic_yu, - //ffi::XK_Cyrillic_a => VirtualKeyCode::Cyrillic_a, - //ffi::XK_Cyrillic_be => VirtualKeyCode::Cyrillic_be, - //ffi::XK_Cyrillic_tse => VirtualKeyCode::Cyrillic_tse, - //ffi::XK_Cyrillic_de => VirtualKeyCode::Cyrillic_de, - //ffi::XK_Cyrillic_ie => VirtualKeyCode::Cyrillic_ie, - //ffi::XK_Cyrillic_ef => VirtualKeyCode::Cyrillic_ef, - //ffi::XK_Cyrillic_ghe => VirtualKeyCode::Cyrillic_ghe, - //ffi::XK_Cyrillic_ha => VirtualKeyCode::Cyrillic_ha, - //ffi::XK_Cyrillic_i => VirtualKeyCode::Cyrillic_i, - //ffi::XK_Cyrillic_shorti => VirtualKeyCode::Cyrillic_shorti, - //ffi::XK_Cyrillic_ka => VirtualKeyCode::Cyrillic_ka, - //ffi::XK_Cyrillic_el => VirtualKeyCode::Cyrillic_el, - //ffi::XK_Cyrillic_em => VirtualKeyCode::Cyrillic_em, - //ffi::XK_Cyrillic_en => VirtualKeyCode::Cyrillic_en, - //ffi::XK_Cyrillic_o => VirtualKeyCode::Cyrillic_o, - //ffi::XK_Cyrillic_pe => VirtualKeyCode::Cyrillic_pe, - //ffi::XK_Cyrillic_ya => VirtualKeyCode::Cyrillic_ya, - //ffi::XK_Cyrillic_er => VirtualKeyCode::Cyrillic_er, - //ffi::XK_Cyrillic_es => VirtualKeyCode::Cyrillic_es, - //ffi::XK_Cyrillic_te => VirtualKeyCode::Cyrillic_te, - //ffi::XK_Cyrillic_u => VirtualKeyCode::Cyrillic_u, - //ffi::XK_Cyrillic_zhe => VirtualKeyCode::Cyrillic_zhe, - //ffi::XK_Cyrillic_ve => VirtualKeyCode::Cyrillic_ve, - //ffi::XK_Cyrillic_softsign => VirtualKeyCode::Cyrillic_softsign, - //ffi::XK_Cyrillic_yeru => VirtualKeyCode::Cyrillic_yeru, - //ffi::XK_Cyrillic_ze => VirtualKeyCode::Cyrillic_ze, - //ffi::XK_Cyrillic_sha => VirtualKeyCode::Cyrillic_sha, - //ffi::XK_Cyrillic_e => VirtualKeyCode::Cyrillic_e, - //ffi::XK_Cyrillic_shcha => VirtualKeyCode::Cyrillic_shcha, - //ffi::XK_Cyrillic_che => VirtualKeyCode::Cyrillic_che, - //ffi::XK_Cyrillic_hardsign => VirtualKeyCode::Cyrillic_hardsign, - //ffi::XK_Cyrillic_YU => VirtualKeyCode::Cyrillic_yu, - //ffi::XK_Cyrillic_A => VirtualKeyCode::Cyrillic_a, - //ffi::XK_Cyrillic_BE => VirtualKeyCode::Cyrillic_be, - //ffi::XK_Cyrillic_TSE => VirtualKeyCode::Cyrillic_tse, - //ffi::XK_Cyrillic_DE => VirtualKeyCode::Cyrillic_de, - //ffi::XK_Cyrillic_IE => VirtualKeyCode::Cyrillic_ie, - //ffi::XK_Cyrillic_EF => VirtualKeyCode::Cyrillic_ef, - //ffi::XK_Cyrillic_GHE => VirtualKeyCode::Cyrillic_ghe, - //ffi::XK_Cyrillic_HA => VirtualKeyCode::Cyrillic_ha, - //ffi::XK_Cyrillic_I => VirtualKeyCode::Cyrillic_i, - //ffi::XK_Cyrillic_SHORTI => VirtualKeyCode::Cyrillic_shorti, - //ffi::XK_Cyrillic_KA => VirtualKeyCode::Cyrillic_ka, - //ffi::XK_Cyrillic_EL => VirtualKeyCode::Cyrillic_el, - //ffi::XK_Cyrillic_EM => VirtualKeyCode::Cyrillic_em, - //ffi::XK_Cyrillic_EN => VirtualKeyCode::Cyrillic_en, - //ffi::XK_Cyrillic_O => VirtualKeyCode::Cyrillic_o, - //ffi::XK_Cyrillic_PE => VirtualKeyCode::Cyrillic_pe, - //ffi::XK_Cyrillic_YA => VirtualKeyCode::Cyrillic_ya, - //ffi::XK_Cyrillic_ER => VirtualKeyCode::Cyrillic_er, - //ffi::XK_Cyrillic_ES => VirtualKeyCode::Cyrillic_es, - //ffi::XK_Cyrillic_TE => VirtualKeyCode::Cyrillic_te, - //ffi::XK_Cyrillic_U => VirtualKeyCode::Cyrillic_u, - //ffi::XK_Cyrillic_ZHE => VirtualKeyCode::Cyrillic_zhe, - //ffi::XK_Cyrillic_VE => VirtualKeyCode::Cyrillic_ve, - //ffi::XK_Cyrillic_SOFTSIGN => VirtualKeyCode::Cyrillic_softsign, - //ffi::XK_Cyrillic_YERU => VirtualKeyCode::Cyrillic_yeru, - //ffi::XK_Cyrillic_ZE => VirtualKeyCode::Cyrillic_ze, - //ffi::XK_Cyrillic_SHA => VirtualKeyCode::Cyrillic_sha, - //ffi::XK_Cyrillic_E => VirtualKeyCode::Cyrillic_e, - //ffi::XK_Cyrillic_SHCHA => VirtualKeyCode::Cyrillic_shcha, - //ffi::XK_Cyrillic_CHE => VirtualKeyCode::Cyrillic_che, - //ffi::XK_Cyrillic_HARDSIGN => VirtualKeyCode::Cyrillic_hardsign, - //ffi::XK_Greek_ALPHAaccent => VirtualKeyCode::Greek_alphaaccent, - //ffi::XK_Greek_EPSILONaccent => VirtualKeyCode::Greek_epsilonaccent, - //ffi::XK_Greek_ETAaccent => VirtualKeyCode::Greek_etaaccent, - //ffi::XK_Greek_IOTAaccent => VirtualKeyCode::Greek_iotaaccent, - //ffi::XK_Greek_IOTAdiaeresis => VirtualKeyCode::Greek_iotadiaeresis, - //ffi::XK_Greek_OMICRONaccent => VirtualKeyCode::Greek_omicronaccent, - //ffi::XK_Greek_UPSILONaccent => VirtualKeyCode::Greek_upsilonaccent, - //ffi::XK_Greek_UPSILONdieresis => VirtualKeyCode::Greek_upsilondieresis, - //ffi::XK_Greek_OMEGAaccent => VirtualKeyCode::Greek_omegaaccent, - //ffi::XK_Greek_accentdieresis => VirtualKeyCode::Greek_accentdieresis, - //ffi::XK_Greek_horizbar => VirtualKeyCode::Greek_horizbar, - //ffi::XK_Greek_alphaaccent => VirtualKeyCode::Greek_alphaaccent, - //ffi::XK_Greek_epsilonaccent => VirtualKeyCode::Greek_epsilonaccent, - //ffi::XK_Greek_etaaccent => VirtualKeyCode::Greek_etaaccent, - //ffi::XK_Greek_iotaaccent => VirtualKeyCode::Greek_iotaaccent, - //ffi::XK_Greek_iotadieresis => VirtualKeyCode::Greek_iotadieresis, - //ffi::XK_Greek_iotaaccentdieresis => VirtualKeyCode::Greek_iotaaccentdieresis, - //ffi::XK_Greek_omicronaccent => VirtualKeyCode::Greek_omicronaccent, - //ffi::XK_Greek_upsilonaccent => VirtualKeyCode::Greek_upsilonaccent, - //ffi::XK_Greek_upsilondieresis => VirtualKeyCode::Greek_upsilondieresis, - //ffi::XK_Greek_upsilonaccentdieresis => VirtualKeyCode::Greek_upsilonaccentdieresis, - //ffi::XK_Greek_omegaaccent => VirtualKeyCode::Greek_omegaaccent, - //ffi::XK_Greek_ALPHA => VirtualKeyCode::Greek_alpha, - //ffi::XK_Greek_BETA => VirtualKeyCode::Greek_beta, - //ffi::XK_Greek_GAMMA => VirtualKeyCode::Greek_gamma, - //ffi::XK_Greek_DELTA => VirtualKeyCode::Greek_delta, - //ffi::XK_Greek_EPSILON => VirtualKeyCode::Greek_epsilon, - //ffi::XK_Greek_ZETA => VirtualKeyCode::Greek_zeta, - //ffi::XK_Greek_ETA => VirtualKeyCode::Greek_eta, - //ffi::XK_Greek_THETA => VirtualKeyCode::Greek_theta, - //ffi::XK_Greek_IOTA => VirtualKeyCode::Greek_iota, - //ffi::XK_Greek_KAPPA => VirtualKeyCode::Greek_kappa, - //ffi::XK_Greek_LAMDA => VirtualKeyCode::Greek_lamda, - //ffi::XK_Greek_LAMBDA => VirtualKeyCode::Greek_lambda, - //ffi::XK_Greek_MU => VirtualKeyCode::Greek_mu, - //ffi::XK_Greek_NU => VirtualKeyCode::Greek_nu, - //ffi::XK_Greek_XI => VirtualKeyCode::Greek_xi, - //ffi::XK_Greek_OMICRON => VirtualKeyCode::Greek_omicron, - //ffi::XK_Greek_PI => VirtualKeyCode::Greek_pi, - //ffi::XK_Greek_RHO => VirtualKeyCode::Greek_rho, - //ffi::XK_Greek_SIGMA => VirtualKeyCode::Greek_sigma, - //ffi::XK_Greek_TAU => VirtualKeyCode::Greek_tau, - //ffi::XK_Greek_UPSILON => VirtualKeyCode::Greek_upsilon, - //ffi::XK_Greek_PHI => VirtualKeyCode::Greek_phi, - //ffi::XK_Greek_CHI => VirtualKeyCode::Greek_chi, - //ffi::XK_Greek_PSI => VirtualKeyCode::Greek_psi, - //ffi::XK_Greek_OMEGA => VirtualKeyCode::Greek_omega, - //ffi::XK_Greek_alpha => VirtualKeyCode::Greek_alpha, - //ffi::XK_Greek_beta => VirtualKeyCode::Greek_beta, - //ffi::XK_Greek_gamma => VirtualKeyCode::Greek_gamma, - //ffi::XK_Greek_delta => VirtualKeyCode::Greek_delta, - //ffi::XK_Greek_epsilon => VirtualKeyCode::Greek_epsilon, - //ffi::XK_Greek_zeta => VirtualKeyCode::Greek_zeta, - //ffi::XK_Greek_eta => VirtualKeyCode::Greek_eta, - //ffi::XK_Greek_theta => VirtualKeyCode::Greek_theta, - //ffi::XK_Greek_iota => VirtualKeyCode::Greek_iota, - //ffi::XK_Greek_kappa => VirtualKeyCode::Greek_kappa, - //ffi::XK_Greek_lamda => VirtualKeyCode::Greek_lamda, - //ffi::XK_Greek_lambda => VirtualKeyCode::Greek_lambda, - //ffi::XK_Greek_mu => VirtualKeyCode::Greek_mu, - //ffi::XK_Greek_nu => VirtualKeyCode::Greek_nu, - //ffi::XK_Greek_xi => VirtualKeyCode::Greek_xi, - //ffi::XK_Greek_omicron => VirtualKeyCode::Greek_omicron, - //ffi::XK_Greek_pi => VirtualKeyCode::Greek_pi, - //ffi::XK_Greek_rho => VirtualKeyCode::Greek_rho, - //ffi::XK_Greek_sigma => VirtualKeyCode::Greek_sigma, - //ffi::XK_Greek_finalsmallsigma => VirtualKeyCode::Greek_finalsmallsigma, - //ffi::XK_Greek_tau => VirtualKeyCode::Greek_tau, - //ffi::XK_Greek_upsilon => VirtualKeyCode::Greek_upsilon, - //ffi::XK_Greek_phi => VirtualKeyCode::Greek_phi, - //ffi::XK_Greek_chi => VirtualKeyCode::Greek_chi, - //ffi::XK_Greek_psi => VirtualKeyCode::Greek_psi, - //ffi::XK_Greek_omega => VirtualKeyCode::Greek_omega, - //ffi::XK_Greek_switch => VirtualKeyCode::Greek_switch, - //ffi::XK_leftradical => VirtualKeyCode::Leftradical, - //ffi::XK_topleftradical => VirtualKeyCode::Topleftradical, - //ffi::XK_horizconnector => VirtualKeyCode::Horizconnector, - //ffi::XK_topintegral => VirtualKeyCode::Topintegral, - //ffi::XK_botintegral => VirtualKeyCode::Botintegral, - //ffi::XK_vertconnector => VirtualKeyCode::Vertconnector, - //ffi::XK_topleftsqbracket => VirtualKeyCode::Topleftsqbracket, - //ffi::XK_botleftsqbracket => VirtualKeyCode::Botleftsqbracket, - //ffi::XK_toprightsqbracket => VirtualKeyCode::Toprightsqbracket, - //ffi::XK_botrightsqbracket => VirtualKeyCode::Botrightsqbracket, - //ffi::XK_topleftparens => VirtualKeyCode::Topleftparens, - //ffi::XK_botleftparens => VirtualKeyCode::Botleftparens, - //ffi::XK_toprightparens => VirtualKeyCode::Toprightparens, - //ffi::XK_botrightparens => VirtualKeyCode::Botrightparens, - //ffi::XK_leftmiddlecurlybrace => VirtualKeyCode::Leftmiddlecurlybrace, - //ffi::XK_rightmiddlecurlybrace => VirtualKeyCode::Rightmiddlecurlybrace, - //ffi::XK_topleftsummation => VirtualKeyCode::Topleftsummation, - //ffi::XK_botleftsummation => VirtualKeyCode::Botleftsummation, - //ffi::XK_topvertsummationconnector => VirtualKeyCode::Topvertsummationconnector, - //ffi::XK_botvertsummationconnector => VirtualKeyCode::Botvertsummationconnector, - //ffi::XK_toprightsummation => VirtualKeyCode::Toprightsummation, - //ffi::XK_botrightsummation => VirtualKeyCode::Botrightsummation, - //ffi::XK_rightmiddlesummation => VirtualKeyCode::Rightmiddlesummation, - //ffi::XK_lessthanequal => VirtualKeyCode::Lessthanequal, - //ffi::XK_notequal => VirtualKeyCode::Notequal, - //ffi::XK_greaterthanequal => VirtualKeyCode::Greaterthanequal, - //ffi::XK_integral => VirtualKeyCode::Integral, - //ffi::XK_therefore => VirtualKeyCode::Therefore, - //ffi::XK_variation => VirtualKeyCode::Variation, - //ffi::XK_infinity => VirtualKeyCode::Infinity, - //ffi::XK_nabla => VirtualKeyCode::Nabla, - //ffi::XK_approximate => VirtualKeyCode::Approximate, - //ffi::XK_similarequal => VirtualKeyCode::Similarequal, - //ffi::XK_ifonlyif => VirtualKeyCode::Ifonlyif, - //ffi::XK_implies => VirtualKeyCode::Implies, - //ffi::XK_identical => VirtualKeyCode::Identical, - //ffi::XK_radical => VirtualKeyCode::Radical, - //ffi::XK_includedin => VirtualKeyCode::Includedin, - //ffi::XK_includes => VirtualKeyCode::Includes, - //ffi::XK_intersection => VirtualKeyCode::Intersection, - //ffi::XK_union => VirtualKeyCode::Union, - //ffi::XK_logicaland => VirtualKeyCode::Logicaland, - //ffi::XK_logicalor => VirtualKeyCode::Logicalor, - //ffi::XK_partialderivative => VirtualKeyCode::Partialderivative, - //ffi::XK_function => VirtualKeyCode::Function, - //ffi::XK_leftarrow => VirtualKeyCode::Leftarrow, - //ffi::XK_uparrow => VirtualKeyCode::Uparrow, - //ffi::XK_rightarrow => VirtualKeyCode::Rightarrow, - //ffi::XK_downarrow => VirtualKeyCode::Downarrow, - //ffi::XK_blank => VirtualKeyCode::Blank, - //ffi::XK_soliddiamond => VirtualKeyCode::Soliddiamond, - //ffi::XK_checkerboard => VirtualKeyCode::Checkerboard, - //ffi::XK_ht => VirtualKeyCode::Ht, - //ffi::XK_ff => VirtualKeyCode::Ff, - //ffi::XK_cr => VirtualKeyCode::Cr, - //ffi::XK_lf => VirtualKeyCode::Lf, - //ffi::XK_nl => VirtualKeyCode::Nl, - //ffi::XK_vt => VirtualKeyCode::Vt, - //ffi::XK_lowrightcorner => VirtualKeyCode::Lowrightcorner, - //ffi::XK_uprightcorner => VirtualKeyCode::Uprightcorner, - //ffi::XK_upleftcorner => VirtualKeyCode::Upleftcorner, - //ffi::XK_lowleftcorner => VirtualKeyCode::Lowleftcorner, - //ffi::XK_crossinglines => VirtualKeyCode::Crossinglines, - //ffi::XK_horizlinescan1 => VirtualKeyCode::Horizlinescan1, - //ffi::XK_horizlinescan3 => VirtualKeyCode::Horizlinescan3, - //ffi::XK_horizlinescan5 => VirtualKeyCode::Horizlinescan5, - //ffi::XK_horizlinescan7 => VirtualKeyCode::Horizlinescan7, - //ffi::XK_horizlinescan9 => VirtualKeyCode::Horizlinescan9, - //ffi::XK_leftt => VirtualKeyCode::Leftt, - //ffi::XK_rightt => VirtualKeyCode::Rightt, - //ffi::XK_bott => VirtualKeyCode::Bott, - //ffi::XK_topt => VirtualKeyCode::Topt, - //ffi::XK_vertbar => VirtualKeyCode::Vertbar, - //ffi::XK_emspace => VirtualKeyCode::Emspace, - //ffi::XK_enspace => VirtualKeyCode::Enspace, - //ffi::XK_em3space => VirtualKeyCode::Em3space, - //ffi::XK_em4space => VirtualKeyCode::Em4space, - //ffi::XK_digitspace => VirtualKeyCode::Digitspace, - //ffi::XK_punctspace => VirtualKeyCode::Punctspace, - //ffi::XK_thinspace => VirtualKeyCode::Thinspace, - //ffi::XK_hairspace => VirtualKeyCode::Hairspace, - //ffi::XK_emdash => VirtualKeyCode::Emdash, - //ffi::XK_endash => VirtualKeyCode::Endash, - //ffi::XK_signifblank => VirtualKeyCode::Signifblank, - //ffi::XK_ellipsis => VirtualKeyCode::Ellipsis, - //ffi::XK_doubbaselinedot => VirtualKeyCode::Doubbaselinedot, - //ffi::XK_onethird => VirtualKeyCode::Onethird, - //ffi::XK_twothirds => VirtualKeyCode::Twothirds, - //ffi::XK_onefifth => VirtualKeyCode::Onefifth, - //ffi::XK_twofifths => VirtualKeyCode::Twofifths, - //ffi::XK_threefifths => VirtualKeyCode::Threefifths, - //ffi::XK_fourfifths => VirtualKeyCode::Fourfifths, - //ffi::XK_onesixth => VirtualKeyCode::Onesixth, - //ffi::XK_fivesixths => VirtualKeyCode::Fivesixths, - //ffi::XK_careof => VirtualKeyCode::Careof, - //ffi::XK_figdash => VirtualKeyCode::Figdash, - //ffi::XK_leftanglebracket => VirtualKeyCode::Leftanglebracket, - //ffi::XK_decimalpoint => VirtualKeyCode::Decimalpoint, - //ffi::XK_rightanglebracket => VirtualKeyCode::Rightanglebracket, - //ffi::XK_marker => VirtualKeyCode::Marker, - //ffi::XK_oneeighth => VirtualKeyCode::Oneeighth, - //ffi::XK_threeeighths => VirtualKeyCode::Threeeighths, - //ffi::XK_fiveeighths => VirtualKeyCode::Fiveeighths, - //ffi::XK_seveneighths => VirtualKeyCode::Seveneighths, - //ffi::XK_trademark => VirtualKeyCode::Trademark, - //ffi::XK_signaturemark => VirtualKeyCode::Signaturemark, - //ffi::XK_trademarkincircle => VirtualKeyCode::Trademarkincircle, - //ffi::XK_leftopentriangle => VirtualKeyCode::Leftopentriangle, - //ffi::XK_rightopentriangle => VirtualKeyCode::Rightopentriangle, - //ffi::XK_emopencircle => VirtualKeyCode::Emopencircle, - //ffi::XK_emopenrectangle => VirtualKeyCode::Emopenrectangle, - //ffi::XK_leftsinglequotemark => VirtualKeyCode::Leftsinglequotemark, - //ffi::XK_rightsinglequotemark => VirtualKeyCode::Rightsinglequotemark, - //ffi::XK_leftdoublequotemark => VirtualKeyCode::Leftdoublequotemark, - //ffi::XK_rightdoublequotemark => VirtualKeyCode::Rightdoublequotemark, - //ffi::XK_prescription => VirtualKeyCode::Prescription, - //ffi::XK_minutes => VirtualKeyCode::Minutes, - //ffi::XK_seconds => VirtualKeyCode::Seconds, - //ffi::XK_latincross => VirtualKeyCode::Latincross, - //ffi::XK_hexagram => VirtualKeyCode::Hexagram, - //ffi::XK_filledrectbullet => VirtualKeyCode::Filledrectbullet, - //ffi::XK_filledlefttribullet => VirtualKeyCode::Filledlefttribullet, - //ffi::XK_filledrighttribullet => VirtualKeyCode::Filledrighttribullet, - //ffi::XK_emfilledcircle => VirtualKeyCode::Emfilledcircle, - //ffi::XK_emfilledrect => VirtualKeyCode::Emfilledrect, - //ffi::XK_enopencircbullet => VirtualKeyCode::Enopencircbullet, - //ffi::XK_enopensquarebullet => VirtualKeyCode::Enopensquarebullet, - //ffi::XK_openrectbullet => VirtualKeyCode::Openrectbullet, - //ffi::XK_opentribulletup => VirtualKeyCode::Opentribulletup, - //ffi::XK_opentribulletdown => VirtualKeyCode::Opentribulletdown, - //ffi::XK_openstar => VirtualKeyCode::Openstar, - //ffi::XK_enfilledcircbullet => VirtualKeyCode::Enfilledcircbullet, - //ffi::XK_enfilledsqbullet => VirtualKeyCode::Enfilledsqbullet, - //ffi::XK_filledtribulletup => VirtualKeyCode::Filledtribulletup, - //ffi::XK_filledtribulletdown => VirtualKeyCode::Filledtribulletdown, - //ffi::XK_leftpointer => VirtualKeyCode::Leftpointer, - //ffi::XK_rightpointer => VirtualKeyCode::Rightpointer, - //ffi::XK_club => VirtualKeyCode::Club, - //ffi::XK_diamond => VirtualKeyCode::Diamond, - //ffi::XK_heart => VirtualKeyCode::Heart, - //ffi::XK_maltesecross => VirtualKeyCode::Maltesecross, - //ffi::XK_dagger => VirtualKeyCode::Dagger, - //ffi::XK_doubledagger => VirtualKeyCode::Doubledagger, - //ffi::XK_checkmark => VirtualKeyCode::Checkmark, - //ffi::XK_ballotcross => VirtualKeyCode::Ballotcross, - //ffi::XK_musicalsharp => VirtualKeyCode::Musicalsharp, - //ffi::XK_musicalflat => VirtualKeyCode::Musicalflat, - //ffi::XK_malesymbol => VirtualKeyCode::Malesymbol, - //ffi::XK_femalesymbol => VirtualKeyCode::Femalesymbol, - //ffi::XK_telephone => VirtualKeyCode::Telephone, - //ffi::XK_telephonerecorder => VirtualKeyCode::Telephonerecorder, - //ffi::XK_phonographcopyright => VirtualKeyCode::Phonographcopyright, - //ffi::XK_caret => VirtualKeyCode::Caret, - //ffi::XK_singlelowquotemark => VirtualKeyCode::Singlelowquotemark, - //ffi::XK_doublelowquotemark => VirtualKeyCode::Doublelowquotemark, - //ffi::XK_cursor => VirtualKeyCode::Cursor, - //ffi::XK_leftcaret => VirtualKeyCode::Leftcaret, - //ffi::XK_rightcaret => VirtualKeyCode::Rightcaret, - //ffi::XK_downcaret => VirtualKeyCode::Downcaret, - //ffi::XK_upcaret => VirtualKeyCode::Upcaret, - //ffi::XK_overbar => VirtualKeyCode::Overbar, - //ffi::XK_downtack => VirtualKeyCode::Downtack, - //ffi::XK_upshoe => VirtualKeyCode::Upshoe, - //ffi::XK_downstile => VirtualKeyCode::Downstile, - //ffi::XK_underbar => VirtualKeyCode::Underbar, - //ffi::XK_jot => VirtualKeyCode::Jot, - //ffi::XK_quad => VirtualKeyCode::Quad, - //ffi::XK_uptack => VirtualKeyCode::Uptack, - //ffi::XK_circle => VirtualKeyCode::Circle, - //ffi::XK_upstile => VirtualKeyCode::Upstile, - //ffi::XK_downshoe => VirtualKeyCode::Downshoe, - //ffi::XK_rightshoe => VirtualKeyCode::Rightshoe, - //ffi::XK_leftshoe => VirtualKeyCode::Leftshoe, - //ffi::XK_lefttack => VirtualKeyCode::Lefttack, - //ffi::XK_righttack => VirtualKeyCode::Righttack, - //ffi::XK_hebrew_doublelowline => VirtualKeyCode::Hebrew_doublelowline, - //ffi::XK_hebrew_aleph => VirtualKeyCode::Hebrew_aleph, - //ffi::XK_hebrew_bet => VirtualKeyCode::Hebrew_bet, - //ffi::XK_hebrew_beth => VirtualKeyCode::Hebrew_beth, - //ffi::XK_hebrew_gimel => VirtualKeyCode::Hebrew_gimel, - //ffi::XK_hebrew_gimmel => VirtualKeyCode::Hebrew_gimmel, - //ffi::XK_hebrew_dalet => VirtualKeyCode::Hebrew_dalet, - //ffi::XK_hebrew_daleth => VirtualKeyCode::Hebrew_daleth, - //ffi::XK_hebrew_he => VirtualKeyCode::Hebrew_he, - //ffi::XK_hebrew_waw => VirtualKeyCode::Hebrew_waw, - //ffi::XK_hebrew_zain => VirtualKeyCode::Hebrew_zain, - //ffi::XK_hebrew_zayin => VirtualKeyCode::Hebrew_zayin, - //ffi::XK_hebrew_chet => VirtualKeyCode::Hebrew_chet, - //ffi::XK_hebrew_het => VirtualKeyCode::Hebrew_het, - //ffi::XK_hebrew_tet => VirtualKeyCode::Hebrew_tet, - //ffi::XK_hebrew_teth => VirtualKeyCode::Hebrew_teth, - //ffi::XK_hebrew_yod => VirtualKeyCode::Hebrew_yod, - //ffi::XK_hebrew_finalkaph => VirtualKeyCode::Hebrew_finalkaph, - //ffi::XK_hebrew_kaph => VirtualKeyCode::Hebrew_kaph, - //ffi::XK_hebrew_lamed => VirtualKeyCode::Hebrew_lamed, - //ffi::XK_hebrew_finalmem => VirtualKeyCode::Hebrew_finalmem, - //ffi::XK_hebrew_mem => VirtualKeyCode::Hebrew_mem, - //ffi::XK_hebrew_finalnun => VirtualKeyCode::Hebrew_finalnun, - //ffi::XK_hebrew_nun => VirtualKeyCode::Hebrew_nun, - //ffi::XK_hebrew_samech => VirtualKeyCode::Hebrew_samech, - //ffi::XK_hebrew_samekh => VirtualKeyCode::Hebrew_samekh, - //ffi::XK_hebrew_ayin => VirtualKeyCode::Hebrew_ayin, - //ffi::XK_hebrew_finalpe => VirtualKeyCode::Hebrew_finalpe, - //ffi::XK_hebrew_pe => VirtualKeyCode::Hebrew_pe, - //ffi::XK_hebrew_finalzade => VirtualKeyCode::Hebrew_finalzade, - //ffi::XK_hebrew_finalzadi => VirtualKeyCode::Hebrew_finalzadi, - //ffi::XK_hebrew_zade => VirtualKeyCode::Hebrew_zade, - //ffi::XK_hebrew_zadi => VirtualKeyCode::Hebrew_zadi, - //ffi::XK_hebrew_qoph => VirtualKeyCode::Hebrew_qoph, - //ffi::XK_hebrew_kuf => VirtualKeyCode::Hebrew_kuf, - //ffi::XK_hebrew_resh => VirtualKeyCode::Hebrew_resh, - //ffi::XK_hebrew_shin => VirtualKeyCode::Hebrew_shin, - //ffi::XK_hebrew_taw => VirtualKeyCode::Hebrew_taw, - //ffi::XK_hebrew_taf => VirtualKeyCode::Hebrew_taf, - //ffi::XK_Hebrew_switch => VirtualKeyCode::Hebrew_switch, - ffi::XF86XK_Back => VirtualKeyCode::NavigateBackward, - ffi::XF86XK_Forward => VirtualKeyCode::NavigateForward, - ffi::XF86XK_Copy => VirtualKeyCode::Copy, - ffi::XF86XK_Paste => VirtualKeyCode::Paste, - ffi::XF86XK_Cut => VirtualKeyCode::Cut, - _ => return None, - }) -} diff --git a/src/platform_impl/linux/x11/ffi.rs b/src/platform_impl/linux/x11/ffi.rs index 8027e56978..f44f9b5a4e 100644 --- a/src/platform_impl/linux/x11/ffi.rs +++ b/src/platform_impl/linux/x11/ffi.rs @@ -1,9 +1 @@ -use x11_dl::xmd::CARD32; -pub use x11_dl::{ - error::OpenError, keysym::*, xcursor::*, xinput::*, xinput2::*, xlib::*, xlib_xcb::*, - xrandr::*, xrender::*, -}; - -// Isn't defined by x11_dl -#[allow(non_upper_case_globals)] -pub const IconicState: CARD32 = 3; +pub use x11_dl::{error::OpenError, xcursor::*, xinput2::*, xlib::*, xlib_xcb::*}; diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs index fd96afbec7..02e26410a6 100644 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ b/src/platform_impl/linux/x11/ime/callbacks.rs @@ -16,7 +16,7 @@ pub(crate) unsafe fn xim_set_callback( ) -> Result<(), XError> { // It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize // access that isn't type-checked. - (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()); + unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) }; xconn.check_errors() } @@ -30,14 +30,16 @@ pub(crate) unsafe fn set_instantiate_callback( xconn: &Arc, client_data: ffi::XPointer, ) -> Result<(), XError> { - (xconn.xlib.XRegisterIMInstantiateCallback)( - xconn.display, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - Some(xim_instantiate_callback), - client_data, - ); + unsafe { + (xconn.xlib.XRegisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; xconn.check_errors() } @@ -45,14 +47,16 @@ pub(crate) unsafe fn unset_instantiate_callback( xconn: &Arc, client_data: ffi::XPointer, ) -> Result<(), XError> { - (xconn.xlib.XUnregisterIMInstantiateCallback)( - xconn.display, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - Some(xim_instantiate_callback), - client_data, - ); + unsafe { + (xconn.xlib.XUnregisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; xconn.check_errors() } @@ -61,35 +65,39 @@ pub(crate) unsafe fn set_destroy_callback( im: ffi::XIM, inner: &ImeInner, ) -> Result<(), XError> { - xim_set_callback( - xconn, - im, - ffi::XNDestroyCallback_0.as_ptr() as *const _, - &inner.destroy_callback as *const _ as *mut _, - ) + unsafe { + xim_set_callback( + xconn, + im, + ffi::XNDestroyCallback_0.as_ptr() as *const _, + &inner.destroy_callback as *const _ as *mut _, + ) + } } #[derive(Debug)] #[allow(clippy::enum_variant_names)] enum ReplaceImError { // Boxed to prevent large error type - MethodOpenFailed(Box), - ContextCreationFailed(ImeContextCreationError), - SetDestroyCallbackFailed(XError), + MethodOpenFailed(#[allow(dead_code)] Box), + ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), } // Attempt to replace current IM (which may or may not be presently valid) with a new one. This // includes replacing all existing input contexts and free'ing resources as necessary. This only // modifies existing state if all operations succeed. unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { - let xconn = &(*inner).xconn; + let xconn = unsafe { &(*inner).xconn }; let (new_im, is_fallback) = { - let new_im = (*inner).potential_input_methods.open_im(xconn, None); + let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) }; let is_fallback = new_im.is_fallback(); ( new_im.ok().ok_or_else(|| { - ReplaceImError::MethodOpenFailed(Box::new((*inner).potential_input_methods.clone())) + ReplaceImError::MethodOpenFailed(Box::new(unsafe { + (*inner).potential_input_methods.clone() + })) })?, is_fallback, ) @@ -98,16 +106,16 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { // It's important to always set a destroy callback, since there's otherwise potential for us // to try to use or free a resource that's already been destroyed on the server. { - let result = set_destroy_callback(xconn, new_im.im, &*inner); + let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) }; if result.is_err() { - let _ = close_im(xconn, new_im.im); + let _ = unsafe { close_im(xconn, new_im.im) }; } result } .map_err(ReplaceImError::SetDestroyCallbackFailed)?; let mut new_contexts = HashMap::new(); - for (window, old_context) in (*inner).contexts.iter() { + for (window, old_context) in unsafe { (*inner).contexts.iter() } { let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); // Check if the IME was allowed on that context. @@ -125,16 +133,18 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { }; let new_context = { - let result = ImeContext::new( - xconn, - new_im.im, - style, - *window, - spot, - (*inner).event_sender.clone(), - ); + let result = unsafe { + ImeContext::new( + xconn, + new_im.im, + style, + *window, + spot, + (*inner).event_sender.clone(), + ) + }; if result.is_err() { - let _ = close_im(xconn, new_im.im); + let _ = unsafe { close_im(xconn, new_im.im) }; } result.map_err(ReplaceImError::ContextCreationFailed)? }; @@ -142,12 +152,14 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { } // If we've made it this far, everything succeeded. - let _ = (*inner).destroy_all_contexts_if_necessary(); - let _ = (*inner).close_im_if_necessary(); - (*inner).im = Some(new_im); - (*inner).contexts = new_contexts; - (*inner).is_destroyed = false; - (*inner).is_fallback = is_fallback; + unsafe { + let _ = (*inner).destroy_all_contexts_if_necessary(); + let _ = (*inner).close_im_if_necessary(); + (*inner).im = Some(new_im); + (*inner).contexts = new_contexts; + (*inner).is_destroyed = false; + (*inner).is_fallback = is_fallback; + } Ok(()) } @@ -159,18 +171,18 @@ pub unsafe extern "C" fn xim_instantiate_callback( ) { let inner: *mut ImeInner = client_data as _; if !inner.is_null() { - let xconn = &(*inner).xconn; - match replace_im(inner) { - Ok(()) => { + let xconn = unsafe { &(*inner).xconn }; + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { let _ = unset_instantiate_callback(xconn, client_data); (*inner).is_fallback = false; - } - Err(err) => { + }, + Err(err) => unsafe { if (*inner).is_destroyed { // We have no usable input methods! panic!("Failed to reopen input method: {err:?}"); } - } + }, } } } @@ -186,13 +198,13 @@ pub unsafe extern "C" fn xim_destroy_callback( ) { let inner: *mut ImeInner = client_data as _; if !inner.is_null() { - (*inner).is_destroyed = true; - let xconn = &(*inner).xconn; - if !(*inner).is_fallback { - let _ = set_instantiate_callback(xconn, client_data); + unsafe { (*inner).is_destroyed = true }; + let xconn = unsafe { &(*inner).xconn }; + if unsafe { !(*inner).is_fallback } { + let _ = unsafe { set_instantiate_callback(xconn, client_data) }; // Attempt to open fallback input method. - match replace_im(inner) { - Ok(()) => (*inner).is_fallback = true, + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { (*inner).is_fallback = true }, Err(err) => { // We have no usable input methods! panic!("Failed to open fallback input method: {err:?}"); diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs index ae3ff127f2..dab2c6bd3d 100644 --- a/src/platform_impl/linux/x11/ime/context.rs +++ b/src/platform_impl/linux/x11/ime/context.rs @@ -217,15 +217,19 @@ impl ImeContext { })); let ic = match style as _ { - Style::Preedit(style) => ImeContext::create_preedit_ic( - xconn, - im, - style, - window, - client_data as ffi::XPointer, - ), - Style::Nothing(style) => ImeContext::create_nothing_ic(xconn, im, style, window), - Style::None(style) => ImeContext::create_none_ic(xconn, im, style, window), + Style::Preedit(style) => unsafe { + ImeContext::create_preedit_ic( + xconn, + im, + style, + window, + client_data as ffi::XPointer, + ) + }, + Style::Nothing(style) => unsafe { + ImeContext::create_nothing_ic(xconn, im, style, window) + }, + Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) }, } .ok_or(ImeContextCreationError::Null)?; @@ -237,7 +241,7 @@ impl ImeContext { ic, ic_spot: ffi::XPoint { x: 0, y: 0 }, style, - _client_data: Box::from_raw(client_data), + _client_data: unsafe { Box::from_raw(client_data) }, }; // Set the spot location, if it's present. @@ -254,14 +258,16 @@ impl ImeContext { style: XIMStyle, window: ffi::Window, ) -> Option { - let ic = (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ptr::null_mut::<()>(), - ); + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; (!ic.is_null()).then_some(ic) } @@ -274,8 +280,7 @@ impl ImeContext { client_data: ffi::XPointer, ) -> Option { let preedit_callbacks = PreeditCallbacks::new(client_data); - let preedit_attr = util::XSmartPointer::new( - xconn, + let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe { (xconn.xlib.XVaCreateNestedList)( 0, ffi::XNPreeditStartCallback_0.as_ptr() as *const _, @@ -287,20 +292,22 @@ impl ImeContext { ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, &(preedit_callbacks.draw_callback) as *const _, ptr::null_mut::<()>(), - ), - ) + ) + }) .expect("XVaCreateNestedList returned NULL"); - let ic = (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ffi::XNPreeditAttributes_0.as_ptr() as *const _, - preedit_attr.ptr, - ptr::null_mut::<()>(), - ); + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ) + }; (!ic.is_null()).then_some(ic) } @@ -311,14 +318,16 @@ impl ImeContext { style: XIMStyle, window: ffi::Window, ) -> Option { - let ic = (xconn.xlib.XCreateIC)( - im, - ffi::XNInputStyle_0.as_ptr() as *const _, - style, - ffi::XNClientWindow_0.as_ptr() as *const _, - window, - ptr::null_mut::<()>(), - ); + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; (!ic.is_null()).then_some(ic) } @@ -354,7 +363,7 @@ impl ImeContext { self.ic_spot = ffi::XPoint { x, y }; unsafe { - let preedit_attr = util::XSmartPointer::new( + let preedit_attr = util::memory::XSmartPointer::new( xconn, (xconn.xlib.XVaCreateNestedList)( 0, diff --git a/src/platform_impl/linux/x11/ime/inner.rs b/src/platform_impl/linux/x11/ime/inner.rs index e6eca9d048..ddff12fb37 100644 --- a/src/platform_impl/linux/x11/ime/inner.rs +++ b/src/platform_impl/linux/x11/ime/inner.rs @@ -9,12 +9,12 @@ use super::{ use crate::platform_impl::platform::x11::ime::ImeEventSender; pub(crate) unsafe fn close_im(xconn: &Arc, im: ffi::XIM) -> Result<(), XError> { - (xconn.xlib.XCloseIM)(im); + unsafe { (xconn.xlib.XCloseIM)(im) }; xconn.check_errors() } pub(crate) unsafe fn destroy_ic(xconn: &Arc, ic: ffi::XIC) -> Result<(), XError> { - (xconn.xlib.XDestroyIC)(ic); + unsafe { (xconn.xlib.XDestroyIC)(ic) }; xconn.check_errors() } @@ -52,7 +52,7 @@ impl ImeInner { pub unsafe fn close_im_if_necessary(&self) -> Result { if !self.is_destroyed && self.im.is_some() { - close_im(&self.xconn, self.im.as_ref().unwrap().im).map(|_| true) + unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true) } else { Ok(false) } @@ -60,7 +60,7 @@ impl ImeInner { pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result { if !self.is_destroyed { - destroy_ic(&self.xconn, ic).map(|_| true) + unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true) } else { Ok(false) } @@ -68,7 +68,7 @@ impl ImeInner { pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result { for context in self.contexts.values().flatten() { - self.destroy_ic_if_necessary(context.ic)?; + unsafe { self.destroy_ic_if_necessary(context.ic)? }; } Ok(!self.is_destroyed) } diff --git a/src/platform_impl/linux/x11/ime/input_method.rs b/src/platform_impl/linux/x11/ime/input_method.rs index 0231d669eb..faad41effb 100644 --- a/src/platform_impl/linux/x11/ime/input_method.rs +++ b/src/platform_impl/linux/x11/ime/input_method.rs @@ -7,9 +7,9 @@ use std::{ sync::{Arc, Mutex}, }; +use super::{super::atoms::*, ffi, util, XConnection, XError}; use once_cell::sync::Lazy; - -use super::{ffi, util, XConnection, XError}; +use x11rb::protocol::xproto; static GLOBAL_LOCK: Lazy> = Lazy::new(Default::default); @@ -21,14 +21,16 @@ unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option for GetXimServersError { + fn from(error: util::GetPropertyError) -> Self { + GetXimServersError::GetPropertyError(error) + } } // The root window has a property named XIM_SERVERS, which contains a list of atoms represeting @@ -169,30 +177,40 @@ enum GetXimServersError { // modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set // XMODIFIERS to `@server=ibus`?!?" unsafe fn get_xim_servers(xconn: &Arc) -> Result, GetXimServersError> { - let servers_atom = xconn.get_atom_unchecked(b"XIM_SERVERS\0"); + let atoms = xconn.atoms(); + let servers_atom = atoms[XIM_SERVERS]; - let root = (xconn.xlib.XDefaultRootWindow)(xconn.display); + let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; let mut atoms: Vec = xconn - .get_property(root, servers_atom, ffi::XA_ATOM) - .map_err(GetXimServersError::GetPropertyError)?; + .get_property::( + root as xproto::Window, + servers_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .map_err(GetXimServersError::GetPropertyError)? + .into_iter() + .map(ffi::Atom::from) + .collect::>(); let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); - (xconn.xlib.XGetAtomNames)( - xconn.display, - atoms.as_mut_ptr(), - atoms.len() as _, - names.as_mut_ptr() as _, - ); - names.set_len(atoms.len()); + unsafe { + (xconn.xlib.XGetAtomNames)( + xconn.display, + atoms.as_mut_ptr(), + atoms.len() as _, + names.as_mut_ptr() as _, + ) + }; + unsafe { names.set_len(atoms.len()) }; let mut formatted_names = Vec::with_capacity(names.len()); for name in names { - let string = CStr::from_ptr(name) + let string = unsafe { CStr::from_ptr(name) } .to_owned() .into_string() .map_err(GetXimServersError::InvalidUtf8)?; - (xconn.xlib.XFree)(name as _); + unsafe { (xconn.xlib.XFree)(name as _) }; formatted_names.push(string.replace("@server=", "@im=")); } xconn.check_errors().map_err(GetXimServersError::XError)?; diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index e0979681d7..b23bb82e95 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -48,7 +48,7 @@ pub enum ImeRequest { pub(crate) enum ImeCreationError { // Boxed to prevent large error type OpenFailure(Box), - SetDestroyCallbackFailed(XError), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), } pub(crate) struct Ime { diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 4400b36be6..965aa2f70b 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -1,5 +1,51 @@ #![cfg(x11_platform)] +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::CStr; +use std::fmt; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::os::raw::*; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; +use std::{ptr, slice, str}; + +pub use self::xdisplay::{XError, XNotSupported}; + +use calloop::generic::Generic; +use calloop::EventLoop as Loop; +use calloop::{ping::Ping, Readiness}; +use libc::{setlocale, LC_CTYPE}; +use log::warn; + +use x11rb::connection::RequestConnection; +use x11rb::errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; +use x11rb::x11_utils::X11Error as LogicalError; +use x11rb::xcb_ffi::ReplyOrIdError; + +use super::{ControlFlow, OsError}; +use crate::{ + error::{EventLoopError, OsError as RootOsError}, + event::{Event, StartCause, WindowEvent}, + event_loop::{DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, + platform::pump_events::PumpStatus, + platform_impl::common::xkb::Context, + platform_impl::{ + platform::{min_timeout, WindowId}, + PlatformSpecificWindowBuilderAttributes, + }, + window::WindowAttributes, +}; + +mod activation; +mod atoms; mod dnd; mod event_processor; pub mod ffi; @@ -8,92 +54,123 @@ mod monitor; pub mod util; mod window; mod xdisplay; +mod xsettings; -pub(crate) use self::{ - monitor::{MonitorHandle, VideoMode}, - window::UnownedWindow, - xdisplay::XConnection, -}; +use atoms::*; +use dnd::{Dnd, DndState}; +use event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; +use ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}; +pub(crate) use monitor::{MonitorHandle, VideoMode}; +use window::UnownedWindow; +pub(crate) use xdisplay::XConnection; -pub use self::xdisplay::{XError, XNotSupported}; +// Xinput constants not defined in x11rb +const ALL_DEVICES: u16 = 0; +const ALL_MASTER_DEVICES: u16 = 1; +const ICONIC_STATE: u32 = 3; -use calloop::channel::{channel, Channel, Event as ChanResult, Sender}; -use calloop::generic::Generic; -use calloop::{Dispatcher, EventLoop as Loop}; - -use std::{ - cell::{Cell, RefCell}, - collections::{HashMap, HashSet, VecDeque}, - ffi::CStr, - mem::{self, MaybeUninit}, - ops::Deref, - os::raw::*, - os::unix::io::RawFd, - ptr, - rc::Rc, - slice, - sync::{mpsc, Arc, Weak}, - time::{Duration, Instant}, -}; +/// The underlying x11rb connection that we are using. +type X11rbConnection = x11rb::xcb_ffi::XCBConnection; -use libc::{self, setlocale, LC_CTYPE}; -use raw_window_handle::{RawDisplayHandle, XlibDisplayHandle}; +type X11Source = Generic>; -use self::{ - dnd::{Dnd, DndState}, - event_processor::EventProcessor, - ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}, -}; -use super::common::xkb_state::KbdState; -use crate::{ - error::OsError as RootOsError, - event::{Event, StartCause}, - event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, - platform_impl::{ - platform::{sticky_exit_callback, WindowId}, - PlatformSpecificWindowBuilderAttributes, - }, - window::WindowAttributes, -}; +struct WakeSender { + sender: Sender, + waker: Ping, +} + +impl Clone for WakeSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + waker: self.waker.clone(), + } + } +} + +impl WakeSender { + pub fn send(&self, t: T) -> Result<(), EventLoopClosed> { + let res = self.sender.send(t).map_err(|e| EventLoopClosed(e.0)); + if res.is_ok() { + self.waker.ping(); + } + res + } +} + +struct PeekableReceiver { + recv: Receiver, + first: Option, +} -type X11Source = Generic; +impl PeekableReceiver { + pub fn from_recv(recv: Receiver) -> Self { + Self { recv, first: None } + } + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + } + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + } + } + } + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} pub struct EventLoopWindowTarget { xconn: Arc, - wm_delete_window: ffi::Atom, - net_wm_ping: ffi::Atom, + wm_delete_window: xproto::Atom, + net_wm_ping: xproto::Atom, ime_sender: ImeSender, - root: ffi::Window, - ime: RefCell, + control_flow: Cell, + exit: Cell>, + root: xproto::Window, + ime: Option>, windows: RefCell>>, - redraw_sender: Sender, + redraw_sender: WakeSender, + activation_sender: WakeSender, device_events: Cell, _marker: ::std::marker::PhantomData, } pub struct EventLoop { - event_loop: Loop<'static, EventLoopState>, + loop_running: bool, + event_loop: Loop<'static, EventLoopState>, + waker: calloop::ping::Ping, event_processor: EventProcessor, + redraw_receiver: PeekableReceiver, + user_receiver: PeekableReceiver, + activation_receiver: PeekableReceiver, user_sender: Sender, - target: Rc>, /// The current state of the event loop. - state: EventLoopState, - - /// Dispatcher for redraw events. - redraw_dispatcher: Dispatcher<'static, Channel, EventLoopState>, + state: EventLoopState, } -struct EventLoopState { - /// Incoming user events. - user_events: VecDeque, +type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial); - /// Incoming redraw events. - redraw_events: VecDeque, +struct EventLoopState { + /// The latest readiness state for the x11 file descriptor + x11_readiness: Readiness, } pub struct EventLoopProxy { - user_sender: Sender, + user_sender: WakeSender, } impl Clone for EventLoopProxy { @@ -106,11 +183,11 @@ impl Clone for EventLoopProxy { impl EventLoop { pub(crate) fn new(xconn: Arc) -> EventLoop { - let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; - - let wm_delete_window = unsafe { xconn.get_atom_unchecked(b"WM_DELETE_WINDOW\0") }; + let root = xconn.default_root().root; + let atoms = xconn.atoms(); - let net_wm_ping = unsafe { xconn.get_atom_unchecked(b"_NET_WM_PING\0") }; + let wm_delete_window = atoms[WM_DELETE_WINDOW]; + let net_wm_ping = atoms[_NET_WM_PING]; let dnd = Dnd::new(Arc::clone(&xconn)) .expect("Failed to call XInternAtoms when initializing drag and drop"); @@ -140,439 +217,458 @@ impl EventLoop { setlocale(LC_CTYPE, default_locale); } } - let ime = RefCell::new({ - 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:#?}"); - } - result.expect("Failed to set input method destruction callback") - }); + + let ime = Ime::new(Arc::clone(&xconn), ime_event_sender); + if let Err(ImeCreationError::OpenFailure(state)) = ime.as_ref() { + warn!("Failed to open input method: {state:#?}"); + } else if let Err(err) = ime.as_ref() { + warn!("Failed to set input method destruction callback: {err:?}"); + } + + let ime = ime.ok().map(RefCell::new); let randr_event_offset = xconn .select_xrandr_input(root) .expect("Failed to query XRandR extension"); - let xi2ext = unsafe { - let mut ext = XExtension::default(); - - let res = (xconn.xlib.XQueryExtension)( - xconn.display, - b"XInputExtension\0".as_ptr() as *const c_char, - &mut ext.opcode, - &mut ext.first_event_id, - &mut ext.first_error_id, - ); - - if res == ffi::False { - panic!("X server missing XInput extension"); - } - - ext - }; - - let xkbext = { - let mut ext = XExtension::default(); - - let res = unsafe { - (xconn.xlib.XkbQueryExtension)( - xconn.display, - &mut ext.opcode, - &mut ext.first_event_id, - &mut ext.first_error_id, - &mut 1, - &mut 0, - ) - }; - - if res == ffi::False { - panic!("X server missing XKB extension"); - } - - // Enable detectable auto repeat. - let mut supported = 0; - unsafe { - (xconn.xlib.XkbSetDetectableAutoRepeat)(xconn.display, 1, &mut supported); - } - if supported == 0 { - warn!("Detectable auto repeart is not supported"); - } - - ext - }; - - unsafe { - let mut xinput_major_ver = ffi::XI_2_Major; - let mut xinput_minor_ver = ffi::XI_2_Minor; - if (xconn.xinput2.XIQueryVersion)( - xconn.display, - &mut xinput_major_ver, - &mut xinput_minor_ver, - ) != ffi::Success as libc::c_int - { - panic!( - "X server has XInput extension {xinput_major_ver}.{xinput_minor_ver} but does not support XInput2", - ); - } - } + let xi2ext = xconn + .xcb_connection() + .extension_information(xinput::X11_EXTENSION_NAME) + .expect("Failed to query XInput extension") + .expect("X server missing XInput extension"); + let xkbext = xconn + .xcb_connection() + .extension_information(xkb::X11_EXTENSION_NAME) + .expect("Failed to query XKB extension") + .expect("X server missing XKB extension"); + + // Check for XInput2 support. + xconn + .xcb_connection() + .xinput_xi_query_version(2, 3) + .expect("Failed to send XInput2 query version request") + .reply() + .expect("Error while checking for XInput2 query version reply"); xconn.update_cached_wm_info(root); // Create an event loop. let event_loop = - Loop::>::try_new().expect("Failed to initialize the event loop"); + Loop::::try_new().expect("Failed to initialize the event loop"); let handle = event_loop.handle(); // Create the X11 event dispatcher. - let source = X11Source::new(xconn.x11_fd, calloop::Interest::READ, calloop::Mode::Level); + let source = X11Source::new( + // SAFETY: xcb owns the FD and outlives the source. + unsafe { BorrowedFd::borrow_raw(xconn.xcb_connection().as_raw_fd()) }, + calloop::Interest::READ, + calloop::Mode::Level, + ); handle - .insert_source(source, |_, _, _| Ok(calloop::PostAction::Continue)) + .insert_source(source, |readiness, _, state| { + state.x11_readiness = readiness; + Ok(calloop::PostAction::Continue) + }) .expect("Failed to register the X11 event dispatcher"); - // Create a channel for sending user events. - let (user_sender, user_channel) = channel(); - handle - .insert_source(user_channel, |ev, _, state| { - if let ChanResult::Msg(user) = ev { - state.user_events.push_back(user); - } + let (waker, waker_source) = + calloop::ping::make_ping().expect("Failed to create event loop waker"); + event_loop + .handle() + .insert_source(waker_source, move |_, _, _| { + // No extra handling is required, we just need to wake-up. }) - .expect("Failed to register the user event channel with the event loop"); + .expect("Failed to register the event loop waker source"); // Create a channel for handling redraw requests. - let (redraw_sender, redraw_channel) = channel(); - - // Create a dispatcher for the redraw channel such that we can dispatch it independent of the - // event loop. - let redraw_dispatcher = - Dispatcher::<_, EventLoopState>::new(redraw_channel, |ev, _, state| { - if let ChanResult::Msg(window_id) = ev { - state.redraw_events.push_back(window_id); - } - }); - handle - .register_dispatcher(redraw_dispatcher.clone()) - .expect("Failed to register the redraw event channel with the event loop"); + let (redraw_sender, redraw_channel) = mpsc::channel(); + + // Create a channel for sending activation tokens. + let (activation_token_sender, activation_token_channel) = mpsc::channel(); + + // Create a channel for sending user events. + let (user_sender, user_channel) = mpsc::channel(); + + let xkb_context = + Context::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap(); - let kb_state = - KbdState::from_x11_xkb(unsafe { (xconn.xlib_xcb.XGetXCBConnection)(xconn.display) }) - .unwrap(); + let mut xmodmap = util::ModifierKeymap::new(); + xmodmap.reload_from_x_connection(&xconn); let window_target = EventLoopWindowTarget { ime, root, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), windows: Default::default(), _marker: ::std::marker::PhantomData, ime_sender, xconn, wm_delete_window, net_wm_ping, - redraw_sender, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + activation_sender: WakeSender { + sender: activation_token_sender, // not used again so no clone + waker: waker.clone(), + }, device_events: Default::default(), }; // Set initial device event filter. window_target.update_listen_device_events(true); - let target = Rc::new(RootELW { + let root_window_target = RootELW { p: super::EventLoopWindowTarget::X(window_target), - _marker: ::std::marker::PhantomData, - }); + _marker: PhantomData, + }; let event_processor = EventProcessor { - target: target.clone(), + target: root_window_target, dnd, devices: Default::default(), randr_event_offset, ime_receiver, ime_event_receiver, xi2ext, + xfiltered_modifiers: VecDeque::with_capacity(MAX_MOD_REPLAY_LEN), + xmodmap, xkbext, - kb_state, + xkb_context, num_touch: 0, + held_key_press: None, first_touch: None, active_window: None, + modifiers: Default::default(), is_composing: false, - got_key_release: true, }; // Register for device hotplug events // (The request buffer is flushed during `init_device`) - get_xtarget(&target) - .xconn - .select_xinput_events(root, ffi::XIAllDevices, ffi::XI_HierarchyChangedMask) - .queue(); + let xconn = &EventProcessor::window_target(&event_processor.target).xconn; - get_xtarget(&target) - .xconn + xconn + .select_xinput_events( + root, + ALL_DEVICES, + x11rb::protocol::xinput::XIEventMask::HIERARCHY, + ) + .expect_then_ignore_error("Failed to register for XInput2 device hotplug events"); + + xconn .select_xkb_events( 0x100, // Use the "core keyboard device" - ffi::XkbNewKeyboardNotifyMask | ffi::XkbStateNotifyMask, + xkb::EventType::NEW_KEYBOARD_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::STATE_NOTIFY, ) - .unwrap() - .queue(); + .unwrap(); - event_processor.init_device(ffi::XIAllDevices); + event_processor.init_device(ALL_DEVICES); EventLoop { + loop_running: false, event_loop, + waker, event_processor, + redraw_receiver: PeekableReceiver::from_recv(redraw_channel), + activation_receiver: PeekableReceiver::from_recv(activation_token_channel), + user_receiver: PeekableReceiver::from_recv(user_channel), user_sender, - target, - redraw_dispatcher, state: EventLoopState { - user_events: VecDeque::new(), - redraw_events: VecDeque::new(), + x11_readiness: Readiness::EMPTY, }, } } pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { - user_sender: self.user_sender.clone(), + user_sender: WakeSender { + sender: self.user_sender.clone(), + waker: self.waker.clone(), + }, } } pub(crate) fn window_target(&self) -> &RootELW { - &self.target + &self.event_processor.target } - pub fn run_return(&mut self, mut callback: F) -> i32 + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> where - F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(Event, &RootELW), { - struct IterationResult { - deadline: Option, - timeout: Option, - wait_start: Instant, - } - fn single_iteration( - this: &mut EventLoop, - control_flow: &mut ControlFlow, - cause: &mut StartCause, - callback: &mut F, - ) -> IterationResult - where - F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), - { - sticky_exit_callback( - crate::event::Event::NewEvents(*cause), - &this.target, - control_flow, - 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, - ); - } + if self.loop_running { + return Err(EventLoopError::AlreadyRunning); + } - // Process all pending events - this.drain_events(callback, control_flow); - - // Empty the user event buffer - { - while let Some(event) = this.state.user_events.pop_front() { - sticky_exit_callback( - crate::event::Event::UserEvent(event), - &this.target, - control_flow, - callback, - ); + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + } + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + } + _ => { + continue; } } - // send MainEventsCleared - { - sticky_exit_callback( - crate::event::Event::MainEventsCleared, - &this.target, - control_flow, - callback, - ); - } + }; - // Quickly dispatch all redraw events to avoid buffering them. - while let Ok(event) = this.redraw_dispatcher.as_source_mut().try_recv() { - this.state.redraw_events.push_back(event); - } + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // X Server. + let wt = EventProcessor::window_target(&self.event_processor.target); + wt.x_connection().sync_with_server().map_err(|x_err| { + EventLoopError::Os(os_error!(OsError::XError(Arc::new(X11Error::Xlib(x_err))))) + })?; - // Empty the redraw requests - { - let mut windows = HashSet::new(); + exit + } - // Empty the channel. + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(Event, &RootELW), + { + if !self.loop_running { + self.loop_running = true; - while let Some(window_id) = this.state.redraw_events.pop_front() { - windows.insert(window_id); - } + // run the initial loop iteration + self.single_iteration(&mut callback, StartCause::Init); + } - for window_id in windows { - let window_id = crate::window::WindowId(window_id); - sticky_exit_callback( - Event::RedrawRequested(window_id), - &this.target, - control_flow, - callback, - ); - } - } - // send RedrawEventsCleared - { - sticky_exit_callback( - crate::event::Event::RedrawEventsCleared, - &this.target, - control_flow, - callback, - ); - } + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; - let start = Instant::now(); - let (deadline, timeout); + callback(Event::LoopExiting, self.window_target()); - match control_flow { - ControlFlow::ExitWithCode(_) => { - return IterationResult { - wait_start: start, - deadline: None, - timeout: None, - }; - } - ControlFlow::Poll => { - *cause = StartCause::Poll; - deadline = None; - timeout = Some(Duration::from_millis(0)); + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + fn has_pending(&mut self) -> bool { + self.event_processor.poll() + || self.user_receiver.has_incoming() + || self.redraw_receiver.has_incoming() + } + + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootELW), + { + let start = Instant::now(); + + let has_pending = self.has_pending(); + + timeout = if has_pending { + // If we already have work to do then we don't want to block on the next poll. + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) } - ControlFlow::Wait => { - *cause = StartCause::WaitCancelled { + }; + + min_timeout(control_flow_timeout, timeout) + }; + + self.state.x11_readiness = Readiness::EMPTY; + if let Err(error) = self + .event_loop + .dispatch(timeout, &mut self.state) + .map_err(std::io::Error::from) + { + log::error!("Failed to poll for events: {error:?}"); + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, - requested_resume: None, - }; - deadline = None; - timeout = None; - } - ControlFlow::WaitUntil(wait_deadline) => { - *cause = StartCause::ResumeTimeReached { + requested_resume: Some(deadline), + } + } else { + StartCause::ResumeTimeReached { start, - requested_resume: *wait_deadline, - }; - timeout = if *wait_deadline > start { - Some(*wait_deadline - start) - } else { - Some(Duration::from_millis(0)) - }; - deadline = Some(*wait_deadline); + requested_resume: deadline, + } } } + }; - IterationResult { - wait_start: start, - deadline, - timeout, - } + // False positive / spurious wake ups could lead to us spamming + // redundant iterations of the event loop with no new events to + // dispatch. + // + // If there's no readable event source then we just double check if we + // have any pending `_receiver` events and if not we return without + // running a loop iteration. + // If we don't have any pending `_receiver` + if !self.has_pending() + && !matches!( + &cause, + StartCause::ResumeTimeReached { .. } | StartCause::Poll + ) + { + return; } - let mut control_flow = ControlFlow::default(); - let mut cause = StartCause::Init; + self.single_iteration(&mut callback, cause); + } - // run the initial loop iteration - let mut iter_result = single_iteration(self, &mut control_flow, &mut cause, &mut callback); + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootELW), + { + callback(Event::NewEvents(cause), &self.event_processor.target); - let exit_code = loop { - if let ControlFlow::ExitWithCode(code) = control_flow { - break code; - } - let has_pending = self.event_processor.poll() - || !self.state.user_events.is_empty() - || !self.state.redraw_events.is_empty(); - if !has_pending { - // Wait until - if let Err(error) = self - .event_loop - .dispatch(iter_result.timeout, &mut self.state) - .map_err(std::io::Error::from) - { - break error.raw_os_error().unwrap_or(1); - } + // 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 { + callback(Event::Resumed, &self.event_processor.target); + } - if control_flow == ControlFlow::Wait { - // We don't go straight into executing the event loop iteration, we instead go - // to the start of this loop and check again if there's any pending event. We - // must do this because during the execution of the iteration we sometimes wake - // the calloop 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 continuously even if the control_flow was `Wait` - continue; + // Process all pending events + self.drain_events(callback); + + // Empty activation tokens. + while let Ok((window_id, serial)) = self.activation_receiver.try_recv() { + let token = self + .event_processor + .with_window(window_id.0 as xproto::Window, |window| { + window.generate_activation_token() + }); + + match token { + Some(Ok(token)) => { + let event = Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ActivationTokenDone { + serial, + token: crate::window::ActivationToken::_new(token), + }, + }; + callback(event, &self.event_processor.target) + } + Some(Err(e)) => { + log::error!("Failed to get activation token: {}", e); } + None => {} } + } - let wait_cancelled = iter_result - .deadline - .map_or(false, |deadline| Instant::now() < deadline); - - if wait_cancelled { - cause = StartCause::WaitCancelled { - start: iter_result.wait_start, - requested_resume: iter_result.deadline, - }; + // Empty the user event buffer + { + while let Ok(event) = self.user_receiver.try_recv() { + callback(Event::UserEvent(event), &self.event_processor.target); } + } - iter_result = single_iteration(self, &mut control_flow, &mut cause, &mut callback); - }; + // Empty the redraw requests + { + let mut windows = HashSet::new(); - callback( - crate::event::Event::LoopDestroyed, - &self.target, - &mut control_flow, - ); - exit_code - } + while let Ok(window_id) = self.redraw_receiver.try_recv() { + windows.insert(window_id); + } - pub fn run(mut self, callback: F) -> ! - where - F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), - { - let exit_code = self.run_return(callback); - ::std::process::exit(exit_code); + for window_id in windows { + let window_id = crate::window::WindowId(window_id); + callback( + Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }, + &self.event_processor.target, + ); + } + } + + // This is always the last event we dispatch before poll again + { + callback(Event::AboutToWait, &self.event_processor.target); + } } - fn drain_events(&mut self, callback: &mut F, control_flow: &mut ControlFlow) + fn drain_events(&mut self, callback: &mut F) where - F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(Event, &RootELW), { - let target = &self.target; let mut xev = MaybeUninit::uninit(); - let wt = get_xtarget(&self.target); while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { let mut xev = unsafe { xev.assume_init() }; - self.event_processor.process_event(&mut xev, |event| { - sticky_exit_callback( - event, - target, - control_flow, - &mut |event, window_target, control_flow| { - if let Event::RedrawRequested(crate::window::WindowId(wid)) = event { - wt.redraw_sender.send(wid).unwrap(); - } else { - callback(event, window_target, control_flow); - } - }, - ); - }); + self.event_processor + .process_event(&mut xev, |window_target, event| { + if let Event::WindowEvent { + window_id: crate::window::WindowId(wid), + event: WindowEvent::RedrawRequested, + } = event + { + let window_target = EventProcessor::window_target(window_target); + window_target.redraw_sender.send(wid).unwrap(); + } else { + callback(event, window_target); + } + }); } } + + fn control_flow(&self) -> ControlFlow { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.control_flow() + } + + fn exiting(&self) -> bool { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exiting() + } + + fn set_exit_code(&self, code: i32) { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.set_exit_code(code); + } + + fn exit_code(&self) -> Option { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exit_code() + } } -pub(crate) fn get_xtarget(target: &RootELW) -> &EventLoopWindowTarget { - match target.p { - super::EventLoopWindowTarget::X(ref target) => target, - #[cfg(wayland_platform)] - _ => unreachable!(), +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() } } @@ -583,7 +679,15 @@ impl EventLoopWindowTarget { &self.xconn } - pub fn set_listen_device_events(&self, allowed: DeviceEvents) { + pub fn available_monitors(&self) -> impl Iterator { + self.xconn.available_monitors().into_iter().flatten() + } + + pub fn primary_monitor(&self) -> Option { + self.xconn.primary_monitor().ok() + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { self.device_events.set(allowed); } @@ -592,26 +696,69 @@ impl EventLoopWindowTarget { let device_events = self.device_events.get() == DeviceEvents::Always || (focus && self.device_events.get() == DeviceEvents::WhenFocused); - let mut mask = 0; + let mut mask = xinput::XIEventMask::from(0u32); if device_events { - mask = ffi::XI_RawMotionMask - | ffi::XI_RawButtonPressMask - | ffi::XI_RawButtonReleaseMask - | ffi::XI_RawKeyPressMask - | ffi::XI_RawKeyReleaseMask; + mask = xinput::XIEventMask::RAW_MOTION + | xinput::XIEventMask::RAW_BUTTON_PRESS + | xinput::XIEventMask::RAW_BUTTON_RELEASE + | xinput::XIEventMask::RAW_KEY_PRESS + | xinput::XIEventMask::RAW_KEY_RELEASE; } self.xconn - .select_xinput_events(self.root, ffi::XIAllMasterDevices, mask) - .queue(); + .select_xinput_events(self.root, ALL_MASTER_DEVICES, mask) + .expect_then_ignore_error("Failed to update device event filter"); } - pub fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { - let mut display_handle = XlibDisplayHandle::empty(); + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::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) + display_handle.screen = self.xconn.default_screen_index() as c_int; + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + let display_handle = rwh_06::XlibDisplayHandle::new( + // SAFETY: display will never be null + Some( + std::ptr::NonNull::new(self.xconn.display as *mut _) + .expect("X11 display should never be null"), + ), + self.xconn.default_screen_index() as c_int, + ); + Ok(display_handle.into()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() } } @@ -664,7 +811,7 @@ impl<'a> Deref for DeviceInfo<'a> { } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(c_int); +pub struct DeviceId(xinput::DeviceId); impl DeviceId { #[allow(unused)] @@ -702,58 +849,183 @@ impl Drop for Window { fn drop(&mut self) { let window = self.deref(); let xconn = &window.xconn; - unsafe { - (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(); + + if let Ok(c) = xconn + .xcb_connection() + .destroy_window(window.id().0 as xproto::Window) + { + c.ignore_error(); } } } -/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. This is a wrapper to -/// extract the cookie from a GenericEvent XEvent and release the cookie data once it has been processed -struct GenericEventCookie<'a> { - xconn: &'a XConnection, - cookie: ffi::XGenericEventCookie, +/// Generic sum error type for X11 errors. +#[derive(Debug)] +pub enum X11Error { + /// An error from the Xlib library. + Xlib(XError), + + /// An error that occurred while trying to connect to the X server. + Connect(ConnectError), + + /// An error that occurred over the connection medium. + Connection(ConnectionError), + + /// An error that occurred logically on the X11 end. + X11(LogicalError), + + /// The XID range has been exhausted. + XidsExhausted(IdsExhausted), + + /// Got `null` from an Xlib function without a reason. + UnexpectedNull(&'static str), + + /// Got an invalid activation token. + InvalidActivationToken(Vec), + + /// An extension that we rely on is not available. + MissingExtension(&'static str), + + /// Could not find a matching X11 visual for this visualid + NoSuchVisual(xproto::Visualid), + + /// Unable to parse xsettings. + XsettingsParse(xsettings::ParserError), + + /// Failed to get property. + GetProperty(util::GetPropertyError), } -impl<'a> GenericEventCookie<'a> { - 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 { - Some(GenericEventCookie { xconn, cookie }) - } else { - None +impl fmt::Display for X11Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + X11Error::Xlib(e) => write!(f, "Xlib error: {}", e), + X11Error::Connect(e) => write!(f, "X11 connection error: {}", e), + X11Error::Connection(e) => write!(f, "X11 connection error: {}", e), + X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e), + X11Error::GetProperty(e) => write!(f, "Failed to get X property {}", e), + X11Error::X11(e) => write!(f, "X11 error: {:?}", e), + X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s), + X11Error::InvalidActivationToken(s) => write!( + f, + "Invalid activation token: {}", + std::str::from_utf8(s).unwrap_or("") + ), + X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {}", s), + X11Error::NoSuchVisual(visualid) => { + write!( + f, + "Could not find a matching X11 visual for ID `{:x}`", + visualid + ) + } + X11Error::XsettingsParse(err) => { + write!(f, "Failed to parse xsettings: {:?}", err) } } } } -impl<'a> Drop for GenericEventCookie<'a> { - fn drop(&mut self) { - unsafe { - (self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie); +impl std::error::Error for X11Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + X11Error::Xlib(e) => Some(e), + X11Error::Connect(e) => Some(e), + X11Error::Connection(e) => Some(e), + X11Error::XidsExhausted(e) => Some(e), + _ => None, } } } -#[derive(Debug, Default, Copy, Clone)] -struct XExtension { - opcode: c_int, - first_event_id: c_int, - first_error_id: c_int, +impl From for X11Error { + fn from(e: XError) -> Self { + X11Error::Xlib(e) + } +} + +impl From for X11Error { + fn from(e: ConnectError) -> Self { + X11Error::Connect(e) + } } -fn mkwid(w: ffi::Window) -> crate::window::WindowId { +impl From for X11Error { + fn from(e: ConnectionError) -> Self { + X11Error::Connection(e) + } +} + +impl From for X11Error { + fn from(e: LogicalError) -> Self { + X11Error::X11(e) + } +} + +impl From for X11Error { + fn from(value: ReplyError) -> Self { + match value { + ReplyError::ConnectionError(e) => e.into(), + ReplyError::X11Error(e) => e.into(), + } + } +} + +impl From for X11Error { + fn from(value: ime::ImeContextCreationError) -> Self { + match value { + ime::ImeContextCreationError::XError(e) => e.into(), + ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"), + } + } +} + +impl From for X11Error { + fn from(value: ReplyOrIdError) -> Self { + match value { + ReplyOrIdError::ConnectionError(e) => e.into(), + ReplyOrIdError::X11Error(e) => e.into(), + ReplyOrIdError::IdsExhausted => Self::XidsExhausted(IdsExhausted), + } + } +} + +impl From for X11Error { + fn from(value: xsettings::ParserError) -> Self { + Self::XsettingsParse(value) + } +} + +impl From for X11Error { + fn from(value: util::GetPropertyError) -> Self { + Self::GetProperty(value) + } +} + +/// Type alias for a void cookie. +type VoidCookie<'a> = x11rb::cookie::VoidCookie<'a, X11rbConnection>; + +/// Extension trait for `Result`. +trait CookieResultExt { + /// Unwrap the send error and ignore the result. + fn expect_then_ignore_error(self, msg: &str); +} + +impl<'a, E: fmt::Debug> CookieResultExt for Result, E> { + fn expect_then_ignore_error(self, msg: &str) { + self.expect(msg).ignore_error() + } +} + +fn mkwid(w: xproto::Window) -> crate::window::WindowId { crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _)) } -fn mkdid(w: c_int) -> crate::event::DeviceId { +fn mkdid(w: xinput::DeviceId) -> crate::event::DeviceId { crate::event::DeviceId(crate::platform_impl::DeviceId::X(DeviceId(w))) } #[derive(Debug)] -struct Device { +pub struct Device { _name: String, scroll_axes: Vec<(i32, ScrollAxis)>, // For master devices, this is the paired device (pointer <-> keyboard). @@ -781,12 +1053,10 @@ impl Device { if Device::physical_device(info) { // Identify scroll axes - for class_ptr in Device::classes(info) { - let class = unsafe { &**class_ptr }; - if class._type == ffi::XIScrollClass { - let info = unsafe { - mem::transmute::<&ffi::XIAnyClassInfo, &ffi::XIScrollClassInfo>(class) - }; + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIScrollClass { + let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; scroll_axes.push(( info.number, ScrollAxis { @@ -814,12 +1084,10 @@ impl Device { fn reset_scroll_position(&mut self, info: &ffi::XIDeviceInfo) { if Device::physical_device(info) { - for class_ptr in Device::classes(info) { - let class = unsafe { &**class_ptr }; - if class._type == ffi::XIValuatorClass { - let info = unsafe { - mem::transmute::<&ffi::XIAnyClassInfo, &ffi::XIValuatorClassInfo>(class) - }; + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIValuatorClass { + let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; if let Some(&mut (_, ref mut axis)) = self .scroll_axes .iter_mut() @@ -849,3 +1117,9 @@ impl Device { } } } + +/// Convert the raw X11 representation for a 32-bit floating point to a double. +#[inline] +fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 { + (fp as f64) / ((1 << 16) as f64) +} diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 297e3debf5..ce0bdd3d1c 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -1,29 +1,24 @@ -use std::os::raw::*; -use std::slice; -use std::sync::Mutex; - -use once_cell::sync::Lazy; - -use super::{ - ffi::{ - RRCrtc, RRCrtcChangeNotifyMask, RRMode, RROutputPropertyNotifyMask, - RRScreenChangeNotifyMask, True, Window, XRRCrtcInfo, XRRModeInfo, XRRScreenResources, - }, - util, XConnection, XError, -}; +use super::{util, X11Error, XConnection}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - platform_impl::{MonitorHandle as PlatformMonitorHandle, VideoMode as PlatformVideoMode}, + platform_impl::VideoMode as PlatformVideoMode, +}; +use x11rb::{ + connection::RequestConnection, + protocol::{ + randr::{self, ConnectionExt as _}, + xproto, + }, }; // Used for testing. This should always be committed as false. const DISABLE_MONITOR_LIST_CACHING: bool = false; -static MONITORS: Lazy>>> = Lazy::new(Mutex::default); - -pub fn invalidate_cached_monitor_list() -> Option> { - // We update this lazily. - (*MONITORS.lock().unwrap()).take() +impl XConnection { + pub fn invalidate_cached_monitor_list(&self) -> Option> { + // We update this lazily. + self.monitor_handles.lock().unwrap().take() + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -31,7 +26,7 @@ pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, pub(crate) refresh_rate_millihertz: u32, - pub(crate) native_mode: RRMode, + pub(crate) native_mode: randr::Mode, pub(crate) monitor: Option, } @@ -52,15 +47,15 @@ impl VideoMode { } #[inline] - pub fn monitor(&self) -> PlatformMonitorHandle { - PlatformMonitorHandle::X(self.monitor.clone().unwrap()) + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone().unwrap() } } #[derive(Debug, Clone)] pub struct MonitorHandle { /// The actual id - pub(crate) id: RRCrtc, + pub(crate) id: randr::Crtc, /// The name of the monitor pub(crate) name: String, /// The size of the monitor @@ -106,10 +101,10 @@ 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 { +pub fn mode_refresh_rate_millihertz(mode: &randr::ModeInfo) -> Option { + if mode.dot_clock > 0 && mode.htotal > 0 && mode.vtotal > 0 { #[allow(clippy::unnecessary_cast)] - Some((mode.dotClock as u64 * 1000 / (mode.hTotal as u64 * mode.vTotal as u64)) as u32) + Some((mode.dot_clock as u64 * 1000 / (mode.htotal as u64 * mode.vtotal as u64)) as u32) } else { None } @@ -118,19 +113,18 @@ pub fn mode_refresh_rate_millihertz(mode: &XRRModeInfo) -> Option { impl MonitorHandle { fn new( xconn: &XConnection, - resources: *mut XRRScreenResources, - id: RRCrtc, - crtc: *mut XRRCrtcInfo, + resources: &ScreenResources, + id: randr::Crtc, + crtc: &randr::GetCrtcInfoReply, primary: bool, ) -> Option { - let (name, scale_factor, video_modes) = unsafe { xconn.get_output_info(resources, crtc)? }; - let dimensions = unsafe { ((*crtc).width, (*crtc).height) }; - let position = unsafe { ((*crtc).x, (*crtc).y) }; + let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?; + let dimensions = (crtc.width as u32, crtc.height as u32); + let position = (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 current_mode = crtc.mode; + let screen_modes = resources.modes(); let refresh_rate_millihertz = screen_modes .iter() .find(|mode| mode.id == current_mode) @@ -207,19 +201,22 @@ impl MonitorHandle { } impl XConnection { - pub fn get_monitor_for_window(&self, window_rect: Option) -> MonitorHandle { - let monitors = self.available_monitors(); + pub fn get_monitor_for_window( + &self, + window_rect: Option, + ) -> Result { + let monitors = self.available_monitors()?; if monitors.is_empty() { // Return a dummy monitor to avoid panicking - return MonitorHandle::dummy(); + return Ok(MonitorHandle::dummy()); } - let default = monitors.get(0).unwrap(); + let default = monitors.first().unwrap(); let window_rect = match window_rect { Some(rect) => rect, - None => return default.to_owned(), + None => return Ok(default.to_owned()), }; let mut largest_overlap = 0; @@ -232,110 +229,147 @@ impl XConnection { } } - matched_monitor.to_owned() + Ok(matched_monitor.to_owned()) } - fn query_monitor_list(&self) -> Vec { - unsafe { - let mut major = 0; - let mut minor = 0; - (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); - - let root = (self.xlib.XDefaultRootWindow)(self.display); - let resources = if (major == 1 && minor >= 3) || major > 1 { - (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) - } else { - // WARNING: this function is supposedly very slow, on the order of hundreds of ms. - // Upon failure, `resources` will be null. - (self.xrandr.XRRGetScreenResources)(self.display, root) - }; - - if resources.is_null() { - panic!("[winit] `XRRGetScreenResources` returned NULL. That should only happen if the root window doesn't exist."); - } + fn query_monitor_list(&self) -> Result, X11Error> { + let root = self.default_root(); + let resources = + ScreenResources::from_connection(self.xcb_connection(), root, self.randr_version())?; + + // Pipeline all of the get-crtc requests. + let mut crtc_cookies = Vec::with_capacity(resources.crtcs().len()); + for &crtc in resources.crtcs() { + crtc_cookies.push( + self.xcb_connection() + .randr_get_crtc_info(crtc, x11rb::CURRENT_TIME)?, + ); + } - let mut has_primary = false; - - let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); - 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); - let is_active = (*crtc).width > 0 && (*crtc).height > 0 && (*crtc).noutput > 0; - if is_active { - let is_primary = *(*crtc).outputs.offset(0) == primary; - has_primary |= is_primary; - if let Some(monitor_id) = - MonitorHandle::new(self, resources, crtc_id, crtc, is_primary) - { - available.push(monitor_id) - } - } - (self.xrandr.XRRFreeCrtcInfo)(crtc); - } + // Do this here so we do all of our requests in one shot. + let primary = self + .xcb_connection() + .randr_get_output_primary(root.root)? + .reply()? + .output; + + let mut crtc_infos = Vec::with_capacity(crtc_cookies.len()); + for cookie in crtc_cookies { + let reply = cookie.reply()?; + crtc_infos.push(reply); + } - // If no monitors were detected as being primary, we just pick one ourselves! - if !has_primary { - if let Some(ref mut fallback) = available.first_mut() { - // Setting this here will come in handy if we ever add an `is_primary` method. - fallback.primary = true; - } + let mut has_primary = false; + let mut available_monitors = Vec::with_capacity(resources.crtcs().len()); + for (crtc_id, crtc) in resources.crtcs().iter().zip(crtc_infos.iter()) { + if crtc.width == 0 || crtc.height == 0 || crtc.outputs.is_empty() { + continue; } - (self.xrandr.XRRFreeScreenResources)(resources); - available + let is_primary = crtc.outputs[0] == primary; + has_primary |= is_primary; + let monitor = MonitorHandle::new(self, &resources, *crtc_id, crtc, is_primary); + available_monitors.extend(monitor); } + + // If we don't have a primary monitor, just pick one ourselves! + if !has_primary { + if let Some(ref mut fallback) = available_monitors.first_mut() { + // Setting this here will come in handy if we ever add an `is_primary` method. + fallback.primary = true; + } + } + + Ok(available_monitors) } - pub fn available_monitors(&self) -> Vec { - let mut monitors_lock = MONITORS.lock().unwrap(); - (*monitors_lock) - .as_ref() - .cloned() - .or_else(|| { - let monitors = Some(self.query_monitor_list()); + pub fn available_monitors(&self) -> Result, X11Error> { + let mut monitors_lock = self.monitor_handles.lock().unwrap(); + match *monitors_lock { + Some(ref monitors) => Ok(monitors.clone()), + None => { + let monitors = self.query_monitor_list()?; if !DISABLE_MONITOR_LIST_CACHING { - (*monitors_lock) = monitors.clone(); + *monitors_lock = Some(monitors.clone()); } - monitors - }) - .unwrap() + Ok(monitors) + } + } } #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - self.available_monitors() + pub fn primary_monitor(&self) -> Result { + Ok(self + .available_monitors()? .into_iter() .find(|monitor| monitor.primary) - .unwrap_or_else(MonitorHandle::dummy) + .unwrap_or_else(MonitorHandle::dummy)) } - pub fn select_xrandr_input(&self, root: Window) -> Result { - let has_xrandr = unsafe { - let mut major = 0; - let mut minor = 0; - (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor) - }; - assert!( - has_xrandr == True, - "[winit] XRandR extension not available." - ); - - let mut event_offset = 0; - let mut error_offset = 0; - let status = unsafe { - (self.xrandr.XRRQueryExtension)(self.display, &mut event_offset, &mut error_offset) - }; + pub fn select_xrandr_input(&self, root: xproto::Window) -> Result { + use randr::NotifyMask; + + // Get extension info. + let info = self + .xcb_connection() + .extension_information(randr::X11_EXTENSION_NAME)? + .ok_or_else(|| X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; + + // Select input data. + let event_mask = + NotifyMask::CRTC_CHANGE | NotifyMask::OUTPUT_PROPERTY | NotifyMask::SCREEN_CHANGE; + self.xcb_connection().randr_select_input(root, event_mask)?; - if status != True { - self.check_errors()?; - unreachable!("[winit] `XRRQueryExtension` failed but no error was received."); + Ok(info.first_event) + } +} + +pub struct ScreenResources { + /// List of attached modes. + modes: Vec, + + /// List of attached CRTCs. + crtcs: Vec, +} + +impl ScreenResources { + pub(crate) fn modes(&self) -> &[randr::ModeInfo] { + &self.modes + } + + pub(crate) fn crtcs(&self) -> &[randr::Crtc] { + &self.crtcs + } + + pub(crate) fn from_connection( + conn: &impl x11rb::connection::Connection, + root: &x11rb::protocol::xproto::Screen, + (major_version, minor_version): (u32, u32), + ) -> Result { + if (major_version == 1 && minor_version >= 3) || major_version > 1 { + let reply = conn + .randr_get_screen_resources_current(root.root)? + .reply()?; + Ok(Self::from_get_screen_resources_current_reply(reply)) + } else { + let reply = conn.randr_get_screen_resources(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_reply(reply)) } + } - let mask = RRCrtcChangeNotifyMask | RROutputPropertyNotifyMask | RRScreenChangeNotifyMask; - unsafe { (self.xrandr.XRRSelectInput)(self.display, root, mask) }; + pub(crate) fn from_get_screen_resources_reply(reply: randr::GetScreenResourcesReply) -> Self { + Self { + modes: reply.modes, + crtcs: reply.crtcs, + } + } - Ok(event_offset) + pub(crate) fn from_get_screen_resources_current_reply( + reply: randr::GetScreenResourcesCurrentReply, + ) -> Self { + Self { + modes: reply.modes, + crtcs: reply.crtcs, + } } } diff --git a/src/platform_impl/linux/x11/tests/xsettings.dat b/src/platform_impl/linux/x11/tests/xsettings.dat new file mode 100644 index 0000000000..e04eeb142a --- /dev/null +++ b/src/platform_impl/linux/x11/tests/xsettings.dat @@ -0,0 +1 @@ +0x6c,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x22,0x00,0x00,0x00,0x00,0x00,0x0b,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x69,0x6e,0x67,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x69,0x61,0x6c,0x6f,0x67,0x73,0x55,0x73,0x65,0x48,0x65,0x61,0x64,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0c,0x00,0x47,0x74,0x6b,0x2f,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x4e,0x6f,0x74,0x6f,0x20,0x53,0x61,0x6e,0x73,0x20,0x39,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x4c,0x63,0x64,0x66,0x69,0x6c,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x6c,0x63,0x64,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4b,0x65,0x79,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x68,0x69,0x6e,0x74,0x73,0x6c,0x69,0x67,0x68,0x74,0x00,0x00,0x01,0x00,0x11,0x00,0x4e,0x65,0x74,0x2f,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x00,0x00,0x65,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x61,0x72,0x79,0x2d,0x78,0x66,0x63,0x65,0x2d,0x64,0x61,0x72,0x6b,0x00,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x41,0x6e,0x74,0x69,0x61,0x6c,0x69,0x61,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x08,0x00,0x58,0x66,0x74,0x2f,0x52,0x47,0x42,0x41,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x72,0x67,0x62,0x00,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0xb0,0x04,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x46,0x61,0x6c,0x6c,0x62,0x61,0x63,0x6b,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x67,0x6e,0x6f,0x6d,0x65,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x69,0x63,0x6f,0x6e,0x73,0x00,0x00,0x00,0x01,0x00,0x12,0x00,0x4e,0x65,0x74,0x2f,0x53,0x6f,0x75,0x6e,0x64,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x45,0x76,0x65,0x6e,0x74,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x43,0x6f,0x6c,0x6f,0x72,0x50,0x61,0x6c,0x65,0x74,0x74,0x65,0x00,0x00,0x00,0x00,0x94,0x00,0x00,0x00,0x62,0x6c,0x61,0x63,0x6b,0x3a,0x77,0x68,0x69,0x74,0x65,0x3a,0x67,0x72,0x61,0x79,0x35,0x30,0x3a,0x72,0x65,0x64,0x3a,0x70,0x75,0x72,0x70,0x6c,0x65,0x3a,0x62,0x6c,0x75,0x65,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x62,0x6c,0x75,0x65,0x3a,0x67,0x72,0x65,0x65,0x6e,0x3a,0x79,0x65,0x6c,0x6c,0x6f,0x77,0x3a,0x6f,0x72,0x61,0x6e,0x67,0x65,0x3a,0x6c,0x61,0x76,0x65,0x6e,0x64,0x65,0x72,0x3a,0x62,0x72,0x6f,0x77,0x6e,0x3a,0x67,0x6f,0x6c,0x64,0x65,0x6e,0x72,0x6f,0x64,0x34,0x3a,0x64,0x6f,0x64,0x67,0x65,0x72,0x20,0x62,0x6c,0x75,0x65,0x3a,0x70,0x69,0x6e,0x6b,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x67,0x72,0x65,0x65,0x6e,0x3a,0x67,0x72,0x61,0x79,0x31,0x30,0x3a,0x67,0x72,0x61,0x79,0x33,0x30,0x3a,0x67,0x72,0x61,0x79,0x37,0x35,0x3a,0x67,0x72,0x61,0x79,0x39,0x30,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x90,0x01,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x61,0x6e,0x43,0x68,0x61,0x6e,0x67,0x65,0x41,0x63,0x63,0x65,0x6c,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x42,0x61,0x72,0x41,0x63,0x63,0x65,0x6c,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x46,0x31,0x30,0x00,0x01,0x00,0x0d,0x00,0x4e,0x65,0x74,0x2f,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x47,0x72,0x65,0x79,0x62,0x69,0x72,0x64,0x01,0x00,0x17,0x00,0x47,0x74,0x6b,0x2f,0x54,0x69,0x74,0x6c,0x65,0x62,0x61,0x72,0x4d,0x69,0x64,0x64,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x6c,0x6f,0x77,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x42,0x75,0x74,0x74,0x6f,0x6e,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x44,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x00,0x00,0x00,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x20,0x31,0x30,0x00,0x00,0x07,0x00,0x58,0x66,0x74,0x2f,0x44,0x50,0x49,0x00,0x02,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x01,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x00,0x00,0x44,0x4d,0x5a,0x2d,0x57,0x68,0x69,0x74,0x65,0x00,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6e,0x64,0x44,0x72,0x61,0x67,0x54,0x68,0x72,0x65,0x73,0x68,0x6f,0x6c,0x64,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x01,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x4c,0x61,0x79,0x6f,0x75,0x74,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x6d,0x65,0x6e,0x75,0x3a,0x6d,0x69,0x6e,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x6d,0x61,0x78,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x63,0x6c,0x6f,0x73,0x65,0x00,0x00,0x1d,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x49,0x6e,0x70,0x75,0x74,0x46,0x65,0x65,0x64,0x62,0x61,0x63,0x6b,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x47,0x64,0x6b,0x2f,0x57,0x69,0x6e,0x64,0x6f,0x77,0x53,0x63,0x61,0x6c,0x69,0x6e,0x67,0x46,0x61,0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x47,0x74,0x6b,0x2f,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x67,0x74,0x6b,0x2d,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3d,0x31,0x36,0x2c,0x31,0x36,0x00,0x00,0x0e,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00 diff --git a/src/platform_impl/linux/x11/util/atom.rs b/src/platform_impl/linux/x11/util/atom.rs deleted file mode 100644 index 55ebeb609c..0000000000 --- a/src/platform_impl/linux/x11/util/atom.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - fmt::Debug, - os::raw::*, - sync::Mutex, -}; - -use once_cell::sync::Lazy; - -use super::*; - -type AtomCache = HashMap; - -static ATOM_CACHE: Lazy> = Lazy::new(|| Mutex::new(HashMap::with_capacity(2048))); - -impl XConnection { - pub fn get_atom + Debug>(&self, name: T) -> ffi::Atom { - let name = name.as_ref(); - let mut atom_cache_lock = ATOM_CACHE.lock().unwrap(); - let cached_atom = (*atom_cache_lock).get(name).cloned(); - if let Some(atom) = cached_atom { - atom - } else { - let atom = unsafe { - (self.xlib.XInternAtom)(self.display, name.as_ptr() as *const c_char, ffi::False) - }; - if atom == 0 { - panic!( - "`XInternAtom` failed, which really shouldn't happen. Atom: {:?}, Error: {:#?}", - name, - self.check_errors(), - ); - } - /*println!( - "XInternAtom name:{:?} atom:{:?}", - name, - atom, - );*/ - (*atom_cache_lock).insert(name.to_owned(), atom); - atom - } - } - - pub unsafe fn get_atom_unchecked(&self, name: &[u8]) -> ffi::Atom { - debug_assert!(CStr::from_bytes_with_nul(name).is_ok()); - let name = CStr::from_bytes_with_nul_unchecked(name); - self.get_atom(name) - } - - // Note: this doesn't use caching, for the sake of simplicity. - // If you're dealing with this many atoms, you'll usually want to cache them locally anyway. - pub unsafe fn get_atoms(&self, names: &[*mut c_char]) -> Result, XError> { - let mut atoms = Vec::with_capacity(names.len()); - (self.xlib.XInternAtoms)( - self.display, - names.as_ptr() as *mut _, - names.len() as c_int, - ffi::False, - atoms.as_mut_ptr(), - ); - self.check_errors()?; - atoms.set_len(names.len()); - /*println!( - "XInternAtoms atoms:{:?}", - atoms, - );*/ - Ok(atoms) - } -} diff --git a/src/platform_impl/linux/x11/util/client_msg.rs b/src/platform_impl/linux/x11/util/client_msg.rs index 2f2b7edf9c..cf5517764d 100644 --- a/src/platform_impl/linux/x11/util/client_msg.rs +++ b/src/platform_impl/linux/x11/util/client_msg.rs @@ -1,46 +1,31 @@ use super::*; - -pub type ClientMsgPayload = [c_long; 5]; +use x11rb::x11_utils::Serialize; impl XConnection { - pub fn send_event>( - &self, - target_window: c_ulong, - event_mask: Option, - event: T, - ) -> Flusher<'_> { - let event_mask = event_mask.unwrap_or(ffi::NoEventMask); - unsafe { - (self.xlib.XSendEvent)( - self.display, - target_window, - ffi::False, - event_mask, - &mut event.into(), - ); - } - Flusher::new(self) - } - pub fn send_client_msg( &self, - window: c_ulong, // The window this is "about"; not necessarily this window - target_window: c_ulong, // The window we're sending to - message_type: ffi::Atom, - event_mask: Option, - data: ClientMsgPayload, - ) -> Flusher<'_> { - let event = ffi::XClientMessageEvent { - type_: ffi::ClientMessage, - display: self.display, + window: xproto::Window, // The window this is "about"; not necessarily this window + target_window: xproto::Window, // The window we're sending to + message_type: xproto::Atom, + event_mask: Option, + data: impl Into, + ) -> Result, X11Error> { + let event = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, window, - message_type, - format: c_long::FORMAT as c_int, - data: unsafe { mem::transmute(data) }, - // These fields are ignored by `XSendEvent` - serial: 0, - send_event: 0, + format: 32, + data: data.into(), + sequence: 0, + type_: message_type, }; - self.send_event(target_window, event_mask, event) + + self.xcb_connection() + .send_event( + false, + target_window, + event_mask.unwrap_or(xproto::EventMask::NO_EVENT), + event.serialize(), + ) + .map_err(Into::into) } } diff --git a/src/platform_impl/linux/x11/util/cookie.rs b/src/platform_impl/linux/x11/util/cookie.rs new file mode 100644 index 0000000000..ccdfa8cd59 --- /dev/null +++ b/src/platform_impl/linux/x11/util/cookie.rs @@ -0,0 +1,55 @@ +use std::ffi::c_int; +use std::sync::Arc; + +use x11_dl::xlib::{self, XEvent, XGenericEventCookie}; + +use crate::platform_impl::x11::XConnection; + +/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. +/// This is a wrapper to extract the cookie from a GenericEvent XEvent and release the cookie data +/// once it has been processed +pub struct GenericEventCookie { + cookie: XGenericEventCookie, + xconn: Arc, +} + +impl GenericEventCookie { + pub fn from_event(xconn: Arc, event: XEvent) -> Option { + unsafe { + let mut cookie: XGenericEventCookie = From::from(event); + if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == xlib::True { + Some(GenericEventCookie { cookie, xconn }) + } else { + None + } + } + } + + #[inline] + pub fn extension(&self) -> u8 { + self.cookie.extension as u8 + } + + #[inline] + pub fn evtype(&self) -> c_int { + self.cookie.evtype + } + + /// Borrow inner event data as `&T`. + /// + /// ## SAFETY + /// + /// The caller must ensure that the event has the `T` inside of it. + #[inline] + pub unsafe fn as_event(&self) -> &T { + unsafe { &*(self.cookie.data as *const _) } + } +} + +impl Drop for GenericEventCookie { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie); + } + } +} diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index 1e70b864e0..8d62cfa7f0 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -1,11 +1,14 @@ use std::ffi::CString; +use std::iter; + +use x11rb::connection::Connection; use crate::window::CursorIcon; use super::*; impl XConnection { - pub fn set_cursor_icon(&self, window: ffi::Window, cursor: Option) { + pub fn set_cursor_icon(&self, window: xproto::Window, cursor: Option) { let cursor = *self .cursor_cache .lock() @@ -13,7 +16,8 @@ impl XConnection { .entry(cursor) .or_insert_with(|| self.get_cursor(cursor)); - self.update_cursor(window, cursor); + self.update_cursor(window, cursor) + .expect("Failed to set cursor"); } fn create_empty_cursor(&self) -> ffi::Cursor { @@ -53,17 +57,33 @@ impl XConnection { None => return self.create_empty_cursor(), }; - let name = CString::new(cursor.name()).unwrap(); - unsafe { - (self.xcursor.XcursorLibraryLoadCursor)(self.display, name.as_ptr() as *const c_char) + let mut xcursor = 0; + for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) { + let name = CString::new(name).unwrap(); + xcursor = unsafe { + (self.xcursor.XcursorLibraryLoadCursor)( + self.display, + name.as_ptr() as *const c_char, + ) + }; + + if xcursor != 0 { + break; + } } + + xcursor } - fn update_cursor(&self, window: ffi::Window, cursor: ffi::Cursor) { - unsafe { - (self.xlib.XDefineCursor)(self.display, window, cursor); + fn update_cursor(&self, window: xproto::Window, cursor: ffi::Cursor) -> Result<(), X11Error> { + self.xcb_connection() + .change_window_attributes( + window, + &xproto::ChangeWindowAttributesAux::new().cursor(cursor as xproto::Cursor), + )? + .ignore_error(); - self.flush_requests().expect("Failed to set the cursor"); - } + self.xcb_connection().flush()?; + Ok(()) } } diff --git a/src/platform_impl/linux/x11/util/format.rs b/src/platform_impl/linux/x11/util/format.rs deleted file mode 100644 index 997946b1ee..0000000000 --- a/src/platform_impl/linux/x11/util/format.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{fmt::Debug, mem, os::raw::*}; - -// This isn't actually the number of the bits in the format. -// X11 does a match on this value to determine which type to call sizeof on. -// Thus, we use 32 for c_long, since 32 maps to c_long which maps to 64. -// ...if that sounds confusing, then you know why this enum is here. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Format { - Char = 8, - Short = 16, - Long = 32, -} - -impl Format { - pub fn from_format(format: usize) -> Option { - match format { - 8 => Some(Format::Char), - 16 => Some(Format::Short), - 32 => Some(Format::Long), - _ => None, - } - } - - pub fn get_actual_size(&self) -> usize { - match self { - Format::Char => mem::size_of::(), - Format::Short => mem::size_of::(), - Format::Long => mem::size_of::(), - } - } -} - -pub trait Formattable: Debug + Clone + Copy + PartialEq + PartialOrd { - const FORMAT: Format; -} - -// You might be surprised by the absence of c_int, but not as surprised as X11 would be by the presence of it. -impl Formattable for c_schar { - const FORMAT: Format = Format::Char; -} -impl Formattable for c_uchar { - const FORMAT: Format = Format::Char; -} -impl Formattable for c_short { - const FORMAT: Format = Format::Short; -} -impl Formattable for c_ushort { - const FORMAT: Format = Format::Short; -} -impl Formattable for c_long { - const FORMAT: Format = Format::Long; -} -impl Formattable for c_ulong { - const FORMAT: Format = Format::Long; -} diff --git a/src/platform_impl/linux/x11/util/geometry.rs b/src/platform_impl/linux/x11/util/geometry.rs index 7afced804a..0a95c4188c 100644 --- a/src/platform_impl/linux/x11/util/geometry.rs +++ b/src/platform_impl/linux/x11/util/geometry.rs @@ -40,16 +40,9 @@ impl AaRect { } } -#[derive(Debug, Default)] -pub struct TranslatedCoords { - pub x_rel_root: c_int, - pub y_rel_root: c_int, - pub child: ffi::Window, -} - #[derive(Debug, Default)] pub struct Geometry { - pub root: ffi::Window, + pub root: xproto::Window, // If you want positions relative to the root window, use translate_coords. // Note that the overwhelming majority of window managers are reparenting WMs, thus the window // ID we get from window creation is for a nested window used as the window's client area. If @@ -69,14 +62,14 @@ pub struct Geometry { #[derive(Debug, Clone)] pub struct FrameExtents { - pub left: c_ulong, - pub right: c_ulong, - pub top: c_ulong, - pub bottom: c_ulong, + pub left: u32, + pub right: u32, + pub top: u32, + pub bottom: u32, } impl FrameExtents { - pub fn new(left: c_ulong, right: c_ulong, top: c_ulong, bottom: c_ulong) -> Self { + pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self { FrameExtents { left, right, @@ -85,19 +78,11 @@ impl FrameExtents { } } - pub fn from_border(border: c_ulong) -> Self { + pub fn from_border(border: u32) -> Self { Self::new(border, border, border, border) } } -#[derive(Debug, Clone)] -pub struct LogicalFrameExtents { - pub left: f64, - pub right: f64, - pub top: f64, - pub bottom: f64, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum FrameExtentsHeuristicPath { Supported, @@ -144,52 +129,29 @@ impl XConnection { // This is adequate for inner_position pub fn translate_coords( &self, - window: ffi::Window, - root: ffi::Window, - ) -> Result { - let mut coords = TranslatedCoords::default(); - - unsafe { - (self.xlib.XTranslateCoordinates)( - self.display, - window, - root, - 0, - 0, - &mut coords.x_rel_root, - &mut coords.y_rel_root, - &mut coords.child, - ); - } - - self.check_errors()?; - Ok(coords) + window: xproto::Window, + root: xproto::Window, + ) -> Result { + self.xcb_connection() + .translate_coordinates(window, root, 0, 0)? + .reply() + .map_err(Into::into) } // This is adequate for inner_size - pub fn get_geometry(&self, window: ffi::Window) -> Result { - let mut geometry = Geometry::default(); - - let _status = unsafe { - (self.xlib.XGetGeometry)( - self.display, - window, - &mut geometry.root, - &mut geometry.x_rel_parent, - &mut geometry.y_rel_parent, - &mut geometry.width, - &mut geometry.height, - &mut geometry.border, - &mut geometry.depth, - ) - }; - - self.check_errors()?; - Ok(geometry) + pub fn get_geometry( + &self, + window: xproto::Window, + ) -> Result { + self.xcb_connection() + .get_geometry(window)? + .reply() + .map_err(Into::into) } - fn get_frame_extents(&self, window: ffi::Window) -> Option { - let extents_atom = unsafe { self.get_atom_unchecked(b"_NET_FRAME_EXTENTS\0") }; + fn get_frame_extents(&self, window: xproto::Window) -> Option { + let atoms = self.atoms(); + let extents_atom = atoms[_NET_FRAME_EXTENTS]; if !hint_is_supported(extents_atom) { return None; @@ -198,8 +160,12 @@ impl XConnection { // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to // be unsupported by many smaller WMs. - let extents: Option> = self - .get_property(window, extents_atom, ffi::XA_CARDINAL) + let extents: Option> = self + .get_property( + window, + extents_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + ) .ok(); extents.and_then(|extents| { @@ -216,52 +182,35 @@ impl XConnection { }) } - pub fn is_top_level(&self, window: ffi::Window, root: ffi::Window) -> Option { - let client_list_atom = unsafe { self.get_atom_unchecked(b"_NET_CLIENT_LIST\0") }; + pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let client_list_atom = atoms[_NET_CLIENT_LIST]; if !hint_is_supported(client_list_atom) { return None; } - let client_list: Option> = self - .get_property(root, client_list_atom, ffi::XA_WINDOW) + let client_list: Option> = self + .get_property( + root, + client_list_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ) .ok(); - client_list.map(|client_list| client_list.contains(&window)) + client_list.map(|client_list| client_list.contains(&(window as xproto::Window))) } - fn get_parent_window(&self, window: ffi::Window) -> Result { - let parent = unsafe { - let mut root = 0; - let mut parent = 0; - let mut children: *mut ffi::Window = ptr::null_mut(); - let mut nchildren = 0; - - // What's filled into `parent` if `window` is the root window? - let _status = (self.xlib.XQueryTree)( - self.display, - window, - &mut root, - &mut parent, - &mut children, - &mut nchildren, - ); - - // The list of children isn't used - if !children.is_null() { - (self.xlib.XFree)(children as *mut _); - } - - parent - }; - self.check_errors().map(|_| parent) + fn get_parent_window(&self, window: xproto::Window) -> Result { + let parent = self.xcb_connection().query_tree(window)?.reply()?.parent; + Ok(parent) } fn climb_hierarchy( &self, - window: ffi::Window, - root: ffi::Window, - ) -> Result { + window: xproto::Window, + root: xproto::Window, + ) -> Result { let mut outer_window = window; loop { let candidate = self.get_parent_window(outer_window)?; @@ -275,8 +224,8 @@ impl XConnection { pub fn get_frame_extents_heuristic( &self, - window: ffi::Window, - root: ffi::Window, + window: xproto::Window, + root: xproto::Window, ) -> FrameExtentsHeuristic { use self::FrameExtentsHeuristicPath::*; @@ -288,7 +237,7 @@ impl XConnection { let coords = self .translate_coords(window, root) .expect("Failed to translate window coordinates"); - (coords.y_rel_root, coords.child) + (coords.dst_y, coords.child) }; let (width, height, border) = { @@ -298,7 +247,7 @@ impl XConnection { ( inner_geometry.width, inner_geometry.height, - inner_geometry.border, + inner_geometry.border_width, ) }; @@ -353,7 +302,7 @@ impl XConnection { .get_geometry(outer_window) .expect("Failed to get outer window geometry"); ( - outer_geometry.y_rel_parent, + outer_geometry.y, outer_geometry.width, outer_geometry.height, ) @@ -361,21 +310,16 @@ impl XConnection { // Since we have the geometry of the outermost window and the geometry of the client // area, we can figure out what's in between. - let diff_x = outer_width.saturating_sub(width); - let diff_y = outer_height.saturating_sub(height); - let offset_y = inner_y_rel_root.saturating_sub(outer_y) as c_uint; + let diff_x = outer_width.saturating_sub(width) as u32; + let diff_y = outer_height.saturating_sub(height) as u32; + let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32; let left = diff_x / 2; let right = left; let top = offset_y; let bottom = diff_y.saturating_sub(offset_y); - let frame_extents = FrameExtents::new( - left as c_ulong, - right as c_ulong, - top as c_ulong, - bottom as c_ulong, - ); + let frame_extents = FrameExtents::new(left, right, top, bottom); FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested, @@ -383,7 +327,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 as c_ulong); + let frame_extents = FrameExtents::from_border(border.into()); 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 96157e7052..968627e120 100644 --- a/src/platform_impl/linux/x11/util/hint.rs +++ b/src/platform_impl/linux/x11/util/hint.rs @@ -1,4 +1,3 @@ -use std::slice; use std::sync::Arc; use super::*; @@ -66,25 +65,27 @@ pub enum WindowType { } impl WindowType { - pub(crate) fn as_atom(&self, xconn: &Arc) -> ffi::Atom { + pub(crate) fn as_atom(&self, xconn: &Arc) -> xproto::Atom { use self::WindowType::*; - let atom_name: &[u8] = match *self { - Desktop => b"_NET_WM_WINDOW_TYPE_DESKTOP\0", - Dock => b"_NET_WM_WINDOW_TYPE_DOCK\0", - Toolbar => b"_NET_WM_WINDOW_TYPE_TOOLBAR\0", - Menu => b"_NET_WM_WINDOW_TYPE_MENU\0", - Utility => b"_NET_WM_WINDOW_TYPE_UTILITY\0", - Splash => b"_NET_WM_WINDOW_TYPE_SPLASH\0", - Dialog => b"_NET_WM_WINDOW_TYPE_DIALOG\0", - DropdownMenu => b"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU\0", - PopupMenu => b"_NET_WM_WINDOW_TYPE_POPUP_MENU\0", - Tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP\0", - Notification => b"_NET_WM_WINDOW_TYPE_NOTIFICATION\0", - Combo => b"_NET_WM_WINDOW_TYPE_COMBO\0", - Dnd => b"_NET_WM_WINDOW_TYPE_DND\0", - Normal => b"_NET_WM_WINDOW_TYPE_NORMAL\0", + let atom_name = match *self { + Desktop => _NET_WM_WINDOW_TYPE_DESKTOP, + Dock => _NET_WM_WINDOW_TYPE_DOCK, + Toolbar => _NET_WM_WINDOW_TYPE_TOOLBAR, + Menu => _NET_WM_WINDOW_TYPE_MENU, + Utility => _NET_WM_WINDOW_TYPE_UTILITY, + Splash => _NET_WM_WINDOW_TYPE_SPLASH, + Dialog => _NET_WM_WINDOW_TYPE_DIALOG, + DropdownMenu => _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + PopupMenu => _NET_WM_WINDOW_TYPE_POPUP_MENU, + Tooltip => _NET_WM_WINDOW_TYPE_TOOLTIP, + Notification => _NET_WM_WINDOW_TYPE_NOTIFICATION, + Combo => _NET_WM_WINDOW_TYPE_COMBO, + Dnd => _NET_WM_WINDOW_TYPE_DND, + Normal => _NET_WM_WINDOW_TYPE_NORMAL, }; - unsafe { xconn.get_atom_unchecked(atom_name) } + + let atoms = xconn.atoms(); + atoms[atom_name] } } @@ -92,30 +93,27 @@ pub struct MotifHints { hints: MwmHints, } -#[repr(C)] struct MwmHints { - flags: c_ulong, - functions: c_ulong, - decorations: c_ulong, - input_mode: c_long, - status: c_ulong, + flags: u32, + functions: u32, + decorations: u32, + input_mode: u32, + status: u32, } #[allow(dead_code)] mod mwm { - use libc::c_ulong; - // Motif WM hints are obsolete, but still widely supported. // https://stackoverflow.com/a/1909708 - pub const MWM_HINTS_FUNCTIONS: c_ulong = 1 << 0; - pub const MWM_HINTS_DECORATIONS: c_ulong = 1 << 1; - - pub const MWM_FUNC_ALL: c_ulong = 1 << 0; - pub const MWM_FUNC_RESIZE: c_ulong = 1 << 1; - pub const MWM_FUNC_MOVE: c_ulong = 1 << 2; - pub const MWM_FUNC_MINIMIZE: c_ulong = 1 << 3; - pub const MWM_FUNC_MAXIMIZE: c_ulong = 1 << 4; - pub const MWM_FUNC_CLOSE: c_ulong = 1 << 5; + pub const MWM_HINTS_FUNCTIONS: u32 = 1 << 0; + pub const MWM_HINTS_DECORATIONS: u32 = 1 << 1; + + pub const MWM_FUNC_ALL: u32 = 1 << 0; + pub const MWM_FUNC_RESIZE: u32 = 1 << 1; + pub const MWM_FUNC_MOVE: u32 = 1 << 2; + pub const MWM_FUNC_MINIMIZE: u32 = 1 << 3; + pub const MWM_FUNC_MAXIMIZE: u32 = 1 << 4; + pub const MWM_FUNC_CLOSE: u32 = 1 << 5; } impl MotifHints { @@ -133,7 +131,7 @@ impl MotifHints { pub fn set_decorations(&mut self, decorations: bool) { self.hints.flags |= mwm::MWM_HINTS_DECORATIONS; - self.hints.decorations = decorations as c_ulong; + self.hints.decorations = decorations as u32; } pub fn set_maximizable(&mut self, maximizable: bool) { @@ -144,7 +142,7 @@ impl MotifHints { } } - fn add_func(&mut self, func: c_ulong) { + fn add_func(&mut self, func: u32) { if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS != 0 { if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { self.hints.functions &= !func; @@ -154,7 +152,7 @@ impl MotifHints { } } - fn remove_func(&mut self, func: c_ulong) { + fn remove_func(&mut self, func: u32) { if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS == 0 { self.hints.flags |= mwm::MWM_HINTS_FUNCTIONS; self.hints.functions = mwm::MWM_FUNC_ALL; @@ -174,170 +172,47 @@ impl Default for MotifHints { } } -impl MwmHints { - fn as_slice(&self) -> &[c_ulong] { - unsafe { slice::from_raw_parts(self as *const _ as *const c_ulong, 5) } - } -} - -pub(crate) struct NormalHints<'a> { - size_hints: XSmartPointer<'a, ffi::XSizeHints>, -} - -impl<'a> NormalHints<'a> { - pub fn new(xconn: &'a XConnection) -> Self { - NormalHints { - size_hints: xconn.alloc_size_hints(), - } - } - - pub fn get_resize_increments(&self) -> Option<(u32, u32)> { - has_flag(self.size_hints.flags, ffi::PResizeInc).then_some({ - ( - self.size_hints.width_inc as u32, - self.size_hints.height_inc as u32, - ) - }) - } - - pub fn set_position(&mut self, position: Option<(i32, i32)>) { - if let Some((x, y)) = position { - self.size_hints.flags |= ffi::PPosition; - self.size_hints.x = x as c_int; - self.size_hints.y = y as c_int; - } else { - self.size_hints.flags &= !ffi::PPosition; - } - } - - // WARNING: This hint is obsolete - pub fn set_size(&mut self, size: Option<(u32, u32)>) { - if let Some((width, height)) = size { - self.size_hints.flags |= ffi::PSize; - self.size_hints.width = width as c_int; - self.size_hints.height = height as c_int; - } else { - self.size_hints.flags &= !ffi::PSize; - } - } - - pub fn set_max_size(&mut self, max_size: Option<(u32, u32)>) { - if let Some((max_width, max_height)) = max_size { - self.size_hints.flags |= ffi::PMaxSize; - self.size_hints.max_width = max_width as c_int; - self.size_hints.max_height = max_height as c_int; - } else { - self.size_hints.flags &= !ffi::PMaxSize; - } - } - - pub fn set_min_size(&mut self, min_size: Option<(u32, u32)>) { - if let Some((min_width, min_height)) = min_size { - self.size_hints.flags |= ffi::PMinSize; - self.size_hints.min_width = min_width as c_int; - self.size_hints.min_height = min_height as c_int; - } else { - self.size_hints.flags &= !ffi::PMinSize; - } - } - - pub fn set_resize_increments(&mut self, resize_increments: Option<(u32, u32)>) { - if let Some((width_inc, height_inc)) = resize_increments { - self.size_hints.flags |= ffi::PResizeInc; - self.size_hints.width_inc = width_inc as c_int; - self.size_hints.height_inc = height_inc as c_int; - } else { - self.size_hints.flags &= !ffi::PResizeInc; - } - } - - pub fn set_base_size(&mut self, base_size: Option<(u32, u32)>) { - if let Some((base_width, base_height)) = base_size { - self.size_hints.flags |= ffi::PBaseSize; - self.size_hints.base_width = base_width as c_int; - self.size_hints.base_height = base_height as c_int; - } else { - self.size_hints.flags &= !ffi::PBaseSize; - } - } -} - impl XConnection { - pub fn get_wm_hints( - &self, - window: ffi::Window, - ) -> Result, XError> { - let wm_hints = unsafe { (self.xlib.XGetWMHints)(self.display, window) }; - self.check_errors()?; - let wm_hints = if wm_hints.is_null() { - self.alloc_wm_hints() - } else { - XSmartPointer::new(self, wm_hints).unwrap() - }; - Ok(wm_hints) - } - - pub fn set_wm_hints( - &self, - window: ffi::Window, - wm_hints: XSmartPointer<'_, ffi::XWMHints>, - ) -> Flusher<'_> { - unsafe { - (self.xlib.XSetWMHints)(self.display, window, wm_hints.ptr); - } - Flusher::new(self) - } - - pub fn get_normal_hints(&self, window: ffi::Window) -> Result, XError> { - let size_hints = self.alloc_size_hints(); - let mut supplied_by_user = MaybeUninit::uninit(); - unsafe { - (self.xlib.XGetWMNormalHints)( - self.display, - window, - size_hints.ptr, - supplied_by_user.as_mut_ptr(), - ); - } - self.check_errors().map(|_| NormalHints { size_hints }) - } - - pub fn set_normal_hints( - &self, - window: ffi::Window, - normal_hints: NormalHints<'_>, - ) -> Flusher<'_> { - unsafe { - (self.xlib.XSetWMNormalHints)(self.display, window, normal_hints.size_hints.ptr); - } - Flusher::new(self) - } - - pub fn get_motif_hints(&self, window: ffi::Window) -> MotifHints { - let motif_hints = unsafe { self.get_atom_unchecked(b"_MOTIF_WM_HINTS\0") }; + pub fn get_motif_hints(&self, window: xproto::Window) -> MotifHints { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; let mut hints = MotifHints::new(); - if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { + if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { 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; + hints.hints.input_mode = props.get(3).cloned().unwrap_or(0); hints.hints.status = props.get(4).cloned().unwrap_or(0); } hints } - pub fn set_motif_hints(&self, window: ffi::Window, hints: &MotifHints) -> Flusher<'_> { - let motif_hints = unsafe { self.get_atom_unchecked(b"_MOTIF_WM_HINTS\0") }; + #[allow(clippy::unnecessary_cast)] + pub fn set_motif_hints( + &self, + window: xproto::Window, + hints: &MotifHints, + ) -> Result, X11Error> { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let hints_data: [u32; 5] = [ + hints.hints.flags as u32, + hints.hints.functions as u32, + hints.hints.decorations as u32, + hints.hints.input_mode as u32, + hints.hints.status as u32, + ]; self.change_property( window, motif_hints, motif_hints, - PropMode::Replace, - hints.hints.as_slice(), + xproto::PropMode::REPLACE, + &hints_data, ) } } diff --git a/src/platform_impl/linux/x11/util/icon.rs b/src/platform_impl/linux/x11/util/icon.rs index 3240a9f896..07b5fee676 100644 --- a/src/platform_impl/linux/x11/util/icon.rs +++ b/src/platform_impl/linux/x11/util/icon.rs @@ -1,7 +1,7 @@ #![allow(clippy::assertions_on_constants)] use super::*; -use crate::icon::{Icon, Pixel, PIXEL_SIZE}; +use crate::icon::{Pixel, RgbaIcon, PIXEL_SIZE}; impl Pixel { pub fn to_packed_argb(&self) -> Cardinal { @@ -18,16 +18,15 @@ impl Pixel { } } -impl Icon { +impl RgbaIcon { pub(crate) fn to_cardinals(&self) -> Vec { - let rgba_icon = &self.inner; - assert_eq!(rgba_icon.rgba.len() % PIXEL_SIZE, 0); - let pixel_count = rgba_icon.rgba.len() / PIXEL_SIZE; - assert_eq!(pixel_count, (rgba_icon.width * rgba_icon.height) as usize); + assert_eq!(self.rgba.len() % PIXEL_SIZE, 0); + let pixel_count = self.rgba.len() / PIXEL_SIZE; + assert_eq!(pixel_count, (self.width * self.height) as usize); let mut data = Vec::with_capacity(pixel_count); - data.push(rgba_icon.width as Cardinal); - data.push(rgba_icon.height as Cardinal); - let pixels = rgba_icon.rgba.as_ptr() as *const Pixel; + data.push(self.width as Cardinal); + data.push(self.height as Cardinal); + let pixels = self.rgba.as_ptr() as *const Pixel; for pixel_index in 0..pixel_count { let pixel = unsafe { &*pixels.add(pixel_index) }; data.push(pixel.to_packed_argb()); diff --git a/src/platform_impl/linux/x11/util/input.rs b/src/platform_impl/linux/x11/util/input.rs index 41108839f8..c515229b68 100644 --- a/src/platform_impl/linux/x11/util/input.rs +++ b/src/platform_impl/linux/x11/util/input.rs @@ -1,119 +1,64 @@ use std::{slice, str}; +use x11rb::protocol::{ + xinput::{self, ConnectionExt as _}, + xkb, +}; use super::*; -pub const VIRTUAL_CORE_POINTER: c_int = 2; -pub const VIRTUAL_CORE_KEYBOARD: c_int = 3; +pub const VIRTUAL_CORE_POINTER: u16 = 2; +pub const VIRTUAL_CORE_KEYBOARD: u16 = 3; // A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to // re-allocate (and make another round-trip) in the *vast* majority of cases. // To test if `lookup_utf8` works correctly, set this to 1. const TEXT_BUFFER_SIZE: usize = 1024; -// NOTE: Some of these fields are not used, but may be of use in the future. -pub struct PointerState<'a> { - xconn: &'a XConnection, - pub root: ffi::Window, - pub child: ffi::Window, - pub root_x: c_double, - pub root_y: c_double, - pub win_x: c_double, - pub win_y: c_double, - buttons: ffi::XIButtonState, - pub group: ffi::XIGroupState, - pub relative_to_window: bool, -} - -impl<'a> Drop for PointerState<'a> { - fn drop(&mut self) { - if !self.buttons.mask.is_null() { - unsafe { - // This is why you need to read the docs carefully... - (self.xconn.xlib.XFree)(self.buttons.mask as _); - } - } - } -} - impl XConnection { pub fn select_xinput_events( &self, - window: c_ulong, - device_id: c_int, - mask: i32, - ) -> Flusher<'_> { - let mut event_mask = ffi::XIEventMask { - deviceid: device_id, - mask: &mask as *const _ as *mut c_uchar, - mask_len: mem::size_of_val(&mask) as c_int, - }; - unsafe { - (self.xinput2.XISelectEvents)( - self.display, + window: xproto::Window, + device_id: u16, + mask: xinput::XIEventMask, + ) -> Result, X11Error> { + self.xcb_connection() + .xinput_xi_select_events( window, - &mut event_mask as *mut ffi::XIEventMask, - 1, // number of masks to read from pointer above - ); - } - Flusher::new(self) + &[xinput::EventMask { + deviceid: device_id, + mask: vec![mask], + }], + ) + .map_err(Into::into) } - pub fn select_xkb_events(&self, device_id: c_uint, mask: c_ulong) -> Option> { - let status = unsafe { (self.xlib.XkbSelectEvents)(self.display, device_id, mask, mask) }; + pub fn select_xkb_events( + &self, + device_id: xkb::DeviceSpec, + mask: xkb::EventType, + ) -> Result { + let mask = u16::from(mask) as _; + let status = + unsafe { (self.xlib.XkbSelectEvents)(self.display, device_id as _, mask, mask) }; + if status == ffi::True { - Some(Flusher::new(self)) + self.flush_requests()?; + Ok(true) } else { error!("Could not select XKB events: The XKB extension is not initialized!"); - None + Ok(false) } } pub fn query_pointer( &self, - window: ffi::Window, - device_id: c_int, - ) -> Result, XError> { - unsafe { - let mut root = 0; - let mut child = 0; - let mut root_x = 0.0; - let mut root_y = 0.0; - let mut win_x = 0.0; - let mut win_y = 0.0; - let mut buttons = Default::default(); - let mut modifiers = Default::default(); - let mut group = Default::default(); - - let relative_to_window = (self.xinput2.XIQueryPointer)( - self.display, - device_id, - window, - &mut root, - &mut child, - &mut root_x, - &mut root_y, - &mut win_x, - &mut win_y, - &mut buttons, - &mut modifiers, - &mut group, - ) == ffi::True; - - self.check_errors()?; - - Ok(PointerState { - xconn: self, - root, - child, - root_x, - root_y, - win_x, - win_y, - buttons, - group, - relative_to_window, - }) - } + window: xproto::Window, + device_id: u16, + ) -> Result { + self.xcb_connection() + .xinput_xi_query_pointer(window, device_id)? + .reply() + .map_err(Into::into) } fn lookup_utf8_inner( diff --git a/src/platform_impl/linux/x11/util/memory.rs b/src/platform_impl/linux/x11/util/memory.rs index 181fc6a97f..d32eb8cebe 100644 --- a/src/platform_impl/linux/x11/util/memory.rs +++ b/src/platform_impl/linux/x11/util/memory.rs @@ -40,20 +40,3 @@ impl<'a, T> Drop for XSmartPointer<'a, T> { } } } - -impl XConnection { - pub fn alloc_class_hint(&self) -> XSmartPointer<'_, ffi::XClassHint> { - XSmartPointer::new(self, unsafe { (self.xlib.XAllocClassHint)() }) - .expect("`XAllocClassHint` returned null; out of memory") - } - - pub fn alloc_size_hints(&self) -> XSmartPointer<'_, ffi::XSizeHints> { - XSmartPointer::new(self, unsafe { (self.xlib.XAllocSizeHints)() }) - .expect("`XAllocSizeHints` returned null; out of memory") - } - - pub fn alloc_wm_hints(&self) -> XSmartPointer<'_, ffi::XWMHints> { - XSmartPointer::new(self, unsafe { (self.xlib.XAllocWMHints)() }) - .expect("`XAllocWMHints` returned null; out of memory") - } -} diff --git a/src/platform_impl/linux/x11/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index ba7d7eaee2..8df4966b1d 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -1,35 +1,33 @@ // Welcome to the util module, where we try to keep you from shooting yourself in the foot. // *results may vary -mod atom; +use std::{ + mem::{self, MaybeUninit}, + ops::BitAnd, + os::raw::*, +}; + mod client_msg; +pub mod cookie; mod cursor; -mod format; mod geometry; mod hint; mod icon; mod input; pub mod keys; -mod memory; +pub(crate) mod memory; +mod mouse; mod randr; mod window_property; mod wm; +mod xmodmap; pub use self::{ - atom::*, client_msg::*, format::*, geometry::*, hint::*, icon::*, input::*, randr::*, - window_property::*, wm::*, + geometry::*, hint::*, input::*, mouse::*, window_property::*, wm::*, xmodmap::ModifierKeymap, }; -pub(crate) use self::memory::*; - -use std::{ - mem::{self, MaybeUninit}, - ops::BitAnd, - os::raw::*, - ptr, -}; - -use super::{ffi, XConnection, XError}; +use super::{atoms::*, ffi, VoidCookie, X11Error, XConnection, XError}; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; pub fn maybe_change(field: &mut Option, value: T) -> bool { let wrapped = Some(value); @@ -48,30 +46,6 @@ where bitset & flag == flag } -#[must_use = "This request was made asynchronously, and is still in the output buffer. You must explicitly choose to either `.flush()` (empty the output buffer, sending the request now) or `.queue()` (wait to send the request, allowing you to continue to add more requests without additional round-trips). For more information, see the documentation for `util::flush_requests`."] -pub(crate) struct Flusher<'a> { - xconn: &'a XConnection, -} - -impl<'a> Flusher<'a> { - pub fn new(xconn: &'a XConnection) -> Self { - Flusher { xconn } - } - - // "I want this request sent now!" - pub fn flush(self) -> Result<(), XError> { - self.xconn.flush_requests() - } - - // "I want the response now too!" - pub fn sync(self) -> Result<(), XError> { - self.xconn.sync_with_server() - } - - // "I'm aware that this request hasn't been sent, and I'm okay with waiting." - pub fn queue(self) {} -} - impl XConnection { // This is impoartant, so pay attention! // Xlib has an output buffer, and tries to hide the async nature of X from you. diff --git a/src/platform_impl/linux/x11/util/modifiers.rs b/src/platform_impl/linux/x11/util/modifiers.rs index 7e317c1b24..bb157e44b4 100644 --- a/src/platform_impl/linux/x11/util/modifiers.rs +++ b/src/platform_impl/linux/x11/util/modifiers.rs @@ -48,8 +48,8 @@ impl ModifierKeymap { } pub fn reset_from_x_connection(&mut self, xconn: &XConnection) { - unsafe { - let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + { + let keymap = xconn.xcb_connection().get_modifier_mapping().expect("get_modifier_mapping failed").reply().expect("get_modifier_mapping failed"); if keymap.is_null() { panic!("failed to allocate XModifierKeymap"); diff --git a/src/platform_impl/linux/x11/util/mouse.rs b/src/platform_impl/linux/x11/util/mouse.rs new file mode 100644 index 0000000000..9f22266da3 --- /dev/null +++ b/src/platform_impl/linux/x11/util/mouse.rs @@ -0,0 +1,52 @@ +//! Utilities for handling mouse events. + +/// Recorded mouse delta designed to filter out noise. +pub struct Delta { + x: T, + y: T, +} + +impl Default for Delta { + fn default() -> Self { + Self { + x: Default::default(), + y: Default::default(), + } + } +} + +impl Delta { + pub(crate) fn set_x(&mut self, x: T) { + self.x = x; + } + + pub(crate) fn set_y(&mut self, y: T) { + self.y = y; + } +} + +macro_rules! consume { + ($this:expr, $ty:ty) => {{ + let this = $this; + let (x, y) = match (this.x.abs() < <$ty>::EPSILON, this.y.abs() < <$ty>::EPSILON) { + (true, true) => return None, + (false, true) => (this.x, 0.0), + (true, false) => (0.0, this.y), + (false, false) => (this.x, this.y), + }; + + Some((x, y)) + }}; +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f32, f32)> { + consume!(self, f32) + } +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f64, f64)> { + consume!(self, f64) + } +} diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index a076157b14..327cc9f7d1 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,12 +1,11 @@ -use std::{env, slice, str::FromStr}; +use std::{env, str, str::FromStr}; -use super::{ - ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, - *, -}; +use super::*; use crate::platform_impl::platform::x11::monitor; use crate::{dpi::validate_scale_factor, platform_impl::platform::x11::VideoMode}; +use x11rb::protocol::randr::{self, ConnectionExt as _}; + /// Represents values of `WINIT_HIDPI_FACTOR`. pub enum EnvVarDPI { Randr, @@ -37,43 +36,44 @@ pub fn calc_dpi_factor( impl XConnection { // Retrieve DPI from Xft.dpi property - pub unsafe fn get_xft_dpi(&self) -> Option { - (self.xlib.XrmInitialize)(); - let resource_manager_str = (self.xlib.XResourceManagerString)(self.display); - if resource_manager_str.is_null() { - return None; - } - if let Ok(res) = ::std::ffi::CStr::from_ptr(resource_manager_str).to_str() { - let name: &str = "Xft.dpi:\t"; - for pair in res.split('\n') { - if let Some(stripped) = pair.strip_prefix(name) { - return f64::from_str(stripped).ok(); + pub fn get_xft_dpi(&self) -> Option { + // Try to get it from XSETTINGS first. + if let Some(xsettings_screen) = self.xsettings_screen() { + match self.xsettings_dpi(xsettings_screen) { + Ok(Some(dpi)) => return Some(dpi), + Ok(None) => {} + Err(err) => { + log::warn!("failed to fetch XSettings: {err}"); } } } - None + + self.database() + .get_string("Xft.dpi", "") + .and_then(|s| f64::from_str(s).ok()) } - pub unsafe fn get_output_info( + + pub fn get_output_info( &self, - resources: *mut XRRScreenResources, - crtc: *mut XRRCrtcInfo, + resources: &monitor::ScreenResources, + crtc: &randr::GetCrtcInfoReply, ) -> Option<(String, f64, Vec)> { - let output_info = - (self.xrandr.XRRGetOutputInfo)(self.display, resources, *(*crtc).outputs.offset(0)); - if output_info.is_null() { - // When calling `XRRGetOutputInfo` on a virtual monitor (versus a physical display) - // it's possible for it to return null. - // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=816596 - let _ = self.check_errors(); // discard `BadRROutput` error - return None; - } - - let screen = (self.xlib.XDefaultScreen)(self.display); - let bit_depth = (self.xlib.XDefaultDepth)(self.display, screen); + let output_info = match self + .xcb_connection() + .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME) + .map_err(X11Error::from) + .and_then(|r| r.reply().map_err(X11Error::from)) + { + Ok(output_info) => output_info, + Err(err) => { + warn!("Failed to get output info: {:?}", err); + return None; + } + }; - let output_modes = - slice::from_raw_parts((*output_info).modes, (*output_info).nmode as usize); - let resource_modes = slice::from_raw_parts((*resources).modes, (*resources).nmode as usize); + let bit_depth = self.default_root().root_depth; + let output_modes = &output_info.modes; + let resource_modes = resources.modes(); let modes = resource_modes .iter() @@ -82,7 +82,7 @@ impl XConnection { .filter(|x| output_modes.iter().any(|id| x.id == *id)) .map(|mode| { VideoMode { - size: (mode.width, mode.height), + size: (mode.width.into(), mode.height.into()), refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode) .unwrap_or(0), bit_depth: bit_depth as u16, @@ -94,11 +94,13 @@ impl XConnection { }) .collect(); - let name_slice = slice::from_raw_parts( - (*output_info).name as *mut u8, - (*output_info).nameLen as usize, - ); - let name = String::from_utf8_lossy(name_slice).into(); + let name = match str::from_utf8(&output_info.name) { + Ok(name) => name.to_owned(), + Err(err) => { + warn!("Failed to get output name: {:?}", err); + return None; + } + }; // Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok(); if deprecated_dpi_override.is_some() { @@ -125,8 +127,8 @@ impl XConnection { let scale_factor = match dpi_env { EnvVarDPI::Randr => calc_dpi_factor( - ((*crtc).width, (*crtc).height), - ((*output_info).mm_width as _, (*output_info).mm_height as _), + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), ), EnvVarDPI::Scale(dpi_override) => { if !validate_scale_factor(dpi_override) { @@ -141,74 +143,47 @@ impl XConnection { dpi / 96. } else { calc_dpi_factor( - ((*crtc).width, (*crtc).height), - ((*output_info).mm_width as _, (*output_info).mm_height as _), + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), ) } } }; - (self.xrandr.XRRFreeOutputInfo)(output_info); Some((name, scale_factor, modes)) } - #[must_use] - pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Option<()> { - unsafe { - let mut major = 0; - let mut minor = 0; - (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); - - let root = (self.xlib.XDefaultRootWindow)(self.display); - let resources = if (major == 1 && minor >= 3) || major > 1 { - (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) - } else { - (self.xrandr.XRRGetScreenResources)(self.display, root) - }; - - let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); - let status = (self.xrandr.XRRSetCrtcConfig)( - self.display, - resources, + pub fn set_crtc_config( + &self, + crtc_id: randr::Crtc, + mode_id: randr::Mode, + ) -> Result<(), X11Error> { + let crtc = self + .xcb_connection() + .randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)? + .reply()?; + + self.xcb_connection() + .randr_set_crtc_config( crtc_id, - CurrentTime, - (*crtc).x, - (*crtc).y, + crtc.timestamp, + x11rb::CURRENT_TIME, + crtc.x, + crtc.y, mode_id, - (*crtc).rotation, - (*crtc).outputs.offset(0), - 1, - ); - - (self.xrandr.XRRFreeCrtcInfo)(crtc); - (self.xrandr.XRRFreeScreenResources)(resources); - - if status == Success as i32 { - Some(()) - } else { - None - } - } + crtc.rotation, + &crtc.outputs, + )? + .reply() + .map(|_| ()) + .map_err(Into::into) } - pub fn get_crtc_mode(&self, crtc_id: RRCrtc) -> RRMode { - unsafe { - let mut major = 0; - let mut minor = 0; - (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); - - let root = (self.xlib.XDefaultRootWindow)(self.display); - let resources = if (major == 1 && minor >= 3) || major > 1 { - (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) - } else { - (self.xrandr.XRRGetScreenResources)(self.display, root) - }; - - let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); - let mode = (*crtc).mode; - (self.xrandr.XRRFreeCrtcInfo)(crtc); - (self.xrandr.XRRFreeScreenResources)(resources); - mode - } + pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result { + Ok(self + .xcb_connection() + .randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)? + .reply()? + .mode) } } diff --git a/src/platform_impl/linux/x11/util/window_property.rs b/src/platform_impl/linux/x11/util/window_property.rs index e93d5bb072..d2c506597e 100644 --- a/src/platform_impl/linux/x11/util/window_property.rs +++ b/src/platform_impl/linux/x11/util/window_property.rs @@ -1,18 +1,27 @@ +use std::error::Error; +use std::fmt; +use std::sync::Arc; + +use bytemuck::{NoUninit, Pod}; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyError; + use super::*; -pub type Cardinal = c_long; -pub const CARDINAL_SIZE: usize = mem::size_of::(); +pub const CARDINAL_SIZE: usize = mem::size_of::(); + +pub type Cardinal = u32; #[derive(Debug, Clone)] pub enum GetPropertyError { - XError(XError), - TypeMismatch(ffi::Atom), + X11rbError(Arc), + TypeMismatch(xproto::Atom), FormatMismatch(c_int), - NothingAllocated, } impl GetPropertyError { - pub fn is_actual_property_type(&self, t: ffi::Atom) -> bool { + pub fn is_actual_property_type(&self, t: xproto::Atom) -> bool { if let GetPropertyError::TypeMismatch(actual_type) = *self { actual_type == t } else { @@ -21,122 +30,170 @@ impl GetPropertyError { } } +impl> From for GetPropertyError { + fn from(e: T) -> Self { + Self::X11rbError(Arc::new(e.into())) + } +} + +impl fmt::Display for GetPropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetPropertyError::X11rbError(err) => err.fmt(f), + GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), + GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + } + } +} + +impl Error for GetPropertyError {} + // Number of 32-bit chunks to retrieve per iteration of get_property's inner loop. // To test if `get_property` works correctly, set this to 1. -const PROPERTY_BUFFER_SIZE: c_long = 1024; // 4k of RAM ought to be enough for anyone! - -#[derive(Debug)] -#[allow(dead_code)] -pub enum PropMode { - Replace = ffi::PropModeReplace as isize, - Prepend = ffi::PropModePrepend as isize, - Append = ffi::PropModeAppend as isize, -} +const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone! impl XConnection { - pub fn get_property( + pub fn get_property( &self, - window: c_ulong, - property: ffi::Atom, - property_type: ffi::Atom, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, ) -> Result, GetPropertyError> { - let mut data = Vec::new(); - let mut offset = 0; - - let mut done = false; - let mut actual_type = 0; - let mut actual_format = 0; - let mut quantity_returned = 0; - let mut bytes_after = 0; - let mut buf: *mut c_uchar = ptr::null_mut(); - - while !done { - unsafe { - (self.xlib.XGetWindowProperty)( - self.display, - window, - property, - // This offset is in terms of 32-bit chunks. - offset, - // This is the quantity of 32-bit chunks to receive at once. - PROPERTY_BUFFER_SIZE, - ffi::False, - property_type, - &mut actual_type, - &mut actual_format, - // This is the quantity of items we retrieved in our format, NOT of 32-bit chunks! - &mut quantity_returned, - // ...and this is a quantity of bytes. So, this function deals in 3 different units. - &mut bytes_after, - &mut buf, - ); - - if let Err(e) = self.check_errors() { - return Err(GetPropertyError::XError(e)); - } - - if actual_type != property_type { - return Err(GetPropertyError::TypeMismatch(actual_type)); - } - - let format_mismatch = Format::from_format(actual_format as _) != Some(T::FORMAT); - if format_mismatch { - return Err(GetPropertyError::FormatMismatch(actual_format)); - } - - if !buf.is_null() { - offset += PROPERTY_BUFFER_SIZE; - let new_data = - std::slice::from_raw_parts(buf as *mut T, quantity_returned as usize); - /*println!( - "XGetWindowProperty prop:{:?} fmt:{:02} len:{:02} off:{:02} out:{:02}, buf:{:?}", - property, - mem::size_of::() * 8, - data.len(), - offset, - quantity_returned, - new_data, - );*/ - data.extend_from_slice(new_data); - // Fun fact: XGetWindowProperty allocates one extra byte at the end. - (self.xlib.XFree)(buf as _); // Don't try to access new_data after this. - } else { - return Err(GetPropertyError::NothingAllocated); - } - - done = bytes_after == 0; + let mut iter = PropIterator::new(self.xcb_connection(), window, property, property_type); + let mut data = vec![]; + + loop { + if !iter.next_window(&mut data)? { + break; } } Ok(data) } - pub fn change_property<'a, T: Formattable>( + pub fn change_property<'a, T: NoUninit>( &'a self, - window: c_ulong, - property: ffi::Atom, - property_type: ffi::Atom, - mode: PropMode, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + mode: xproto::PropMode, new_value: &[T], - ) -> Flusher<'a> { - debug_assert_eq!(mem::size_of::(), T::FORMAT.get_actual_size()); - unsafe { - (self.xlib.XChangeProperty)( - self.display, + ) -> Result, X11Error> { + assert!([1usize, 2, 4].contains(&mem::size_of::())); + self.xcb_connection() + .change_property( + mode, window, property, property_type, - T::FORMAT as c_int, - mode as c_int, - new_value.as_ptr() as *const c_uchar, - new_value.len() as c_int, - ); - } - /*println!( - "XChangeProperty prop:{:?} val:{:?}", + (mem::size_of::() * 8) as u8, + new_value + .len() + .try_into() + .expect("too many items for propery"), + bytemuck::cast_slice::(new_value), + ) + .map_err(Into::into) + } +} + +/// An iterator over the "windows" of the property that we are fetching. +struct PropIterator<'a, C: ?Sized, T> { + /// Handle to the connection. + conn: &'a C, + + /// The window that we're fetching the property from. + window: xproto::Window, + + /// The property that we're fetching. + property: xproto::Atom, + + /// The type of the property that we're fetching. + property_type: xproto::Atom, + + /// The offset of the next window, in 32-bit chunks. + offset: u32, + + /// The format of the type. + format: u8, + + /// Keep a reference to `T`. + _phantom: std::marker::PhantomData, +} + +impl<'a, C: Connection + ?Sized, T: Pod> PropIterator<'a, C, T> { + /// Create a new property iterator. + fn new( + conn: &'a C, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Self { + let format = match mem::size_of::() { + 1 => 8, + 2 => 16, + 4 => 32, + _ => unreachable!(), + }; + + Self { + conn, + window, property, - new_value, - );*/ - Flusher::new(self) + property_type, + offset: 0, + format, + _phantom: Default::default(), + } + } + + /// Get the next window and append it to `data`. + /// + /// Returns whether there are more windows to fetch. + fn next_window(&mut self, data: &mut Vec) -> Result { + // Send the request and wait for the reply. + let reply = self + .conn + .get_property( + false, + self.window, + self.property, + self.property_type, + self.offset, + PROPERTY_BUFFER_SIZE, + )? + .reply()?; + + // Make sure that the reply is of the correct type. + if reply.type_ != self.property_type { + return Err(GetPropertyError::TypeMismatch(reply.type_)); + } + + // Make sure that the reply is of the correct format. + if reply.format != self.format { + return Err(GetPropertyError::FormatMismatch(reply.format.into())); + } + + // Append the data to the output. + if mem::size_of::() == 1 && mem::align_of::() == 1 { + // We can just do a bytewise append. + data.extend_from_slice(bytemuck::cast_slice(&reply.value)); + } else { + // Rust's borrowing and types system makes this a bit tricky. + // + // We need to make sure that the data is properly aligned. Unfortunately the best + // safe way to do this is to copy the data to another buffer and then append. + // + // TODO(notgull): It may be worth it to use `unsafe` to copy directly from + // `reply.value` to `data`; check if this is faster. Use benchmarks! + let old_len = data.len(); + let added_len = reply.value.len() / mem::size_of::(); + data.resize(old_len + added_len, T::zeroed()); + bytemuck::cast_slice_mut::(&mut data[old_len..]).copy_from_slice(&reply.value); + } + + // Check `bytes_after` to see if there are more windows to fetch. + self.offset += PROPERTY_BUFFER_SIZE; + Ok(reply.bytes_after != 0) } } diff --git a/src/platform_impl/linux/x11/util/wm.rs b/src/platform_impl/linux/x11/util/wm.rs index 03890facc8..43a800dd73 100644 --- a/src/platform_impl/linux/x11/util/wm.rs +++ b/src/platform_impl/linux/x11/util/wm.rs @@ -16,11 +16,11 @@ pub const MOVERESIZE_LEFT: isize = 7; pub const MOVERESIZE_MOVE: isize = 8; // This info is global to the window manager. -static SUPPORTED_HINTS: Lazy>> = +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 { +pub fn hint_is_supported(hint: xproto::Atom) -> bool { (*SUPPORTED_HINTS.lock().unwrap()).contains(&hint) } @@ -33,20 +33,27 @@ pub fn wm_name_is_one_of(names: &[&str]) -> bool { } impl XConnection { - pub fn update_cached_wm_info(&self, root: ffi::Window) { + pub fn update_cached_wm_info(&self, root: xproto::Window) { *SUPPORTED_HINTS.lock().unwrap() = self.get_supported_hints(root); *WM_NAME.lock().unwrap() = self.get_wm_name(root); } - fn get_supported_hints(&self, root: ffi::Window) -> Vec { - let supported_atom = unsafe { self.get_atom_unchecked(b"_NET_SUPPORTED\0") }; - self.get_property(root, supported_atom, ffi::XA_ATOM) - .unwrap_or_else(|_| Vec::with_capacity(0)) + fn get_supported_hints(&self, root: xproto::Window) -> Vec { + let atoms = self.atoms(); + let supported_atom = atoms[_NET_SUPPORTED]; + self.get_property( + root, + supported_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .unwrap_or_else(|_| Vec::with_capacity(0)) } - fn get_wm_name(&self, root: ffi::Window) -> Option { - let check_atom = unsafe { self.get_atom_unchecked(b"_NET_SUPPORTING_WM_CHECK\0") }; - let wm_name_atom = unsafe { self.get_atom_unchecked(b"_NET_WM_NAME\0") }; + #[allow(clippy::useless_conversion)] + fn get_wm_name(&self, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let check_atom = atoms[_NET_SUPPORTING_WM_CHECK]; + let wm_name_atom = atoms[_NET_WM_NAME]; // Mutter/Muffin/Budgie doesn't have _NET_SUPPORTING_WM_CHECK in its _NET_SUPPORTED, despite // it working and being supported. This has been reported upstream, but due to the @@ -70,7 +77,11 @@ impl XConnection { // Querying this property on the root window will give us the ID of a child window created by // the WM. let root_window_wm_check = { - let result = self.get_property(root, check_atom, ffi::XA_WINDOW); + let result = self.get_property::( + root, + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); @@ -80,7 +91,11 @@ impl XConnection { // Querying the same property on the child window we were given, we should get this child // window's ID again. let child_window_wm_check = { - let result = self.get_property(root_window_wm_check, check_atom, ffi::XA_WINDOW); + let result = self.get_property::( + root_window_wm_check.into(), + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); @@ -94,9 +109,11 @@ impl XConnection { // All of that work gives us a window ID that we can get the WM name from. let wm_name = { - let utf8_string_atom = unsafe { self.get_atom_unchecked(b"UTF8_STRING\0") }; + let atoms = self.atoms(); + let utf8_string_atom = atoms[UTF8_STRING]; - let result = self.get_property(root_window_wm_check, wm_name_atom, utf8_string_atom); + let result = + self.get_property(root_window_wm_check.into(), wm_name_atom, utf8_string_atom); // IceWM requires this. IceWM was also the only WM tested that returns a null-terminated // string. For more fun trivia, IceWM is also unique in including version and uname @@ -105,13 +122,17 @@ impl XConnection { // The unofficial 1.4 fork of IceWM still includes the extra details, but properly // returns a UTF8 string that isn't null-terminated. let no_utf8 = if let Err(ref err) = result { - err.is_actual_property_type(ffi::XA_STRING) + err.is_actual_property_type(xproto::Atom::from(xproto::AtomEnum::STRING)) } else { false }; if no_utf8 { - self.get_property(root_window_wm_check, wm_name_atom, ffi::XA_STRING) + self.get_property( + root_window_wm_check.into(), + wm_name_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + ) } else { result } diff --git a/src/platform_impl/linux/x11/util/xmodmap.rs b/src/platform_impl/linux/x11/util/xmodmap.rs new file mode 100644 index 0000000000..523a582970 --- /dev/null +++ b/src/platform_impl/linux/x11/util/xmodmap.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::slice; + +use x11_dl::xlib::{KeyCode as XKeyCode, XModifierKeymap}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const NUM_MODS: usize = 8; + +/// Track which keys are modifiers, so we can properly replay them when they were filtered. +#[derive(Debug, Default)] +pub struct ModifierKeymap { + // Maps keycodes to modifiers + modifers: HashSet, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn is_modifier(&self, keycode: XKeyCode) -> bool { + self.modifers.contains(&keycode) + } + + pub fn reload_from_x_connection(&mut self, xconn: &super::XConnection) { + unsafe { + let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + + if keymap.is_null() { + return; + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + fn reset_from_x_keymap(&mut self, keymap: &XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + self.modifers.clear(); + for key in keys { + self.modifers.insert(*key); + } + } +} diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index e64d41789c..9197b4f00f 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1,35 +1,47 @@ use std::{ cmp, env, ffi::CString, - mem::{self, replace, MaybeUninit}, + mem::replace, os::raw::*, path::Path, - ptr, slice, sync::{Arc, Mutex, MutexGuard}, }; -use libc; -use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XlibDisplayHandle, XlibWindowHandle}; -use x11_dl::xlib::{TrueColor, XID}; +use x11rb::{ + connection::Connection, + properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}, + protocol::{ + randr, + shape::SK, + xfixes::{ConnectionExt, RegionWrapper}, + xinput, + xproto::{self, ConnectionExt as _, Rectangle}, + }, +}; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::{Event, InnerSizeWriter, WindowEvent}, + event_loop::AsyncRequestSerial, platform_impl::{ - x11::{ime::ImeContextCreationError, MonitorHandle as X11MonitorHandle}, - Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, + x11::{ + atoms::*, xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender, + X11Error, + }, + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, window::{ - CursorGrabMode, CursorIcon, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, }, }; use super::{ - ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WindowId, XConnection, XError, + ffi, util, CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId, + XConnection, }; -use calloop::channel::Sender; #[derive(Debug)] pub struct SharedState { @@ -48,7 +60,7 @@ pub struct SharedState { // Used to restore position after exiting fullscreen pub restore_position: Option<(i32, i32)>, // Used to restore video mode after exiting fullscreen - pub desktop_video_mode: Option<(ffi::RRCrtc, ffi::RRMode)>, + pub desktop_video_mode: Option<(randr::Crtc, randr::Mode)>, pub frame_extents: Option, pub min_inner_size: Option, pub max_inner_size: Option, @@ -56,6 +68,8 @@ pub struct SharedState { pub base_size: Option, pub visibility: Visibility, pub has_focus: bool, + // Use `Option` to not apply hittest logic when it was never requested. + pub cursor_hittest: Option, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -96,6 +110,7 @@ impl SharedState { resize_increments: None, base_size: None, has_focus: false, + cursor_hittest: None, }) } } @@ -103,35 +118,53 @@ impl SharedState { unsafe impl Send for UnownedWindow {} unsafe impl Sync for UnownedWindow {} -pub(crate) struct UnownedWindow { +pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - xwindow: ffi::Window, // never changes - root: ffi::Window, // never changes - screen_id: i32, // never changes + xwindow: xproto::Window, // never changes + #[allow(dead_code)] + visual: u32, // never changes + root: xproto::Window, // never changes + #[allow(dead_code)] + screen_id: i32, // never changes cursor: Mutex, cursor_grabbed_mode: Mutex, #[allow(clippy::mutex_atomic)] cursor_visible: Mutex, ime_sender: Mutex, pub shared_state: Mutex, - redraw_sender: Sender, + redraw_sender: WakeSender, + activation_sender: WakeSender, +} + +macro_rules! leap { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(err) => return Err(os_error!(OsError::XError(X11Error::from(err).into()))), + } + }; } impl UnownedWindow { + #[allow(clippy::unnecessary_cast)] pub(crate) fn new( event_loop: &EventLoopWindowTarget, window_attrs: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, ) -> Result { let xconn = &event_loop.xconn; - let root = match window_attrs.parent_window { - Some(RawWindowHandle::Xlib(handle)) => handle.window, - Some(RawWindowHandle::Xcb(handle)) => handle.window as XID, + let atoms = xconn.atoms(); + #[cfg(feature = "rwh_06")] + let root = match window_attrs.parent_window.0 { + Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window, + Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"), None => event_loop.root, }; + #[cfg(not(feature = "rwh_06"))] + let root = event_loop.root; - let mut monitors = xconn.available_monitors(); + let mut monitors = leap!(xconn.available_monitors()); let guessed_monitor = if monitors.is_empty() { X11MonitorHandle::dummy() } else { @@ -190,99 +223,136 @@ impl UnownedWindow { dimensions }; - let screen_id = match pl_attribs.screen_id { + let screen_id = match pl_attribs.x11.screen_id { Some(id) => id, - None => unsafe { (xconn.xlib.XDefaultScreen)(xconn.display) }, + None => xconn.default_screen_index() as c_int, }; + // An iterator over all of the visuals combined with their depths. + let mut all_visuals = xconn + .xcb_connection() + .setup() + .roots + .iter() + .flat_map(|root| &root.allowed_depths) + .flat_map(|depth| { + depth + .visuals + .iter() + .map(move |visual| (visual, depth.depth)) + }); + // creating - let (visual, depth, require_colormap) = match pl_attribs.visual_infos { - Some(vi) => (vi.visual, vi.depth, false), + let (visualtype, depth, require_colormap) = match pl_attribs.x11.visual_id { + Some(vi) => { + // Find this specific visual. + let (visualtype, depth) = all_visuals + .find(|(visual, _)| visual.visual_id == vi) + .ok_or_else(|| os_error!(OsError::XError(X11Error::NoSuchVisual(vi).into())))?; + + (Some(visualtype), depth, true) + } None if window_attrs.transparent => { - // Find a suitable visual - let mut vinfo = MaybeUninit::uninit(); - let vinfo_initialized = unsafe { - (xconn.xlib.XMatchVisualInfo)( - xconn.display, - screen_id, - 32, - TrueColor, - vinfo.as_mut_ptr(), - ) != 0 - }; - if vinfo_initialized { - let vinfo = unsafe { vinfo.assume_init() }; - (vinfo.visual, vinfo.depth, true) - } else { - debug!("Could not set transparency, because XMatchVisualInfo returned zero for the required parameters"); - ( - ffi::CopyFromParent as *mut ffi::Visual, - ffi::CopyFromParent, - false, - ) - } + // Find a suitable visual, true color with 32 bits of depth. + all_visuals + .find_map(|(visual, depth)| { + (depth == 32 && visual.class == xproto::VisualClass::TRUE_COLOR) + .then_some((Some(visual), depth, true)) + }) + .unwrap_or_else(|| { + debug!("Could not set transparency, because XMatchVisualInfo returned zero for the required parameters"); + (None as _, x11rb::COPY_FROM_PARENT as _, false) + }) } - _ => ( - ffi::CopyFromParent as *mut ffi::Visual, - ffi::CopyFromParent, - false, - ), + _ => (None, x11rb::COPY_FROM_PARENT as _, false), }; + let mut visual = visualtype.map_or(x11rb::COPY_FROM_PARENT, |v| v.visual_id); + + let window_attributes = { + use xproto::EventMask; + + let mut aux = xproto::CreateWindowAux::new(); + let event_mask = EventMask::EXPOSURE + | EventMask::STRUCTURE_NOTIFY + | EventMask::VISIBILITY_CHANGE + | EventMask::KEY_PRESS + | EventMask::KEY_RELEASE + | EventMask::KEYMAP_STATE + | EventMask::BUTTON_PRESS + | EventMask::BUTTON_RELEASE + | EventMask::POINTER_MOTION + | EventMask::PROPERTY_CHANGE; + + aux = aux.event_mask(event_mask).border_pixel(0); + + if pl_attribs.x11.override_redirect { + aux = aux.override_redirect(true as u32); + } - let mut set_win_attr = { - let mut swa: ffi::XSetWindowAttributes = unsafe { mem::zeroed() }; - swa.colormap = if let Some(vi) = pl_attribs.visual_infos { - unsafe { - let visual = vi.visual; - (xconn.xlib.XCreateColormap)(xconn.display, root, visual, ffi::AllocNone) - } - } else if require_colormap { - unsafe { (xconn.xlib.XCreateColormap)(xconn.display, root, visual, ffi::AllocNone) } - } else { - 0 + // Add a colormap if needed. + let colormap_visual = match pl_attribs.x11.visual_id { + Some(vi) => Some(vi), + None if require_colormap => Some(visual), + _ => None, }; - swa.event_mask = ffi::ExposureMask - | ffi::StructureNotifyMask - | ffi::VisibilityChangeMask - | ffi::KeyPressMask - | ffi::KeyReleaseMask - | ffi::KeymapStateMask - | ffi::ButtonPressMask - | ffi::ButtonReleaseMask - | ffi::PointerMotionMask; - swa.border_pixel = 0; - swa.override_redirect = pl_attribs.override_redirect as c_int; - swa - }; - let mut window_attributes = ffi::CWBorderPixel | ffi::CWColormap | ffi::CWEventMask; + if let Some(visual) = colormap_visual { + let colormap = leap!(xconn.xcb_connection().generate_id()); + leap!(xconn.xcb_connection().create_colormap( + xproto::ColormapAlloc::NONE, + colormap, + root, + visual, + )); + aux = aux.colormap(colormap); + } else { + aux = aux.colormap(0); + } - if pl_attribs.override_redirect { - window_attributes |= ffi::CWOverrideRedirect; - } + aux + }; + + // Figure out the window's parent. + let parent = pl_attribs.x11.embed_window.unwrap_or(root); // finally creating the window - let xwindow = unsafe { - (xconn.xlib.XCreateWindow)( - xconn.display, - root, - position.map_or(0, |p: PhysicalPosition| p.x as c_int), - position.map_or(0, |p: PhysicalPosition| p.y as c_int), - dimensions.0 as c_uint, - dimensions.1 as c_uint, - 0, + let xwindow = { + let (x, y) = position.map_or((0, 0), Into::into); + let wid = leap!(xconn.xcb_connection().generate_id()); + let result = xconn.xcb_connection().create_window( depth, - ffi::InputOutput as c_uint, + wid, + parent, + x, + y, + dimensions.0.try_into().unwrap(), + dimensions.1.try_into().unwrap(), + 0, + xproto::WindowClass::INPUT_OUTPUT, visual, - window_attributes, - &mut set_win_attr, - ) + &window_attributes, + ); + leap!(leap!(result).check()); + + wid }; + // The COPY_FROM_PARENT is a special value for the visual used to copy + // the visual from the parent window, thus we have to query the visual + // we've got when we built the window above. + if visual == x11rb::COPY_FROM_PARENT { + visual = leap!(leap!(xconn + .xcb_connection() + .get_window_attributes(xwindow as xproto::Window)) + .reply()) + .visual; + } + #[allow(clippy::mutex_atomic)] let mut window = UnownedWindow { xconn: Arc::clone(xconn), - xwindow, + xwindow: xwindow as xproto::Window, + visual, root, screen_id, cursor: Default::default(), @@ -291,141 +361,155 @@ impl UnownedWindow { ime_sender: Mutex::new(event_loop.ime_sender.clone()), shared_state: SharedState::new(guessed_monitor, &window_attrs), redraw_sender: event_loop.redraw_sender.clone(), + activation_sender: event_loop.activation_sender.clone(), }; // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window // title to determine placement/etc., so doing this after mapping would cause the WM to // act on the wrong title state. - window.set_title_inner(&window_attrs.title).queue(); - window - .set_decorations_inner(window_attrs.decorations) - .queue(); + leap!(window.set_title_inner(&window_attrs.title)).ignore_error(); + leap!(window.set_decorations_inner(window_attrs.decorations)).ignore_error(); if let Some(theme) = window_attrs.preferred_theme { - window.set_theme_inner(Some(theme)).queue(); + leap!(window.set_theme_inner(Some(theme))).ignore_error(); + } + + // Embed the window if needed. + if pl_attribs.x11.embed_window.is_some() { + window.embed_window()?; } { // Enable drag and drop (TODO: extend API to make this toggleable) - unsafe { - let dnd_aware_atom = xconn.get_atom_unchecked(b"XdndAware\0"); - let version = &[5 as c_ulong]; // Latest version; hasn't changed since 2002 - xconn.change_property( + { + let dnd_aware_atom = atoms[XdndAware]; + let version = &[5u32]; // Latest version; hasn't changed since 2002 + leap!(xconn.change_property( window.xwindow, dnd_aware_atom, - ffi::XA_ATOM, - util::PropMode::Replace, + u32::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, version, - ) + )) + .ignore_error(); } - .queue(); // WM_CLASS must be set *before* mapping the window, as per ICCCM! { - 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(name.general.as_str()) - .expect("`WM_CLASS` class contained null byte"); - (instance, class) + let (instance, class) = if let Some(name) = pl_attribs.name { + (name.instance, name.general) } else { - let class = env::args() + let class = env::args_os() .next() .as_ref() // Default to the name of the binary (via argv[0]) .and_then(|path| Path::new(path).file_name()) .and_then(|bin_name| bin_name.to_str()) .map(|bin_name| bin_name.to_owned()) - .or_else(|| Some(window_attrs.title.clone())) - .and_then(|string| CString::new(string.as_str()).ok()) - .expect("Default `WM_CLASS` class contained null byte"); + .unwrap_or_else(|| window_attrs.title.clone()); // This environment variable is extraordinarily unlikely to actually be used... let instance = env::var("RESOURCE_NAME") .ok() - .and_then(|instance| CString::new(instance.as_str()).ok()) - .or_else(|| Some(class.clone())) - .expect("Default `WM_CLASS` instance contained null byte"); + .unwrap_or_else(|| class.clone()); (instance, class) }; - let mut class_hint = xconn.alloc_class_hint(); - class_hint.res_name = class.as_ptr() as *mut c_char; - class_hint.res_class = instance.as_ptr() as *mut c_char; - - unsafe { - (xconn.xlib.XSetClassHint)(xconn.display, window.xwindow, class_hint.ptr); - } //.queue(); + let class = format!("{instance}\0{class}\0"); + leap!(xconn.change_property( + window.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_CLASS), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + class.as_bytes(), + )) + .ignore_error(); } - if let Some(flusher) = window.set_pid() { - flusher.queue() + if let Some(flusher) = leap!(window.set_pid()) { + flusher.ignore_error() } - window.set_window_types(pl_attribs.x11_window_types).queue(); + leap!(window.set_window_types(pl_attribs.x11.x11_window_types)).ignore_error(); - // set size hints - { - let mut min_inner_size = window_attrs - .min_inner_size - .map(|size| size.to_physical::(scale_factor)); - let mut max_inner_size = window_attrs - .max_inner_size - .map(|size| size.to_physical::(scale_factor)); - - if !window_attrs.resizable { - if util::wm_name_is_one_of(&["Xfwm4"]) { - warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); - } else { - max_inner_size = Some(dimensions.into()); - min_inner_size = Some(dimensions.into()); - } - } + // Set size hints. + let mut min_inner_size = window_attrs + .min_inner_size + .map(|size| size.to_physical::(scale_factor)); + let mut max_inner_size = window_attrs + .max_inner_size + .map(|size| size.to_physical::(scale_factor)); - let shared_state = window.shared_state.get_mut().unwrap(); - 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 = window_attrs.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)); - normal_hints.set_min_size(min_inner_size.map(Into::into)); - normal_hints.set_max_size(max_inner_size.map(Into::into)); - normal_hints.set_resize_increments( - window_attrs - .resize_increments - .map(|size| size.to_physical::(scale_factor).into()), - ); - normal_hints.set_base_size( - pl_attribs - .base_size - .map(|size| size.to_physical::(scale_factor).into()), - ); - xconn.set_normal_hints(window.xwindow, normal_hints).queue(); + if !window_attrs.resizable { + if util::wm_name_is_one_of(&["Xfwm4"]) { + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + } else { + max_inner_size = Some(dimensions.into()); + min_inner_size = Some(dimensions.into()); + } } + let shared_state = window.shared_state.get_mut().unwrap(); + 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 = window_attrs.resize_increments; + shared_state.base_size = pl_attribs.x11.base_size; + + let normal_hints = WmSizeHints { + position: position.map(|PhysicalPosition { x, y }| { + (WmSizeHintsSpecification::UserSpecified, x, y) + }), + size: Some(( + WmSizeHintsSpecification::UserSpecified, + cast_dimension_to_hint(dimensions.0), + cast_dimension_to_hint(dimensions.1), + )), + max_size: max_inner_size.map(cast_physical_size_to_hint), + min_size: min_inner_size.map(cast_physical_size_to_hint), + size_increment: window_attrs + .resize_increments + .map(|size| cast_size_to_hint(size, scale_factor)), + base_size: pl_attribs + .x11 + .base_size + .map(|size| cast_size_to_hint(size, scale_factor)), + aspect: None, + win_gravity: None, + }; + leap!(leap!(normal_hints.set( + xconn.xcb_connection(), + window.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )) + .check()); + // Set window icons if let Some(icon) = window_attrs.window_icon { - window.set_icon_inner(icon).queue(); + leap!(window.set_icon_inner(icon.inner)).ignore_error(); } // Opt into handling window close - unsafe { - (xconn.xlib.XSetWMProtocols)( - xconn.display, - window.xwindow, - &[event_loop.wm_delete_window, event_loop.net_wm_ping] as *const ffi::Atom - as *mut ffi::Atom, - 2, - ); - } //.queue(); + let result = xconn.xcb_connection().change_property( + xproto::PropMode::REPLACE, + window.xwindow, + atoms[WM_PROTOCOLS], + xproto::AtomEnum::ATOM, + 32, + 2, + bytemuck::cast_slice::(&[ + atoms[WM_DELETE_WINDOW], + atoms[_NET_WM_PING], + ]), + ); + leap!(result).ignore_error(); // Set visibility (map window) if window_attrs.visible { - unsafe { - (xconn.xlib.XMapRaised)(xconn.display, window.xwindow); - } //.queue(); + leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); + leap!(xconn.xcb_connection().configure_window( + xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE) + )) + .ignore_error(); } // Attempt to make keyboard input repeat detectable @@ -437,52 +521,44 @@ impl UnownedWindow { &mut supported_ptr, ); if supported_ptr == ffi::False { - return Err(os_error!(OsError::XMisc( + return Err(os_error!(OsError::Misc( "`XkbSetDetectableAutoRepeat` failed" ))); } } // Select XInput2 events - let mask = ffi::XI_MotionMask - | ffi::XI_ButtonPressMask - | ffi::XI_ButtonReleaseMask - | ffi::XI_EnterMask - | ffi::XI_LeaveMask - | ffi::XI_FocusInMask - | ffi::XI_FocusOutMask - | ffi::XI_TouchBeginMask - | ffi::XI_TouchUpdateMask - | ffi::XI_TouchEndMask; - xconn - .select_xinput_events(window.xwindow, ffi::XIAllMasterDevices, mask) - .queue(); - - { - let result = event_loop - .ime + let mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + | xinput::XIEventMask::TOUCH_BEGIN + | xinput::XIEventMask::TOUCH_UPDATE + | xinput::XIEventMask::TOUCH_END; + leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) + .ignore_error(); + + // Try to create input context for the window. + if let Some(ime) = event_loop.ime.as_ref() { + let result = ime .borrow_mut() - .create_context(window.xwindow, false); - if let Err(err) = result { - let e = match err { - ImeContextCreationError::XError(err) => OsError::XError(err), - ImeContextCreationError::Null => { - OsError::XMisc("IME Context creation failed") - } - }; - return Err(os_error!(e)); - } + .create_context(window.xwindow as ffi::Window, false); + leap!(result); } // These properties must be set after mapping if window_attrs.maximized { - window.set_maximized_inner(window_attrs.maximized).queue(); + leap!(window.set_maximized_inner(window_attrs.maximized)).ignore_error(); } - if window_attrs.fullscreen.is_some() { + if window_attrs.fullscreen.0.is_some() { if let Some(flusher) = - window.set_fullscreen_inner(window_attrs.fullscreen.clone().map(Into::into)) + leap!(window + .set_fullscreen_inner(window_attrs.fullscreen.0.clone().map(Into::into))) { - flusher.queue() + flusher.ignore_error() } if let Some(PhysicalPosition { x, y }) = position { @@ -492,64 +568,73 @@ impl UnownedWindow { } } - window - .set_window_level_inner(window_attrs.window_level) - .queue(); + leap!(window.set_window_level_inner(window_attrs.window_level)).ignore_error(); + } + + // Remove the startup notification if we have one. + if let Some(startup) = pl_attribs.activation_token.as_ref() { + leap!(xconn.remove_activation_token(xwindow, &startup._token)); } // We never want to give the user a broken window, since by then, it's too late to handle. - xconn - .sync_with_server() - .map(|_| window) - .map_err(|x_err| os_error!(OsError::XError(x_err))) + let window = leap!(xconn.sync_with_server().map(|_| window)); + + Ok(window) + } + + /// Embed this window into a parent window. + pub(super) fn embed_window(&self) -> Result<(), RootOsError> { + let atoms = self.xconn.atoms(); + leap!(leap!(self.xconn.change_property( + self.xwindow, + atoms[_XEMBED], + atoms[_XEMBED], + xproto::PropMode::REPLACE, + &[0u32, 1u32], + )) + .check()); + + Ok(()) } pub(super) fn shared_state_lock(&self) -> MutexGuard<'_, SharedState> { self.shared_state.lock().unwrap() } - fn set_pid(&self) -> Option> { - let pid_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_PID\0") }; - let client_machine_atom = unsafe { self.xconn.get_atom_unchecked(b"WM_CLIENT_MACHINE\0") }; - unsafe { - // 64 would suffice for Linux, but 256 will be enough everywhere (as per SUSv2). For instance, this is - // the limit defined by OpenBSD. - const MAXHOSTNAMELEN: usize = 256; - // `assume_init` is safe here because the array consists of `MaybeUninit` values, - // which do not require initialization. - let mut buffer: [MaybeUninit; MAXHOSTNAMELEN] = - MaybeUninit::uninit().assume_init(); - let status = libc::gethostname(buffer.as_mut_ptr() as *mut c_char, buffer.len()); - if status != 0 { - return None; - } - ptr::write(buffer[MAXHOSTNAMELEN - 1].as_mut_ptr() as *mut u8, b'\0'); // a little extra safety - let hostname_length = libc::strlen(buffer.as_ptr() as *const c_char); + fn set_pid(&self) -> Result>, X11Error> { + let atoms = self.xconn.atoms(); + let pid_atom = atoms[_NET_WM_PID]; + let client_machine_atom = atoms[WM_CLIENT_MACHINE]; - let hostname = slice::from_raw_parts(buffer.as_ptr() as *const c_char, hostname_length); + // Get the hostname and the PID. + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); - self.xconn - .change_property( - self.xwindow, - pid_atom, - ffi::XA_CARDINAL, - util::PropMode::Replace, - &[libc::getpid() as util::Cardinal], - ) - .queue(); - let flusher = self.xconn.change_property( + self.xconn + .change_property( self.xwindow, - client_machine_atom, - ffi::XA_STRING, - util::PropMode::Replace, - &hostname[0..hostname_length], - ); - Some(flusher) - } + pid_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &[pid.as_raw_nonzero().get() as util::Cardinal], + )? + .ignore_error(); + let flusher = self.xconn.change_property( + self.xwindow, + client_machine_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + uname.nodename().to_bytes(), + ); + flusher.map(Some) } - fn set_window_types(&self, window_types: Vec) -> util::Flusher<'_> { - let hint_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_WINDOW_TYPE\0") }; + fn set_window_types( + &self, + window_types: Vec, + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_NET_WM_WINDOW_TYPE]; let atoms: Vec<_> = window_types .iter() .map(|t| t.as_atom(&self.xconn)) @@ -558,15 +643,16 @@ impl UnownedWindow { self.xconn.change_property( self.xwindow, hint_atom, - ffi::XA_ATOM, - util::PropMode::Replace, + xproto::Atom::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, &atoms, ) } - pub fn set_theme_inner(&self, theme: Option) -> util::Flusher<'_> { - let hint_atom = unsafe { self.xconn.get_atom_unchecked(b"_GTK_THEME_VARIANT\0") }; - let utf8_atom = unsafe { self.xconn.get_atom_unchecked(b"UTF8_STRING\0") }; + pub fn set_theme_inner(&self, theme: Option) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_GTK_THEME_VARIANT]; + let utf8_atom = atoms[UTF8_STRING]; let variant = match theme { Some(Theme::Dark) => "dark", Some(Theme::Light) => "light", @@ -577,7 +663,7 @@ impl UnownedWindow { self.xwindow, hint_atom, utf8_atom, - util::PropMode::Replace, + xproto::PropMode::REPLACE, variant.as_bytes(), ) } @@ -585,23 +671,28 @@ impl UnownedWindow { #[inline] pub fn set_theme(&self, theme: Option) { self.set_theme_inner(theme) - .flush() .expect("Failed to change window theme") + .ignore_error(); + + self.xconn + .flush_requests() + .expect("Failed to change window theme"); } fn set_netwm( &self, operation: util::StateOperation, - properties: (c_long, c_long, c_long, c_long), - ) -> util::Flusher<'_> { - let state_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE\0") }; + properties: (u32, u32, u32, u32), + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; self.xconn.send_client_msg( self.xwindow, self.root, state_atom, - Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), + Some(xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY), [ - operation as c_long, + operation as u32, properties.0, properties.1, properties.2, @@ -610,42 +701,45 @@ impl UnownedWindow { ) } - fn set_fullscreen_hint(&self, fullscreen: bool) -> util::Flusher<'_> { - let fullscreen_atom = - unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE_FULLSCREEN\0") }; - let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom as c_long, 0, 0, 0)); + fn set_fullscreen_hint(&self, fullscreen: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let fullscreen_atom = atoms[_NET_WM_STATE_FULLSCREEN]; + let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom, 0, 0, 0)); if fullscreen { // Ensure that the fullscreen window receives input focus to prevent // locking up the user's display. - unsafe { - (self.xconn.xlib.XSetInputFocus)( - self.xconn.display, + self.xconn + .xcb_connection() + .set_input_focus( + xproto::InputFocus::PARENT, self.xwindow, - ffi::RevertToParent, - ffi::CurrentTime, - ); - } + x11rb::CURRENT_TIME, + )? + .ignore_error(); } flusher } - fn set_fullscreen_inner(&self, fullscreen: Option) -> Option> { + fn set_fullscreen_inner( + &self, + fullscreen: Option, + ) -> Result>, X11Error> { let mut shared_state_lock = self.shared_state_lock(); match shared_state_lock.visibility { // Setting fullscreen on a window that is not visible will generate an error. Visibility::No | Visibility::YesWait => { shared_state_lock.desired_fullscreen = Some(fullscreen); - return None; + return Ok(None); } Visibility::Yes => (), } let old_fullscreen = shared_state_lock.fullscreen.clone(); if old_fullscreen == fullscreen { - return None; + return Ok(None); } shared_state_lock.fullscreen = fullscreen.clone(); @@ -660,8 +754,12 @@ impl UnownedWindow { &Some(Fullscreen::Exclusive(PlatformVideoMode::X(ref video_mode))), ) => { let monitor = video_mode.monitor.as_ref().unwrap(); - shared_state_lock.desktop_video_mode = - Some((monitor.id, self.xconn.get_crtc_mode(monitor.id))); + shared_state_lock.desktop_video_mode = Some(( + monitor.id, + self.xconn + .get_crtc_mode(monitor.id) + .expect("Failed to get desktop video mode"), + )); } // Restore desktop video mode upon exiting exclusive fullscreen (&Some(Fullscreen::Exclusive(_)), &None) @@ -682,9 +780,10 @@ impl UnownedWindow { let mut shared_state_lock = self.shared_state_lock(); if let Some(position) = shared_state_lock.restore_position.take() { drop(shared_state_lock); - self.set_position_inner(position.0, position.1).queue(); + self.set_position_inner(position.0, position.1) + .expect_then_ignore_error("Failed to restore window position"); } - Some(flusher) + flusher.map(Some) } Some(fullscreen) => { let (video_mode, monitor) = match fullscreen { @@ -694,14 +793,16 @@ impl UnownedWindow { Fullscreen::Borderless(Some(PlatformMonitorHandle::X(monitor))) => { (None, monitor) } - Fullscreen::Borderless(None) => (None, self.current_monitor()), + Fullscreen::Borderless(None) => { + (None, self.shared_state_lock().last_monitor.clone()) + } #[cfg(wayland_platform)] _ => unreachable!(), }; // Don't set fullscreen on an invalid dummy monitor handle if monitor.is_dummy() { - return None; + return Ok(None); } if let Some(video_mode) = video_mode { @@ -739,8 +840,8 @@ impl UnownedWindow { self.shared_state_lock().restore_position = Some(window_position); let monitor_origin: (i32, i32) = monitor.position().into(); self.set_position_inner(monitor_origin.0, monitor_origin.1) - .queue(); - Some(self.set_fullscreen_hint(true)) + .expect_then_ignore_error("Failed to set window position"); + self.set_fullscreen_hint(true).map(Some) } } } @@ -757,9 +858,12 @@ impl UnownedWindow { #[inline] pub(crate) fn set_fullscreen(&self, fullscreen: Option) { - if let Some(flusher) = self.set_fullscreen_inner(fullscreen) { + if let Some(flusher) = self + .set_fullscreen_inner(fullscreen) + .expect("Failed to change window fullscreen state") + { flusher - .sync() + .check() .expect("Failed to change window fullscreen state"); self.invalidate_cached_frame_extents(); } @@ -770,9 +874,11 @@ impl UnownedWindow { let mut shared_state = self.shared_state_lock(); match shared_state.visibility { - Visibility::No => unsafe { - (self.xconn.xlib.XUnmapWindow)(self.xconn.display, self.xwindow); - }, + Visibility::No => self + .xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to unmap window"), Visibility::Yes => (), Visibility::YesWait => { shared_state.visibility = Visibility::Yes; @@ -785,140 +891,206 @@ impl UnownedWindow { } } - #[inline] - pub fn current_monitor(&self) -> X11MonitorHandle { - self.shared_state_lock().last_monitor.clone() + pub fn current_monitor(&self) -> Option { + Some(self.shared_state_lock().last_monitor.clone()) } pub fn available_monitors(&self) -> Vec { - self.xconn.available_monitors() + self.xconn + .available_monitors() + .expect("Failed to get available monitors") } - pub fn primary_monitor(&self) -> X11MonitorHandle { - self.xconn.primary_monitor() + pub fn primary_monitor(&self) -> Option { + Some( + self.xconn + .primary_monitor() + .expect("Failed to get primary monitor"), + ) } #[inline] pub fn is_minimized(&self) -> Option { - let state_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE\0") }; - let state = self - .xconn - .get_property(self.xwindow, state_atom, ffi::XA_ATOM); - let hidden_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE_HIDDEN\0") }; + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let hidden_atom = atoms[_NET_WM_STATE_HIDDEN]; Some(match state { - Ok(atoms) => atoms.iter().any(|atom: &ffi::Atom| *atom == hidden_atom), + Ok(atoms) => atoms + .iter() + .any(|atom: &xproto::Atom| *atom as xproto::Atom == hidden_atom), _ => false, }) } - fn set_minimized_inner(&self, minimized: bool) -> util::Flusher<'_> { - unsafe { - if minimized { - let screen = (self.xconn.xlib.XDefaultScreen)(self.xconn.display); + /// Refresh the API for the given monitor. + #[inline] + pub(super) fn refresh_dpi_for_monitor( + &self, + new_monitor: &X11MonitorHandle, + maybe_prev_scale_factor: Option, + mut callback: impl FnMut(Event), + ) { + // Check if the self is on this monitor + let monitor = self.shared_state_lock().last_monitor.clone(); + if monitor.name == new_monitor.name { + let (width, height) = self.inner_size_physical(); + let (new_width, new_height) = self.adjust_for_dpi( + // If we couldn't determine the previous scale + // factor (e.g., because all monitors were closed + // before), just pick whatever the current monitor + // has set as a baseline. + maybe_prev_scale_factor.unwrap_or(monitor.scale_factor), + new_monitor.scale_factor, + width, + height, + &self.shared_state_lock(), + ); + + let window_id = crate::window::WindowId(self.id()); + let old_inner_size = PhysicalSize::new(width, height); + let inner_size = Arc::new(Mutex::new(PhysicalSize::new(new_width, new_height))); + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_monitor.scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + let (new_width, new_height) = new_inner_size.into(); + self.request_inner_size_physical(new_width, new_height); + } + } + } - (self.xconn.xlib.XIconifyWindow)(self.xconn.display, self.xwindow, screen); + fn set_minimized_inner(&self, minimized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); - util::Flusher::new(&self.xconn) - } else { - let atom = self.xconn.get_atom_unchecked(b"_NET_ACTIVE_WINDOW\0"); + if minimized { + let root_window = self.xconn.default_root().root; - self.xconn.send_client_msg( - self.xwindow, - self.root, - atom, - Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), - [1, ffi::CurrentTime as c_long, 0, 0, 0], - ) - } + self.xconn.send_client_msg( + self.xwindow, + root_window, + atoms[WM_CHANGE_STATE], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [3u32, 0, 0, 0, 0], + ) + } else { + self.xconn.send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) } } #[inline] pub fn set_minimized(&self, minimized: bool) { self.set_minimized_inner(minimized) - .flush() + .expect_then_ignore_error("Failed to change window minimization"); + + self.xconn + .flush_requests() .expect("Failed to change window minimization"); } #[inline] pub fn is_maximized(&self) -> bool { - let state_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE\0") }; - let state = self - .xconn - .get_property(self.xwindow, state_atom, ffi::XA_ATOM); - let horz_atom = unsafe { - self.xconn - .get_atom_unchecked(b"_NET_WM_STATE_MAXIMIZED_HORZ\0") - }; - let vert_atom = unsafe { - self.xconn - .get_atom_unchecked(b"_NET_WM_STATE_MAXIMIZED_VERT\0") - }; + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; match state { Ok(atoms) => { - let horz_maximized = atoms.iter().any(|atom: &ffi::Atom| *atom == horz_atom); - let vert_maximized = atoms.iter().any(|atom: &ffi::Atom| *atom == vert_atom); + let horz_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == horz_atom); + let vert_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == vert_atom); horz_maximized && vert_maximized } _ => false, } } - fn set_maximized_inner(&self, maximized: bool) -> util::Flusher<'_> { - let horz_atom = unsafe { - self.xconn - .get_atom_unchecked(b"_NET_WM_STATE_MAXIMIZED_HORZ\0") - }; - let vert_atom = unsafe { - self.xconn - .get_atom_unchecked(b"_NET_WM_STATE_MAXIMIZED_VERT\0") - }; - self.set_netwm( - maximized.into(), - (horz_atom as c_long, vert_atom as c_long, 0, 0), - ) + fn set_maximized_inner(&self, maximized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + + self.set_netwm(maximized.into(), (horz_atom, vert_atom, 0, 0)) } #[inline] pub fn set_maximized(&self, maximized: bool) { self.set_maximized_inner(maximized) - .flush() + .expect_then_ignore_error("Failed to change window maximization"); + self.xconn + .flush_requests() .expect("Failed to change window maximization"); self.invalidate_cached_frame_extents(); } - fn set_title_inner(&self, title: &str) -> util::Flusher<'_> { - let wm_name_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_NAME\0") }; - let utf8_atom = unsafe { self.xconn.get_atom_unchecked(b"UTF8_STRING\0") }; + fn set_title_inner(&self, title: &str) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let title = CString::new(title).expect("Window title contained null byte"); - unsafe { - (self.xconn.xlib.XStoreName)( - self.xconn.display, - self.xwindow, - title.as_ptr() as *const c_char, - ); - self.xconn.change_property( + self.xconn + .change_property( self.xwindow, - wm_name_atom, - utf8_atom, - util::PropMode::Replace, + xproto::Atom::from(xproto::AtomEnum::WM_NAME), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, title.as_bytes(), - ) - } + )? + .ignore_error(); + self.xconn.change_property( + self.xwindow, + atoms[_NET_WM_NAME], + atoms[UTF8_STRING], + xproto::PropMode::REPLACE, + title.as_bytes(), + ) } #[inline] pub fn set_title(&self, title: &str) { self.set_title_inner(title) - .flush() + .expect_then_ignore_error("Failed to set window title"); + + self.xconn + .flush_requests() .expect("Failed to set window title"); } #[inline] pub fn set_transparent(&self, _transparent: bool) {} - fn set_decorations_inner(&self, decorations: bool) -> util::Flusher<'_> { + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + fn set_decorations_inner(&self, decorations: bool) -> Result, X11Error> { self.shared_state_lock().is_decorated = decorations; let mut hints = self.xconn.get_motif_hints(self.xwindow); @@ -930,7 +1102,9 @@ impl UnownedWindow { #[inline] pub fn set_decorations(&self, decorations: bool) { self.set_decorations_inner(decorations) - .flush() + .expect_then_ignore_error("Failed to set decoration state"); + self.xconn + .flush_requests() .expect("Failed to set decoration state"); self.invalidate_cached_frame_extents(); } @@ -940,7 +1114,7 @@ impl UnownedWindow { self.shared_state_lock().is_decorated } - fn set_maximizable_inner(&self, maximizable: bool) -> util::Flusher<'_> { + fn set_maximizable_inner(&self, maximizable: bool) -> Result, X11Error> { let mut hints = self.xconn.get_motif_hints(self.xwindow); hints.set_maximizable(maximizable); @@ -948,59 +1122,62 @@ impl UnownedWindow { self.xconn.set_motif_hints(self.xwindow, &hints) } - fn toggle_atom(&self, atom_bytes: &[u8], enable: bool) -> util::Flusher<'_> { - let atom = unsafe { self.xconn.get_atom_unchecked(atom_bytes) }; - self.set_netwm(enable.into(), (atom as c_long, 0, 0, 0)) + fn toggle_atom(&self, atom_name: AtomName, enable: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let atom = atoms[atom_name]; + self.set_netwm(enable.into(), (atom, 0, 0, 0)) } - fn set_window_level_inner(&self, level: WindowLevel) -> util::Flusher<'_> { - self.toggle_atom(b"_NET_WM_STATE_ABOVE\0", level == WindowLevel::AlwaysOnTop) - .queue(); - self.toggle_atom( - b"_NET_WM_STATE_BELOW\0", - level == WindowLevel::AlwaysOnBottom, - ) + fn set_window_level_inner(&self, level: WindowLevel) -> Result, X11Error> { + self.toggle_atom(_NET_WM_STATE_ABOVE, level == WindowLevel::AlwaysOnTop)? + .ignore_error(); + self.toggle_atom(_NET_WM_STATE_BELOW, level == WindowLevel::AlwaysOnBottom) } #[inline] pub fn set_window_level(&self, level: WindowLevel) { self.set_window_level_inner(level) - .flush() + .expect_then_ignore_error("Failed to set window-level state"); + self.xconn + .flush_requests() .expect("Failed to set window-level state"); } - fn set_icon_inner(&self, icon: Icon) -> util::Flusher<'_> { - let icon_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_ICON\0") }; + fn set_icon_inner(&self, icon: PlatformIcon) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; let data = icon.to_cardinals(); self.xconn.change_property( self.xwindow, icon_atom, - ffi::XA_CARDINAL, - util::PropMode::Replace, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, data.as_slice(), ) } - fn unset_icon_inner(&self) -> util::Flusher<'_> { - let icon_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_ICON\0") }; + fn unset_icon_inner(&self) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; let empty_data: [util::Cardinal; 0] = []; self.xconn.change_property( self.xwindow, icon_atom, - ffi::XA_CARDINAL, - util::PropMode::Replace, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, &empty_data, ) } #[inline] - pub fn set_window_icon(&self, icon: Option) { + pub(crate) fn set_window_icon(&self, icon: Option) { match icon { Some(icon) => self.set_icon_inner(icon), None => self.unset_icon_inner(), } - .flush() - .expect("Failed to set icons"); + .expect_then_ignore_error("Failed to set icons"); + + self.xconn.flush_requests().expect("Failed to set icons"); } #[inline] @@ -1015,17 +1192,26 @@ impl UnownedWindow { } if visible { - unsafe { - (self.xconn.xlib.XMapRaised)(self.xconn.display, self.xwindow); - } + self.xconn + .xcb_connection() + .map_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_map_window`"); + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); self.xconn .flush_requests() .expect("Failed to call XMapRaised"); shared_state.visibility = Visibility::YesWait; } else { - unsafe { - (self.xconn.xlib.XUnmapWindow)(self.xconn.display, self.xwindow); - } + self.xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_unmap_window`"); self.xconn .flush_requests() .expect("Failed to call XUnmapWindow"); @@ -1077,7 +1263,7 @@ impl UnownedWindow { // is BadWindow, and if the window handle is bad we have bigger problems. self.xconn .translate_coords(self.xwindow, self.root) - .map(|coords| (coords.x_rel_root, coords.y_rel_root)) + .map(|coords| (coords.dst_x.into(), coords.dst_y.into())) .unwrap() } @@ -1086,29 +1272,33 @@ impl UnownedWindow { Ok(self.inner_position_physical().into()) } - pub(crate) fn set_position_inner(&self, mut x: i32, mut y: i32) -> util::Flusher<'_> { + pub(crate) fn set_position_inner( + &self, + mut x: i32, + mut y: i32, + ) -> Result, X11Error> { // There are a few WMs that set client area position rather than window position, so // we'll translate for consistency. if util::wm_name_is_one_of(&["Enlightenment", "FVWM"]) { let extents = self.shared_state_lock().frame_extents.clone(); if let Some(extents) = extents { - x += extents.frame_extents.left as i32; - y += extents.frame_extents.top as i32; + x += cast_dimension_to_hint(extents.frame_extents.left); + y += cast_dimension_to_hint(extents.frame_extents.top); } else { self.update_cached_frame_extents(); return self.set_position_inner(x, y); } } - unsafe { - (self.xconn.xlib.XMoveWindow)(self.xconn.display, self.xwindow, x as c_int, y as c_int); - } - util::Flusher::new(&self.xconn) + + self.xconn + .xcb_connection() + .configure_window(self.xwindow, &xproto::ConfigureWindowAux::new().x(x).y(y)) + .map_err(Into::into) } pub(crate) fn set_position_physical(&self, x: i32, y: i32) { self.set_position_inner(x, y) - .flush() - .expect("Failed to call `XMoveWindow`"); + .expect_then_ignore_error("Failed to call `XMoveWindow`"); } #[inline] @@ -1122,7 +1312,7 @@ impl UnownedWindow { // is BadWindow, and if the window handle is bad we have bigger problems. self.xconn .get_geometry(self.xwindow) - .map(|geo| (geo.width, geo.height)) + .map(|geo| (geo.width.into(), geo.height.into())) .unwrap() } @@ -1143,47 +1333,69 @@ impl UnownedWindow { } } - pub(crate) fn set_inner_size_physical(&self, width: u32, height: u32) { - unsafe { - (self.xconn.xlib.XResizeWindow)( - self.xconn.display, + pub(crate) fn request_inner_size_physical(&self, width: u32, height: u32) { + self.xconn + .xcb_connection() + .configure_window( self.xwindow, - width as c_uint, - height as c_uint, - ); - self.xconn.flush_requests() + &xproto::ConfigureWindowAux::new() + .width(width) + .height(height), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn + .flush_requests() + .expect("Failed to call XResizeWindow"); + // cursor_hittest needs to be reapplied after each window resize. + if self.shared_state_lock().cursor_hittest.unwrap_or(false) { + let _ = self.set_cursor_hittest(true); } - .expect("Failed to call `XResizeWindow`"); } #[inline] - pub fn set_inner_size(&self, size: Size) { + pub fn request_inner_size(&self, size: Size) -> Option> { let scale_factor = self.scale_factor(); 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)); + normal_hints.min_size = Some(size); + normal_hints.max_size = Some(size); }) .expect("Failed to call `XSetWMNormalHints`"); } - self.set_inner_size_physical(size.0, size.1); + self.request_inner_size_physical(size.0 as u32, size.1 as u32); + + None } - fn update_normal_hints(&self, callback: F) -> Result<(), XError> + fn update_normal_hints(&self, callback: F) -> Result<(), X11Error> where - F: FnOnce(&mut util::NormalHints<'_>), + F: FnOnce(&mut WmSizeHints), { - let mut normal_hints = self.xconn.get_normal_hints(self.xwindow)?; + let mut normal_hints = WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .reply()? + .unwrap_or_default(); callback(&mut normal_hints); - self.xconn - .set_normal_hints(self.xwindow, normal_hints) - .flush() + normal_hints + .set( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .ignore_error(); + Ok(()) } pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { - self.update_normal_hints(|normal_hints| normal_hints.set_min_size(dimensions)) - .expect("Failed to call `XSetWMNormalHints`"); + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); } #[inline] @@ -1195,8 +1407,11 @@ impl UnownedWindow { } pub(crate) fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { - self.update_normal_hints(|normal_hints| normal_hints.set_max_size(dimensions)) - .expect("Failed to call `XSetWMNormalHints`"); + self.update_normal_hints(|normal_hints| { + normal_hints.max_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); } #[inline] @@ -1209,19 +1424,24 @@ impl UnownedWindow { #[inline] pub fn resize_increments(&self) -> Option> { - self.xconn - .get_normal_hints(self.xwindow) - .ok() - .and_then(|hints| hints.get_resize_increments()) - .map(Into::into) + WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + ) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .and_then(|hints| hints.size_increment) + .map(|(width, height)| (width as u32, height as u32).into()) } #[inline] pub fn set_resize_increments(&self, increments: Option) { self.shared_state_lock().resize_increments = increments; let physical_increments = - increments.map(|increments| increments.to_physical::(self.scale_factor()).into()); - self.update_normal_hints(|hints| hints.set_resize_increments(physical_increments)) + increments.map(|increments| cast_size_to_hint(increments, self.scale_factor())); + self.update_normal_hints(|hints| hints.size_increment = physical_increments) .expect("Failed to call `XSetWMNormalHints`"); } @@ -1235,16 +1455,16 @@ impl UnownedWindow { ) -> (u32, u32) { let scale_factor = new_scale_factor / old_scale_factor; self.update_normal_hints(|normal_hints| { - let dpi_adjuster = - |size: Size| -> (u32, u32) { size.to_physical::(new_scale_factor).into() }; + let dpi_adjuster = |size: Size| -> (i32, i32) { cast_size_to_hint(size, scale_factor) }; let max_size = shared_state.max_inner_size.map(dpi_adjuster); let min_size = shared_state.min_inner_size.map(dpi_adjuster); let resize_increments = shared_state.resize_increments.map(dpi_adjuster); let base_size = shared_state.base_size.map(dpi_adjuster); - normal_hints.set_max_size(max_size); - normal_hints.set_min_size(min_size); - normal_hints.set_resize_increments(resize_increments); - normal_hints.set_base_size(base_size); + + normal_hints.max_size = max_size; + normal_hints.min_size = min_size; + normal_hints.size_increment = resize_increments; + normal_hints.base_size = base_size; }) .expect("Failed to update normal hints"); @@ -1275,18 +1495,15 @@ impl UnownedWindow { }; self.shared_state_lock().is_resizable = resizable; - self.set_maximizable_inner(resizable).queue(); + self.set_maximizable_inner(resizable) + .expect_then_ignore_error("Failed to call `XSetWMNormalHints`"); let scale_factor = self.scale_factor(); - let min_inner_size = min_size - .map(|size| size.to_physical::(scale_factor)) - .map(Into::into); - let max_inner_size = max_size - .map(|size| size.to_physical::(scale_factor)) - .map(Into::into); + let min_inner_size = min_size.map(|size| cast_size_to_hint(size, scale_factor)); + let max_inner_size = max_size.map(|size| cast_size_to_hint(size, scale_factor)); self.update_normal_hints(|normal_hints| { - normal_hints.set_min_size(min_inner_size); - normal_hints.set_max_size(max_inner_size); + normal_hints.min_size = min_inner_size; + normal_hints.max_size = max_inner_size; }) .expect("Failed to call `XSetWMNormalHints`"); } @@ -1304,24 +1521,16 @@ impl UnownedWindow { WindowButtons::all() } + #[allow(dead_code)] #[inline] pub fn xlib_display(&self) -> *mut c_void { self.xconn.display as _ } - #[inline] - pub fn xlib_screen_id(&self) -> c_int { - self.screen_id - } - + #[allow(dead_code)] #[inline] pub fn xlib_window(&self) -> c_ulong { - self.xwindow - } - - #[inline] - pub fn xcb_connection(&self) -> *mut c_void { - unsafe { (self.xconn.xlib_xcb.XGetXCBConnection)(self.xconn.display) as *mut _ } + self.xwindow as ffi::Window } #[inline] @@ -1340,59 +1549,64 @@ impl UnownedWindow { 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); - } + // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. + // Therefore, this is common to both codepaths. + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`"); let result = match mode { - CursorGrabMode::None => self - .xconn - .flush_requests() - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))), + CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }), 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, - ) + let result = { + self.xconn + .xcb_connection() + .grab_pointer( + true as _, + self.xwindow, + xproto::EventMask::BUTTON_PRESS + | xproto::EventMask::BUTTON_RELEASE + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::POINTER_MOTION + | xproto::EventMask::POINTER_MOTION_HINT + | xproto::EventMask::BUTTON1_MOTION + | xproto::EventMask::BUTTON2_MOTION + | xproto::EventMask::BUTTON3_MOTION + | xproto::EventMask::BUTTON4_MOTION + | xproto::EventMask::BUTTON5_MOTION + | xproto::EventMask::KEYMAP_STATE, + xproto::GrabMode::ASYNC, + xproto::GrabMode::ASYNC, + self.xwindow, + 0u32, + x11rb::CURRENT_TIME, + ) + .expect("Failed to call `grab_pointer`") + .reply() + .expect("Failed to receive reply from `grab_pointer`") }; - match result { - ffi::GrabSuccess => Ok(()), - ffi::AlreadyGrabbed => { + match result.status { + xproto::GrabStatus::SUCCESS => Ok(()), + xproto::GrabStatus::ALREADY_GRABBED => { Err("Cursor could not be confined: already confined by another client") } - ffi::GrabInvalidTime => Err("Cursor could not be confined: invalid time"), - ffi::GrabNotViewable => { + xproto::GrabStatus::INVALID_TIME => { + Err("Cursor could not be confined: invalid time") + } + xproto::GrabStatus::NOT_VIEWABLE => { Err("Cursor could not be confined: confine location not viewable") } - ffi::GrabFrozen => { + xproto::GrabStatus::FROZEN => { Err("Cursor could not be confined: frozen by another client") } _ => unreachable!(), } - .map_err(|err| ExternalError::Os(os_error!(OsError::XMisc(err)))) + .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) } CursorGrabMode::Locked => { return Err(ExternalError::NotSupported(NotSupportedError::new())); @@ -1425,15 +1639,20 @@ impl UnownedWindow { #[inline] pub fn scale_factor(&self) -> f64 { - self.current_monitor().scale_factor + self.shared_state_lock().last_monitor.scale_factor } pub fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { - unsafe { - (self.xconn.xlib.XWarpPointer)(self.xconn.display, 0, self.xwindow, 0, 0, 0, 0, x, y); + { self.xconn - .flush_requests() - .map_err(|e| ExternalError::Os(os_error!(OsError::XError(e)))) + .xcb_connection() + .warp_pointer(x11rb::NONE, self.xwindow, 0, 0, 0, 0, x as _, y as _) + .map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(e).into()))) + })?; + self.xconn.flush_requests().map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(e).into()))) + }) } } @@ -1444,8 +1663,25 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let mut rectangles: Vec = Vec::new(); + if hittest { + let size = self.inner_size(); + rectangles.push(Rectangle { + x: 0, + y: 0, + width: size.width as u16, + height: size.height as u16, + }) + } + let region = RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles) + .map_err(|_e| ExternalError::Ignored)?; + self.xconn + .xcb_connection() + .xfixes_set_window_shape_region(self.xwindow, SK::INPUT, 0, 0, region.region()) + .map_err(|_e| ExternalError::Ignored)?; + self.shared_state_lock().cursor_hittest = Some(hittest); + Ok(()) } /// Moves the window while it is being dragged. @@ -1453,6 +1689,9 @@ impl UnownedWindow { self.drag_initiate(util::MOVERESIZE_MOVE) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + /// Resizes the window while it is being dragged. pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { self.drag_initiate(match direction { @@ -1472,21 +1711,26 @@ impl UnownedWindow { let pointer = self .xconn .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; let window = self.inner_position().map_err(ExternalError::NotSupported)?; - let message = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_MOVERESIZE\0") }; + let atoms = self.xconn.atoms(); + let message = atoms[_NET_WM_MOVERESIZE]; // 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_mode.lock().unwrap(); - unsafe { - (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); - } self.xconn - .flush_requests() - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(err).into()))) + })? + .ignore_error(); + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + })?; *grabbed_lock = CursorGrabMode::None; // we keep the lock until we are done @@ -1495,27 +1739,33 @@ impl UnownedWindow { self.xwindow, self.root, message, - Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), [ - (window.x as c_long + pointer.win_x as c_long), - (window.y as c_long + pointer.win_y as c_long), + (window.x as u32 + xinput_fp1616_to_float(pointer.win_x) as u32), + (window.y as u32 + xinput_fp1616_to_float(pointer.win_y) as u32), action.try_into().unwrap(), - ffi::Button1 as c_long, + 1, // Button 1 1, ], ) - .flush() - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }) } #[inline] pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { let (x, y) = spot.to_physical::(self.scale_factor()).into(); - let _ = self - .ime_sender - .lock() - .unwrap() - .send(ImeRequest::Position(self.xwindow, x, y)); + let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( + self.xwindow as ffi::Window, + x, + y, + )); } #[inline] @@ -1524,7 +1774,7 @@ impl UnownedWindow { .ime_sender .lock() .unwrap() - .send(ImeRequest::Allow(self.xwindow, allowed)); + .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); } #[inline] @@ -1532,13 +1782,14 @@ impl UnownedWindow { #[inline] pub fn focus_window(&self) { - let state_atom = unsafe { self.xconn.get_atom_unchecked(b"WM_STATE\0") }; - let state_type_atom = unsafe { self.xconn.get_atom_unchecked(b"CARD32\0") }; + let atoms = self.xconn.atoms(); + let state_atom = atoms[WM_STATE]; + let state_type_atom = atoms[CARD32]; let is_minimized = if let Ok(state) = self.xconn - .get_property(self.xwindow, state_atom, state_type_atom) + .get_property::(self.xwindow, state_atom, state_type_atom) { - state.contains(&(ffi::IconicState as c_ulong)) + state.contains(&super::ICONIC_STATE) } else { false }; @@ -1548,15 +1799,19 @@ impl UnownedWindow { }; if is_visible && !is_minimized { - let atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_ACTIVE_WINDOW\0") }; - let flusher = self.xconn.send_client_msg( - self.xwindow, - self.root, - atom, - Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), - [1, ffi::CurrentTime as c_long, 0, 0, 0], - ); - if let Err(e) = flusher.flush() { + self.xconn + .send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + .expect_then_ignore_error("Failed to send client message"); + if let Err(e) = self.xconn.flush_requests() { log::error!( "`flush` returned an error when focusing the window. Error was: {}", e @@ -1567,19 +1822,45 @@ impl UnownedWindow { #[inline] pub fn request_user_attention(&self, request_type: Option) { - let mut wm_hints = self - .xconn - .get_wm_hints(self.xwindow) - .expect("`XGetWMHints` failed"); - if request_type.is_some() { - wm_hints.flags |= ffi::XUrgencyHint; - } else { - wm_hints.flags &= !ffi::XUrgencyHint; - } - self.xconn - .set_wm_hints(self.xwindow, wm_hints) - .flush() - .expect("Failed to set urgency hint"); + let mut wm_hints = + WmHints::get(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .unwrap_or_default(); + + wm_hints.urgent = request_type.is_some(); + wm_hints + .set(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .expect_then_ignore_error("Failed to set WM hints"); + } + + #[inline] + pub(crate) fn generate_activation_token(&self) -> Result { + // Get the title from the WM_NAME property. + let atoms = self.xconn.atoms(); + let title = { + let title_bytes = self + .xconn + .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) + .expect("Failed to get title"); + + String::from_utf8(title_bytes).expect("Bad title") + }; + + // Get the activation token and then put it in the event queue. + let token = self.xconn.request_activation_token(&title)?; + + Ok(token) + } + + #[inline] + pub fn request_activation_token(&self) -> Result { + let serial = AsyncRequestSerial::get(); + self.activation_sender + .send((self.id(), serial)) + .expect("activation token channel should never be closed"); + Ok(serial) } #[inline] @@ -1595,18 +1876,59 @@ impl UnownedWindow { } #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut window_handle = XlibWindowHandle::empty(); + pub fn pre_present_notify(&self) { + // TODO timer + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::XlibHandle::empty(); + window_handle.display = self.xlib_display(); + window_handle.window = self.xlib_window(); + window_handle.visual_id = self.visual as c_ulong; + rwh_04::RawWindowHandle::Xlib(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::XlibWindowHandle::empty(); window_handle.window = self.xlib_window(); - RawWindowHandle::Xlib(window_handle) + window_handle.visual_id = self.visual as c_ulong; + window_handle.into() } + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - let mut display_handle = XlibDisplayHandle::empty(); + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); display_handle.display = self.xlib_display(); display_handle.screen = self.screen_id; - RawDisplayHandle::Xlib(display_handle) + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::XlibWindowHandle::new(self.xlib_window()); + window_handle.visual_id = self.visual as c_ulong; + Ok(window_handle.into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::XlibDisplayHandle::new( + Some( + std::ptr::NonNull::new(self.xlib_display()) + .expect("display pointer should never be null"), + ), + self.screen_id, + ) + .into()) } #[inline] @@ -1614,6 +1936,8 @@ impl UnownedWindow { None } + pub fn set_content_protected(&self, _protected: bool) {} + #[inline] pub fn has_focus(&self) -> bool { self.shared_state_lock().has_focus @@ -1623,3 +1947,25 @@ impl UnownedWindow { String::new() } } + +/// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. +fn cast_dimension_to_hint(val: u32) -> i32 { + val.try_into().unwrap_or(i32::MAX) +} + +/// Use the above strategy to cast a physical size into a hinted size. +fn cast_physical_size_to_hint(size: PhysicalSize) -> (i32, i32) { + let PhysicalSize { width, height } = size; + ( + cast_dimension_to_hint(width), + cast_dimension_to_hint(height), + ) +} + +/// Use the above strategy to cast a size into a hinted size. +fn cast_size_to_hint(size: Size, scale_factor: f64) -> (i32, i32) { + match size { + Size::Physical(size) => cast_physical_size_to_hint(size), + Size::Logical(size) => size.to_physical::(scale_factor).into(), + } +} diff --git a/src/platform_impl/linux/x11/xdisplay.rs b/src/platform_impl/linux/x11/xdisplay.rs index e3d7f4ffdc..757fc1e3e1 100644 --- a/src/platform_impl/linux/x11/xdisplay.rs +++ b/src/platform_impl/linux/x11/xdisplay.rs @@ -1,19 +1,66 @@ -use std::{collections::HashMap, error::Error, fmt, os::raw::c_int, ptr, sync::Mutex}; +use std::{ + collections::HashMap, + error::Error, + fmt, ptr, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, Mutex, RwLock, RwLockReadGuard, + }, +}; use crate::window::CursorIcon; -use super::ffi; +use super::{atoms::Atoms, ffi, monitor::MonitorHandle}; +use x11rb::{ + connection::Connection, + protocol::{ + randr::ConnectionExt as _, + xproto::{self, ConnectionExt}, + }, + resource_manager, + xcb_ffi::XCBConnection, +}; /// A connection to an X server. -pub(crate) struct XConnection { +pub struct XConnection { pub xlib: ffi::Xlib, - /// Exposes XRandR functions from version < 1.5 - pub xrandr: ffi::Xrandr_2_2_0, pub xcursor: ffi::Xcursor, + + // TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together + // for some reason. pub xinput2: ffi::XInput2, - pub xlib_xcb: ffi::Xlib_xcb, + pub display: *mut ffi::Display, - pub x11_fd: c_int, + + /// The manager for the XCB connection. + /// + /// The `Option` ensures that we can drop it before we close the `Display`. + xcb: Option, + + /// The atoms used by `winit`. + /// + /// This is a large structure, so I've elected to Box it to make accessing the fields of + /// this struct easier. Feel free to unbox it if you like kicking puppies. + atoms: Box, + + /// The index of the default screen. + default_screen: usize, + + /// The last timestamp received by this connection. + timestamp: AtomicU32, + + /// List of monitor handles. + pub monitor_handles: Mutex>>, + + /// The resource database. + database: RwLock, + + /// RandR version. + randr_version: (u32, u32), + + /// Atom for the XSettings screen. + xsettings_screen: Option, + pub latest_error: Mutex>, pub cursor_cache: Mutex, ffi::Cursor>>, } @@ -22,16 +69,15 @@ unsafe impl Send for XConnection {} unsafe impl Sync for XConnection {} pub type XErrorHandler = - Option libc::c_int>; + Option std::os::raw::c_int>; impl XConnection { pub fn new(error_handler: XErrorHandler) -> Result { // opening the libraries let xlib = ffi::Xlib::open()?; let xcursor = ffi::Xcursor::open()?; - let xrandr = ffi::Xrandr_2_2_0::open()?; - let xinput2 = ffi::XInput2::open()?; let xlib_xcb = ffi::Xlib_xcb::open()?; + let xinput2 = ffi::XInput2::open()?; unsafe { (xlib.XInitThreads)() }; unsafe { (xlib.XSetErrorHandler)(error_handler) }; @@ -45,22 +91,94 @@ impl XConnection { display }; - // Get X11 socket file descriptor - let fd = unsafe { (xlib.XConnectionNumber)(display) }; + // Open the x11rb XCB connection. + let xcb = { + // Get a pointer to the underlying XCB connection + let xcb_connection = + unsafe { (xlib_xcb.XGetXCBConnection)(display as *mut ffi::Display) }; + assert!(!xcb_connection.is_null()); + + // Wrap the XCB connection in an x11rb XCB connection + let conn = + unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection.cast(), false) }; + + conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))? + }; + + // Get the default screen. + let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize; + + // Load the database. + let database = resource_manager::new_from_default(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Load the RandR version. + let randr_version = xcb + .randr_query_version(1, 3) + .expect("failed to request XRandR version") + .reply() + .expect("failed to query XRandR version"); + + let xsettings_screen = Self::new_xsettings_screen(&xcb, default_screen); + if xsettings_screen.is_none() { + log::warn!("error setting XSETTINGS; Xft options won't reload automatically") + } + + // Fetch atoms. + let atoms = Atoms::new(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))? + .reply() + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; Ok(XConnection { xlib, - xrandr, xcursor, xinput2, - xlib_xcb, display, - x11_fd: fd, + xcb: Some(xcb), + atoms: Box::new(atoms), + default_screen, + timestamp: AtomicU32::new(0), latest_error: Mutex::new(None), + monitor_handles: Mutex::new(None), + database: RwLock::new(database), cursor_cache: Default::default(), + randr_version: (randr_version.major_version, randr_version.minor_version), + xsettings_screen, }) } + fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option { + // Fetch the _XSETTINGS_S[screen number] atom. + let xsettings_screen = xcb + .intern_atom(false, format!("_XSETTINGS_S{}", default_screen).as_bytes()) + .ok()? + .reply() + .ok()? + .atom; + + // Get PropertyNotify events from the XSETTINGS window. + // TODO: The XSETTINGS window here can change. In the future, listen for DestroyNotify on this window + // in order to accomodate for a changed window here. + let selector_window = xcb + .get_selection_owner(xsettings_screen) + .ok()? + .reply() + .ok()? + .owner; + + xcb.change_window_attributes( + selector_window, + &xproto::ChangeWindowAttributesAux::new() + .event_mask(xproto::EventMask::PROPERTY_CHANGE), + ) + .ok()? + .check() + .ok()?; + + Some(xsettings_screen) + } + /// Checks whether an error has been triggered by the previous function calls. #[inline] pub fn check_errors(&self) -> Result<(), XError> { @@ -71,6 +189,87 @@ impl XConnection { Ok(()) } } + + #[inline] + pub fn randr_version(&self) -> (u32, u32) { + self.randr_version + } + + /// Get the underlying XCB connection. + #[inline] + pub fn xcb_connection(&self) -> &XCBConnection { + self.xcb + .as_ref() + .expect("xcb_connection somehow called after drop?") + } + + /// Get the list of atoms. + #[inline] + pub fn atoms(&self) -> &Atoms { + &self.atoms + } + + /// Get the index of the default screen. + #[inline] + pub fn default_screen_index(&self) -> usize { + self.default_screen + } + + /// Get the default screen. + #[inline] + pub fn default_root(&self) -> &xproto::Screen { + &self.xcb_connection().setup().roots[self.default_screen] + } + + /// Get the resource database. + #[inline] + pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> { + self.database.read().unwrap_or_else(|e| e.into_inner()) + } + + /// Reload the resource database. + #[inline] + pub fn reload_database(&self) -> Result<(), super::X11Error> { + let database = resource_manager::new_from_default(self.xcb_connection())?; + *self.database.write().unwrap_or_else(|e| e.into_inner()) = database; + Ok(()) + } + + /// Get the latest timestamp. + #[inline] + pub fn timestamp(&self) -> u32 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Set the last witnessed timestamp. + #[inline] + pub fn set_timestamp(&self, timestamp: u32) { + // Store the timestamp in the slot if it's greater than the last one. + let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); + loop { + let wrapping_sub = |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32); + + if wrapping_sub(timestamp, last_timestamp) <= 0 { + break; + } + + match self.timestamp.compare_exchange( + last_timestamp, + timestamp, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => last_timestamp = x, + } + } + } + + /// Get the atom for Xsettings. + #[inline] + pub fn xsettings_screen(&self) -> Option { + self.xsettings_screen + } } impl fmt::Debug for XConnection { @@ -82,6 +281,7 @@ impl fmt::Debug for XConnection { impl Drop for XConnection { #[inline] fn drop(&mut self) { + self.xcb = None; unsafe { (self.xlib.XCloseDisplay)(self.display) }; } } @@ -112,8 +312,12 @@ impl fmt::Display for XError { pub enum XNotSupported { /// Failed to load one or several shared libraries. LibraryOpenError(ffi::OpenError), + /// Connecting to the X server with `XOpenDisplay` failed. - XOpenDisplayFailed, // TODO: add better message + XOpenDisplayFailed, // TODO: add better message. + + /// We encountered an error while converting the connection to XCB. + XcbConversionError(Arc), } impl From for XNotSupported { @@ -128,6 +332,7 @@ impl XNotSupported { match self { XNotSupported::LibraryOpenError(_) => "Failed to load one of xlib's shared libraries", XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server", + XNotSupported::XcbConversionError(_) => "Failed to convert Xlib connection to XCB", } } } @@ -137,6 +342,7 @@ impl Error for XNotSupported { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { XNotSupported::LibraryOpenError(ref err) => Some(err), + XNotSupported::XcbConversionError(ref err) => Some(&**err), _ => None, } } @@ -147,3 +353,19 @@ impl fmt::Display for XNotSupported { formatter.write_str(self.description()) } } + +/// A newtype wrapper around a `ConnectError` that can't be accessed by downstream libraries. +/// +/// Without this, `x11rb` would become a public dependency. +#[derive(Debug)] +struct WrapConnectError(x11rb::rust_connection::ConnectError); + +impl fmt::Display for WrapConnectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Error for WrapConnectError { + // We can't implement `source()` here or otherwise risk exposing `x11rb`. +} diff --git a/src/platform_impl/linux/x11/xsettings.rs b/src/platform_impl/linux/x11/xsettings.rs new file mode 100644 index 0000000000..f820c2fed6 --- /dev/null +++ b/src/platform_impl/linux/x11/xsettings.rs @@ -0,0 +1,343 @@ +//! Parser for the xsettings data format. +//! +//! Some of this code is referenced from [here]. +//! +//! [here]: https://github.com/derat/xsettingsd + +use std::iter; +use std::num::NonZeroUsize; + +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::{atoms::*, XConnection}; + +type Result = core::result::Result; + +const DPI_NAME: &[u8] = b"Xft/DPI"; +const DPI_MULTIPLIER: f64 = 1024.0; +const LITTLE_ENDIAN: u8 = b'l'; +const BIG_ENDIAN: u8 = b'B'; + +impl XConnection { + /// Get the DPI from XSettings. + pub(crate) fn xsettings_dpi( + &self, + xsettings_screen: xproto::Atom, + ) -> core::result::Result, super::X11Error> { + let atoms = self.atoms(); + + // Get the current owner of the screen's settings. + let owner = self + .xcb_connection() + .get_selection_owner(xsettings_screen)? + .reply()?; + + // Read the _XSETTINGS_SETTINGS property. + let data: Vec = self.get_property( + owner.owner, + atoms[_XSETTINGS_SETTINGS], + atoms[_XSETTINGS_SETTINGS], + )?; + + // Parse the property. + let dpi_setting = read_settings(&data)? + .find(|res| res.as_ref().map_or(true, |s| s.name == DPI_NAME)) + .transpose()?; + if let Some(dpi_setting) = dpi_setting { + let base_dpi = match dpi_setting.data { + SettingData::Integer(dpi) => dpi as f64, + SettingData::String(_) => { + return Err(ParserError::BadType(SettingType::String).into()) + } + SettingData::Color(_) => { + return Err(ParserError::BadType(SettingType::Color).into()) + } + }; + + Ok(Some(base_dpi / DPI_MULTIPLIER)) + } else { + Ok(None) + } + } +} + +/// Read over the settings in the block of data. +fn read_settings(data: &[u8]) -> Result>> + '_> { + // Create a parser. This automatically parses the first 8 bytes for metadata. + let mut parser = Parser::new(data)?; + + // Read the total number of settings. + let total_settings = parser.i32()?; + + // Iterate over the settings. + let iter = iter::repeat_with(move || Setting::parse(&mut parser)).take(total_settings as usize); + Ok(iter) +} + +/// A setting in the settings list. +struct Setting<'a> { + /// The name of the setting. + name: &'a [u8], + + /// The data contained in the setting. + data: SettingData<'a>, +} + +/// The data contained in a setting. +enum SettingData<'a> { + Integer(i32), + String(#[allow(dead_code)] &'a [u8]), + Color(#[allow(dead_code)] [i16; 4]), +} + +impl<'a> Setting<'a> { + /// Parse a new `SettingData`. + fn parse(parser: &mut Parser<'a>) -> Result { + // Read the type. + let ty: SettingType = parser.i8()?.try_into()?; + + // Read another byte of padding. + parser.advance(1)?; + + // Read the name of the setting. + let name_len = parser.i16()?; + let name = parser.advance(name_len as usize)?; + parser.pad(name.len(), 4)?; + + // Ignore the serial number. + parser.advance(4)?; + + let data = match ty { + SettingType::Integer => { + // Read a 32-bit integer. + SettingData::Integer(parser.i32()?) + } + + SettingType::String => { + // Read the data. + let data_len = parser.i32()?; + let data = parser.advance(data_len as usize)?; + parser.pad(data.len(), 4)?; + + SettingData::String(data) + } + + SettingType::Color => { + // Read i16's of color. + let (red, blue, green, alpha) = + (parser.i16()?, parser.i16()?, parser.i16()?, parser.i16()?); + + SettingData::Color([red, blue, green, alpha]) + } + }; + + Ok(Setting { name, data }) + } +} + +#[derive(Debug)] +pub enum SettingType { + Integer = 0, + String = 1, + Color = 2, +} + +impl TryFrom for SettingType { + type Error = ParserError; + + fn try_from(value: i8) -> Result { + Ok(match value { + 0 => Self::Integer, + 1 => Self::String, + 2 => Self::Color, + x => return Err(ParserError::InvalidType(x)), + }) + } +} + +/// Parser for the incoming byte stream. +struct Parser<'a> { + bytes: &'a [u8], + endianness: Endianness, +} + +impl<'a> Parser<'a> { + /// Create a new parser. + fn new(bytes: &'a [u8]) -> Result { + let (endianness, bytes) = bytes + .split_first() + .ok_or_else(|| ParserError::ran_out(1, 0))?; + let endianness = match *endianness { + BIG_ENDIAN => Endianness::Big, + LITTLE_ENDIAN => Endianness::Little, + _ => Endianness::native(), + }; + + Ok(Self { + // Ignore three bytes of padding and the four-byte serial. + bytes: bytes + .get(7..) + .ok_or_else(|| ParserError::ran_out(7, bytes.len()))?, + endianness, + }) + } + + /// Get a slice of bytes. + fn advance(&mut self, n: usize) -> Result<&'a [u8]> { + if n == 0 { + return Ok(&[]); + } + + if n > self.bytes.len() { + Err(ParserError::ran_out(n, self.bytes.len())) + } else { + let (part, rem) = self.bytes.split_at(n); + self.bytes = rem; + Ok(part) + } + } + + /// Skip some padding. + fn pad(&mut self, size: usize, pad: usize) -> Result<()> { + let advance = (pad - (size % pad)) % pad; + self.advance(advance)?; + Ok(()) + } + + /// Get a single byte. + fn i8(&mut self) -> Result { + self.advance(1).map(|s| s[0] as i8) + } + + /// Get two bytes. + fn i16(&mut self) -> Result { + self.advance(2).map(|s| { + let bytes: &[u8; 2] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i16::from_be_bytes(*bytes), + Endianness::Little => i16::from_le_bytes(*bytes), + } + }) + } + + /// Get four bytes. + fn i32(&mut self) -> Result { + self.advance(4).map(|s| { + let bytes: &[u8; 4] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i32::from_be_bytes(*bytes), + Endianness::Little => i32::from_le_bytes(*bytes), + } + }) + } +} + +/// Endianness of the incoming data. +enum Endianness { + Little, + Big, +} + +impl Endianness { + #[cfg(target_endian = "little")] + fn native() -> Self { + Endianness::Little + } + + #[cfg(target_endian = "big")] + fn native() -> Self { + Endianness::Big + } +} + +/// Parser errors. +#[derive(Debug)] +pub enum ParserError { + /// Ran out of bytes. + NoMoreBytes { + expected: NonZeroUsize, + found: usize, + }, + + /// Invalid type. + InvalidType(i8), + + /// Bad setting type. + BadType(SettingType), +} + +impl ParserError { + fn ran_out(expected: usize, found: usize) -> ParserError { + let expected = NonZeroUsize::new(expected).unwrap(); + Self::NoMoreBytes { expected, found } + } +} + +#[cfg(test)] +mod tests { + //! Tests for the XSETTINGS parser. + + use super::*; + + const XSETTINGS: &str = include_str!("tests/xsettings.dat"); + + #[test] + fn empty() { + let err = match read_settings(&[]) { + Ok(_) => panic!(), + Err(err) => err, + }; + match err { + ParserError::NoMoreBytes { expected, found } => { + assert_eq!(expected.get(), 1); + assert_eq!(found, 0); + } + + _ => panic!(), + } + } + + #[test] + fn parse_xsettings() { + let data = XSETTINGS + .trim() + .split(',') + .map(|tok| { + let val = tok.strip_prefix("0x").unwrap(); + u8::from_str_radix(val, 16).unwrap() + }) + .collect::>(); + + let settings = read_settings(&data) + .unwrap() + .collect::>>() + .unwrap(); + + let dpi = settings.iter().find(|s| s.name == b"Xft/DPI").unwrap(); + assert_int(&dpi.data, 96 * 1024); + let hinting = settings.iter().find(|s| s.name == b"Xft/Hinting").unwrap(); + assert_int(&hinting.data, 1); + + let rgba = settings.iter().find(|s| s.name == b"Xft/RGBA").unwrap(); + assert_string(&rgba.data, "rgb"); + let lcd = settings + .iter() + .find(|s| s.name == b"Xft/Lcdfilter") + .unwrap(); + assert_string(&lcd.data, "lcddefault"); + } + + fn assert_string(dat: &SettingData<'_>, s: &str) { + match dat { + SettingData::String(left) => assert_eq!(*left, s.as_bytes()), + _ => panic!("invalid data type"), + } + } + + fn assert_int(dat: &SettingData<'_>, i: i32) { + match dat { + SettingData::Integer(left) => assert_eq!(*left, i), + _ => panic!("invalid data type"), + } + } +} diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index 864cabd303..326805aa0f 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -1,26 +1,28 @@ #![allow(clippy::unnecessary_cast)] -use objc2::foundation::NSObject; -use objc2::{declare_class, msg_send, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{declare_class, msg_send, mutability, ClassType}; use super::appkit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder}; -use super::{app_state::AppState, event::EventWrapper, DEVICE_ID}; +use super::{app_state::AppState, DEVICE_ID}; use crate::event::{DeviceEvent, ElementState, Event}; declare_class!( #[derive(Debug, PartialEq, Eq, Hash)] - pub(super) struct WinitApplication {} + pub(super) struct WinitApplication; unsafe impl ClassType for WinitApplication { #[inherits(NSResponder, NSObject)] type Super = NSApplication; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitApplication"; } unsafe impl WinitApplication { // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. // Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) - #[sel(sendEvent:)] + #[method(sendEvent:)] fn send_event(&self, event: &NSEvent) { // For posterity, there are some undocumented event types // (https://github.com/servo/cocoa-rs/issues/155) @@ -94,5 +96,5 @@ fn queue_device_event(event: DeviceEvent) { device_id: DEVICE_ID, event, }; - AppState::queue_event(EventWrapper::StaticEvent(event)); + AppState::queue_event(event); } diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 0c9eff1747..4b0991f1e5 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,7 +1,10 @@ -use objc2::foundation::NSObject; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; -use objc2::{declare_class, msg_send, msg_send_id, ClassType}; +use std::ptr::NonNull; + +use icrate::Foundation::NSObject; +use objc2::declare::{IvarBool, IvarEncode}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{declare_class, msg_send, msg_send_id, mutability, ClassType}; use super::app_state::AppState; use super::appkit::NSApplicationActivationPolicy; @@ -9,35 +12,38 @@ use super::appkit::NSApplicationActivationPolicy; declare_class!( #[derive(Debug)] pub(super) struct ApplicationDelegate { - activation_policy: NSApplicationActivationPolicy, - default_menu: bool, - activate_ignoring_other_apps: bool, + activation_policy: IvarEncode, + default_menu: IvarBool<"_default_menu">, + activate_ignoring_other_apps: IvarBool<"_activate_ignoring_other_apps">, } + mod ivars; + unsafe impl ClassType for ApplicationDelegate { type Super = NSObject; + type Mutability = mutability::InteriorMutable; const NAME: &'static str = "WinitApplicationDelegate"; } unsafe impl ApplicationDelegate { - #[sel(initWithActivationPolicy:defaultMenu:activateIgnoringOtherApps:)] - fn init( - &mut self, + #[method(initWithActivationPolicy:defaultMenu:activateIgnoringOtherApps:)] + unsafe fn init( + this: *mut Self, activation_policy: NSApplicationActivationPolicy, default_menu: bool, activate_ignoring_other_apps: bool, - ) -> Option<&mut Self> { - let this: Option<&mut Self> = unsafe { msg_send![super(self), init] }; + ) -> Option> { + let this: Option<&mut Self> = unsafe { msg_send![super(this), init] }; this.map(|this| { *this.activation_policy = activation_policy; *this.default_menu = default_menu; *this.activate_ignoring_other_apps = activate_ignoring_other_apps; - this + NonNull::from(this) }) } - #[sel(applicationDidFinishLaunching:)] - fn did_finish_launching(&self, _sender: *const Object) { + #[method(applicationDidFinishLaunching:)] + fn did_finish_launching(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationDidFinishLaunching:"); AppState::launched( *self.activation_policy, @@ -46,11 +52,11 @@ declare_class!( ); } - #[sel(applicationWillTerminate:)] - fn will_terminate(&self, _sender: *const Object) { + #[method(applicationWillTerminate:)] + fn will_terminate(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationWillTerminate:"); // TODO: Notify every window that it will be destroyed, like done in iOS? - AppState::exit(); + AppState::internal_exit(); } } ); @@ -60,10 +66,10 @@ impl ApplicationDelegate { activation_policy: NSApplicationActivationPolicy, default_menu: bool, activate_ignoring_other_apps: bool, - ) -> Id { + ) -> Id { unsafe { msg_send_id![ - msg_send_id![Self::class(), alloc], + Self::alloc(), initWithActivationPolicy: activation_policy, defaultMenu: default_menu, activateIgnoringOtherApps: activate_ignoring_other_apps, diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 1a85b40f63..3d9de05967 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -6,36 +6,31 @@ use std::{ rc::{Rc, Weak}, sync::{ atomic::{AtomicBool, Ordering}, - Mutex, MutexGuard, + mpsc, Arc, Mutex, MutexGuard, }, time::Instant, }; use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp}; -use objc2::foundation::{is_main_thread, NSSize}; -use objc2::rc::autoreleasepool; +use icrate::Foundation::{is_main_thread, NSSize}; +use objc2::rc::{autoreleasepool, Id}; use once_cell::sync::Lazy; use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent}; +use super::{ + event_loop::PanicInfo, menu, observer::EventLoopWaker, util::Never, window::WinitWindow, +}; use crate::{ - dpi::LogicalSize, - event::{Event, StartCause, WindowEvent}, + dpi::PhysicalSize, + event::{Event, InnerSizeWriter, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, - platform_impl::platform::{ - event::{EventProxy, EventWrapper}, - event_loop::PanicInfo, - menu, - observer::EventLoopWaker, - util::Never, - window::WinitWindow, - }, window::WindowId, }; static HANDLER: Lazy = Lazy::new(Default::default); -impl<'a, Never> Event<'a, Never> { - fn userify(self) -> Event<'a, T> { +impl Event { + fn userify(self) -> Event { self.map_nonuser_event() // `Never` can't be constructed, so the `UserEvent` variant can't // be present here. @@ -45,34 +40,34 @@ impl<'a, Never> Event<'a, Never> { pub trait EventHandler: Debug { // Not sure probably it should accept Event<'static, Never> - fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow); - fn handle_user_events(&mut self, control_flow: &mut ControlFlow); + fn handle_nonuser_event(&mut self, event: Event); + fn handle_user_events(&mut self); } -pub(crate) type Callback = - RefCell, &RootWindowTarget, &mut ControlFlow)>; +pub(crate) type Callback = RefCell, &RootWindowTarget)>; struct EventLoopHandler { callback: Weak>, window_target: Rc>, + receiver: Rc>, } impl EventLoopHandler { fn with_callback(&mut self, f: F) where - F: FnOnce( - &mut EventLoopHandler, - RefMut<'_, dyn FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow)>, - ), + F: FnOnce(&mut EventLoopHandler, RefMut<'_, dyn FnMut(Event, &RootWindowTarget)>), { + // The `NSApp` and our `HANDLER` are global state and so it's possible that + // we could get a delegate callback after the application has exit an + // `EventLoop`. If the loop has been exit then our weak `self.callback` + // will fail to upgrade. + // + // We don't want to panic or output any verbose logging if we fail to + // upgrade the weak reference since it might be valid that the application + // re-starts the `NSApp` after exiting a Winit `EventLoop` if let Some(callback) = self.callback.upgrade() { let callback = callback.borrow_mut(); (f)(self, callback); - } else { - panic!( - "Tried to dispatch an event, but the event loop that \ - owned the event handler callback seems to be destroyed" - ); } } } @@ -87,41 +82,47 @@ impl Debug for EventLoopHandler { } impl EventHandler for EventLoopHandler { - fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { + fn handle_nonuser_event(&mut self, event: Event) { self.with_callback(|this, mut callback| { - if let ControlFlow::ExitWithCode(code) = *control_flow { - let dummy = &mut ControlFlow::ExitWithCode(code); - (callback)(event.userify(), &this.window_target, dummy); - } else { - (callback)(event.userify(), &this.window_target, control_flow); - } + (callback)(event.userify(), &this.window_target); }); } - fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { + fn handle_user_events(&mut self) { self.with_callback(|this, mut callback| { - for event in this.window_target.p.receiver.try_iter() { - if let ControlFlow::ExitWithCode(code) = *control_flow { - let dummy = &mut ControlFlow::ExitWithCode(code); - (callback)(Event::UserEvent(event), &this.window_target, dummy); - } else { - (callback)(Event::UserEvent(event), &this.window_target, control_flow); - } + for event in this.receiver.try_iter() { + (callback)(Event::UserEvent(event), &this.window_target); } }); } } +#[derive(Debug)] +enum EventWrapper { + StaticEvent(Event), + ScaleFactorChanged { + window: Id, + suggested_size: PhysicalSize, + scale_factor: f64, + }, +} + #[derive(Default)] struct Handler { - ready: AtomicBool, + stop_app_on_launch: AtomicBool, + stop_app_before_wait: AtomicBool, + stop_app_after_wait: AtomicBool, + stop_app_on_redraw: AtomicBool, + launched: AtomicBool, + running: AtomicBool, in_callback: AtomicBool, control_flow: Mutex, - control_flow_prev: Mutex, + exit: AtomicBool, start_time: Mutex>, callback: Mutex>>, pending_events: Mutex>, pending_redraw: Mutex>, + wait_timeout: Mutex>, waker: Mutex, } @@ -141,31 +142,147 @@ impl Handler { self.waker.lock().unwrap() } - fn is_ready(&self) -> bool { - self.ready.load(Ordering::Acquire) + /// `true` after `ApplicationDelegate::applicationDidFinishLaunching` called + /// + /// NB: This is global / `NSApp` state and since the app will only be launched + /// once but an `EventLoop` may be run more than once then only the first + /// `EventLoop` will observe the `NSApp` before it is launched. + fn is_launched(&self) -> bool { + self.launched.load(Ordering::Acquire) + } + + /// Set via `ApplicationDelegate::applicationDidFinishLaunching` + fn set_launched(&self) { + self.launched.store(true, Ordering::Release); + } + + /// `true` if an `EventLoop` is currently running + /// + /// NB: This is global / `NSApp` state and may persist beyond the lifetime of + /// a running `EventLoop`. + /// + /// # Caveat + /// This is only intended to be called from the main thread + fn is_running(&self) -> bool { + self.running.load(Ordering::Relaxed) + } + + /// Set when an `EventLoop` starts running, after the `NSApp` is launched + /// + /// # Caveat + /// This is only intended to be called from the main thread + fn set_running(&self) { + self.running.store(true, Ordering::Relaxed); + } + + /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits + /// + /// Since an `EventLoop` may be run more than once we need make sure to reset the + /// `control_flow` state back to `Poll` each time the loop exits. + /// + /// Note: that if the `NSApp` has been launched then that state is preserved, and we won't + /// need to re-launch the app if subsequent EventLoops are run. + /// + /// # Caveat + /// This is only intended to be called from the main thread + fn internal_exit(&self) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + // + // XXX: As an aside; having each individual bit of state for `Handler` be atomic or wrapped in a + // `Mutex` for the sake of interior mutability seems a bit odd, and also a potential foot + // gun in case the state is unwittingly accessed across threads because the fine-grained locking + // wouldn't ensure that there's interior consistency. + // + // Maybe the whole thing should just be put in a static `Mutex<>` to make it clear + // the we can mutate more than one peice of state while maintaining consistency. (though it + // looks like there have been recuring re-entrancy issues with callback handling that might + // make that awkward) + self.running.store(false, Ordering::Relaxed); + self.set_stop_app_on_redraw_requested(false); + self.set_stop_app_before_wait(false); + self.set_stop_app_after_wait(false); + self.set_wait_timeout(None); } - fn set_ready(&self) { - self.ready.store(true, Ordering::Release); + pub fn exit(&self) { + self.exit.store(true, Ordering::Relaxed) } - fn should_exit(&self) -> bool { - matches!( - *self.control_flow.lock().unwrap(), - ControlFlow::ExitWithCode(_) - ) + pub fn clear_exit(&self) { + self.exit.store(false, Ordering::Relaxed) } - fn get_control_flow_and_update_prev(&self) -> ControlFlow { - let control_flow = self.control_flow.lock().unwrap(); - *self.control_flow_prev.lock().unwrap() = *control_flow; - *control_flow + pub fn exiting(&self) -> bool { + self.exit.load(Ordering::Relaxed) } - fn get_old_and_new_control_flow(&self) -> (ControlFlow, ControlFlow) { - let old = *self.control_flow_prev.lock().unwrap(); - let new = *self.control_flow.lock().unwrap(); - (old, new) + pub fn request_stop_app_on_launch(&self) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_on_launch.store(true, Ordering::Relaxed); + } + + pub fn should_stop_app_on_launch(&self) -> bool { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_on_launch.load(Ordering::Relaxed) + } + + pub fn set_stop_app_before_wait(&self, stop_before_wait: bool) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_before_wait + .store(stop_before_wait, Ordering::Relaxed); + } + + pub fn should_stop_app_before_wait(&self) -> bool { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_before_wait.load(Ordering::Relaxed) + } + + pub fn set_stop_app_after_wait(&self, stop_after_wait: bool) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_after_wait + .store(stop_after_wait, Ordering::Relaxed); + } + + pub fn set_wait_timeout(&self, new_timeout: Option) { + let mut timeout = self.wait_timeout.lock().unwrap(); + *timeout = new_timeout; + } + + pub fn wait_timeout(&self) -> Option { + *self.wait_timeout.lock().unwrap() + } + + pub fn should_stop_app_after_wait(&self) -> bool { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_after_wait.load(Ordering::Relaxed) + } + + pub fn set_stop_app_on_redraw_requested(&self, stop_on_redraw: bool) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_on_redraw + .store(stop_on_redraw, Ordering::Relaxed); + } + + pub fn should_stop_app_on_redraw_requested(&self) -> bool { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interior mutability + self.stop_app_on_redraw.load(Ordering::Relaxed) + } + + fn set_control_flow(&self, new_control_flow: ControlFlow) { + *self.control_flow.lock().unwrap() = new_control_flow + } + + fn control_flow(&self) -> ControlFlow { + *self.control_flow.lock().unwrap() } fn get_start_time(&self) -> Option { @@ -192,60 +309,51 @@ impl Handler { self.in_callback.store(in_callback, Ordering::Release); } - fn handle_nonuser_event(&self, wrapper: EventWrapper) { + fn have_callback(&self) -> bool { + self.callback.lock().unwrap().is_some() + } + + fn handle_nonuser_event(&self, event: Event) { if let Some(ref mut callback) = *self.callback.lock().unwrap() { - match wrapper { - EventWrapper::StaticEvent(event) => { - callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap()) - } - EventWrapper::EventProxy(proxy) => self.handle_proxy(proxy, callback), - } + callback.handle_nonuser_event(event) } } fn handle_user_events(&self) { if let Some(ref mut callback) = *self.callback.lock().unwrap() { - callback.handle_user_events(&mut self.control_flow.lock().unwrap()); + callback.handle_user_events(); } } fn handle_scale_factor_changed_event( &self, - callback: &mut Box, window: &WinitWindow, - suggested_size: LogicalSize, + suggested_size: PhysicalSize, scale_factor: f64, ) { - let mut size = suggested_size.to_physical(scale_factor); - let new_inner_size = &mut size; - let event = Event::WindowEvent { - window_id: WindowId(window.id()), - event: WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size, - }, - }; - - callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap()); - - let physical_size = *new_inner_size; - let logical_size = physical_size.to_logical(scale_factor); - let size = NSSize::new(logical_size.width, logical_size.height); - window.setContentSize(size); - } - - fn handle_proxy(&self, proxy: EventProxy, callback: &mut Box) { - match proxy { - EventProxy::DpiChangedProxy { - window, - suggested_size, - scale_factor, - } => self.handle_scale_factor_changed_event( - callback, - &window, - suggested_size, - scale_factor, - ), + if let Some(ref mut callback) = *self.callback.lock().unwrap() { + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let scale_factor_changed_event = Event::WindowEvent { + window_id: WindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }; + + callback.handle_nonuser_event(scale_factor_changed_event); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let logical_size = physical_size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + window.setContentSize(size); + + let resized_event = Event::WindowEvent { + window_id: WindowId(window.id()), + event: WindowEvent::Resized(physical_size), + }; + callback.handle_nonuser_event(resized_event); } } } @@ -253,23 +361,105 @@ impl Handler { pub(crate) enum AppState {} impl AppState { - pub fn set_callback(callback: Weak>, window_target: Rc>) { + /// Associate the application's event callback with the (global static) Handler state + /// + /// # Safety + /// This is ignoring the lifetime of the application callback (which may not be 'static) + /// and can lead to undefined behaviour if the callback is not cleared before the end of + /// its real lifetime. + /// + /// All public APIs that take an event callback (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_callback` with + /// a call to `clear_callback` before returning to avoid undefined behaviour. + pub unsafe fn set_callback( + callback: Weak>, + window_target: Rc>, + receiver: Rc>, + ) { *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { callback, window_target, + receiver, })); } - pub fn exit() -> i32 { + pub fn clear_callback() { + HANDLER.callback.lock().unwrap().take(); + } + + pub fn is_launched() -> bool { + HANDLER.is_launched() + } + + pub fn is_running() -> bool { + HANDLER.is_running() + } + + // If `pump_events` is called to progress the event loop then we bootstrap the event + // loop via `[NSApp run]` but will use `CFRunLoopRunInMode` for subsequent calls to + // `pump_events` + pub fn request_stop_on_launch() { + HANDLER.request_stop_app_on_launch(); + } + + pub fn set_stop_app_before_wait(stop_before_wait: bool) { + HANDLER.set_stop_app_before_wait(stop_before_wait); + } + + pub fn set_stop_app_after_wait(stop_after_wait: bool) { + HANDLER.set_stop_app_after_wait(stop_after_wait); + } + + pub fn set_wait_timeout(timeout: Option) { + HANDLER.set_wait_timeout(timeout); + } + + pub fn set_stop_app_on_redraw_requested(stop_on_redraw: bool) { + HANDLER.set_stop_app_on_redraw_requested(stop_on_redraw); + } + + pub fn set_control_flow(control_flow: ControlFlow) { + HANDLER.set_control_flow(control_flow) + } + + pub fn control_flow() -> ControlFlow { + HANDLER.control_flow() + } + + pub fn internal_exit() { HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::LoopDestroyed)); + HANDLER.handle_nonuser_event(Event::LoopExiting); + HANDLER.set_in_callback(false); + HANDLER.internal_exit(); + Self::clear_callback(); + } + + pub fn exit() { + HANDLER.exit() + } + + pub fn clear_exit() { + HANDLER.clear_exit() + } + + pub fn exiting() -> bool { + HANDLER.exiting() + } + + pub fn dispatch_init_events() { + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(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(Event::Resumed); HANDLER.set_in_callback(false); - HANDLER.callback.lock().unwrap().take(); - if let ControlFlow::ExitWithCode(code) = HANDLER.get_old_and_new_control_flow().1 { - code - } else { - 0 - } + } + + pub fn start_running() { + debug_assert!(HANDLER.is_launched()); + + HANDLER.set_running(); + Self::dispatch_init_events() } pub fn launched( @@ -286,34 +476,51 @@ impl AppState { window_activation_hack(&app); app.activateIgnoringOtherApps(activate_ignoring_other_apps); - HANDLER.set_ready(); + HANDLER.set_launched(); HANDLER.waker().start(); if create_default_menu { // The menubar initialization should be before the `NewEvents` event, to allow // overriding of the default menu even if it's created menu::initialize(); } - HANDLER.set_in_callback(true); - 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); + + Self::start_running(); + + // If the `NSApp` is being launched via `EventLoop::pump_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if HANDLER.should_stop_app_on_launch() { + // Note: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`[NSApp run]` effectively + // ignored the attempt to stop the RunLoop and re-started it.). So we + // return from `pump_events` by stopping the `NSApp` + Self::stop(); + } } + // Called by RunLoopObserver after finishing waiting for new events pub fn wakeup(panic_info: Weak) { let panic_info = panic_info .upgrade() .expect("The panic info must exist here. This failure indicates a developer error."); // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 - if panic_info.is_panicking() || !HANDLER.is_ready() || HANDLER.get_in_callback() { + if panic_info.is_panicking() + || HANDLER.get_in_callback() + || !HANDLER.have_callback() + || !HANDLER.is_running() + { return; } + + if HANDLER.should_stop_app_after_wait() { + Self::stop(); + } + let start = HANDLER.get_start_time().unwrap(); - let cause = match HANDLER.get_control_flow_and_update_prev() { + let cause = match HANDLER.control_flow() { ControlFlow::Poll => StartCause::Poll, ControlFlow::Wait => StartCause::WaitCancelled { start, @@ -332,10 +539,9 @@ impl AppState { } } } - ControlFlow::ExitWithCode(_) => StartCause::Poll, //panic!("unexpected `ControlFlow::Exit`"), }; HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(cause))); + HANDLER.handle_nonuser_event(Event::NewEvents(cause)); HANDLER.set_in_callback(false); } @@ -355,61 +561,127 @@ impl AppState { // Redraw request might come out of order from the OS. // -> Don't go back into the callback when our callstack originates from there if !HANDLER.in_callback.swap(true, Ordering::AcqRel) { - HANDLER - .handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id))); + HANDLER.handle_nonuser_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); HANDLER.set_in_callback(false); + + // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested events + // as a way to ensure that `pump_events` can't block an external loop indefinitely + if HANDLER.should_stop_app_on_redraw_requested() { + AppState::stop(); + } } } - pub fn queue_event(wrapper: EventWrapper) { + pub fn queue_event(event: Event) { if !is_main_thread() { - panic!("Event queued from different thread: {wrapper:#?}"); + panic!("Event queued from different thread: {event:#?}"); } - HANDLER.events().push_back(wrapper); + HANDLER.events().push_back(EventWrapper::StaticEvent(event)); + } + + pub fn queue_static_scale_factor_changed_event( + window: Id, + suggested_size: PhysicalSize, + scale_factor: f64, + ) { + HANDLER + .events() + .push_back(EventWrapper::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + }); + } + + pub fn stop() { + let app = NSApp(); + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + app.postEvent_atStart(&NSEvent::dummy(), true); + }); } + // Called by RunLoopObserver before waiting for new events pub fn cleared(panic_info: Weak) { let panic_info = panic_info .upgrade() .expect("The panic info must exist here. This failure indicates a developer error."); // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 - if panic_info.is_panicking() || !HANDLER.is_ready() || HANDLER.get_in_callback() { + // XXX: how does it make sense that `get_in_callback()` can ever return `true` here if we're + // about to return to the `CFRunLoop` to poll for new events? + if panic_info.is_panicking() + || HANDLER.get_in_callback() + || !HANDLER.have_callback() + || !HANDLER.is_running() + { return; } HANDLER.set_in_callback(true); HANDLER.handle_user_events(); for event in HANDLER.take_events() { - HANDLER.handle_nonuser_event(event); + match event { + EventWrapper::StaticEvent(event) => { + HANDLER.handle_nonuser_event(event); + } + EventWrapper::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + } => { + HANDLER.handle_scale_factor_changed_event( + &window, + suggested_size, + scale_factor, + ); + } + } } - HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); + for window_id in HANDLER.should_redraw() { - HANDLER - .handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id))); + HANDLER.handle_nonuser_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); } - HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); + + HANDLER.handle_nonuser_event(Event::AboutToWait); HANDLER.set_in_callback(false); - if HANDLER.should_exit() { - let app = NSApp(); - autoreleasepool(|_| { - app.stop(None); - // To stop event loop immediately, we need to post some event here. - app.postEvent_atStart(&NSEvent::dummy(), true); - }); + if HANDLER.exiting() { + Self::stop(); } - HANDLER.update_start_time(); - match HANDLER.get_old_and_new_control_flow() { - (ControlFlow::ExitWithCode(_), _) | (_, ControlFlow::ExitWithCode(_)) => (), - (old, new) if old == new => (), - (_, ControlFlow::Wait) => HANDLER.waker().stop(), - (_, ControlFlow::WaitUntil(instant)) => HANDLER.waker().start_at(instant), - (_, ControlFlow::Poll) => HANDLER.waker().start(), + + if HANDLER.should_stop_app_before_wait() { + Self::stop(); } + HANDLER.update_start_time(); + let wait_timeout = HANDLER.wait_timeout(); // configured by pump_events + let app_timeout = match HANDLER.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Instant::now()), + ControlFlow::WaitUntil(instant) => Some(instant), + }; + HANDLER + .waker() + .start_at(min_timeout(wait_timeout, app_timeout)); } } +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + /// A hack to make activation of multiple windows work when creating them before /// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. /// diff --git a/src/platform_impl/macos/appkit/appearance.rs b/src/platform_impl/macos/appkit/appearance.rs index c13abbff02..6db7c6d039 100644 --- a/src/platform_impl/macos/appkit/appearance.rs +++ b/src/platform_impl/macos/appkit/appearance.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{NSArray, NSObject, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{NSArray, NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -8,6 +8,7 @@ extern_class!( unsafe impl ClassType for NSAppearance { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -15,15 +16,13 @@ type NSAppearanceName = NSString; extern_methods!( unsafe impl NSAppearance { - pub fn appearanceNamed(name: &NSAppearanceName) -> Id { - unsafe { msg_send_id![Self::class(), appearanceNamed: name] } - } + #[method_id(appearanceNamed:)] + pub fn appearanceNamed(name: &NSAppearanceName) -> Id; + #[method_id(bestMatchFromAppearancesWithNames:)] pub fn bestMatchFromAppearancesWithNames( &self, appearances: &NSArray, - ) -> Id { - unsafe { msg_send_id![self, bestMatchFromAppearancesWithNames: appearances,] } - } + ) -> Id; } ); diff --git a/src/platform_impl/macos/appkit/application.rs b/src/platform_impl/macos/appkit/application.rs index 27183423e0..f5f2e77d14 100644 --- a/src/platform_impl/macos/appkit/application.rs +++ b/src/platform_impl/macos/appkit/application.rs @@ -1,7 +1,7 @@ -use objc2::foundation::{MainThreadMarker, NSArray, NSInteger, NSObject, NSUInteger}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{MainThreadMarker, NSArray, NSInteger, NSObject, NSUInteger}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use objc2::{Encode, Encoding}; use super::{NSAppearance, NSEvent, NSMenu, NSResponder, NSWindow}; @@ -13,10 +13,11 @@ extern_class!( unsafe impl ClassType for NSApplication { #[inherits(NSObject)] type Super = NSResponder; + type Mutability = mutability::InteriorMutable; } ); -pub(crate) fn NSApp() -> Id { +pub(crate) fn NSApp() -> Id { // TODO: Only allow access from main thread NSApplication::shared(unsafe { MainThreadMarker::new_unchecked() }) } @@ -26,70 +27,66 @@ extern_methods!( /// This can only be called on the main thread since it may initialize /// the application and since it's parameters may be changed by the main /// thread at any time (hence it is only safe to access on the main thread). - pub fn shared(_mtm: MainThreadMarker) -> Id { + pub fn shared(_mtm: MainThreadMarker) -> Id { let app: Option<_> = unsafe { msg_send_id![Self::class(), sharedApplication] }; // SAFETY: `sharedApplication` always initializes the app if it isn't already unsafe { app.unwrap_unchecked() } } - pub fn currentEvent(&self) -> Option> { - unsafe { msg_send_id![self, currentEvent] } - } + #[method_id(currentEvent)] + pub fn currentEvent(&self) -> Option>; - #[sel(postEvent:atStart:)] + #[method(postEvent:atStart:)] pub fn postEvent_atStart(&self, event: &NSEvent, front_of_queue: bool); - #[sel(presentationOptions)] + #[method(presentationOptions)] pub fn presentationOptions(&self) -> NSApplicationPresentationOptions; - pub fn windows(&self) -> Id, Shared> { - unsafe { msg_send_id![self, windows] } - } + #[method_id(windows)] + pub fn windows(&self) -> Id>; - pub fn keyWindow(&self) -> Option> { - unsafe { msg_send_id![self, keyWindow] } - } + #[method_id(keyWindow)] + pub fn keyWindow(&self) -> Option>; // TODO: NSApplicationDelegate - #[sel(setDelegate:)] - pub fn setDelegate(&self, delegate: &Object); + #[method(setDelegate:)] + pub fn setDelegate(&self, delegate: &AnyObject); - #[sel(setPresentationOptions:)] + #[method(setPresentationOptions:)] pub fn setPresentationOptions(&self, options: NSApplicationPresentationOptions); - #[sel(hide:)] - pub fn hide(&self, sender: Option<&Object>); + #[method(hide:)] + pub fn hide(&self, sender: Option<&AnyObject>); - #[sel(orderFrontCharacterPalette:)] + #[method(orderFrontCharacterPalette:)] #[allow(dead_code)] - pub fn orderFrontCharacterPalette(&self, sender: Option<&Object>); + pub fn orderFrontCharacterPalette(&self, sender: Option<&AnyObject>); - #[sel(hideOtherApplications:)] - pub fn hideOtherApplications(&self, sender: Option<&Object>); + #[method(hideOtherApplications:)] + pub fn hideOtherApplications(&self, sender: Option<&AnyObject>); - #[sel(stop:)] - pub fn stop(&self, sender: Option<&Object>); + #[method(stop:)] + pub fn stop(&self, sender: Option<&AnyObject>); - #[sel(activateIgnoringOtherApps:)] + #[method(activateIgnoringOtherApps:)] pub fn activateIgnoringOtherApps(&self, ignore: bool); - #[sel(requestUserAttention:)] + #[method(requestUserAttention:)] pub fn requestUserAttention(&self, type_: NSRequestUserAttentionType) -> NSInteger; - #[sel(setActivationPolicy:)] + #[method(setActivationPolicy:)] pub fn setActivationPolicy(&self, policy: NSApplicationActivationPolicy) -> bool; - #[sel(setMainMenu:)] + #[method(setMainMenu:)] pub fn setMainMenu(&self, menu: &NSMenu); - pub fn effectiveAppearance(&self) -> Id { - unsafe { msg_send_id![self, effectiveAppearance] } - } + #[method_id(effectiveAppearance)] + pub fn effectiveAppearance(&self) -> Id; - #[sel(setAppearance:)] + #[method(setAppearance:)] pub fn setAppearance(&self, appearance: Option<&NSAppearance>); - #[sel(run)] + #[method(run)] pub unsafe fn run(&self); } ); diff --git a/src/platform_impl/macos/appkit/button.rs b/src/platform_impl/macos/appkit/button.rs index c4a61ca1aa..eb3c57a543 100644 --- a/src/platform_impl/macos/appkit/button.rs +++ b/src/platform_impl/macos/appkit/button.rs @@ -1,5 +1,5 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{extern_class, mutability, ClassType}; use super::{NSControl, NSResponder, NSView}; @@ -10,5 +10,6 @@ extern_class!( unsafe impl ClassType for NSButton { #[inherits(NSView, NSResponder, NSObject)] type Super = NSControl; + type Mutability = mutability::InteriorMutable; } ); diff --git a/src/platform_impl/macos/appkit/color.rs b/src/platform_impl/macos/appkit/color.rs index 59399731e0..b5477a1eb9 100644 --- a/src/platform_impl/macos/appkit/color.rs +++ b/src/platform_impl/macos/appkit/color.rs @@ -1,6 +1,6 @@ -use objc2::foundation::NSObject; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::NSObject; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( /// An object that stores color data and sometimes opacity (alpha value). @@ -11,6 +11,7 @@ extern_class!( unsafe impl ClassType for NSColor { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -21,8 +22,7 @@ unsafe impl Sync for NSColor {} extern_methods!( unsafe impl NSColor { - pub fn clear() -> Id { - unsafe { msg_send_id![Self::class(), clearColor] } - } + #[method_id(clearColor)] + pub fn clear() -> Id; } ); diff --git a/src/platform_impl/macos/appkit/control.rs b/src/platform_impl/macos/appkit/control.rs index 1ca5219592..d38f4b1096 100644 --- a/src/platform_impl/macos/appkit/control.rs +++ b/src/platform_impl/macos/appkit/control.rs @@ -1,5 +1,5 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, extern_methods, ClassType}; +use icrate::Foundation::NSObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; use super::{NSResponder, NSView}; @@ -10,15 +10,16 @@ extern_class!( unsafe impl ClassType for NSControl { #[inherits(NSResponder, NSObject)] type Super = NSView; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl NSControl { - #[sel(setEnabled:)] + #[method(setEnabled:)] pub fn setEnabled(&self, enabled: bool); - #[sel(isEnabled)] + #[method(isEnabled)] pub fn isEnabled(&self) -> bool; } ); diff --git a/src/platform_impl/macos/appkit/cursor.rs b/src/platform_impl/macos/appkit/cursor.rs index dd86026c5e..6377ad420c 100644 --- a/src/platform_impl/macos/appkit/cursor.rs +++ b/src/platform_impl/macos/appkit/cursor.rs @@ -1,9 +1,12 @@ use once_cell::sync::Lazy; -use objc2::foundation::{NSData, NSDictionary, NSNumber, NSObject, NSPoint, NSString}; -use objc2::rc::{DefaultId, Id, Shared}; +use icrate::ns_string; +use icrate::Foundation::{ + NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSString, +}; +use objc2::rc::{DefaultId, Id}; use objc2::runtime::Sel; -use objc2::{extern_class, extern_methods, msg_send_id, ns_string, sel, ClassType}; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType}; use super::NSImage; use crate::window::CursorIcon; @@ -15,6 +18,7 @@ extern_class!( unsafe impl ClassType for NSCursor { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -29,7 +33,7 @@ macro_rules! def_cursor { pub fn $name:ident(); )*} => {$( $(#[$($m)*])* - pub fn $name() -> Id { + pub fn $name() -> Id { unsafe { msg_send_id![Self::class(), $name] } } )*}; @@ -41,7 +45,7 @@ macro_rules! def_undocumented_cursor { pub fn $name:ident(); )*} => {$( $(#[$($m)*])* - pub fn $name() -> Id { + pub fn $name() -> Id { unsafe { Self::from_selector(sel!($name)).unwrap_or_else(|| Default::default()) } } )*}; @@ -71,12 +75,11 @@ extern_methods!( ); // Creating cursors should be thread-safe, though using them for anything probably isn't. - pub fn new(image: &NSImage, hotSpot: NSPoint) -> Id { - let this = unsafe { msg_send_id![Self::class(), alloc] }; - unsafe { msg_send_id![this, initWithImage: image, hotSpot: hotSpot] } + pub fn new(image: &NSImage, hotSpot: NSPoint) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithImage: image, hotSpot: hotSpot] } } - pub fn invisible() -> Id { + pub fn invisible() -> Id { // 16x16 GIF data for invisible cursor // You can reproduce this via ImageMagick. // $ convert -size 16x16 xc:none cursor.gif @@ -87,7 +90,7 @@ extern_methods!( 0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B, ]; - static CURSOR: Lazy> = Lazy::new(|| { + static CURSOR: Lazy> = Lazy::new(|| { // TODO: Consider using `dataWithBytesNoCopy:` let data = NSData::with_bytes(CURSOR_BYTES); let image = NSImage::new_with_data(&data); @@ -100,14 +103,13 @@ extern_methods!( /// Undocumented cursors unsafe impl NSCursor { - #[sel(respondsToSelector:)] + #[method(respondsToSelector:)] fn class_responds_to(sel: Sel) -> bool; - unsafe fn from_selector_unchecked(sel: Sel) -> Id { - unsafe { msg_send_id![Self::class(), performSelector: sel] } - } + #[method_id(performSelector:)] + unsafe fn from_selector_unchecked(sel: Sel) -> Id; - unsafe fn from_selector(sel: Sel) -> Option> { + unsafe fn from_selector(sel: Sel) -> Option> { if Self::class_responds_to(sel) { Some(unsafe { Self::from_selector_unchecked(sel) }) } else { @@ -143,20 +145,20 @@ extern_methods!( unsafe impl NSCursor { // Note that loading `busybutclickable` with this code won't animate // the frames; instead you'll just get them all in a column. - unsafe fn load_webkit_cursor(name: &NSString) -> Id { + unsafe fn load_webkit_cursor(name: &NSString) -> Id { // Snatch a cursor from WebKit; They fit the style of the native // cursors, and will seem completely standard to macOS users. // // https://stackoverflow.com/a/21786835/5435443 let root = ns_string!("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors"); - let cursor_path = root.join_path(name); + let cursor_path = root.stringByAppendingPathComponent(name); - let pdf_path = cursor_path.join_path(ns_string!("cursor.pdf")); + let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf")); let image = NSImage::new_by_referencing_file(&pdf_path); // TODO: Handle PLists better - let info_path = cursor_path.join_path(ns_string!("info.plist")); - let info: Id, Shared> = unsafe { + let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist")); + let info: Id> = unsafe { msg_send_id![ >::class(), dictionaryWithContentsOfFile: &*info_path, @@ -183,18 +185,18 @@ extern_methods!( Self::new(&image, hotspot) } - pub fn moveCursor() -> Id { + pub fn moveCursor() -> Id { unsafe { Self::load_webkit_cursor(ns_string!("move")) } } - pub fn cellCursor() -> Id { + pub fn cellCursor() -> Id { unsafe { Self::load_webkit_cursor(ns_string!("cell")) } } } ); impl NSCursor { - pub fn from_icon(icon: CursorIcon) -> Id { + pub fn from_icon(icon: CursorIcon) -> Id { match icon { CursorIcon::Default => Default::default(), CursorIcon::Pointer => Self::pointingHandCursor(), @@ -233,9 +235,7 @@ impl NSCursor { } impl DefaultId for NSCursor { - type Ownership = Shared; - - fn default_id() -> Id { + fn default_id() -> Id { Self::arrowCursor() } } diff --git a/src/platform_impl/macos/appkit/event.rs b/src/platform_impl/macos/appkit/event.rs index 5952c43c7e..f749639904 100644 --- a/src/platform_impl/macos/appkit/event.rs +++ b/src/platform_impl/macos/appkit/event.rs @@ -1,11 +1,11 @@ use std::os::raw::c_ushort; -use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{ +use icrate::Foundation::{ CGFloat, NSCopying, NSInteger, NSObject, NSPoint, NSString, NSTimeInterval, NSUInteger, }; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::encode::{Encode, Encoding}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -13,6 +13,7 @@ extern_class!( unsafe impl ClassType for NSEvent { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -24,6 +25,17 @@ extern_class!( extern_methods!( unsafe impl NSEvent { + #[method_id( + otherEventWithType: + location: + modifierFlags: + timestamp: + windowNumber: + context: + subtype: + data1: + data2: + )] unsafe fn otherEventWithType( type_: NSEventType, location: NSPoint, @@ -34,24 +46,9 @@ extern_methods!( subtype: NSEventSubtype, data1: NSInteger, data2: NSInteger, - ) -> Id { - unsafe { - msg_send_id![ - Self::class(), - otherEventWithType: type_, - location: location, - modifierFlags: flags, - timestamp: time, - windowNumber: window_num, - context: context, - subtype: subtype, - data1: data1, - data2: data2, - ] - } - } + ) -> Id; - pub fn dummy() -> Id { + pub fn dummy() -> Id { unsafe { Self::otherEventWithType( NSEventType::NSApplicationDefined, @@ -67,6 +64,18 @@ extern_methods!( } } + #[method_id( + keyEventWithType: + location: + modifierFlags: + timestamp: + windowNumber: + context: + characters: + charactersIgnoringModifiers: + isARepeat: + keyCode: + )] pub fn keyEventWithType( type_: NSEventType, location: NSPoint, @@ -78,92 +87,74 @@ extern_methods!( characters_ignoring_modifiers: &NSString, is_a_repeat: bool, scancode: c_ushort, - ) -> Id { - unsafe { - msg_send_id![ - Self::class(), - keyEventWithType: type_, - location: location, - modifierFlags: modifier_flags, - timestamp: timestamp, - windowNumber: window_num, - context: context, - characters: characters, - charactersIgnoringModifiers: characters_ignoring_modifiers, - isARepeat: is_a_repeat, - keyCode: scancode, - ] - } - } + ) -> Id; - #[sel(locationInWindow)] + #[method(locationInWindow)] pub fn locationInWindow(&self) -> NSPoint; // TODO: MainThreadMarker - #[sel(pressedMouseButtons)] + #[method(pressedMouseButtons)] pub fn pressedMouseButtons() -> NSUInteger; - #[sel(modifierFlags)] + #[method(modifierFlags)] pub fn modifierFlags(&self) -> NSEventModifierFlags; - #[sel(type)] + #[method(type)] pub fn type_(&self) -> NSEventType; - #[sel(keyCode)] + #[method(keyCode)] pub fn key_code(&self) -> c_ushort; - #[sel(magnification)] + #[method(magnification)] pub fn magnification(&self) -> CGFloat; - #[sel(phase)] + #[method(phase)] pub fn phase(&self) -> NSEventPhase; - #[sel(momentumPhase)] + #[method(momentumPhase)] pub fn momentumPhase(&self) -> NSEventPhase; - #[sel(deltaX)] + #[method(deltaX)] pub fn deltaX(&self) -> CGFloat; - #[sel(deltaY)] + #[method(deltaY)] pub fn deltaY(&self) -> CGFloat; - #[sel(buttonNumber)] + #[method(buttonNumber)] pub fn buttonNumber(&self) -> NSInteger; - #[sel(scrollingDeltaX)] + #[method(scrollingDeltaX)] pub fn scrollingDeltaX(&self) -> CGFloat; - #[sel(scrollingDeltaY)] + #[method(scrollingDeltaY)] pub fn scrollingDeltaY(&self) -> CGFloat; - #[sel(hasPreciseScrollingDeltas)] + #[method(hasPreciseScrollingDeltas)] pub fn hasPreciseScrollingDeltas(&self) -> bool; - #[sel(rotation)] + #[method(rotation)] pub fn rotation(&self) -> f32; - #[sel(pressure)] + #[method(pressure)] pub fn pressure(&self) -> f32; - #[sel(stage)] + #[method(stage)] pub fn stage(&self) -> NSInteger; - #[sel(isARepeat)] + #[method(isARepeat)] pub fn is_a_repeat(&self) -> bool; - #[sel(windowNumber)] + #[method(windowNumber)] pub fn window_number(&self) -> NSInteger; - #[sel(timestamp)] + #[method(timestamp)] pub fn timestamp(&self) -> NSTimeInterval; - pub fn characters(&self) -> Option> { - unsafe { msg_send_id![self, characters] } - } + #[method_id(characters)] + pub fn characters(&self) -> Option>; - pub fn charactersIgnoringModifiers(&self) -> Option> { - unsafe { msg_send_id![self, charactersIgnoringModifiers] } - } + #[method_id(charactersIgnoringModifiers)] + pub fn charactersIgnoringModifiers(&self) -> Option>; pub fn lshift_pressed(&self) -> bool { let raw_modifiers = self.modifierFlags().bits() as u32; @@ -207,10 +198,7 @@ extern_methods!( } ); -unsafe impl NSCopying for NSEvent { - type Ownership = Shared; - type Output = NSEvent; -} +unsafe impl NSCopying for NSEvent {} // The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 const NX_DEVICELCTLKEYMASK: u32 = 0x00000001; diff --git a/src/platform_impl/macos/appkit/image.rs b/src/platform_impl/macos/appkit/image.rs index af5674d338..0b5944c3da 100644 --- a/src/platform_impl/macos/appkit/image.rs +++ b/src/platform_impl/macos/appkit/image.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{NSData, NSObject, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{NSData, NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; extern_class!( // TODO: Can this be mutable? @@ -9,6 +9,7 @@ extern_class!( unsafe impl ClassType for NSImage { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -24,14 +25,12 @@ unsafe impl Sync for NSImage {} extern_methods!( unsafe impl NSImage { - pub fn new_by_referencing_file(path: &NSString) -> Id { - let this = unsafe { msg_send_id![Self::class(), alloc] }; - unsafe { msg_send_id![this, initByReferencingFile: path] } + pub fn new_by_referencing_file(path: &NSString) -> Id { + unsafe { msg_send_id![Self::alloc(), initByReferencingFile: path] } } - pub fn new_with_data(data: &NSData) -> Id { - let this = unsafe { msg_send_id![Self::class(), alloc] }; - unsafe { msg_send_id![this, initWithData: data] } + pub fn new_with_data(data: &NSData) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithData: data] } } } ); diff --git a/src/platform_impl/macos/appkit/menu.rs b/src/platform_impl/macos/appkit/menu.rs index cb8b47facb..e03ec95945 100644 --- a/src/platform_impl/macos/appkit/menu.rs +++ b/src/platform_impl/macos/appkit/menu.rs @@ -1,6 +1,6 @@ -use objc2::foundation::NSObject; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::NSObject; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; use super::NSMenuItem; @@ -10,16 +10,16 @@ extern_class!( unsafe impl ClassType for NSMenu { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl NSMenu { - pub fn new() -> Id { - unsafe { msg_send_id![Self::class(), new] } - } + #[method_id(new)] + pub fn new() -> Id; - #[sel(addItem:)] + #[method(addItem:)] pub fn addItem(&self, item: &NSMenuItem); } ); diff --git a/src/platform_impl/macos/appkit/menu_item.rs b/src/platform_impl/macos/appkit/menu_item.rs index 6daed88ee3..63c9a501cc 100644 --- a/src/platform_impl/macos/appkit/menu_item.rs +++ b/src/platform_impl/macos/appkit/menu_item.rs @@ -1,7 +1,7 @@ -use objc2::foundation::{NSObject, NSString}; -use objc2::rc::{Id, Shared}; +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; use objc2::runtime::Sel; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; use super::{NSEventModifierFlags, NSMenu}; @@ -11,23 +11,19 @@ extern_class!( unsafe impl ClassType for NSMenuItem { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl NSMenuItem { - pub fn new() -> Id { - unsafe { msg_send_id![Self::class(), new] } - } + #[method_id(new)] + pub fn new() -> Id; - pub fn newWithTitle( - title: &NSString, - action: Sel, - key_equivalent: &NSString, - ) -> Id { + pub fn newWithTitle(title: &NSString, action: Sel, key_equivalent: &NSString) -> Id { unsafe { msg_send_id![ - msg_send_id![Self::class(), alloc], + Self::alloc(), initWithTitle: title, action: action, keyEquivalent: key_equivalent, @@ -35,14 +31,13 @@ extern_methods!( } } - pub fn separatorItem() -> Id { - unsafe { msg_send_id![Self::class(), separatorItem] } - } + #[method_id(separatorItem)] + pub fn separatorItem() -> Id; - #[sel(setKeyEquivalentModifierMask:)] + #[method(setKeyEquivalentModifierMask:)] pub fn setKeyEquivalentModifierMask(&self, mask: NSEventModifierFlags); - #[sel(setSubmenu:)] + #[method(setSubmenu:)] pub fn setSubmenu(&self, submenu: &NSMenu); } ); diff --git a/src/platform_impl/macos/appkit/mod.rs b/src/platform_impl/macos/appkit/mod.rs index 7f96e35d5c..832fc149c1 100644 --- a/src/platform_impl/macos/appkit/mod.rs +++ b/src/platform_impl/macos/appkit/mod.rs @@ -24,6 +24,8 @@ mod menu_item; mod pasteboard; mod responder; mod screen; +mod tab_group; +mod text_input_client; mod text_input_context; mod version; mod view; @@ -49,12 +51,15 @@ pub(crate) use self::pasteboard::{NSFilenamesPboardType, NSPasteboard, NSPastebo pub(crate) use self::responder::NSResponder; #[allow(unused_imports)] pub(crate) use self::screen::{NSDeviceDescriptionKey, NSScreen}; +pub(crate) use self::tab_group::NSWindowTabGroup; +pub(crate) use self::text_input_client::NSTextInputClient; pub(crate) use self::text_input_context::NSTextInputContext; pub(crate) use self::version::NSAppKitVersion; pub(crate) use self::view::{NSTrackingRectTag, NSView}; pub(crate) use self::window::{ NSBackingStoreType, NSWindow, NSWindowButton, NSWindowLevel, NSWindowOcclusionState, - NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTitleVisibility, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, + NSWindowTitleVisibility, }; #[link(name = "AppKit", kind = "framework")] diff --git a/src/platform_impl/macos/appkit/pasteboard.rs b/src/platform_impl/macos/appkit/pasteboard.rs index 6564540dc6..01a9408983 100644 --- a/src/platform_impl/macos/appkit/pasteboard.rs +++ b/src/platform_impl/macos/appkit/pasteboard.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{NSObject, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -8,14 +8,14 @@ extern_class!( unsafe impl ClassType for NSPasteboard { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl NSPasteboard { - pub fn propertyListForType(&self, type_: &NSPasteboardType) -> Id { - unsafe { msg_send_id![self, propertyListForType: type_] } - } + #[method_id(propertyListForType:)] + pub fn propertyListForType(&self, type_: &NSPasteboardType) -> Id; } ); diff --git a/src/platform_impl/macos/appkit/responder.rs b/src/platform_impl/macos/appkit/responder.rs index a1ae1877bc..11f3bddf06 100644 --- a/src/platform_impl/macos/appkit/responder.rs +++ b/src/platform_impl/macos/appkit/responder.rs @@ -1,15 +1,15 @@ -use objc2::foundation::{NSArray, NSObject}; -use objc2::rc::Shared; -use objc2::{extern_class, extern_methods, ClassType}; +use icrate::Foundation::{NSArray, NSObject}; +use objc2::{extern_class, extern_methods, mutability, ClassType}; use super::NSEvent; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] - pub(crate) struct NSResponder; + pub struct NSResponder; unsafe impl ClassType for NSResponder { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -17,8 +17,7 @@ extern_class!( extern_methods!( unsafe impl NSResponder { - // TODO: Allow "immutably" on main thread - #[sel(interpretKeyEvents:)] - pub unsafe fn interpretKeyEvents(&mut self, events: &NSArray); + #[method(interpretKeyEvents:)] + pub(crate) unsafe fn interpretKeyEvents(&self, events: &NSArray); } ); diff --git a/src/platform_impl/macos/appkit/screen.rs b/src/platform_impl/macos/appkit/screen.rs index 7eb8b2a751..aa10123936 100644 --- a/src/platform_impl/macos/appkit/screen.rs +++ b/src/platform_impl/macos/appkit/screen.rs @@ -1,7 +1,8 @@ -use objc2::foundation::{CGFloat, NSArray, NSDictionary, NSNumber, NSObject, NSRect, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, msg_send_id, ns_string, ClassType}; +use icrate::ns_string; +use icrate::Foundation::{CGFloat, NSArray, NSDictionary, NSNumber, NSObject, NSRect, NSString}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -9,6 +10,7 @@ extern_class!( unsafe impl ClassType for NSScreen { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); @@ -17,26 +19,21 @@ extern_class!( extern_methods!( unsafe impl NSScreen { /// The application object must have been created. - pub fn main() -> Option> { - unsafe { msg_send_id![Self::class(), mainScreen] } - } + #[method_id(mainScreen)] + pub fn main() -> Option>; /// The application object must have been created. - pub fn screens() -> Id, Shared> { - unsafe { msg_send_id![Self::class(), screens] } - } + #[method_id(screens)] + pub fn screens() -> Id>; - #[sel(frame)] + #[method(frame)] pub fn frame(&self) -> NSRect; - #[sel(visibleFrame)] + #[method(visibleFrame)] pub fn visibleFrame(&self) -> NSRect; - pub fn deviceDescription( - &self, - ) -> Id, Shared> { - unsafe { msg_send_id![self, deviceDescription] } - } + #[method_id(deviceDescription)] + pub fn deviceDescription(&self) -> Id>; pub fn display_id(&self) -> u32 { let key = ns_string!("NSScreenNumber"); @@ -52,7 +49,7 @@ extern_methods!( let obj = device_description .get(key) .expect("failed getting screen display id from device description"); - let obj: *const Object = obj; + let obj: *const AnyObject = obj; let obj: *const NSNumber = obj.cast(); let obj: &NSNumber = unsafe { &*obj }; @@ -60,7 +57,7 @@ extern_methods!( }) } - #[sel(backingScaleFactor)] + #[method(backingScaleFactor)] pub fn backingScaleFactor(&self) -> CGFloat; } ); diff --git a/src/platform_impl/macos/appkit/tab_group.rs b/src/platform_impl/macos/appkit/tab_group.rs new file mode 100644 index 0000000000..e52bf572e6 --- /dev/null +++ b/src/platform_impl/macos/appkit/tab_group.rs @@ -0,0 +1,31 @@ +use icrate::Foundation::{NSArray, NSObject}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::NSWindow; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSWindowTabGroup; + + unsafe impl ClassType for NSWindowTabGroup { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSWindowTabGroup { + #[method(selectNextTab)] + pub fn selectNextTab(&self); + + #[method(selectPreviousTab)] + pub fn selectPreviousTab(&self); + + #[method_id(windows)] + pub fn tabbedWindows(&self) -> Option>>; + + #[method(setSelectedWindow:)] + pub fn setSelectedWindow(&self, window: &NSWindow); + } +); diff --git a/src/platform_impl/macos/appkit/text_input_client.rs b/src/platform_impl/macos/appkit/text_input_client.rs new file mode 100644 index 0000000000..12d03fcb5c --- /dev/null +++ b/src/platform_impl/macos/appkit/text_input_client.rs @@ -0,0 +1,9 @@ +use objc2::{extern_protocol, ProtocolType}; + +extern_protocol!( + pub(crate) unsafe trait NSTextInputClient { + // TODO: Methods + } + + unsafe impl ProtocolType for dyn NSTextInputClient {} +); diff --git a/src/platform_impl/macos/appkit/text_input_context.rs b/src/platform_impl/macos/appkit/text_input_context.rs index 79a9611b5f..ee72079872 100644 --- a/src/platform_impl/macos/appkit/text_input_context.rs +++ b/src/platform_impl/macos/appkit/text_input_context.rs @@ -1,6 +1,6 @@ -use objc2::foundation::{NSObject, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; type NSTextInputSourceIdentifier = NSString; @@ -11,21 +11,19 @@ extern_class!( unsafe impl ClassType for NSTextInputContext { type Super = NSObject; + type Mutability = mutability::InteriorMutable; } ); extern_methods!( unsafe impl NSTextInputContext { - #[sel(invalidateCharacterCoordinates)] + #[method(invalidateCharacterCoordinates)] pub fn invalidateCharacterCoordinates(&self); - #[sel(discardMarkedText)] + #[method(discardMarkedText)] pub fn discardMarkedText(&self); - pub fn selectedKeyboardInputSource( - &self, - ) -> Option> { - unsafe { msg_send_id![self, selectedKeyboardInputSource] } - } + #[method_id(selectedKeyboardInputSource)] + pub fn selectedKeyboardInputSource(&self) -> Option>; } ); diff --git a/src/platform_impl/macos/appkit/view.rs b/src/platform_impl/macos/appkit/view.rs index b72b712df4..d6720ec529 100644 --- a/src/platform_impl/macos/appkit/view.rs +++ b/src/platform_impl/macos/appkit/view.rs @@ -2,10 +2,10 @@ use std::ffi::c_void; use std::num::NonZeroIsize; use std::ptr; -use objc2::foundation::{NSObject, NSPoint, NSRect}; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use icrate::Foundation::{NSObject, NSPoint, NSRect}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; use super::{NSCursor, NSResponder, NSTextInputContext, NSWindow}; @@ -16,6 +16,7 @@ extern_class!( unsafe impl ClassType for NSView { #[inherits(NSObject)] type Super = NSResponder; + type Mutability = mutability::InteriorMutable; } ); @@ -31,51 +32,46 @@ extern_class!( extern_methods!( /// Getter methods unsafe impl NSView { - #[sel(frame)] + #[method(frame)] pub fn frame(&self) -> NSRect; - #[sel(bounds)] + #[method(bounds)] pub fn bounds(&self) -> NSRect; + #[method_id(inputContext)] pub fn inputContext( &self, // _mtm: MainThreadMarker, - ) -> Option> { - unsafe { msg_send_id![self, inputContext] } - } - - #[sel(visibleRect)] - pub fn visibleRect(&self) -> NSRect; + ) -> Option>; - #[sel(hasMarkedText)] + #[method(hasMarkedText)] pub fn hasMarkedText(&self) -> bool; - #[sel(convertPoint:fromView:)] + #[method(convertPoint:fromView:)] pub fn convertPoint_fromView(&self, point: NSPoint, view: Option<&NSView>) -> NSPoint; - pub fn window(&self) -> Option> { - unsafe { msg_send_id![self, window] } - } + #[method_id(window)] + pub fn window(&self) -> Option>; } unsafe impl NSView { - #[sel(setWantsBestResolutionOpenGLSurface:)] + #[method(setWantsBestResolutionOpenGLSurface:)] pub fn setWantsBestResolutionOpenGLSurface(&self, value: bool); - #[sel(setWantsLayer:)] + #[method(setWantsLayer:)] pub fn setWantsLayer(&self, wants_layer: bool); - #[sel(setPostsFrameChangedNotifications:)] - pub fn setPostsFrameChangedNotifications(&mut self, value: bool); + #[method(setPostsFrameChangedNotifications:)] + pub fn setPostsFrameChangedNotifications(&self, value: bool); - #[sel(removeTrackingRect:)] + #[method(removeTrackingRect:)] pub fn removeTrackingRect(&self, tag: NSTrackingRectTag); - #[sel(addTrackingRect:owner:userData:assumeInside:)] + #[method(addTrackingRect:owner:userData:assumeInside:)] unsafe fn inner_addTrackingRect( &self, rect: NSRect, - owner: &Object, + owner: &AnyObject, user_data: *mut c_void, assume_inside: bool, ) -> Option; @@ -86,11 +82,11 @@ extern_methods!( .expect("failed creating tracking rect") } - #[sel(addCursorRect:cursor:)] + #[method(addCursorRect:cursor:)] // NSCursor safe to take by shared reference since it is already immutable pub fn addCursorRect(&self, rect: NSRect, cursor: &NSCursor); - #[sel(setHidden:)] + #[method(setHidden:)] pub fn setHidden(&self, hidden: bool); } ); diff --git a/src/platform_impl/macos/appkit/window.rs b/src/platform_impl/macos/appkit/window.rs index c6f078eb23..5f5d8f179f 100644 --- a/src/platform_impl/macos/appkit/window.rs +++ b/src/platform_impl/macos/appkit/window.rs @@ -1,21 +1,24 @@ -use objc2::encode::{Encode, Encoding}; -use objc2::foundation::{ +use icrate::Foundation::{ CGFloat, NSArray, NSInteger, NSObject, NSPoint, NSRect, NSSize, NSString, NSUInteger, }; -use objc2::rc::{Id, Shared}; -use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::encode::{Encode, Encoding}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; -use super::{NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView}; +use super::{ + NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView, NSWindowTabGroup, +}; extern_class!( /// Main-Thread-Only! #[derive(Debug, PartialEq, Eq, Hash)] - pub(crate) struct NSWindow; + pub struct NSWindow; unsafe impl ClassType for NSWindow { #[inherits(NSObject)] type Super = NSResponder; + type Mutability = mutability::InteriorMutable; } ); @@ -30,198 +33,217 @@ extern_class!( extern_methods!( unsafe impl NSWindow { - #[sel(frame)] - pub fn frame(&self) -> NSRect; + #[method(frame)] + pub(crate) fn frame(&self) -> NSRect; - #[sel(backingScaleFactor)] - pub fn backingScaleFactor(&self) -> CGFloat; + #[method(windowNumber)] + pub(crate) fn windowNumber(&self) -> NSInteger; - pub fn contentView(&self) -> Id { - unsafe { msg_send_id![self, contentView] } - } + #[method(backingScaleFactor)] + pub(crate) fn backingScaleFactor(&self) -> CGFloat; - #[sel(setContentView:)] - pub fn setContentView(&self, view: &NSView); + #[method_id(contentView)] + pub(crate) fn contentView(&self) -> Id; - #[sel(setInitialFirstResponder:)] - pub fn setInitialFirstResponder(&self, view: &NSView); + #[method(setContentView:)] + pub(crate) fn setContentView(&self, view: &NSView); - #[sel(makeFirstResponder:)] + #[method(setInitialFirstResponder:)] + pub(crate) fn setInitialFirstResponder(&self, view: &NSView); + + #[method(makeFirstResponder:)] #[must_use] - pub fn makeFirstResponder(&self, responder: Option<&NSResponder>) -> bool; + pub(crate) fn makeFirstResponder(&self, responder: Option<&NSResponder>) -> bool; + + #[method(contentRectForFrameRect:)] + pub(crate) fn contentRectForFrameRect(&self, windowFrame: NSRect) -> NSRect; + + #[method_id(screen)] + pub(crate) fn screen(&self) -> Option>; + + #[method(setContentSize:)] + pub(crate) fn setContentSize(&self, contentSize: NSSize); + + #[method(setFrameTopLeftPoint:)] + pub(crate) fn setFrameTopLeftPoint(&self, point: NSPoint); + + #[method(setMinSize:)] + pub(crate) fn setMinSize(&self, minSize: NSSize); + + #[method(setMaxSize:)] + pub(crate) fn setMaxSize(&self, maxSize: NSSize); - #[sel(contentRectForFrameRect:)] - pub fn contentRectForFrameRect(&self, windowFrame: NSRect) -> NSRect; + #[method(setResizeIncrements:)] + pub(crate) fn setResizeIncrements(&self, increments: NSSize); - pub fn screen(&self) -> Option> { - unsafe { msg_send_id![self, screen] } - } + #[method(contentResizeIncrements)] + pub(crate) fn contentResizeIncrements(&self) -> NSSize; - #[sel(setContentSize:)] - pub fn setContentSize(&self, contentSize: NSSize); + #[method(setContentResizeIncrements:)] + pub(crate) fn setContentResizeIncrements(&self, increments: NSSize); - #[sel(setFrameTopLeftPoint:)] - pub fn setFrameTopLeftPoint(&self, point: NSPoint); + #[method(setFrame:display:)] + pub(crate) fn setFrame_display(&self, frameRect: NSRect, flag: bool); - #[sel(setMinSize:)] - pub fn setMinSize(&self, minSize: NSSize); + #[method(setMovable:)] + pub(crate) fn setMovable(&self, movable: bool); - #[sel(setMaxSize:)] - pub fn setMaxSize(&self, maxSize: NSSize); + #[method(setSharingType:)] + pub(crate) fn setSharingType(&self, sharingType: NSWindowSharingType); - #[sel(setResizeIncrements:)] - pub fn setResizeIncrements(&self, increments: NSSize); + #[method(setTabbingMode:)] + pub(crate) fn setTabbingMode(&self, tabbingMode: NSWindowTabbingMode); - #[sel(contentResizeIncrements)] - pub fn contentResizeIncrements(&self) -> NSSize; + #[method(setOpaque:)] + pub(crate) fn setOpaque(&self, opaque: bool); - #[sel(setContentResizeIncrements:)] - pub fn setContentResizeIncrements(&self, increments: NSSize); + #[method(hasShadow)] + pub(crate) fn hasShadow(&self) -> bool; - #[sel(setFrame:display:)] - pub fn setFrame_display(&self, frameRect: NSRect, flag: bool); + #[method(setHasShadow:)] + pub(crate) fn setHasShadow(&self, has_shadow: bool); - #[sel(setMovable:)] - pub fn setMovable(&self, movable: bool); + #[method(setIgnoresMouseEvents:)] + pub(crate) fn setIgnoresMouseEvents(&self, ignores: bool); - #[sel(setSharingType:)] - pub fn setSharingType(&self, sharingType: NSWindowSharingType); + #[method(setBackgroundColor:)] + pub(crate) fn setBackgroundColor(&self, color: &NSColor); - #[sel(setOpaque:)] - pub fn setOpaque(&self, opaque: bool); + #[method(styleMask)] + pub(crate) fn styleMask(&self) -> NSWindowStyleMask; - #[sel(hasShadow)] - pub fn hasShadow(&self) -> bool; + #[method(setStyleMask:)] + pub(crate) fn setStyleMask(&self, mask: NSWindowStyleMask); - #[sel(setHasShadow:)] - pub fn setHasShadow(&self, has_shadow: bool); + #[method(registerForDraggedTypes:)] + pub(crate) fn registerForDraggedTypes(&self, types: &NSArray); - #[sel(setIgnoresMouseEvents:)] - pub fn setIgnoresMouseEvents(&self, ignores: bool); + #[method(makeKeyAndOrderFront:)] + pub(crate) fn makeKeyAndOrderFront(&self, sender: Option<&AnyObject>); - #[sel(setBackgroundColor:)] - pub fn setBackgroundColor(&self, color: &NSColor); + #[method(orderFront:)] + pub(crate) fn orderFront(&self, sender: Option<&AnyObject>); - #[sel(styleMask)] - pub fn styleMask(&self) -> NSWindowStyleMask; + #[method(miniaturize:)] + pub(crate) fn miniaturize(&self, sender: Option<&AnyObject>); - #[sel(setStyleMask:)] - pub fn setStyleMask(&self, mask: NSWindowStyleMask); + #[method(deminiaturize:)] + pub(crate) fn deminiaturize(&self, sender: Option<&AnyObject>); - #[sel(registerForDraggedTypes:)] - pub fn registerForDraggedTypes(&self, types: &NSArray); + #[method(toggleFullScreen:)] + pub(crate) fn toggleFullScreen(&self, sender: Option<&AnyObject>); - #[sel(makeKeyAndOrderFront:)] - pub fn makeKeyAndOrderFront(&self, sender: Option<&Object>); + #[method(orderOut:)] + pub(crate) fn orderOut(&self, sender: Option<&AnyObject>); - #[sel(orderFront:)] - pub fn orderFront(&self, sender: Option<&Object>); + #[method(zoom:)] + pub(crate) fn zoom(&self, sender: Option<&AnyObject>); - #[sel(miniaturize:)] - pub fn miniaturize(&self, sender: Option<&Object>); + #[method(selectNextKeyView:)] + pub(crate) fn selectNextKeyView(&self, sender: Option<&AnyObject>); - #[sel(sender:)] - pub fn deminiaturize(&self, sender: Option<&Object>); + #[method(selectPreviousKeyView:)] + pub(crate) fn selectPreviousKeyView(&self, sender: Option<&AnyObject>); - #[sel(toggleFullScreen:)] - pub fn toggleFullScreen(&self, sender: Option<&Object>); + #[method_id(firstResponder)] + pub(crate) fn firstResponder(&self) -> Option>; - #[sel(orderOut:)] - pub fn orderOut(&self, sender: Option<&Object>); + #[method_id(standardWindowButton:)] + pub(crate) fn standardWindowButton(&self, kind: NSWindowButton) -> Option>; - #[sel(zoom:)] - pub fn zoom(&self, sender: Option<&Object>); + #[method(setTitle:)] + pub(crate) fn setTitle(&self, title: &NSString); - #[sel(selectNextKeyView:)] - pub fn selectNextKeyView(&self, sender: Option<&Object>); + #[method_id(title)] + pub(crate) fn title_(&self) -> Id; - #[sel(selectPreviousKeyView:)] - pub fn selectPreviousKeyView(&self, sender: Option<&Object>); + #[method(setReleasedWhenClosed:)] + pub(crate) fn setReleasedWhenClosed(&self, val: bool); - pub fn firstResponder(&self) -> Option> { - unsafe { msg_send_id![self, firstResponder] } - } + #[method(setAcceptsMouseMovedEvents:)] + pub(crate) fn setAcceptsMouseMovedEvents(&self, val: bool); - pub fn standardWindowButton(&self, kind: NSWindowButton) -> Option> { - unsafe { msg_send_id![self, standardWindowButton: kind] } - } + #[method(setTitlebarAppearsTransparent:)] + pub(crate) fn setTitlebarAppearsTransparent(&self, val: bool); - #[sel(setTitle:)] - pub fn setTitle(&self, title: &NSString); + #[method(setTitleVisibility:)] + pub(crate) fn setTitleVisibility(&self, visibility: NSWindowTitleVisibility); - pub fn title_(&self) -> Id { - unsafe { msg_send_id![self, title] } - } + #[method(setMovableByWindowBackground:)] + pub(crate) fn setMovableByWindowBackground(&self, val: bool); - #[sel(setReleasedWhenClosed:)] - pub fn setReleasedWhenClosed(&self, val: bool); + #[method(setLevel:)] + pub(crate) fn setLevel(&self, level: NSWindowLevel); - #[sel(setAcceptsMouseMovedEvents:)] - pub fn setAcceptsMouseMovedEvents(&self, val: bool); + #[method(setAllowsAutomaticWindowTabbing:)] + pub(crate) fn setAllowsAutomaticWindowTabbing(val: bool); - #[sel(setTitlebarAppearsTransparent:)] - pub fn setTitlebarAppearsTransparent(&self, val: bool); + #[method(setTabbingIdentifier:)] + pub(crate) fn setTabbingIdentifier(&self, identifier: &NSString); - #[sel(setTitleVisibility:)] - pub fn setTitleVisibility(&self, visibility: NSWindowTitleVisibility); + #[method(setDocumentEdited:)] + pub(crate) fn setDocumentEdited(&self, val: bool); - #[sel(setMovableByWindowBackground:)] - pub fn setMovableByWindowBackground(&self, val: bool); + #[method(occlusionState)] + pub(crate) fn occlusionState(&self) -> NSWindowOcclusionState; - #[sel(setLevel:)] - pub fn setLevel(&self, level: NSWindowLevel); + #[method(center)] + pub(crate) fn center(&self); - #[sel(setDocumentEdited:)] - pub fn setDocumentEdited(&self, val: bool); + #[method(isResizable)] + pub(crate) fn isResizable(&self) -> bool; - #[sel(occlusionState)] - pub fn occlusionState(&self) -> NSWindowOcclusionState; + #[method(isMiniaturizable)] + pub(crate) fn isMiniaturizable(&self) -> bool; - #[sel(center)] - pub fn center(&self); + #[method(hasCloseBox)] + pub(crate) fn hasCloseBox(&self) -> bool; - #[sel(isResizable)] - pub fn isResizable(&self) -> bool; + #[method(isMiniaturized)] + pub(crate) fn isMiniaturized(&self) -> bool; - #[sel(isMiniaturizable)] - pub fn isMiniaturizable(&self) -> bool; + #[method(isVisible)] + pub(crate) fn isVisible(&self) -> bool; - #[sel(hasCloseBox)] - pub fn hasCloseBox(&self) -> bool; + #[method(isKeyWindow)] + pub(crate) fn isKeyWindow(&self) -> bool; - #[sel(isMiniaturized)] - pub fn isMiniaturized(&self) -> bool; + #[method(isZoomed)] + pub(crate) fn isZoomed(&self) -> bool; - #[sel(isVisible)] - pub fn isVisible(&self) -> bool; + #[method(allowsAutomaticWindowTabbing)] + pub(crate) fn allowsAutomaticWindowTabbing() -> bool; - #[sel(isKeyWindow)] - pub fn isKeyWindow(&self) -> bool; + #[method(selectNextTab)] + pub(crate) fn selectNextTab(&self); - #[sel(isZoomed)] - pub fn isZoomed(&self) -> bool; + #[method_id(tabbingIdentifier)] + pub(crate) fn tabbingIdentifier(&self) -> Id; - #[sel(isDocumentEdited)] - pub fn isDocumentEdited(&self) -> bool; + #[method_id(tabGroup)] + pub(crate) fn tabGroup(&self) -> Option>; - #[sel(close)] - pub fn close(&self); + #[method(isDocumentEdited)] + pub(crate) fn isDocumentEdited(&self) -> bool; - #[sel(performWindowDragWithEvent:)] + #[method(close)] + pub(crate) fn close(&self); + + #[method(performWindowDragWithEvent:)] // TODO: Can this actually accept NULL? - pub fn performWindowDragWithEvent(&self, event: Option<&NSEvent>); + pub(crate) fn performWindowDragWithEvent(&self, event: Option<&NSEvent>); - #[sel(invalidateCursorRectsForView:)] - pub fn invalidateCursorRectsForView(&self, view: &NSView); + #[method(invalidateCursorRectsForView:)] + pub(crate) fn invalidateCursorRectsForView(&self, view: &NSView); - #[sel(setDelegate:)] - pub fn setDelegate(&self, delegate: Option<&NSObject>); + #[method(setDelegate:)] + pub(crate) fn setDelegate(&self, delegate: Option<&NSObject>); - #[sel(sendEvent:)] - pub unsafe fn sendEvent(&self, event: &NSEvent); + #[method(sendEvent:)] + pub(crate) unsafe fn sendEvent(&self, event: &NSEvent); - #[sel(addChildWindow:ordered:)] - pub unsafe fn addChildWindow(&self, child: &NSWindow, ordered: NSWindowOrderingMode); + #[method(addChildWindow:ordered:)] + pub(crate) unsafe fn addChildWindow(&self, child: &NSWindow, ordered: NSWindowOrderingMode); } ); @@ -403,3 +425,16 @@ pub enum NSWindowOrderingMode { unsafe impl Encode for NSWindowOrderingMode { const ENCODING: Encoding = NSInteger::ENCODING; } + +#[allow(dead_code)] +#[repr(isize)] // NSInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowTabbingMode { + NSWindowTabbingModeAutomatic = 0, + NSWindowTabbingModeDisallowed = 2, + NSWindowTabbingModePreferred = 1, +} + +unsafe impl Encode for NSWindowTabbingMode { + const ENCODING: Encoding = NSInteger::ENCODING; +} diff --git a/src/platform_impl/macos/event.rs b/src/platform_impl/macos/event.rs index 785647e3f2..77d527eb54 100644 --- a/src/platform_impl/macos/event.rs +++ b/src/platform_impl/macos/event.rs @@ -4,39 +4,22 @@ use core_foundation::{ base::CFRelease, data::{CFDataGetBytePtr, CFDataRef}, }; -use objc2::rc::{Id, Shared}; +use icrate::Foundation::MainThreadMarker; use smol_str::SmolStr; use super::appkit::{NSEvent, NSEventModifierFlags}; -use super::window::WinitWindow; use crate::{ - dpi::LogicalSize, - event::{ElementState, Event, KeyEvent, Modifiers}, + event::{ElementState, KeyEvent, Modifiers}, keyboard::{ - Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NativeKey, NativeKeyCode, + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, + NativeKeyCode, PhysicalKey, }, - platform::{modifier_supplement::KeyEventExtModifierSupplement, scancode::KeyCodeExtScancode}, - platform_impl::platform::{ - ffi, - util::{get_kbd_type, Never}, + platform::{ + modifier_supplement::KeyEventExtModifierSupplement, scancode::PhysicalKeyExtScancode, }, + platform_impl::platform::ffi, }; -#[derive(Debug)] -pub(crate) enum EventWrapper { - StaticEvent(Event<'static, Never>), - EventProxy(EventProxy), -} - -#[derive(Debug)] -pub(crate) enum EventProxy { - DpiChangedProxy { - window: Id, - suggested_size: LogicalSize, - scale_factor: f64, - }, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct KeyEventExtra { pub text_with_all_modifiers: Option, @@ -56,6 +39,7 @@ impl KeyEventExtModifierSupplement for KeyEvent { } } +/// Ignores ALL modifiers. pub fn get_modifierless_char(scancode: u16) -> Key { let mut string = [0; 16]; let input_source; @@ -75,7 +59,7 @@ pub fn get_modifierless_char(scancode: u16) -> Key { } layout = CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout; } - let keyboard_type = get_kbd_type(); + let keyboard_type = MainThreadMarker::run_on_main(|_mtm| unsafe { ffi::LMGetKbdType() }); let mut result_len = 0; let mut dead_keys = 0; @@ -105,18 +89,21 @@ pub fn get_modifierless_char(scancode: u16) -> Key { return Key::Unidentified(NativeKey::MacOS(scancode)); } if result_len == 0 { - log::error!("`UCKeyTranslate` was succesful but gave a string of 0 length."); + // This is fine - not all keys have text representation. + // For instance, users that have mapped the `Fn` key to toggle + // keyboard layouts will hit this code path. return Key::Unidentified(NativeKey::MacOS(scancode)); } let chars = String::from_utf16_lossy(&string[0..result_len as usize]); Key::Character(SmolStr::new(chars)) } +// Ignores all modifiers except for SHIFT (yes, even ALT is ignored). fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key { let string = ns_event .charactersIgnoringModifiers() .map(|s| s.to_string()) - .unwrap_or_else(String::new); + .unwrap_or_default(); if string.is_empty() { // Probably a dead key let first_char = modifierless_chars.chars().next(); @@ -132,13 +119,21 @@ pub(crate) fn create_key_event( ns_event: &NSEvent, is_press: bool, is_repeat: bool, - key_override: Option, + key_override: Option, ) -> KeyEvent { use ElementState::{Pressed, Released}; let state = if is_press { Pressed } else { Released }; let scancode = ns_event.key_code(); - let mut physical_key = key_override.unwrap_or_else(|| KeyCode::from_scancode(scancode as u32)); + let mut physical_key = + key_override.unwrap_or_else(|| PhysicalKey::from_scancode(scancode as u32)); + + // NOTE: The logical key should heed both SHIFT and ALT if possible. + // For instance: + // * Pressing the A key: logical key should be "a" + // * Pressing SHIFT A: logical key should be "A" + // * Pressing CTRL SHIFT A: logical key should also be "A" + // This is not easy to tease out of `NSEvent`, but we do our best. let text_with_all_modifiers: Option = if key_override.is_some() { None @@ -146,11 +141,11 @@ pub(crate) fn create_key_event( let characters = ns_event .characters() .map(|s| s.to_string()) - .unwrap_or_else(String::new); + .unwrap_or_default(); if characters.is_empty() { None } else { - if matches!(physical_key, KeyCode::Unidentified(_)) { + if matches!(physical_key, PhysicalKey::Unidentified(_)) { // The key may be one of the funky function keys physical_key = extra_function_key_to_code(scancode, &characters); } @@ -160,25 +155,31 @@ pub(crate) fn create_key_event( let key_from_code = code_to_key(physical_key, scancode); let (logical_key, key_without_modifiers) = if matches!(key_from_code, Key::Unidentified(_)) { + // `get_modifierless_char/key_without_modifiers` ignores ALL modifiers. let key_without_modifiers = get_modifierless_char(scancode); let modifiers = NSEvent::modifierFlags(ns_event); let has_ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let has_cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); let logical_key = match text_with_all_modifiers.as_ref() { - // Only checking for ctrl here, not checking for alt because we DO want to + // Only checking for ctrl and cmd here, not checking for alt because we DO want to // include its effect in the key. For example if -on the Germay layout- one // presses alt+8, the logical key should be "{" // Also not checking if this is a release event because then this issue would // still affect the key release. - Some(text) if !has_ctrl => Key::Character(text.clone()), - _ => { - let modifierless_chars = match key_without_modifiers.as_ref() { - Key::Character(ch) => ch, - _ => "", - }; - get_logical_key_char(ns_event, modifierless_chars) + Some(text) if !has_ctrl && !has_cmd => { + // Character heeding both SHIFT and ALT. + Key::Character(text.clone()) } + + _ => match key_without_modifiers.as_ref() { + // Character heeding just SHIFT, ignoring ALT. + Key::Character(ch) => get_logical_key_char(ns_event, ch), + + // Character ignoring ALL modifiers. + _ => key_without_modifiers.clone(), + }, }; (logical_key, key_without_modifiers) @@ -208,65 +209,75 @@ pub(crate) fn create_key_event( } } -pub fn code_to_key(code: KeyCode, scancode: u16) -> Key { - match code { - KeyCode::Enter => Key::Enter, - KeyCode::Tab => Key::Tab, - KeyCode::Space => Key::Space, - KeyCode::Backspace => Key::Backspace, - KeyCode::Escape => Key::Escape, - KeyCode::SuperRight => Key::Super, - KeyCode::SuperLeft => Key::Super, - KeyCode::ShiftLeft => Key::Shift, - KeyCode::AltLeft => Key::Alt, - KeyCode::ControlLeft => Key::Control, - KeyCode::ShiftRight => Key::Shift, - KeyCode::AltRight => Key::Alt, - KeyCode::ControlRight => Key::Control, - - KeyCode::NumLock => Key::NumLock, - KeyCode::AudioVolumeUp => Key::AudioVolumeUp, - KeyCode::AudioVolumeDown => Key::AudioVolumeDown, +pub fn code_to_key(key: PhysicalKey, scancode: u16) -> Key { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => return Key::Unidentified(code.into()), + }; + + Key::Named(match code { + KeyCode::Enter => NamedKey::Enter, + KeyCode::Tab => NamedKey::Tab, + KeyCode::Space => NamedKey::Space, + KeyCode::Backspace => NamedKey::Backspace, + KeyCode::Escape => NamedKey::Escape, + KeyCode::SuperRight => NamedKey::Super, + KeyCode::SuperLeft => NamedKey::Super, + KeyCode::ShiftLeft => NamedKey::Shift, + KeyCode::AltLeft => NamedKey::Alt, + KeyCode::ControlLeft => NamedKey::Control, + KeyCode::ShiftRight => NamedKey::Shift, + KeyCode::AltRight => NamedKey::Alt, + KeyCode::ControlRight => NamedKey::Control, + + KeyCode::NumLock => NamedKey::NumLock, + KeyCode::AudioVolumeUp => NamedKey::AudioVolumeUp, + KeyCode::AudioVolumeDown => NamedKey::AudioVolumeDown, // Other numpad keys all generate text on macOS (if I understand correctly) - KeyCode::NumpadEnter => Key::Enter, - - KeyCode::F1 => Key::F1, - KeyCode::F2 => Key::F2, - KeyCode::F3 => Key::F3, - KeyCode::F4 => Key::F4, - KeyCode::F5 => Key::F5, - KeyCode::F6 => Key::F6, - KeyCode::F7 => Key::F7, - KeyCode::F8 => Key::F8, - KeyCode::F9 => Key::F9, - KeyCode::F10 => Key::F10, - KeyCode::F11 => Key::F11, - KeyCode::F12 => Key::F12, - KeyCode::F13 => Key::F13, - KeyCode::F14 => Key::F14, - KeyCode::F15 => Key::F15, - KeyCode::F16 => Key::F16, - KeyCode::F17 => Key::F17, - KeyCode::F18 => Key::F18, - KeyCode::F19 => Key::F19, - KeyCode::F20 => Key::F20, - - KeyCode::Insert => Key::Insert, - KeyCode::Home => Key::Home, - KeyCode::PageUp => Key::PageUp, - KeyCode::Delete => Key::Delete, - KeyCode::End => Key::End, - KeyCode::PageDown => Key::PageDown, - KeyCode::ArrowLeft => Key::ArrowLeft, - KeyCode::ArrowRight => Key::ArrowRight, - KeyCode::ArrowDown => Key::ArrowDown, - KeyCode::ArrowUp => Key::ArrowUp, - _ => Key::Unidentified(NativeKey::MacOS(scancode)), - } + KeyCode::NumpadEnter => NamedKey::Enter, + + KeyCode::F1 => NamedKey::F1, + KeyCode::F2 => NamedKey::F2, + KeyCode::F3 => NamedKey::F3, + KeyCode::F4 => NamedKey::F4, + KeyCode::F5 => NamedKey::F5, + KeyCode::F6 => NamedKey::F6, + KeyCode::F7 => NamedKey::F7, + KeyCode::F8 => NamedKey::F8, + KeyCode::F9 => NamedKey::F9, + KeyCode::F10 => NamedKey::F10, + KeyCode::F11 => NamedKey::F11, + KeyCode::F12 => NamedKey::F12, + KeyCode::F13 => NamedKey::F13, + KeyCode::F14 => NamedKey::F14, + KeyCode::F15 => NamedKey::F15, + KeyCode::F16 => NamedKey::F16, + KeyCode::F17 => NamedKey::F17, + KeyCode::F18 => NamedKey::F18, + KeyCode::F19 => NamedKey::F19, + KeyCode::F20 => NamedKey::F20, + + KeyCode::Insert => NamedKey::Insert, + KeyCode::Home => NamedKey::Home, + KeyCode::PageUp => NamedKey::PageUp, + KeyCode::Delete => NamedKey::Delete, + KeyCode::End => NamedKey::End, + KeyCode::PageDown => NamedKey::PageDown, + KeyCode::ArrowLeft => NamedKey::ArrowLeft, + KeyCode::ArrowRight => NamedKey::ArrowRight, + KeyCode::ArrowDown => NamedKey::ArrowDown, + KeyCode::ArrowUp => NamedKey::ArrowUp, + _ => return Key::Unidentified(NativeKey::MacOS(scancode)), + }) } -pub fn code_to_location(code: KeyCode) -> KeyLocation { +pub fn code_to_location(key: PhysicalKey) -> KeyLocation { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return KeyLocation::Standard, + }; + match code { KeyCode::SuperRight => KeyLocation::Right, KeyCode::SuperLeft => KeyLocation::Left, @@ -303,17 +314,17 @@ pub fn code_to_location(code: KeyCode) -> KeyLocation { // While F1-F20 have scancodes we can match on, we have to check against UTF-16 // constants for the rest. // https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ -pub fn extra_function_key_to_code(scancode: u16, string: &str) -> KeyCode { +pub fn extra_function_key_to_code(scancode: u16, string: &str) -> PhysicalKey { if let Some(ch) = string.encode_utf16().next() { match ch { - 0xf718 => KeyCode::F21, - 0xf719 => KeyCode::F22, - 0xf71a => KeyCode::F23, - 0xf71b => KeyCode::F24, - _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)), + 0xf718 => PhysicalKey::Code(KeyCode::F21), + 0xf719 => PhysicalKey::Code(KeyCode::F22), + 0xf71a => PhysicalKey::Code(KeyCode::F23), + 0xf71b => PhysicalKey::Code(KeyCode::F24), + _ => PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)), } } else { - KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)) + PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)) } } @@ -360,9 +371,14 @@ pub(super) fn event_mods(event: &NSEvent) -> Modifiers { } } -impl KeyCodeExtScancode for KeyCode { +impl PhysicalKeyExtScancode for PhysicalKey { fn to_scancode(self) -> Option { - match self { + let code = match self { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return None, + }; + + match code { KeyCode::KeyA => Some(0x00), KeyCode::KeyS => Some(0x01), KeyCode::KeyD => Some(0x02), @@ -478,8 +494,8 @@ impl KeyCodeExtScancode for KeyCode { } } - fn from_scancode(scancode: u32) -> KeyCode { - match scancode { + fn from_scancode(scancode: u32) -> PhysicalKey { + PhysicalKey::Code(match scancode { 0x00 => KeyCode::KeyA, 0x01 => KeyCode::KeyS, 0x02 => KeyCode::KeyD, @@ -616,7 +632,7 @@ impl KeyCodeExtScancode for KeyCode { // 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as // backquote (`) on Windows' US layout. 0xa => KeyCode::Backquote, - _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode as u16)), - } + _ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + }) } } diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 72434ac478..c0da3ee14a 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -5,10 +5,11 @@ use std::{ marker::PhantomData, mem, os::raw::c_void, - panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}, - process, ptr, + panic::{catch_unwind, resume_unwind, AssertUnwindSafe, RefUnwindSafe, UnwindSafe}, + ptr, rc::{Rc, Weak}, sync::mpsc, + time::{Duration, Instant}, }; use core_foundation::base::{CFIndex, CFRelease}; @@ -16,16 +17,19 @@ use core_foundation::runloop::{ kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, }; -use objc2::foundation::is_main_thread; -use objc2::rc::{autoreleasepool, Id, Shared}; +use icrate::Foundation::MainThreadMarker; +use objc2::rc::{autoreleasepool, Id}; +use objc2::runtime::NSObjectProtocol; use objc2::{msg_send_id, ClassType}; -use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle}; -use super::appkit::{NSApp, NSApplicationActivationPolicy, NSEvent}; +use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent, NSWindow}; use crate::{ + error::EventLoopError, event::Event, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, - platform::macos::ActivationPolicy, + event_loop::{ + ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget, + }, + platform::{macos::ActivationPolicy, pump_events::PumpStatus}, platform_impl::platform::{ app::WinitApplication, app_delegate::ApplicationDelegate, @@ -63,16 +67,10 @@ impl PanicInfo { } } +#[derive(Debug)] pub struct EventLoopWindowTarget { - pub sender: mpsc::Sender, // this is only here to be cloned elsewhere - pub receiver: mpsc::Receiver, -} - -impl Default for EventLoopWindowTarget { - fn default() -> Self { - let (sender, receiver) = mpsc::channel(); - EventLoopWindowTarget { sender, receiver } - } + mtm: MainThreadMarker, + p: PhantomData, } impl EventLoopWindowTarget { @@ -88,25 +86,76 @@ impl EventLoopWindowTarget { } #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()) + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + AppState::set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + AppState::control_flow() + } + + pub(crate) fn exit(&self) { + AppState::exit() + } + + pub(crate) fn clear_exit(&self) { + AppState::clear_exit() + } + + pub(crate) fn exiting(&self) -> bool { + AppState::exiting() } } impl EventLoopWindowTarget { pub(crate) fn hide_application(&self) { - NSApp().hide(None) + NSApplication::shared(self.mtm).hide(None) } pub(crate) fn hide_other_applications(&self) { - NSApp().hideOtherApplications(None) + NSApplication::shared(self.mtm).hideOtherApplications(None) + } + + pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + NSWindow::setAllowsAutomaticWindowTabbing(enabled) + } + + pub(crate) fn allows_automatic_window_tabbing(&self) -> bool { + NSWindow::allowsAutomaticWindowTabbing() } } pub struct EventLoop { + /// Store a reference to the application for convenience. + /// + /// We intentially don't store `WinitApplication` since we want to have + /// the possiblity of swapping that out at some point. + app: Id, /// The delegate is only weakly referenced by NSApplication, so we keep /// it around here as well. - _delegate: Id, + _delegate: Id, + + // Event sender and receiver, used for EventLoopProxy. + sender: mpsc::Sender, + receiver: Rc>, window_target: Rc>, panic_info: Rc, @@ -138,18 +187,19 @@ impl Default for PlatformSpecificEventLoopAttributes { } impl EventLoop { - pub(crate) fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Self { - if !is_main_thread() { - panic!("On macOS, `EventLoop` must be created on the main thread!"); - } + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let mtm = MainThreadMarker::new() + .expect("on macOS, `EventLoop` must be created on the main thread!"); - // This must be done before `NSApp()` (equivalent to sending - // `sharedApplication`) is called anywhere else, or we'll end up - // with the wrong `NSApplication` class and the wrong thread could - // be marked as main. - let app: Id = + let app: Id = unsafe { msg_send_id![WinitApplication::class(), sharedApplication] }; + if !app.is_kind_of::() { + panic!("`winit` requires control over the principal class. You must create the event loop before other parts of your application initialize NSApplication"); + } + use NSApplicationActivationPolicy::*; let activation_policy = match attributes.activation_policy { ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, @@ -168,70 +218,244 @@ impl EventLoop { let panic_info: Rc = Default::default(); setup_control_flow_observers(Rc::downgrade(&panic_info)); - EventLoop { + + let (sender, receiver) = mpsc::channel(); + Ok(EventLoop { + app, _delegate: delegate, + sender, + receiver: Rc::new(receiver), window_target: Rc::new(RootWindowTarget { - p: Default::default(), + p: EventLoopWindowTarget { + mtm, + p: PhantomData, + }, _marker: PhantomData, }), panic_info, _callback: None, - } + }) } pub fn window_target(&self) -> &RootWindowTarget { &self.window_target } - pub fn run(mut self, callback: F) -> ! + pub fn run(mut self, callback: F) -> Result<(), EventLoopError> where - F: 'static + FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), + F: FnMut(Event, &RootWindowTarget), { - let exit_code = self.run_return(callback); - process::exit(exit_code); + self.run_on_demand(callback) } - pub fn run_return(&mut self, callback: F) -> i32 + // NB: we don't base this on `pump_events` because for `MacOs` we can't support + // `pump_events` elegantly (we just ask to run the loop for a "short" amount of + // time and so a layered implementation would end up using a lot of CPU due to + // redundant wake ups. + pub fn run_on_demand(&mut self, callback: F) -> Result<(), EventLoopError> where - F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), + F: FnMut(Event, &RootWindowTarget), { - // This transmute is always safe, in case it was reached through `run`, since our - // lifetime will be already 'static. In other cases caller should ensure that all data - // they passed to callback will actually outlive it, some apps just can't move - // everything to event loop, so this is something that they should care about. + if AppState::is_running() { + return Err(EventLoopError::AlreadyRunning); + } + + // # Safety + // We are erasing the lifetime of the application callback here so that we + // can (temporarily) store it within 'static global `AppState` that's + // accessible to objc delegate callbacks. + // + // The safety of this depends on on making sure to also clear the callback + // from the global `AppState` before we return from here, ensuring that + // we don't retain a reference beyond the real lifetime of the callback. + let callback = unsafe { mem::transmute::< - Rc, &RootWindowTarget, &mut ControlFlow)>>, - Rc, &RootWindowTarget, &mut ControlFlow)>>, + Rc, &RootWindowTarget)>>, + Rc, &RootWindowTarget)>>, >(Rc::new(RefCell::new(callback))) }; self._callback = Some(Rc::clone(&callback)); - let exit_code = autoreleasepool(|_| { - let app = NSApp(); - + autoreleasepool(|_| { // 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); drop(callback); - AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); - unsafe { app.run() }; + // # Safety + // We make sure to call `AppState::clear_callback` before returning + unsafe { + AppState::set_callback( + weak_cb, + Rc::clone(&self.window_target), + Rc::clone(&self.receiver), + ); + } - if let Some(panic) = self.panic_info.take() { - drop(self._callback.take()); - resume_unwind(panic); + // catch panics to make sure we can't unwind without clearing the set callback + // (which would leave the global `AppState` in an undefined, unsafe state) + let catch_result = catch_unwind(AssertUnwindSafe(|| { + // clear / normalize pump_events state + AppState::set_wait_timeout(None); + AppState::set_stop_app_before_wait(false); + AppState::set_stop_app_after_wait(false); + AppState::set_stop_app_on_redraw_requested(false); + + if AppState::is_launched() { + debug_assert!(!AppState::is_running()); + AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + } + unsafe { self.app.run() }; + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the `NSApp` and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + AppState::internal_exit() + })); + + // # Safety + // This pairs up with the `unsafe` call to `set_callback` above and ensures that + // we always clear the application callback from the global `AppState` before + // returning + drop(self._callback.take()); + AppState::clear_callback(); + + if let Err(payload) = catch_result { + resume_unwind(payload) } - AppState::exit() }); - drop(self._callback.take()); - exit_code + Ok(()) + } + + pub fn pump_events(&mut self, timeout: Option, callback: F) -> PumpStatus + where + F: FnMut(Event, &RootWindowTarget), + { + // # Safety + // We are erasing the lifetime of the application callback here so that we + // can (temporarily) store it within 'static global `AppState` that's + // accessible to objc delegate callbacks. + // + // The safety of this depends on on making sure to also clear the callback + // from the global `AppState` before we return from here, ensuring that + // we don't retain a reference beyond the real lifetime of the callback. + + let callback = unsafe { + mem::transmute::< + Rc, &RootWindowTarget)>>, + Rc, &RootWindowTarget)>>, + >(Rc::new(RefCell::new(callback))) + }; + + self._callback = Some(Rc::clone(&callback)); + + autoreleasepool(|_| { + let app = NSApp(); + + // 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); + drop(callback); + + // # Safety + // We will make sure to call `AppState::clear_callback` before returning + // to ensure that we don't hold on to the callback beyond its (erased) + // lifetime + unsafe { + AppState::set_callback( + weak_cb, + Rc::clone(&self.window_target), + Rc::clone(&self.receiver), + ); + } + + // catch panics to make sure we can't unwind without clearing the set callback + // (which would leave the global `AppState` in an undefined, unsafe state) + let catch_result = catch_unwind(AssertUnwindSafe(|| { + // As a special case, if the `NSApp` hasn't been launched yet then we at least run + // the loop until it has fully launched. + if !AppState::is_launched() { + debug_assert!(!AppState::is_running()); + + AppState::request_stop_on_launch(); + unsafe { + app.run(); + } + + // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched + } else if !AppState::is_running() { + // Even though the NSApp may have been launched, it's possible we aren't running + // if the `EventLoop` was run before and has since exited. This indicates that + // we just starting to re-run the same `EventLoop` again. + AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + } else { + // Only run the NSApp for as long as the given `Duration` allows so we + // don't block the external loop. + match timeout { + Some(Duration::ZERO) => { + AppState::set_wait_timeout(None); + AppState::set_stop_app_before_wait(true); + } + Some(duration) => { + AppState::set_stop_app_before_wait(false); + let timeout = Instant::now() + duration; + AppState::set_wait_timeout(Some(timeout)); + AppState::set_stop_app_after_wait(true); + } + None => { + AppState::set_wait_timeout(None); + AppState::set_stop_app_before_wait(false); + AppState::set_stop_app_after_wait(true); + } + } + AppState::set_stop_app_on_redraw_requested(true); + unsafe { + app.run(); + } + } + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the `NSApp` and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + if AppState::exiting() { + AppState::internal_exit(); + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + })); + + // # Safety + // This pairs up with the `unsafe` call to `set_callback` above and ensures that + // we always clear the application callback from the global `AppState` before + // returning + AppState::clear_callback(); + drop(self._callback.take()); + + match catch_result { + Ok(pump_status) => pump_status, + Err(payload) => resume_unwind(payload), + } + }) } pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy::new(self.window_target.p.sender.clone()) + EventLoopProxy::new(self.sender.clone()) } } diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 05397facf0..5396d32584 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -11,6 +11,7 @@ use core_graphics::{ base::CGError, display::{CGDirectDisplayID, CGDisplayConfigRef}, }; +use objc2::{ffi::NSInteger, runtime::AnyObject}; pub type CGDisplayFadeInterval = f32; pub type CGDisplayReservationInterval = f32; @@ -113,6 +114,14 @@ extern "C" { pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); + + // Wildly used private APIs; Apple uses them for their Terminal.app. + pub fn CGSMainConnectionID() -> *mut AnyObject; + pub fn CGSSetWindowBackgroundBlurRadius( + connection_id: *mut AnyObject, + window_id: NSInteger, + radius: i64, + ) -> i32; } mod core_video { diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index f7ad2df0d9..8e69cbbb26 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -1,7 +1,8 @@ -use objc2::foundation::{NSProcessInfo, NSString}; -use objc2::rc::{Id, Shared}; +use icrate::ns_string; +use icrate::Foundation::{NSProcessInfo, NSString}; +use objc2::rc::Id; use objc2::runtime::Sel; -use objc2::{ns_string, sel}; +use objc2::sel; use super::appkit::{NSApp, NSEventModifierFlags, NSMenu, NSMenuItem}; @@ -16,17 +17,17 @@ pub fn initialize() { menubar.addItem(&app_menu_item); let app_menu = NSMenu::new(); - let process_name = NSProcessInfo::process_info().process_name(); + let process_name = NSProcessInfo::processInfo().processName(); // About menu item - let about_item_title = ns_string!("About ").concat(&process_name); + let about_item_title = ns_string!("About ").stringByAppendingString(&process_name); let about_item = menu_item(&about_item_title, sel!(orderFrontStandardAboutPanel:), None); // Seperator menu item let sep_first = NSMenuItem::separatorItem(); // Hide application menu item - let hide_item_title = ns_string!("Hide ").concat(&process_name); + let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name); let hide_item = menu_item( &hide_item_title, sel!(hide:), @@ -57,7 +58,7 @@ pub fn initialize() { let sep = NSMenuItem::separatorItem(); // Quit application menu item - let quit_item_title = ns_string!("Quit ").concat(&process_name); + let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name); let quit_item = menu_item( &quit_item_title, sel!(terminate:), @@ -84,7 +85,7 @@ fn menu_item( title: &NSString, selector: Sel, key_equivalent: Option>, -) -> Id { +) -> Id { let (key, masks) = match key_equivalent { Some(ke) => (ke.key, ke.masks), None => (ns_string!(""), None), diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 4559e0c9fb..7169ca9dbf 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,5 +1,3 @@ -#![deny(unsafe_op_in_unsafe_fn)] - #[macro_use] mod util; @@ -17,10 +15,8 @@ mod view; mod window; mod window_delegate; -use std::{fmt, ops::Deref}; +use std::fmt; -use self::window::WinitWindow; -use self::window_delegate::WinitWindowDelegate; pub(crate) use self::{ event::KeyEventExtra, event_loop::{ @@ -29,13 +25,11 @@ pub(crate) use self::{ monitor::{MonitorHandle, VideoMode}, window::{PlatformSpecificWindowBuilderAttributes, WindowId}, }; -use crate::{ - error::OsError as RootOsError, event::DeviceId as RootDeviceId, window::WindowAttributes, -}; -use objc2::rc::{autoreleasepool, Id, Shared}; +use crate::event::DeviceId as RootDeviceId; +pub(crate) use self::window::Window; pub(crate) use crate::icon::NoIcon as PlatformIcon; -pub(self) use crate::platform_impl::Fullscreen; +pub(crate) use crate::platform_impl::Fullscreen; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId; @@ -49,47 +43,12 @@ impl DeviceId { // Constant device ID; to be removed when if backend is updated to report real device IDs. pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); -pub(crate) struct Window { - pub(crate) window: Id, - // We keep this around so that it doesn't get dropped until the window does. - _delegate: Id, -} - -impl Drop for Window { - fn drop(&mut self) { - // Ensure the window is closed - util::close_sync(&self.window); - } -} - #[derive(Debug)] pub enum OsError { CGError(core_graphics::base::CGError), CreationError(&'static str), } -unsafe impl Send for Window {} -unsafe impl Sync for Window {} - -impl Deref for Window { - type Target = WinitWindow; - #[inline] - fn deref(&self) -> &Self::Target { - &self.window - } -} - -impl Window { - pub(crate) fn new( - _window_target: &EventLoopWindowTarget, - attributes: WindowAttributes, - pl_attribs: PlatformSpecificWindowBuilderAttributes, - ) -> Result { - let (window, _delegate) = autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs))?; - Ok(Window { window, _delegate }) - } -} - impl fmt::Display for OsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index 54a99040ad..16e92f3b1f 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -7,8 +7,10 @@ use core_foundation::{ base::{CFRelease, TCFType}, string::CFString, }; -use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; -use objc2::rc::{Id, Shared}; +use core_graphics::display::{ + CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode, +}; +use objc2::rc::Id; use super::appkit::NSScreen; use super::ffi; @@ -216,6 +218,12 @@ impl MonitorHandle { pub fn refresh_rate_millihertz(&self) -> Option { unsafe { + let current_display_mode = NativeDisplayMode(CGDisplayCopyDisplayMode(self.0) as _); + let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0); + if refresh_rate > 0.0 { + return Some((refresh_rate * 1000.0).round() as u32); + } + let mut display_link = std::ptr::null_mut(); if ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link) != ffi::kCVReturnSuccess @@ -295,19 +303,14 @@ impl MonitorHandle { } } - pub(crate) fn ns_screen(&self) -> Option> { + pub(crate) fn ns_screen(&self) -> Option> { let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) }; - NSScreen::screens() - .into_iter() - .find(|screen| { - let other_native_id = screen.display_id(); - let other_uuid = unsafe { - ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID) - }; - uuid == other_uuid - }) - .map(|screen| unsafe { - Id::retain(screen as *const NSScreen as *mut NSScreen).unwrap() - }) + NSScreen::screens().into_iter().find(|screen| { + let other_native_id = screen.display_id(); + let other_uuid = unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID) + }; + uuid == other_uuid + }) } } diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index bce63c87c8..c17db7ddef 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -1,5 +1,4 @@ use std::{ - self, ffi::c_void, panic::{AssertUnwindSafe, UnwindSafe}, ptr, @@ -64,7 +63,7 @@ extern "C" fn control_flow_begin_handler( } // end is queued with the lowest priority to ensure it is processed after other observers -// without that, LoopDestroyed would get sent after MainEventsCleared +// without that, LoopExiting would get sent after AboutToWait extern "C" fn control_flow_end_handler( _: CFRunLoopObserverRef, activity: CFRunLoopActivity, @@ -141,6 +140,16 @@ pub fn setup_control_flow_observers(panic_info: Weak) { pub struct EventLoopWaker { timer: CFRunLoopTimerRef, + + /// An arbitrary instant in the past, that will trigger an immediate wake + /// We save this as the `next_fire_date` for consistency so we can + /// easily check if the next_fire_date needs updating. + start_instant: Instant, + + /// This is what the `NextFireDate` has been set to. + /// `None` corresponds to `waker.stop()` and `start_instant` is used + /// for `waker.start()` + next_fire_date: Option, } impl Drop for EventLoopWaker { @@ -169,31 +178,50 @@ impl Default for EventLoopWaker { ptr::null_mut(), ); CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); - EventLoopWaker { timer } + EventLoopWaker { + timer, + start_instant: Instant::now(), + next_fire_date: None, + } } } } impl EventLoopWaker { pub fn stop(&mut self) { - unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + if self.next_fire_date.is_some() { + self.next_fire_date = None; + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + } } pub fn start(&mut self) { - unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + if self.next_fire_date != Some(self.start_instant) { + self.next_fire_date = Some(self.start_instant); + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + } } - pub fn start_at(&mut self, instant: Instant) { + pub fn start_at(&mut self, instant: Option) { let now = Instant::now(); - if now >= instant { - self.start(); - } else { - unsafe { - let current = CFAbsoluteTimeGetCurrent(); - let duration = instant - now; - let fsecs = - duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; - CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + match instant { + Some(instant) if now >= instant => { + self.start(); + } + Some(instant) => { + if self.next_fire_date != Some(instant) { + self.next_fire_date = Some(instant); + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } + None => { + self.stop(); } } } diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util.rs similarity index 90% rename from src/platform_impl/macos/util/mod.rs rename to src/platform_impl/macos/util.rs index 2cfdc47e84..2447a5dc07 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util.rs @@ -1,11 +1,5 @@ -#![allow(clippy::unnecessary_cast)] - -mod r#async; - -pub(crate) use self::r#async::*; - use core_graphics::display::CGDisplay; -use objc2::foundation::{CGFloat, NSNotFound, NSPoint, NSRange, NSRect, NSUInteger}; +use icrate::Foundation::{CGFloat, NSNotFound, NSPoint, NSRange, NSRect, NSUInteger}; use crate::dpi::LogicalPosition; @@ -50,6 +44,7 @@ impl Drop for TraceGuard { // For consistency with other platforms, this will... // 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 +#[allow(clippy::unnecessary_cast)] pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) as f64 } diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs deleted file mode 100644 index b8e781d198..0000000000 --- a/src/platform_impl/macos/util/async.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::ops::Deref; - -use dispatch::Queue; -use objc2::foundation::{is_main_thread, CGFloat, NSPoint, NSSize, NSString}; -use objc2::rc::{autoreleasepool, Id}; - -use crate::{ - dpi::{LogicalPosition, LogicalSize}, - platform_impl::platform::{ - appkit::{NSScreen, NSWindow, NSWindowLevel, NSWindowStyleMask}, - ffi, - window::WinitWindow, - }, -}; - -// Unsafe wrapper type that allows us to dispatch things that aren't Send. -// This should *only* be used to dispatch to the main queue. -// While it is indeed not guaranteed that these types can safely be sent to -// other threads, we know that they're safe to use on the main thread. -struct MainThreadSafe(T); - -unsafe impl Send for MainThreadSafe {} - -impl Deref for MainThreadSafe { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -fn run_on_main(f: impl FnOnce() -> R + Send) -> R { - if is_main_thread() { - f() - } else { - Queue::main().exec_sync(f) - } -} - -fn set_style_mask(window: &NSWindow, mask: NSWindowStyleMask) { - window.setStyleMask(mask); - // If we don't do this, key handling will break - // (at least until the window is clicked again/etc.) - let _ = window.makeFirstResponder(Some(&window.contentView())); -} - -// Always use this function instead of trying to modify `styleMask` directly! -// `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch. -// Otherwise, this would vomit out errors about not being on the main thread -// and fail to do anything. -pub(crate) fn set_style_mask_sync(window: &NSWindow, mask: NSWindowStyleMask) { - let window = MainThreadSafe(window); - run_on_main(move || { - set_style_mask(&window, mask); - }) -} - -// `setContentSize:` isn't thread-safe either, though it doesn't log any errors -// and just fails silently. Anyway, GCD to the rescue! -pub(crate) fn set_content_size_sync(window: &NSWindow, size: LogicalSize) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.setContentSize(NSSize::new(size.width as CGFloat, size.height as CGFloat)); - }); -} - -// `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy -// to log errors. -pub(crate) fn set_frame_top_left_point_sync(window: &NSWindow, point: NSPoint) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.setFrameTopLeftPoint(point); - }); -} - -// `setFrameTopLeftPoint:` isn't thread-safe, and fails silently. -pub(crate) fn set_level_sync(window: &NSWindow, level: NSWindowLevel) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.setLevel(level); - }); -} - -// `setIgnoresMouseEvents_:` isn't thread-safe, and fails silently. -pub(crate) fn set_ignore_mouse_events_sync(window: &NSWindow, ignore: bool) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.setIgnoresMouseEvents(ignore); - }); -} - -// `toggleFullScreen` is thread-safe, but our additional logic to account for -// window styles isn't. -pub(crate) fn toggle_full_screen_sync(window: &WinitWindow, not_fullscreen: bool) { - let window = MainThreadSafe(window); - run_on_main(move || { - // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we - // set a normal style temporarily. The previous state will be - // restored in `WindowDelegate::window_did_exit_fullscreen`. - if not_fullscreen { - let curr_mask = window.styleMask(); - let required = - NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask; - if !curr_mask.contains(required) { - set_style_mask(&window, required); - window - .lock_shared_state("toggle_full_screen_sync") - .saved_style = Some(curr_mask); - } - } - // Window level must be restored from `CGShieldingWindowLevel() - // + 1` back to normal in order for `toggleFullScreen` to do - // anything - window.setLevel(NSWindowLevel::Normal); - window.toggleFullScreen(None); - }); -} - -pub(crate) unsafe fn restore_display_mode_sync(ns_screen: u32) { - run_on_main(move || { - unsafe { ffi::CGRestorePermanentDisplayConfiguration() }; - assert_eq!( - unsafe { ffi::CGDisplayRelease(ns_screen) }, - ffi::kCGErrorSuccess - ); - }); -} - -// `setMaximized` is not thread-safe -pub(crate) fn set_maximized_sync(window: &WinitWindow, is_zoomed: bool, maximized: bool) { - let window = MainThreadSafe(window); - run_on_main(move || { - let mut shared_state = window.lock_shared_state("set_maximized_sync"); - // Save the standard frame sized if it is not zoomed - if !is_zoomed { - shared_state.standard_frame = Some(window.frame()); - } - - shared_state.maximized = maximized; - - if shared_state.fullscreen.is_some() { - // Handle it in window_did_exit_fullscreen - return; - } - - if window - .styleMask() - .contains(NSWindowStyleMask::NSResizableWindowMask) - { - drop(shared_state); - // Just use the native zoom if resizable - window.zoom(None); - } else { - // if it's not resizable, we set the frame directly - let new_rect = if maximized { - let screen = NSScreen::main().expect("no screen found"); - screen.visibleFrame() - } else { - shared_state.saved_standard_frame() - }; - drop(shared_state); - window.setFrame_display(new_rect, false); - } - }); -} - -// `orderOut:` isn't thread-safe. Calling it from another thread actually works, -// but with an odd delay. -pub(crate) fn order_out_sync(window: &NSWindow) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.orderOut(None); - }); -} - -// `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread -// actually works, but with an odd delay. -pub(crate) fn make_key_and_order_front_sync(window: &NSWindow) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.makeKeyAndOrderFront(None); - }); -} - -// `setTitle:` isn't thread-safe. Calling it from another thread invalidates the -// window drag regions, which throws an exception when not done in the main -// thread -pub(crate) fn set_title_sync(window: &NSWindow, title: &str) { - let window = MainThreadSafe(window); - run_on_main(move || { - window.setTitle(&NSString::from_str(title)); - }); -} - -// `close:` is thread-safe, but we want the event to be triggered from the main -// thread. Though, it's a good idea to look into that more... -pub(crate) fn close_sync(window: &NSWindow) { - let window = MainThreadSafe(window); - run_on_main(move || { - autoreleasepool(move |_| { - window.close(); - }); - }); -} - -pub(crate) fn set_ime_cursor_area_sync( - window: &WinitWindow, - logical_spot: LogicalPosition, - size: LogicalSize, -) { - let window = MainThreadSafe(window); - run_on_main(move || { - // TODO(madsmtm): Remove the need for this - unsafe { Id::from_shared(window.view()) }.set_ime_cursor_area(logical_spot, size); - }); -} - -pub(crate) fn get_kbd_type() -> u8 { - run_on_main(|| unsafe { ffi::LMGetKbdType() }) -} diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index d5375b26d6..69b357b7ab 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -1,24 +1,23 @@ #![allow(clippy::unnecessary_cast)] +use std::boxed::Box; +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::ptr::NonNull; -use std::{ - boxed::Box, - collections::{HashMap, VecDeque}, - os::raw::*, - ptr, str, - sync::Mutex, -}; - -use objc2::declare::{Ivar, IvarDrop}; -use objc2::foundation::{ +use icrate::Foundation::{ NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString, - NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, + NSObject, NSObjectProtocol, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, }; -use objc2::rc::{Id, Owned, Shared, WeakId}; -use objc2::runtime::{Object, Sel}; -use objc2::{class, declare_class, msg_send, msg_send_id, sel, ClassType}; +use objc2::declare::{Ivar, IvarDrop}; +use objc2::rc::{Id, WeakId}; +use objc2::runtime::{AnyObject, Sel}; +use objc2::{class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType}; use super::{ - appkit::{NSApp, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTrackingRectTag, NSView}, + appkit::{ + NSApp, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingRectTag, + NSView, + }, event::{code_to_key, code_to_location}, }; use crate::{ @@ -27,12 +26,12 @@ use crate::{ DeviceEvent, ElementState, Event, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, }, - keyboard::{Key, KeyCode, KeyLocation, ModifiersState}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey, PhysicalKey}, platform::macos::{OptionAsAlt, WindowExtMacOS}, - platform::scancode::KeyCodeExtScancode, + platform::scancode::PhysicalKeyExtScancode, platform_impl::platform::{ app_state::AppState, - event::{create_key_event, event_mods, EventWrapper}, + event::{create_key_event, event_mods}, util, window::WinitWindow, DEVICE_ID, @@ -41,9 +40,9 @@ use crate::{ }; #[derive(Debug)] -pub struct CursorState { - pub visible: bool, - pub(super) cursor: Id, +struct CursorState { + visible: bool, + cursor: Id, } impl Default for CursorState { @@ -55,8 +54,9 @@ impl Default for CursorState { } } -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)] enum ImeState { + #[default] /// The IME events are disabled, so only `ReceivedCharacter` is being sent to the user. Disabled, @@ -74,8 +74,8 @@ enum ImeState { bitflags! { #[derive(Debug, Clone, Copy, PartialEq)] struct ModLocationMask: u8 { - const LEFT = 1; - const RIGHT = 2; + const LEFT = 0b0001; + const RIGHT = 0b0010; } } impl ModLocationMask { @@ -88,56 +88,58 @@ impl ModLocationMask { } } -pub fn key_to_modifier(key: &Key) -> ModifiersState { +fn key_to_modifier(key: &Key) -> Option { match key { - Key::Alt => ModifiersState::ALT, - Key::Control => ModifiersState::CONTROL, - Key::Super => ModifiersState::SUPER, - Key::Shift => ModifiersState::SHIFT, - _ => unreachable!(), + Key::Named(NamedKey::Alt) => Some(ModifiersState::ALT), + Key::Named(NamedKey::Control) => Some(ModifiersState::CONTROL), + Key::Named(NamedKey::Super) => Some(ModifiersState::SUPER), + Key::Named(NamedKey::Shift) => Some(ModifiersState::SHIFT), + _ => None, } } fn get_right_modifier_code(key: &Key) -> KeyCode { match key { - Key::Alt => KeyCode::AltRight, - Key::Control => KeyCode::ControlRight, - Key::Shift => KeyCode::ShiftRight, - Key::Super => KeyCode::SuperRight, + Key::Named(NamedKey::Alt) => KeyCode::AltRight, + Key::Named(NamedKey::Control) => KeyCode::ControlRight, + Key::Named(NamedKey::Shift) => KeyCode::ShiftRight, + Key::Named(NamedKey::Super) => KeyCode::SuperRight, _ => unreachable!(), } } fn get_left_modifier_code(key: &Key) -> KeyCode { match key { - Key::Alt => KeyCode::AltLeft, - Key::Control => KeyCode::ControlLeft, - Key::Shift => KeyCode::ShiftLeft, - Key::Super => KeyCode::SuperLeft, + Key::Named(NamedKey::Alt) => KeyCode::AltLeft, + Key::Named(NamedKey::Control) => KeyCode::ControlLeft, + Key::Named(NamedKey::Shift) => KeyCode::ShiftLeft, + Key::Named(NamedKey::Super) => KeyCode::SuperLeft, _ => unreachable!(), } } -#[derive(Debug)] -pub(super) struct ViewState { - pub cursor_state: Mutex, - ime_position: LogicalPosition, - ime_size: LogicalSize, - pub(super) modifiers: Modifiers, - phys_modifiers: HashMap, - tracking_rect: Option, - // phys_modifiers: HashSet, - ime_state: ImeState, - input_source: String, +#[derive(Debug, Default)] +pub struct ViewState { + cursor_state: RefCell, + ime_position: Cell>, + ime_size: Cell>, + modifiers: Cell, + phys_modifiers: RefCell>, + tracking_rect: Cell>, + ime_state: Cell, + input_source: RefCell, /// True iff the application wants IME events. /// /// Can be set using `set_ime_allowed` - ime_allowed: bool, + ime_allowed: Cell, /// True if the current key event should be forwarded /// to the application, even during IME - forward_key_to_app: bool, + forward_key_to_app: Cell, + + marked_text: RefCell>, + accepts_first_mouse: bool, } declare_class!( @@ -145,37 +147,31 @@ declare_class!( #[allow(non_snake_case)] pub(super) struct WinitView { // Weak reference because the window keeps a strong reference to the view - _ns_window: IvarDrop>>, - pub(super) state: IvarDrop>, - marked_text: IvarDrop>, - accepts_first_mouse: bool, + _ns_window: IvarDrop>, "__ns_window">, + state: IvarDrop, "_state">, } + mod ivars; + unsafe impl ClassType for WinitView { #[inherits(NSResponder, NSObject)] type Super = NSView; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitView"; } unsafe impl WinitView { - #[sel(initWithId:acceptsFirstMouse:)] - fn init_with_id( - &mut self, + #[method(initWithId:acceptsFirstMouse:)] + unsafe fn init_with_id( + this: *mut Self, window: &WinitWindow, accepts_first_mouse: bool, - ) -> Option<&mut Self> { - let this: Option<&mut Self> = unsafe { msg_send![super(self), init] }; + ) -> Option> { + let this: Option<&mut Self> = unsafe { msg_send![super(this), init] }; this.map(|this| { let state = ViewState { - cursor_state: Default::default(), - ime_position: LogicalPosition::new(0.0, 0.0), - ime_size: Default::default(), - modifiers: Default::default(), - phys_modifiers: Default::default(), - tracking_rect: None, - ime_state: ImeState::Disabled, - input_source: String::new(), - ime_allowed: false, - forward_key_to_app: false, + accepts_first_mouse, + ..Default::default() }; Ivar::write( @@ -183,12 +179,10 @@ declare_class!( Box::new(WeakId::new(&window.retain())), ); Ivar::write(&mut this.state, Box::new(state)); - Ivar::write(&mut this.marked_text, NSMutableAttributedString::new()); - Ivar::write(&mut this.accepts_first_mouse, accepts_first_mouse); this.setPostsFrameChangedNotifications(true); - let notification_center: &Object = + let notification_center: &AnyObject = unsafe { msg_send![class!(NSNotificationCenter), defaultCenter] }; // About frame change let frame_did_change_notification_name = @@ -204,35 +198,35 @@ declare_class!( ]; } - this.state.input_source = this.current_input_source(); - this + *this.state.input_source.borrow_mut() = this.current_input_source(); + NonNull::from(this) }) } } unsafe impl WinitView { - #[sel(viewDidMoveToWindow)] - fn view_did_move_to_window(&mut self) { + #[method(viewDidMoveToWindow)] + fn view_did_move_to_window(&self) { trace_scope!("viewDidMoveToWindow"); if let Some(tracking_rect) = self.state.tracking_rect.take() { self.removeTrackingRect(tracking_rect); } - let rect = self.visibleRect(); + let rect = self.frame(); let tracking_rect = self.add_tracking_rect(rect, false); - self.state.tracking_rect = Some(tracking_rect); + self.state.tracking_rect.set(Some(tracking_rect)); } - #[sel(frameDidChange:)] - fn frame_did_change(&mut self, _event: &NSEvent) { + #[method(frameDidChange:)] + fn frame_did_change(&self, _event: &NSEvent) { trace_scope!("frameDidChange:"); if let Some(tracking_rect) = self.state.tracking_rect.take() { self.removeTrackingRect(tracking_rect); } - let rect = self.visibleRect(); + let rect = self.frame(); let tracking_rect = self.add_tracking_rect(rect, false); - self.state.tracking_rect = Some(tracking_rect); + self.state.tracking_rect.set(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. @@ -242,8 +236,8 @@ declare_class!( self.queue_event(WindowEvent::Resized(size)); } - #[sel(drawRect:)] - fn draw_rect(&mut self, rect: NSRect) { + #[method(drawRect:)] + fn draw_rect(&self, rect: NSRect) { trace_scope!("drawRect:"); // It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`. @@ -257,7 +251,7 @@ declare_class!( } } - #[sel(acceptsFirstResponder)] + #[method(acceptsFirstResponder)] fn accepts_first_responder(&self) -> bool { trace_scope!("acceptsFirstResponder"); true @@ -266,17 +260,17 @@ declare_class!( // This is necessary to prevent a beefy terminal error on MacBook Pros: // IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem // TODO: Add an API extension for using `NSTouchBar` - #[sel(touchBar)] - fn touch_bar(&self) -> bool { + #[method_id(touchBar)] + fn touch_bar(&self) -> Option> { trace_scope!("touchBar"); - false + None } - #[sel(resetCursorRects)] + #[method(resetCursorRects)] fn reset_cursor_rects(&self) { trace_scope!("resetCursorRects"); let bounds = self.bounds(); - let cursor_state = self.state.cursor_state.lock().unwrap(); + let cursor_state = self.state.cursor_state.borrow(); // We correctly invoke `addCursorRect` only from inside `resetCursorRects` if cursor_state.visible { self.addCursorRect(bounds, &cursor_state.cursor); @@ -286,17 +280,17 @@ declare_class!( } } - unsafe impl Protocol for WinitView { - #[sel(hasMarkedText)] + unsafe impl NSTextInputClient for WinitView { + #[method(hasMarkedText)] fn has_marked_text(&self) -> bool { trace_scope!("hasMarkedText"); - self.marked_text.len_utf16() > 0 + self.state.marked_text.borrow().length() > 0 } - #[sel(markedRange)] + #[method(markedRange)] fn marked_range(&self) -> NSRange { trace_scope!("markedRange"); - let length = self.marked_text.len_utf16(); + let length = self.state.marked_text.borrow().length(); if length > 0 { NSRange::new(0, length) } else { @@ -304,15 +298,15 @@ declare_class!( } } - #[sel(selectedRange)] + #[method(selectedRange)] fn selected_range(&self) -> NSRange { trace_scope!("selectedRange"); util::EMPTY_RANGE } - #[sel(setMarkedText:selectedRange:replacementRange:)] + #[method(setMarkedText:selectedRange:replacementRange:)] fn set_marked_text( - &mut self, + &self, string: &NSObject, _selected_range: NSRange, _replacement_range: NSRange, @@ -339,19 +333,19 @@ declare_class!( }; // Update marked text. - *self.marked_text = marked_text; + *self.state.marked_text.borrow_mut() = marked_text; // Notify IME is active if application still doesn't know it. - if self.state.ime_state == ImeState::Disabled { - self.state.input_source = self.current_input_source(); + if self.state.ime_state.get() == ImeState::Disabled { + *self.state.input_source.borrow_mut() = self.current_input_source(); self.queue_event(WindowEvent::Ime(Ime::Enabled)); } if self.hasMarkedText() { - self.state.ime_state = ImeState::Preedit; + self.state.ime_state.set(ImeState::Preedit); } else { // In case the preedit was cleared, set IME into the Ground state. - self.state.ime_state = ImeState::Ground; + self.state.ime_state.set(ImeState::Ground); } // Empty string basically means that there's no preedit, so indicate that by sending @@ -366,10 +360,10 @@ declare_class!( self.queue_event(WindowEvent::Ime(Ime::Preedit(preedit_string, cursor_range))); } - #[sel(unmarkText)] - fn unmark_text(&mut self) { + #[method(unmarkText)] + fn unmark_text(&self) { trace_scope!("unmarkText"); - *self.marked_text = NSMutableAttributedString::new(); + *self.state.marked_text.borrow_mut() = NSMutableAttributedString::new(); let input_context = self.inputContext().expect("input context"); input_context.discardMarkedText(); @@ -377,53 +371,53 @@ declare_class!( self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); if self.is_ime_enabled() { // Leave the Preedit self.state - self.state.ime_state = ImeState::Ground; + self.state.ime_state.set(ImeState::Ground); } else { warn!("Expected to have IME enabled when receiving unmarkText"); } } - #[sel(validAttributesForMarkedText)] - fn valid_attributes_for_marked_text(&self) -> *const NSArray { + #[method_id(validAttributesForMarkedText)] + fn valid_attributes_for_marked_text(&self) -> Id> { trace_scope!("validAttributesForMarkedText"); - Id::autorelease_return(NSArray::new()) + NSArray::new() } - #[sel(attributedSubstringForProposedRange:actualRange:)] + #[method_id(attributedSubstringForProposedRange:actualRange:)] fn attributed_substring_for_proposed_range( &self, _range: NSRange, - _actual_range: *mut c_void, // *mut NSRange - ) -> *const NSAttributedString { + _actual_range: *mut NSRange, + ) -> Option> { trace_scope!("attributedSubstringForProposedRange:actualRange:"); - ptr::null() + None } - #[sel(characterIndexForPoint:)] + #[method(characterIndexForPoint:)] fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger { trace_scope!("characterIndexForPoint:"); 0 } - #[sel(firstRectForCharacterRange:actualRange:)] + #[method(firstRectForCharacterRange:actualRange:)] fn first_rect_for_character_range( &self, _range: NSRange, - _actual_range: *mut c_void, // *mut NSRange + _actual_range: *mut NSRange, ) -> NSRect { trace_scope!("firstRectForCharacterRange:actualRange:"); let window = self.window(); let content_rect = window.contentRectForFrameRect(window.frame()); let base_x = content_rect.origin.x as f64; let base_y = (content_rect.origin.y + content_rect.size.height) as f64; - let x = base_x + self.state.ime_position.x; - let y = base_y - self.state.ime_position.y; - let LogicalSize { width, height } = self.state.ime_size; + let LogicalSize { width, height } = self.state.ime_size.get(); + let x = base_x + self.state.ime_position.get().x; + let y = base_y - self.state.ime_position.get().y - height; NSRect::new(NSPoint::new(x as _, y as _), NSSize::new(width, height)) } - #[sel(insertText:replacementRange:)] - fn insert_text(&mut self, string: &NSObject, _replacement_range: NSRange) { + #[method(insertText:replacementRange:)] + fn insert_text(&self, string: &NSObject, _replacement_range: NSRange) { trace_scope!("insertText:replacementRange:"); // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. @@ -443,45 +437,49 @@ declare_class!( if self.hasMarkedText() && self.is_ime_enabled() && !is_control { self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); self.queue_event(WindowEvent::Ime(Ime::Commit(string))); - self.state.ime_state = ImeState::Commited; + self.state.ime_state.set(ImeState::Commited); } } // 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. - #[sel(doCommandBySelector:)] - fn do_command_by_selector(&mut self, _command: Sel) { + #[method(doCommandBySelector:)] + fn do_command_by_selector(&self, _command: Sel) { trace_scope!("doCommandBySelector:"); // 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 self.state.ime_state == ImeState::Commited { + if self.state.ime_state.get() == ImeState::Commited { return; } - self.state.forward_key_to_app = true; + self.state.forward_key_to_app.set(true); - if self.hasMarkedText() && self.state.ime_state == ImeState::Preedit { + if self.hasMarkedText() && self.state.ime_state.get() == ImeState::Preedit { // Leave preedit so that we also report the key-up for this key. - self.state.ime_state = ImeState::Ground; + self.state.ime_state.set(ImeState::Ground); } } } unsafe impl WinitView { - #[sel(keyDown:)] - fn key_down(&mut self, event: &NSEvent) { + #[method(keyDown:)] + fn key_down(&self, event: &NSEvent) { trace_scope!("keyDown:"); - let input_source = self.current_input_source(); - if self.state.input_source != input_source && self.is_ime_enabled() { - self.state.ime_state = ImeState::Disabled; - self.state.input_source = input_source; - self.queue_event(WindowEvent::Ime(Ime::Disabled)); + { + let mut prev_input_source = self.state.input_source.borrow_mut(); + let current_input_source = self.current_input_source(); + if *prev_input_source != current_input_source && self.is_ime_enabled() { + *prev_input_source = current_input_source; + drop(prev_input_source); + self.state.ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } } // Get the characters from the event. - let old_ime_state = self.state.ime_state; - self.state.forward_key_to_app = false; + let old_ime_state = self.state.ime_state.get(); + self.state.forward_key_to_app.set(false); let event = replace_event(event, self.window().option_as_alt()); // The `interpretKeyEvents` function might call @@ -490,31 +488,31 @@ declare_class!( // 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) - if self.state.ime_allowed { - let events_for_nsview = NSArray::from_slice(&[event.copy()]); + if self.state.ime_allowed.get() { + let events_for_nsview = NSArray::from_slice(&[&*event]); unsafe { self.interpretKeyEvents(&events_for_nsview) }; // If the text was commited we must treat the next keyboard event as IME related. - if self.state.ime_state == ImeState::Commited { + if self.state.ime_state.get() == ImeState::Commited { // Remove any marked text, so normal input can continue. - *self.marked_text = NSMutableAttributedString::new(); + *self.state.marked_text.borrow_mut() = NSMutableAttributedString::new(); } } self.update_modifiers(&event, false); - let had_ime_input = match self.state.ime_state { + let had_ime_input = match self.state.ime_state.get() { ImeState::Commited => { // Allow normal input after the commit. - self.state.ime_state = ImeState::Ground; + self.state.ime_state.set(ImeState::Ground); true } ImeState::Preedit => true, // `key_down` could result in preedit clear, so compare old and current state. - _ => old_ime_state != self.state.ime_state, + _ => old_ime_state != self.state.ime_state.get(), }; - if !had_ime_input || self.state.forward_key_to_app { + if !had_ime_input || self.state.forward_key_to_app.get() { let key_event = create_key_event(&event, true, event.is_a_repeat(), None); self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, @@ -524,15 +522,18 @@ declare_class!( } } - #[sel(keyUp:)] - fn key_up(&mut self, event: &NSEvent) { + #[method(keyUp:)] + fn key_up(&self, event: &NSEvent) { trace_scope!("keyUp:"); let event = replace_event(event, self.window().option_as_alt()); self.update_modifiers(&event, false); // We want to send keyboard input when we are currently in the ground state. - if matches!(self.state.ime_state, ImeState::Ground | ImeState::Disabled) { + if matches!( + self.state.ime_state.get(), + ImeState::Ground | ImeState::Disabled + ) { self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, event: create_key_event(&event, false, false, None), @@ -541,15 +542,15 @@ declare_class!( } } - #[sel(flagsChanged:)] - fn flags_changed(&mut self, ns_event: &NSEvent) { + #[method(flagsChanged:)] + fn flags_changed(&self, event: &NSEvent) { trace_scope!("flagsChanged:"); - self.update_modifiers(ns_event, true); + self.update_modifiers(event, true); } - #[sel(insertTab:)] - fn insert_tab(&self, _sender: *const Object) { + #[method(insertTab:)] + fn insert_tab(&self, _sender: Option<&AnyObject>) { trace_scope!("insertTab:"); let window = self.window(); if let Some(first_responder) = window.firstResponder() { @@ -559,8 +560,8 @@ declare_class!( } } - #[sel(insertBackTab:)] - fn insert_back_tab(&self, _sender: *const Object) { + #[method(insertBackTab:)] + fn insert_back_tab(&self, _sender: Option<&AnyObject>) { trace_scope!("insertBackTab:"); let window = self.window(); if let Some(first_responder) = window.firstResponder() { @@ -572,8 +573,8 @@ declare_class!( // Allows us to receive Cmd-. (the shortcut for closing a dialog) // https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 - #[sel(cancelOperation:)] - fn cancel_operation(&mut self, _sender: *const Object) { + #[method(cancelOperation:)] + fn cancel_operation(&self, _sender: Option<&AnyObject>) { trace_scope!("cancelOperation:"); let event = NSApp() @@ -590,43 +591,43 @@ declare_class!( }); } - #[sel(mouseDown:)] - fn mouse_down(&mut self, event: &NSEvent) { + #[method(mouseDown:)] + fn mouse_down(&self, event: &NSEvent) { trace_scope!("mouseDown:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Pressed); } - #[sel(mouseUp:)] - fn mouse_up(&mut self, event: &NSEvent) { + #[method(mouseUp:)] + fn mouse_up(&self, event: &NSEvent) { trace_scope!("mouseUp:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Released); } - #[sel(rightMouseDown:)] - fn right_mouse_down(&mut self, event: &NSEvent) { + #[method(rightMouseDown:)] + fn right_mouse_down(&self, event: &NSEvent) { trace_scope!("rightMouseDown:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Pressed); } - #[sel(rightMouseUp:)] - fn right_mouse_up(&mut self, event: &NSEvent) { + #[method(rightMouseUp:)] + fn right_mouse_up(&self, event: &NSEvent) { trace_scope!("rightMouseUp:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Released); } - #[sel(otherMouseDown:)] - fn other_mouse_down(&mut self, event: &NSEvent) { + #[method(otherMouseDown:)] + fn other_mouse_down(&self, event: &NSEvent) { trace_scope!("otherMouseDown:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Pressed); } - #[sel(otherMouseUp:)] - fn other_mouse_up(&mut self, event: &NSEvent) { + #[method(otherMouseUp:)] + fn other_mouse_up(&self, event: &NSEvent) { trace_scope!("otherMouseUp:"); self.mouse_motion(event); self.mouse_click(event, ElementState::Released); @@ -634,27 +635,27 @@ declare_class!( // No tracing on these because that would be overly verbose - #[sel(mouseMoved:)] - fn mouse_moved(&mut self, event: &NSEvent) { + #[method(mouseMoved:)] + fn mouse_moved(&self, event: &NSEvent) { self.mouse_motion(event); } - #[sel(mouseDragged:)] - fn mouse_dragged(&mut self, event: &NSEvent) { + #[method(mouseDragged:)] + fn mouse_dragged(&self, event: &NSEvent) { self.mouse_motion(event); } - #[sel(rightMouseDragged:)] - fn right_mouse_dragged(&mut self, event: &NSEvent) { + #[method(rightMouseDragged:)] + fn right_mouse_dragged(&self, event: &NSEvent) { self.mouse_motion(event); } - #[sel(otherMouseDragged:)] - fn other_mouse_dragged(&mut self, event: &NSEvent) { + #[method(otherMouseDragged:)] + fn other_mouse_dragged(&self, event: &NSEvent) { self.mouse_motion(event); } - #[sel(mouseEntered:)] + #[method(mouseEntered:)] fn mouse_entered(&self, _event: &NSEvent) { trace_scope!("mouseEntered:"); self.queue_event(WindowEvent::CursorEntered { @@ -662,7 +663,7 @@ declare_class!( }); } - #[sel(mouseExited:)] + #[method(mouseExited:)] fn mouse_exited(&self, _event: &NSEvent) { trace_scope!("mouseExited:"); @@ -671,8 +672,8 @@ declare_class!( }); } - #[sel(scrollWheel:)] - fn scroll_wheel(&mut self, event: &NSEvent) { + #[method(scrollWheel:)] + fn scroll_wheel(&self, event: &NSEvent) { trace_scope!("scrollWheel:"); self.mouse_motion(event); @@ -719,7 +720,7 @@ declare_class!( }); } - #[sel(magnifyWithEvent:)] + #[method(magnifyWithEvent:)] fn magnify_with_event(&self, event: &NSEvent) { trace_scope!("magnifyWithEvent:"); @@ -738,7 +739,7 @@ declare_class!( }); } - #[sel(smartMagnifyWithEvent:)] + #[method(smartMagnifyWithEvent:)] fn smart_magnify_with_event(&self, _event: &NSEvent) { trace_scope!("smartMagnifyWithEvent:"); @@ -747,7 +748,7 @@ declare_class!( }); } - #[sel(rotateWithEvent:)] + #[method(rotateWithEvent:)] fn rotate_with_event(&self, event: &NSEvent) { trace_scope!("rotateWithEvent:"); @@ -766,8 +767,8 @@ declare_class!( }); } - #[sel(pressureChangeWithEvent:)] - fn pressure_change_with_event(&mut self, event: &NSEvent) { + #[method(pressureChangeWithEvent:)] + fn pressure_change_with_event(&self, event: &NSEvent) { trace_scope!("pressureChangeWithEvent:"); self.mouse_motion(event); @@ -782,32 +783,32 @@ declare_class!( // Allows us to receive Ctrl-Tab and Ctrl-Esc. // Note that this *doesn't* help with any missing Cmd inputs. // https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816 - #[sel(_wantsKeyDownForEvent:)] + #[method(_wantsKeyDownForEvent:)] fn wants_key_down_for_event(&self, _event: &NSEvent) -> bool { trace_scope!("_wantsKeyDownForEvent:"); true } - #[sel(acceptsFirstMouse:)] + #[method(acceptsFirstMouse:)] fn accepts_first_mouse(&self, _event: &NSEvent) -> bool { trace_scope!("acceptsFirstMouse:"); - *self.accepts_first_mouse + self.state.accepts_first_mouse } } ); impl WinitView { - pub(super) fn new(window: &WinitWindow, accepts_first_mouse: bool) -> Id { + pub(super) fn new(window: &WinitWindow, accepts_first_mouse: bool) -> Id { unsafe { msg_send_id![ - msg_send_id![Self::class(), alloc], + Self::alloc(), initWithId: window, acceptsFirstMouse: accepts_first_mouse, ] } } - fn window(&self) -> Id { + fn window(&self) -> Id { // TODO: Simply use `window` property on `NSView`. // That only returns a window _after_ the view has been attached though! // (which is incompatible with `frameDidChange:`) @@ -820,12 +821,12 @@ impl WinitView { WindowId(self.window().id()) } - fn queue_event(&self, event: WindowEvent<'static>) { + fn queue_event(&self, event: WindowEvent) { let event = Event::WindowEvent { window_id: self.window_id(), event, }; - AppState::queue_event(EventWrapper::StaticEvent(event)); + AppState::queue_event(event); } fn queue_device_event(&self, event: DeviceEvent) { @@ -833,7 +834,7 @@ impl WinitView { device_id: DEVICE_ID, event, }; - AppState::queue_event(EventWrapper::StaticEvent(event)); + AppState::queue_event(event); } fn scale_factor(&self) -> f64 { @@ -841,7 +842,7 @@ impl WinitView { } fn is_ime_enabled(&self) -> bool { - !matches!(self.state.ime_state, ImeState::Disabled) + !matches!(self.state.ime_state.get(), ImeState::Disabled) } fn current_input_source(&self) -> String { @@ -849,134 +850,170 @@ impl WinitView { .expect("input context") .selectedKeyboardInputSource() .map(|input_source| input_source.to_string()) - .unwrap_or_else(String::new) + .unwrap_or_default() } - pub(super) fn set_ime_allowed(&mut self, ime_allowed: bool) { - if self.state.ime_allowed == ime_allowed { + pub(super) fn set_cursor_icon(&self, icon: Id) { + let mut cursor_state = self.state.cursor_state.borrow_mut(); + cursor_state.cursor = icon; + } + + /// Set whether the cursor should be visible or not. + /// + /// Returns whether the state changed. + pub(super) fn set_cursor_visible(&self, visible: bool) -> bool { + let mut cursor_state = self.state.cursor_state.borrow_mut(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + true + } else { + false + } + } + + pub(super) fn set_ime_allowed(&self, ime_allowed: bool) { + if self.state.ime_allowed.get() == ime_allowed { return; } - self.state.ime_allowed = ime_allowed; - if self.state.ime_allowed { + self.state.ime_allowed.set(ime_allowed); + if self.state.ime_allowed.get() { return; } // Clear markedText - *self.marked_text = NSMutableAttributedString::new(); + *self.state.marked_text.borrow_mut() = NSMutableAttributedString::new(); - if self.state.ime_state != ImeState::Disabled { - self.state.ime_state = ImeState::Disabled; + if self.state.ime_state.get() != ImeState::Disabled { + self.state.ime_state.set(ImeState::Disabled); self.queue_event(WindowEvent::Ime(Ime::Disabled)); } } pub(super) fn set_ime_cursor_area( - &mut self, + &self, position: LogicalPosition, size: LogicalSize, ) { - self.state.ime_position = position; - self.state.ime_size = size; + self.state.ime_position.set(position); + self.state.ime_size.set(size); let input_context = self.inputContext().expect("input context"); input_context.invalidateCharacterCoordinates(); } - // Update `state.modifiers` if `event` has something different - fn update_modifiers(&mut self, ns_event: &NSEvent, is_flags_changed_event: bool) { + /// Reset modifiers and emit a synthetic ModifiersChanged event if deemed necessary. + pub(super) fn reset_modifiers(&self) { + if !self.state.modifiers.get().state().is_empty() { + self.state.modifiers.set(Modifiers::default()); + self.queue_event(WindowEvent::ModifiersChanged(self.state.modifiers.get())); + } + } + + /// Update modifiers if `event` has something different + fn update_modifiers(&self, ns_event: &NSEvent, is_flags_changed_event: bool) { use ElementState::{Pressed, Released}; let current_modifiers = event_mods(ns_event); - let prev_modifiers = self.state.modifiers; - - self.state.modifiers = current_modifiers; + let prev_modifiers = self.state.modifiers.get(); + self.state.modifiers.set(current_modifiers); // This function was called form the flagsChanged event, which is triggered // when the user presses/releases a modifier even if the same kind of modifier - // has already been pressed - if is_flags_changed_event { - let scancode = ns_event.key_code(); - let keycode = KeyCode::from_scancode(scancode as u32); - - // We'll correct the `is_press` later. - let mut event = create_key_event(ns_event, false, false, Some(keycode)); - - let key = code_to_key(keycode, scancode); - let event_modifier = key_to_modifier(&key); - event.physical_key = keycode; - event.logical_key = key.clone(); - event.location = code_to_location(keycode); - let location_mask = ModLocationMask::from_location(event.location); - - let phys_mod = self - .state - .phys_modifiers - .entry(key) - .or_insert(ModLocationMask::empty()); - - let is_active = current_modifiers.state().contains(event_modifier); - let mut events = VecDeque::with_capacity(2); - - // There is no API for getting whether the button was pressed or released - // during this event. For this reason we have to do a bit of magic below - // to come up with a good guess whether this key was pressed or released. - // (This is not trivial because there are multiple buttons that may affect - // the same modifier) - if !is_active { - event.state = Released; - if phys_mod.contains(ModLocationMask::LEFT) { - let mut event = event.clone(); - event.location = KeyLocation::Left; - event.physical_key = get_left_modifier_code(&event.logical_key); - events.push_back(WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - event, - is_synthetic: false, - }); - } - if phys_mod.contains(ModLocationMask::RIGHT) { - event.location = KeyLocation::Right; - event.physical_key = get_right_modifier_code(&event.logical_key); + // has already been pressed. + // + // When flags changed event has key code of zero it means that event doesn't carry any key + // event, thus we can't generate regular presses based on that. The `ModifiersChanged` + // later will work though, since the flags are attached to the event and contain valid + // information. + 'send_event: { + if is_flags_changed_event && ns_event.key_code() != 0 { + let scancode = ns_event.key_code(); + let physical_key = PhysicalKey::from_scancode(scancode as u32); + + // We'll correct the `is_press` later. + let mut event = create_key_event(ns_event, false, false, Some(physical_key)); + + let key = code_to_key(physical_key, scancode); + // Ignore processing of unkown modifiers because we can't determine whether + // it was pressed or release reliably. + let Some(event_modifier) = key_to_modifier(&key) else { + break 'send_event; + }; + event.physical_key = physical_key; + event.logical_key = key.clone(); + event.location = code_to_location(physical_key); + let location_mask = ModLocationMask::from_location(event.location); + + let mut phys_mod_state = self.state.phys_modifiers.borrow_mut(); + let phys_mod = phys_mod_state + .entry(key) + .or_insert(ModLocationMask::empty()); + + let is_active = current_modifiers.state().contains(event_modifier); + let mut events = VecDeque::with_capacity(2); + + // There is no API for getting whether the button was pressed or released + // during this event. For this reason we have to do a bit of magic below + // to come up with a good guess whether this key was pressed or released. + // (This is not trivial because there are multiple buttons that may affect + // the same modifier) + if !is_active { + event.state = Released; + if phys_mod.contains(ModLocationMask::LEFT) { + let mut event = event.clone(); + event.location = KeyLocation::Left; + event.physical_key = get_left_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + if phys_mod.contains(ModLocationMask::RIGHT) { + event.location = KeyLocation::Right; + event.physical_key = get_right_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + *phys_mod = ModLocationMask::empty(); + } else { + if *phys_mod == location_mask { + // Here we hit a contradiction: + // The modifier state was "changed" to active, + // yet the only pressed modifier key was the one that we + // just got a change event for. + // This seemingly means that the only pressed modifier is now released, + // but at the same time the modifier became active. + // + // But this scenario is possible if we released modifiers + // while the application was not in focus. (Because we don't + // get informed of modifier key events while the application + // is not focused) + + // In this case we prioritize the information + // about the current modifier state which means + // that the button was pressed. + event.state = Pressed; + } else { + phys_mod.toggle(location_mask); + let is_pressed = phys_mod.contains(location_mask); + event.state = if is_pressed { Pressed } else { Released }; + } + events.push_back(WindowEvent::KeyboardInput { device_id: DEVICE_ID, event, is_synthetic: false, }); } - *phys_mod = ModLocationMask::empty(); - } else { - // is_active - if *phys_mod == location_mask { - // Here we hit a contradiction: - // The modifier state was "changed" to active, - // yet the only pressed modifier key was the one that we - // just got a change event for. - // This seemingly means that the only pressed modifier is now released, - // but at the same time the modifier became active. - // - // But this scenario is possible if we released modifiers - // while the application was not in focus. (Because we don't - // get informed of modifier key events while the application - // is not focused) - - // In this case we prioritize the information - // about the current modifier state which means - // that the button was pressed. - event.state = Pressed; - } else { - phys_mod.toggle(location_mask); - let is_pressed = phys_mod.contains(location_mask); - event.state = if is_pressed { Pressed } else { Released }; - } - events.push_back(WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - event, - is_synthetic: false, - }); - } + drop(phys_mod_state); - for event in events { - self.queue_event(event); + for event in events { + self.queue_event(event); + } } } @@ -984,10 +1021,10 @@ impl WinitView { return; } - self.queue_event(WindowEvent::ModifiersChanged(self.state.modifiers)); + self.queue_event(WindowEvent::ModifiersChanged(self.state.modifiers.get())); } - fn mouse_click(&mut self, event: &NSEvent, button_state: ElementState) { + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { let button = mouse_button(event); self.update_modifiers(event, false); @@ -999,7 +1036,7 @@ impl WinitView { }); } - fn mouse_motion(&mut self, event: &NSEvent) { + fn mouse_motion(&self, event: &NSEvent) { let window_point = event.locationInWindow(); let view_point = self.convertPoint_fromView(window_point, None); let view_rect = self.frame(); @@ -1049,7 +1086,7 @@ fn mouse_button(event: &NSEvent) -> MouseButton { // NOTE: to get option as alt working we need to rewrite events // we're getting from the operating system, which makes it // impossible to provide such events as extra in `KeyEvent`. -fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id { +fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id { let ev_mods = event_mods(event).state; let ignore_alt_characters = match option_as_alt { OptionAsAlt::OnlyLeft if event.lalt_pressed() => true, diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7e356eec75..7e7d6f383e 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1,18 +1,11 @@ #![allow(clippy::unnecessary_cast)] -use std::{ - collections::VecDeque, - f64, ops, - os::raw::c_void, - sync::{ - atomic::{AtomicBool, Ordering}, - Mutex, MutexGuard, - }, -}; - -use raw_window_handle::{ - AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle, -}; +use std::collections::VecDeque; +use std::f64; +use std::ops; +use std::os::raw::c_void; +use std::ptr::NonNull; +use std::sync::{Mutex, MutexGuard}; use crate::{ dpi::{ @@ -25,6 +18,7 @@ use crate::{ platform_impl::platform::{ app_state::AppState, appkit::NSWindowOrderingMode, + event_loop::EventLoopWindowTarget, ffi, monitor::{self, MonitorHandle, VideoMode}, util, @@ -38,20 +32,63 @@ use crate::{ }, }; use core_graphics::display::{CGDisplay, CGPoint}; -use objc2::declare::{Ivar, IvarDrop}; -use objc2::foundation::{ - is_main_thread, CGFloat, NSArray, NSCopying, NSInteger, NSObject, NSPoint, NSRect, NSSize, - NSString, +use icrate::Foundation::{ + CGFloat, MainThreadBound, MainThreadMarker, NSArray, NSCopying, NSInteger, NSObject, NSPoint, + NSRect, NSSize, NSString, }; -use objc2::rc::{autoreleasepool, Id, Owned, Shared}; -use objc2::{declare_class, msg_send, msg_send_id, sel, ClassType}; +use objc2::declare::{Ivar, IvarDrop}; +use objc2::rc::{autoreleasepool, Id}; +use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType}; use super::appkit::{ NSApp, NSAppKitVersion, NSAppearance, NSApplicationPresentationOptions, NSBackingStoreType, NSColor, NSCursor, NSFilenamesPboardType, NSRequestUserAttentionType, NSResponder, NSScreen, NSView, NSWindow, NSWindowButton, NSWindowLevel, NSWindowSharingType, NSWindowStyleMask, - NSWindowTitleVisibility, + NSWindowTabbingMode, NSWindowTitleVisibility, }; +use super::ffi::CGSMainConnectionID; +use super::ffi::CGSSetWindowBackgroundBlurRadius; + +pub(crate) struct Window { + window: MainThreadBound>, + // We keep this around so that it doesn't get dropped until the window does. + _delegate: MainThreadBound>, +} + +impl Drop for Window { + fn drop(&mut self) { + self.window + .get_on_main(|window, _| autoreleasepool(|_| window.close())) + } +} + +impl Window { + pub(crate) fn new( + _window_target: &EventLoopWindowTarget, + attributes: WindowAttributes, + pl_attribs: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + let mtm = MainThreadMarker::new() + .expect("windows can only be created on the main thread on macOS"); + let (window, _delegate) = autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs))?; + Ok(Window { + window: MainThreadBound::new(window, mtm), + _delegate: MainThreadBound::new(_delegate, mtm), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WinitWindow) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&WinitWindow) -> R + Send, + ) -> R { + self.window.get_on_main(|window, _mtm| f(window)) + } +} #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId(pub usize); @@ -85,6 +122,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub disallow_hidpi: bool, pub has_shadow: bool, pub accepts_first_mouse: bool, + pub tabbing_identifier: Option, pub option_as_alt: OptionAsAlt, } @@ -101,6 +139,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, + tabbing_identifier: None, option_as_alt: Default::default(), } } @@ -108,26 +147,62 @@ impl Default for PlatformSpecificWindowBuilderAttributes { declare_class!( #[derive(Debug)] - pub(crate) struct WinitWindow { + pub struct WinitWindow { // TODO: Fix unnecessary boxing here - // SAFETY: These are initialized in WinitWindow::new, right after it is created. - shared_state: IvarDrop>>, - decorations: IvarDrop>, + shared_state: IvarDrop>, "_shared_state">, } + mod ivars; + unsafe impl ClassType for WinitWindow { #[inherits(NSResponder, NSObject)] type Super = NSWindow; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitWindow"; } unsafe impl WinitWindow { - #[sel(canBecomeMainWindow)] + #[method(initWithContentRect:styleMask:state:)] + unsafe fn init( + this: *mut Self, + frame: NSRect, + mask: NSWindowStyleMask, + state: *mut c_void, + ) -> Option> { + let this: Option<&mut Self> = unsafe { + msg_send![ + super(this), + initWithContentRect: frame, + styleMask: mask, + backing: NSBackingStoreType::NSBackingStoreBuffered, + defer: false, + ] + }; + + this.map(|this| { + // SAFETY: The pointer originally came from `Box::into_raw`. + Ivar::write(&mut this.shared_state, unsafe { + Box::from_raw(state as *mut Mutex) + }); + + // It is imperative to correct memory management that we + // disable the extra release that would otherwise happen when + // calling `clone` on the window. + this.setReleasedWhenClosed(false); + + NonNull::from(this) + }) + } + } + + unsafe impl WinitWindow { + #[method(canBecomeMainWindow)] fn can_become_main_window(&self) -> bool { trace_scope!("canBecomeMainWindow"); true } - #[sel(canBecomeKeyWindow)] + #[method(canBecomeKeyWindow)] fn can_become_key_window(&self) -> bool { trace_scope!("canBecomeKeyWindow"); true @@ -164,6 +239,8 @@ pub struct SharedState { pub(crate) resize_increments: NSSize, /// The state of the `Option` as `Alt`. pub(crate) option_as_alt: OptionAsAlt, + + decorations: bool, } impl SharedState { @@ -213,18 +290,14 @@ impl Drop for SharedStateMutexGuard<'_> { impl WinitWindow { #[allow(clippy::type_complexity)] - pub(crate) fn new( + fn new( attrs: WindowAttributes, pl_attrs: PlatformSpecificWindowBuilderAttributes, - ) -> Result<(Id, Id), RootOsError> { + ) -> Result<(Id, Id), RootOsError> { trace_scope!("WinitWindow::new"); - if !is_main_thread() { - panic!("Windows can only be created on the main thread on macOS"); - } - let this = autoreleasepool(|_| { - let screen = match attrs.fullscreen.clone().map(Into::into) { + let screen = match attrs.fullscreen.0.clone().map(Into::into) { Some(Fullscreen::Borderless(Some(monitor))) | Some(Fullscreen::Exclusive(VideoMode { monitor, .. })) => { monitor.ns_screen().or_else(NSScreen::main) @@ -292,118 +365,104 @@ impl WinitWindow { masks |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; } - let this: Option> = unsafe { + let state = SharedState { + resizable: attrs.resizable, + maximized: attrs.maximized, + decorations: attrs.decorations, + ..Default::default() + }; + + // Pass the state through FFI to the method declared on the class + let state_ptr: *mut c_void = Box::into_raw(Box::new(Mutex::new(state))).cast(); + let this: Option> = unsafe { msg_send_id![ - msg_send_id![WinitWindow::class(), alloc], + WinitWindow::alloc(), initWithContentRect: frame, styleMask: masks, - backing: NSBackingStoreType::NSBackingStoreBuffered, - defer: false, + state: state_ptr, ] }; + let this = this?; - this.map(|mut this| { - let resize_increments = match attrs - .resize_increments - .map(|i| i.to_logical::(this.scale_factor())) - { - Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { - NSSize::new(width, height) - } - _ => NSSize::new(1., 1.), - }; + let resize_increments = match attrs + .resize_increments + .map(|i| i.to_logical::(this.scale_factor())) + { + Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { + NSSize::new(width, height) + } + _ => NSSize::new(1., 1.), + }; - // Properly initialize the window's variables - // - // Ideally this should be done in an `init` method, - // but for convenience we do it here instead. - let state = SharedState { - resizable: attrs.resizable, - maximized: attrs.maximized, - resize_increments, - ..Default::default() - }; - Ivar::write(&mut this.shared_state, Box::new(Mutex::new(state))); - Ivar::write( - &mut this.decorations, - Box::new(AtomicBool::new(attrs.decorations)), - ); + this.lock_shared_state("init").resize_increments = resize_increments; - this.setReleasedWhenClosed(false); - this.setTitle(&NSString::from_str(&attrs.title)); - this.setAcceptsMouseMovedEvents(true); + this.setTitle(&NSString::from_str(&attrs.title)); + this.setAcceptsMouseMovedEvents(true); - if attrs.content_protected { - this.setSharingType(NSWindowSharingType::NSWindowSharingNone); - } + if let Some(identifier) = pl_attrs.tabbing_identifier { + this.setTabbingIdentifier(&NSString::from_str(&identifier)); + this.setTabbingMode(NSWindowTabbingMode::NSWindowTabbingModePreferred); + } - if pl_attrs.titlebar_transparent { - this.setTitlebarAppearsTransparent(true); - } - if pl_attrs.title_hidden { - this.setTitleVisibility(NSWindowTitleVisibility::Hidden); - } - if pl_attrs.titlebar_buttons_hidden { - for titlebar_button in &[ - #[allow(deprecated)] - NSWindowButton::FullScreen, - NSWindowButton::Miniaturize, - NSWindowButton::Close, - NSWindowButton::Zoom, - ] { - if let Some(button) = this.standardWindowButton(*titlebar_button) { - button.setHidden(true); - } - } - } - if pl_attrs.movable_by_window_background { - this.setMovableByWindowBackground(true); - } + if attrs.content_protected { + this.setSharingType(NSWindowSharingType::NSWindowSharingNone); + } - if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { - if let Some(button) = this.standardWindowButton(NSWindowButton::Zoom) { - button.setEnabled(false); + if pl_attrs.titlebar_transparent { + this.setTitlebarAppearsTransparent(true); + } + if pl_attrs.title_hidden { + this.setTitleVisibility(NSWindowTitleVisibility::Hidden); + } + if pl_attrs.titlebar_buttons_hidden { + for titlebar_button in &[ + #[allow(deprecated)] + NSWindowButton::FullScreen, + NSWindowButton::Miniaturize, + NSWindowButton::Close, + NSWindowButton::Zoom, + ] { + if let Some(button) = this.standardWindowButton(*titlebar_button) { + button.setHidden(true); } } + } + if pl_attrs.movable_by_window_background { + this.setMovableByWindowBackground(true); + } - if !pl_attrs.has_shadow { - this.setHasShadow(false); - } - if attrs.position.is_none() { - this.center(); + if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { + if let Some(button) = this.standardWindowButton(NSWindowButton::Zoom) { + button.setEnabled(false); } + } - this.set_option_as_alt(pl_attrs.option_as_alt); + if !pl_attrs.has_shadow { + this.setHasShadow(false); + } + if attrs.position.is_none() { + this.center(); + } - Id::into_shared(this) - }) + this.set_option_as_alt(pl_attrs.option_as_alt); + + Some(this) }) .ok_or_else(|| os_error!(OsError::CreationError("Couldn't create `NSWindow`")))?; - match attrs.parent_window { - Some(RawWindowHandle::AppKit(handle)) => { + #[cfg(feature = "rwh_06")] + match attrs.parent_window.0 { + Some(rwh_06::RawWindowHandle::AppKit(handle)) => { // SAFETY: Caller ensures the pointer is valid or NULL - let parent: Id = - match unsafe { Id::retain(handle.ns_window.cast()) } { - Some(window) => window, - None => { - // SAFETY: Caller ensures the pointer is valid or NULL - let parent_view: Id = - match unsafe { Id::retain(handle.ns_view.cast()) } { - Some(view) => view, - None => { - return Err(os_error!(OsError::CreationError( - "raw window handle should be non-empty" - ))) - } - }; - parent_view.window().ok_or_else(|| { - os_error!(OsError::CreationError( - "parent view should be installed in a window" - )) - })? - } - }; + // Unwrap is fine, since the pointer comes from `NonNull`. + let parent_view: Id = + unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); + let parent = parent_view.window().ok_or_else(|| { + os_error!(OsError::CreationError( + "parent view should be installed in a window" + )) + })?; + // SAFETY: We know that there are no parent -> child -> parent cycles since the only place in `winit` // where we allow making a window a child window is right here, just after it's been created. unsafe { parent.addChildWindow(&this, NSWindowOrderingMode::NSWindowAbove) }; @@ -437,6 +496,10 @@ impl WinitWindow { this.setBackgroundColor(&NSColor::clear()); } + if attrs.blur { + this.set_blur(attrs.blur); + } + if let Some(dim) = attrs.min_inner_size { this.set_min_inner_size(Some(dim)); } @@ -447,7 +510,7 @@ impl WinitWindow { this.set_window_level(attrs.window_level); // register for drag and drop operations. - this.registerForDraggedTypes(&NSArray::from_slice(&[ + this.registerForDraggedTypes(&NSArray::from_id_slice(&[ unsafe { NSFilenamesPboardType }.copy() ])); @@ -463,14 +526,14 @@ impl WinitWindow { } } - let delegate = WinitWindowDelegate::new(&this, attrs.fullscreen.is_some()); + let delegate = WinitWindowDelegate::new(&this, attrs.fullscreen.0.is_some()); // XXX Send `Focused(false)` right after creating the window delegate, so we won't // obscure the real focused events on the startup. delegate.queue_event(WindowEvent::Focused(false)); // Set fullscreen mode after we setup everything - this.set_fullscreen(attrs.fullscreen.map(Into::into)); + this.set_fullscreen(attrs.fullscreen.0.map(Into::into)); // Setting the window as key has to happen *after* we set the fullscreen // state, since otherwise we'll briefly see the window at normal size @@ -491,13 +554,7 @@ impl WinitWindow { Ok((this, delegate)) } - pub(super) fn retain(&self) -> Id { - // SAFETY: The pointer is valid, and the window is always `Shared` - // TODO(madsmtm): Remove the need for unsafety here - unsafe { Id::retain(self as *const Self as *mut Self).unwrap() } - } - - pub(super) fn view(&self) -> Id { + pub(super) fn view(&self) -> Id { // SAFETY: The view inside WinitWindow is always `WinitView` unsafe { Id::cast(self.contentView()) } } @@ -510,26 +567,41 @@ impl WinitWindow { SharedStateMutexGuard::new(self.shared_state.lock().unwrap(), called_from_fn) } - fn set_style_mask_sync(&self, mask: NSWindowStyleMask) { - util::set_style_mask_sync(self, mask); + fn set_style_mask(&self, mask: NSWindowStyleMask) { + self.setStyleMask(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + let _ = self.makeFirstResponder(Some(&self.contentView())); } +} +impl WinitWindow { pub fn id(&self) -> WindowId { WindowId(self as *const Self as usize) } pub fn set_title(&self, title: &str) { - util::set_title_sync(self, title); + self.setTitle(&NSString::from_str(title)) } pub fn set_transparent(&self, transparent: bool) { self.setOpaque(!transparent) } + pub fn set_blur(&self, blur: bool) { + // NOTE: in general we want to specify the blur radius, but the choice of 80 + // should be a reasonable default. + let radius = if blur { 80 } else { 0 }; + let window_number = self.windowNumber(); + unsafe { + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, radius); + } + } + pub fn set_visible(&self, visible: bool) { match visible { - true => util::make_key_and_order_front_sync(self), - false => util::order_out_sync(self), + true => self.makeKeyAndOrderFront(None), + false => self.orderOut(None), } } @@ -542,6 +614,9 @@ impl WinitWindow { AppState::queue_redraw(RootWindowId(self.id())); } + #[inline] + pub fn pre_present_notify(&self) {} + pub fn outer_position(&self) -> Result, NotSupportedError> { let frame_rect = self.frame(); let position = LogicalPosition::new( @@ -565,7 +640,7 @@ impl WinitWindow { pub fn set_outer_position(&self, position: Position) { let scale_factor = self.scale_factor(); let position = position.to_logical(scale_factor); - util::set_frame_top_left_point_sync(self, util::window_position(position)); + self.setFrameTopLeftPoint(util::window_position(position)); } #[inline] @@ -585,9 +660,11 @@ impl WinitWindow { } #[inline] - pub fn set_inner_size(&self, size: Size) { + pub fn request_inner_size(&self, size: Size) -> Option> { let scale_factor = self.scale_factor(); - util::set_content_size_sync(self, size.to_logical(scale_factor)); + let size: LogicalSize = size.to_logical(scale_factor); + self.setContentSize(NSSize::new(size.width as CGFloat, size.height as CGFloat)); + None } pub fn set_min_inner_size(&self, dimensions: Option) { @@ -694,7 +771,7 @@ impl WinitWindow { } else { mask &= !NSWindowStyleMask::NSResizableWindowMask; } - self.set_style_mask_sync(mask); + self.set_style_mask(mask); } // Otherwise, we don't change the mask until we exit fullscreen. } @@ -722,7 +799,7 @@ impl WinitWindow { // This must happen before the button's "enabled" status has been set, // hence we do it synchronously. - self.set_style_mask_sync(mask); + self.set_style_mask(mask); // We edit the button directly instead of using `NSResizableWindowMask`, // since that mask also affect the resizability of the window (which is @@ -753,9 +830,7 @@ impl WinitWindow { pub fn set_cursor_icon(&self, icon: CursorIcon) { let view = self.view(); - let mut cursor_state = view.state.cursor_state.lock().unwrap(); - cursor_state.cursor = NSCursor::from_icon(icon); - drop(cursor_state); + view.set_cursor_icon(NSCursor::from_icon(icon)); self.invalidateCursorRectsForView(&view); } @@ -777,10 +852,8 @@ impl WinitWindow { #[inline] pub fn set_cursor_visible(&self, visible: bool) { let view = self.view(); - let mut cursor_state = view.state.cursor_state.lock().unwrap(); - if visible != cursor_state.visible { - cursor_state.visible = visible; - drop(cursor_state); + let state_changed = view.set_cursor_visible(visible); + if state_changed { self.invalidateCursorRectsForView(&view); } } @@ -820,9 +893,12 @@ impl WinitWindow { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { - util::set_ignore_mouse_events_sync(self, !hittest); + self.setIgnoresMouseEvents(!hittest); Ok(()) } @@ -835,14 +911,14 @@ impl WinitWindow { NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask; let needs_temp_mask = !curr_mask.contains(required); if needs_temp_mask { - self.set_style_mask_sync(required); + self.set_style_mask(required); } let is_zoomed = self.isZoomed(); // Roll back temp styles if needs_temp_mask { - self.set_style_mask_sync(curr_mask); + self.set_style_mask(curr_mask); } is_zoomed @@ -873,7 +949,7 @@ impl WinitWindow { drop(shared_state_lock); - self.set_style_mask_sync(mask); + self.set_style_mask(mask); self.set_maximized(maximized); } @@ -902,7 +978,38 @@ impl WinitWindow { if is_zoomed == maximized { return; }; - util::set_maximized_sync(self, is_zoomed, maximized); + + let mut shared_state = self.lock_shared_state("set_maximized"); + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + shared_state.standard_frame = Some(self.frame()); + } + + shared_state.maximized = maximized; + + if shared_state.fullscreen.is_some() { + // Handle it in window_did_exit_fullscreen + return; + } + + if self + .styleMask() + .contains(NSWindowStyleMask::NSResizableWindowMask) + { + drop(shared_state); + // Just use the native zoom if resizable + self.zoom(None); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let screen = NSScreen::main().expect("no screen found"); + screen.visibleFrame() + } else { + shared_state.saved_standard_frame() + }; + drop(shared_state); + self.setFrame_display(new_rect, false); + } } #[inline] @@ -958,7 +1065,7 @@ impl WinitWindow { // The coordinate system here has its origin at bottom-left // and Y goes up screen_frame.origin.y += screen_frame.size.height; - util::set_frame_top_left_point_sync(self, screen_frame.origin); + self.setFrameTopLeftPoint(screen_frame.origin); } } @@ -1034,22 +1141,43 @@ impl WinitWindow { self.lock_shared_state("set_fullscreen").fullscreen = fullscreen.clone(); - match (&old_fullscreen, &fullscreen) { - (&None, &Some(_)) => { - util::toggle_full_screen_sync(self, old_fullscreen.is_none()); + fn toggle_fullscreen(window: &WinitWindow) { + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + window.setLevel(NSWindowLevel::Normal); + window.toggleFullScreen(None); + } + + match (old_fullscreen, fullscreen) { + (None, Some(_)) => { + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + let curr_mask = self.styleMask(); + let required = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSResizableWindowMask; + if !curr_mask.contains(required) { + self.set_style_mask(required); + self.lock_shared_state("set_fullscreen").saved_style = Some(curr_mask); + } + toggle_fullscreen(self); } - (&Some(Fullscreen::Borderless(_)), &None) => { + (Some(Fullscreen::Borderless(_)), None) => { // State is restored by `window_did_exit_fullscreen` - util::toggle_full_screen_sync(self, old_fullscreen.is_none()); + toggle_fullscreen(self); } - (&Some(Fullscreen::Exclusive(ref video_mode)), &None) => { + (Some(Fullscreen::Exclusive(ref video_mode)), None) => { unsafe { - util::restore_display_mode_sync(video_mode.monitor().native_identifier()) + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), + ffi::kCGErrorSuccess + ); }; - // Rest of the state is restored by `window_did_exit_fullscreen` - util::toggle_full_screen_sync(self, old_fullscreen.is_none()); + toggle_fullscreen(self); } - (&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => { + (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { // If we're already in fullscreen mode, calling // `CGDisplayCapture` will place the shielding window on top of // our window, which results in a black display and is not what @@ -1072,7 +1200,7 @@ impl WinitWindow { NSWindowLevel(unsafe { ffi::CGShieldingWindowLevel() } as NSInteger + 1); self.setLevel(window_level); } - (&Some(Fullscreen::Exclusive(ref video_mode)), &Some(Fullscreen::Borderless(_))) => { + (Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => { let presentation_options = self .lock_shared_state("set_fullscreen") .save_presentation_opts @@ -1084,7 +1212,11 @@ impl WinitWindow { NSApp().setPresentationOptions(presentation_options); unsafe { - util::restore_display_mode_sync(video_mode.monitor().native_identifier()) + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), + ffi::kCGErrorSuccess + ); }; // Restore the normal window level following the Borderless fullscreen @@ -1097,45 +1229,44 @@ impl WinitWindow { #[inline] pub fn set_decorations(&self, decorations: bool) { - if decorations != self.decorations.load(Ordering::Acquire) { - self.decorations.store(decorations, Ordering::Release); - - let (fullscreen, resizable) = { - let shared_state_lock = self.lock_shared_state("set_decorations"); - ( - shared_state_lock.fullscreen.is_some(), - shared_state_lock.resizable, - ) - }; + let mut shared_state_lock = self.lock_shared_state("set_decorations"); + if decorations == shared_state_lock.decorations { + return; + } - // If we're in fullscreen mode, we wait to apply decoration changes - // until we're in `window_did_exit_fullscreen`. - if fullscreen { - return; - } + shared_state_lock.decorations = decorations; - let new_mask = { - let mut new_mask = if decorations { - NSWindowStyleMask::NSClosableWindowMask - | NSWindowStyleMask::NSMiniaturizableWindowMask - | NSWindowStyleMask::NSResizableWindowMask - | NSWindowStyleMask::NSTitledWindowMask - } else { - NSWindowStyleMask::NSBorderlessWindowMask - | NSWindowStyleMask::NSResizableWindowMask - }; - if !resizable { - new_mask &= !NSWindowStyleMask::NSResizableWindowMask; - } - new_mask - }; - self.set_style_mask_sync(new_mask); + let fullscreen = shared_state_lock.fullscreen.is_some(); + let resizable = shared_state_lock.resizable; + + drop(shared_state_lock); + + // If we're in fullscreen mode, we wait to apply decoration changes + // until we're in `window_did_exit_fullscreen`. + if fullscreen { + return; } + + let new_mask = { + let mut new_mask = if decorations { + NSWindowStyleMask::NSClosableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSTitledWindowMask + } else { + NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask + }; + if !resizable { + new_mask &= !NSWindowStyleMask::NSResizableWindowMask; + } + new_mask + }; + self.set_style_mask(new_mask); } #[inline] pub fn is_decorated(&self) -> bool { - self.decorations.load(Ordering::Acquire) + self.lock_shared_state("is_decorated").decorations } #[inline] @@ -1145,7 +1276,7 @@ impl WinitWindow { WindowLevel::AlwaysOnBottom => NSWindowLevel::BELOW_NORMAL, WindowLevel::Normal => NSWindowLevel::Normal, }; - util::set_level_sync(self, level); + self.setLevel(level); } #[inline] @@ -1165,13 +1296,12 @@ impl WinitWindow { let scale_factor = self.scale_factor(); let logical_spot = spot.to_logical(scale_factor); let size = size.to_logical(scale_factor); - util::set_ime_cursor_area_sync(self, logical_spot, size); + self.view().set_ime_cursor_area(logical_spot, size); } #[inline] pub fn set_ime_allowed(&self, allowed: bool) { - // TODO(madsmtm): Remove the need for this - unsafe { Id::from_shared(self.view()) }.set_ime_allowed(allowed); + self.view().set_ime_allowed(allowed); } #[inline] @@ -1184,7 +1314,7 @@ impl WinitWindow { if !is_minimized && is_visible { NSApp().activateIgnoringOtherApps(true); - util::make_key_and_order_front_sync(self); + self.makeKeyAndOrderFront(None); } } @@ -1222,25 +1352,56 @@ impl WinitWindow { Some(monitor) } + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::AppKitHandle::empty(); + window_handle.ns_window = self as *const Self as *mut _; + window_handle.ns_view = Id::as_ptr(&self.contentView()) as *mut _; + rwh_04::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut window_handle = AppKitWindowHandle::empty(); - window_handle.ns_window = self.ns_window(); - window_handle.ns_view = self.ns_view(); - RawWindowHandle::AppKit(window_handle) + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::AppKitWindowHandle::empty(); + window_handle.ns_window = self as *const Self as *mut _; + window_handle.ns_view = Id::as_ptr(&self.contentView()) as *mut _; + rwh_05::RawWindowHandle::AppKit(window_handle) } + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()) + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = Id::as_ptr(&self.contentView()) as *mut _; + std::ptr::NonNull::new(ptr).expect("Id should never be null") + }); + Ok(rwh_06::RawWindowHandle::AppKit(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) } fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) { let current_style_mask = self.styleMask(); if on { - util::set_style_mask_sync(self, current_style_mask | mask); + self.set_style_mask(current_style_mask | mask); } else { - util::set_style_mask_sync(self, current_style_mask & (!mask)); + self.set_style_mask(current_style_mask & (!mask)); } } @@ -1278,16 +1439,6 @@ impl WinitWindow { } impl WindowExtMacOS for WinitWindow { - #[inline] - fn ns_window(&self) -> *mut c_void { - self as *const Self as *mut _ - } - - #[inline] - fn ns_view(&self) -> *mut c_void { - Id::as_ptr(&self.contentView()) as *mut _ - } - #[inline] fn simple_fullscreen(&self) -> bool { self.lock_shared_state("simple_fullscreen") @@ -1345,7 +1496,7 @@ impl WindowExtMacOS for WinitWindow { true } else { let new_mask = self.saved_style(&mut shared_state_lock); - self.set_style_mask_sync(new_mask); + self.set_style_mask(new_mask); shared_state_lock.is_simple_fullscreen = false; let save_presentation_opts = shared_state_lock.save_presentation_opts; @@ -1376,6 +1527,49 @@ impl WindowExtMacOS for WinitWindow { self.setHasShadow(has_shadow) } + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.setTabbingIdentifier(&NSString::from_str(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.tabbingIdentifier().to_string() + } + + #[inline] + fn select_next_tab(&self) { + if let Some(group) = self.tabGroup() { + group.selectNextTab(); + } + } + + #[inline] + fn select_previous_tab(&self) { + if let Some(group) = self.tabGroup() { + group.selectPreviousTab() + } + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + if let Some(group) = self.tabGroup() { + if let Some(windows) = group.tabbedWindows() { + if index < windows.len() { + group.setSelectedWindow(&windows[index]); + } + } + } + } + + #[inline] + fn num_tabs(&self) -> usize { + self.tabGroup() + .and_then(|group| group.tabbedWindows()) + .map(|windows| windows.len()) + .unwrap_or(1) + } + fn is_document_edited(&self) -> bool { self.isDocumentEdited() } @@ -1385,12 +1579,12 @@ impl WindowExtMacOS for WinitWindow { } fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { - let mut shared_state_lock = self.shared_state.lock().unwrap(); + let mut shared_state_lock = self.lock_shared_state("set_option_as_alt"); shared_state_lock.option_as_alt = option_as_alt; } fn option_as_alt(&self) -> OptionAsAlt { - let shared_state_lock = self.shared_state.lock().unwrap(); + let shared_state_lock = self.lock_shared_state("option_as_alt"); shared_state_lock.option_as_alt } } @@ -1402,7 +1596,7 @@ pub(super) fn get_ns_theme() -> Theme { return Theme::Light; } let appearance = app.effectiveAppearance(); - let name = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_slice(&[ + let name = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ NSString::from_str("NSAppearanceNameAqua"), NSString::from_str("NSAppearanceNameDarkAqua"), ])); diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index ff3bbc78fa..fb6df1b451 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -1,69 +1,80 @@ #![allow(clippy::unnecessary_cast)] +use std::cell::Cell; +use std::ptr::{self, NonNull}; -use std::ptr; - +use icrate::Foundation::{NSArray, NSObject, NSSize, NSString}; use objc2::declare::{Ivar, IvarDrop}; -use objc2::foundation::{NSArray, NSObject, NSSize, NSString}; -use objc2::rc::{autoreleasepool, Id, Shared}; -use objc2::runtime::Object; -use objc2::{class, declare_class, msg_send, msg_send_id, sel, ClassType}; +use objc2::rc::{autoreleasepool, Id}; +use objc2::runtime::AnyObject; +use objc2::{class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType}; use super::appkit::{ NSApplicationPresentationOptions, NSFilenamesPboardType, NSPasteboard, NSWindowOcclusionState, }; +use super::{ + app_state::AppState, + util, + window::{get_ns_theme, WinitWindow}, + Fullscreen, +}; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{Event, WindowEvent}, - keyboard::ModifiersState, - platform_impl::platform::{ - app_state::AppState, - event::{EventProxy, EventWrapper}, - util, - window::{get_ns_theme, WinitWindow}, - Fullscreen, - }, window::WindowId, }; +#[derive(Debug)] +pub struct State { + // This is set when WindowBuilder::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: Cell, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + previous_position: Cell>, + + // Used to prevent redundant events. + previous_scale_factor: Cell, +} + declare_class!( #[derive(Debug)] pub(crate) struct WinitWindowDelegate { - window: IvarDrop>, - - // TODO: It's possible for delegate methods to be called asynchronously, - // causing data races / `RefCell` panics. + window: IvarDrop, "_window">, - // This is set when WindowBuilder::with_fullscreen was set, - // see comments of `window_did_fail_to_enter_fullscreen` - initial_fullscreen: bool, - - // During `windowDidResize`, we use this to only send Moved if the position changed. + // TODO: It may be possible for delegate methods to be called + // asynchronously, causing data races panics? // TODO: Remove unnecessary boxing here - previous_position: IvarDrop>>, - - // Used to prevent redundant events. - previous_scale_factor: f64, + state: IvarDrop, "_state">, } + mod ivars; + unsafe impl ClassType for WinitWindowDelegate { type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitWindowDelegate"; } unsafe impl WinitWindowDelegate { - #[sel(initWithWindow:initialFullscreen:)] - fn init_with_winit( - &mut self, + #[method(initWithWindow:initialFullscreen:)] + unsafe fn init_with_winit( + this: *mut Self, window: &WinitWindow, initial_fullscreen: bool, - ) -> Option<&mut Self> { - let this: Option<&mut Self> = unsafe { msg_send![self, init] }; + ) -> Option> { + let this: Option<&mut Self> = unsafe { msg_send![super(this), init] }; this.map(|this| { let scale_factor = window.scale_factor(); Ivar::write(&mut this.window, window.retain()); - Ivar::write(&mut this.initial_fullscreen, initial_fullscreen); - Ivar::write(&mut this.previous_position, None); - Ivar::write(&mut this.previous_scale_factor, scale_factor); + Ivar::write( + &mut this.state, + Box::new(State { + initial_fullscreen: Cell::new(initial_fullscreen), + previous_position: Cell::new(None), + previous_scale_factor: Cell::new(scale_factor), + }), + ); if scale_factor != 1.0 { this.queue_static_scale_factor_changed_event(); @@ -71,7 +82,7 @@ declare_class!( this.window.setDelegate(Some(this)); // Enable theme change event - let notification_center: Id = + let notification_center: Id = unsafe { msg_send_id![class!(NSDistributedNotificationCenter), defaultCenter] }; let notification_name = NSString::from_str("AppleInterfaceThemeChangedNotification"); @@ -81,26 +92,26 @@ declare_class!( addObserver: &*this selector: sel!(effectiveAppearanceDidChange:) name: &*notification_name - object: ptr::null::() + object: ptr::null::() ] }; - this + NonNull::from(this) }) } } // NSWindowDelegate + NSDraggingDestination protocols unsafe impl WinitWindowDelegate { - #[sel(windowShouldClose:)] - fn window_should_close(&self, _: Option<&Object>) -> bool { + #[method(windowShouldClose:)] + fn window_should_close(&self, _: Option<&AnyObject>) -> bool { trace_scope!("windowShouldClose:"); self.queue_event(WindowEvent::CloseRequested); false } - #[sel(windowWillClose:)] - fn window_will_close(&self, _: Option<&Object>) { + #[method(windowWillClose:)] + fn window_will_close(&self, _: Option<&AnyObject>) { trace_scope!("windowWillClose:"); // `setDelegate:` retains the previous value and then autoreleases it autoreleasepool(|_| { @@ -111,15 +122,15 @@ declare_class!( self.queue_event(WindowEvent::Destroyed); } - #[sel(windowDidResize:)] - fn window_did_resize(&mut self, _: Option<&Object>) { + #[method(windowDidResize:)] + fn window_did_resize(&self, _: Option<&AnyObject>) { trace_scope!("windowDidResize:"); // NOTE: WindowEvent::Resized is reported in frameDidChange. self.emit_move_event(); } - #[sel(windowWillStartLiveResize:)] - fn window_will_start_live_resize(&mut self, _: Option<&Object>) { + #[method(windowWillStartLiveResize:)] + fn window_will_start_live_resize(&self, _: Option<&AnyObject>) { trace_scope!("windowWillStartLiveResize:"); let increments = self @@ -129,35 +140,35 @@ declare_class!( self.window.set_resize_increments_inner(increments); } - #[sel(windowDidEndLiveResize:)] - fn window_did_end_live_resize(&mut self, _: Option<&Object>) { + #[method(windowDidEndLiveResize:)] + fn window_did_end_live_resize(&self, _: Option<&AnyObject>) { trace_scope!("windowDidEndLiveResize:"); self.window.set_resize_increments_inner(NSSize::new(1., 1.)); } // This won't be triggered if the move was part of a resize. - #[sel(windowDidMove:)] - fn window_did_move(&mut self, _: Option<&Object>) { + #[method(windowDidMove:)] + fn window_did_move(&self, _: Option<&AnyObject>) { trace_scope!("windowDidMove:"); self.emit_move_event(); } - #[sel(windowDidChangeBackingProperties:)] - fn window_did_change_backing_properties(&mut self, _: Option<&Object>) { + #[method(windowDidChangeBackingProperties:)] + fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeBackingProperties:"); self.queue_static_scale_factor_changed_event(); } - #[sel(windowDidBecomeKey:)] - fn window_did_become_key(&self, _: Option<&Object>) { + #[method(windowDidBecomeKey:)] + fn window_did_become_key(&self, _: Option<&AnyObject>) { trace_scope!("windowDidBecomeKey:"); // TODO: center the cursor if the window had mouse grab when it // lost focus self.queue_event(WindowEvent::Focused(true)); } - #[sel(windowDidResignKey:)] - fn window_did_resign_key(&self, _: Option<&Object>) { + #[method(windowDidResignKey:)] + fn window_did_resign_key(&self, _: Option<&AnyObject>) { trace_scope!("windowDidResignKey:"); // It happens rather often, e.g. when the user is Cmd+Tabbing, that the // NSWindowDelegate will receive a didResignKey event despite no event @@ -166,31 +177,21 @@ declare_class!( // NSWindowDelegate, and as a result a tracked modifiers state can quite // easily fall out of synchrony with reality. This requires us to emit // a synthetic ModifiersChanged event when we lose focus. - - // TODO(madsmtm): Remove the need for this unsafety - let mut view = unsafe { Id::from_shared(self.window.view()) }; - - // Both update the state and emit a ModifiersChanged event. - if !view.state.modifiers.state().is_empty() { - view.state.modifiers = ModifiersState::empty().into(); - self.queue_event(WindowEvent::ModifiersChanged( - ModifiersState::empty().into(), - )); - } + self.window.view().reset_modifiers(); self.queue_event(WindowEvent::Focused(false)); } /// Invoked when the dragged image enters destination bounds or frame - #[sel(draggingEntered:)] - fn dragging_entered(&self, sender: *mut Object) -> bool { + #[method(draggingEntered:)] + fn dragging_entered(&self, sender: &NSObject) -> bool { trace_scope!("draggingEntered:"); use std::path::PathBuf; - let pb: Id = unsafe { msg_send_id![sender, draggingPasteboard] }; + let pb: Id = unsafe { msg_send_id![sender, draggingPasteboard] }; let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }); - let filenames: Id, Shared> = unsafe { Id::cast(filenames) }; + let filenames: Id> = unsafe { Id::cast(filenames) }; filenames.into_iter().for_each(|file| { let path = PathBuf::from(file.to_string()); @@ -201,22 +202,22 @@ declare_class!( } /// Invoked when the image is released - #[sel(prepareForDragOperation:)] - fn prepare_for_drag_operation(&self, _: Option<&Object>) -> bool { + #[method(prepareForDragOperation:)] + fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool { trace_scope!("prepareForDragOperation:"); true } /// Invoked after the released image has been removed from the screen - #[sel(performDragOperation:)] - fn perform_drag_operation(&self, sender: *mut Object) -> bool { + #[method(performDragOperation:)] + fn perform_drag_operation(&self, sender: &NSObject) -> bool { trace_scope!("performDragOperation:"); use std::path::PathBuf; - let pb: Id = unsafe { msg_send_id![sender, draggingPasteboard] }; + let pb: Id = unsafe { msg_send_id![sender, draggingPasteboard] }; let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }); - let filenames: Id, Shared> = unsafe { Id::cast(filenames) }; + let filenames: Id> = unsafe { Id::cast(filenames) }; filenames.into_iter().for_each(|file| { let path = PathBuf::from(file.to_string()); @@ -227,21 +228,21 @@ declare_class!( } /// Invoked when the dragging operation is complete - #[sel(concludeDragOperation:)] - fn conclude_drag_operation(&self, _: Option<&Object>) { + #[method(concludeDragOperation:)] + fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { trace_scope!("concludeDragOperation:"); } /// Invoked when the dragging operation is cancelled - #[sel(draggingExited:)] - fn dragging_exited(&self, _: Option<&Object>) { + #[method(draggingExited:)] + fn dragging_exited(&self, _sender: Option<&NSObject>) { trace_scope!("draggingExited:"); self.queue_event(WindowEvent::HoveredFileCancelled); } /// Invoked when before enter fullscreen - #[sel(windowWillEnterFullScreen:)] - fn window_will_enter_fullscreen(&self, _: Option<&Object>) { + #[method(windowWillEnterFullScreen:)] + fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowWillEnterFullScreen:"); let mut shared_state = self @@ -269,18 +270,18 @@ declare_class!( } /// Invoked when before exit fullscreen - #[sel(windowWillExitFullScreen:)] - fn window_will_exit_fullscreen(&self, _: Option<&Object>) { + #[method(windowWillExitFullScreen:)] + fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowWillExitFullScreen:"); let mut shared_state = self.window.lock_shared_state("window_will_exit_fullscreen"); shared_state.in_fullscreen_transition = true; } - #[sel(window:willUseFullScreenPresentationOptions:)] + #[method(window:willUseFullScreenPresentationOptions:)] fn window_will_use_fullscreen_presentation_options( &self, - _: Option<&Object>, + _: Option<&AnyObject>, proposed_options: NSApplicationPresentationOptions, ) -> NSApplicationPresentationOptions { trace_scope!("window:willUseFullScreenPresentationOptions:"); @@ -306,10 +307,10 @@ declare_class!( } /// Invoked when entered fullscreen - #[sel(windowDidEnterFullScreen:)] - fn window_did_enter_fullscreen(&mut self, _: Option<&Object>) { + #[method(windowDidEnterFullScreen:)] + fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidEnterFullScreen:"); - *self.initial_fullscreen = false; + self.state.initial_fullscreen.set(false); let mut shared_state = self.window.lock_shared_state("window_did_enter_fullscreen"); shared_state.in_fullscreen_transition = false; let target_fullscreen = shared_state.target_fullscreen.take(); @@ -320,8 +321,8 @@ declare_class!( } /// Invoked when exited fullscreen - #[sel(windowDidExitFullScreen:)] - fn window_did_exit_fullscreen(&self, _: Option<&Object>) { + #[method(windowDidExitFullScreen:)] + fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidExitFullScreen:"); self.window.restore_state_from_fullscreen(); @@ -350,21 +351,21 @@ declare_class!( /// due to being in the midst of handling some other animation or user gesture. /// This method indicates that there was an error, and you should clean up any /// work you may have done to prepare to enter full-screen mode. - #[sel(windowDidFailToEnterFullScreen:)] - fn window_did_fail_to_enter_fullscreen(&self, _: Option<&Object>) { + #[method(windowDidFailToEnterFullScreen:)] + fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidFailToEnterFullScreen:"); let mut shared_state = self .window .lock_shared_state("window_did_fail_to_enter_fullscreen"); shared_state.in_fullscreen_transition = false; shared_state.target_fullscreen = None; - if *self.initial_fullscreen { + if self.state.initial_fullscreen.get() { #[allow(clippy::let_unit_value)] unsafe { let _: () = msg_send![ &*self.window, performSelector: sel!(toggleFullScreen:), - withObject: ptr::null::(), + withObject: ptr::null::(), afterDelay: 0.5, ]; }; @@ -374,8 +375,8 @@ declare_class!( } // Invoked when the occlusion state of the window changes - #[sel(windowDidChangeOcclusionState:)] - fn window_did_change_occlusion_state(&self, _: Option<&Object>) { + #[method(windowDidChangeOcclusionState:)] + fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeOcclusionState:"); self.queue_event(WindowEvent::Occluded( !self @@ -386,8 +387,8 @@ declare_class!( } // Observe theme change - #[sel(effectiveAppearanceDidChange:)] - fn effective_appearance_did_change(&self, sender: Option<&Object>) { + #[method(effectiveAppearanceDidChange:)] + fn effective_appearance_did_change(&self, sender: Option<&AnyObject>) { trace_scope!("Triggered `effectiveAppearanceDidChange:`"); unsafe { msg_send![ @@ -399,8 +400,8 @@ declare_class!( } } - #[sel(effectiveAppearanceDidChangedOnMainThread:)] - fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&Object>) { + #[method(effectiveAppearanceDidChangedOnMainThread:)] + fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) { let theme = get_ns_theme(); let mut shared_state = self .window @@ -413,8 +414,8 @@ declare_class!( } } - #[sel(windowDidChangeScreen:)] - fn window_did_change_screen(&self, _: Option<&Object>) { + #[method(windowDidChangeScreen:)] + fn window_did_change_screen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeScreen:"); let is_simple_fullscreen = self .window @@ -430,45 +431,45 @@ declare_class!( ); impl WinitWindowDelegate { - pub fn new(window: &WinitWindow, initial_fullscreen: bool) -> Id { + pub fn new(window: &WinitWindow, initial_fullscreen: bool) -> Id { unsafe { msg_send_id![ - msg_send_id![Self::class(), alloc], + Self::alloc(), initWithWindow: window, initialFullscreen: initial_fullscreen, ] } } - pub(crate) fn queue_event(&self, event: WindowEvent<'static>) { + pub(crate) fn queue_event(&self, event: WindowEvent) { let event = Event::WindowEvent { window_id: WindowId(self.window.id()), event, }; - AppState::queue_event(EventWrapper::StaticEvent(event)); + AppState::queue_event(event); } - fn queue_static_scale_factor_changed_event(&mut self) { + fn queue_static_scale_factor_changed_event(&self) { let scale_factor = self.window.scale_factor(); - if scale_factor == *self.previous_scale_factor { + if scale_factor == self.state.previous_scale_factor.get() { return; }; - *self.previous_scale_factor = scale_factor; - let wrapper = EventWrapper::EventProxy(EventProxy::DpiChangedProxy { - window: self.window.clone(), - suggested_size: self.view_size(), + self.state.previous_scale_factor.set(scale_factor); + let suggested_size = self.view_size(); + AppState::queue_static_scale_factor_changed_event( + self.window.clone(), + suggested_size.to_physical(scale_factor), scale_factor, - }); - AppState::queue_event(wrapper); + ); } - fn emit_move_event(&mut self) { + fn emit_move_event(&self) { let rect = self.window.frame(); let x = rect.origin.x as f64; let y = util::bottom_left_to_top_left(rect); - if self.previous_position.as_deref() != Some(&(x, y)) { - *self.previous_position = Some(Box::new((x, y))); + if self.state.previous_position.get() != Some((x, y)) { + self.state.previous_position.set(Some((x, y))); let scale_factor = self.window.scale_factor(); let physical_pos = LogicalPosition::::from((x, y)).to_physical(scale_factor); self.queue_event(WindowEvent::Moved(physical_pos)); diff --git a/src/platform_impl/orbital/event_loop.rs b/src/platform_impl/orbital/event_loop.rs index 16cf6f4137..898f6b8b5a 100644 --- a/src/platform_impl/orbital/event_loop.rs +++ b/src/platform_impl/orbital/event_loop.rs @@ -1,114 +1,135 @@ use std::{ + cell::Cell, collections::VecDeque, + marker::PhantomData, mem, slice, sync::{mpsc, Arc, Mutex}, time::Instant, }; use orbclient::{ - ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, MoveEvent, QuitEvent, - ResizeEvent, ScrollEvent, TextInputEvent, + ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, MouseRelativeEvent, + MoveEvent, QuitEvent, ResizeEvent, ScrollEvent, TextInputEvent, }; -use raw_window_handle::{OrbitalDisplayHandle, RawDisplayHandle}; +use smol_str::SmolStr; use crate::{ + error::EventLoopError, event::{self, Ime, Modifiers, StartCause}, - event_loop::{self, ControlFlow}, + event_loop::{self, ControlFlow, DeviceEvents}, keyboard::{ - Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NativeKey, NativeKeyCode, + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, + NativeKeyCode, PhysicalKey, }, window::WindowId as RootWindowId, }; use super::{ - DeviceId, KeyEventExtra, MonitorHandle, PlatformSpecificEventLoopAttributes, RedoxSocket, - TimeSocket, WindowId, WindowProperties, + DeviceId, KeyEventExtra, MonitorHandle, OsError, PlatformSpecificEventLoopAttributes, + RedoxSocket, TimeSocket, WindowId, WindowProperties, }; -fn convert_scancode(scancode: u8) -> KeyCode { - match scancode { - orbclient::K_A => KeyCode::KeyA, - orbclient::K_B => KeyCode::KeyB, - orbclient::K_C => KeyCode::KeyC, - orbclient::K_D => KeyCode::KeyD, - orbclient::K_E => KeyCode::KeyE, - orbclient::K_F => KeyCode::KeyF, - orbclient::K_G => KeyCode::KeyG, - orbclient::K_H => KeyCode::KeyH, - orbclient::K_I => KeyCode::KeyI, - orbclient::K_J => KeyCode::KeyJ, - orbclient::K_K => KeyCode::KeyK, - orbclient::K_L => KeyCode::KeyL, - orbclient::K_M => KeyCode::KeyM, - orbclient::K_N => KeyCode::KeyN, - orbclient::K_O => KeyCode::KeyO, - orbclient::K_P => KeyCode::KeyP, - orbclient::K_Q => KeyCode::KeyQ, - orbclient::K_R => KeyCode::KeyR, - orbclient::K_S => KeyCode::KeyS, - orbclient::K_T => KeyCode::KeyT, - orbclient::K_U => KeyCode::KeyU, - orbclient::K_V => KeyCode::KeyV, - orbclient::K_W => KeyCode::KeyW, - orbclient::K_X => KeyCode::KeyX, - orbclient::K_Y => KeyCode::KeyY, - orbclient::K_Z => KeyCode::KeyZ, - orbclient::K_0 => KeyCode::Digit0, - orbclient::K_1 => KeyCode::Digit1, - orbclient::K_2 => KeyCode::Digit2, - orbclient::K_3 => KeyCode::Digit3, - orbclient::K_4 => KeyCode::Digit4, - orbclient::K_5 => KeyCode::Digit5, - orbclient::K_6 => KeyCode::Digit6, - orbclient::K_7 => KeyCode::Digit7, - orbclient::K_8 => KeyCode::Digit8, - orbclient::K_9 => KeyCode::Digit9, - - orbclient::K_TICK => KeyCode::Backquote, - orbclient::K_MINUS => KeyCode::Minus, - orbclient::K_EQUALS => KeyCode::Equal, - orbclient::K_BACKSLASH => KeyCode::Backslash, - orbclient::K_BRACE_OPEN => KeyCode::BracketLeft, - orbclient::K_BRACE_CLOSE => KeyCode::BracketRight, - orbclient::K_SEMICOLON => KeyCode::Semicolon, - orbclient::K_QUOTE => KeyCode::Quote, - orbclient::K_COMMA => KeyCode::Comma, - orbclient::K_PERIOD => KeyCode::Period, - orbclient::K_SLASH => KeyCode::Slash, - orbclient::K_BKSP => KeyCode::Backspace, - orbclient::K_SPACE => KeyCode::Space, - orbclient::K_TAB => KeyCode::Tab, - //orbclient::K_CAPS => KeyCode::CAPS, - orbclient::K_LEFT_SHIFT => KeyCode::ShiftLeft, - orbclient::K_RIGHT_SHIFT => KeyCode::ShiftRight, - orbclient::K_CTRL => KeyCode::ControlLeft, - orbclient::K_ALT => KeyCode::AltLeft, - orbclient::K_ENTER => KeyCode::Enter, - orbclient::K_ESC => KeyCode::Escape, - orbclient::K_F1 => KeyCode::F1, - orbclient::K_F2 => KeyCode::F2, - orbclient::K_F3 => KeyCode::F3, - orbclient::K_F4 => KeyCode::F4, - orbclient::K_F5 => KeyCode::F5, - orbclient::K_F6 => KeyCode::F6, - orbclient::K_F7 => KeyCode::F7, - orbclient::K_F8 => KeyCode::F8, - orbclient::K_F9 => KeyCode::F9, - orbclient::K_F10 => KeyCode::F10, - orbclient::K_HOME => KeyCode::Home, - orbclient::K_UP => KeyCode::ArrowUp, - orbclient::K_PGUP => KeyCode::PageUp, - orbclient::K_LEFT => KeyCode::ArrowLeft, - orbclient::K_RIGHT => KeyCode::ArrowRight, - orbclient::K_END => KeyCode::End, - orbclient::K_DOWN => KeyCode::ArrowDown, - orbclient::K_PGDN => KeyCode::PageDown, - orbclient::K_DEL => KeyCode::Delete, - orbclient::K_F11 => KeyCode::F11, - orbclient::K_F12 => KeyCode::F12, - - _ => KeyCode::Unidentified(NativeKeyCode::Unidentified), - } +fn convert_scancode(scancode: u8) -> (PhysicalKey, Option) { + // Key constants from https://docs.rs/orbclient/latest/orbclient/event/index.html + let (key_code, named_key_opt) = match scancode { + orbclient::K_A => (KeyCode::KeyA, None), + orbclient::K_B => (KeyCode::KeyB, None), + orbclient::K_C => (KeyCode::KeyC, None), + orbclient::K_D => (KeyCode::KeyD, None), + orbclient::K_E => (KeyCode::KeyE, None), + orbclient::K_F => (KeyCode::KeyF, None), + orbclient::K_G => (KeyCode::KeyG, None), + orbclient::K_H => (KeyCode::KeyH, None), + orbclient::K_I => (KeyCode::KeyI, None), + orbclient::K_J => (KeyCode::KeyJ, None), + orbclient::K_K => (KeyCode::KeyK, None), + orbclient::K_L => (KeyCode::KeyL, None), + orbclient::K_M => (KeyCode::KeyM, None), + orbclient::K_N => (KeyCode::KeyN, None), + orbclient::K_O => (KeyCode::KeyO, None), + orbclient::K_P => (KeyCode::KeyP, None), + orbclient::K_Q => (KeyCode::KeyQ, None), + orbclient::K_R => (KeyCode::KeyR, None), + orbclient::K_S => (KeyCode::KeyS, None), + orbclient::K_T => (KeyCode::KeyT, None), + orbclient::K_U => (KeyCode::KeyU, None), + orbclient::K_V => (KeyCode::KeyV, None), + orbclient::K_W => (KeyCode::KeyW, None), + orbclient::K_X => (KeyCode::KeyX, None), + orbclient::K_Y => (KeyCode::KeyY, None), + orbclient::K_Z => (KeyCode::KeyZ, None), + orbclient::K_0 => (KeyCode::Digit0, None), + orbclient::K_1 => (KeyCode::Digit1, None), + orbclient::K_2 => (KeyCode::Digit2, None), + orbclient::K_3 => (KeyCode::Digit3, None), + orbclient::K_4 => (KeyCode::Digit4, None), + orbclient::K_5 => (KeyCode::Digit5, None), + orbclient::K_6 => (KeyCode::Digit6, None), + orbclient::K_7 => (KeyCode::Digit7, None), + orbclient::K_8 => (KeyCode::Digit8, None), + orbclient::K_9 => (KeyCode::Digit9, None), + + orbclient::K_ALT => (KeyCode::AltLeft, Some(NamedKey::Alt)), + orbclient::K_ALT_GR => (KeyCode::AltRight, Some(NamedKey::AltGraph)), + orbclient::K_BACKSLASH => (KeyCode::Backslash, None), + orbclient::K_BKSP => (KeyCode::Backspace, Some(NamedKey::Backspace)), + orbclient::K_BRACE_CLOSE => (KeyCode::BracketRight, None), + orbclient::K_BRACE_OPEN => (KeyCode::BracketLeft, None), + orbclient::K_CAPS => (KeyCode::CapsLock, Some(NamedKey::CapsLock)), + orbclient::K_COMMA => (KeyCode::Comma, None), + orbclient::K_CTRL => (KeyCode::ControlLeft, Some(NamedKey::Control)), + orbclient::K_DEL => (KeyCode::Delete, Some(NamedKey::Delete)), + orbclient::K_DOWN => (KeyCode::ArrowDown, Some(NamedKey::ArrowDown)), + orbclient::K_END => (KeyCode::End, Some(NamedKey::End)), + orbclient::K_ENTER => (KeyCode::Enter, Some(NamedKey::Enter)), + orbclient::K_EQUALS => (KeyCode::Equal, None), + orbclient::K_ESC => (KeyCode::Escape, Some(NamedKey::Escape)), + orbclient::K_F1 => (KeyCode::F1, Some(NamedKey::F1)), + orbclient::K_F2 => (KeyCode::F2, Some(NamedKey::F2)), + orbclient::K_F3 => (KeyCode::F3, Some(NamedKey::F3)), + orbclient::K_F4 => (KeyCode::F4, Some(NamedKey::F4)), + orbclient::K_F5 => (KeyCode::F5, Some(NamedKey::F5)), + orbclient::K_F6 => (KeyCode::F6, Some(NamedKey::F6)), + orbclient::K_F7 => (KeyCode::F7, Some(NamedKey::F7)), + orbclient::K_F8 => (KeyCode::F8, Some(NamedKey::F8)), + orbclient::K_F9 => (KeyCode::F9, Some(NamedKey::F9)), + orbclient::K_F10 => (KeyCode::F10, Some(NamedKey::F10)), + orbclient::K_F11 => (KeyCode::F11, Some(NamedKey::F11)), + orbclient::K_F12 => (KeyCode::F12, Some(NamedKey::F12)), + orbclient::K_HOME => (KeyCode::Home, Some(NamedKey::Home)), + orbclient::K_LEFT => (KeyCode::ArrowLeft, Some(NamedKey::ArrowLeft)), + orbclient::K_LEFT_SHIFT => (KeyCode::ShiftLeft, Some(NamedKey::Shift)), + orbclient::K_MINUS => (KeyCode::Minus, None), + orbclient::K_NUM_0 => (KeyCode::Numpad0, None), + orbclient::K_NUM_1 => (KeyCode::Numpad1, None), + orbclient::K_NUM_2 => (KeyCode::Numpad2, None), + orbclient::K_NUM_3 => (KeyCode::Numpad3, None), + orbclient::K_NUM_4 => (KeyCode::Numpad4, None), + orbclient::K_NUM_5 => (KeyCode::Numpad5, None), + orbclient::K_NUM_6 => (KeyCode::Numpad6, None), + orbclient::K_NUM_7 => (KeyCode::Numpad7, None), + orbclient::K_NUM_8 => (KeyCode::Numpad8, None), + orbclient::K_NUM_9 => (KeyCode::Numpad9, None), + orbclient::K_PERIOD => (KeyCode::Period, None), + orbclient::K_PGDN => (KeyCode::PageDown, Some(NamedKey::PageDown)), + orbclient::K_PGUP => (KeyCode::PageUp, Some(NamedKey::PageUp)), + orbclient::K_QUOTE => (KeyCode::Quote, None), + orbclient::K_RIGHT => (KeyCode::ArrowRight, Some(NamedKey::ArrowRight)), + orbclient::K_RIGHT_SHIFT => (KeyCode::ShiftRight, Some(NamedKey::Shift)), + orbclient::K_SEMICOLON => (KeyCode::Semicolon, None), + orbclient::K_SLASH => (KeyCode::Slash, None), + orbclient::K_SPACE => (KeyCode::Space, Some(NamedKey::Space)), + orbclient::K_SUPER => (KeyCode::SuperLeft, Some(NamedKey::Super)), + orbclient::K_TAB => (KeyCode::Tab, Some(NamedKey::Tab)), + orbclient::K_TICK => (KeyCode::Backquote, None), + orbclient::K_UP => (KeyCode::ArrowUp, Some(NamedKey::ArrowUp)), + orbclient::K_VOLUME_DOWN => (KeyCode::AudioVolumeDown, Some(NamedKey::AudioVolumeDown)), + orbclient::K_VOLUME_TOGGLE => (KeyCode::AudioVolumeMute, Some(NamedKey::AudioVolumeMute)), + orbclient::K_VOLUME_UP => (KeyCode::AudioVolumeUp, Some(NamedKey::AudioVolumeUp)), + + _ => return (PhysicalKey::Unidentified(NativeKeyCode::Unidentified), None), + }; + (PhysicalKey::Code(key_code), named_key_opt) } fn element_state(pressed: bool) -> event::ElementState { @@ -150,7 +171,28 @@ struct EventState { } impl EventState { - fn key(&mut self, code: KeyCode, pressed: bool) { + fn character_all_modifiers(&self, character: char) -> char { + // Modify character if Ctrl is pressed + #[allow(clippy::collapsible_if)] + if self.keyboard.contains(KeyboardModifierState::LCTRL) + || self.keyboard.contains(KeyboardModifierState::RCTRL) + { + if character.is_ascii_lowercase() { + return ((character as u8 - b'a') + 1) as char; + } + //TODO: more control key variants? + } + + // Return character as-is if no special handling required + character + } + + fn key(&mut self, key: PhysicalKey, pressed: bool) { + let code = match key { + PhysicalKey::Code(code) => code, + _ => return, + }; + match code { KeyCode::ShiftLeft => self.keyboard.set(KeyboardModifierState::LSHIFT, pressed), KeyCode::ShiftRight => self.keyboard.set(KeyboardModifierState::RSHIFT, pressed), @@ -266,15 +308,25 @@ impl EventState { pub struct EventLoop { windows: Vec<(Arc, EventState)>, window_target: event_loop::EventLoopWindowTarget, + user_events_sender: mpsc::Sender, + user_events_receiver: mpsc::Receiver, } impl EventLoop { - pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { let (user_events_sender, user_events_receiver) = mpsc::channel(); - let event_socket = Arc::new(RedoxSocket::event().unwrap()); + let event_socket = Arc::new( + RedoxSocket::event() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); - let wake_socket = Arc::new(TimeSocket::open().unwrap()); + let wake_socket = Arc::new( + TimeSocket::open() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); event_socket .write(&syscall::Event { @@ -282,32 +334,27 @@ impl EventLoop { flags: syscall::EventFlags::EVENT_READ, data: wake_socket.0.fd, }) - .unwrap(); + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?; - Self { + Ok(Self { windows: Vec::new(), window_target: event_loop::EventLoopWindowTarget { p: EventLoopWindowTarget { - user_events_sender, - user_events_receiver, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), creates: Mutex::new(VecDeque::new()), redraws: Arc::new(Mutex::new(VecDeque::new())), destroys: Arc::new(Mutex::new(VecDeque::new())), event_socket, wake_socket, + p: PhantomData, }, - _marker: std::marker::PhantomData, + _marker: PhantomData, }, - } - } - - pub fn run(mut self, event_handler: F) -> ! - where - F: 'static - + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), - { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); + user_events_sender, + user_events_receiver, + }) } fn process_event( @@ -316,43 +363,79 @@ impl EventLoop { event_state: &mut EventState, mut event_handler: F, ) where - F: FnMut(event::Event<'_, T>), + F: FnMut(event::Event), { match event_option { EventOption::Key(KeyEvent { - character: _, + character, scancode, pressed, }) => { - if scancode != 0 { - let code = convert_scancode(scancode); - let modifiers_before = event_state.keyboard; - event_state.key(code, pressed); - event_handler(event::Event::WindowEvent { - window_id: RootWindowId(window_id), - event: event::WindowEvent::KeyboardInput { - device_id: event::DeviceId(DeviceId), - event: event::KeyEvent { - logical_key: Key::Unidentified(NativeKey::Unidentified), - physical_key: code, - location: KeyLocation::Standard, - state: element_state(pressed), - repeat: false, - text: None, - - platform_specific: KeyEventExtra {}, + // Convert scancode + let (physical_key, named_key_opt) = convert_scancode(scancode); + + // Get previous modifiers and update modifiers based on physical key + let modifiers_before = event_state.keyboard; + event_state.key(physical_key, pressed); + + // Default to unidentified key with no text + let mut logical_key = Key::Unidentified(NativeKey::Unidentified); + let mut key_without_modifiers = logical_key.clone(); + let mut text = None; + let mut text_with_all_modifiers = None; + + // Set key and text based on character + if character != '\0' { + let mut tmp = [0u8; 4]; + let character_str = character.encode_utf8(&mut tmp); + // The key with Shift and Caps Lock applied (but not Ctrl) + logical_key = Key::Character(character_str.into()); + // The key without Shift or Caps Lock applied + key_without_modifiers = + Key::Character(SmolStr::from_iter(character.to_lowercase())); + if pressed { + // The key with Shift and Caps Lock applied (but not Ctrl) + text = Some(character_str.into()); + // The key with Shift, Caps Lock, and Ctrl applied + let character_all_modifiers = + event_state.character_all_modifiers(character); + text_with_all_modifiers = + Some(character_all_modifiers.encode_utf8(&mut tmp).into()) + } + }; + + // Override key if a named key was found (this is to allow Enter to replace '\n') + if let Some(named_key) = named_key_opt { + logical_key = Key::Named(named_key); + key_without_modifiers = logical_key.clone(); + } + + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId), + event: event::KeyEvent { + logical_key, + physical_key, + location: KeyLocation::Standard, + state: element_state(pressed), + repeat: false, + text, + platform_specific: KeyEventExtra { + key_without_modifiers, + text_with_all_modifiers, }, - is_synthetic: false, }, - }); + is_synthetic: false, + }, + }); - // If the state of the modifiers has changed, send the event. - if modifiers_before != event_state.keyboard { - event_handler(event::Event::WindowEvent { - window_id: RootWindowId(window_id), - event: event::WindowEvent::ModifiersChanged(event_state.modifiers()), - }) - } + // If the state of the modifiers has changed, send the event. + if modifiers_before != event_state.keyboard { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::ModifiersChanged(event_state.modifiers()), + }) } } EventOption::TextInput(TextInputEvent { character }) => { @@ -374,6 +457,14 @@ impl EventLoop { }, }); } + EventOption::MouseRelative(MouseRelativeEvent { dx, dy }) => { + event_handler(event::Event::DeviceEvent { + device_id: event::DeviceId(DeviceId), + event: event::DeviceEvent::MouseMotion { + delta: (dx as f64, dy as f64), + }, + }); + } EventOption::Button(ButtonEvent { left, middle, @@ -427,7 +518,7 @@ impl EventLoop { // Acknowledge resize after event loop. event_state.resize_opt = Some((width, height)); } - //TODO: Clipboard + //TODO: Screen, Clipboard, Drop EventOption::Hover(HoverEvent { entered }) => { if entered { event_handler(event::Event::WindowEvent { @@ -451,42 +542,22 @@ impl EventLoop { } } - pub fn run_return(&mut self, mut event_handler_inner: F) -> i32 + pub fn run(mut self, mut event_handler_inner: F) -> Result<(), EventLoopError> where - F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(event::Event, &event_loop::EventLoopWindowTarget), { - // Wrapper for event handler function that prevents ExitWithCode from being unset. let mut event_handler = - move |event: event::Event<'_, T>, - window_target: &event_loop::EventLoopWindowTarget, - control_flow: &mut ControlFlow| { - if let ControlFlow::ExitWithCode(code) = control_flow { - event_handler_inner( - event, - window_target, - &mut ControlFlow::ExitWithCode(*code), - ); - } else { - event_handler_inner(event, window_target, control_flow); - } + move |event: event::Event, window_target: &event_loop::EventLoopWindowTarget| { + event_handler_inner(event, window_target); }; - let mut control_flow = ControlFlow::default(); let mut start_cause = StartCause::Init; - let code = loop { - event_handler( - event::Event::NewEvents(start_cause), - &self.window_target, - &mut control_flow, - ); + loop { + event_handler(event::Event::NewEvents(start_cause), &self.window_target); if start_cause == StartCause::Init { - event_handler( - event::Event::Resumed, - &self.window_target, - &mut control_flow, - ); + event_handler(event::Event::Resumed, &self.window_target); } // Handle window creates. @@ -511,7 +582,6 @@ impl EventLoop { event: event::WindowEvent::Resized((properties.w, properties.h).into()), }, &self.window_target, - &mut control_flow, ); // Send resize event on create to indicate first position. @@ -521,7 +591,6 @@ impl EventLoop { event: event::WindowEvent::Moved((properties.x, properties.y).into()), }, &self.window_target, - &mut control_flow, ); } @@ -536,7 +605,6 @@ impl EventLoop { event: event::WindowEvent::Destroyed, }, &self.window_target, - &mut control_flow, ); self.windows @@ -567,7 +635,7 @@ impl EventLoop { window_id, orbital_event.to_option(), event_state, - |event| event_handler(event, &self.window_target, &mut control_flow), + |event| event_handler(event, &self.window_target), ); } @@ -593,46 +661,37 @@ impl EventLoop { i += 1; } - while let Ok(event) = self.window_target.p.user_events_receiver.try_recv() { - event_handler( - event::Event::UserEvent(event), - &self.window_target, - &mut control_flow, - ); + while let Ok(event) = self.user_events_receiver.try_recv() { + event_handler(event::Event::UserEvent(event), &self.window_target); } - event_handler( - event::Event::MainEventsCleared, - &self.window_target, - &mut control_flow, - ); - // To avoid deadlocks the redraws lock is not held during event processing. while let Some(window_id) = { let mut redraws = self.window_target.p.redraws.lock().unwrap(); redraws.pop_front() } { event_handler( - event::Event::RedrawRequested(RootWindowId(window_id)), + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::RedrawRequested, + }, &self.window_target, - &mut control_flow, ); } - event_handler( - event::Event::RedrawEventsCleared, - &self.window_target, - &mut control_flow, - ); + event_handler(event::Event::AboutToWait, &self.window_target); + + if self.window_target.p.exiting() { + break; + } - let requested_resume = match control_flow { + let requested_resume = match self.window_target.p.control_flow() { ControlFlow::Poll => { start_cause = StartCause::Poll; continue; } ControlFlow::Wait => None, ControlFlow::WaitUntil(instant) => Some(instant), - ControlFlow::ExitWithCode(code) => break code, }; // Re-using wake socket caused extra wake events before because there were leftover @@ -688,15 +747,11 @@ impl EventLoop { }; } } - }; + } - event_handler( - event::Event::LoopDestroyed, - &self.window_target, - &mut control_flow, - ); + event_handler(event::Event::LoopExiting, &self.window_target); - code + Ok(()) } pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { @@ -705,7 +760,7 @@ impl EventLoop { pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { - user_events_sender: self.window_target.p.user_events_sender.clone(), + user_events_sender: self.user_events_sender.clone(), wake_socket: self.window_target.p.wake_socket.clone(), } } @@ -740,13 +795,14 @@ impl Clone for EventLoopProxy { impl Unpin for EventLoopProxy {} pub struct EventLoopWindowTarget { - pub(super) user_events_sender: mpsc::Sender, - pub(super) user_events_receiver: mpsc::Receiver, + control_flow: Cell, + exit: Cell, pub(super) creates: Mutex>>, pub(super) redraws: Arc>>, pub(super) destroys: Arc>>, pub(super) event_socket: Arc, pub(super) wake_socket: Arc, + p: PhantomData, } impl EventLoopWindowTarget { @@ -760,7 +816,38 @@ impl EventLoopWindowTarget { v } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Orbital(OrbitalDisplayHandle::empty()) + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital( + rwh_06::OrbitalDisplayHandle::new(), + )) + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true); + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() } } diff --git a/src/platform_impl/orbital/mod.rs b/src/platform_impl/orbital/mod.rs index 961f6c4e31..60d7c5b330 100644 --- a/src/platform_impl/orbital/mod.rs +++ b/src/platform_impl/orbital/mod.rs @@ -1,8 +1,15 @@ #![cfg(target_os = "redox")] +use std::fmt::{self, Display, Formatter}; use std::str; +use std::sync::Arc; + +use smol_str::SmolStr; -use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + keyboard::Key, +}; pub use self::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; mod event_loop; @@ -176,13 +183,18 @@ impl<'a> fmt::Display for WindowProperties<'a> { } } -#[derive(Default, Clone, Debug)] -pub struct OsError; +#[derive(Clone, Debug)] +pub struct OsError(Arc); + +impl OsError { + fn new(error: syscall::Error) -> Self { + Self(Arc::new(error)) + } +} -use std::fmt::{self, Display, Formatter}; impl Display for OsError { fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { - write!(fmt, "Redox OS Error") + self.0.fmt(fmt) } } @@ -252,5 +264,8 @@ impl VideoMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct KeyEventExtra {} +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub key_without_modifiers: Key, + pub text_with_all_modifiers: Option, +} diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 922c6c4757..23cc4e208c 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -3,10 +3,6 @@ use std::{ sync::{Arc, Mutex}, }; -use raw_window_handle::{ - OrbitalDisplayHandle, OrbitalWindowHandle, RawDisplayHandle, RawWindowHandle, -}; - use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, @@ -16,8 +12,8 @@ use crate::{ }; use super::{ - EventLoopWindowTarget, MonitorHandle, PlatformSpecificWindowBuilderAttributes, RedoxSocket, - TimeSocket, WindowId, WindowProperties, + EventLoopWindowTarget, MonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, + RedoxSocket, TimeSocket, WindowId, WindowProperties, }; // These values match the values uses in the `window_new` function in orbital: @@ -25,7 +21,9 @@ use super::{ const ORBITAL_FLAG_ASYNC: char = 'a'; const ORBITAL_FLAG_BACK: char = 'b'; const ORBITAL_FLAG_FRONT: char = 'f'; +const ORBITAL_FLAG_HIDDEN: char = 'h'; const ORBITAL_FLAG_BORDERLESS: char = 'l'; +const ORBITAL_FLAG_MAXIMIZED: char = 'm'; const ORBITAL_FLAG_RESIZABLE: char = 'r'; const ORBITAL_FLAG_TRANSPARENT: char = 't'; @@ -62,11 +60,15 @@ impl Window { // Async by default. let mut flag_str = ORBITAL_FLAG_ASYNC.to_string(); + if attrs.maximized { + flag_str.push(ORBITAL_FLAG_MAXIMIZED); + } + if attrs.resizable { flag_str.push(ORBITAL_FLAG_RESIZABLE); } - //TODO: maximized, fullscreen, visible + //TODO: fullscreen if attrs.transparent { flag_str.push(ORBITAL_FLAG_TRANSPARENT); @@ -76,6 +78,10 @@ impl Window { flag_str.push(ORBITAL_FLAG_BORDERLESS); } + if !attrs.visible { + flag_str.push(ORBITAL_FLAG_HIDDEN); + } + match attrs.window_level { window::WindowLevel::AlwaysOnBottom => { flag_str.push(ORBITAL_FLAG_BACK); @@ -126,6 +132,31 @@ impl Window { }) } + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + fn get_flag(&self, flag: char) -> Result { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + let properties = WindowProperties::new(path); + Ok(properties.flags.contains(flag)) + } + + fn set_flag(&self, flag: char, value: bool) -> Result<(), error::ExternalError> { + self.window_socket + .write(format!("F,{flag},{}", if value { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId { @@ -166,6 +197,9 @@ impl Window { } } + #[inline] + pub fn pre_present_notify(&self) {} + #[inline] pub fn reset_dead_keys(&self) { // TODO? @@ -209,11 +243,12 @@ impl Window { } #[inline] - pub fn set_inner_size(&self, size: Size) { + pub fn request_inner_size(&self, size: Size) -> Option> { let (w, h): (u32, u32) = size.to_physical::(self.scale_factor()).into(); self.window_socket .write(format!("S,{w},{h}").as_bytes()) .expect("failed to set size"); + None } #[inline] @@ -247,14 +282,21 @@ impl Window { } #[inline] - pub fn set_transparent(&self, _transparent: bool) {} + pub fn set_transparent(&self, transparent: bool) { + let _ = self.set_flag(ORBITAL_FLAG_TRANSPARENT, transparent); + } + + #[inline] + pub fn set_blur(&self, _blur: bool) {} #[inline] - pub fn set_visible(&self, _visibility: bool) {} + pub fn set_visible(&self, visible: bool) { + let _ = self.set_flag(ORBITAL_FLAG_HIDDEN, !visible); + } #[inline] pub fn is_visible(&self) -> Option { - None + Some(!self.get_flag(ORBITAL_FLAG_HIDDEN).unwrap_or(false)) } #[inline] @@ -266,17 +308,13 @@ impl Window { pub fn set_resize_increments(&self, _increments: Option) {} #[inline] - pub fn set_resizable(&self, _resizeable: bool) {} + pub fn set_resizable(&self, resizeable: bool) { + let _ = self.set_flag(ORBITAL_FLAG_RESIZABLE, resizeable); + } #[inline] pub fn is_resizable(&self) -> bool { - let mut buf: [u8; 4096] = [0; 4096]; - let path = self - .window_socket - .fpath(&mut buf) - .expect("failed to read properties"); - let properties = WindowProperties::new(path); - properties.flags.contains(ORBITAL_FLAG_RESIZABLE) + self.get_flag(ORBITAL_FLAG_RESIZABLE).unwrap_or(false) } #[inline] @@ -288,11 +326,13 @@ impl Window { } #[inline] - pub fn set_maximized(&self, _maximized: bool) {} + pub fn set_maximized(&self, maximized: bool) { + let _ = self.set_flag(ORBITAL_FLAG_MAXIMIZED, maximized); + } #[inline] pub fn is_maximized(&self) -> bool { - false + self.get_flag(ORBITAL_FLAG_MAXIMIZED).unwrap_or(false) } #[inline] @@ -304,21 +344,30 @@ impl Window { } #[inline] - pub fn set_decorations(&self, _decorations: bool) {} + pub fn set_decorations(&self, decorations: bool) { + let _ = self.set_flag(ORBITAL_FLAG_BORDERLESS, !decorations); + } #[inline] pub fn is_decorated(&self) -> bool { - let mut buf: [u8; 4096] = [0; 4096]; - let path = self - .window_socket - .fpath(&mut buf) - .expect("failed to read properties"); - let properties = WindowProperties::new(path); - !properties.flags.contains(ORBITAL_FLAG_BORDERLESS) + !self.get_flag(ORBITAL_FLAG_BORDERLESS).unwrap_or(false) } #[inline] - pub fn set_window_level(&self, _level: window::WindowLevel) {} + pub fn set_window_level(&self, level: window::WindowLevel) { + match level { + window::WindowLevel::AlwaysOnBottom => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, true); + } + window::WindowLevel::Normal => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, false); + let _ = self.set_flag(ORBITAL_FLAG_FRONT, false); + } + window::WindowLevel::AlwaysOnTop => { + let _ = self.set_flag(ORBITAL_FLAG_FRONT, true); + } + } + } #[inline] pub fn set_window_icon(&self, _window_icon: Option) {} @@ -349,32 +398,63 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, _: window::CursorGrabMode) -> Result<(), error::ExternalError> { - Err(error::ExternalError::NotSupported( - error::NotSupportedError::new(), - )) + pub fn set_cursor_grab( + &self, + mode: window::CursorGrabMode, + ) -> Result<(), error::ExternalError> { + let (grab, relative) = match mode { + window::CursorGrabMode::None => (false, false), + window::CursorGrabMode::Confined => (true, false), + window::CursorGrabMode::Locked => (true, true), + }; + self.window_socket + .write(format!("M,G,{}", if grab { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + self.window_socket + .write(format!("M,R,{}", if relative { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) } #[inline] - pub fn set_cursor_visible(&self, _: bool) {} + pub fn set_cursor_visible(&self, visible: bool) { + let _ = self + .window_socket + .write(format!("M,C,{}", if visible { 1 } else { 0 }).as_bytes()); + } #[inline] pub fn drag_window(&self) -> Result<(), error::ExternalError> { - Err(error::ExternalError::NotSupported( - error::NotSupportedError::new(), - )) + self.window_socket + .write(b"D") + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) } #[inline] pub fn drag_resize_window( &self, - _direction: window::ResizeDirection, + direction: window::ResizeDirection, ) -> Result<(), error::ExternalError> { - Err(error::ExternalError::NotSupported( - error::NotSupportedError::new(), - )) + let arg = match direction { + window::ResizeDirection::East => "R", + window::ResizeDirection::North => "T", + window::ResizeDirection::NorthEast => "T,R", + window::ResizeDirection::NorthWest => "T,L", + window::ResizeDirection::South => "B", + window::ResizeDirection::SouthEast => "B,R", + window::ResizeDirection::SouthWest => "B,L", + window::ResizeDirection::West => "L", + }; + self.window_socket + .write(format!("D,{}", arg).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + #[inline] pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( @@ -382,16 +462,46 @@ impl Window { )) } + #[cfg(feature = "rwh_04")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut handle = OrbitalWindowHandle::empty(); + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut handle = rwh_04::OrbitalHandle::empty(); handle.window = self.window_socket.fd as *mut _; - RawWindowHandle::Orbital(handle) + rwh_04::RawWindowHandle::Orbital(handle) } + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Orbital(OrbitalDisplayHandle::empty()) + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut handle = rwh_05::OrbitalWindowHandle::empty(); + handle.window = self.window_socket.fd as *mut _; + rwh_05::RawWindowHandle::Orbital(handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let handle = rwh_06::OrbitalWindowHandle::new({ + let window = self.window_socket.fd as *mut _; + std::ptr::NonNull::new(window).expect("orbital fd shoul never be null") + }); + Ok(rwh_06::RawWindowHandle::Orbital(handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital( + rwh_06::OrbitalDisplayHandle::new(), + )) } #[inline] @@ -414,6 +524,8 @@ impl Window { #[inline] pub fn set_theme(&self, _theme: Option) {} + + pub fn set_content_protected(&self, _protected: bool) {} } impl Drop for Window { diff --git a/src/platform_impl/web/async.rs b/src/platform_impl/web/async.rs deleted file mode 100644 index 2fc4fe39cc..0000000000 --- a/src/platform_impl/web/async.rs +++ /dev/null @@ -1,314 +0,0 @@ -use atomic_waker::AtomicWaker; -use once_cell::unsync::Lazy; -use std::future; -use std::marker::PhantomData; -use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError}; -use std::sync::{Arc, Condvar, Mutex, RwLock}; -use std::task::Poll; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{JsCast, JsValue}; - -// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. -// `value` **must** only be accessed on the main thread. -pub struct MainThreadSafe { - // We wrap this in an `Arc` to allow it to be safely cloned without accessing the value. - // The `RwLock` lets us safely drop in any thread. - // The `Option` lets us safely drop `T` only in the main thread, while letting other threads drop `None`. - value: Arc>>, - handler: fn(&RwLock>, E), - sender: AsyncSender, - // Prevent's `Send` or `Sync` to be automatically implemented. - local: PhantomData<*const ()>, -} - -impl MainThreadSafe { - thread_local! { - static MAIN_THREAD: Lazy = Lazy::new(|| { - #[wasm_bindgen] - extern "C" { - #[derive(Clone)] - type Global; - - #[wasm_bindgen(method, getter, js_name = Window)] - fn window(this: &Global) -> JsValue; - } - - let global: Global = js_sys::global().unchecked_into(); - !global.window().is_undefined() - }); - } - - #[track_caller] - fn new(value: T, handler: fn(&RwLock>, E)) -> Option { - Self::MAIN_THREAD.with(|safe| { - if !*safe.deref() { - panic!("only callable from inside the `Window`") - } - }); - - let value = Arc::new(RwLock::new(Some(value))); - - let (sender, receiver) = channel::(); - - wasm_bindgen_futures::spawn_local({ - let value = Arc::clone(&value); - async move { - while let Ok(event) = receiver.next().await { - handler(&value, event) - } - - // An error was returned because the channel was closed, which - // happens when all senders are dropped. - value.write().unwrap().take().unwrap(); - } - }); - - Some(Self { - value, - handler, - sender, - local: PhantomData, - }) - } - - pub fn send(&self, event: E) { - Self::MAIN_THREAD.with(|is_main_thread| { - if *is_main_thread.deref() { - (self.handler)(&self.value, event) - } else { - self.sender.send(event).unwrap() - } - }) - } - - fn is_main_thread(&self) -> bool { - Self::MAIN_THREAD.with(|is_main_thread| *is_main_thread.deref()) - } - - pub fn with(&self, f: impl FnOnce(&T) -> R) -> Option { - Self::MAIN_THREAD.with(|is_main_thread| { - if *is_main_thread.deref() { - Some(f(self.value.read().unwrap().as_ref().unwrap())) - } else { - None - } - }) - } - - fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> Option { - Self::MAIN_THREAD.with(|is_main_thread| { - if *is_main_thread.deref() { - Some(f(self.value.write().unwrap().as_mut().unwrap())) - } else { - None - } - }) - } -} - -impl Clone for MainThreadSafe { - fn clone(&self) -> Self { - Self { - value: self.value.clone(), - handler: self.handler, - sender: self.sender.clone(), - local: PhantomData, - } - } -} - -unsafe impl Send for MainThreadSafe {} -unsafe impl Sync for MainThreadSafe {} - -fn channel() -> (AsyncSender, AsyncReceiver) { - let (sender, receiver) = mpsc::channel(); - let sender = Arc::new(Mutex::new(sender)); - let waker = Arc::new(AtomicWaker::new()); - let closed = Arc::new(AtomicBool::new(false)); - - let sender = AsyncSender { - sender, - closed: closed.clone(), - waker: Arc::clone(&waker), - }; - let receiver = AsyncReceiver { - receiver, - closed, - waker, - }; - - (sender, receiver) -} - -struct AsyncSender { - // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't - // be accessed on the main thread, as it could block. Additionally we need - // to wrap it in an `Arc` to make it clonable on the main thread without - // having to block. - sender: Arc>>, - closed: Arc, - waker: Arc, -} - -impl AsyncSender { - pub fn send(&self, event: T) -> Result<(), SendError> { - self.sender.lock().unwrap().send(event)?; - self.waker.wake(); - - Ok(()) - } -} - -impl Clone for AsyncSender { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - waker: self.waker.clone(), - closed: self.closed.clone(), - } - } -} - -impl Drop for AsyncSender { - fn drop(&mut self) { - // If it's the last + the one held by the receiver make sure to wake it - // up and tell it that all receiver have dropped. - if Arc::strong_count(&self.closed) == 2 { - self.closed.store(true, Ordering::Relaxed); - self.waker.wake() - } - } -} - -struct AsyncReceiver { - receiver: Receiver, - closed: Arc, - waker: Arc, -} - -impl AsyncReceiver { - pub async fn next(&self) -> Result { - future::poll_fn(|cx| match self.receiver.try_recv() { - Ok(event) => Poll::Ready(Ok(event)), - Err(TryRecvError::Empty) => { - if self.closed.load(Ordering::Relaxed) { - return Poll::Ready(Err(RecvError)); - } - - self.waker.register(cx.waker()); - - match self.receiver.try_recv() { - Ok(event) => Poll::Ready(Ok(event)), - Err(TryRecvError::Empty) => { - if self.closed.load(Ordering::Relaxed) { - Poll::Ready(Err(RecvError)) - } else { - Poll::Pending - } - } - Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), - } - } - Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), - }) - .await - } -} - -pub struct Dispatcher(MainThreadSafe>); - -pub enum Closure { - Ref(Box), - RefMut(Box), -} - -impl Dispatcher { - #[track_caller] - pub fn new(value: T) -> Option { - MainThreadSafe::new(value, |value, closure| match closure { - Closure::Ref(f) => f(value.read().unwrap().as_ref().unwrap()), - Closure::RefMut(f) => f(value.write().unwrap().as_mut().unwrap()), - }) - .map(Self) - } - - pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { - if self.is_main_thread() { - self.0.with(f).unwrap() - } else { - self.0.send(Closure::Ref(Box::new(f))) - } - } - - pub fn dispatch_mut(&self, f: impl 'static + FnOnce(&mut T) + Send) { - if self.is_main_thread() { - self.0.with_mut(f).unwrap() - } else { - self.0.send(Closure::RefMut(Box::new(f))) - } - } - - pub fn queue(&self, f: impl 'static + FnOnce(&T) -> R + Send) -> R { - if self.is_main_thread() { - self.0.with(f).unwrap() - } else { - let pair = Arc::new((Mutex::new(None), Condvar::new())); - let closure = Closure::Ref(Box::new({ - let pair = pair.clone(); - move |value| { - *pair.0.lock().unwrap() = Some(f(value)); - pair.1.notify_one(); - } - })); - - self.0.send(closure); - - let mut started = pair.0.lock().unwrap(); - - while started.is_none() { - started = pair.1.wait(started).unwrap(); - } - - started.take().unwrap() - } - } -} - -impl Deref for Dispatcher { - type Target = MainThreadSafe>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -type ChannelValue = MainThreadSafe; - -pub struct Channel(ChannelValue); - -impl Channel { - pub fn new(value: T, handler: fn(&T, E)) -> Option { - MainThreadSafe::new((value, handler), |runner, event| { - let lock = runner.read().unwrap(); - let (value, handler) = lock.as_ref().unwrap(); - handler(value, event); - }) - .map(Self) - } -} - -impl Clone for Channel { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Deref for Channel { - type Target = ChannelValue; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/src/platform_impl/web/async/channel.rs b/src/platform_impl/web/async/channel.rs new file mode 100644 index 0000000000..3f9c72a156 --- /dev/null +++ b/src/platform_impl/web/async/channel.rs @@ -0,0 +1,115 @@ +use atomic_waker::AtomicWaker; +use std::future; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +// NOTE: This channel doesn't wake up when all senders or receivers are +// dropped. This is acceptable as long as it's only used in `Dispatcher`, which +// has it's own `Drop` behavior. + +pub fn channel() -> (AsyncSender, AsyncReceiver) { + let (sender, receiver) = mpsc::channel(); + let sender = Arc::new(Mutex::new(sender)); + let inner = Arc::new(Inner { + closed: AtomicBool::new(false), + waker: AtomicWaker::new(), + }); + + let sender = AsyncSender { + sender, + inner: Arc::clone(&inner), + }; + let receiver = AsyncReceiver { + receiver: Rc::new(receiver), + inner, + }; + + (sender, receiver) +} + +pub struct AsyncSender { + // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't + // be accessed on the main thread, as it could block. Additionally we need + // to wrap it in an `Arc` to make it clonable on the main thread without + // having to block. + sender: Arc>>, + inner: Arc, +} + +impl AsyncSender { + pub fn send(&self, event: T) -> Result<(), SendError> { + self.sender.lock().unwrap().send(event)?; + self.inner.waker.wake(); + + Ok(()) + } + + pub fn close(&self) { + self.inner.closed.store(true, Ordering::Relaxed); + self.inner.waker.wake() + } +} + +impl Clone for AsyncSender { + fn clone(&self) -> Self { + Self { + sender: Arc::clone(&self.sender), + inner: Arc::clone(&self.inner), + } + } +} + +pub struct AsyncReceiver { + receiver: Rc>, + inner: Arc, +} + +impl AsyncReceiver { + pub async fn next(&self) -> Result { + future::poll_fn(|cx| match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + self.inner.waker.register(cx.waker()); + + match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.inner.closed.load(Ordering::Relaxed) { + Poll::Ready(Err(RecvError)) + } else { + Poll::Pending + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + }) + .await + } + + pub fn try_recv(&self) -> Result, RecvError> { + match self.receiver.try_recv() { + Ok(value) => Ok(Some(value)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Disconnected) => Err(RecvError), + } + } +} + +impl Clone for AsyncReceiver { + fn clone(&self) -> Self { + Self { + receiver: Rc::clone(&self.receiver), + inner: Arc::clone(&self.inner), + } + } +} + +struct Inner { + closed: AtomicBool, + waker: AtomicWaker, +} diff --git a/src/platform_impl/web/async/dispatcher.rs b/src/platform_impl/web/async/dispatcher.rs new file mode 100644 index 0000000000..daa7702558 --- /dev/null +++ b/src/platform_impl/web/async/dispatcher.rs @@ -0,0 +1,113 @@ +use super::{channel, AsyncReceiver, AsyncSender, Wrapper}; +use std::{ + cell::Ref, + sync::{Arc, Condvar, Mutex}, +}; + +pub struct Dispatcher(Wrapper>, Closure>); + +struct Closure(Box); + +impl Dispatcher { + #[track_caller] + pub fn new(value: T) -> Option<(Self, DispatchRunner)> { + let (sender, receiver) = channel::>(); + + Wrapper::new( + value, + |value, Closure(closure)| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + }, + { + let receiver = receiver.clone(); + move |value| async move { + while let Ok(Closure(closure)) = receiver.next().await { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + } + } + }, + sender, + |sender, closure| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + sender.send(closure).unwrap() + }, + ) + .map(|wrapper| (Self(wrapper.clone()), DispatchRunner { wrapper, receiver })) + } + + pub fn value(&self) -> Option> { + self.0.value() + } + + pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { + if let Some(value) = self.0.value() { + f(&value) + } else { + self.0.send(Closure(Box::new(f))) + } + } + + pub fn queue(&self, f: impl FnOnce(&T) -> R + Send) -> R { + if let Some(value) = self.0.value() { + f(&value) + } else { + let pair = Arc::new((Mutex::new(None), Condvar::new())); + let closure = Box::new({ + let pair = pair.clone(); + move |value: &T| { + *pair.0.lock().unwrap() = Some(f(value)); + pair.1.notify_one(); + } + }) as Box; + // SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is + // safe because this function won't return until `f` has finished executing. See + // `Self::new()`. + let closure = Closure(unsafe { std::mem::transmute(closure) }); + + self.0.send(closure); + + let mut started = pair.0.lock().unwrap(); + + while started.is_none() { + started = pair.1.wait(started).unwrap(); + } + + started.take().unwrap() + } + } +} + +impl Drop for Dispatcher { + fn drop(&mut self) { + self.0.with_sender_data(|sender| sender.close()) + } +} + +pub struct DispatchRunner { + wrapper: Wrapper>, Closure>, + receiver: AsyncReceiver>, +} + +impl DispatchRunner { + pub fn run(&self) { + while let Some(Closure(closure)) = self + .receiver + .try_recv() + .expect("should only be closed when `Dispatcher` is dropped") + { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure( + &self + .wrapper + .value() + .expect("don't call this outside the main thread"), + ) + } + } +} diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs new file mode 100644 index 0000000000..f1317f6e39 --- /dev/null +++ b/src/platform_impl/web/async/mod.rs @@ -0,0 +1,9 @@ +mod channel; +mod dispatcher; +mod waker; +mod wrapper; + +use self::channel::{channel, AsyncReceiver, AsyncSender}; +pub use self::dispatcher::{DispatchRunner, Dispatcher}; +pub use self::waker::{Waker, WakerSpawner}; +use self::wrapper::Wrapper; diff --git a/src/platform_impl/web/async/waker.rs b/src/platform_impl/web/async/waker.rs new file mode 100644 index 0000000000..2c27c474c9 --- /dev/null +++ b/src/platform_impl/web/async/waker.rs @@ -0,0 +1,123 @@ +use super::Wrapper; +use atomic_waker::AtomicWaker; +use std::future; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::task::Poll; + +pub struct WakerSpawner(Wrapper, Sender, usize>); + +pub struct Waker(Wrapper, Sender, usize>); + +struct Handler { + value: T, + handler: fn(&T, usize), +} + +#[derive(Clone)] +struct Sender(Arc); + +impl WakerSpawner { + #[track_caller] + pub fn new(value: T, handler: fn(&T, usize)) -> Option { + let inner = Arc::new(Inner { + counter: AtomicUsize::new(0), + waker: AtomicWaker::new(), + closed: AtomicBool::new(false), + }); + + let handler = Handler { value, handler }; + + let sender = Sender(Arc::clone(&inner)); + + let wrapper = Wrapper::new( + handler, + |handler, count| { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count); + }, + { + let inner = Arc::clone(&inner); + + move |handler| async move { + while let Some(count) = future::poll_fn(|cx| { + let count = inner.counter.swap(0, Ordering::Relaxed); + + if count > 0 { + Poll::Ready(Some(count)) + } else { + inner.waker.register(cx.waker()); + + let count = inner.counter.swap(0, Ordering::Relaxed); + + if count > 0 { + Poll::Ready(Some(count)) + } else { + if inner.closed.load(Ordering::Relaxed) { + return Poll::Ready(None); + } + + Poll::Pending + } + } + }) + .await + { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count); + } + } + }, + sender, + |inner, _| { + inner.0.counter.fetch_add(1, Ordering::Relaxed); + inner.0.waker.wake(); + }, + )?; + + Some(Self(wrapper)) + } + + pub fn waker(&self) -> Waker { + Waker(self.0.clone()) + } + + pub fn fetch(&self) -> usize { + debug_assert!( + self.0.is_main_thread(), + "this should only be called from the main thread" + ); + + self.0 + .with_sender_data(|inner| inner.0.counter.swap(0, Ordering::Relaxed)) + } +} + +impl Drop for WakerSpawner { + fn drop(&mut self) { + self.0.with_sender_data(|inner| { + inner.0.closed.store(true, Ordering::Relaxed); + inner.0.waker.wake(); + }); + } +} + +impl Waker { + pub fn wake(&self) { + self.0.send(1) + } +} + +impl Clone for Waker { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct Inner { + counter: AtomicUsize, + waker: AtomicWaker, + closed: AtomicBool, +} diff --git a/src/platform_impl/web/async/wrapper.rs b/src/platform_impl/web/async/wrapper.rs new file mode 100644 index 0000000000..22088ef1fc --- /dev/null +++ b/src/platform_impl/web/async/wrapper.rs @@ -0,0 +1,131 @@ +use std::cell::{Ref, RefCell}; +use std::future::Future; +use std::marker::PhantomData; +use std::sync::Arc; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. +// `value` **must** only be accessed on the main thread. +pub struct Wrapper { + value: Value, + handler: fn(&RefCell>, E), + sender_data: S, + sender_handler: fn(&S, E), +} + +struct Value { + // SAFETY: + // This value must not be accessed if not on the main thread. + // + // - We wrap this in an `Arc` to allow it to be safely cloned without + // accessing the value. + // - The `RefCell` lets us mutably access in the main thread but is safe to + // drop in any thread because it has no `Drop` behavior. + // - The `Option` lets us safely drop `T` only in the main thread. + value: Arc>>, + // Prevent's `Send` or `Sync` to be automatically implemented. + local: PhantomData<*const ()>, +} + +// SAFETY: See `Self::value`. +unsafe impl Send for Value {} +// SAFETY: See `Self::value`. +unsafe impl Sync for Value {} + +impl Wrapper { + thread_local! { + static MAIN_THREAD: bool = { + #[wasm_bindgen] + extern "C" { + #[derive(Clone)] + type Global; + + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + } + + let global: Global = js_sys::global().unchecked_into(); + !global.window().is_undefined() + }; + } + + #[track_caller] + pub fn new>( + value: V, + handler: fn(&RefCell>, E), + receiver: impl 'static + FnOnce(Arc>>) -> R, + sender_data: S, + sender_handler: fn(&S, E), + ) -> Option { + Self::MAIN_THREAD.with(|safe| { + if !safe { + panic!("only callable from inside the `Window`") + } + }); + + let value = Arc::new(RefCell::new(Some(value))); + + wasm_bindgen_futures::spawn_local({ + let value = Arc::clone(&value); + async move { + receiver(Arc::clone(&value)).await; + drop(value.borrow_mut().take().unwrap()); + } + }); + + Some(Self { + value: Value { + value, + local: PhantomData, + }, + handler, + sender_data, + sender_handler, + }) + } + + pub fn send(&self, event: E) { + Self::MAIN_THREAD.with(|is_main_thread| { + if *is_main_thread { + (self.handler)(&self.value.value, event) + } else { + (self.sender_handler)(&self.sender_data, event) + } + }) + } + + pub fn is_main_thread(&self) -> bool { + Self::MAIN_THREAD.with(|is_main_thread| *is_main_thread) + } + + pub fn value(&self) -> Option> { + Self::MAIN_THREAD.with(|is_main_thread| { + if *is_main_thread { + Some(Ref::map(self.value.value.borrow(), |value| { + value.as_ref().unwrap() + })) + } else { + None + } + }) + } + + pub fn with_sender_data(&self, f: impl FnOnce(&S) -> T) -> T { + f(&self.sender_data) + } +} + +impl Clone for Wrapper { + fn clone(&self) -> Self { + Self { + value: Value { + value: self.value.value.clone(), + local: PhantomData, + }, + handler: self.handler, + sender_data: self.sender_data.clone(), + sender_handler: self.sender_handler, + } + } +} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 1dcee87840..d0b7f5d37b 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,39 +1,70 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{self, Receiver, Sender}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::EventLoopWindowTarget as RootEventLoopWindowTarget; + +use super::{backend, device, window}; + mod proxy; pub(crate) mod runner; mod state; mod window_target; -pub use self::proxy::EventLoopProxy; -pub use self::window_target::EventLoopWindowTarget; - -use super::{backend, device, window}; -use crate::event::Event; -use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; - -use std::marker::PhantomData; +pub use proxy::EventLoopProxy; +pub use window_target::EventLoopWindowTarget; pub struct EventLoop { elw: RootEventLoopWindowTarget, + user_event_sender: Sender, + user_event_receiver: Receiver, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct PlatformSpecificEventLoopAttributes {} impl EventLoop { - pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { - EventLoop { - elw: RootEventLoopWindowTarget { - p: EventLoopWindowTarget::new(), - _marker: PhantomData, - }, - } + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { + let (user_event_sender, user_event_receiver) = mpsc::channel(); + let elw = RootEventLoopWindowTarget { + p: EventLoopWindowTarget::new(), + _marker: PhantomData, + }; + Ok(EventLoop { + elw, + user_event_sender, + user_event_receiver, + }) } - pub fn run(self, event_handler: F) -> ! + pub fn run(self, mut event_handler: F) -> ! where - F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + F: FnMut(Event, &RootEventLoopWindowTarget), { - self.spawn_inner(event_handler, false); + let target = RootEventLoopWindowTarget { + p: self.elw.p.clone(), + _marker: PhantomData, + }; + + // SAFETY: Don't use `move` to make sure we leak the `event_handler` and `target`. + let handler: Box)> = Box::new(|event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }); + // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe + // because this function will never return and all resources not cleaned up by the point we + // `throw` will leak, making this actually `'static`. + let handler = unsafe { std::mem::transmute(handler) }; + self.elw.p.run(handler, false); // 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 '!' @@ -44,16 +75,9 @@ impl EventLoop { unreachable!(); } - pub fn spawn(self, event_handler: F) - where - F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), - { - self.spawn_inner(event_handler, true); - } - - fn spawn_inner(self, mut event_handler: F, event_loop_recreation: bool) + pub fn spawn(self, mut event_handler: F) where - F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + F: 'static + FnMut(Event, &RootEventLoopWindowTarget), { let target = RootEventLoopWindowTarget { p: self.elw.p.clone(), @@ -61,13 +85,24 @@ impl EventLoop { }; self.elw.p.run( - Box::new(move |event, flow| event_handler(event, &target, flow)), - event_loop_recreation, + Box::new(move |event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }), + true, ); } pub fn create_proxy(&self) -> EventLoopProxy { - self.elw.p.proxy() + EventLoopProxy::new(self.elw.p.waker(), self.user_event_sender.clone()) } pub fn window_target(&self) -> &RootEventLoopWindowTarget { diff --git a/src/platform_impl/web/event_loop/proxy.rs b/src/platform_impl/web/event_loop/proxy.rs index 7094daaf67..691efa32b8 100644 --- a/src/platform_impl/web/event_loop/proxy.rs +++ b/src/platform_impl/web/event_loop/proxy.rs @@ -1,24 +1,25 @@ -use super::runner; -use crate::event::Event; +use std::rc::Weak; +use std::sync::mpsc::{SendError, Sender}; + +use super::runner::Execution; use crate::event_loop::EventLoopClosed; -use crate::platform_impl::platform::r#async::Channel; +use crate::platform_impl::platform::r#async::Waker; pub struct EventLoopProxy { - runner: Channel, T>, + runner: Waker>, + sender: Sender, } impl EventLoopProxy { - pub fn new(runner: runner::Shared) -> Self { - Self { - runner: Channel::new(runner, |runner, event| { - runner.send_event(Event::UserEvent(event)) - }) - .unwrap(), - } + pub fn new(runner: Waker>, sender: Sender) -> Self { + Self { runner, sender } } pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.runner.send(event); + self.sender + .send(event) + .map_err(|SendError(event)| EventLoopClosed(event))?; + self.runner.wake(); Ok(()) } } @@ -27,6 +28,7 @@ impl Clone for EventLoopProxy { fn clone(&self) -> Self { Self { runner: self.runner.clone(), + sender: self.sender.clone(), } } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index a47dc8c523..9db208fb87 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -3,29 +3,31 @@ use super::{backend, state::State}; use crate::dpi::PhysicalSize; use crate::event::{ DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, + WindowEvent, }; use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform::web::PollStrategy; use crate::platform_impl::platform::backend::EventListenerHandle; +use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner}; +use crate::platform_impl::platform::window::Inner; use crate::window::WindowId; -use std::sync::atomic::Ordering; use std::{ cell::{Cell, RefCell}, - clone::Clone, collections::{HashSet, VecDeque}, iter, ops::Deref, rc::{Rc, Weak}, }; use wasm_bindgen::prelude::Closure; -use web_sys::{KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; use web_time::{Duration, Instant}; -pub struct Shared(Rc>); +pub struct Shared(Rc); -pub(super) type EventHandler = dyn FnMut(Event<'_, T>, &mut ControlFlow); +pub(super) type EventHandler = dyn FnMut(Event<()>); -impl Clone for Shared { +impl Clone for Shared { fn clone(&self) -> Self { Shared(self.0.clone()) } @@ -33,13 +35,26 @@ impl Clone for Shared { type OnEventHandle = RefCell>>; -pub struct Execution { - runner: RefCell>, +pub struct Execution { + proxy_spawner: WakerSpawner>, + control_flow: Cell, + poll_strategy: Cell, + exit: Cell, + runner: RefCell, + suspended: Cell, event_loop_recreation: Cell, - events: RefCell>>, + events: RefCell>, id: RefCell, window: web_sys::Window, - all_canvases: RefCell>)>>, + document: Document, + #[allow(clippy::type_complexity)] + all_canvases: RefCell< + Vec<( + WindowId, + Weak>, + DispatchRunner, + )>, + >, redraw_pending: RefCell>, destroy_pending: RefCell>, page_transition_event_handle: RefCell>, @@ -50,21 +65,23 @@ pub struct Execution { on_mouse_release: OnEventHandle, on_key_press: OnEventHandle, on_key_release: OnEventHandle, + on_visibility_change: OnEventHandle, + on_touch_end: OnEventHandle, } -enum RunnerEnum { +enum RunnerEnum { /// The `EventLoop` is created but not being run. Pending, /// The `EventLoop` is being run. - Running(Runner), + Running(Runner), /// The `EventLoop` is exited after being started with `EventLoop::run`. Since /// `EventLoop::run` takes ownership of the `EventLoop`, we can be certain /// that this event loop will never be run again. Destroyed, } -impl RunnerEnum { - fn maybe_runner(&self) -> Option<&Runner> { +impl RunnerEnum { + fn maybe_runner(&self) -> Option<&Runner> { match self { RunnerEnum::Running(runner) => Some(runner), _ => None, @@ -72,13 +89,13 @@ impl RunnerEnum { } } -struct Runner { +struct Runner { state: State, - event_handler: Box>, + event_handler: Box, } -impl Runner { - pub fn new(event_handler: Box>) -> Self { +impl Runner { + pub fn new(event_handler: Box) -> Self { Runner { state: State::Init, event_handler, @@ -103,16 +120,9 @@ impl Runner { }) } - fn handle_single_event( - &mut self, - runner: &Shared, - event: impl Into>, - control: &mut ControlFlow, - ) { - let is_closed = matches!(*control, ControlFlow::ExitWithCode(_)); - + fn handle_single_event(&mut self, runner: &Shared, event: impl Into) { match event.into() { - EventWrapper::Event(event) => (self.event_handler)(event, control), + EventWrapper::Event(event) => (self.event_handler)(event), EventWrapper::ScaleChange { canvas, size, @@ -121,41 +131,57 @@ impl Runner { if let Some(canvas) = canvas.upgrade() { canvas.borrow().handle_scale_change( runner, - |event| (self.event_handler)(event, control), + |event| (self.event_handler)(event), size, scale, ) } } } - - // Maintain closed state, even if the callback changes it - if is_closed { - *control = ControlFlow::Exit; - } } } -impl Shared { +impl Shared { pub fn new() -> Self { - Shared(Rc::new(Execution { - runner: RefCell::new(RunnerEnum::Pending), - event_loop_recreation: Cell::new(false), - events: RefCell::new(VecDeque::new()), - #[allow(clippy::disallowed_methods)] - window: web_sys::window().expect("only callable from inside the `Window`"), - id: RefCell::new(0), - all_canvases: RefCell::new(Vec::new()), - redraw_pending: RefCell::new(HashSet::new()), - destroy_pending: RefCell::new(VecDeque::new()), - page_transition_event_handle: RefCell::new(None), - device_events: Cell::default(), - on_mouse_move: RefCell::new(None), - on_wheel: RefCell::new(None), - on_mouse_press: RefCell::new(None), - on_mouse_release: RefCell::new(None), - on_key_press: RefCell::new(None), - on_key_release: RefCell::new(None), + #[allow(clippy::disallowed_methods)] + let window = web_sys::window().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let document = window.document().expect("Failed to obtain document"); + + Shared(Rc::::new_cyclic(|weak| { + let proxy_spawner = WakerSpawner::new(weak.clone(), |runner, count| { + if let Some(runner) = runner.upgrade() { + Shared(runner).send_events(iter::repeat(Event::UserEvent(())).take(count)) + } + }) + .expect("`EventLoop` has to be created in the main thread"); + + Execution { + proxy_spawner, + control_flow: Cell::new(ControlFlow::default()), + poll_strategy: Cell::new(PollStrategy::default()), + exit: Cell::new(false), + runner: RefCell::new(RunnerEnum::Pending), + suspended: Cell::new(false), + event_loop_recreation: Cell::new(false), + events: RefCell::new(VecDeque::new()), + window, + document, + id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), + redraw_pending: RefCell::new(HashSet::new()), + destroy_pending: RefCell::new(VecDeque::new()), + page_transition_event_handle: RefCell::new(None), + device_events: Cell::default(), + on_mouse_move: RefCell::new(None), + on_wheel: RefCell::new(None), + on_mouse_press: RefCell::new(None), + on_mouse_release: RefCell::new(None), + on_key_press: RefCell::new(None), + on_key_release: RefCell::new(None), + on_visibility_change: RefCell::new(None), + on_touch_end: RefCell::new(None), + } })) } @@ -163,11 +189,17 @@ impl Shared { &self.0.window } - pub fn add_canvas(&self, id: WindowId, canvas: &Rc>) { - self.0 - .all_canvases - .borrow_mut() - .push((id, Rc::downgrade(canvas))); + pub fn document(&self) -> &Document { + &self.0.document + } + + pub fn add_canvas( + &self, + id: WindowId, + canvas: Weak>, + runner: DispatchRunner, + ) { + self.0.all_canvases.borrow_mut().push((id, canvas, runner)); } pub fn notify_destroy_window(&self, id: WindowId) { @@ -177,7 +209,7 @@ impl Shared { // Set the event callback to use for the event loop runner // This the event callback is a fairly thin layer over the user-provided callback that closes // over a RootEventLoopWindowTarget reference - pub fn set_listener(&self, event_handler: Box>) { + pub fn set_listener(&self, event_handler: Box) { { let mut runner = self.0.runner.borrow_mut(); assert!(matches!(*runner, RunnerEnum::Pending)); @@ -186,11 +218,12 @@ impl Shared { self.init(); *self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition( - self.window(), + self.window().clone(), { let runner = self.clone(); move |event: PageTransitionEvent| { if event.persisted() { + runner.0.suspended.set(false); runner.send_event(Event::Resumed); } } @@ -198,6 +231,7 @@ impl Shared { { let runner = self.clone(); move |event: PageTransitionEvent| { + runner.0.suspended.set(true); if event.persisted() { runner.send_event(Event::Suspended); } else { @@ -210,7 +244,7 @@ impl Shared { let runner = self.clone(); let window = self.window().clone(); *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "pointermove", Closure::new(move |event: PointerEvent| { if !runner.device_events() { @@ -287,7 +321,7 @@ impl Shared { let runner = self.clone(); let window = self.window().clone(); *self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "wheel", Closure::new(move |event: WheelEvent| { if !runner.device_events() { @@ -304,9 +338,11 @@ impl Shared { )); let runner = self.clone(); *self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "pointerdown", Closure::new(move |event: PointerEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -327,9 +363,11 @@ impl Shared { )); let runner = self.clone(); *self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "pointerup", Closure::new(move |event: PointerEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -350,9 +388,11 @@ impl Shared { )); let runner = self.clone(); *self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "keydown", Closure::new(move |event: KeyboardEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -368,7 +408,7 @@ impl Shared { )); let runner = self.clone(); *self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new( - self.window(), + self.window().clone(), "keyup", Closure::new(move |event: KeyboardEvent| { if !runner.device_events() { @@ -384,6 +424,41 @@ impl Shared { }); }), )); + let runner = self.clone(); + *self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new( + // Safari <14 doesn't support the `visibilitychange` event on `Window`. + self.document().clone(), + "visibilitychange", + Closure::new(move |_| { + if !runner.0.suspended.get() { + for (id, canvas, _) in &*runner.0.all_canvases.borrow() { + if let Some(canvas) = canvas.upgrade() { + let is_visible = backend::is_visible(runner.document()); + // only fire if: + // - not visible and intersects + // - not visible and we don't know if it intersects yet + // - visible and intersects + if let (false, Some(true) | None) | (true, Some(true)) = + (is_visible, canvas.borrow().is_intersecting) + { + runner.send_event(Event::WindowEvent { + window_id: *id, + event: WindowEvent::Occluded(!is_visible), + }); + } + } + } + } + }), + )); + let runner = self.clone(); + *self.0.on_touch_end.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "touchend", + Closure::new(move |_| { + runner.transient_activation(); + }), + )); } // Generate a strictly increasing ID @@ -397,7 +472,7 @@ impl Shared { pub fn request_redraw(&self, id: WindowId) { self.0.redraw_pending.borrow_mut().insert(id); - self.send_events::>(iter::empty()); + self.send_events::(iter::empty()); } pub fn init(&self) { @@ -425,17 +500,14 @@ impl Shared { // Add an event to the event loop runner, from the user or an event handler // // It will determine if the event should be immediately sent to the user or buffered for later - pub(crate) fn send_event>>(&self, event: E) { + pub(crate) fn send_event>(&self, event: E) { self.send_events(iter::once(event)); } // Add a series of events to the event loop runner // // It will determine if the event should be immediately sent to the user or buffered for later - pub(crate) fn send_events>>( - &self, - events: impl IntoIterator, - ) { + pub(crate) fn send_events>(&self, events: impl IntoIterator) { // If the event loop is closed, it should discard any new events if self.is_closed() { return; @@ -491,20 +563,17 @@ impl Shared { } // Process the destroy-pending windows. This should only be called from - // `run_until_cleared`, somewhere between emitting `NewEvents` and `MainEventsCleared`. - fn process_destroy_pending_windows(&self, control: &mut ControlFlow) { + // `run_until_cleared`, somewhere between emitting `NewEvents` and `AboutToWait`. + fn process_destroy_pending_windows(&self) { while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { self.0 .all_canvases .borrow_mut() - .retain(|&(item_id, _)| item_id != id); - self.handle_event( - Event::WindowEvent { - window_id: id, - event: crate::event::WindowEvent::Destroyed, - }, - control, - ); + .retain(|&(item_id, _, _)| item_id != id); + self.handle_event(Event::WindowEvent { + window_id: id, + event: crate::event::WindowEvent::Destroyed, + }); self.0.redraw_pending.borrow_mut().remove(&id); } } @@ -513,47 +582,49 @@ impl Shared { // cleared // // This will also process any events that have been queued or that are queued during processing - fn run_until_cleared>>(&self, events: impl Iterator) { - let mut control = self.current_control_flow(); + fn run_until_cleared>(&self, events: impl Iterator) { for event in events { - self.handle_event(event.into(), &mut control); + self.handle_event(event.into()); } - self.process_destroy_pending_windows(&mut control); - self.handle_event(Event::MainEventsCleared, &mut control); + self.process_destroy_pending_windows(); // Collect all of the redraw events to avoid double-locking the RefCell let redraw_events: Vec = self.0.redraw_pending.borrow_mut().drain().collect(); for window_id in redraw_events { - self.handle_event(Event::RedrawRequested(window_id), &mut control); + self.handle_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); } - self.handle_event(Event::RedrawEventsCleared, &mut control); - self.apply_control_flow(control); + self.handle_event(Event::AboutToWait); + + self.apply_control_flow(); // If the event loop is closed, it has been closed this iteration and now the closing // event should be emitted if self.is_closed() { - self.handle_loop_destroyed(&mut control); + self.handle_loop_destroyed(); } } fn handle_unload(&self) { - self.apply_control_flow(ControlFlow::Exit); - let mut control = self.current_control_flow(); + self.exit(); + self.apply_control_flow(); // We don't call `handle_loop_destroyed` here because we don't need to // perform cleanup when the web browser is going to destroy the page. - self.handle_event(Event::LoopDestroyed, &mut control); + self.handle_event(Event::LoopExiting); } // handle_event takes in events and either queues them or applies a callback // // It should only ever be called from `run_until_cleared`. - fn handle_event(&self, event: impl Into>, control: &mut ControlFlow) { + fn handle_event(&self, event: impl Into) { if self.is_closed() { - *control = ControlFlow::Exit; + self.exit(); } match *self.0.runner.borrow_mut() { RunnerEnum::Running(ref mut runner) => { - runner.handle_single_event(self, event, control); + runner.handle_single_event(self, event); } // If an event is being handled without a runner somehow, add it to the event queue so // it will eventually be processed @@ -562,57 +633,82 @@ impl Shared { RunnerEnum::Destroyed => return, } - let is_closed = matches!(*control, ControlFlow::ExitWithCode(_)); + let is_closed = self.exiting(); // Don't take events out of the queue if the loop is closed or the runner doesn't exist // If the runner doesn't exist and this method recurses, it will recurse infinitely if !is_closed && self.0.runner.borrow().maybe_runner().is_some() { + // Pre-fetch window commands to avoid having to wait until the next event loop cycle + // and potentially block other threads in the meantime. + for (_, window, runner) in self.0.all_canvases.borrow().iter() { + if let Some(window) = window.upgrade() { + runner.run(); + drop(window) + } + } + // Take an event out of the queue and handle it // Make sure not to let the borrow_mut live during the next handle_event - let event = { self.0.events.borrow_mut().pop_front() }; + let event = { + let mut events = self.0.events.borrow_mut(); + + // Pre-fetch `UserEvent`s to avoid having to wait until the next event loop cycle. + events.extend( + iter::repeat(Event::UserEvent(())) + .take(self.0.proxy_spawner.fetch()) + .map(EventWrapper::from), + ); + + events.pop_front() + }; if let Some(event) = event { - self.handle_event(event, control); + self.handle_event(event); } } } // Apply the new ControlFlow that has been selected by the user // Start any necessary timeouts etc - fn apply_control_flow(&self, control_flow: ControlFlow) { - let new_state = match control_flow { - ControlFlow::Poll => { - let cloned = self.clone(); - State::Poll { - request: backend::IdleCallback::new(self.window().clone(), move || { - cloned.poll() - }), + fn apply_control_flow(&self) { + let new_state = if self.exiting() { + State::Exit + } else { + match self.control_flow() { + ControlFlow::Poll => { + let cloned = self.clone(); + State::Poll { + request: backend::Schedule::new( + self.poll_strategy(), + self.window(), + move || cloned.poll(), + ), + } } - } - ControlFlow::Wait => State::Wait { - start: Instant::now(), - }, - ControlFlow::WaitUntil(end) => { - let start = Instant::now(); - - let delay = if end <= start { - Duration::from_millis(0) - } else { - end - start - }; - - let cloned = self.clone(); - - State::WaitUntil { - start, - end, - timeout: backend::Timeout::new( - self.window().clone(), - move || cloned.resume_time_reached(start, end), - delay, - ), + ControlFlow::Wait => State::Wait { + start: Instant::now(), + }, + ControlFlow::WaitUntil(end) => { + let start = Instant::now(); + + let delay = if end <= start { + Duration::from_millis(0) + } else { + end - start + }; + + let cloned = self.clone(); + + State::WaitUntil { + start, + end, + timeout: backend::Schedule::new_with_duration( + self.window(), + move || cloned.resume_time_reached(start, end), + delay, + ), + } } } - ControlFlow::ExitWithCode(_) => State::Exit, }; if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() { @@ -620,8 +716,8 @@ impl Shared { } } - fn handle_loop_destroyed(&self, control: &mut ControlFlow) { - self.handle_event(Event::LoopDestroyed, control); + fn handle_loop_destroyed(&self) { + self.handle_event(Event::LoopExiting); let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); *self.0.page_transition_event_handle.borrow_mut() = None; *self.0.on_mouse_move.borrow_mut() = None; @@ -630,10 +726,11 @@ impl Shared { *self.0.on_mouse_release.borrow_mut() = None; *self.0.on_key_press.borrow_mut() = None; *self.0.on_key_release.borrow_mut() = None; + *self.0.on_visibility_change.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; - for (_, canvas) in all_canvases { + for (_, canvas, _) in all_canvases { // In case any remaining `Window`s are still not dropped, we will need // to explicitly remove the event handlers associated with their canvases. if let Some(canvas) = canvas.upgrade() { @@ -652,14 +749,14 @@ impl Shared { // * The `register_redraw_request` closure. // * The `destroy_fn` closure. if self.0.event_loop_recreation.get() { - crate::event_loop::EventLoopBuilder::::allow_event_loop_recreation(); + crate::event_loop::EventLoopBuilder::<()>::allow_event_loop_recreation(); } } // Check if the event loop is currently closed fn is_closed(&self) -> bool { match self.0.runner.try_borrow().as_ref().map(Deref::deref) { - Ok(RunnerEnum::Running(runner)) => runner.state.is_exit(), + Ok(RunnerEnum::Running(runner)) => runner.state.exiting(), // The event loop is not closed since it is not initialized. Ok(RunnerEnum::Pending) => false, // The event loop is closed since it has been destroyed. @@ -670,40 +767,73 @@ impl Shared { } } - // Get the current control flow state - fn current_control_flow(&self) -> ControlFlow { - match *self.0.runner.borrow() { - RunnerEnum::Running(ref runner) => runner.state.control_flow(), - RunnerEnum::Pending => ControlFlow::Poll, - RunnerEnum::Destroyed => ControlFlow::Exit, - } - } - pub fn listen_device_events(&self, allowed: DeviceEvents) { self.0.device_events.set(allowed) } - pub fn device_events(&self) -> bool { + fn device_events(&self) -> bool { match self.0.device_events.get() { DeviceEvents::Always => true, - DeviceEvents::WhenFocused => self.0.all_canvases.borrow().iter().any(|(_, canvas)| { - if let Some(canvas) = canvas.upgrade() { - canvas.borrow().has_focus.load(Ordering::Relaxed) - } else { - false - } - }), + DeviceEvents::WhenFocused => { + self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().has_focus.get() + } else { + false + } + }) + } DeviceEvents::Never => false, } } + fn transient_activation(&self) { + self.0 + .all_canvases + .borrow() + .iter() + .for_each(|(_, canvas, _)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().transient_activation(); + } + }); + } + pub fn event_loop_recreation(&self, allow: bool) { self.0.event_loop_recreation.set(allow) } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.0.control_flow.get() + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.0.control_flow.set(control_flow) + } + + pub(crate) fn exit(&self) { + self.0.exit.set(true) + } + + pub(crate) fn exiting(&self) -> bool { + self.0.exit.get() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.0.poll_strategy.set(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.0.poll_strategy.get() + } + + pub(crate) fn waker(&self) -> Waker> { + self.0.proxy_spawner.waker() + } } -pub(crate) enum EventWrapper { - Event(Event<'static, T>), +pub(crate) enum EventWrapper { + Event(Event<()>), ScaleChange { canvas: Weak>, size: PhysicalSize, @@ -711,8 +841,8 @@ pub(crate) enum EventWrapper { }, } -impl From> for EventWrapper { - fn from(value: Event<'static, T>) -> Self { +impl From> for EventWrapper { + fn from(value: Event<()>) -> Self { Self::Event(value) } } diff --git a/src/platform_impl/web/event_loop/state.rs b/src/platform_impl/web/event_loop/state.rs index b9dacee630..8a354c78c4 100644 --- a/src/platform_impl/web/event_loop/state.rs +++ b/src/platform_impl/web/event_loop/state.rs @@ -1,5 +1,4 @@ use super::backend; -use crate::event_loop::ControlFlow; use web_time::Instant; @@ -7,7 +6,7 @@ use web_time::Instant; pub enum State { Init, WaitUntil { - timeout: backend::Timeout, + timeout: backend::Schedule, start: Instant, end: Instant, }, @@ -15,23 +14,13 @@ pub enum State { start: Instant, }, Poll { - request: backend::IdleCallback, + request: backend::Schedule, }, Exit, } impl State { - pub fn is_exit(&self) -> bool { + pub fn exiting(&self) -> bool { matches!(self, State::Exit) } - - pub fn control_flow(&self) -> ControlFlow { - match self { - State::Init => ControlFlow::Poll, - State::WaitUntil { end, .. } => ControlFlow::WaitUntil(*end), - State::Wait { .. } => ControlFlow::Wait, - State::Poll { .. } => ControlFlow::Poll, - State::Exit => ControlFlow::Exit, - } - } } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 7534aa7b16..6fd9d8635c 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,27 +1,26 @@ use std::cell::{Cell, RefCell}; -use std::clone::Clone; use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; use std::iter; -use std::rc::Rc; -use std::sync::atomic::Ordering; +use std::marker::PhantomData; +use std::rc::{Rc, Weak}; -use raw_window_handle::{RawDisplayHandle, WebDisplayHandle}; +use web_sys::Element; -use super::runner::EventWrapper; +use super::runner::{EventWrapper, Execution}; use super::{ super::{monitor::MonitorHandle, KeyEventExtra}, backend, device::DeviceId, - proxy::EventLoopProxy, runner, window::WindowId, }; use crate::event::{ - DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyEvent, RawKeyEvent, Touch, - TouchPhase, WindowEvent, + DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, }; -use crate::event_loop::DeviceEvents; +use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::keyboard::ModifiersState; +use crate::platform::web::PollStrategy; +use crate::platform_impl::platform::r#async::Waker; use crate::window::{Theme, WindowId as RootWindowId}; #[derive(Default)] @@ -44,8 +43,9 @@ impl Clone for ModifiersShared { } pub struct EventLoopWindowTarget { - pub(crate) runner: runner::Shared, + pub(crate) runner: runner::Shared, modifiers: ModifiersShared, + _marker: PhantomData, } impl Clone for EventLoopWindowTarget { @@ -53,6 +53,7 @@ impl Clone for EventLoopWindowTarget { Self { runner: self.runner.clone(), modifiers: self.modifiers.clone(), + _marker: PhantomData, } } } @@ -62,14 +63,11 @@ impl EventLoopWindowTarget { Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default(), + _marker: PhantomData, } } - pub fn proxy(&self) -> EventLoopProxy { - EventLoopProxy::new(self.runner.clone()) - } - - pub fn run(&self, event_handler: Box>, event_loop_recreation: bool) { + pub fn run(&self, event_handler: Box, event_loop_recreation: bool) { self.runner.event_loop_recreation(event_loop_recreation); self.runner.set_listener(event_handler); } @@ -84,7 +82,6 @@ impl EventLoopWindowTarget { id: WindowId, prevent_default: bool, ) { - self.runner.add_canvas(RootWindowId(id), canvas); let canvas_clone = canvas.clone(); let mut canvas = canvas.borrow_mut(); canvas.set_attribute("data-raw-handle", &id.0.to_string()); @@ -95,7 +92,7 @@ impl EventLoopWindowTarget { let has_focus = canvas.has_focus.clone(); let modifiers = self.modifiers.clone(); canvas.on_blur(move || { - has_focus.store(false, Ordering::Relaxed); + has_focus.set(false); let clear_modifiers = (!modifiers.get().is_empty()).then(|| { modifiers.set(ModifiersState::empty()); @@ -118,7 +115,7 @@ impl EventLoopWindowTarget { let runner = self.runner.clone(); let has_focus = canvas.has_focus.clone(); canvas.on_focus(move || { - if !has_focus.swap(true, Ordering::Relaxed) { + if !has_focus.replace(true) { runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::Focused(true), @@ -126,6 +123,25 @@ impl EventLoopWindowTarget { } }); + // It is possible that at this point the canvas has + // been focused before the callback can be called. + let focused = canvas + .document() + .active_element() + .filter(|element| { + let canvas: &Element = canvas.raw(); + element == canvas + }) + .is_some(); + + if focused { + canvas.has_focus.set(true); + self.runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + } + let runner = self.runner.clone(); let modifiers = self.modifiers.clone(); canvas.on_keyboard_press( @@ -140,34 +156,24 @@ impl EventLoopWindowTarget { let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); - let device_event = runner.device_events().then_some(Event::DeviceEvent { - device_id, - event: DeviceEvent::Key(RawKeyEvent { - physical_key, - state: ElementState::Pressed, - }), - }); - runner.send_events( - device_event - .into_iter() - .chain(iter::once(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id, - event: KeyEvent { - physical_key, - logical_key, - text, - location, - state: ElementState::Pressed, - repeat, - platform_specific: KeyEventExtra, - }, - is_synthetic: false, + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Pressed, + repeat, + platform_specific: KeyEventExtra, }, - })) - .chain(modifiers_changed), + is_synthetic: false, + }, + }) + .chain(modifiers_changed), ); }, prevent_default, @@ -187,34 +193,24 @@ impl EventLoopWindowTarget { let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); - let device_event = runner.device_events().then_some(Event::DeviceEvent { - device_id, - event: DeviceEvent::Key(RawKeyEvent { - physical_key, - state: ElementState::Pressed, - }), - }); - runner.send_events( - device_event - .into_iter() - .chain(iter::once(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id, - event: KeyEvent { - physical_key, - logical_key, - text, - location, - state: ElementState::Released, - repeat, - platform_specific: KeyEventExtra, - }, - is_synthetic: false, + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Released, + repeat, + platform_specific: KeyEventExtra, }, - })) - .chain(modifiers_changed), + is_synthetic: false, + }, + }) + .chain(modifiers_changed), ) }, prevent_default, @@ -227,15 +223,13 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, pointer_id| { - let focus = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { - modifiers.set(active_modifiers); - Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ModifiersChanged(active_modifiers.into()), - } - }); + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { window_id: RootWindowId(id), @@ -256,15 +250,13 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, pointer_id| { - let focus = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { - modifiers.set(active_modifiers); - Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::ModifiersChanged(active_modifiers.into()), - } - }); + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { window_id: RootWindowId(id), @@ -286,7 +278,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { + if has_focus.get() && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -301,9 +293,8 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, pointer_id, events| { - let modifiers = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { modifiers.set(active_modifiers); Event::WindowEvent { window_id: RootWindowId(id), @@ -311,48 +302,17 @@ impl EventLoopWindowTarget { } }); - runner.send_events(modifiers.into_iter().chain(events.flat_map( - |(position, delta)| { - let device_id = RootDeviceId(DeviceId(pointer_id)); - - let device_events = runner.device_events().then(|| { - let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { - device_id, - event: DeviceEvent::Motion { - axis: 0, - value: delta.x, - }, - }); - - let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { - device_id, - event: DeviceEvent::Motion { - axis: 1, - value: delta.y, - }, - }); - - x_motion.into_iter().chain(y_motion).chain(iter::once( - Event::DeviceEvent { - device_id, - event: DeviceEvent::MouseMotion { - delta: (delta.x, delta.y), - }, - }, - )) - }); - - device_events.into_iter().flatten().chain(iter::once( - Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::CursorMoved { - device_id, - position, - }, - }, - )) - }, - ))); + runner.send_events(modifiers.into_iter().chain(events.flat_map(|position| { + let device_id = RootDeviceId(DeviceId(pointer_id)); + + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }) + }))); } }, { @@ -361,9 +321,8 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, device_id, events| { - let modifiers = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { modifiers.set(active_modifiers); Event::WindowEvent { window_id: RootWindowId(id), @@ -395,9 +354,8 @@ impl EventLoopWindowTarget { position: crate::dpi::PhysicalPosition, buttons, button| { - let modifiers = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { modifiers.set(active_modifiers); Event::WindowEvent { window_id: RootWindowId(id), @@ -413,18 +371,10 @@ impl EventLoopWindowTarget { ElementState::Released }; - let device_event = runner.device_events().then(|| Event::DeviceEvent { - device_id, - event: DeviceEvent::Button { - button: button.to_id(), - state, - }, - }); - // A chorded button event may come in without any prior CursorMoved events, // therefore we should send a CursorMoved event to make sure that the // user code has the correct cursor position. - runner.send_events(modifiers.into_iter().chain(device_event).chain([ + runner.send_events(modifiers.into_iter().chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { @@ -475,18 +425,11 @@ impl EventLoopWindowTarget { }); let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); - let device_event = runner.device_events().then(|| Event::DeviceEvent { - device_id, - event: DeviceEvent::Button { - button: button.to_id(), - state: ElementState::Pressed, - }, - }); // A mouse down event may come in without any prior CursorMoved events, // therefore we should send a CursorMoved event to make sure that the // user code has the correct cursor position. - runner.send_events(modifiers.into_iter().chain(device_event).chain([ + runner.send_events(modifiers.into_iter().chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { @@ -542,7 +485,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { + if has_focus.get() && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -557,9 +500,8 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, pointer_id, position, button| { - let modifiers = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { modifiers.set(active_modifiers); Event::WindowEvent { window_id: RootWindowId(id), @@ -568,18 +510,11 @@ impl EventLoopWindowTarget { }); let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); - let device_event = runner.device_events().then(|| Event::DeviceEvent { - device_id, - event: DeviceEvent::Button { - button: button.to_id(), - state: ElementState::Pressed, - }, - }); // A mouse up event may come in without any prior CursorMoved events, // therefore we should send a CursorMoved event to make sure that the // user code has the correct cursor position. - runner.send_events(modifiers.into_iter().chain(device_event).chain([ + runner.send_events(modifiers.into_iter().chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { @@ -604,9 +539,8 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers, device_id, location, force| { - let modifiers = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) - .then(|| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { modifiers.set(active_modifiers); Event::WindowEvent { window_id: RootWindowId(id), @@ -634,8 +568,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); canvas.on_mouse_wheel( move |pointer_id, delta, active_modifiers| { - let modifiers_changed = (has_focus.load(Ordering::Relaxed) - && modifiers.get() != active_modifiers) + let modifiers_changed = (has_focus.get() && modifiers.get() != active_modifiers) .then(|| { modifiers.set(active_modifiers); Event::WindowEvent { @@ -644,21 +577,16 @@ impl EventLoopWindowTarget { } }); - let device_event = runner.device_events().then_some(Event::DeviceEvent { - device_id: RootDeviceId(DeviceId(pointer_id)), - event: DeviceEvent::MouseWheel { delta }, - }); - - runner.send_events(modifiers_changed.into_iter().chain(device_event).chain( - iter::once(Event::WindowEvent { + runner.send_events(modifiers_changed.into_iter().chain(iter::once( + Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseWheel { device_id: RootDeviceId(DeviceId(pointer_id)), delta, phase: TouchPhase::Moved, }, - }), - )); + }, + ))); }, prevent_default, ); @@ -705,9 +633,10 @@ impl EventLoopWindowTarget { }, { let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); move |new_size| { - let canvas = RefCell::borrow(&canvas_clone); + let canvas = canvas.borrow(); canvas.set_current_size(new_size); if canvas.old_size() != new_size { canvas.set_old_size(new_size); @@ -720,6 +649,28 @@ impl EventLoopWindowTarget { } }, ); + + let runner = self.runner.clone(); + canvas.on_intersection(move |is_intersecting| { + // only fire if visible while skipping the first event if it's intersecting + if backend::is_visible(runner.document()) + && !(is_intersecting && canvas_clone.borrow().is_intersecting.is_none()) + { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Occluded(!is_intersecting), + }); + } + + canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting); + }); + + let runner = self.runner.clone(); + canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); + + canvas.on_touch_end(); + + canvas.on_context_menu(prevent_default); } pub fn available_monitors(&self) -> VecDequeIter { @@ -727,14 +678,54 @@ impl EventLoopWindowTarget { } pub fn primary_monitor(&self) -> Option { - Some(MonitorHandle) + None + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Web(WebDisplayHandle::empty()) + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web( + rwh_06::WebDisplayHandle::new(), + )) } pub fn listen_device_events(&self, allowed: DeviceEvents) { self.runner.listen_device_events(allowed) } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner.exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.runner.exiting() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.runner.set_poll_strategy(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.runner.poll_strategy() + } + + pub(crate) fn waker(&self) -> Waker> { + self.runner.waker() + } } diff --git a/src/platform_impl/web/keyboard.rs b/src/platform_impl/web/keyboard.rs index 84c0003975..6f8d69c760 100644 --- a/src/platform_impl/web/keyboard.rs +++ b/src/platform_impl/web/keyboard.rs @@ -1,328 +1,328 @@ use smol_str::SmolStr; -use crate::keyboard::{Key, KeyCode, NativeKey, NativeKeyCode}; +use crate::keyboard::{Key, KeyCode, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub(crate) struct KeyEventExtra; impl Key { pub(crate) fn from_key_attribute_value(kav: &str) -> Self { - match kav { - "Unidentified" => Key::Unidentified(NativeKey::Web(SmolStr::new(kav))), - "Dead" => Key::Dead(None), - "Alt" => Key::Alt, - "AltGraph" => Key::AltGraph, - "CapsLock" => Key::CapsLock, - "Control" => Key::Control, - "Fn" => Key::Fn, - "FnLock" => Key::FnLock, - "NumLock" => Key::NumLock, - "ScrollLock" => Key::ScrollLock, - "Shift" => Key::Shift, - "Symbol" => Key::Symbol, - "SymbolLock" => Key::SymbolLock, - "Hyper" => Key::Hyper, - "Meta" => Key::Super, - "Enter" => Key::Enter, - "Tab" => Key::Tab, - " " => Key::Space, - "ArrowDown" => Key::ArrowDown, - "ArrowLeft" => Key::ArrowLeft, - "ArrowRight" => Key::ArrowRight, - "ArrowUp" => Key::ArrowUp, - "End" => Key::End, - "Home" => Key::Home, - "PageDown" => Key::PageDown, - "PageUp" => Key::PageUp, - "Backspace" => Key::Backspace, - "Clear" => Key::Clear, - "Copy" => Key::Copy, - "CrSel" => Key::CrSel, - "Cut" => Key::Cut, - "Delete" => Key::Delete, - "EraseEof" => Key::EraseEof, - "ExSel" => Key::ExSel, - "Insert" => Key::Insert, - "Paste" => Key::Paste, - "Redo" => Key::Redo, - "Undo" => Key::Undo, - "Accept" => Key::Accept, - "Again" => Key::Again, - "Attn" => Key::Attn, - "Cancel" => Key::Cancel, - "ContextMenu" => Key::ContextMenu, - "Escape" => Key::Escape, - "Execute" => Key::Execute, - "Find" => Key::Find, - "Help" => Key::Help, - "Pause" => Key::Pause, - "Play" => Key::Play, - "Props" => Key::Props, - "Select" => Key::Select, - "ZoomIn" => Key::ZoomIn, - "ZoomOut" => Key::ZoomOut, - "BrightnessDown" => Key::BrightnessDown, - "BrightnessUp" => Key::BrightnessUp, - "Eject" => Key::Eject, - "LogOff" => Key::LogOff, - "Power" => Key::Power, - "PowerOff" => Key::PowerOff, - "PrintScreen" => Key::PrintScreen, - "Hibernate" => Key::Hibernate, - "Standby" => Key::Standby, - "WakeUp" => Key::WakeUp, - "AllCandidates" => Key::AllCandidates, - "Alphanumeric" => Key::Alphanumeric, - "CodeInput" => Key::CodeInput, - "Compose" => Key::Compose, - "Convert" => Key::Convert, - "FinalMode" => Key::FinalMode, - "GroupFirst" => Key::GroupFirst, - "GroupLast" => Key::GroupLast, - "GroupNext" => Key::GroupNext, - "GroupPrevious" => Key::GroupPrevious, - "ModeChange" => Key::ModeChange, - "NextCandidate" => Key::NextCandidate, - "NonConvert" => Key::NonConvert, - "PreviousCandidate" => Key::PreviousCandidate, - "Process" => Key::Process, - "SingleCandidate" => Key::SingleCandidate, - "HangulMode" => Key::HangulMode, - "HanjaMode" => Key::HanjaMode, - "JunjaMode" => Key::JunjaMode, - "Eisu" => Key::Eisu, - "Hankaku" => Key::Hankaku, - "Hiragana" => Key::Hiragana, - "HiraganaKatakana" => Key::HiraganaKatakana, - "KanaMode" => Key::KanaMode, - "KanjiMode" => Key::KanjiMode, - "Katakana" => Key::Katakana, - "Romaji" => Key::Romaji, - "Zenkaku" => Key::Zenkaku, - "ZenkakuHankaku" => Key::ZenkakuHankaku, - "Soft1" => Key::Soft1, - "Soft2" => Key::Soft2, - "Soft3" => Key::Soft3, - "Soft4" => Key::Soft4, - "ChannelDown" => Key::ChannelDown, - "ChannelUp" => Key::ChannelUp, - "Close" => Key::Close, - "MailForward" => Key::MailForward, - "MailReply" => Key::MailReply, - "MailSend" => Key::MailSend, - "MediaClose" => Key::MediaClose, - "MediaFastForward" => Key::MediaFastForward, - "MediaPause" => Key::MediaPause, - "MediaPlay" => Key::MediaPlay, - "MediaPlayPause" => Key::MediaPlayPause, - "MediaRecord" => Key::MediaRecord, - "MediaRewind" => Key::MediaRewind, - "MediaStop" => Key::MediaStop, - "MediaTrackNext" => Key::MediaTrackNext, - "MediaTrackPrevious" => Key::MediaTrackPrevious, - "New" => Key::New, - "Open" => Key::Open, - "Print" => Key::Print, - "Save" => Key::Save, - "SpellCheck" => Key::SpellCheck, - "Key11" => Key::Key11, - "Key12" => Key::Key12, - "AudioBalanceLeft" => Key::AudioBalanceLeft, - "AudioBalanceRight" => Key::AudioBalanceRight, - "AudioBassBoostDown" => Key::AudioBassBoostDown, - "AudioBassBoostToggle" => Key::AudioBassBoostToggle, - "AudioBassBoostUp" => Key::AudioBassBoostUp, - "AudioFaderFront" => Key::AudioFaderFront, - "AudioFaderRear" => Key::AudioFaderRear, - "AudioSurroundModeNext" => Key::AudioSurroundModeNext, - "AudioTrebleDown" => Key::AudioTrebleDown, - "AudioTrebleUp" => Key::AudioTrebleUp, - "AudioVolumeDown" => Key::AudioVolumeDown, - "AudioVolumeUp" => Key::AudioVolumeUp, - "AudioVolumeMute" => Key::AudioVolumeMute, - "MicrophoneToggle" => Key::MicrophoneToggle, - "MicrophoneVolumeDown" => Key::MicrophoneVolumeDown, - "MicrophoneVolumeUp" => Key::MicrophoneVolumeUp, - "MicrophoneVolumeMute" => Key::MicrophoneVolumeMute, - "SpeechCorrectionList" => Key::SpeechCorrectionList, - "SpeechInputToggle" => Key::SpeechInputToggle, - "LaunchApplication1" => Key::LaunchApplication1, - "LaunchApplication2" => Key::LaunchApplication2, - "LaunchCalendar" => Key::LaunchCalendar, - "LaunchContacts" => Key::LaunchContacts, - "LaunchMail" => Key::LaunchMail, - "LaunchMediaPlayer" => Key::LaunchMediaPlayer, - "LaunchMusicPlayer" => Key::LaunchMusicPlayer, - "LaunchPhone" => Key::LaunchPhone, - "LaunchScreenSaver" => Key::LaunchScreenSaver, - "LaunchSpreadsheet" => Key::LaunchSpreadsheet, - "LaunchWebBrowser" => Key::LaunchWebBrowser, - "LaunchWebCam" => Key::LaunchWebCam, - "LaunchWordProcessor" => Key::LaunchWordProcessor, - "BrowserBack" => Key::BrowserBack, - "BrowserFavorites" => Key::BrowserFavorites, - "BrowserForward" => Key::BrowserForward, - "BrowserHome" => Key::BrowserHome, - "BrowserRefresh" => Key::BrowserRefresh, - "BrowserSearch" => Key::BrowserSearch, - "BrowserStop" => Key::BrowserStop, - "AppSwitch" => Key::AppSwitch, - "Call" => Key::Call, - "Camera" => Key::Camera, - "CameraFocus" => Key::CameraFocus, - "EndCall" => Key::EndCall, - "GoBack" => Key::GoBack, - "GoHome" => Key::GoHome, - "HeadsetHook" => Key::HeadsetHook, - "LastNumberRedial" => Key::LastNumberRedial, - "Notification" => Key::Notification, - "MannerMode" => Key::MannerMode, - "VoiceDial" => Key::VoiceDial, - "TV" => Key::TV, - "TV3DMode" => Key::TV3DMode, - "TVAntennaCable" => Key::TVAntennaCable, - "TVAudioDescription" => Key::TVAudioDescription, - "TVAudioDescriptionMixDown" => Key::TVAudioDescriptionMixDown, - "TVAudioDescriptionMixUp" => Key::TVAudioDescriptionMixUp, - "TVContentsMenu" => Key::TVContentsMenu, - "TVDataService" => Key::TVDataService, - "TVInput" => Key::TVInput, - "TVInputComponent1" => Key::TVInputComponent1, - "TVInputComponent2" => Key::TVInputComponent2, - "TVInputComposite1" => Key::TVInputComposite1, - "TVInputComposite2" => Key::TVInputComposite2, - "TVInputHDMI1" => Key::TVInputHDMI1, - "TVInputHDMI2" => Key::TVInputHDMI2, - "TVInputHDMI3" => Key::TVInputHDMI3, - "TVInputHDMI4" => Key::TVInputHDMI4, - "TVInputVGA1" => Key::TVInputVGA1, - "TVMediaContext" => Key::TVMediaContext, - "TVNetwork" => Key::TVNetwork, - "TVNumberEntry" => Key::TVNumberEntry, - "TVPower" => Key::TVPower, - "TVRadioService" => Key::TVRadioService, - "TVSatellite" => Key::TVSatellite, - "TVSatelliteBS" => Key::TVSatelliteBS, - "TVSatelliteCS" => Key::TVSatelliteCS, - "TVSatelliteToggle" => Key::TVSatelliteToggle, - "TVTerrestrialAnalog" => Key::TVTerrestrialAnalog, - "TVTerrestrialDigital" => Key::TVTerrestrialDigital, - "TVTimer" => Key::TVTimer, - "AVRInput" => Key::AVRInput, - "AVRPower" => Key::AVRPower, - "ColorF0Red" => Key::ColorF0Red, - "ColorF1Green" => Key::ColorF1Green, - "ColorF2Yellow" => Key::ColorF2Yellow, - "ColorF3Blue" => Key::ColorF3Blue, - "ColorF4Grey" => Key::ColorF4Grey, - "ColorF5Brown" => Key::ColorF5Brown, - "ClosedCaptionToggle" => Key::ClosedCaptionToggle, - "Dimmer" => Key::Dimmer, - "DisplaySwap" => Key::DisplaySwap, - "DVR" => Key::DVR, - "Exit" => Key::Exit, - "FavoriteClear0" => Key::FavoriteClear0, - "FavoriteClear1" => Key::FavoriteClear1, - "FavoriteClear2" => Key::FavoriteClear2, - "FavoriteClear3" => Key::FavoriteClear3, - "FavoriteRecall0" => Key::FavoriteRecall0, - "FavoriteRecall1" => Key::FavoriteRecall1, - "FavoriteRecall2" => Key::FavoriteRecall2, - "FavoriteRecall3" => Key::FavoriteRecall3, - "FavoriteStore0" => Key::FavoriteStore0, - "FavoriteStore1" => Key::FavoriteStore1, - "FavoriteStore2" => Key::FavoriteStore2, - "FavoriteStore3" => Key::FavoriteStore3, - "Guide" => Key::Guide, - "GuideNextDay" => Key::GuideNextDay, - "GuidePreviousDay" => Key::GuidePreviousDay, - "Info" => Key::Info, - "InstantReplay" => Key::InstantReplay, - "Link" => Key::Link, - "ListProgram" => Key::ListProgram, - "LiveContent" => Key::LiveContent, - "Lock" => Key::Lock, - "MediaApps" => Key::MediaApps, - "MediaAudioTrack" => Key::MediaAudioTrack, - "MediaLast" => Key::MediaLast, - "MediaSkipBackward" => Key::MediaSkipBackward, - "MediaSkipForward" => Key::MediaSkipForward, - "MediaStepBackward" => Key::MediaStepBackward, - "MediaStepForward" => Key::MediaStepForward, - "MediaTopMenu" => Key::MediaTopMenu, - "NavigateIn" => Key::NavigateIn, - "NavigateNext" => Key::NavigateNext, - "NavigateOut" => Key::NavigateOut, - "NavigatePrevious" => Key::NavigatePrevious, - "NextFavoriteChannel" => Key::NextFavoriteChannel, - "NextUserProfile" => Key::NextUserProfile, - "OnDemand" => Key::OnDemand, - "Pairing" => Key::Pairing, - "PinPDown" => Key::PinPDown, - "PinPMove" => Key::PinPMove, - "PinPToggle" => Key::PinPToggle, - "PinPUp" => Key::PinPUp, - "PlaySpeedDown" => Key::PlaySpeedDown, - "PlaySpeedReset" => Key::PlaySpeedReset, - "PlaySpeedUp" => Key::PlaySpeedUp, - "RandomToggle" => Key::RandomToggle, - "RcLowBattery" => Key::RcLowBattery, - "RecordSpeedNext" => Key::RecordSpeedNext, - "RfBypass" => Key::RfBypass, - "ScanChannelsToggle" => Key::ScanChannelsToggle, - "ScreenModeNext" => Key::ScreenModeNext, - "Settings" => Key::Settings, - "SplitScreenToggle" => Key::SplitScreenToggle, - "STBInput" => Key::STBInput, - "STBPower" => Key::STBPower, - "Subtitle" => Key::Subtitle, - "Teletext" => Key::Teletext, - "VideoModeNext" => Key::VideoModeNext, - "Wink" => Key::Wink, - "ZoomToggle" => Key::ZoomToggle, - "F1" => Key::F1, - "F2" => Key::F2, - "F3" => Key::F3, - "F4" => Key::F4, - "F5" => Key::F5, - "F6" => Key::F6, - "F7" => Key::F7, - "F8" => Key::F8, - "F9" => Key::F9, - "F10" => Key::F10, - "F11" => Key::F11, - "F12" => Key::F12, - "F13" => Key::F13, - "F14" => Key::F14, - "F15" => Key::F15, - "F16" => Key::F16, - "F17" => Key::F17, - "F18" => Key::F18, - "F19" => Key::F19, - "F20" => Key::F20, - "F21" => Key::F21, - "F22" => Key::F22, - "F23" => Key::F23, - "F24" => Key::F24, - "F25" => Key::F25, - "F26" => Key::F26, - "F27" => Key::F27, - "F28" => Key::F28, - "F29" => Key::F29, - "F30" => Key::F30, - "F31" => Key::F31, - "F32" => Key::F32, - "F33" => Key::F33, - "F34" => Key::F34, - "F35" => Key::F35, - string => Key::Character(SmolStr::new(string)), - } + Key::Named(match kav { + "Unidentified" => return Key::Unidentified(NativeKey::Web(SmolStr::new(kav))), + "Dead" => return Key::Dead(None), + "Alt" => NamedKey::Alt, + "AltGraph" => NamedKey::AltGraph, + "CapsLock" => NamedKey::CapsLock, + "Control" => NamedKey::Control, + "Fn" => NamedKey::Fn, + "FnLock" => NamedKey::FnLock, + "NumLock" => NamedKey::NumLock, + "ScrollLock" => NamedKey::ScrollLock, + "Shift" => NamedKey::Shift, + "Symbol" => NamedKey::Symbol, + "SymbolLock" => NamedKey::SymbolLock, + "Hyper" => NamedKey::Hyper, + "Meta" => NamedKey::Super, + "Enter" => NamedKey::Enter, + "Tab" => NamedKey::Tab, + " " => NamedKey::Space, + "ArrowDown" => NamedKey::ArrowDown, + "ArrowLeft" => NamedKey::ArrowLeft, + "ArrowRight" => NamedKey::ArrowRight, + "ArrowUp" => NamedKey::ArrowUp, + "End" => NamedKey::End, + "Home" => NamedKey::Home, + "PageDown" => NamedKey::PageDown, + "PageUp" => NamedKey::PageUp, + "Backspace" => NamedKey::Backspace, + "Clear" => NamedKey::Clear, + "Copy" => NamedKey::Copy, + "CrSel" => NamedKey::CrSel, + "Cut" => NamedKey::Cut, + "Delete" => NamedKey::Delete, + "EraseEof" => NamedKey::EraseEof, + "ExSel" => NamedKey::ExSel, + "Insert" => NamedKey::Insert, + "Paste" => NamedKey::Paste, + "Redo" => NamedKey::Redo, + "Undo" => NamedKey::Undo, + "Accept" => NamedKey::Accept, + "Again" => NamedKey::Again, + "Attn" => NamedKey::Attn, + "Cancel" => NamedKey::Cancel, + "ContextMenu" => NamedKey::ContextMenu, + "Escape" => NamedKey::Escape, + "Execute" => NamedKey::Execute, + "Find" => NamedKey::Find, + "Help" => NamedKey::Help, + "Pause" => NamedKey::Pause, + "Play" => NamedKey::Play, + "Props" => NamedKey::Props, + "Select" => NamedKey::Select, + "ZoomIn" => NamedKey::ZoomIn, + "ZoomOut" => NamedKey::ZoomOut, + "BrightnessDown" => NamedKey::BrightnessDown, + "BrightnessUp" => NamedKey::BrightnessUp, + "Eject" => NamedKey::Eject, + "LogOff" => NamedKey::LogOff, + "Power" => NamedKey::Power, + "PowerOff" => NamedKey::PowerOff, + "PrintScreen" => NamedKey::PrintScreen, + "Hibernate" => NamedKey::Hibernate, + "Standby" => NamedKey::Standby, + "WakeUp" => NamedKey::WakeUp, + "AllCandidates" => NamedKey::AllCandidates, + "Alphanumeric" => NamedKey::Alphanumeric, + "CodeInput" => NamedKey::CodeInput, + "Compose" => NamedKey::Compose, + "Convert" => NamedKey::Convert, + "FinalMode" => NamedKey::FinalMode, + "GroupFirst" => NamedKey::GroupFirst, + "GroupLast" => NamedKey::GroupLast, + "GroupNext" => NamedKey::GroupNext, + "GroupPrevious" => NamedKey::GroupPrevious, + "ModeChange" => NamedKey::ModeChange, + "NextCandidate" => NamedKey::NextCandidate, + "NonConvert" => NamedKey::NonConvert, + "PreviousCandidate" => NamedKey::PreviousCandidate, + "Process" => NamedKey::Process, + "SingleCandidate" => NamedKey::SingleCandidate, + "HangulMode" => NamedKey::HangulMode, + "HanjaMode" => NamedKey::HanjaMode, + "JunjaMode" => NamedKey::JunjaMode, + "Eisu" => NamedKey::Eisu, + "Hankaku" => NamedKey::Hankaku, + "Hiragana" => NamedKey::Hiragana, + "HiraganaKatakana" => NamedKey::HiraganaKatakana, + "KanaMode" => NamedKey::KanaMode, + "KanjiMode" => NamedKey::KanjiMode, + "Katakana" => NamedKey::Katakana, + "Romaji" => NamedKey::Romaji, + "Zenkaku" => NamedKey::Zenkaku, + "ZenkakuHankaku" => NamedKey::ZenkakuHankaku, + "Soft1" => NamedKey::Soft1, + "Soft2" => NamedKey::Soft2, + "Soft3" => NamedKey::Soft3, + "Soft4" => NamedKey::Soft4, + "ChannelDown" => NamedKey::ChannelDown, + "ChannelUp" => NamedKey::ChannelUp, + "Close" => NamedKey::Close, + "MailForward" => NamedKey::MailForward, + "MailReply" => NamedKey::MailReply, + "MailSend" => NamedKey::MailSend, + "MediaClose" => NamedKey::MediaClose, + "MediaFastForward" => NamedKey::MediaFastForward, + "MediaPause" => NamedKey::MediaPause, + "MediaPlay" => NamedKey::MediaPlay, + "MediaPlayPause" => NamedKey::MediaPlayPause, + "MediaRecord" => NamedKey::MediaRecord, + "MediaRewind" => NamedKey::MediaRewind, + "MediaStop" => NamedKey::MediaStop, + "MediaTrackNext" => NamedKey::MediaTrackNext, + "MediaTrackPrevious" => NamedKey::MediaTrackPrevious, + "New" => NamedKey::New, + "Open" => NamedKey::Open, + "Print" => NamedKey::Print, + "Save" => NamedKey::Save, + "SpellCheck" => NamedKey::SpellCheck, + "Key11" => NamedKey::Key11, + "Key12" => NamedKey::Key12, + "AudioBalanceLeft" => NamedKey::AudioBalanceLeft, + "AudioBalanceRight" => NamedKey::AudioBalanceRight, + "AudioBassBoostDown" => NamedKey::AudioBassBoostDown, + "AudioBassBoostToggle" => NamedKey::AudioBassBoostToggle, + "AudioBassBoostUp" => NamedKey::AudioBassBoostUp, + "AudioFaderFront" => NamedKey::AudioFaderFront, + "AudioFaderRear" => NamedKey::AudioFaderRear, + "AudioSurroundModeNext" => NamedKey::AudioSurroundModeNext, + "AudioTrebleDown" => NamedKey::AudioTrebleDown, + "AudioTrebleUp" => NamedKey::AudioTrebleUp, + "AudioVolumeDown" => NamedKey::AudioVolumeDown, + "AudioVolumeUp" => NamedKey::AudioVolumeUp, + "AudioVolumeMute" => NamedKey::AudioVolumeMute, + "MicrophoneToggle" => NamedKey::MicrophoneToggle, + "MicrophoneVolumeDown" => NamedKey::MicrophoneVolumeDown, + "MicrophoneVolumeUp" => NamedKey::MicrophoneVolumeUp, + "MicrophoneVolumeMute" => NamedKey::MicrophoneVolumeMute, + "SpeechCorrectionList" => NamedKey::SpeechCorrectionList, + "SpeechInputToggle" => NamedKey::SpeechInputToggle, + "LaunchApplication1" => NamedKey::LaunchApplication1, + "LaunchApplication2" => NamedKey::LaunchApplication2, + "LaunchCalendar" => NamedKey::LaunchCalendar, + "LaunchContacts" => NamedKey::LaunchContacts, + "LaunchMail" => NamedKey::LaunchMail, + "LaunchMediaPlayer" => NamedKey::LaunchMediaPlayer, + "LaunchMusicPlayer" => NamedKey::LaunchMusicPlayer, + "LaunchPhone" => NamedKey::LaunchPhone, + "LaunchScreenSaver" => NamedKey::LaunchScreenSaver, + "LaunchSpreadsheet" => NamedKey::LaunchSpreadsheet, + "LaunchWebBrowser" => NamedKey::LaunchWebBrowser, + "LaunchWebCam" => NamedKey::LaunchWebCam, + "LaunchWordProcessor" => NamedKey::LaunchWordProcessor, + "BrowserBack" => NamedKey::BrowserBack, + "BrowserFavorites" => NamedKey::BrowserFavorites, + "BrowserForward" => NamedKey::BrowserForward, + "BrowserHome" => NamedKey::BrowserHome, + "BrowserRefresh" => NamedKey::BrowserRefresh, + "BrowserSearch" => NamedKey::BrowserSearch, + "BrowserStop" => NamedKey::BrowserStop, + "AppSwitch" => NamedKey::AppSwitch, + "Call" => NamedKey::Call, + "Camera" => NamedKey::Camera, + "CameraFocus" => NamedKey::CameraFocus, + "EndCall" => NamedKey::EndCall, + "GoBack" => NamedKey::GoBack, + "GoHome" => NamedKey::GoHome, + "HeadsetHook" => NamedKey::HeadsetHook, + "LastNumberRedial" => NamedKey::LastNumberRedial, + "Notification" => NamedKey::Notification, + "MannerMode" => NamedKey::MannerMode, + "VoiceDial" => NamedKey::VoiceDial, + "TV" => NamedKey::TV, + "TV3DMode" => NamedKey::TV3DMode, + "TVAntennaCable" => NamedKey::TVAntennaCable, + "TVAudioDescription" => NamedKey::TVAudioDescription, + "TVAudioDescriptionMixDown" => NamedKey::TVAudioDescriptionMixDown, + "TVAudioDescriptionMixUp" => NamedKey::TVAudioDescriptionMixUp, + "TVContentsMenu" => NamedKey::TVContentsMenu, + "TVDataService" => NamedKey::TVDataService, + "TVInput" => NamedKey::TVInput, + "TVInputComponent1" => NamedKey::TVInputComponent1, + "TVInputComponent2" => NamedKey::TVInputComponent2, + "TVInputComposite1" => NamedKey::TVInputComposite1, + "TVInputComposite2" => NamedKey::TVInputComposite2, + "TVInputHDMI1" => NamedKey::TVInputHDMI1, + "TVInputHDMI2" => NamedKey::TVInputHDMI2, + "TVInputHDMI3" => NamedKey::TVInputHDMI3, + "TVInputHDMI4" => NamedKey::TVInputHDMI4, + "TVInputVGA1" => NamedKey::TVInputVGA1, + "TVMediaContext" => NamedKey::TVMediaContext, + "TVNetwork" => NamedKey::TVNetwork, + "TVNumberEntry" => NamedKey::TVNumberEntry, + "TVPower" => NamedKey::TVPower, + "TVRadioService" => NamedKey::TVRadioService, + "TVSatellite" => NamedKey::TVSatellite, + "TVSatelliteBS" => NamedKey::TVSatelliteBS, + "TVSatelliteCS" => NamedKey::TVSatelliteCS, + "TVSatelliteToggle" => NamedKey::TVSatelliteToggle, + "TVTerrestrialAnalog" => NamedKey::TVTerrestrialAnalog, + "TVTerrestrialDigital" => NamedKey::TVTerrestrialDigital, + "TVTimer" => NamedKey::TVTimer, + "AVRInput" => NamedKey::AVRInput, + "AVRPower" => NamedKey::AVRPower, + "ColorF0Red" => NamedKey::ColorF0Red, + "ColorF1Green" => NamedKey::ColorF1Green, + "ColorF2Yellow" => NamedKey::ColorF2Yellow, + "ColorF3Blue" => NamedKey::ColorF3Blue, + "ColorF4Grey" => NamedKey::ColorF4Grey, + "ColorF5Brown" => NamedKey::ColorF5Brown, + "ClosedCaptionToggle" => NamedKey::ClosedCaptionToggle, + "Dimmer" => NamedKey::Dimmer, + "DisplaySwap" => NamedKey::DisplaySwap, + "DVR" => NamedKey::DVR, + "Exit" => NamedKey::Exit, + "FavoriteClear0" => NamedKey::FavoriteClear0, + "FavoriteClear1" => NamedKey::FavoriteClear1, + "FavoriteClear2" => NamedKey::FavoriteClear2, + "FavoriteClear3" => NamedKey::FavoriteClear3, + "FavoriteRecall0" => NamedKey::FavoriteRecall0, + "FavoriteRecall1" => NamedKey::FavoriteRecall1, + "FavoriteRecall2" => NamedKey::FavoriteRecall2, + "FavoriteRecall3" => NamedKey::FavoriteRecall3, + "FavoriteStore0" => NamedKey::FavoriteStore0, + "FavoriteStore1" => NamedKey::FavoriteStore1, + "FavoriteStore2" => NamedKey::FavoriteStore2, + "FavoriteStore3" => NamedKey::FavoriteStore3, + "Guide" => NamedKey::Guide, + "GuideNextDay" => NamedKey::GuideNextDay, + "GuidePreviousDay" => NamedKey::GuidePreviousDay, + "Info" => NamedKey::Info, + "InstantReplay" => NamedKey::InstantReplay, + "Link" => NamedKey::Link, + "ListProgram" => NamedKey::ListProgram, + "LiveContent" => NamedKey::LiveContent, + "Lock" => NamedKey::Lock, + "MediaApps" => NamedKey::MediaApps, + "MediaAudioTrack" => NamedKey::MediaAudioTrack, + "MediaLast" => NamedKey::MediaLast, + "MediaSkipBackward" => NamedKey::MediaSkipBackward, + "MediaSkipForward" => NamedKey::MediaSkipForward, + "MediaStepBackward" => NamedKey::MediaStepBackward, + "MediaStepForward" => NamedKey::MediaStepForward, + "MediaTopMenu" => NamedKey::MediaTopMenu, + "NavigateIn" => NamedKey::NavigateIn, + "NavigateNext" => NamedKey::NavigateNext, + "NavigateOut" => NamedKey::NavigateOut, + "NavigatePrevious" => NamedKey::NavigatePrevious, + "NextFavoriteChannel" => NamedKey::NextFavoriteChannel, + "NextUserProfile" => NamedKey::NextUserProfile, + "OnDemand" => NamedKey::OnDemand, + "Pairing" => NamedKey::Pairing, + "PinPDown" => NamedKey::PinPDown, + "PinPMove" => NamedKey::PinPMove, + "PinPToggle" => NamedKey::PinPToggle, + "PinPUp" => NamedKey::PinPUp, + "PlaySpeedDown" => NamedKey::PlaySpeedDown, + "PlaySpeedReset" => NamedKey::PlaySpeedReset, + "PlaySpeedUp" => NamedKey::PlaySpeedUp, + "RandomToggle" => NamedKey::RandomToggle, + "RcLowBattery" => NamedKey::RcLowBattery, + "RecordSpeedNext" => NamedKey::RecordSpeedNext, + "RfBypass" => NamedKey::RfBypass, + "ScanChannelsToggle" => NamedKey::ScanChannelsToggle, + "ScreenModeNext" => NamedKey::ScreenModeNext, + "Settings" => NamedKey::Settings, + "SplitScreenToggle" => NamedKey::SplitScreenToggle, + "STBInput" => NamedKey::STBInput, + "STBPower" => NamedKey::STBPower, + "Subtitle" => NamedKey::Subtitle, + "Teletext" => NamedKey::Teletext, + "VideoModeNext" => NamedKey::VideoModeNext, + "Wink" => NamedKey::Wink, + "ZoomToggle" => NamedKey::ZoomToggle, + "F1" => NamedKey::F1, + "F2" => NamedKey::F2, + "F3" => NamedKey::F3, + "F4" => NamedKey::F4, + "F5" => NamedKey::F5, + "F6" => NamedKey::F6, + "F7" => NamedKey::F7, + "F8" => NamedKey::F8, + "F9" => NamedKey::F9, + "F10" => NamedKey::F10, + "F11" => NamedKey::F11, + "F12" => NamedKey::F12, + "F13" => NamedKey::F13, + "F14" => NamedKey::F14, + "F15" => NamedKey::F15, + "F16" => NamedKey::F16, + "F17" => NamedKey::F17, + "F18" => NamedKey::F18, + "F19" => NamedKey::F19, + "F20" => NamedKey::F20, + "F21" => NamedKey::F21, + "F22" => NamedKey::F22, + "F23" => NamedKey::F23, + "F24" => NamedKey::F24, + "F25" => NamedKey::F25, + "F26" => NamedKey::F26, + "F27" => NamedKey::F27, + "F28" => NamedKey::F28, + "F29" => NamedKey::F29, + "F30" => NamedKey::F30, + "F31" => NamedKey::F31, + "F32" => NamedKey::F32, + "F33" => NamedKey::F33, + "F34" => NamedKey::F34, + "F35" => NamedKey::F35, + string => return Key::Character(SmolStr::new(string)), + }) } } -impl KeyCode { +impl PhysicalKey { pub fn from_key_code_attribute_value(kcav: &str) -> Self { - match kcav { + PhysicalKey::Code(match kcav { "Backquote" => KeyCode::Backquote, "Backslash" => KeyCode::Backslash, "BracketLeft" => KeyCode::BracketLeft, @@ -516,7 +516,7 @@ impl KeyCode { "F33" => KeyCode::F33, "F34" => KeyCode::F34, "F35" => KeyCode::F35, - _ => KeyCode::Unidentified(NativeKeyCode::Unidentified), - } + _ => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + }) } } diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index dc178179b4..831fa5a86e 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -17,6 +17,9 @@ // incoming events (from the registered handlers) and ensuring they are passed to the user in a // compliant way. +// TODO: FP, remove when is fixed. +#![allow(unknown_lints, non_local_definitions)] + mod r#async; mod device; mod error; @@ -38,4 +41,4 @@ pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId pub(crate) use self::keyboard::KeyEventExtra; pub(crate) use crate::icon::NoIcon as PlatformIcon; -pub(self) use crate::platform_impl::Fullscreen; +pub(crate) use crate::platform_impl::Fullscreen; diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 6fdbcae28d..5353e7e624 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -1,3 +1,5 @@ +use std::iter::Empty; + use crate::dpi::{PhysicalPosition, PhysicalSize}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -5,30 +7,27 @@ pub struct MonitorHandle; impl MonitorHandle { pub fn scale_factor(&self) -> f64 { - 1.0 + unreachable!() } pub fn position(&self) -> PhysicalPosition { - PhysicalPosition { x: 0, y: 0 } + unreachable!() } pub fn name(&self) -> Option { - None + unreachable!() } pub fn refresh_rate_millihertz(&self) -> Option { - None + unreachable!() } pub fn size(&self) -> PhysicalSize { - PhysicalSize { - width: 0, - height: 0, - } + unreachable!() } - pub fn video_modes(&self) -> impl Iterator { - std::iter::empty() + pub fn video_modes(&self) -> Empty { + unreachable!() } } @@ -37,18 +36,18 @@ pub struct VideoMode; impl VideoMode { pub fn size(&self) -> PhysicalSize { - unimplemented!(); + unreachable!(); } pub fn bit_depth(&self) -> u16 { - unimplemented!(); + unreachable!(); } pub fn refresh_rate_millihertz(&self) -> u32 { - 32000 + unreachable!(); } pub fn monitor(&self) -> MonitorHandle { - MonitorHandle + unreachable!(); } } diff --git a/src/platform_impl/web/web_sys/animation_frame.rs b/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 0000000000..b761f9a9a7 --- /dev/null +++ b/src/platform_impl/web/web_sys/animation_frame.rs @@ -0,0 +1,70 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct AnimationFrameHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl AnimationFrameHandler { + pub fn new(window: web_sys::Window) -> Self { + let handle = Rc::new(Cell::new(None)); + let closure = Closure::new({ + let handle = handle.clone(); + move || handle.set(None) + }); + + Self { + window, + closure, + handle, + } + } + + pub fn on_animation_frame(&mut self, mut f: F) + where + F: 'static + FnMut(), + { + let handle = self.handle.clone(); + self.closure = Closure::new(move || { + handle.set(None); + f(); + }) + } + + pub fn request(&self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + + let handle = self + .window + .request_animation_frame(self.closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + self.handle.set(Some(handle)); + } + + pub fn cancel(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } +} + +impl Drop for AnimationFrameHandler { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 67a383e8a5..908a2c0423 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,34 +1,37 @@ -use super::super::WindowId; -use super::event_handle::EventListenerHandle; -use super::media_query_handle::MediaQueryListHandle; -use super::pointer::PointerHandler; -use super::{event, ButtonsState, ResizeScaleHandle}; +use std::cell::Cell; +use std::rc::{Rc, Weak}; +use std::sync::{Arc, Mutex}; + +use smol_str::SmolStr; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + PointerEvent, WheelEvent, +}; + use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; -use crate::event::{Force, MouseButton, MouseScrollDelta}; -use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState}; +use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use crate::window::{WindowAttributes, WindowId as RootWindowId}; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use js_sys::Promise; -use smol_str::SmolStr; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent}; +use super::super::WindowId; +use super::animation_frame::AnimationFrameHandler; +use super::event_handle::EventListenerHandle; +use super::fullscreen::FullscreenHandler; +use super::intersection_handle::IntersectionObserverHandle; +use super::media_query_handle::MediaQueryListHandle; +use super::pointer::PointerHandler; +use super::{event, ButtonsState, ResizeScaleHandle}; #[allow(dead_code)] pub struct Canvas { common: Common, id: WindowId, - pub has_focus: Arc, + pub has_focus: Rc>, + pub is_intersecting: Option, on_touch_start: Option>, - on_touch_end: Option>, on_focus: Option>, on_blur: Option>, on_keyboard_release: Option>, @@ -37,38 +40,47 @@ pub struct Canvas { on_dark_mode: Option, pointer_handler: PointerHandler, on_resize_scale: Option, + on_intersect: Option, + animation_frame_handler: AnimationFrameHandler, + on_touch_end: Option>, + on_context_menu: Option>, } pub struct Common { pub window: web_sys::Window, + pub document: Document, /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. pub raw: HtmlCanvasElement, + style: CssStyleDeclaration, old_size: Rc>>, current_size: Rc>>, - wants_fullscreen: Rc>, + fullscreen_handler: Rc, } impl Canvas { pub fn create( id: WindowId, window: web_sys::Window, + document: Document, attr: &WindowAttributes, platform_attr: PlatformSpecificWindowBuilderAttributes, ) -> Result { - let canvas = match platform_attr.canvas { + let canvas = match platform_attr.canvas.0 { Some(canvas) => canvas, - None => { - let document = window - .document() - .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; - - document - .create_element("canvas") - .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? - .unchecked_into() - } + None => document + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .unchecked_into(), }; + if platform_attr.append && !document.contains(Some(&canvas)) { + document + .body() + .expect("Failed to get body from document") + .append_child(&canvas) + .expect("Failed to append canvas to body"); + } + // A tabindex is needed in order to capture local keyboard events. // A "0" value means that the element should be focusable in // sequential keyboard navigation, but its order is defined by the @@ -80,23 +92,57 @@ impl Canvas { .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; } + #[allow(clippy::disallowed_methods)] + let style = window + .get_computed_style(&canvas) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); + + let common = Common { + window: window.clone(), + document: document.clone(), + raw: canvas.clone(), + style, + old_size: Rc::default(), + current_size: Rc::default(), + fullscreen_handler: Rc::new(FullscreenHandler::new(document.clone(), canvas.clone())), + }; + if let Some(size) = attr.inner_size { - let size = size.to_logical(super::scale_factor(&window)); - super::set_canvas_size(&window, &canvas, size); + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_size(&common.document, &common.raw, &common.style, size); + } + + if let Some(size) = attr.min_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_min_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(size) = attr.max_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_max_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(position) = attr.position { + let position = position.to_logical(super::scale_factor(&common.window)); + super::set_canvas_position(&common.document, &common.raw, &common.style, position); + } + + if attr.fullscreen.0.is_some() { + common.fullscreen_handler.request_fullscreen(); + } + + if attr.active { + let _ = common.raw.focus(); } Ok(Canvas { - common: Common { - window, - raw: canvas, - old_size: Rc::default(), - current_size: Rc::default(), - wants_fullscreen: Rc::new(RefCell::new(false)), - }, + common, id, - has_focus: Arc::new(AtomicBool::new(false)), + has_focus: Rc::new(Cell::new(false)), + is_intersecting: None, on_touch_start: None, - on_touch_end: None, on_blur: None, on_focus: None, on_keyboard_release: None, @@ -105,6 +151,10 @@ impl Canvas { on_dark_mode: None, pointer_handler: PointerHandler::new(), on_resize_scale: None, + on_intersect: None, + animation_frame_handler: AnimationFrameHandler::new(window), + on_touch_end: None, + on_context_menu: None, }) } @@ -112,12 +162,7 @@ impl Canvas { if lock { self.raw().request_pointer_lock(); } else { - let document = self - .common - .window - .document() - .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; - document.exit_pointer_lock(); + self.common.document.exit_pointer_lock(); } Ok(()) } @@ -131,37 +176,63 @@ impl Canvas { pub fn position(&self) -> LogicalPosition { let bounds = self.common.raw.get_bounding_client_rect(); - - LogicalPosition { + let mut position = LogicalPosition { x: bounds.x(), y: bounds.y(), + }; + + if self.document().contains(Some(self.raw())) + && self.style().get_property_value("display").unwrap() != "none" + { + position.x += super::style_size_property(self.style(), "border-left-width") + + super::style_size_property(self.style(), "padding-left"); + position.y += super::style_size_property(self.style(), "border-top-width") + + super::style_size_property(self.style(), "padding-top"); } + + position } + #[inline] pub fn old_size(&self) -> PhysicalSize { self.common.old_size.get() } + #[inline] pub fn inner_size(&self) -> PhysicalSize { self.common.current_size.get() } + #[inline] pub fn set_old_size(&self, size: PhysicalSize) { self.common.old_size.set(size) } + #[inline] pub fn set_current_size(&self, size: PhysicalSize) { self.common.current_size.set(size) } + #[inline] pub fn window(&self) -> &web_sys::Window { &self.common.window } + #[inline] + pub fn document(&self) -> &Document { + &self.common.document + } + + #[inline] pub fn raw(&self) -> &HtmlCanvasElement { &self.common.raw } + #[inline] + pub fn style(&self) -> &CssStyleDeclaration { + &self.common.style + } + pub fn on_touch_start(&mut self, prevent_default: bool) { self.on_touch_start = Some(self.common.add_event("touchstart", move |event: Event| { if prevent_default { @@ -190,11 +261,10 @@ impl Canvas { pub fn on_keyboard_release(&mut self, mut handler: F, prevent_default: bool) where - F: 'static + FnMut(KeyCode, Key, Option, KeyLocation, bool, ModifiersState), + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), { - self.on_keyboard_release = Some(self.common.add_user_event( - "keyup", - move |event: KeyboardEvent| { + self.on_keyboard_release = + Some(self.common.add_event("keyup", move |event: KeyboardEvent| { if prevent_default { event.prevent_default(); } @@ -208,15 +278,14 @@ impl Canvas { event.repeat(), modifiers, ); - }, - )); + })); } pub fn on_keyboard_press(&mut self, mut handler: F, prevent_default: bool) where - F: 'static + FnMut(KeyCode, Key, Option, KeyLocation, bool, ModifiersState), + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), { - self.on_keyboard_press = Some(self.common.add_user_event( + self.on_keyboard_press = Some(self.common.add_transient_event( "keydown", move |event: KeyboardEvent| { if prevent_default { @@ -297,12 +366,7 @@ impl Canvas { prevent_default: bool, ) where MOD: 'static + FnMut(ModifiersState), - M: 'static - + FnMut( - ModifiersState, - i32, - &mut dyn Iterator, PhysicalPosition)>, - ), + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), T: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), @@ -359,43 +423,87 @@ impl Canvas { { self.on_resize_scale = Some(ResizeScaleHandle::new( self.window().clone(), + self.document().clone(), self.raw().clone(), + self.style().clone(), scale_handler, size_handler, )); } + pub(crate) fn on_intersection(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); + } + + pub(crate) fn on_animation_frame(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.animation_frame_handler.on_animation_frame(f) + } + + pub(crate) fn on_touch_end(&mut self) { + self.on_touch_end = Some(self.common.add_transient_event("touchend", |_| {})); + } + + pub(crate) fn on_context_menu(&mut self, prevent_default: bool) { + self.on_context_menu = Some(self.common.add_event( + "contextmenu", + move |event: PointerEvent| { + if prevent_default { + event.prevent_default(); + } + }, + )); + } + pub fn request_fullscreen(&self) { - self.common.request_fullscreen() + self.common.fullscreen_handler.request_fullscreen() + } + + pub fn exit_fullscreen(&self) { + self.common.fullscreen_handler.exit_fullscreen() } pub fn is_fullscreen(&self) -> bool { - self.common.is_fullscreen() + self.common.fullscreen_handler.is_fullscreen() + } + + pub fn request_animation_frame(&self) { + self.animation_frame_handler.request(); } - pub(crate) fn handle_scale_change( + pub(crate) fn handle_scale_change( &self, - runner: &super::super::event_loop::runner::Shared, - event_handler: impl FnOnce(crate::event::Event<'_, T>), + runner: &super::super::event_loop::runner::Shared, + event_handler: impl FnOnce(crate::event::Event<()>), current_size: PhysicalSize, scale: f64, ) { // First, we send the `ScaleFactorChanged` event: self.set_current_size(current_size); - let mut new_size = current_size; - event_handler(crate::event::Event::WindowEvent { - window_id: RootWindowId(self.id), - event: crate::event::WindowEvent::ScaleFactorChanged { - scale_factor: scale, - new_inner_size: &mut new_size, - }, - }); + let new_size = { + let new_size = Arc::new(Mutex::new(current_size)); + event_handler(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_size)), + }, + }); + + let new_size = *new_size.lock().unwrap(); + new_size + }; if current_size != new_size { // Then we resize the canvas to the new size, a new // `Resized` event will be sent by the `ResizeObserver`: let new_size = new_size.to_logical(scale); - super::set_canvas_size(self.window(), self.raw(), new_size); + super::set_canvas_size(self.document(), self.raw(), self.style(), new_size); // Set the size might not trigger the event because the calculation is inaccurate. self.on_resize_scale @@ -412,7 +520,12 @@ impl Canvas { } } + pub(crate) fn transient_activation(&self) { + self.common.fullscreen_handler.transient_activation() + } + pub fn remove_listeners(&mut self) { + self.on_touch_start = None; self.on_focus = None; self.on_blur = None; self.on_keyboard_release = None; @@ -421,6 +534,11 @@ impl Canvas { self.on_dark_mode = None; self.pointer_handler.remove_listeners(); self.on_resize_scale = None; + self.on_intersect = None; + self.animation_frame_handler.cancel(); + self.on_touch_end = None; + self.common.fullscreen_handler.cancel(); + self.on_context_menu = None; } } @@ -428,23 +546,19 @@ impl Common { pub fn add_event( &self, event_name: &'static str, - mut handler: F, + handler: F, ) -> EventListenerHandle where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), { - let closure = Closure::new(move |event: E| { - event.as_ref().stop_propagation(); - handler(event); - }); - EventListenerHandle::new(&self.raw, event_name, closure) + EventListenerHandle::new(self.raw.clone(), event_name, Closure::new(handler)) } // The difference between add_event and add_user_event is that the latter has a special meaning // for browser security. A user event is a deliberate action by the user (like a mouse or key // press) and is the only time things like a fullscreen request may be successfully completed.) - pub fn add_user_event( + pub fn add_transient_event( &self, event_name: &'static str, mut handler: F, @@ -453,49 +567,14 @@ impl Common { E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), { - let wants_fullscreen = self.wants_fullscreen.clone(); - let canvas = self.raw.clone(); + let fullscreen_handler = Rc::downgrade(&self.fullscreen_handler); self.add_event(event_name, move |event: E| { handler(event); - if *wants_fullscreen.borrow() { - canvas - .request_fullscreen() - .expect("Failed to enter fullscreen"); - *wants_fullscreen.borrow_mut() = false; + if let Some(fullscreen_handler) = Weak::upgrade(&fullscreen_handler) { + fullscreen_handler.transient_activation() } }) } - - pub fn request_fullscreen(&self) { - #[wasm_bindgen] - extern "C" { - type ElementExt; - - #[wasm_bindgen(catch, method, js_name = requestFullscreen)] - fn request_fullscreen(this: &ElementExt) -> Result; - } - - let raw: &ElementExt = self.raw.unchecked_ref(); - - // This should return a `Promise`, but Safari v<16.4 is not up-to-date with the spec. - match raw.request_fullscreen() { - Ok(value) if !value.is_undefined() => { - let promise: Promise = value.unchecked_into(); - let wants_fullscreen = self.wants_fullscreen.clone(); - wasm_bindgen_futures::spawn_local(async move { - if JsFuture::from(promise).await.is_err() { - *wants_fullscreen.borrow_mut() = true - } - }); - } - // We are on Safari v<16.4, let's try again on the next transient activation. - _ => *self.wants_fullscreen.borrow_mut() = true, - } - } - - pub fn is_fullscreen(&self) -> bool { - super::is_fullscreen(&self.window, &self.raw) - } } diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 70ece5fea8..274b9f2c18 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -1,13 +1,12 @@ use crate::dpi::LogicalPosition; use crate::event::{MouseButton, MouseScrollDelta}; -use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; use once_cell::unsync::OnceCell; use smol_str::SmolStr; -use std::convert::TryInto; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{HtmlCanvasElement, KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; +use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; bitflags! { // https://www.w3.org/TR/pointerevents3/#the-buttons-property @@ -81,9 +80,22 @@ impl MouseButton { } pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { + #[wasm_bindgen] + extern "C" { + type MouseEventExt; + + #[wasm_bindgen(method, getter, js_name = offsetX)] + fn offset_x(this: &MouseEventExt) -> f64; + + #[wasm_bindgen(method, getter, js_name = offsetY)] + fn offset_y(this: &MouseEventExt) -> f64; + } + + let event: &MouseEventExt = event.unchecked_ref(); + LogicalPosition { - x: event.offset_x() as f64, - y: event.offset_y() as f64, + x: event.offset_x(), + y: event.offset_y(), } } @@ -132,17 +144,6 @@ impl MouseDelta { } } -pub fn mouse_position_by_client( - event: &MouseEvent, - canvas: &HtmlCanvasElement, -) -> LogicalPosition { - let bounding_client_rect = canvas.get_bounding_client_rect(); - LogicalPosition { - x: event.client_x() as f64 - bounding_client_rect.x(), - y: event.client_y() as f64 - bounding_client_rect.y(), - } -} - pub fn mouse_scroll_delta( window: &web_sys::Window, event: &WheelEvent, @@ -160,9 +161,9 @@ pub fn mouse_scroll_delta( } } -pub fn key_code(event: &KeyboardEvent) -> KeyCode { +pub fn key_code(event: &KeyboardEvent) -> PhysicalKey { let code = event.code(); - KeyCode::from_key_code_attribute_value(&code) + PhysicalKey::from_key_code_attribute_value(&code) } pub fn key(event: &KeyboardEvent) -> Key { @@ -174,9 +175,9 @@ pub fn key_text(event: &KeyboardEvent) -> Option { let key = Key::from_key_attribute_value(&key); match &key { Key::Character(text) => Some(text.clone()), - Key::Tab => Some(SmolStr::new("\t")), - Key::Enter => Some(SmolStr::new("\r")), - Key::Space => Some(SmolStr::new(" ")), + Key::Named(NamedKey::Tab) => Some(SmolStr::new("\t")), + Key::Named(NamedKey::Enter) => Some(SmolStr::new("\r")), + Key::Named(NamedKey::Space) => Some(SmolStr::new(" ")), _ => None, } .map(SmolStr::new) @@ -233,10 +234,6 @@ pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState { state } -pub fn touch_position(event: &PointerEvent, canvas: &HtmlCanvasElement) -> LogicalPosition { - mouse_position_by_client(event, canvas) -} - pub fn pointer_move_event(event: PointerEvent) -> impl Iterator { // make a single iterator depending on the availability of coalesced events if has_coalesced_events_support(&event) { @@ -259,7 +256,7 @@ pub fn pointer_move_event(event: PointerEvent) -> impl Iterator. pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool { thread_local! { - static POINTER_RAW_SUPPORT: OnceCell = OnceCell::new(); + static POINTER_RAW_SUPPORT: OnceCell = const { OnceCell::new() }; } POINTER_RAW_SUPPORT.with(|support| { @@ -282,7 +279,7 @@ pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool { // See . pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { thread_local! { - static COALESCED_EVENTS_SUPPORT: OnceCell = OnceCell::new(); + static COALESCED_EVENTS_SUPPORT: OnceCell = const { OnceCell::new() }; } COALESCED_EVENTS_SUPPORT.with(|support| { diff --git a/src/platform_impl/web/web_sys/event_handle.rs b/src/platform_impl/web/web_sys/event_handle.rs index 505d58a299..bf2c3471f0 100644 --- a/src/platform_impl/web/web_sys/event_handle.rs +++ b/src/platform_impl/web/web_sys/event_handle.rs @@ -8,11 +8,11 @@ pub struct EventListenerHandle { } impl EventListenerHandle { - pub fn new(target: &U, event_type: &'static str, listener: Closure) -> Self + pub fn new(target: U, event_type: &'static str, listener: Closure) -> Self where - U: Clone + Into, + U: Into, { - let target = target.clone().into(); + let target = target.into(); target .add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref()) .expect("Failed to add event listener"); diff --git a/src/platform_impl/web/web_sys/fullscreen.rs b/src/platform_impl/web/web_sys/fullscreen.rs new file mode 100644 index 0000000000..03e680d333 --- /dev/null +++ b/src/platform_impl/web/web_sys/fullscreen.rs @@ -0,0 +1,157 @@ +use std::cell::Cell; +use std::rc::Rc; + +use js_sys::Promise; +use once_cell::unsync::OnceCell; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{Document, Element, HtmlCanvasElement}; + +use super::EventListenerHandle; + +thread_local! { + static FULLSCREEN_API_SUPPORT: OnceCell = const { OnceCell::new() }; +} + +pub struct FullscreenHandler { + document: Document, + canvas: HtmlCanvasElement, + fullscreen_requested: Rc>, + _fullscreen_change: EventListenerHandle, +} + +impl FullscreenHandler { + pub fn new(document: Document, canvas: HtmlCanvasElement) -> Self { + let fullscreen_requested = Rc::new(Cell::new(false)); + let fullscreen_change = EventListenerHandle::new( + canvas.clone(), + if has_fullscreen_api_support(&canvas) { + "fullscreenchange" + } else { + "webkitfullscreenchange" + }, + Closure::new({ + let fullscreen_requested = fullscreen_requested.clone(); + move || { + // It doesn't matter if the canvas entered or exitted fullscreen mode, + // we don't want to request it again later. + fullscreen_requested.set(false); + } + }), + ); + + Self { + document, + canvas, + fullscreen_requested, + _fullscreen_change: fullscreen_change, + } + } + + fn internal_request_fullscreen(&self) { + #[wasm_bindgen] + extern "C" { + type RequestFullscreen; + + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen(this: &RequestFullscreen) -> Promise; + + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] + fn webkit_request_fullscreen(this: &RequestFullscreen); + } + + let canvas: &RequestFullscreen = self.canvas.unchecked_ref(); + + if has_fullscreen_api_support(&self.canvas) { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } + } + + pub fn request_fullscreen(&self) { + if !self.is_fullscreen() { + self.internal_request_fullscreen(); + self.fullscreen_requested.set(true); + } + } + + pub fn transient_activation(&self) { + if self.fullscreen_requested.get() { + self.internal_request_fullscreen() + } + } + + pub fn is_fullscreen(&self) -> bool { + #[wasm_bindgen] + extern "C" { + type FullscreenElement; + + #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] + fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; + } + + let element = if has_fullscreen_api_support(&self.canvas) { + #[allow(clippy::disallowed_methods)] + self.document.fullscreen_element() + } else { + let document: &FullscreenElement = self.document.unchecked_ref(); + document.webkit_fullscreen_element() + }; + + match element { + Some(element) => { + let canvas: &Element = &self.canvas; + canvas == &element + } + None => false, + } + } + + pub fn exit_fullscreen(&self) { + #[wasm_bindgen] + extern "C" { + type ExitFullscreen; + + #[wasm_bindgen(method, js_name = webkitExitFullscreen)] + fn webkit_exit_fullscreen(this: &ExitFullscreen); + } + + if has_fullscreen_api_support(&self.canvas) { + #[allow(clippy::disallowed_methods)] + self.document.exit_fullscreen() + } else { + let document: &ExitFullscreen = self.document.unchecked_ref(); + document.webkit_exit_fullscreen() + } + + self.fullscreen_requested.set(false); + } + + pub fn cancel(&self) { + self.fullscreen_requested.set(false); + } +} + +fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { + FULLSCREEN_API_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type CanvasFullScreenApiSupport; + + #[wasm_bindgen(method, getter, js_name = requestFullscreen)] + fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; + } + + let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); + !support.has_request_fullscreen().is_undefined() + }) + }) +} diff --git a/src/platform_impl/web/web_sys/intersection_handle.rs b/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 0000000000..cae8eeb722 --- /dev/null +++ b/src/platform_impl/web/web_sys/intersection_handle.rs @@ -0,0 +1,35 @@ +use js_sys::Array; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry}; + +pub(super) struct IntersectionObserverHandle { + observer: IntersectionObserver, + _closure: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(element: &Element, mut callback: F) -> Self + where + F: 'static + FnMut(bool), + { + let closure = Closure::new(move |entries: Array| { + let entry: IntersectionObserverEntry = entries.get(0).unchecked_into(); + callback(entry.is_intersecting()); + }); + let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref()) + // we don't provide any `options` + .expect("Invalid `options`"); + observer.observe(element); + + Self { + observer, + _closure: closure, + } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.disconnect() + } +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 558bd1a29a..45dd7bf487 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,47 +1,45 @@ +mod animation_frame; mod canvas; pub mod event; mod event_handle; +mod fullscreen; +mod intersection_handle; mod media_query_handle; mod pointer; mod resize_scaling; -mod timeout; +mod schedule; pub use self::canvas::Canvas; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; pub use self::resize_scaling::ResizeScaleHandle; -pub use self::timeout::{IdleCallback, Timeout}; +pub use self::schedule::Schedule; -use crate::dpi::LogicalSize; -use crate::platform::web::WindowExtWebSys; -use crate::window::Window; +use crate::dpi::{LogicalPosition, LogicalSize}; use wasm_bindgen::closure::Closure; -use web_sys::{CssStyleDeclaration, Element, HtmlCanvasElement, PageTransitionEvent}; +use web_sys::{ + CssStyleDeclaration, Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState, +}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); } -pub fn exit_fullscreen(window: &web_sys::Window) { - let document = window.document().expect("Failed to obtain document"); - - document.exit_fullscreen(); -} - pub struct PageTransitionEventHandle { _show_listener: event_handle::EventListenerHandle, _hide_listener: event_handle::EventListenerHandle, } pub fn on_page_transition( - window: &web_sys::Window, + window: web_sys::Window, show_handler: impl FnMut(PageTransitionEvent) + 'static, hide_handler: impl FnMut(PageTransitionEvent) + 'static, ) -> PageTransitionEventHandle { let show_closure = Closure::new(show_handler); let hide_closure = Closure::new(hide_handler); - let show_listener = event_handle::EventListenerHandle::new(window, "pageshow", show_closure); + let show_listener = + event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure); let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure); PageTransitionEventHandle { _show_listener: show_listener, @@ -49,54 +47,111 @@ pub fn on_page_transition( } } -impl WindowExtWebSys for Window { - fn canvas(&self) -> Option { - self.window.canvas() - } +pub fn scale_factor(window: &web_sys::Window) -> f64 { + window.device_pixel_ratio() +} - fn is_dark_mode(&self) -> bool { - self.window - .inner - .queue(|inner| is_dark_mode(&inner.window).unwrap_or(false)) +fn fix_canvas_size(style: &CssStyleDeclaration, mut size: LogicalSize) -> LogicalSize { + if style.get_property_value("box-sizing").unwrap() == "border-box" { + size.width += style_size_property(style, "border-left-width") + + style_size_property(style, "border-right-width") + + style_size_property(style, "padding-left") + + style_size_property(style, "padding-right"); + size.height += style_size_property(style, "border-top-width") + + style_size_property(style, "border-bottom-width") + + style_size_property(style, "padding-top") + + style_size_property(style, "padding-bottom"); } -} -pub fn scale_factor(window: &web_sys::Window) -> f64 { - window.device_pixel_ratio() + size } pub fn set_canvas_size( - window: &web_sys::Window, + document: &Document, raw: &HtmlCanvasElement, - mut new_size: LogicalSize, + style: &CssStyleDeclaration, + new_size: LogicalSize, ) { - let document = window.document().expect("Failed to obtain document"); - - let style = window - .get_computed_style(raw) - .expect("Failed to obtain computed style") - // this can't fail: we aren't using a pseudo-element - .expect("Invalid pseudo-element"); - if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" { return; } - if style.get_property_value("box-sizing").unwrap() == "border-box" { - new_size.width += style_size_property(&style, "border-left-width") - + style_size_property(&style, "border-right-width") - + style_size_property(&style, "padding-left") - + style_size_property(&style, "padding-right"); - new_size.height += style_size_property(&style, "border-top-width") - + style_size_property(&style, "border-bottom-width") - + style_size_property(&style, "padding-top") - + style_size_property(&style, "padding-bottom"); - } + let new_size = fix_canvas_size(style, new_size); set_canvas_style_property(raw, "width", &format!("{}px", new_size.width)); set_canvas_style_property(raw, "height", &format!("{}px", new_size.height)); } +pub fn set_canvas_min_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &CssStyleDeclaration, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + set_canvas_style_property(raw, "min-width", &format!("{}px", new_size.width)); + set_canvas_style_property(raw, "min-height", &format!("{}px", new_size.height)); + } else { + style + .remove_property("min-width") + .expect("Property is read only"); + style + .remove_property("min-height") + .expect("Property is read only"); + } +} + +pub fn set_canvas_max_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &CssStyleDeclaration, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + set_canvas_style_property(raw, "max-width", &format!("{}px", new_size.width)); + set_canvas_style_property(raw, "max-height", &format!("{}px", new_size.height)); + } else { + style + .remove_property("max-width") + .expect("Property is read only"); + style + .remove_property("max-height") + .expect("Property is read only"); + } +} + +pub fn set_canvas_position( + document: &Document, + raw: &HtmlCanvasElement, + style: &CssStyleDeclaration, + mut position: LogicalPosition, +) { + if document.contains(Some(raw)) && style.get_property_value("display").unwrap() != "none" { + position.x -= style_size_property(style, "margin-left") + + style_size_property(style, "border-left-width") + + style_size_property(style, "padding-left"); + position.y -= style_size_property(style, "margin-top") + + style_size_property(style, "border-top-width") + + style_size_property(style, "padding-top"); + } + + set_canvas_style_property(raw, "position", "fixed"); + set_canvas_style_property(raw, "left", &format!("{}px", position.x)); + set_canvas_style_property(raw, "top", &format!("{}px", position.y)); +} + /// This function will panic if the element is not inserted in the DOM /// or is not a CSS property that represents a size in pixel. pub fn style_size_property(style: &CssStyleDeclaration, property: &str) -> f64 { @@ -116,18 +171,6 @@ pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: .unwrap_or_else(|err| panic!("error: {err:?}\nFailed to set {property}")) } -pub fn is_fullscreen(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> bool { - let document = window.document().expect("Failed to obtain document"); - - match document.fullscreen_element() { - Some(elem) => { - let raw: Element = canvas.clone().into(); - raw == elem - } - None => false, - } -} - pub fn is_dark_mode(window: &web_sys::Window) -> Option { window .match_media("(prefers-color-scheme: dark)") @@ -136,4 +179,8 @@ pub fn is_dark_mode(window: &web_sys::Window) -> Option { .map(|media| media.matches()) } +pub fn is_visible(document: &Document) -> bool { + document.visibility_state() == VisibilityState::Visible +} + pub type RawCanvasType = HtmlCanvasElement; diff --git a/src/platform_impl/web/web_sys/pointer.rs b/src/platform_impl/web/web_sys/pointer.rs index a22a01aab2..4605d0aca0 100644 --- a/src/platform_impl/web/web_sys/pointer.rs +++ b/src/platform_impl/web/web_sys/pointer.rs @@ -80,8 +80,7 @@ impl PointerHandler { T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { let window = canvas_common.window.clone(); - let canvas = canvas_common.raw.clone(); - self.on_pointer_release = Some(canvas_common.add_user_event( + self.on_pointer_release = Some(canvas_common.add_transient_event( "pointerup", move |event: PointerEvent| { let modifiers = event::mouse_modifiers(&event); @@ -90,8 +89,7 @@ impl PointerHandler { "touch" => touch_handler( modifiers, event.pointer_id(), - event::touch_position(&event, &canvas) - .to_physical(super::scale_factor(&window)), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ), "mouse" => mouse_handler( @@ -120,7 +118,7 @@ impl PointerHandler { { let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); - self.on_pointer_press = Some(canvas_common.add_user_event( + self.on_pointer_press = Some(canvas_common.add_transient_event( "pointerdown", move |event: PointerEvent| { if prevent_default { @@ -137,8 +135,7 @@ impl PointerHandler { touch_handler( modifiers, event.pointer_id(), - event::touch_position(&event, &canvas) - .to_physical(super::scale_factor(&window)), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ); } @@ -171,12 +168,7 @@ impl PointerHandler { prevent_default: bool, ) where MOD: 'static + FnMut(ModifiersState), - M: 'static - + FnMut( - ModifiersState, - i32, - &mut dyn Iterator, PhysicalPosition)>, - ), + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), T: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), @@ -226,28 +218,18 @@ impl PointerHandler { // pointer move event let scale = super::scale_factor(&window); match pointer_type.as_str() { - "mouse" => { - let mut delta = event::MouseDelta::init(&window, &event); - - mouse_handler( - modifiers, - id, - &mut event::pointer_move_event(event).map(|event| { - let position = event::mouse_position(&event).to_physical(scale); - let delta = delta - .delta(&event) - .to_physical(super::scale_factor(&window)); - - (position, delta) - }), - ) - } + "mouse" => mouse_handler( + modifiers, + id, + &mut event::pointer_move_event(event) + .map(|event| event::mouse_position(&event).to_physical(scale)), + ), "touch" => touch_handler( modifiers, id, &mut event::pointer_move_event(event).map(|event| { ( - event::touch_position(&event, &canvas).to_physical(scale), + event::mouse_position(&event).to_physical(scale), Force::Normalized(event.pressure() as f64), ) }), @@ -263,15 +245,13 @@ impl PointerHandler { F: 'static + FnMut(i32, PhysicalPosition, Force), { let window = canvas_common.window.clone(); - let canvas = canvas_common.raw.clone(); self.on_touch_cancel = Some(canvas_common.add_event( "pointercancel", move |event: PointerEvent| { if event.pointer_type() == "touch" { handler( event.pointer_id(), - event::touch_position(&event, &canvas) - .to_physical(super::scale_factor(&window)), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ); } diff --git a/src/platform_impl/web/web_sys/resize_scaling.rs b/src/platform_impl/web/web_sys/resize_scaling.rs index d80f94a842..9897328202 100644 --- a/src/platform_impl/web/web_sys/resize_scaling.rs +++ b/src/platform_impl/web/web_sys/resize_scaling.rs @@ -1,10 +1,10 @@ use js_sys::{Array, Object}; -use once_cell::unsync::Lazy; use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ - HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions, - ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window, + CssStyleDeclaration, Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, + ResizeObserverBoxOptions, ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, + Window, }; use crate::dpi::{LogicalSize, PhysicalSize}; @@ -20,7 +20,9 @@ pub struct ResizeScaleHandle(Rc>); impl ResizeScaleHandle { pub(crate) fn new( window: Window, + document: Document, canvas: HtmlCanvasElement, + style: CssStyleDeclaration, scale_handler: S, resize_handler: R, ) -> Self @@ -30,7 +32,9 @@ impl ResizeScaleHandle { { Self(ResizeScaleInternal::new( window, + document, canvas, + style, scale_handler, resize_handler, )) @@ -45,7 +49,9 @@ impl ResizeScaleHandle { /// changes of the `devicePixelRatio`. struct ResizeScaleInternal { window: Window, + document: Document, canvas: HtmlCanvasElement, + style: CssStyleDeclaration, mql: MediaQueryListHandle, observer: ResizeObserver, _observer_closure: Closure, @@ -57,7 +63,9 @@ struct ResizeScaleInternal { impl ResizeScaleInternal { fn new( window: Window, + document: Document, canvas: HtmlCanvasElement, + style: CssStyleDeclaration, scale_handler: S, resize_handler: R, ) -> Rc> @@ -80,7 +88,7 @@ impl ResizeScaleInternal { if let Some(rc_self) = weak_self.upgrade() { let mut this = rc_self.borrow_mut(); - let size = Self::process_entry(&this.window, &this.canvas, entries); + let size = this.process_entry(entries); if this.notify_scale.replace(false) { let scale = backend::scale_factor(&this.window); @@ -94,7 +102,9 @@ impl ResizeScaleInternal { RefCell::new(Self { window, + document, canvas, + style, mql, observer, _observer_closure: observer_closure, @@ -116,7 +126,7 @@ impl ResizeScaleInternal { (-webkit-device-pixel-ratio: {current_scale})", ); let mql = MediaQueryListHandle::new(window, &media_query, closure); - assert!( + debug_assert!( mql.mql().matches(), "created media query doesn't match, {current_scale} != {}", super::scale_factor(window) @@ -142,17 +152,8 @@ impl ResizeScaleInternal { } fn notify(&mut self) { - let style = self - .window - .get_computed_style(&self.canvas) - .expect("Failed to obtain computed style") - // this can't fail: we aren't using a pseudo-element - .expect("Invalid pseudo-element"); - - let document = self.window.document().expect("Failed to obtain document"); - - if !document.contains(Some(&self.canvas)) - || style.get_property_value("display").unwrap() == "none" + if !self.document.contains(Some(&self.canvas)) + || self.style.get_property_value("display").unwrap() == "none" { let size = PhysicalSize::new(0, 0); @@ -175,19 +176,19 @@ impl ResizeScaleInternal { } let mut size = LogicalSize::new( - backend::style_size_property(&style, "width"), - backend::style_size_property(&style, "height"), + backend::style_size_property(&self.style, "width"), + backend::style_size_property(&self.style, "height"), ); - if style.get_property_value("box-sizing").unwrap() == "border-box" { - size.width -= backend::style_size_property(&style, "border-left-width") - + backend::style_size_property(&style, "border-right-width") - + backend::style_size_property(&style, "padding-left") - + backend::style_size_property(&style, "padding-right"); - size.height -= backend::style_size_property(&style, "border-top-width") - + backend::style_size_property(&style, "border-bottom-width") - + backend::style_size_property(&style, "padding-top") - + backend::style_size_property(&style, "padding-bottom"); + if self.style.get_property_value("box-sizing").unwrap() == "border-box" { + size.width -= backend::style_size_property(&self.style, "border-left-width") + + backend::style_size_property(&self.style, "border-right-width") + + backend::style_size_property(&self.style, "padding-left") + + backend::style_size_property(&self.style, "padding-right"); + size.height -= backend::style_size_property(&self.style, "border-top-width") + + backend::style_size_property(&self.style, "border-bottom-width") + + backend::style_size_property(&self.style, "padding-top") + + backend::style_size_property(&self.style, "padding-bottom"); } let size = size.to_physical(backend::scale_factor(&self.window)); @@ -229,11 +230,7 @@ impl ResizeScaleInternal { this.notify(); } - fn process_entry( - window: &Window, - canvas: &HtmlCanvasElement, - entries: Array, - ) -> PhysicalSize { + fn process_entry(&self, entries: Array) -> PhysicalSize { let entry: ResizeObserverEntry = entries.get(0).unchecked_into(); // Safari doesn't support `devicePixelContentBoxSize` @@ -241,7 +238,7 @@ impl ResizeScaleInternal { let rect = entry.content_rect(); return LogicalSize::new(rect.width(), rect.height()) - .to_physical(backend::scale_factor(window)); + .to_physical(backend::scale_factor(&self.window)); } let entry: ResizeObserverSize = entry @@ -249,13 +246,8 @@ impl ResizeScaleInternal { .get(0) .unchecked_into(); - let style = window - .get_computed_style(canvas) - .expect("Failed to get computed style of canvas") - // this can only be empty if we provided an invalid `pseudoElt` - .expect("`getComputedStyle` can not be empty"); - - let writing_mode = style + let writing_mode = self + .style .get_property_value("writing-mode") .expect("`writing-mode` is a valid CSS property"); @@ -299,7 +291,7 @@ impl Drop for ResizeScaleInternal { // See . pub fn has_device_pixel_support() -> bool { thread_local! { - static DEVICE_PIXEL_SUPPORT: Lazy = Lazy::new(|| { + static DEVICE_PIXEL_SUPPORT: bool = { #[wasm_bindgen] extern "C" { type ResizeObserverEntryExt; @@ -314,8 +306,8 @@ pub fn has_device_pixel_support() -> bool { &JsValue::from_str("devicePixelContentBoxSize"), ); !descriptor.is_undefined() - }); + }; } - DEVICE_PIXEL_SUPPORT.with(|support| **support) + DEVICE_PIXEL_SUPPORT.with(|support| *support) } diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 0000000000..0682b3b85a --- /dev/null +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -0,0 +1,277 @@ +use js_sys::{Function, Object, Promise, Reflect}; +use once_cell::unsync::OnceCell; +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; + +use crate::platform::web::PollStrategy; + +#[derive(Debug)] +pub struct Schedule { + _closure: Closure, + inner: Inner, +} + +#[derive(Debug)] +enum Inner { + Scheduler { + controller: AbortController, + }, + IdleCallback { + window: web_sys::Window, + handle: u32, + }, + Timeout { + window: web_sys::Window, + handle: i32, + port: MessagePort, + _timeout_closure: Closure, + }, +} + +impl Schedule { + pub fn new(strategy: PollStrategy, window: &web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + if strategy == PollStrategy::Scheduler && has_scheduler_support(window) { + Self::new_scheduler(window, f, None) + } else if strategy == PollStrategy::IdleCallback && has_idle_callback_support(window) { + Self::new_idle_callback(window.clone(), f) + } else { + Self::new_timeout(window.clone(), f, None) + } + } + + pub fn new_with_duration(window: &web_sys::Window, f: F, duration: Duration) -> Schedule + where + F: 'static + FnMut(), + { + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) + } else { + Self::new_timeout(window.clone(), f, Some(duration)) + } + } + + fn new_scheduler(window: &web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let window: &WindowSupportExt = window.unchecked_ref(); + let scheduler = window.scheduler(); + + let closure = Closure::new(f); + let mut options = SchedulerPostTaskOptions::new(); + let controller = AbortController::new().expect("Failed to create `AbortController`"); + options.signal(&controller.signal()); + + if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .checked_mul(1000) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration).into())) + .unwrap_or(u64::MAX); + + options.delay(duration as f64); + } + + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = scheduler + .post_task_with_options(closure.as_ref().unchecked_ref(), &options) + .catch(handler); + }); + + Schedule { + _closure: closure, + inner: Inner::Scheduler { controller }, + } + } + + fn new_idle_callback(window: web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + let closure = Closure::new(f); + let handle = window + .request_idle_callback(closure.as_ref().unchecked_ref()) + .expect("Failed to request idle callback"); + + Schedule { + _closure: closure, + inner: Inner::IdleCallback { window, handle }, + } + } + + fn new_timeout(window: web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + let port_2 = channel.port2(); + let timeout_closure = Closure::new(move || { + port_2 + .post_message(&JsValue::UNDEFINED) + .expect("Failed to send message") + }); + let handle = if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: i32| secs.checked_mul(1000)) + .and_then(|secs: i32| { + let millis: i32 = duration_millis_ceil(duration) + .try_into() + .expect("millis are somehow bigger then 1K"); + secs.checked_add(millis) + }) + .unwrap_or(i32::MAX); + + window.set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_closure.as_ref().unchecked_ref(), + duration, + ) + } else { + window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref()) + } + .expect("Failed to set timeout"); + + Schedule { + _closure: closure, + inner: Inner::Timeout { + window, + handle, + port: port_1, + _timeout_closure: timeout_closure, + }, + } + } +} + +impl Drop for Schedule { + fn drop(&mut self) { + match &self.inner { + Inner::Scheduler { controller, .. } => controller.abort(), + Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle), + Inner::Timeout { + window, + handle, + port, + .. + } => { + window.clear_timeout_with_handle(*handle); + port.close(); + port.set_onmessage(None); + } + } + } +} + +// TODO: Replace with `u32::div_ceil()` when we hit Rust v1.73. +fn duration_millis_ceil(duration: Duration) -> u32 { + let micros = duration.subsec_micros(); + + // From . + let d = micros / 1000; + let r = micros % 1000; + if r > 0 && 1000 > 0 { + d + 1 + } else { + d + } +} + +fn has_scheduler_support(window: &web_sys::Window) -> bool { + thread_local! { + static SCHEDULER_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + SCHEDULER_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type SchedulerSupport; + + #[wasm_bindgen(method, getter, js_name = scheduler)] + fn has_scheduler(this: &SchedulerSupport) -> JsValue; + } + + let support: &SchedulerSupport = window.unchecked_ref(); + + !support.has_scheduler().is_undefined() + }) + }) +} + +fn has_idle_callback_support(window: &web_sys::Window) -> bool { + thread_local! { + static IDLE_CALLBACK_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + IDLE_CALLBACK_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type IdleCallbackSupport; + + #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] + fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; + } + + let support: &IdleCallbackSupport = window.unchecked_ref(); + !support.has_request_idle_callback().is_undefined() + }) + }) +} + +#[wasm_bindgen] +extern "C" { + type WindowSupportExt; + + #[wasm_bindgen(method, getter)] + fn scheduler(this: &WindowSupportExt) -> Scheduler; + + type Scheduler; + + #[wasm_bindgen(method, js_name = postTask)] + fn post_task_with_options( + this: &Scheduler, + callback: &Function, + options: &SchedulerPostTaskOptions, + ) -> Promise; + + type SchedulerPostTaskOptions; +} + +impl SchedulerPostTaskOptions { + fn new() -> Self { + Object::new().unchecked_into() + } + + fn delay(&mut self, val: f64) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `delay` property"); + self + } + + fn signal(&mut self, val: &AbortSignal) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `signal` property"); + self + } +} diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs deleted file mode 100644 index 1273a84d0a..0000000000 --- a/src/platform_impl/web/web_sys/timeout.rs +++ /dev/null @@ -1,124 +0,0 @@ -use once_cell::unsync::OnceCell; -use std::cell::Cell; -use std::rc::Rc; -use std::time::Duration; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; - -#[derive(Debug)] -pub struct Timeout { - window: web_sys::Window, - handle: i32, - _closure: Closure, -} - -impl Timeout { - pub fn new(window: web_sys::Window, f: F, duration: Duration) -> Timeout - where - F: 'static + FnMut(), - { - let closure = Closure::wrap(Box::new(f) as Box); - - let handle = window - .set_timeout_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - duration.as_millis() as i32, - ) - .expect("Failed to set timeout"); - - Timeout { - window, - handle, - _closure: closure, - } - } -} - -impl Drop for Timeout { - fn drop(&mut self) { - self.window.clear_timeout_with_handle(self.handle); - } -} - -#[derive(Debug)] -pub struct IdleCallback { - window: web_sys::Window, - handle: Handle, - fired: Rc>, - _closure: Closure, -} - -#[derive(Clone, Copy, Debug)] -enum Handle { - IdleCallback(u32), - Timeout(i32), -} - -impl IdleCallback { - pub fn new(window: web_sys::Window, mut f: F) -> IdleCallback - where - F: 'static + FnMut(), - { - let fired = Rc::new(Cell::new(false)); - let c_fired = fired.clone(); - let closure = Closure::wrap(Box::new(move || { - (*c_fired).set(true); - f(); - }) as Box); - - let handle = if has_idle_callback_support(&window) { - Handle::IdleCallback( - window - .request_idle_callback(closure.as_ref().unchecked_ref()) - .expect("Failed to request idle callback"), - ) - } else { - Handle::Timeout( - window - .set_timeout_with_callback(closure.as_ref().unchecked_ref()) - .expect("Failed to set timeout"), - ) - }; - - IdleCallback { - window, - handle, - fired, - _closure: closure, - } - } -} - -impl Drop for IdleCallback { - fn drop(&mut self) { - if !(*self.fired).get() { - match self.handle { - Handle::IdleCallback(handle) => self.window.cancel_idle_callback(handle), - Handle::Timeout(handle) => self.window.clear_timeout_with_handle(handle), - } - } - } -} - -fn has_idle_callback_support(window: &web_sys::Window) -> bool { - thread_local! { - static IDLE_CALLBACK_SUPPORT: OnceCell = OnceCell::new(); - } - - IDLE_CALLBACK_SUPPORT.with(|support| { - *support.get_or_init(|| { - #[wasm_bindgen] - extern "C" { - type IdleCallbackSupport; - - #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] - fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; - } - - let support: &IdleCallbackSupport = window.unchecked_ref(); - !support.has_request_idle_callback().is_undefined() - }) - }) -} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 0d577b8656..7d54528702 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -5,31 +5,26 @@ use crate::window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, }; +use crate::SendSyncWrapper; -use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; use web_sys::HtmlCanvasElement; use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; use std::cell::RefCell; -use std::collections::vec_deque::IntoIter as VecDequeIter; use std::collections::VecDeque; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; pub struct Window { - id: WindowId, - has_focus: Arc, - pub inner: Dispatcher, + inner: Dispatcher, } pub struct Inner { + id: WindowId, pub window: web_sys::Window, canvas: Rc>, previous_pointer: RefCell<&'static str>, - register_redraw_request: Box, destroy_fn: Option>, } @@ -39,63 +34,65 @@ impl Window { attr: WindowAttributes, platform_attr: PlatformSpecificWindowBuilderAttributes, ) -> Result { - let runner = target.runner.clone(); - let id = target.generate_id(); let prevent_default = platform_attr.prevent_default; let window = target.runner.window(); - let canvas = backend::Canvas::create(id, window.clone(), &attr, platform_attr)?; + let document = target.runner.document(); + let canvas = + backend::Canvas::create(id, window.clone(), document.clone(), &attr, platform_attr)?; let canvas = Rc::new(RefCell::new(canvas)); - let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - target.register(&canvas, id, prevent_default); let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); - let has_focus = canvas.borrow().has_focus.clone(); - let window = Window { + let inner = Inner { id, - has_focus, - inner: Dispatcher::new(Inner { - window: window.clone(), - canvas, - previous_pointer: RefCell::new("auto"), - register_redraw_request, - destroy_fn: Some(destroy_fn), - }) - .unwrap(), + window: window.clone(), + canvas, + previous_pointer: RefCell::new("auto"), + destroy_fn: Some(destroy_fn), }; - window.set_title(&attr.title); - window.set_maximized(attr.maximized); - window.set_visible(attr.visible); - window.set_window_icon(attr.window_icon); + inner.set_title(&attr.title); + inner.set_maximized(attr.maximized); + inner.set_visible(attr.visible); + inner.set_window_icon(attr.window_icon); + + let canvas = Rc::downgrade(&inner.canvas); + let (dispatcher, runner) = Dispatcher::new(inner).unwrap(); + target.runner.add_canvas(RootWI(id), canvas, runner); + + Ok(Window { inner: dispatcher }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + self.inner.dispatch(f) + } - Ok(window) + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.queue(f) } pub fn canvas(&self) -> Option { - self.inner.with(|inner| inner.canvas.borrow().raw().clone()) + self.inner + .value() + .map(|inner| inner.canvas.borrow().raw().clone()) } +} +impl Inner { pub fn set_title(&self, title: &str) { - if self - .inner - .with(|inner| inner.canvas.borrow().set_attribute("alt", title)) - .is_none() - { - let title = title.to_owned(); - self.inner - .dispatch(move |inner| inner.canvas.borrow().set_attribute("alt", &title)); - } + self.canvas.borrow().set_attribute("alt", title) } pub fn set_transparent(&self, _transparent: bool) {} + pub fn set_blur(&self, _blur: bool) {} + pub fn set_visible(&self, _visible: bool) { // Intentionally a no-op } @@ -106,18 +103,17 @@ impl Window { } pub fn request_redraw(&self) { - self.inner - .dispatch(|inner| (inner.register_redraw_request)()); + self.canvas.borrow().request_animation_frame(); } + pub fn pre_present_notify(&self) {} + pub fn outer_position(&self) -> Result, NotSupportedError> { - self.inner.queue(|inner| { - Ok(inner - .canvas - .borrow() - .position() - .to_physical(inner.scale_factor())) - }) + Ok(self + .canvas + .borrow() + .position() + .to_physical(self.scale_factor())) } pub fn inner_position(&self) -> Result, NotSupportedError> { @@ -126,19 +122,15 @@ impl Window { } pub fn set_outer_position(&self, position: Position) { - self.inner.dispatch(move |inner| { - let position = position.to_logical::(inner.scale_factor()); + let canvas = self.canvas.borrow(); + let position = position.to_logical::(self.scale_factor()); - let canvas = inner.canvas.borrow(); - canvas.set_attribute("position", "fixed"); - canvas.set_attribute("left", &position.x.to_string()); - canvas.set_attribute("top", &position.y.to_string()); - }); + backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position) } #[inline] pub fn inner_size(&self) -> PhysicalSize { - self.inner.queue(|inner| inner.canvas.borrow().inner_size()) + self.canvas.borrow().inner_size() } #[inline] @@ -148,22 +140,25 @@ impl Window { } #[inline] - pub fn set_inner_size(&self, size: Size) { - self.inner.dispatch(move |inner| { - let size = size.to_logical(inner.scale_factor()); - let canvas = inner.canvas.borrow(); - backend::set_canvas_size(canvas.window(), canvas.raw(), size); - }); + pub fn request_inner_size(&self, size: Size) -> Option> { + let size = size.to_logical(self.scale_factor()); + let canvas = self.canvas.borrow(); + backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size); + None } #[inline] - pub fn set_min_inner_size(&self, _dimensions: Option) { - // Intentionally a no-op: users can't resize canvas elements + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_min_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) } #[inline] - pub fn set_max_inner_size(&self, _dimensions: Option) { - // Intentionally a no-op: users can't resize canvas elements + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_max_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) } #[inline] @@ -195,19 +190,13 @@ impl Window { #[inline] pub fn scale_factor(&self) -> f64 { - self.inner.queue(|inner| inner.scale_factor()) + super::backend::scale_factor(&self.window) } #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - self.inner.dispatch(move |inner| { - *inner.previous_pointer.borrow_mut() = cursor.name(); - backend::set_canvas_style_property( - inner.canvas.borrow().raw(), - "cursor", - cursor.name(), - ); - }); + *self.previous_pointer.borrow_mut() = cursor.name(); + backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", cursor.name()); } #[inline] @@ -217,35 +206,31 @@ impl Window { #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - self.inner.queue(move |inner| { - let lock = match mode { - CursorGrabMode::None => false, - CursorGrabMode::Locked => true, - CursorGrabMode::Confined => { - return Err(ExternalError::NotSupported(NotSupportedError::new())) - } - }; - - inner - .canvas - .borrow() - .set_cursor_lock(lock) - .map_err(ExternalError::Os) - }) + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + + self.canvas + .borrow() + .set_cursor_lock(lock) + .map_err(ExternalError::Os) } #[inline] pub fn set_cursor_visible(&self, visible: bool) { - self.inner.dispatch(move |inner| { - if !visible { - inner.canvas.borrow().set_attribute("cursor", "none"); - } else { - inner - .canvas - .borrow() - .set_attribute("cursor", &inner.previous_pointer.borrow()); - } - }); + if !visible { + backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", "none"); + } else { + backend::set_canvas_style_property( + self.canvas.borrow().raw(), + "cursor", + &self.previous_pointer.borrow(), + ); + } } #[inline] @@ -258,6 +243,9 @@ impl Window { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + #[inline] pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) @@ -287,24 +275,22 @@ impl Window { #[inline] pub(crate) fn fullscreen(&self) -> Option { - self.inner.queue(|inner| { - if inner.canvas.borrow().is_fullscreen() { - Some(Fullscreen::Borderless(Some(MonitorHandle))) - } else { - None - } - }) + if self.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(None)) + } else { + None + } } #[inline] pub(crate) fn set_fullscreen(&self, fullscreen: Option) { - self.inner.dispatch(move |inner| { - if fullscreen.is_some() { - inner.canvas.borrow().request_fullscreen(); - } else if inner.canvas.borrow().is_fullscreen() { - backend::exit_fullscreen(&inner.window); - } - }); + let canvas = &self.canvas.borrow(); + + if fullscreen.is_some() { + canvas.request_fullscreen(); + } else { + canvas.exit_fullscreen() + } } #[inline] @@ -343,7 +329,7 @@ impl Window { #[inline] pub fn focus_window(&self) { - // Currently a no-op as it does not seem there is good support for this on web + let _ = self.canvas.borrow().raw().focus(); } #[inline] @@ -353,17 +339,17 @@ impl Window { #[inline] pub fn current_monitor(&self) -> Option { - Some(MonitorHandle) + None } #[inline] - pub fn available_monitors(&self) -> VecDequeIter { - VecDeque::new().into_iter() + pub fn available_monitors(&self) -> VecDeque { + VecDeque::new() } #[inline] pub fn primary_monitor(&self) -> Option { - Some(MonitorHandle) + None } #[inline] @@ -371,16 +357,43 @@ impl Window { self.id } + #[cfg(feature = "rwh_04")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let mut window_handle = WebWindowHandle::empty(); + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WebHandle::empty(); window_handle.id = self.id.0; - RawWindowHandle::Web(window_handle) + rwh_04::RawWindowHandle::Web(window_handle) } + #[cfg(feature = "rwh_05")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Web(WebDisplayHandle::empty()) + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WebWindowHandle::empty(); + window_handle.id = self.id.0; + rwh_05::RawWindowHandle::Web(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let window_handle = rwh_06::WebWindowHandle::new(self.id.0); + Ok(rwh_06::RawWindowHandle::Web(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web( + rwh_06::WebDisplayHandle::new(), + )) } #[inline] @@ -388,20 +401,20 @@ impl Window { #[inline] pub fn theme(&self) -> Option { - self.inner.queue(|inner| { - backend::is_dark_mode(&inner.window).map(|is_dark_mode| { - if is_dark_mode { - Theme::Dark - } else { - Theme::Light - } - }) + backend::is_dark_mode(&self.window).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } }) } + pub fn set_content_protected(&self, _protected: bool) {} + #[inline] pub fn has_focus(&self) -> bool { - self.has_focus.load(Ordering::Relaxed) + self.canvas.borrow().has_focus.get() } pub fn title(&self) -> String { @@ -413,23 +426,13 @@ impl Window { } } -impl Drop for Window { +impl Drop for Inner { fn drop(&mut self) { - self.inner.dispatch_mut(|inner| { - if let Some(destroy_fn) = inner.destroy_fn.take() { - destroy_fn(); - } - }); - } -} - -impl Inner { - #[inline] - pub fn scale_factor(&self) -> f64 { - super::backend::scale_factor(&self.window) + if let Some(destroy_fn) = self.destroy_fn.take() { + destroy_fn(); + } } } - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId(pub(crate) u32); @@ -453,17 +456,19 @@ impl From for WindowId { #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { - pub(crate) canvas: Option, + pub(crate) canvas: SendSyncWrapper>, pub(crate) prevent_default: bool, pub(crate) focusable: bool, + pub(crate) append: bool, } impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { - canvas: None, + canvas: SendSyncWrapper(None), prevent_default: true, focusable: true, + append: false, } } } diff --git a/src/platform_impl/windows/dpi.rs b/src/platform_impl/windows/dpi.rs index e1a5f0aed2..c4ed08a6db 100644 --- a/src/platform_impl/windows/dpi.rs +++ b/src/platform_impl/windows/dpi.rs @@ -9,8 +9,8 @@ use windows_sys::Win32::{ }, UI::{ HiDpi::{ - DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, MDT_EFFECTIVE_DPI, - PROCESS_PER_MONITOR_DPI_AWARE, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE, }, WindowsAndMessaging::IsProcessDPIAware, }, @@ -21,8 +21,6 @@ use crate::platform_impl::platform::util::{ SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT, }; -const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2: DPI_AWARENESS_CONTEXT = -4; - pub fn become_dpi_aware() { static ENABLE_DPI_AWARENESS: Once = Once::new(); ENABLE_DPI_AWARENESS.call_once(|| { @@ -78,36 +76,36 @@ pub fn dpi_to_scale_factor(dpi: u32) -> f64 { } pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { - let hdc = GetDC(hwnd); + let hdc = unsafe { GetDC(hwnd) }; if hdc == 0 { panic!("[winit] `GetDC` returned null!"); } if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { // We are on Windows 10 Anniversary Update (1607) or later. - match GetDpiForWindow(hwnd) { + match unsafe { GetDpiForWindow(hwnd) } { 0 => BASE_DPI, // 0 is returned if hwnd is invalid dpi => dpi, } } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { // We are on Windows 8.1 or later. - let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; if monitor == 0 { return BASE_DPI; } let mut dpi_x = 0; let mut dpi_y = 0; - if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK { + if unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) } == S_OK { dpi_x } else { BASE_DPI } } else { // We are on Vista or later. - if IsProcessDPIAware() != false.into() { + if unsafe { IsProcessDPIAware() } != false.into() { // If the process is DPI aware, then scaling must be handled by the application using // this DPI value. - GetDeviceCaps(hdc, LOGPIXELSX) as u32 + unsafe { GetDeviceCaps(hdc, LOGPIXELSX) as u32 } } else { // If the process is DPI unaware, then scaling is performed by the OS; we thus return // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the diff --git a/src/platform_impl/windows/drop_handler.rs b/src/platform_impl/windows/drop_handler.rs index 5de7cd0c7f..1326fa9986 100644 --- a/src/platform_impl/windows/drop_handler.rs +++ b/src/platform_impl/windows/drop_handler.rs @@ -30,7 +30,7 @@ pub struct FileDropHandlerData { pub interface: IDropTarget, refcount: AtomicUsize, window: HWND, - send_event: Box)>, + send_event: Box)>, cursor_effect: u32, hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ } @@ -41,7 +41,7 @@ pub struct FileDropHandler { #[allow(non_snake_case)] impl FileDropHandler { - pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { + pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { let data = Box::new(FileDropHandlerData { interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl, @@ -69,17 +69,17 @@ impl FileDropHandler { } pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { - let drop_handler_data = Self::from_interface(this); + let drop_handler_data = unsafe { Self::from_interface(this) }; let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; count as u32 } pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { - let drop_handler = Self::from_interface(this); + let drop_handler = unsafe { Self::from_interface(this) }; let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { // Destroy the underlying data - drop(Box::from_raw(drop_handler as *mut FileDropHandlerData)); + drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); } count as u32 } @@ -92,20 +92,24 @@ impl FileDropHandler { pdwEffect: *mut u32, ) -> HRESULT { use crate::event::WindowEvent::HoveredFile; - let drop_handler = Self::from_interface(this); - let hdrop = Self::iterate_filenames(pDataObj, |filename| { - drop_handler.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(drop_handler.window)), - event: HoveredFile(filename), - }); - }); + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFile(filename), + }); + }) + }; drop_handler.hovered_is_valid = hdrop.is_some(); drop_handler.cursor_effect = if drop_handler.hovered_is_valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; - *pdwEffect = drop_handler.cursor_effect; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } S_OK } @@ -116,15 +120,17 @@ impl FileDropHandler { _pt: *const POINTL, pdwEffect: *mut u32, ) -> HRESULT { - let drop_handler = Self::from_interface(this); - *pdwEffect = drop_handler.cursor_effect; + let drop_handler = unsafe { Self::from_interface(this) }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } S_OK } pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { use crate::event::WindowEvent::HoveredFileCancelled; - let drop_handler = Self::from_interface(this); + let drop_handler = unsafe { Self::from_interface(this) }; if drop_handler.hovered_is_valid { drop_handler.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(drop_handler.window)), @@ -143,22 +149,24 @@ impl FileDropHandler { _pdwEffect: *mut u32, ) -> HRESULT { use crate::event::WindowEvent::DroppedFile; - let drop_handler = Self::from_interface(this); - let hdrop = Self::iterate_filenames(pDataObj, |filename| { - drop_handler.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(drop_handler.window)), - event: DroppedFile(filename), - }); - }); + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: DroppedFile(filename), + }); + }) + }; if let Some(hdrop) = hdrop { - DragFinish(hdrop); + unsafe { DragFinish(hdrop) }; } S_OK } unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { - &mut *(this as *mut _) + unsafe { &mut *(this as *mut _) } } unsafe fn iterate_filenames(data_obj: *const IDataObject, callback: F) -> Option @@ -173,26 +181,29 @@ impl FileDropHandler { tymed: TYMED_HGLOBAL as u32, }; - let mut medium = std::mem::zeroed(); - let get_data_fn = (*(*data_obj).cast::()).GetData; - let get_data_result = get_data_fn(data_obj as *mut _, &drop_format, &mut medium); + let mut medium = unsafe { std::mem::zeroed() }; + let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; + let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; if get_data_result >= 0 { - let hdrop = medium.Anonymous.hGlobal; + let hdrop = unsafe { medium.Anonymous.hGlobal }; // The second parameter (0xFFFFFFFF) instructs the function to return the item count - let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, ptr::null_mut(), 0); + let item_count = unsafe { DragQueryFileW(hdrop, 0xFFFFFFFF, ptr::null_mut(), 0) }; for i in 0..item_count { // Get the length of the path string NOT including the terminating null character. // Previously, this was using a fixed size array of MAX_PATH length, but the // Windows API allows longer paths under certain circumstances. - let character_count = DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize; + let character_count = + unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize }; let str_len = character_count + 1; // Fill path_buf with the null-terminated file name let mut path_buf = Vec::with_capacity(str_len); - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); - path_buf.set_len(str_len); + unsafe { + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); + path_buf.set_len(str_len); + } callback(OsString::from_wide(&path_buf[0..character_count]).into()); } @@ -211,7 +222,7 @@ impl FileDropHandler { } impl FileDropHandlerData { - fn send_event(&self, event: Event<'static, ()>) { + fn send_event(&self, event: Event<()>) { (self.send_event)(event); } } diff --git a/src/platform_impl/windows/event.rs b/src/platform_impl/windows/event.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index d3b4bf6a82..98ac44866a 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -14,22 +14,18 @@ use std::{ mpsc::{self, Receiver, Sender}, Arc, Mutex, MutexGuard, }, - thread, time::{Duration, Instant}, }; use once_cell::sync::Lazy; -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}, + Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM}, Graphics::Gdi::{ - GetMonitorInfoW, GetUpdateRect, MonitorFromRect, MonitorFromWindow, RedrawWindow, - ScreenToClient, ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, - SC_SCREENSAVE, + GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient, + ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE, }, - Media::{timeBeginPeriod, timeEndPeriod, timeGetDevCaps, TIMECAPS, TIMERR_NOERROR}, System::{ Ole::RevokeDragDrop, Threading::{GetCurrentThreadId, INFINITE}, @@ -39,13 +35,9 @@ use windows_sys::Win32::{ Input::{ Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}, KeyboardAndMouse::{ - MapVirtualKeyW, ReleaseCapture, SetCapture, TrackMouseEvent, MAPVK_VK_TO_VSC_EX, - TME_LEAVE, TRACKMOUSEEVENT, VK_NUMLOCK, VK_SHIFT, - }, - Pointer::{ - POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, POINTER_INFO, - POINTER_PEN_INFO, POINTER_TOUCH_INFO, + ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, }, + Pointer::{POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE}, Touch::{ CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE, TOUCHEVENTF_UP, TOUCHINPUT, @@ -53,13 +45,12 @@ use windows_sys::Win32::{ RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }, WindowsAndMessaging::{ - CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetCursorPos, - GetMenu, GetMessageW, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, - PostMessageW, PostThreadMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, - SetWindowPos, TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, - GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, - NCCALCSIZE_PARAMS, PM_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, + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, + GetCursorPos, GetMenu, GetMessageW, KillTimer, LoadCursorW, PeekMessageW, PostMessageW, + RegisterClassExW, RegisterWindowMessageA, SetCursor, SetTimer, SetWindowPos, + TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, + HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, + PT_TOUCH, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, @@ -78,10 +69,14 @@ use windows_sys::Win32::{ use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, Event, Force, Ime, RawKeyEvent, Touch, TouchPhase, WindowEvent}, + error::EventLoopError, + event::{ + DeviceEvent, Event, Force, Ime, InnerSizeWriter, RawKeyEvent, Touch, TouchPhase, + WindowEvent, + }, event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, - keyboard::{KeyCode, ModifiersState}, - platform::scancode::KeyCodeExtScancode, + keyboard::ModifiersState, + platform::pump_events::PumpStatus, platform_impl::platform::{ dark_mode::try_theme, dpi::{become_dpi_aware, dpi_to_scale_factor}, @@ -99,38 +94,9 @@ use crate::{ }; use runner::{EventLoopRunner, EventLoopRunnerShared}; -use super::window::set_skip_taskbar; +use self::runner::RunnerState; -type GetPointerFrameInfoHistory = unsafe extern "system" fn( - pointerId: u32, - entriesCount: *mut u32, - pointerCount: *mut u32, - pointerInfo: *mut POINTER_INFO, -) -> BOOL; - -type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL; -type GetPointerDeviceRects = unsafe extern "system" fn( - device: HANDLE, - pointerDeviceRect: *mut RECT, - displayRect: *mut RECT, -) -> BOOL; - -type GetPointerTouchInfo = - unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL; - -type GetPointerPenInfo = - unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; - -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)); +use super::window::set_skip_taskbar; pub(crate) struct WindowData { pub window_state: Arc>, @@ -142,7 +108,7 @@ pub(crate) struct WindowData { } impl WindowData { - unsafe fn send_event(&self, event: Event<'_, T>) { + fn send_event(&self, event: Event) { self.event_loop_runner.send_event(event); } @@ -157,7 +123,7 @@ struct ThreadMsgTargetData { } impl ThreadMsgTargetData { - unsafe fn send_event(&self, event: Event<'_, T>) { + fn send_event(&self, event: Event) { self.event_loop_runner.send_event(event); } } @@ -198,7 +164,9 @@ pub struct EventLoopWindowTarget { } impl EventLoop { - pub(crate) fn new(attributes: &mut PlatformSpecificEventLoopAttributes) -> Self { + pub(crate) fn new( + attributes: &mut PlatformSpecificEventLoopAttributes, + ) -> Result { let thread_id = unsafe { GetCurrentThreadId() }; if !attributes.any_thread && thread_id != main_thread_id() { @@ -216,13 +184,7 @@ impl EventLoop { let thread_msg_target = create_event_target_window::(); - thread::Builder::new() - .name("winit wait thread".to_string()) - .spawn(move || wait_thread(thread_id, thread_msg_target)) - .expect("Failed to spawn winit wait thread"); - let wait_thread_id = get_wait_thread_id(); - - let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target, wait_thread_id)); + let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target)); let thread_msg_sender = insert_event_target_window_data::(thread_msg_target, runner_shared.clone()); @@ -231,7 +193,7 @@ impl EventLoop { Default::default(), ); - EventLoop { + Ok(EventLoop { thread_msg_sender, window_target: RootELW { p: EventLoopWindowTarget { @@ -242,45 +204,239 @@ impl EventLoop { _marker: PhantomData, }, msg_hook: attributes.msg_hook.take(), - } + }) } pub fn window_target(&self) -> &RootELW { &self.window_target } - pub fn run(mut self, event_handler: F) -> ! + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> where - F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(Event, &RootELW), { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); + self.run_on_demand(event_handler) } - pub fn run_return(&mut self, mut event_handler: F) -> i32 + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> where - F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + F: FnMut(Event, &RootELW), { - let event_loop_windows_ref = &self.window_target; + { + let runner = &self.window_target.p.runner_shared; + if runner.state() != RunnerState::Uninitialized { + return Err(EventLoopError::AlreadyRunning); + } - unsafe { - self.window_target - .p - .runner_shared - .set_event_handler(move |event, control_flow| { - event_handler(event, event_loop_windows_ref, control_flow) - }); + let event_loop_windows_ref = &self.window_target; + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + unsafe { + runner.set_event_handler(move |event| event_handler(event, event_loop_windows_ref)); + } + } + + let exit_code = loop { + self.wait_and_dispatch_message(None); + + if let Some(code) = self.exit_code() { + break code; + } + + self.dispatch_peeked_messages(); + + if let Some(code) = self.exit_code() { + break code; + } + }; + + let runner = &self.window_target.p.runner_shared; + runner.loop_destroyed(); + + // # Safety + // We assume that this will effectively call `runner.clear_event_handler()` + // to meet the safety requirements for calling `runner.set_event_handler()` above. + runner.reset_runner(); + + if exit_code == 0 { + Ok(()) + } else { + Err(EventLoopError::ExitFailure(exit_code)) + } + } + + pub fn pump_events(&mut self, timeout: Option, mut event_handler: F) -> PumpStatus + where + F: FnMut(Event, &RootELW), + { + { + let runner = &self.window_target.p.runner_shared; + let event_loop_windows_ref = &self.window_target; + + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + // + // Note: we're currently assuming nothing can panic and unwind + // to leave the runner in an unsound state with an associated + // event handler. + unsafe { + runner.set_event_handler(move |event| event_handler(event, event_loop_windows_ref)); + runner.wakeup(); + } + } + + self.wait_and_dispatch_message(timeout); + + if self.exit_code().is_none() { + self.dispatch_peeked_messages(); } let runner = &self.window_target.p.runner_shared; - let exit_code = unsafe { - let mut msg = mem::zeroed(); + let status = if let Some(code) = runner.exit_code() { + runner.loop_destroyed(); + + // Immediately reset the internal state for the loop to allow + // the loop to be run more than once. + runner.reset_runner(); + PumpStatus::Exit(code) + } else { + runner.prepare_wait(); + PumpStatus::Continue + }; + + // We wait until we've checked for an exit status before clearing the + // application callback, in case we need to dispatch a LoopExiting event + // + // # Safety + // This pairs up with our call to `runner.set_event_handler` and ensures + // the application's callback can't be held beyond its lifetime. + runner.clear_event_handler(); + + status + } + + /// Wait for one message and dispatch it, optionally with a timeout + fn wait_and_dispatch_message(&mut self, timeout: Option) { + fn get_msg_with_timeout(msg: &mut MSG, timeout: Option) -> PumpStatus { + unsafe { + // A timeout of None means wait indefinitely (so we don't need to call SetTimer) + let timer_id = timeout.map(|timeout| SetTimer(0, 0, dur2timeout(timeout), None)); + let get_status = GetMessageW(msg, 0, 0, 0); + if let Some(timer_id) = timer_id { + KillTimer(0, timer_id); + } + // A return value of 0 implies `WM_QUIT` + if get_status == 0 { + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + } + } + + /// Fetch the next MSG either via PeekMessage or GetMessage depending on whether the + /// requested timeout is `ZERO` (and so we don't want to block) + /// + /// Returns `None` if if no MSG was read, else a `Continue` or `Exit` status + fn wait_for_msg(msg: &mut MSG, timeout: Option) -> Option { + if timeout == Some(Duration::ZERO) { + unsafe { + if PeekMessageW(msg, 0, 0, 0, PM_REMOVE) != 0 { + Some(PumpStatus::Continue) + } else { + None + } + } + } else { + Some(get_msg_with_timeout(msg, timeout)) + } + } + + let runner = &self.window_target.p.runner_shared; + + // We aim to be consistent with the MacOS backend which has a RunLoop + // observer that will dispatch AboutToWait when about to wait for + // events, and NewEvents after the RunLoop wakes up. + // + // We emulate similar behaviour by treating `GetMessage` as our wait + // point and wake up point (when it returns) and we drain all other + // pending messages via `PeekMessage` until we come back to "wait" via + // `GetMessage` + // + runner.prepare_wait(); + + let control_flow_timeout = match runner.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + let start = Instant::now(); + Some(wait_deadline.saturating_duration_since(start)) + } + }; + let timeout = min_timeout(control_flow_timeout, timeout); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid unitialized memory in Rust + let mut msg = unsafe { mem::zeroed() }; + let msg_status = wait_for_msg(&mut msg, timeout); + + // Before we potentially exit, make sure to consistently emit an event for the wake up + runner.wakeup(); + + match msg_status { + None => {} // No MSG to dispatch + Some(PumpStatus::Exit(code)) => { + runner.set_exit_code(code); + } + Some(PumpStatus::Continue) => { + unsafe { + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + } + } + } + + /// Dispatch all queued messages via `PeekMessageW` + fn dispatch_peeked_messages(&mut self) { + let runner = &self.window_target.p.runner_shared; - runner.poll(); - 'main: loop { - if GetMessageW(&mut msg, 0, 0, 0) == false.into() { - break 'main 0; + // We generally want to continue dispatching all pending messages + // but we also allow dispatching to be interrupted as a means to + // ensure the `pump_events` won't indefinitely block an external + // event loop if there are too many pending events. This interrupt + // flag will be set after dispatching `RedrawRequested` events. + runner.interrupt_msg_dispatch.set(false); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid unitialized memory in Rust + let mut msg = unsafe { mem::zeroed() }; + + loop { + unsafe { + if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) == false.into() { + break; } let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { @@ -292,26 +448,21 @@ impl EventLoop { TranslateMessage(&msg); DispatchMessageW(&msg); } + } - if let Err(payload) = runner.take_panic_error() { - runner.reset_runner(); - panic::resume_unwind(payload); - } + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } - if let ControlFlow::ExitWithCode(code) = runner.control_flow() { - if !runner.handling_events() { - break 'main code; - } - } + if let Some(_code) = runner.exit_code() { + break; } - }; - unsafe { - runner.loop_destroyed(); + if runner.interrupt_msg_dispatch.get() { + break; + } } - - runner.reset_runner(); - exit_code } pub fn create_proxy(&self) -> EventLoopProxy { @@ -320,6 +471,10 @@ impl EventLoop { event_send: self.thread_msg_sender.clone(), } } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } } impl EventLoopWindowTarget { @@ -341,13 +496,47 @@ impl EventLoopWindowTarget { Some(monitor) } - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Windows(WindowsDisplayHandle::empty()) + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) } pub fn listen_device_events(&self, allowed: DeviceEvents) { raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, allowed); } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner_shared.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner_shared.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner_shared.set_exit_code(0) + } + + pub(crate) fn exiting(&self) -> bool { + self.runner_shared.exit_code().is_some() + } + + pub(crate) fn clear_exit(&self) { + self.runner_shared.clear_exit(); + } + + fn exit_code(&self) -> Option { + self.runner_shared.exit_code() + } } /// Returns the id of the main thread. @@ -383,7 +572,7 @@ fn main_thread_id() -> u32 { #[link_section = ".CRT$XCU"] static INIT_MAIN_THREAD_ID: unsafe fn() = { unsafe fn initer() { - MAIN_THREAD_ID = GetCurrentThreadId(); + unsafe { MAIN_THREAD_ID = GetCurrentThreadId() }; } initer }; @@ -391,107 +580,13 @@ fn main_thread_id() -> u32 { unsafe { MAIN_THREAD_ID } } -fn get_wait_thread_id() -> u32 { - unsafe { - let mut msg = mem::zeroed(); - let result = GetMessageW( - &mut msg, - -1, - SEND_WAIT_THREAD_ID_MSG_ID.get(), - SEND_WAIT_THREAD_ID_MSG_ID.get(), - ); - assert_eq!( - msg.message, - SEND_WAIT_THREAD_ID_MSG_ID.get(), - "this shouldn't be possible. please open an issue with Winit. error code: {result}" - ); - msg.lParam as u32 - } -} - -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 { - let mut msg: MSG; - - let cur_thread_id = GetCurrentThreadId(); - PostThreadMessageW( - parent_thread_id, - SEND_WAIT_THREAD_ID_MSG_ID.get(), - 0, - cur_thread_id as LPARAM, - ); - - let mut wait_until_opt = None; - 'main: loop { - // Zeroing out the message ensures that the `WaitUntilInstantBox` doesn't get - // double-freed if `MsgWaitForMultipleObjectsEx` returns early and there aren't - // additional messages to process. - msg = mem::zeroed(); - - if wait_until_opt.is_some() { - if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) != false.into() { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } else if GetMessageW(&mut msg, 0, 0, 0) == false.into() { - break 'main; - } else { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - if msg.message == WAIT_UNTIL_MSG_ID.get() { - wait_until_opt = Some(*WaitUntilInstantBox::from_raw(msg.lParam as *mut _)); - } else if msg.message == CANCEL_WAIT_UNTIL_MSG_ID.get() { - wait_until_opt = None; - } - - if let Some(wait_until) = wait_until_opt { - let now = Instant::now(); - if now < wait_until { - // Windows' scheduler has a default accuracy of several ms. This isn't good enough for - // `WaitUntil`, so we request the Windows scheduler to use a higher accuracy if possible. - // If we couldn't query the timer capabilities, then we use the default resolution. - if let Some(period) = *WAIT_PERIOD_MIN { - timeBeginPeriod(period); - } - // `MsgWaitForMultipleObjects` is bound by the granularity of the scheduler period. - // Because of this, we try to reduce the requested time just enough to undershoot `wait_until` - // by the smallest amount possible, and then we busy loop for the remaining time inside the - // NewEvents message handler. - let resume_reason = MsgWaitForMultipleObjectsEx( - 0, - ptr::null(), - dur2timeout(wait_until - now).saturating_sub(WAIT_PERIOD_MIN.unwrap_or(1)), - QS_ALLEVENTS, - MWMO_INPUTAVAILABLE, - ); - if let Some(period) = *WAIT_PERIOD_MIN { - timeEndPeriod(period); - } - if resume_reason == WAIT_TIMEOUT { - PostMessageW(msg_window_id, PROCESS_NEW_EVENTS_MSG_ID.get(), 0, 0); - wait_until_opt = None; - } - } else { - PostMessageW(msg_window_id, PROCESS_NEW_EVENTS_MSG_ID.get(), 0, 0); - wait_until_opt = None; - } - } - } - } +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) } // Implementation taken from https://github.com/rust-lang/rust/blob/db5476571d9b27c862b95c1e64764b0ac8980e23/src/libstd/sys/windows/mod.rs @@ -601,19 +696,16 @@ impl Clone for EventLoopProxy { impl EventLoopProxy { pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - unsafe { - if PostMessageW(self.target_window, USER_EVENT_MSG_ID.get(), 0, 0) != false.into() { - self.event_send.send(event).ok(); - Ok(()) - } else { - Err(EventLoopClosed(event)) - } - } + self.event_send + .send(event) + .map(|result| { + unsafe { PostMessageW(self.target_window, USER_EVENT_MSG_ID.get(), 0, 0) }; + result + }) + .map_err(|e| EventLoopClosed(e.0)) } } -type WaitUntilInstantBox = Box; - /// A lazily-initialized window message ID. pub struct LazyMessageId { /// The ID. @@ -673,13 +765,6 @@ static USER_EVENT_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::WakeupMsg\0 // WPARAM contains a Box> that must be retrieved with `Box::from_raw`, // and LPARAM is unused. static EXEC_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::ExecMsg\0"); -static PROCESS_NEW_EVENTS_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::ProcessNewEvents\0"); -/// lparam is the wait thread's message id. -static SEND_WAIT_THREAD_ID_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::SendWaitThreadId\0"); -/// lparam points to a `Box` signifying the time `PROCESS_NEW_EVENTS_MSG_ID` should -/// be sent. -static WAIT_UNTIL_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::WaitUntil\0"); -static CANCEL_WAIT_UNTIL_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::CancelWaitUntil\0"); // 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: LazyMessageId = LazyMessageId::new("Winit::DestroyMsg\0"); @@ -774,7 +859,7 @@ fn insert_event_target_window_data( /// the window. unsafe fn capture_mouse(window: HWND, window_state: &mut WindowState) { window_state.mouse.capture_count += 1; - SetCapture(window); + unsafe { SetCapture(window) }; } /// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor @@ -784,7 +869,7 @@ unsafe fn release_mouse(mut window_state: MutexGuard<'_, WindowState>) { if window_state.mouse.capture_count == 0 { // ReleaseCapture() causes a WM_CAPTURECHANGED where we lock the window_state. drop(window_state); - ReleaseCapture(); + unsafe { ReleaseCapture() }; } } @@ -795,74 +880,6 @@ fn normalize_pointer_pressure(pressure: u32) -> Option { } } -/// Flush redraw events for Winit's windows. -/// -/// Winit's API guarantees that all redraw events will be clustered together and dispatched all at -/// once, but the standard Windows message loop doesn't always exhibit that behavior. If multiple -/// windows have had redraws scheduled, but an input event is pushed to the message queue between -/// the `WM_PAINT` call for the first window and the `WM_PAINT` call for the second window, Windows -/// will dispatch the input event immediately instead of flushing all the redraw events. This -/// function explicitly pulls all of Winit's redraw events out of the event queue so that they -/// always all get processed in one fell swoop. -/// -/// Returns `true` if this invocation flushed all the redraw events. If this function is re-entrant, -/// it won't flush the redraw events and will return `false`. -#[must_use] -unsafe fn flush_paint_messages( - except: Option, - runner: &EventLoopRunner, -) -> bool { - if !runner.redrawing() { - runner.main_events_cleared(); - let mut msg = mem::zeroed(); - runner.owned_windows(|redraw_window| { - if Some(redraw_window) == except { - return; - } - - if PeekMessageW( - &mut msg, - redraw_window, - WM_PAINT, - WM_PAINT, - PM_REMOVE | PM_QS_PAINT, - ) == false.into() - { - return; - } - - TranslateMessage(&msg); - DispatchMessageW(&msg); - }); - true - } else { - false - } -} - -unsafe fn process_control_flow(runner: &EventLoopRunner) { - match runner.control_flow() { - ControlFlow::Poll => { - PostMessageW( - runner.thread_msg_target(), - PROCESS_NEW_EVENTS_MSG_ID.get(), - 0, - 0, - ); - } - ControlFlow::Wait => (), - ControlFlow::WaitUntil(until) => { - PostThreadMessageW( - runner.wait_thread_id(), - WAIT_UNTIL_MSG_ID.get(), - 0, - Box::into_raw(WaitUntilInstantBox::new(until)) as isize, - ); - } - ControlFlow::ExitWithCode(_) => (), - } -} - /// Emit a `ModifiersChanged` event whenever modifiers have changed. /// Returns the current modifier state fn update_modifiers(window: HWND, userdata: &WindowData) { @@ -880,12 +897,10 @@ fn update_modifiers(window: HWND, userdata: &WindowData) { // Drop lock drop(window_state); - unsafe { - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: ModifiersChanged(modifiers.into()), - }); - } + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(modifiers.into()), + }); } } @@ -928,18 +943,18 @@ pub(super) unsafe extern "system" fn public_window_callback( wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { - let userdata = super::get_window_long(window, GWL_USERDATA); + let userdata = unsafe { super::get_window_long(window, GWL_USERDATA) }; let userdata_ptr = match (userdata, msg) { (0, WM_NCCREATE) => { - let createstruct = &mut *(lparam as *mut CREATESTRUCTW); - let initdata = &mut *(createstruct.lpCreateParams as *mut InitData<'_, T>); + let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) }; + let initdata = unsafe { &mut *(createstruct.lpCreateParams as *mut InitData<'_, T>) }; - let result = match initdata.on_nccreate(window) { - Some(userdata) => { + let result = match unsafe { initdata.on_nccreate(window) } { + Some(userdata) => unsafe { super::set_window_long(window, GWL_USERDATA, userdata as _); DefWindowProcW(window, msg, wparam, lparam) - } + }, None => -1, // failed to create the window }; @@ -948,24 +963,24 @@ pub(super) unsafe extern "system" fn public_window_callback( // Getting here should quite frankly be impossible, // but we'll make window creation fail here just in case. (0, WM_CREATE) => return -1, - (_, WM_CREATE) => { + (_, WM_CREATE) => unsafe { let createstruct = &mut *(lparam as *mut CREATESTRUCTW); let initdata = createstruct.lpCreateParams; let initdata = &mut *(initdata as *mut InitData<'_, T>); initdata.on_create(); return DefWindowProcW(window, msg, wparam, lparam); - } - (0, _) => return DefWindowProcW(window, msg, wparam, lparam), + }, + (0, _) => return unsafe { DefWindowProcW(window, msg, wparam, lparam) }, _ => userdata as *mut WindowData, }; let (result, userdata_removed, recurse_depth) = { - let userdata = &*(userdata_ptr); + let userdata = unsafe { &*(userdata_ptr) }; userdata.recurse_depth.set(userdata.recurse_depth.get() + 1); - let result = public_window_callback_inner(window, msg, wparam, lparam, userdata); + let result = unsafe { public_window_callback_inner(window, msg, wparam, lparam, userdata) }; let userdata_removed = userdata.userdata_removed.get(); let recurse_depth = userdata.recurse_depth.get() - 1; @@ -975,7 +990,7 @@ pub(super) unsafe extern "system" fn public_window_callback( }; if userdata_removed && recurse_depth == 0 { - drop(Box::from_raw(userdata_ptr)); + drop(unsafe { Box::from_raw(userdata_ptr) }); } result @@ -988,13 +1003,6 @@ unsafe fn public_window_callback_inner( lparam: LPARAM, userdata: &WindowData, ) -> LRESULT { - RedrawWindow( - userdata.event_loop_runner.thread_msg_target(), - ptr::null(), - 0, - RDW_INTERNALPAINT, - ); - let mut result = ProcResult::DefWindowProc(wparam); // Send new modifiers before sending key events. @@ -1043,7 +1051,7 @@ unsafe fn public_window_callback_inner( return; } - let params = &mut *(lparam as *mut NCCALCSIZE_PARAMS); + let params = unsafe { &mut *(lparam as *mut NCCALCSIZE_PARAMS) }; if util::is_maximized(window) { // Limit the window size when maximized to the current monitor. @@ -1052,7 +1060,7 @@ unsafe fn public_window_callback_inner( // Use `MonitorFromRect` instead of `MonitorFromWindow` to select // the correct monitor here. // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549 - let monitor = MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL); + let monitor = unsafe { MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL) }; if let Ok(monitor_info) = monitor::get_monitor_info(monitor) { params.rgrc[0] = monitor_info.monitorInfo.rcWork; } @@ -1086,7 +1094,7 @@ unsafe fn public_window_callback_inner( let mut state = userdata.window_state_lock(); if state.dragging { state.dragging = false; - PostMessageW(window, WM_LBUTTONUP, 0, lparam); + unsafe { PostMessageW(window, WM_LBUTTONUP, 0, lparam) }; } state.set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); @@ -1095,7 +1103,7 @@ unsafe fn public_window_callback_inner( WM_NCLBUTTONDOWN => { if wparam == HTCAPTION as _ { - PostMessageW(window, WM_MOUSEMOVE, 0, lparam); + unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) }; } result = ProcResult::DefWindowProc(wparam); } @@ -1111,42 +1119,47 @@ unsafe fn public_window_callback_inner( WM_DESTROY => { use crate::event::WindowEvent::Destroyed; - RevokeDragDrop(window); + unsafe { RevokeDragDrop(window) }; userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: Destroyed, }); - userdata.event_loop_runner.remove_window(window); result = ProcResult::Value(0); } WM_NCDESTROY => { - super::set_window_long(window, GWL_USERDATA, 0); + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; userdata.userdata_removed.set(true); result = ProcResult::Value(0); } WM_PAINT => { - if userdata.event_loop_runner.should_buffer() { - // this branch can happen in response to `UpdateWindow`, if win32 decides to - // redraw the window outside the normal flow of the event loop. - RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT); - } else { - let managing_redraw = - flush_paint_messages(Some(window), &userdata.event_loop_runner); - userdata.send_event(Event::RedrawRequested(RootWindowId(WindowId(window)))); - if managing_redraw { - userdata.event_loop_runner.redraw_events_cleared(); - process_control_flow(&userdata.event_loop_runner); - } + userdata.window_state_lock().redraw_requested = + userdata.event_loop_runner.should_buffer(); + + // We'll buffer only in response to `UpdateWindow`, if win32 decides to redraw the + // window outside the normal flow of the event loop. This way mark event as handled + // and request a normal redraw with `RedrawWindow`. + if !userdata.event_loop_runner.should_buffer() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::RedrawRequested, + }); } - result = ProcResult::DefWindowProc(wparam); - } + // NOTE: calling `RedrawWindow` during `WM_PAINT` does nothing, since to mark + // `WM_PAINT` as handled we should call the `DefWindowProcW`. Call it and check whether + // user asked for redraw during `RedrawRequested` event handling and request it again + // after marking `WM_PAINT` as handled. + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + if std::mem::take(&mut userdata.window_state_lock().redraw_requested) { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + } WM_WINDOWPOSCHANGING => { let mut window_state = userdata.window_state_lock(); if let Some(ref mut fullscreen) = window_state.fullscreen { - let window_pos = &mut *(lparam as *mut WINDOWPOS); + let window_pos = unsafe { &mut *(lparam as *mut WINDOWPOS) }; let new_rect = RECT { left: window_pos.x, top: window_pos.y, @@ -1184,7 +1197,7 @@ unsafe fn public_window_callback_inner( }; if let Some(new_rect) = new_rect { - let new_monitor = MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL); + let new_monitor = unsafe { MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL) }; match fullscreen { Fullscreen::Borderless(ref mut fullscreen_monitor) => { if new_monitor != 0 @@ -1226,8 +1239,9 @@ unsafe fn public_window_callback_inner( use crate::event::WindowEvent::Moved; let windowpos = lparam as *const WINDOWPOS; - if (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE { - let physical_position = PhysicalPosition::new((*windowpos).x, (*windowpos).y); + if unsafe { (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE } { + let physical_position = + unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: Moved(physical_position), @@ -1290,7 +1304,7 @@ unsafe fn public_window_callback_inner( // 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); + let ime_context = unsafe { ImeContext::current(window) }; if lparam == 0 { userdata.send_event(Event::WindowEvent { @@ -1302,7 +1316,7 @@ unsafe fn public_window_callback_inner( // 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() { + if let Some(text) = unsafe { ime_context.get_composed_text() } { userdata.window_state_lock().ime_state = ImeState::Enabled; userdata.send_event(Event::WindowEvent { @@ -1318,7 +1332,9 @@ unsafe fn public_window_callback_inner( // 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() { + if let Some((text, first, last)) = + unsafe { 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))); @@ -1343,8 +1359,8 @@ unsafe fn public_window_callback_inner( 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() { + let ime_context = unsafe { ImeContext::current(window) }; + if let Some(text) = unsafe { ime_context.get_composed_text() } { userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), @@ -1397,39 +1413,58 @@ unsafe fn public_window_callback_inner( } WM_MOUSEMOVE => { - use crate::event::WindowEvent::{CursorEntered, CursorMoved}; - let mouse_was_outside_window = { + use crate::event::WindowEvent::{CursorEntered, CursorLeft, CursorMoved}; + + let x = super::get_x_lparam(lparam as u32) as i32; + let y = super::get_y_lparam(lparam as u32) as i32; + let position = PhysicalPosition::new(x as f64, y as f64); + + let cursor_moved; + { let mut w = userdata.window_state_lock(); + let mouse_was_inside_window = + w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); - let was_outside_window = !w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); - w.mouse - .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, true)) - .ok(); - was_outside_window - }; + match get_pointer_move_kind(window, mouse_was_inside_window, x, y) { + PointerMoveKind::Enter => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, true)) + .ok(); - if mouse_was_outside_window { - userdata.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: CursorEntered { - device_id: DEVICE_ID, - }, - }); + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorEntered { + device_id: DEVICE_ID, + }, + }); - // Calling TrackMouseEvent in order to receive mouse leave events. - TrackMouseEvent(&mut TRACKMOUSEEVENT { - cbSize: mem::size_of::() as u32, - dwFlags: TME_LEAVE, - hwndTrack: window, - dwHoverTime: HOVER_DEFAULT, - }); - } + // Calling TrackMouseEvent in order to receive mouse leave events. + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: window, + dwHoverTime: HOVER_DEFAULT, + }) + }; + } + PointerMoveKind::Leave => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { + device_id: DEVICE_ID, + }, + }); + } + PointerMoveKind::None => drop(w), + } - let x = super::get_x_lparam(lparam as u32) as f64; - let y = super::get_y_lparam(lparam as u32) as f64; - let position = PhysicalPosition::new(x, y); - let cursor_moved; - { // handle spurious WM_MOUSEMOVE messages // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html @@ -1437,6 +1472,7 @@ unsafe fn public_window_callback_inner( cursor_moved = w.mouse.last_position != Some(position); w.mouse.last_position = Some(position); } + if cursor_moved { update_modifiers(window, userdata); @@ -1475,7 +1511,6 @@ unsafe fn public_window_callback_inner( use crate::event::MouseScrollDelta::LineDelta; let value = (wparam >> 16) as i16; - let value = value as i32; let value = value as f32 / WHEEL_DELTA as f32; update_modifiers(window, userdata); @@ -1496,7 +1531,6 @@ unsafe fn public_window_callback_inner( use crate::event::MouseScrollDelta::LineDelta; let value = (wparam >> 16) as i16; - let value = value as i32; let value = -value as f32 / WHEEL_DELTA as f32; // NOTE: inverted! See https://github.com/rust-windowing/winit/pull/2105/ update_modifiers(window, userdata); @@ -1520,7 +1554,7 @@ unsafe fn public_window_callback_inner( } WM_KEYUP | WM_SYSKEYUP => { - if msg == WM_SYSKEYUP && GetMenu(window) != 0 { + if msg == WM_SYSKEYUP && unsafe { GetMenu(window) != 0 } { // let Windows handle event if the window has a native menu, a modal event loop // is started here on Alt key up. result = ProcResult::DefWindowProc(wparam); @@ -1530,7 +1564,7 @@ unsafe fn public_window_callback_inner( WM_LBUTTONDOWN => { use crate::event::{ElementState::Pressed, MouseButton::Left, WindowEvent::MouseInput}; - capture_mouse(window, &mut userdata.window_state_lock()); + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1550,7 +1584,7 @@ unsafe fn public_window_callback_inner( ElementState::Released, MouseButton::Left, WindowEvent::MouseInput, }; - release_mouse(userdata.window_state_lock()); + unsafe { release_mouse(userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1570,7 +1604,7 @@ unsafe fn public_window_callback_inner( ElementState::Pressed, MouseButton::Right, WindowEvent::MouseInput, }; - capture_mouse(window, &mut userdata.window_state_lock()); + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1590,7 +1624,7 @@ unsafe fn public_window_callback_inner( ElementState::Released, MouseButton::Right, WindowEvent::MouseInput, }; - release_mouse(userdata.window_state_lock()); + unsafe { release_mouse(userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1610,7 +1644,7 @@ unsafe fn public_window_callback_inner( ElementState::Pressed, MouseButton::Middle, WindowEvent::MouseInput, }; - capture_mouse(window, &mut userdata.window_state_lock()); + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1630,7 +1664,7 @@ unsafe fn public_window_callback_inner( ElementState::Released, MouseButton::Middle, WindowEvent::MouseInput, }; - release_mouse(userdata.window_state_lock()); + unsafe { release_mouse(userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1652,7 +1686,7 @@ unsafe fn public_window_callback_inner( }; let xbutton = super::get_xbutton_wparam(wparam as u32); - capture_mouse(window, &mut userdata.window_state_lock()); + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1678,7 +1712,7 @@ unsafe fn public_window_callback_inner( }; let xbutton = super::get_xbutton_wparam(wparam as u32); - release_mouse(userdata.window_state_lock()); + unsafe { release_mouse(userdata.window_state_lock()) }; update_modifiers(window, userdata); @@ -1712,21 +1746,22 @@ unsafe fn public_window_callback_inner( let pcount = super::loword(wparam as u32) as usize; let mut inputs = Vec::with_capacity(pcount); let htouch = lparam; - if GetTouchInputInfo( - htouch, - pcount as u32, - inputs.as_mut_ptr(), - mem::size_of::() as i32, - ) > 0 - { - inputs.set_len(pcount); + if unsafe { + GetTouchInputInfo( + htouch, + pcount as u32, + inputs.as_mut_ptr(), + mem::size_of::() as i32, + ) > 0 + } { + unsafe { inputs.set_len(pcount) }; for input in &inputs { let mut location = POINT { x: input.x / 100, y: input.y / 100, }; - if ScreenToClient(window, &mut location) == false.into() { + if unsafe { ScreenToClient(window, &mut location) } == false.into() { continue; } @@ -1753,7 +1788,7 @@ unsafe fn public_window_callback_inner( }); } } - CloseTouchInputHandle(htouch); + unsafe { CloseTouchInputHandle(htouch) }; result = ProcResult::Value(0); } @@ -1763,19 +1798,21 @@ unsafe fn public_window_callback_inner( Some(SkipPointerFrameMessages), Some(GetPointerDeviceRects), ) = ( - *GET_POINTER_FRAME_INFO_HISTORY, - *SKIP_POINTER_FRAME_MESSAGES, - *GET_POINTER_DEVICE_RECTS, + *util::GET_POINTER_FRAME_INFO_HISTORY, + *util::SKIP_POINTER_FRAME_MESSAGES, + *util::GET_POINTER_DEVICE_RECTS, ) { let pointer_id = super::loword(wparam as u32) as u32; let mut entries_count = 0u32; let mut pointers_count = 0u32; - if GetPointerFrameInfoHistory( - pointer_id, - &mut entries_count, - &mut pointers_count, - ptr::null_mut(), - ) == false.into() + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + ptr::null_mut(), + ) + } == false.into() { result = ProcResult::Value(0); return; @@ -1783,17 +1820,19 @@ unsafe fn public_window_callback_inner( let pointer_info_count = (entries_count * pointers_count) as usize; let mut pointer_infos = Vec::with_capacity(pointer_info_count); - if GetPointerFrameInfoHistory( - pointer_id, - &mut entries_count, - &mut pointers_count, - pointer_infos.as_mut_ptr(), - ) == false.into() + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + pointer_infos.as_mut_ptr(), + ) + } == false.into() { result = ProcResult::Value(0); return; } - pointer_infos.set_len(pointer_info_count); + unsafe { pointer_infos.set_len(pointer_info_count) }; // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getpointerframeinfohistory // The information retrieved appears in reverse chronological order, with the most recent entry in the first @@ -1802,17 +1841,19 @@ unsafe fn public_window_callback_inner( let mut device_rect = mem::MaybeUninit::uninit(); let mut display_rect = mem::MaybeUninit::uninit(); - if GetPointerDeviceRects( - pointer_info.sourceDevice, - device_rect.as_mut_ptr(), - display_rect.as_mut_ptr(), - ) == false.into() + if unsafe { + GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + ) + } == false.into() { continue; } - let device_rect = device_rect.assume_init(); - let display_rect = display_rect.assume_init(); + let device_rect = unsafe { device_rect.assume_init() }; + let display_rect = unsafe { display_rect.assume_init() }; // For the most precise himetric to pixel conversion we calculate the ratio between the resolution // of the display device (pixel) and the touch device (himetric). @@ -1834,36 +1875,37 @@ unsafe fn public_window_callback_inner( y: y.floor() as i32, }; - if ScreenToClient(window, &mut location) == false.into() { + if unsafe { ScreenToClient(window, &mut location) } == false.into() { continue; } let force = match pointer_info.pointerType { PT_TOUCH => { let mut touch_info = mem::MaybeUninit::uninit(); - GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { - match GetPointerTouchInfo( - pointer_info.pointerId, - touch_info.as_mut_ptr(), - ) { + util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + match unsafe { + GetPointerTouchInfo( + pointer_info.pointerId, + touch_info.as_mut_ptr(), + ) + } { 0 => None, - _ => normalize_pointer_pressure( - touch_info.assume_init().pressure, - ), + _ => normalize_pointer_pressure(unsafe { + touch_info.assume_init().pressure + }), } }) } PT_PEN => { let mut pen_info = mem::MaybeUninit::uninit(); - GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { - match GetPointerPenInfo( - pointer_info.pointerId, - pen_info.as_mut_ptr(), - ) { + util::GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { + match unsafe { + GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr()) + } { 0 => None, - _ => { - normalize_pointer_pressure(pen_info.assume_init().pressure) - } + _ => normalize_pointer_pressure(unsafe { + pen_info.assume_init().pressure + }), } }) } @@ -1894,7 +1936,7 @@ unsafe fn public_window_callback_inner( }); } - SkipPointerFrameMessages(pointer_id); + unsafe { SkipPointerFrameMessages(pointer_id) }; } result = ProcResult::Value(0); } @@ -1904,9 +1946,9 @@ unsafe fn public_window_callback_inner( let active_focus_changed = userdata.window_state_lock().set_active(is_active); if active_focus_changed { if is_active { - gain_active_focus(window, userdata); + unsafe { gain_active_focus(window, userdata) }; } else { - lose_active_focus(window, userdata); + unsafe { lose_active_focus(window, userdata) }; } } result = ProcResult::DefWindowProc(wparam); @@ -1915,7 +1957,7 @@ unsafe fn public_window_callback_inner( WM_SETFOCUS => { let active_focus_changed = userdata.window_state_lock().set_focused(true); if active_focus_changed { - gain_active_focus(window, userdata); + unsafe { gain_active_focus(window, userdata) }; } result = ProcResult::Value(0); } @@ -1923,7 +1965,7 @@ unsafe fn public_window_callback_inner( WM_KILLFOCUS => { let active_focus_changed = userdata.window_state_lock().set_focused(false); if active_focus_changed { - lose_active_focus(window, userdata); + unsafe { lose_active_focus(window, userdata) }; } result = ProcResult::Value(0); } @@ -1944,8 +1986,8 @@ unsafe fn public_window_callback_inner( match set_cursor_to { Some(cursor) => { - let cursor = LoadCursorW(0, util::to_windows_cursor(cursor)); - SetCursor(cursor); + let cursor = unsafe { LoadCursorW(0, util::to_windows_cursor(cursor)) }; + unsafe { SetCursor(cursor) }; result = ProcResult::Value(0); } None => result = ProcResult::DefWindowProc(wparam), @@ -1963,18 +2005,22 @@ unsafe fn public_window_callback_inner( let min_size = min_size.to_physical(window_state.scale_factor); let (width, height): (u32, u32) = window_flags.adjust_size(window, min_size).into(); - (*mmi).ptMinTrackSize = POINT { - x: width as i32, - y: height as i32, + unsafe { + (*mmi).ptMinTrackSize = POINT { + x: width as i32, + y: height as i32, + } }; } 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) = window_flags.adjust_size(window, max_size).into(); - (*mmi).ptMaxTrackSize = POINT { - x: width as i32, - y: height as i32, + unsafe { + (*mmi).ptMaxTrackSize = POINT { + x: width as i32, + y: height as i32, + } }; } } @@ -2012,7 +2058,7 @@ unsafe fn public_window_callback_inner( }; // New size as suggested by Windows. - let suggested_rect = *(lparam as *const RECT); + let suggested_rect = unsafe { *(lparam as *const RECT) }; // The window rect provided is the window's outer size, not it's inner size. However, // win32 doesn't provide an `UnadjustWindowRectEx` function to get the client rect from @@ -2042,7 +2088,7 @@ unsafe fn public_window_callback_inner( // `allow_resize` prevents us from re-applying DPI adjustment to the restored size after // exiting fullscreen (the restored size is already DPI adjusted). - let mut new_physical_inner_size = match allow_resize { + let new_physical_inner_size = match allow_resize { // We calculate our own size because the default suggested rect doesn't do a great job // of preserving the window's logical size. true => old_physical_inner_size @@ -2051,14 +2097,18 @@ unsafe fn public_window_callback_inner( false => old_physical_inner_size, }; + let new_inner_size = Arc::new(Mutex::new(new_physical_inner_size)); userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: ScaleFactorChanged { scale_factor: new_scale_factor, - new_inner_size: &mut new_physical_inner_size, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), }, }); + let new_physical_inner_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let dragging_window: bool; { @@ -2097,8 +2147,8 @@ unsafe fn public_window_callback_inner( if dragging_window { let bias = { let cursor_pos = { - let mut pos = mem::zeroed(); - GetCursorPos(&mut pos); + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; pos }; let suggested_cursor_horizontal_ratio = (cursor_pos.x - suggested_rect.left) @@ -2117,18 +2167,18 @@ unsafe fn public_window_callback_inner( // Check to see if the new window rect is on the monitor with the new DPI factor. // If it isn't, offset the window so that it is. - let new_dpi_monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + let new_dpi_monitor = unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) }; let conservative_rect_monitor = - MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL); + unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) }; new_outer_rect = if conservative_rect_monitor == new_dpi_monitor { conservative_rect } else { let get_monitor_rect = |monitor| { let mut monitor_info = MONITORINFO { cbSize: mem::size_of::() as _, - ..mem::zeroed() + ..unsafe { mem::zeroed() } }; - GetMonitorInfoW(monitor, &mut monitor_info); + unsafe { GetMonitorInfoW(monitor, &mut monitor_info) }; monitor_info.rcMonitor }; let wrong_monitor = conservative_rect_monitor; @@ -2165,7 +2215,7 @@ unsafe fn public_window_callback_inner( conservative_rect.top += delta_nudge_to_dpi_monitor.1; conservative_rect.bottom += delta_nudge_to_dpi_monitor.1; - if MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) + if unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) } == new_dpi_monitor { break; @@ -2176,15 +2226,17 @@ unsafe fn public_window_callback_inner( }; } - SetWindowPos( - window, - 0, - new_outer_rect.left, - new_outer_rect.top, - new_outer_rect.right - new_outer_rect.left, - new_outer_rect.bottom - new_outer_rect.top, - SWP_NOZORDER | SWP_NOACTIVATE, - ); + unsafe { + SetWindowPos( + window, + 0, + new_outer_rect.left, + new_outer_rect.top, + new_outer_rect.right - new_outer_rect.left, + new_outer_rect.bottom - new_outer_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + }; result = ProcResult::Value(0); } @@ -2212,7 +2264,7 @@ unsafe fn public_window_callback_inner( _ => { if msg == DESTROY_MSG_ID.get() { - DestroyWindow(window); + unsafe { DestroyWindow(window) }; result = ProcResult::Value(0); } else if msg == SET_RETAIN_STATE_ON_SIZE_MSG_ID.get() { let mut window_state = userdata.window_state_lock(); @@ -2222,7 +2274,7 @@ unsafe fn public_window_callback_inner( result = ProcResult::Value(0); } else if msg == TASKBAR_CREATED.get() { let window_state = userdata.window_state_lock(); - set_skip_taskbar(window, window_state.skip_taskbar); + unsafe { set_skip_taskbar(window, window_state.skip_taskbar) }; result = ProcResult::DefWindowProc(wparam); } else { result = ProcResult::DefWindowProc(wparam); @@ -2236,7 +2288,7 @@ unsafe fn public_window_callback_inner( .unwrap_or_else(|| result = ProcResult::Value(-1)); match result { - ProcResult::DefWindowProc(wparam) => DefWindowProcW(window, msg, wparam, lparam), + ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, ProcResult::Value(val) => val, } } @@ -2247,16 +2299,17 @@ unsafe extern "system" fn thread_event_target_callback( wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { - let userdata_ptr = super::get_window_long(window, GWL_USERDATA) as *mut ThreadMsgTargetData; + let userdata_ptr = + unsafe { super::get_window_long(window, GWL_USERDATA) } as *mut ThreadMsgTargetData; if userdata_ptr.is_null() { // `userdata_ptr` will always be null for the first `WM_GETMINMAXINFO`, as well as `WM_NCCREATE` and // `WM_CREATE`. - return DefWindowProcW(window, msg, wparam, lparam); + return unsafe { DefWindowProcW(window, msg, wparam, lparam) }; } - let userdata = Box::from_raw(userdata_ptr); + let userdata = unsafe { Box::from_raw(userdata_ptr) }; if msg != WM_PAINT { - RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT); + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; } let mut userdata_removed = false; @@ -2266,34 +2319,15 @@ unsafe extern "system" fn thread_event_target_callback( // the git blame and history would be preserved. let callback = || match msg { WM_NCDESTROY => { - super::set_window_long(window, GWL_USERDATA, 0); + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; userdata_removed = true; 0 } - // Because WM_PAINT comes after all other messages, we use it during modal loops to detect - // when the event queue has been emptied. See `process_event` for more details. - WM_PAINT => { + WM_PAINT => unsafe { ValidateRect(window, ptr::null()); - // If the WM_PAINT handler in `public_window_callback` has already flushed the redraw - // events, `handling_events` will return false and we won't emit a second - // `RedrawEventsCleared` event. - if userdata.event_loop_runner.handling_events() { - if userdata.event_loop_runner.should_buffer() { - // This branch can be triggered when a nested win32 event loop is triggered - // inside of the `event_handler` callback. - RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT); - } else { - // This WM_PAINT handler will never be re-entrant because `flush_paint_messages` - // doesn't call WM_PAINT for the thread event target (i.e. this window). - assert!(flush_paint_messages(None, &userdata.event_loop_runner)); - userdata.event_loop_runner.redraw_events_cleared(); - process_control_flow(&userdata.event_loop_runner); - } - } - // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediatly when opening them. DefWindowProcW(window, msg, wparam, lparam) - } + }, WM_INPUT_DEVICE_CHANGE => { let event = match wparam as u32 { @@ -2312,10 +2346,10 @@ unsafe extern "system" fn thread_event_target_callback( WM_INPUT => { if let Some(data) = raw_input::get_raw_input_data(lparam as _) { - handle_raw_input(&userdata, data); + unsafe { handle_raw_input(&userdata, data) }; } - DefWindowProcW(window, msg, wparam, lparam) + unsafe { DefWindowProcW(window, msg, wparam, lparam) } } _ if msg == USER_EVENT_MSG_ID.get() => { @@ -2325,45 +2359,11 @@ unsafe extern "system" fn thread_event_target_callback( 0 } _ if msg == EXEC_MSG_ID.get() => { - let mut function: ThreadExecFn = Box::from_raw(wparam as *mut _); + let mut function: ThreadExecFn = unsafe { Box::from_raw(wparam as *mut _) }; function(); 0 } - _ if msg == PROCESS_NEW_EVENTS_MSG_ID.get() => { - PostThreadMessageW( - userdata.event_loop_runner.wait_thread_id(), - CANCEL_WAIT_UNTIL_MSG_ID.get(), - 0, - 0, - ); - - // if the control_flow is WaitUntil, make sure the given moment has actually passed - // before emitting NewEvents - if let ControlFlow::WaitUntil(wait_until) = userdata.event_loop_runner.control_flow() { - let mut msg = mem::zeroed(); - while Instant::now() < wait_until { - if PeekMessageW(&mut msg, 0, 0, 0, PM_NOREMOVE) != false.into() { - // This works around a "feature" in PeekMessageW. If the message PeekMessageW - // gets is a WM_PAINT message that had RDW_INTERNALPAINT set (i.e. doesn't - // have an update region), PeekMessageW will remove that window from the - // redraw queue even though we told it not to remove messages from the - // queue. We fix it by re-dispatching an internal paint message to that - // window. - if msg.message == WM_PAINT { - let mut rect = mem::zeroed(); - if GetUpdateRect(msg.hwnd, &mut rect, false.into()) == false.into() { - RedrawWindow(msg.hwnd, ptr::null(), 0, RDW_INTERNALPAINT); - } - } - - break; - } - } - } - userdata.event_loop_runner.poll(); - 0 - } - _ => DefWindowProcW(window, msg, wparam, lparam), + _ => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, }; let result = userdata @@ -2388,7 +2388,7 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: let device_id = wrap_device_id(data.header.hDevice as _); if data.header.dwType == RIM_TYPEMOUSE { - let mouse = data.data.mouse; + let mouse = unsafe { data.data.mouse }; if util::has_flag(mouse.usFlags as u32, MOUSE_MOVE_RELATIVE) { let x = mouse.lLastX as f64; @@ -2416,12 +2416,10 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: } } - let button_flags = mouse.Anonymous.Anonymous.usButtonFlags; - + let button_flags = unsafe { mouse.Anonymous.Anonymous.usButtonFlags }; if util::has_flag(button_flags as u32, RI_MOUSE_WHEEL) { - let button_data = mouse.Anonymous.Anonymous.usButtonData; - // We must cast to i16 first, becaues `usButtonData` must be interpreted as signed. - let delta = button_data as i16 as f32 / WHEEL_DELTA as f32; + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = button_data as f32 / WHEEL_DELTA as f32; userdata.send_event(Event::DeviceEvent { device_id, event: MouseWheel { @@ -2429,23 +2427,31 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: }, }); } + if util::has_flag(button_flags as u32, RI_MOUSE_HWHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = -button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { + delta: LineDelta(delta, 0.0), + }, + }); + } let button_state = raw_input::get_raw_mouse_button_state(button_flags as u32); - // Left, middle, and right, respectively. - for (index, state) in button_state.iter().enumerate() { + for (button, state) in button_state.iter().enumerate() { if let Some(state) = *state { - // This gives us consistency with X11, since there doesn't - // seem to be anything else reasonable to do for a mouse - // button ID. - let button = (index + 1) as _; userdata.send_event(Event::DeviceEvent { device_id, - event: Button { button, state }, + event: Button { + button: button as _, + state, + }, }); } } } else if data.header.dwType == RIM_TYPEKEYBOARD { - let keyboard = data.data.keyboard; + let keyboard = unsafe { data.data.keyboard }; let pressed = keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN; let released = keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP; @@ -2454,102 +2460,51 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: return; } - let state = if pressed { Pressed } else { Released }; - let extension = { - if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) { - 0xE000 - } else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) { - 0xE100 - } else { - 0x0000 - } - }; - let scancode = if keyboard.MakeCode == 0 { - // In some cases (often with media keys) the device reports a scancode of 0 but a - // valid virtual key. In these cases we obtain the scancode from the virtual key. - MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 - } else { - keyboard.MakeCode | extension - }; - if scancode == 0xE11D || scancode == 0xE02A { - // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing - // Ctrl+NumLock. - // This equvalence means that if the user presses Pause, the keyboard will emit two - // subsequent keypresses: - // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) - // 2, 0x0045 - Which on its own can be interpreted as Pause - // - // There's another combination which isn't quite an equivalence: - // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing - // PrtSc (print screen) produces the following sequence: - // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) - // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on - // its own it can be interpreted as PrtSc - // - // For this reason, if we encounter the first keypress, we simply ignore it, trusting - // that there's going to be another event coming, from which we can extract the - // appropriate key. - // For more on this, read the article by Raymond Chen, titled: - // "Why does Ctrl+ScrollLock cancel dialogs?" - // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 - return; - } - let code = if keyboard.VKey == VK_NUMLOCK { - // Historically, the NumLock and the Pause key were one and the same physical key. - // The user could trigger Pause by pressing Ctrl+NumLock. - // Now these are often physically separate and the two keys can be differentiated by - // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. - // - // However in this event, both keys are reported as 0x0045 even on modern hardware. - // Therefore we use the virtual key instead to determine whether it's a NumLock and - // set the KeyCode accordingly. - // - // For more on this, read the article by Raymond Chen, titled: - // "Why does Ctrl+ScrollLock cancel dialogs?" - // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 - KeyCode::NumLock - } else { - KeyCode::from_scancode(scancode as u32) - }; - if keyboard.VKey == VK_SHIFT { - match code { - KeyCode::NumpadDecimal - | KeyCode::Numpad0 - | KeyCode::Numpad1 - | KeyCode::Numpad2 - | KeyCode::Numpad3 - | KeyCode::Numpad4 - | KeyCode::Numpad5 - | KeyCode::Numpad6 - | KeyCode::Numpad7 - | KeyCode::Numpad8 - | KeyCode::Numpad9 => { - // On Windows, holding the Shift key makes numpad keys behave as if NumLock - // wasn't active. The way this is exposed to applications by the system is that - // the application receives a fake key release event for the shift key at the - // moment when the numpad key is pressed, just before receiving the numpad key - // as well. - // - // The issue is that in the raw device event (here), the fake shift release - // event reports the numpad key as the scancode. Unfortunately, the event doesn't - // have any information to tell whether it's the left shift or the right shift - // that needs to get the fake release (or press) event so we don't forward this - // event to the application at all. - // - // For more on this, read the article by Raymond Chen, titled: - // "The shift key overrides NumLock" - // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 - return; - } - _ => (), - } + if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) { + let state = if pressed { Pressed } else { Released }; + + userdata.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { + physical_key, + state, + }), + }); } - userdata.send_event(Event::DeviceEvent { - device_id, - event: Key(RawKeyEvent { - physical_key: code, - state, - }), - }); + } +} + +enum PointerMoveKind { + /// Pointer enterd to the window. + Enter, + /// Pointer leaved the window client area. + Leave, + /// Pointer is inside the window or `GetClientRect` failed. + None, +} + +fn get_pointer_move_kind( + window: HWND, + mouse_was_inside_window: bool, + x: i32, + y: i32, +) -> PointerMoveKind { + let rect: RECT = unsafe { + let mut rect: RECT = mem::zeroed(); + if GetClientRect(window, &mut rect) == false.into() { + return PointerMoveKind::None; // exit early if GetClientRect failed + } + rect + }; + + let x = (rect.left..rect.right).contains(&x); + let y = (rect.top..rect.bottom).contains(&y); + + if !mouse_was_inside_window && x && y { + PointerMoveKind::Enter + } else if mouse_was_inside_window && !(x && y) { + PointerMoveKind::Leave + } else { + PointerMoveKind::None } } diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs index c4ac1eb348..4881ce7452 100644 --- a/src/platform_impl/windows/event_loop/runner.rs +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -1,21 +1,18 @@ use std::{ any::Any, cell::{Cell, RefCell}, - collections::{HashSet, VecDeque}, - mem, panic, ptr, + collections::VecDeque, + mem, panic, rc::Rc, + sync::{Arc, Mutex}, time::Instant, }; -use windows_sys::Win32::{ - Foundation::HWND, - Graphics::Gdi::{RedrawWindow, RDW_INTERNALPAINT}, -}; +use windows_sys::Win32::Foundation::HWND; use crate::{ dpi::PhysicalSize, - event::{Event, StartCause, WindowEvent}, - event_loop::ControlFlow, + event::{Event, InnerSizeWriter, StartCause, WindowEvent}, platform_impl::platform::{ event_loop::{WindowData, GWL_USERDATA}, get_window_long, @@ -23,23 +20,28 @@ use crate::{ window::WindowId, }; +use super::ControlFlow; + pub(crate) type EventLoopRunnerShared = Rc>; -type EventHandler = Cell, &mut ControlFlow)>>>; +type EventHandler = Cell)>>>; pub(crate) struct EventLoopRunner { // The event loop's win32 handles pub(super) thread_msg_target: HWND, - wait_thread_id: u32, + + // Setting this will ensure pump_events will return to the external + // loop asap. E.g. set after each RedrawRequested to ensure pump_events + // can't stall an external loop beyond a frame + pub(super) interrupt_msg_dispatch: Cell, control_flow: Cell, + exit: Cell>, runner_state: Cell, last_events_cleared: Cell, event_handler: EventHandler, event_buffer: RefCell>>, - owned_windows: Cell>, - panic_error: Cell>, } @@ -47,85 +49,93 @@ pub type PanicError = Box; /// See `move_state_to` function for details on how the state loop works. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum RunnerState { +pub(crate) enum RunnerState { /// The event loop has just been created, and an `Init` event must be sent. Uninitialized, /// The event loop is idling. Idle, /// The event loop is handling the OS's events and sending them to the user's callback. - /// `NewEvents` has been sent, and `MainEventsCleared` hasn't. + /// `NewEvents` has been sent, and `AboutToWait` hasn't. HandlingMainEvents, - /// The event loop is handling the redraw events and sending them to the user's callback. - /// `MainEventsCleared` has been sent, and `RedrawEventsCleared` hasn't. - HandlingRedrawEvents, /// The event loop has been destroyed. No other events will be emitted. Destroyed, } enum BufferedEvent { - Event(Event<'static, T>), + Event(Event), ScaleFactorChanged(WindowId, f64, PhysicalSize), } impl EventLoopRunner { - pub(crate) fn new(thread_msg_target: HWND, wait_thread_id: u32) -> EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND) -> EventLoopRunner { EventLoopRunner { thread_msg_target, - wait_thread_id, + interrupt_msg_dispatch: Cell::new(false), runner_state: Cell::new(RunnerState::Uninitialized), - control_flow: Cell::new(ControlFlow::Poll), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), panic_error: Cell::new(None), last_events_cleared: Cell::new(Instant::now()), event_handler: Cell::new(None), event_buffer: RefCell::new(VecDeque::new()), - owned_windows: Cell::new(HashSet::new()), } } + /// Associate the application's event handler with the runner + /// + /// # Safety + /// This is ignoring the lifetime of the application handler (which may not + /// outlive the EventLoopRunner) and can lead to undefined behaviour if + /// the handler is not cleared before the end of real lifetime. + /// + /// All public APIs that take an event handler (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_event_handler` with + /// a call to `clear_event_handler` before returning to avoid + /// undefined behaviour. pub(crate) unsafe fn set_event_handler(&self, f: F) where - F: FnMut(Event<'_, T>, &mut ControlFlow), + F: FnMut(Event), { - let old_event_handler = self.event_handler.replace(mem::transmute::< - Option, &mut ControlFlow)>>, - Option, &mut ControlFlow)>>, - >(Some(Box::new(f)))); + // Erase closure lifetime. + // SAFETY: Caller upholds that the lifetime of the closure is upheld. + let f = unsafe { + mem::transmute::)>, Box)>>(Box::new(f)) + }; + let old_event_handler = self.event_handler.replace(Some(f)); assert!(old_event_handler.is_none()); } + pub(crate) fn clear_event_handler(&self) { + self.event_handler.set(None); + } + pub(crate) fn reset_runner(&self) { let EventLoopRunner { thread_msg_target: _, - wait_thread_id: _, + interrupt_msg_dispatch, runner_state, panic_error, - control_flow, + control_flow: _, + exit, last_events_cleared: _, event_handler, event_buffer: _, - owned_windows: _, } = self; + interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); panic_error.set(None); - control_flow.set(ControlFlow::Poll); + exit.set(None); event_handler.set(None); } } /// State retrieval functions. impl EventLoopRunner { + #[allow(unused)] pub fn thread_msg_target(&self) -> HWND { self.thread_msg_target } - pub fn wait_thread_id(&self) -> u32 { - self.wait_thread_id - } - - pub fn redrawing(&self) -> bool { - self.runner_state.get() == RunnerState::HandlingRedrawEvents - } - pub fn take_panic_error(&self) -> Result<(), PanicError> { match self.panic_error.take() { Some(err) => Err(err), @@ -133,12 +143,28 @@ impl EventLoopRunner { } } + pub fn state(&self) -> RunnerState { + self.runner_state.get() + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + pub fn control_flow(&self) -> ControlFlow { self.control_flow.get() } - pub fn handling_events(&self) -> bool { - self.runner_state.get() != RunnerState::Idle + pub fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub fn exit_code(&self) -> Option { + self.exit.get() + } + + pub fn clear_exit(&self) { + self.exit.set(None); } pub fn should_buffer(&self) -> bool { @@ -177,42 +203,29 @@ impl EventLoopRunner { None } } - pub fn register_window(&self, window: HWND) { - let mut owned_windows = self.owned_windows.take(); - owned_windows.insert(window); - self.owned_windows.set(owned_windows); - } - - pub fn remove_window(&self, window: HWND) { - let mut owned_windows = self.owned_windows.take(); - owned_windows.remove(&window); - self.owned_windows.set(owned_windows); - } - - pub fn owned_windows(&self, mut f: impl FnMut(HWND)) { - let mut owned_windows = self.owned_windows.take(); - for hwnd in &owned_windows { - f(*hwnd); - } - let new_owned_windows = self.owned_windows.take(); - owned_windows.extend(&new_owned_windows); - self.owned_windows.set(owned_windows); - } } /// Event dispatch functions. impl EventLoopRunner { - pub(crate) unsafe fn poll(&self) { + pub(crate) fn prepare_wait(&self) { + self.move_state_to(RunnerState::Idle); + } + + pub(crate) fn wakeup(&self) { self.move_state_to(RunnerState::HandlingMainEvents); } - pub(crate) unsafe fn send_event(&self, event: Event<'_, T>) { - if let Event::RedrawRequested(_) = event { - if self.runner_state.get() != RunnerState::HandlingRedrawEvents { - warn!("RedrawRequested dispatched without explicit MainEventsCleared"); - self.move_state_to(RunnerState::HandlingRedrawEvents); - } + pub(crate) fn send_event(&self, event: Event) { + if let Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } = event + { self.call_event_handler(event); + // As a rule, to ensure that `pump_events` can't block an external event loop + // for too long, we always guarantee that `pump_events` will return control to + // the external loop asap after a `RedrawRequested` event is dispatched. + self.interrupt_msg_dispatch.set(true); } else if self.should_buffer() { // If the runner is already borrowed, we're in the middle of an event loop invocation. Add // the event to a buffer to be processed later. @@ -220,42 +233,27 @@ impl EventLoopRunner { .borrow_mut() .push_back(BufferedEvent::from_event(event)) } else { - self.move_state_to(RunnerState::HandlingMainEvents); self.call_event_handler(event); self.dispatch_buffered_events(); } } - pub(crate) unsafe fn main_events_cleared(&self) { - self.move_state_to(RunnerState::HandlingRedrawEvents); - } - - pub(crate) unsafe fn redraw_events_cleared(&self) { - self.move_state_to(RunnerState::Idle); - } - - pub(crate) unsafe fn loop_destroyed(&self) { + pub(crate) fn loop_destroyed(&self) { self.move_state_to(RunnerState::Destroyed); } - unsafe fn call_event_handler(&self, event: Event<'_, T>) { + fn call_event_handler(&self, event: Event) { self.catch_unwind(|| { - let mut control_flow = self.control_flow.take(); let mut event_handler = self.event_handler.take() .expect("either event handler is re-entrant (likely), or no event handler is registered (very unlikely)"); - if let ControlFlow::ExitWithCode(code) = control_flow { - event_handler(event, &mut ControlFlow::ExitWithCode(code)); - } else { - event_handler(event, &mut control_flow); - } + event_handler(event); assert!(self.event_handler.replace(Some(event_handler)).is_none()); - self.control_flow.set(control_flow); }); } - unsafe fn dispatch_buffered_events(&self) { + fn dispatch_buffered_events(&self) { loop { // We do this instead of using a `while let` loop because if we use a `while let` // loop the reference returned `borrow_mut()` doesn't get dropped until the end @@ -269,8 +267,9 @@ impl EventLoopRunner { } } - /// Dispatch control flow events (`NewEvents`, `MainEventsCleared`, `RedrawEventsCleared`, and - /// `LoopDestroyed`) as necessary to bring the internal `RunnerState` to the new runner state. + /// Dispatch control flow events (`NewEvents`, `AboutToWait`, and + /// `LoopExiting`) as necessary to bring the internal `RunnerState` to the + /// new runner state. /// /// The state transitions are defined as follows: /// @@ -278,24 +277,22 @@ impl EventLoopRunner { /// Uninitialized /// | /// V - /// HandlingMainEvents - /// ^ | - /// | V - /// Idle <--- HandlingRedrawEvents - /// | - /// V - /// Destroyed + /// Idle + /// ^ | + /// | V + /// HandlingMainEvents + /// | + /// V + /// Destroyed /// ``` /// /// Attempting to transition back to `Uninitialized` will result in a panic. Attempting to - /// transition *from* `Destroyed` will also reuslt in a panic. Transitioning to the current + /// transition *from* `Destroyed` will also result in a panic. Transitioning to the current /// state is a no-op. Even if the `new_runner_state` isn't the immediate next state in the /// runner state machine (e.g. `self.runner_state == HandlingMainEvents` and /// `new_runner_state == Idle`), the intermediate state transitions will still be executed. - unsafe fn move_state_to(&self, new_runner_state: RunnerState) { - use RunnerState::{ - Destroyed, HandlingMainEvents, HandlingRedrawEvents, Idle, Uninitialized, - }; + fn move_state_to(&self, new_runner_state: RunnerState) { + use RunnerState::{Destroyed, HandlingMainEvents, Idle, Uninitialized}; match ( self.runner_state.replace(new_runner_state), @@ -304,27 +301,22 @@ impl EventLoopRunner { (Uninitialized, Uninitialized) | (Idle, Idle) | (HandlingMainEvents, HandlingMainEvents) - | (HandlingRedrawEvents, HandlingRedrawEvents) | (Destroyed, Destroyed) => (), // State transitions that initialize the event loop. (Uninitialized, HandlingMainEvents) => { self.call_new_events(true); } - (Uninitialized, HandlingRedrawEvents) => { - self.call_new_events(true); - self.call_event_handler(Event::MainEventsCleared); - } (Uninitialized, Idle) => { self.call_new_events(true); - self.call_event_handler(Event::MainEventsCleared); - self.call_redraw_events_cleared(); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); } (Uninitialized, Destroyed) => { self.call_new_events(true); - self.call_event_handler(Event::MainEventsCleared); - self.call_redraw_events_cleared(); - self.call_event_handler(Event::LoopDestroyed); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); } (_, Uninitialized) => panic!("cannot move state to Uninitialized"), @@ -332,56 +324,34 @@ impl EventLoopRunner { (Idle, HandlingMainEvents) => { self.call_new_events(false); } - (Idle, HandlingRedrawEvents) => { - self.call_new_events(false); - self.call_event_handler(Event::MainEventsCleared); - } (Idle, Destroyed) => { - self.call_event_handler(Event::LoopDestroyed); + self.call_event_handler(Event::LoopExiting); } - (HandlingMainEvents, HandlingRedrawEvents) => { - self.call_event_handler(Event::MainEventsCleared); - } (HandlingMainEvents, Idle) => { - warn!("RedrawEventsCleared emitted without explicit MainEventsCleared"); - self.call_event_handler(Event::MainEventsCleared); - self.call_redraw_events_cleared(); + // This is always the last event we dispatch before waiting for new events + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); } (HandlingMainEvents, Destroyed) => { - self.call_event_handler(Event::MainEventsCleared); - self.call_redraw_events_cleared(); - self.call_event_handler(Event::LoopDestroyed); - } - - (HandlingRedrawEvents, Idle) => { - self.call_redraw_events_cleared(); - } - (HandlingRedrawEvents, HandlingMainEvents) => { - warn!("NewEvents emitted without explicit RedrawEventsCleared"); - self.call_redraw_events_cleared(); - self.call_new_events(false); - } - (HandlingRedrawEvents, Destroyed) => { - self.call_redraw_events_cleared(); - self.call_event_handler(Event::LoopDestroyed); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); } (Destroyed, _) => panic!("cannot move state from Destroyed"), } } - unsafe fn call_new_events(&self, init: bool) { - let start_cause = match (init, self.control_flow()) { - (true, _) => StartCause::Init, - (false, ControlFlow::Poll) => StartCause::Poll, - (false, ControlFlow::ExitWithCode(_)) | (false, ControlFlow::Wait) => { - StartCause::WaitCancelled { - requested_resume: None, - start: self.last_events_cleared.get(), - } - } - (false, ControlFlow::WaitUntil(requested_resume)) => { + fn call_new_events(&self, init: bool) { + let start_cause = match (init, self.control_flow(), self.exit.get()) { + (true, _, _) => StartCause::Init, + (false, ControlFlow::Poll, None) => StartCause::Poll, + (false, _, Some(_)) | (false, ControlFlow::Wait, None) => StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + }, + (false, ControlFlow::WaitUntil(requested_resume), None) => { if Instant::now() < requested_resume { StartCause::WaitCancelled { requested_resume: Some(requested_resume), @@ -402,48 +372,60 @@ impl EventLoopRunner { self.call_event_handler(Event::Resumed); } self.dispatch_buffered_events(); - RedrawWindow(self.thread_msg_target, ptr::null(), 0, RDW_INTERNALPAINT); - } - - unsafe fn call_redraw_events_cleared(&self) { - self.call_event_handler(Event::RedrawEventsCleared); - self.last_events_cleared.set(Instant::now()); } } impl BufferedEvent { - pub fn from_event(event: Event<'_, T>) -> BufferedEvent { + pub fn from_event(event: Event) -> BufferedEvent { match event { Event::WindowEvent { event: WindowEvent::ScaleFactorChanged { scale_factor, - new_inner_size, + inner_size_writer, }, window_id, - } => BufferedEvent::ScaleFactorChanged(window_id, scale_factor, *new_inner_size), - event => BufferedEvent::Event(event.to_static().unwrap()), + } => BufferedEvent::ScaleFactorChanged( + window_id, + scale_factor, + *inner_size_writer + .new_inner_size + .upgrade() + .unwrap() + .lock() + .unwrap(), + ), + event => BufferedEvent::Event(event), } } - pub fn dispatch_event(self, dispatch: impl FnOnce(Event<'_, T>)) { + pub fn dispatch_event(self, dispatch: impl FnOnce(Event)) { match self { Self::Event(event) => dispatch(event), - Self::ScaleFactorChanged(window_id, scale_factor, mut new_inner_size) => { + Self::ScaleFactorChanged(window_id, scale_factor, new_inner_size) => { + let user_new_innner_size = Arc::new(Mutex::new(new_inner_size)); dispatch(Event::WindowEvent { window_id, event: WindowEvent::ScaleFactorChanged { scale_factor, - new_inner_size: &mut new_inner_size, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &user_new_innner_size, + )), }, }); + let inner_size = *user_new_innner_size.lock().unwrap(); - 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); + drop(user_new_innner_size); + + if inner_size != new_inner_size { + 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, inner_size); + } } } } diff --git a/src/platform_impl/windows/ime.rs b/src/platform_impl/windows/ime.rs index 2f9320f8ca..fc48046938 100644 --- a/src/platform_impl/windows/ime.rs +++ b/src/platform_impl/windows/ime.rs @@ -10,9 +10,9 @@ use windows_sys::Win32::{ 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, + ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED, + ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM, + GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT, }, WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}, }, @@ -30,15 +30,15 @@ pub struct ImeContext { impl ImeContext { pub unsafe fn current(hwnd: HWND) -> Self { - let himc = ImmGetContext(hwnd); + let himc = unsafe { 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 text = unsafe { self.get_composition_string(GCS_COMPSTR) }?; + let attrs = unsafe { self.get_composition_data(GCS_COMPATTR) }.unwrap_or_default(); let mut first = None; let mut last = None; @@ -61,7 +61,7 @@ impl ImeContext { 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); + let cursor = unsafe { self.get_composition_cursor(&text) }; first = cursor; last = cursor; } @@ -70,17 +70,17 @@ impl ImeContext { } pub unsafe fn get_composed_text(&self) -> Option { - self.get_composition_string(GCS_RESULTSTR) + unsafe { 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); + let cursor = unsafe { 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::(); + let data = unsafe { self.get_composition_data(gcs_mode) }?; + let (prefix, shorts, suffix) = unsafe { data.align_to::() }; if prefix.is_empty() && suffix.is_empty() { OsString::from_wide(shorts).into_string().ok() } else { @@ -89,30 +89,32 @@ impl ImeContext { } unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { - let size = match ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) { + let size = match unsafe { 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 _, - ); + let size = unsafe { + 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 _); + unsafe { buf.set_len(size as _) }; Some(buf) } } pub unsafe fn set_ime_cursor_area(&self, spot: Position, size: Size, scale_factor: f64) { - if !ImeContext::system_has_ime() { + if !unsafe { ImeContext::system_has_ime() } { return; } @@ -122,7 +124,7 @@ impl ImeContext { left: x, top: y, right: x + width, - bottom: y - height, + bottom: y + height, }; let candidate_form = CANDIDATEFORM { dwIndex: 0, @@ -130,24 +132,32 @@ impl ImeContext { ptCurrentPos: POINT { x, y }, rcArea: rc_area, }; + let composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y: y + height }, + rcArea: rc_area, + }; - ImmSetCandidateWindow(self.himc, &candidate_form); + unsafe { + ImmSetCompositionWindow(self.himc, &composition_form); + ImmSetCandidateWindow(self.himc, &candidate_form); + } } pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { - if !ImeContext::system_has_ime() { + if !unsafe { ImeContext::system_has_ime() } { return; } if allowed { - ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT); + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) }; } else { - ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN); + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) }; } } unsafe fn system_has_ime() -> bool { - GetSystemMetrics(SM_IMMENABLED) != 0 + unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } } } diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs index 595cd4c51e..c46136bb3c 100644 --- a/src/platform_impl/windows/keyboard.rs +++ b/src/platform_impl/windows/keyboard.rs @@ -37,8 +37,8 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ event::{ElementState, KeyEvent}, - keyboard::{Key, KeyCode, KeyLocation, NativeKey, NativeKeyCode}, - platform::scancode::KeyCodeExtScancode, + keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}, + platform::scancode::PhysicalKeyExtScancode, platform_impl::platform::{ event_loop::ProcResult, keyboard_layout::{Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE}, @@ -264,8 +264,8 @@ impl KeyEventBuilder { let mod_no_ctrl = mod_state.remove_only_ctrl(); let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; let vkey = event_info.vkey; - let keycode = &event_info.code; - let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, keycode); + let physical_key = &event_info.physical_key; + let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, physical_key); event_info.text = PartialText::Text(key.to_text().map(SmolStr::new)); } let ev = event_info.finalize(); @@ -454,15 +454,16 @@ impl KeyEventBuilder { return None; } let scancode = scancode as ExScancode; - let code = KeyCode::from_scancode(scancode as u32); + let physical_key = PhysicalKey::from_scancode(scancode as u32); let mods = if caps_lock_on { WindowsModifiers::CAPS_LOCK } else { WindowsModifiers::empty() }; let layout = layouts.layouts.get(&(locale_id as u64)).unwrap(); - let logical_key = layout.get_key(mods, num_lock_on, vk, &code); - let key_without_modifiers = layout.get_key(WindowsModifiers::empty(), false, vk, &code); + let logical_key = layout.get_key(mods, num_lock_on, vk, &physical_key); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, &physical_key); let text = if key_state == ElementState::Pressed { logical_key.to_text().map(SmolStr::new) } else { @@ -474,7 +475,7 @@ impl KeyEventBuilder { key_without_modifiers, key_state, is_repeat: false, - code, + physical_key, location: get_location(scancode, locale_id), utf16parts: Vec::with_capacity(8), text: PartialText::Text(text.clone()), @@ -511,7 +512,7 @@ struct PartialKeyEventInfo { vkey: VIRTUAL_KEY, key_state: ElementState, is_repeat: bool, - code: KeyCode, + physical_key: PhysicalKey, location: KeyLocation, logical_key: PartialLogicalKey, @@ -543,7 +544,7 @@ impl PartialKeyEventInfo { } else { new_ex_scancode(lparam_struct.scancode, lparam_struct.extended) }; - let code = KeyCode::from_scancode(scancode as u32); + let physical_key = PhysicalKey::from_scancode(scancode as u32); let location = get_location(scancode, layout.hkl as HKL); let kbd_state = get_kbd_state(); @@ -558,16 +559,17 @@ impl PartialKeyEventInfo { // "Why does Ctrl+ScrollLock cancel dialogs?" // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 let code_as_key = if mods.contains(WindowsModifiers::CONTROL) { - match code { - KeyCode::NumLock => Some(Key::NumLock), - KeyCode::Pause => Some(Key::Pause), + match physical_key { + PhysicalKey::Code(KeyCode::NumLock) => Some(Key::Named(NamedKey::NumLock)), + PhysicalKey::Code(KeyCode::Pause) => Some(Key::Named(NamedKey::Pause)), _ => None, } } else { None }; - let preliminary_logical_key = layout.get_key(mods_without_ctrl, num_lock_on, vkey, &code); + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, &physical_key); let key_is_char = matches!(preliminary_logical_key, Key::Character(_)); let is_pressed = state == ElementState::Pressed; @@ -583,7 +585,7 @@ impl PartialKeyEventInfo { let key_without_modifiers = if let Some(key) = code_as_key { key } else { - match layout.get_key(NO_MODS, false, vkey, &code) { + match layout.get_key(NO_MODS, false, vkey, &physical_key) { // We convert dead keys into their character. // The reason for this is that `key_without_modifiers` is designed for key-bindings, // but the US International layout treats `'` (apostrophe) as a dead key and the @@ -609,7 +611,7 @@ impl PartialKeyEventInfo { logical_key, key_without_modifiers, is_repeat: lparam_struct.is_repeat, - code, + physical_key, location, utf16parts: Vec::with_capacity(8), text: PartialText::System(Vec::new()), @@ -656,7 +658,7 @@ impl PartialKeyEventInfo { }; KeyEvent { - physical_key: self.code, + physical_key: self.physical_key, logical_key, text, location: self.location, @@ -740,7 +742,10 @@ fn get_async_kbd_state() -> [u8; 256] { /// the next event is a right Alt (AltGr) event. If this is the case, the current event must be the /// fake Ctrl event. fn is_current_fake(curr_info: &PartialKeyEventInfo, next_msg: MSG, layout: &Layout) -> bool { - let curr_is_ctrl = matches!(curr_info.logical_key, PartialLogicalKey::This(Key::Control)); + let curr_is_ctrl = matches!( + curr_info.logical_key, + PartialLogicalKey::This(Key::Named(NamedKey::Control)) + ); if layout.has_alt_graph { let next_code = ex_scancode_from_lparam(next_msg.lParam); let next_is_altgr = next_code == 0xE038; // 0xE038 is right alt @@ -937,7 +942,7 @@ fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { } } -impl KeyCodeExtScancode for KeyCode { +impl PhysicalKeyExtScancode for PhysicalKey { fn to_scancode(self) -> Option { // See `from_scancode` for more info @@ -946,7 +951,17 @@ impl KeyCodeExtScancode for KeyCode { let primary_lang_id = primarylangid(loword(hkl as u32)); let is_korean = primary_lang_id as u32 == LANG_KOREAN; - match self { + let code = match self { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Windows(scancode) => Some(scancode as u32), + _ => None, + }; + } + }; + + match code { KeyCode::Backquote => Some(0x0029), KeyCode::Backslash => Some(0x002B), KeyCode::Backspace => Some(0x000E), @@ -1106,17 +1121,16 @@ impl KeyCodeExtScancode for KeyCode { KeyCode::AudioVolumeDown => Some(0xE02E), KeyCode::AudioVolumeMute => Some(0xE020), KeyCode::AudioVolumeUp => Some(0xE030), - KeyCode::Unidentified(NativeKeyCode::Windows(scancode)) => Some(scancode as u32), _ => None, } } - fn from_scancode(scancode: u32) -> KeyCode { + fn from_scancode(scancode: u32) -> PhysicalKey { // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html // and: https://www.w3.org/TR/uievents-code/ // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source - match scancode { + PhysicalKey::Code(match scancode { 0x0029 => KeyCode::Backquote, 0x002B => KeyCode::Backslash, 0x000E => KeyCode::Backspace, @@ -1266,7 +1280,7 @@ impl KeyCodeExtScancode for KeyCode { 0xE02E => KeyCode::AudioVolumeDown, 0xE020 => KeyCode::AudioVolumeMute, 0xE030 => KeyCode::AudioVolumeUp, - _ => KeyCode::Unidentified(NativeKeyCode::Windows(scancode as u16)), - } + _ => return PhysicalKey::Unidentified(NativeKeyCode::Windows(scancode as u16)), + }) } } diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs index 1064cec125..d80267637a 100644 --- a/src/platform_impl/windows/keyboard_layout.rs +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -52,8 +52,8 @@ use windows_sys::Win32::{ }; use crate::{ - keyboard::{Key, KeyCode, ModifiersState, NativeKey}, - platform::scancode::KeyCodeExtScancode, + keyboard::{Key, KeyCode, ModifiersState, NamedKey, NativeKey, PhysicalKey}, + platform::scancode::PhysicalKeyExtScancode, platform_impl::{loword, primarylangid}, }; @@ -224,7 +224,7 @@ impl Layout { mods: WindowsModifiers, num_lock_on: bool, vkey: VIRTUAL_KEY, - keycode: &KeyCode, + physical_key: &PhysicalKey, ) -> Key { let native_code = NativeKey::Windows(vkey); @@ -252,9 +252,11 @@ impl Layout { } else if let Some(key) = self.numlock_off_keys.get(&vkey) { return key.clone(); } - if let Some(keys) = self.keys.get(&mods) { - if let Some(key) = keys.get(keycode) { - return key.clone(); + if let PhysicalKey::Code(code) = physical_key { + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(code) { + return key.clone(); + } } } Key::Unidentified(native_code) @@ -334,7 +336,11 @@ impl LayoutCache { if scancode == 0 { continue; } - let keycode = KeyCode::from_scancode(scancode); + let keycode = match PhysicalKey::from_scancode(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; if !is_numpad_specific(vk as VIRTUAL_KEY) && NUMPAD_KEYCODES.contains(&keycode) { let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); let map_vkey = keycode_to_vkey(keycode, locale_id); @@ -382,7 +388,11 @@ impl LayoutCache { } let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); - let key_code = KeyCode::from_scancode(scancode); + let key_code = match PhysicalKey::from_scancode(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; // Let's try to get the key from just the scancode and vk // We don't necessarily know yet if AltGraph is present on this layout so we'll // assume it isn't. Then we'll do a second pass where we set the "AltRight" keys to @@ -446,7 +456,7 @@ impl LayoutCache { let mod_state = WindowsModifiers::from_bits_retain(mod_state); if let Some(keys) = layout.keys.get_mut(&mod_state) { if let Some(key) = keys.get_mut(&KeyCode::AltRight) { - *key = Key::AltGraph; + *key = Key::Named(NamedKey::AltGraph); } } } @@ -733,7 +743,6 @@ fn keycode_to_vkey(keycode: KeyCode, hkl: u64) -> VIRTUAL_KEY { KeyCode::F33 => 0, KeyCode::F34 => 0, KeyCode::F35 => 0, - KeyCode::Unidentified(_) => 0, _ => 0, } } @@ -769,56 +778,56 @@ fn vkey_to_non_char_key( VK_MBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse VK_XBUTTON1 => Key::Unidentified(NativeKey::Unidentified), // Mouse VK_XBUTTON2 => Key::Unidentified(NativeKey::Unidentified), // Mouse - VK_BACK => Key::Backspace, - VK_TAB => Key::Tab, - VK_CLEAR => Key::Clear, - VK_RETURN => Key::Enter, - VK_SHIFT => Key::Shift, - VK_CONTROL => Key::Control, - VK_MENU => Key::Alt, - VK_PAUSE => Key::Pause, - VK_CAPITAL => Key::CapsLock, - - //VK_HANGEUL => Key::HangulMode, // Deprecated in favour of VK_HANGUL + VK_BACK => Key::Named(NamedKey::Backspace), + VK_TAB => Key::Named(NamedKey::Tab), + VK_CLEAR => Key::Named(NamedKey::Clear), + VK_RETURN => Key::Named(NamedKey::Enter), + VK_SHIFT => Key::Named(NamedKey::Shift), + VK_CONTROL => Key::Named(NamedKey::Control), + VK_MENU => Key::Named(NamedKey::Alt), + VK_PAUSE => Key::Named(NamedKey::Pause), + VK_CAPITAL => Key::Named(NamedKey::CapsLock), + + //VK_HANGEUL => Key::Named(NamedKey::HangulMode), // Deprecated in favour of VK_HANGUL // VK_HANGUL and VK_KANA are defined as the same constant, therefore // we use appropriate conditions to differentate between them - VK_HANGUL if is_korean => Key::HangulMode, - VK_KANA if is_japanese => Key::KanaMode, + VK_HANGUL if is_korean => Key::Named(NamedKey::HangulMode), + VK_KANA if is_japanese => Key::Named(NamedKey::KanaMode), - VK_JUNJA => Key::JunjaMode, - VK_FINAL => Key::FinalMode, + VK_JUNJA => Key::Named(NamedKey::JunjaMode), + VK_FINAL => Key::Named(NamedKey::FinalMode), // VK_HANJA and VK_KANJI are defined as the same constant, therefore // we use appropriate conditions to differentate between them - VK_HANJA if is_korean => Key::HanjaMode, - VK_KANJI if is_japanese => Key::KanjiMode, - - VK_ESCAPE => Key::Escape, - VK_CONVERT => Key::Convert, - VK_NONCONVERT => Key::NonConvert, - VK_ACCEPT => Key::Accept, - VK_MODECHANGE => Key::ModeChange, - VK_SPACE => Key::Space, - VK_PRIOR => Key::PageUp, - VK_NEXT => Key::PageDown, - VK_END => Key::End, - VK_HOME => Key::Home, - VK_LEFT => Key::ArrowLeft, - VK_UP => Key::ArrowUp, - VK_RIGHT => Key::ArrowRight, - VK_DOWN => Key::ArrowDown, - VK_SELECT => Key::Select, - VK_PRINT => Key::Print, - VK_EXECUTE => Key::Execute, - VK_SNAPSHOT => Key::PrintScreen, - VK_INSERT => Key::Insert, - VK_DELETE => Key::Delete, - VK_HELP => Key::Help, - VK_LWIN => Key::Super, - VK_RWIN => Key::Super, - VK_APPS => Key::ContextMenu, - VK_SLEEP => Key::Standby, + VK_HANJA if is_korean => Key::Named(NamedKey::HanjaMode), + VK_KANJI if is_japanese => Key::Named(NamedKey::KanjiMode), + + VK_ESCAPE => Key::Named(NamedKey::Escape), + VK_CONVERT => Key::Named(NamedKey::Convert), + VK_NONCONVERT => Key::Named(NamedKey::NonConvert), + VK_ACCEPT => Key::Named(NamedKey::Accept), + VK_MODECHANGE => Key::Named(NamedKey::ModeChange), + VK_SPACE => Key::Named(NamedKey::Space), + VK_PRIOR => Key::Named(NamedKey::PageUp), + VK_NEXT => Key::Named(NamedKey::PageDown), + VK_END => Key::Named(NamedKey::End), + VK_HOME => Key::Named(NamedKey::Home), + VK_LEFT => Key::Named(NamedKey::ArrowLeft), + VK_UP => Key::Named(NamedKey::ArrowUp), + VK_RIGHT => Key::Named(NamedKey::ArrowRight), + VK_DOWN => Key::Named(NamedKey::ArrowDown), + VK_SELECT => Key::Named(NamedKey::Select), + VK_PRINT => Key::Named(NamedKey::Print), + VK_EXECUTE => Key::Named(NamedKey::Execute), + VK_SNAPSHOT => Key::Named(NamedKey::PrintScreen), + VK_INSERT => Key::Named(NamedKey::Insert), + VK_DELETE => Key::Named(NamedKey::Delete), + VK_HELP => Key::Named(NamedKey::Help), + VK_LWIN => Key::Named(NamedKey::Super), + VK_RWIN => Key::Named(NamedKey::Super), + VK_APPS => Key::Named(NamedKey::ContextMenu), + VK_SLEEP => Key::Named(NamedKey::Standby), // Numpad keys produce characters VK_NUMPAD0 => Key::Unidentified(native_code), @@ -838,30 +847,30 @@ fn vkey_to_non_char_key( VK_DECIMAL => Key::Unidentified(native_code), VK_DIVIDE => Key::Unidentified(native_code), - VK_F1 => Key::F1, - VK_F2 => Key::F2, - VK_F3 => Key::F3, - VK_F4 => Key::F4, - VK_F5 => Key::F5, - VK_F6 => Key::F6, - VK_F7 => Key::F7, - VK_F8 => Key::F8, - VK_F9 => Key::F9, - VK_F10 => Key::F10, - VK_F11 => Key::F11, - VK_F12 => Key::F12, - VK_F13 => Key::F13, - VK_F14 => Key::F14, - VK_F15 => Key::F15, - VK_F16 => Key::F16, - VK_F17 => Key::F17, - VK_F18 => Key::F18, - VK_F19 => Key::F19, - VK_F20 => Key::F20, - VK_F21 => Key::F21, - VK_F22 => Key::F22, - VK_F23 => Key::F23, - VK_F24 => Key::F24, + VK_F1 => Key::Named(NamedKey::F1), + VK_F2 => Key::Named(NamedKey::F2), + VK_F3 => Key::Named(NamedKey::F3), + VK_F4 => Key::Named(NamedKey::F4), + VK_F5 => Key::Named(NamedKey::F5), + VK_F6 => Key::Named(NamedKey::F6), + VK_F7 => Key::Named(NamedKey::F7), + VK_F8 => Key::Named(NamedKey::F8), + VK_F9 => Key::Named(NamedKey::F9), + VK_F10 => Key::Named(NamedKey::F10), + VK_F11 => Key::Named(NamedKey::F11), + VK_F12 => Key::Named(NamedKey::F12), + VK_F13 => Key::Named(NamedKey::F13), + VK_F14 => Key::Named(NamedKey::F14), + VK_F15 => Key::Named(NamedKey::F15), + VK_F16 => Key::Named(NamedKey::F16), + VK_F17 => Key::Named(NamedKey::F17), + VK_F18 => Key::Named(NamedKey::F18), + VK_F19 => Key::Named(NamedKey::F19), + VK_F20 => Key::Named(NamedKey::F20), + VK_F21 => Key::Named(NamedKey::F21), + VK_F22 => Key::Named(NamedKey::F22), + VK_F23 => Key::Named(NamedKey::F23), + VK_F24 => Key::Named(NamedKey::F24), VK_NAVIGATION_VIEW => Key::Unidentified(native_code), VK_NAVIGATION_MENU => Key::Unidentified(native_code), VK_NAVIGATION_UP => Key::Unidentified(native_code), @@ -870,44 +879,44 @@ fn vkey_to_non_char_key( VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), - VK_NUMLOCK => Key::NumLock, - VK_SCROLL => Key::ScrollLock, + VK_NUMLOCK => Key::Named(NamedKey::NumLock), + VK_SCROLL => Key::Named(NamedKey::ScrollLock), VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), //VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), VK_OEM_FJ_LOYA => Key::Unidentified(native_code), VK_OEM_FJ_ROYA => Key::Unidentified(native_code), - VK_LSHIFT => Key::Shift, - VK_RSHIFT => Key::Shift, - VK_LCONTROL => Key::Control, - VK_RCONTROL => Key::Control, - VK_LMENU => Key::Alt, + VK_LSHIFT => Key::Named(NamedKey::Shift), + VK_RSHIFT => Key::Named(NamedKey::Shift), + VK_LCONTROL => Key::Named(NamedKey::Control), + VK_RCONTROL => Key::Named(NamedKey::Control), + VK_LMENU => Key::Named(NamedKey::Alt), VK_RMENU => { if has_alt_graph { - Key::AltGraph + Key::Named(NamedKey::AltGraph) } else { - Key::Alt + Key::Named(NamedKey::Alt) } } - VK_BROWSER_BACK => Key::BrowserBack, - VK_BROWSER_FORWARD => Key::BrowserForward, - VK_BROWSER_REFRESH => Key::BrowserRefresh, - VK_BROWSER_STOP => Key::BrowserStop, - VK_BROWSER_SEARCH => Key::BrowserSearch, - VK_BROWSER_FAVORITES => Key::BrowserFavorites, - VK_BROWSER_HOME => Key::BrowserHome, - VK_VOLUME_MUTE => Key::AudioVolumeMute, - VK_VOLUME_DOWN => Key::AudioVolumeDown, - VK_VOLUME_UP => Key::AudioVolumeUp, - VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext, - VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious, - VK_MEDIA_STOP => Key::MediaStop, - VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause, - VK_LAUNCH_MAIL => Key::LaunchMail, - VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer, - VK_LAUNCH_APP1 => Key::LaunchApplication1, - VK_LAUNCH_APP2 => Key::LaunchApplication2, + VK_BROWSER_BACK => Key::Named(NamedKey::BrowserBack), + VK_BROWSER_FORWARD => Key::Named(NamedKey::BrowserForward), + VK_BROWSER_REFRESH => Key::Named(NamedKey::BrowserRefresh), + VK_BROWSER_STOP => Key::Named(NamedKey::BrowserStop), + VK_BROWSER_SEARCH => Key::Named(NamedKey::BrowserSearch), + VK_BROWSER_FAVORITES => Key::Named(NamedKey::BrowserFavorites), + VK_BROWSER_HOME => Key::Named(NamedKey::BrowserHome), + VK_VOLUME_MUTE => Key::Named(NamedKey::AudioVolumeMute), + VK_VOLUME_DOWN => Key::Named(NamedKey::AudioVolumeDown), + VK_VOLUME_UP => Key::Named(NamedKey::AudioVolumeUp), + VK_MEDIA_NEXT_TRACK => Key::Named(NamedKey::MediaTrackNext), + VK_MEDIA_PREV_TRACK => Key::Named(NamedKey::MediaTrackPrevious), + VK_MEDIA_STOP => Key::Named(NamedKey::MediaStop), + VK_MEDIA_PLAY_PAUSE => Key::Named(NamedKey::MediaPlayPause), + VK_LAUNCH_MAIL => Key::Named(NamedKey::LaunchMail), + VK_LAUNCH_MEDIA_SELECT => Key::Named(NamedKey::LaunchMediaPlayer), + VK_LAUNCH_APP1 => Key::Named(NamedKey::LaunchApplication1), + VK_LAUNCH_APP2 => Key::Named(NamedKey::LaunchApplication2), // This function only converts "non-printable" VK_OEM_1 => Key::Unidentified(native_code), @@ -955,7 +964,7 @@ fn vkey_to_non_char_key( VK_ICO_HELP => Key::Unidentified(native_code), VK_ICO_00 => Key::Unidentified(native_code), - VK_PROCESSKEY => Key::Process, + VK_PROCESSKEY => Key::Named(NamedKey::Process), VK_ICO_CLEAR => Key::Unidentified(native_code), VK_PACKET => Key::Unidentified(native_code), @@ -967,32 +976,32 @@ fn vkey_to_non_char_key( VK_OEM_WSCTRL => Key::Unidentified(native_code), VK_OEM_CUSEL => Key::Unidentified(native_code), - VK_OEM_ATTN => Key::Attn, + VK_OEM_ATTN => Key::Named(NamedKey::Attn), VK_OEM_FINISH => { if is_japanese { - Key::Katakana + Key::Named(NamedKey::Katakana) } else { // This matches IE and Firefox behaviour according to // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values - // At the time of writing, there is no `Key::Finish` variant as + // At the time of writing, there is no `NamedKey::Finish` variant as // Finish is not mentionned at https://w3c.github.io/uievents-key/ // Also see: https://github.com/pyfisch/keyboard-types/issues/9 Key::Unidentified(native_code) } } - VK_OEM_COPY => Key::Copy, - VK_OEM_AUTO => Key::Hankaku, - VK_OEM_ENLW => Key::Zenkaku, - VK_OEM_BACKTAB => Key::Romaji, - VK_ATTN => Key::KanaMode, - VK_CRSEL => Key::CrSel, - VK_EXSEL => Key::ExSel, - VK_EREOF => Key::EraseEof, - VK_PLAY => Key::Play, - VK_ZOOM => Key::ZoomToggle, + VK_OEM_COPY => Key::Named(NamedKey::Copy), + VK_OEM_AUTO => Key::Named(NamedKey::Hankaku), + VK_OEM_ENLW => Key::Named(NamedKey::Zenkaku), + VK_OEM_BACKTAB => Key::Named(NamedKey::Romaji), + VK_ATTN => Key::Named(NamedKey::KanaMode), + VK_CRSEL => Key::Named(NamedKey::CrSel), + VK_EXSEL => Key::Named(NamedKey::ExSel), + VK_EREOF => Key::Named(NamedKey::EraseEof), + VK_PLAY => Key::Named(NamedKey::Play), + VK_ZOOM => Key::Named(NamedKey::ZoomToggle), VK_NONAME => Key::Unidentified(native_code), VK_PA1 => Key::Unidentified(native_code), - VK_OEM_CLEAR => Key::Clear, + VK_OEM_CLEAR => Key::Named(NamedKey::Clear), _ => Key::Unidentified(native_code), } } diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index ee256694c9..68118aed44 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -16,7 +16,7 @@ pub(crate) use self::{ }; pub use self::icon::WinIcon as PlatformIcon; -pub(self) use crate::platform_impl::Fullscreen; +use crate::platform_impl::Fullscreen; use crate::event::DeviceId as RootDeviceId; use crate::icon::Icon; @@ -30,6 +30,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub no_redirection_bitmap: bool, pub drag_and_drop: bool, pub skip_taskbar: bool, + pub class_name: String, pub decoration_shadow: bool, } @@ -42,6 +43,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { no_redirection_bitmap: false, drag_and_drop: true, skip_taskbar: false, + class_name: "Window Class".to_string(), decoration_shadow: false, } } @@ -50,12 +52,6 @@ impl Default for PlatformSpecificWindowBuilderAttributes { unsafe impl Send for PlatformSpecificWindowBuilderAttributes {} unsafe impl Sync for PlatformSpecificWindowBuilderAttributes {} -// Cursor name in UTF-16. Used to set cursor in `WM_SETCURSOR`. -#[derive(Debug, Clone, Copy)] -pub struct Cursor(pub *const u16); -unsafe impl Send for Cursor {} -unsafe impl Sync for Cursor {} - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId(u32); @@ -152,21 +148,24 @@ const fn hiword(x: u32) -> u16 { #[inline(always)] unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { #[cfg(target_pointer_width = "64")] - return windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex); + return unsafe { windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex) }; #[cfg(target_pointer_width = "32")] - return windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize; + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize + }; } #[inline(always)] unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: isize) -> isize { #[cfg(target_pointer_width = "64")] - return windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, nindex, dwnewlong); + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, nindex, dwnewlong) + }; #[cfg(target_pointer_width = "32")] - return windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW( - hwnd, - nindex, - dwnewlong as i32, - ) as isize; + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW(hwnd, nindex, dwnewlong as i32) + as isize + }; } #[macro_use] diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index 7b38fb195b..1548bc05e1 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -101,7 +101,7 @@ unsafe extern "system" fn monitor_enum_proc( data: LPARAM, ) -> BOOL { let monitors = data as *mut VecDeque; - (*monitors).push_back(MonitorHandle::new(hmonitor)); + unsafe { (*monitors).push_back(MonitorHandle::new(hmonitor)) }; true.into() // continue enumeration } @@ -209,11 +209,15 @@ impl MonitorHandle { #[inline] pub fn position(&self) -> PhysicalPosition { - let rc_monitor = get_monitor_info(self.0).unwrap().monitorInfo.rcMonitor; - PhysicalPosition { - x: rc_monitor.left, - y: rc_monitor.top, - } + get_monitor_info(self.0) + .map(|info| { + let rc_monitor = info.monitorInfo.rcMonitor; + PhysicalPosition { + x: rc_monitor.left, + y: rc_monitor.top, + } + }) + .unwrap_or(PhysicalPosition { x: 0, y: 0 }) } #[inline] @@ -226,37 +230,45 @@ impl MonitorHandle { // EnumDisplaySettingsExW can return duplicate values (or some of the // fields are probably changing, but we aren't looking at those fields // anyway), so we're using a BTreeSet deduplicate - let mut modes = BTreeSet::new(); - let mut i = 0; + let mut modes = BTreeSet::::new(); + let mod_map = |mode: RootVideoMode| mode.video_mode; + + let monitor_info = match get_monitor_info(self.0) { + Ok(monitor_info) => monitor_info, + Err(error) => { + log::warn!("Error from get_monitor_info: {error}"); + return modes.into_iter().map(mod_map); + } + }; + let device_name = monitor_info.szDevice.as_ptr(); + + let mut i = 0; loop { - unsafe { - let monitor_info = get_monitor_info(self.0).unwrap(); - let device_name = monitor_info.szDevice.as_ptr(); - let mut mode: DEVMODEW = mem::zeroed(); - mode.dmSize = mem::size_of_val(&mode) as u16; - if EnumDisplaySettingsExW(device_name, i, &mut mode, 0) == false.into() { - break; - } - i += 1; - - const REQUIRED_FIELDS: u32 = - DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; - assert!(has_flag(mode.dmFields, REQUIRED_FIELDS)); - - // Use Ord impl of RootVideoMode - modes.insert(RootVideoMode { - video_mode: VideoMode { - size: (mode.dmPelsWidth, mode.dmPelsHeight), - bit_depth: mode.dmBitsPerPel as u16, - refresh_rate_millihertz: mode.dmDisplayFrequency * 1000, - monitor: self.clone(), - native_video_mode: Box::new(mode), - }, - }); + let mut mode: DEVMODEW = unsafe { mem::zeroed() }; + mode.dmSize = mem::size_of_val(&mode) as u16; + if unsafe { EnumDisplaySettingsExW(device_name, i, &mut mode, 0) } == false.into() { + break; } + + const REQUIRED_FIELDS: u32 = + DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + assert!(has_flag(mode.dmFields, REQUIRED_FIELDS)); + + // Use Ord impl of RootVideoMode + modes.insert(RootVideoMode { + video_mode: VideoMode { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate_millihertz: mode.dmDisplayFrequency * 1000, + monitor: self.clone(), + native_video_mode: Box::new(mode), + }, + }); + + i += 1; } - modes.into_iter().map(|mode| mode.video_mode) + modes.into_iter().map(mod_map) } } diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 69f2d0355d..897b2a29b1 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -11,19 +11,29 @@ use windows_sys::Win32::{ UI::{ Input::{ GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList, + KeyboardAndMouse::{MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT}, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, - 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, + RAWINPUTHEADER, RAWKEYBOARD, 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, - RI_MOUSE_MIDDLE_BUTTON_UP, RI_MOUSE_RIGHT_BUTTON_DOWN, RI_MOUSE_RIGHT_BUTTON_UP, + RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, + RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, + RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP, + RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP, }, }, }; -use crate::{event::ElementState, event_loop::DeviceEvents, platform_impl::platform::util}; +use crate::{ + event::ElementState, + event_loop::DeviceEvents, + keyboard::{KeyCode, PhysicalKey}, + platform::scancode::PhysicalKeyExtScancode, + platform_impl::platform::util, +}; #[allow(dead_code)] pub fn get_raw_input_device_list() -> Option> { @@ -209,22 +219,108 @@ fn button_flags_to_element_state( } } -pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option; 3] { +pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option; 5] { [ - button_flags_to_element_state( - button_flags, - RI_MOUSE_LEFT_BUTTON_DOWN, - RI_MOUSE_LEFT_BUTTON_UP, - ), - button_flags_to_element_state( - button_flags, - RI_MOUSE_MIDDLE_BUTTON_DOWN, - RI_MOUSE_MIDDLE_BUTTON_UP, - ), - button_flags_to_element_state( - button_flags, - RI_MOUSE_RIGHT_BUTTON_DOWN, - RI_MOUSE_RIGHT_BUTTON_UP, - ), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP), ] } + +pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { + let extension = { + if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) { + 0xE000 + } else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) { + 0xE100 + } else { + 0x0000 + } + }; + let scancode = if keyboard.MakeCode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 } + } else { + keyboard.MakeCode | extension + }; + if scancode == 0xE11D || scancode == 0xE02A { + // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing + // Ctrl+NumLock. + // This equvalence means that if the user presses Pause, the keyboard will emit two + // subsequent keypresses: + // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) + // 2, 0x0045 - Which on its own can be interpreted as Pause + // + // There's another combination which isn't quite an equivalence: + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing + // PrtSc (print screen) produces the following sequence: + // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) + // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on + // its own it can be interpreted as PrtSc + // + // For this reason, if we encounter the first keypress, we simply ignore it, trusting + // that there's going to be another event coming, from which we can extract the + // appropriate key. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + return None; + } + let physical_key = if keyboard.VKey == VK_NUMLOCK { + // Historically, the NumLock and the Pause key were one and the same physical key. + // The user could trigger Pause by pressing Ctrl+NumLock. + // Now these are often physically separate and the two keys can be differentiated by + // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. + // + // However in this event, both keys are reported as 0x0045 even on modern hardware. + // Therefore we use the virtual key instead to determine whether it's a NumLock and + // set the KeyCode accordingly. + // + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + PhysicalKey::Code(KeyCode::NumLock) + } else { + PhysicalKey::from_scancode(scancode as u32) + }; + if keyboard.VKey == VK_SHIFT { + if let PhysicalKey::Code(code) = physical_key { + match code { + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9 => { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event doesn't + // have any information to tell whether it's the left shift or the right shift + // that needs to get the fake release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return None; + } + _ => (), + } + } + } + + Some(physical_key) +} diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 355b366a19..66e6be0980 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -13,7 +13,7 @@ use once_cell::sync::Lazy; use windows_sys::{ core::{HRESULT, PCWSTR}, Win32::{ - Foundation::{BOOL, HMODULE, HWND, RECT}, + Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT}, Graphics::Gdi::{ClientToScreen, HMONITOR}, System::{ LibraryLoader::{GetProcAddress, LoadLibraryA}, @@ -21,7 +21,10 @@ use windows_sys::{ }, UI::{ HiDpi::{DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS}, - Input::KeyboardAndMouse::GetActiveWindow, + Input::{ + KeyboardAndMouse::GetActiveWindow, + Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO}, + }, WindowsAndMessaging::{ ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement, GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, @@ -191,7 +194,9 @@ pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR { } } -// Helper function to dynamically load function pointer. +// Helper function to dynamically load function pointer as some functions +// may not be available on all Windows platforms supported by winit. +// // `library` and `function` must be zero-terminated. pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { assert_eq!(library.chars().last(), Some('\0')); @@ -237,6 +242,26 @@ pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( dpi: u32, ) -> BOOL; +pub type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointerId: u32, + entriesCount: *mut u32, + pointerCount: *mut u32, + pointerInfo: *mut POINTER_INFO, +) -> BOOL; + +pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL; +pub type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointerDeviceRect: *mut RECT, + displayRect: *mut RECT, +) -> BOOL; + +pub type GetPointerTouchInfo = + unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL; + +pub type GetPointerPenInfo = + unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; + pub static GET_DPI_FOR_WINDOW: Lazy> = Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); pub static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy> = @@ -251,3 +276,13 @@ 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)); +pub static GET_POINTER_FRAME_INFO_HISTORY: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory)); +pub static SKIP_POINTER_FRAME_MESSAGES: Lazy> = + Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages)); +pub static GET_POINTER_DEVICE_RECTS: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects)); +pub static GET_POINTER_TOUCH_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo)); +pub static GET_POINTER_PEN_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo)); diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 23ec2947c2..3f2d9ae5e8 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1,8 +1,5 @@ #![cfg(windows_platform)] -use raw_window_handle::{ - RawDisplayHandle, RawWindowHandle, Win32WindowHandle, WindowsDisplayHandle, -}; use std::{ cell::Cell, ffi::c_void, @@ -14,8 +11,7 @@ use std::{ use windows_sys::Win32::{ Foundation::{ - HMODULE, HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, - WPARAM, + HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM, }, Graphics::{ Dwm::{DwmEnableBlurBehindWindow, DWM_BB_BLURREGION, DWM_BB_ENABLE, DWM_BLURBEHIND}, @@ -41,14 +37,19 @@ use windows_sys::Win32::{ Touch::{RegisterTouchWindow, TWF_WANTPALM}, }, WindowsAndMessaging::{ - CreateWindowExW, FlashWindowEx, GetClientRect, GetCursorPos, GetForegroundWindow, - GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, GetWindowTextW, - IsWindowVisible, LoadCursorW, PeekMessageW, PostMessageW, RegisterClassExW, SetCursor, - SetCursorPos, SetForegroundWindow, SetWindowDisplayAffinity, SetWindowPlacement, - SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, FLASHWINFO, - FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, GWLP_HINSTANCE, HTCAPTION, - NID_READY, PM_NOREMOVE, SM_DIGITIZER, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, - SWP_NOZORDER, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WNDCLASSEXW, + CreateWindowExW, EnableMenuItem, FlashWindowEx, GetClientRect, GetCursorPos, + GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, + GetWindowTextLengthW, GetWindowTextW, IsWindowVisible, LoadCursorW, PeekMessageW, + PostMessageW, RegisterClassExW, SetCursor, SetCursorPos, SetForegroundWindow, + SetMenuDefaultItem, SetWindowDisplayAffinity, SetWindowPlacement, SetWindowPos, + SetWindowTextW, TrackPopupMenu, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, FLASHWINFO, + FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, GWLP_HINSTANCE, HTBOTTOM, + HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, + MENU_ITEM_STATE, MFS_DISABLED, MFS_ENABLED, MF_BYCOMMAND, NID_READY, PM_NOREMOVE, + SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_MOVE, SC_RESTORE, SC_SIZE, SM_DIGITIZER, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, TPM_LEFTALIGN, + TPM_RETURNCMD, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SYSCOMMAND, + WNDCLASSEXW, }, }, }; @@ -82,7 +83,7 @@ use crate::{ /// The Win32 implementation of the main `Window` object. pub(crate) struct Window { /// Main handle for the window. - window: WindowWrapper, + window: HWND, /// The current window state. window_state: Arc>, @@ -104,6 +105,16 @@ impl Window { unsafe { init(w_attr, pl_attr, event_loop) } } + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + // TODO: Use `thread_executor` here + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + // TODO: Use `thread_executor` here + f(self) + } + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { self.window_state.lock().unwrap() } @@ -115,15 +126,26 @@ impl Window { } } - pub fn set_transparent(&self, _transparent: bool) {} + pub fn set_transparent(&self, transparent: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::TRANSPARENT, transparent) + }); + }); + } + + pub fn set_blur(&self, _blur: bool) {} #[inline] pub fn set_visible(&self, visible: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::VISIBLE, visible) }); }); @@ -131,16 +153,21 @@ impl Window { #[inline] pub fn is_visible(&self) -> Option { - Some(unsafe { IsWindowVisible(self.window.0) == 1 }) + Some(unsafe { IsWindowVisible(self.window) == 1 }) } #[inline] pub fn request_redraw(&self) { + // NOTE: mark that we requested a redraw to handle requests during `WM_PAINT` handling. + self.window_state.lock().unwrap().redraw_requested = true; unsafe { RedrawWindow(self.hwnd(), ptr::null(), 0, RDW_INTERNALPAINT); } } + #[inline] + pub fn pre_present_notify(&self) {} + #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { util::WindowArea::Outer.get_rect(self.hwnd()) @@ -162,10 +189,10 @@ impl Window { let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); let window_state = Arc::clone(&self.window_state); - let window = self.window.clone(); + let window = self.window; self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::MAXIMIZED, false) }); }); @@ -210,21 +237,25 @@ impl Window { } #[inline] - pub fn set_inner_size(&self, size: Size) { + pub fn request_inner_size(&self, size: Size) -> Option> { let scale_factor = self.scale_factor(); let physical_size = size.to_physical::(scale_factor); - let window_state = Arc::clone(&self.window_state); - let window = self.window.clone(); - self.thread_executor.execute_in_thread(move || { - let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { - f.set(WindowFlags::MAXIMIZED, false) - }); - }); - let window_flags = self.window_state_lock().window_flags; window_flags.set_size(self.hwnd(), physical_size); + + if physical_size != self.inner_size() { + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + } + + None } #[inline] @@ -232,7 +263,7 @@ impl Window { self.window_state_lock().min_size = size; // Make windows re-check the window size bounds. let size = self.inner_size(); - self.set_inner_size(size.into()); + self.request_inner_size(size.into()); } #[inline] @@ -240,7 +271,7 @@ impl Window { self.window_state_lock().max_size = size; // Make windows re-check the window size bounds. let size = self.inner_size(); - self.set_inner_size(size.into()); + self.request_inner_size(size.into()); } #[inline] @@ -253,12 +284,12 @@ impl Window { #[inline] pub fn set_resizable(&self, resizable: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::RESIZABLE, resizable) }); }); @@ -272,12 +303,12 @@ impl Window { #[inline] pub fn set_enabled_buttons(&self, buttons: WindowButtons) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set( WindowFlags::MINIMIZABLE, buttons.contains(WindowButtons::MINIMIZE), @@ -311,25 +342,55 @@ impl Window { /// Returns the `hwnd` of this window. #[inline] pub fn hwnd(&self) -> HWND { - self.window.0 + self.window + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::Win32Handle::empty(); + window_handle.hwnd = self.window as *mut _; + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = hinstance as *mut _; + rwh_04::RawWindowHandle::Win32(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::Win32WindowHandle::empty(); + window_handle.hwnd = self.window as *mut _; + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = hinstance as *mut _; + rwh_05::RawWindowHandle::Win32(window_handle) } + #[cfg(feature = "rwh_05")] #[inline] - pub fn hinstance(&self) -> HMODULE { - unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) } + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) } + #[cfg(feature = "rwh_06")] #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - 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) + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe { + // SAFETY: Handle will never be zero. + std::num::NonZeroIsize::new_unchecked(self.window) + }); + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = std::num::NonZeroIsize::new(hinstance); + Ok(rwh_06::RawWindowHandle::Win32(window_handle)) } + #[cfg(feature = "rwh_06")] #[inline] - pub fn raw_display_handle(&self) -> RawDisplayHandle { - RawDisplayHandle::Windows(WindowsDisplayHandle::empty()) + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) } #[inline] @@ -351,7 +412,7 @@ impl Window { } }; - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); let (tx, rx) = channel(); @@ -361,7 +422,7 @@ impl Window { .lock() .unwrap() .mouse - .set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, confine)) + .set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine)) .map_err(|e| ExternalError::Os(os_error!(e))); let _ = tx.send(result); }); @@ -370,7 +431,7 @@ impl Window { #[inline] pub fn set_cursor_visible(&self, visible: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); let (tx, rx) = channel(); @@ -380,7 +441,7 @@ impl Window { .lock() .unwrap() .mouse - .set_cursor_flags(window.0, |f| f.set(CursorFlags::HIDDEN, !visible)) + .set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible)) .map_err(|e| e.to_string()); let _ = tx.send(result); }); @@ -409,44 +470,154 @@ impl Window { Ok(()) } - #[inline] - pub fn drag_window(&self) -> Result<(), ExternalError> { - unsafe { + unsafe fn handle_os_dragging(&self, wparam: WPARAM) { + let window = self.window; + let window_state = self.window_state.clone(); + + self.thread_executor.execute_in_thread(move || { + { + let mut guard = window_state.lock().unwrap(); + if !guard.dragging { + guard.dragging = true; + } else { + return; + } + } + let points = { - let mut pos = mem::zeroed(); - GetCursorPos(&mut pos); + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; pos }; let points = POINTS { x: points.x as i16, y: points.y as i16, }; - ReleaseCapture(); - self.window_state_lock().dragging = true; + // ReleaseCapture needs to execute on the main thread + unsafe { ReleaseCapture() }; - PostMessageW( - self.hwnd(), - WM_NCLBUTTONDOWN, - HTCAPTION as WPARAM, - &points as *const _ as LPARAM, - ); + unsafe { + PostMessageW( + window, + WM_NCLBUTTONDOWN, + wparam, + &points as *const _ as LPARAM, + ) + }; + }); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(HTCAPTION as WPARAM); } Ok(()) } #[inline] - pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(match direction { + ResizeDirection::East => HTRIGHT, + ResizeDirection::North => HTTOP, + ResizeDirection::NorthEast => HTTOPRIGHT, + ResizeDirection::NorthWest => HTTOPLEFT, + ResizeDirection::South => HTBOTTOM, + ResizeDirection::SouthEast => HTBOTTOMRIGHT, + ResizeDirection::SouthWest => HTBOTTOMLEFT, + ResizeDirection::West => HTLEFT, + } as WPARAM); + } + + Ok(()) + } + + unsafe fn handle_showing_window_menu(&self, position: Position) { + unsafe { + let point = { + let mut point = POINT { x: 0, y: 0 }; + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + point.x = x; + point.y = y; + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + warn!("Can't convert client-area coordinates to screen coordinates when showing window menu."); + return; + } + point + }; + + // get the current system menu + let h_menu = GetSystemMenu(self.hwnd(), 0); + if h_menu == 0 { + warn!("The corresponding window doesn't have a system menu"); + // This situation should not be treated as an error so just return without showing menu. + return; + } + + fn enable(b: bool) -> MENU_ITEM_STATE { + if b { + MFS_ENABLED + } else { + MFS_DISABLED + } + } + + // Change the menu items according to the current window status. + + let restore_btn = enable(self.is_maximized() && self.is_resizable()); + let size_btn = enable(!self.is_maximized() && self.is_resizable()); + let maximize_btn = enable(!self.is_maximized() && self.is_resizable()); + + EnableMenuItem(h_menu, SC_RESTORE, MF_BYCOMMAND | restore_btn); + EnableMenuItem(h_menu, SC_MOVE, MF_BYCOMMAND | enable(!self.is_maximized())); + EnableMenuItem(h_menu, SC_SIZE, MF_BYCOMMAND | size_btn); + EnableMenuItem(h_menu, SC_MINIMIZE, MF_BYCOMMAND | MFS_ENABLED); + EnableMenuItem(h_menu, SC_MAXIMIZE, MF_BYCOMMAND | maximize_btn); + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MFS_ENABLED); + + // Set the default menu item. + SetMenuDefaultItem(h_menu, SC_CLOSE, 0); + + // Popup the system menu at the position. + let result = TrackPopupMenu( + h_menu, + TPM_RETURNCMD | TPM_LEFTALIGN, // for now im using LTR, but we have to use user layout direction + point.x, + point.y, + 0, + self.hwnd(), + std::ptr::null_mut(), + ); + + if result == 0 { + // User canceled the menu, no need to continue. + return; + } + + // Send the command that the user select to the corresponding window. + if PostMessageW(self.hwnd(), WM_SYSCOMMAND, result as _, 0) == 0 { + warn!("Can't post the system menu message to the window."); + } + } + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + unsafe { + self.handle_showing_window_menu(position); + } } #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest) }); }); @@ -461,7 +632,7 @@ impl Window { #[inline] pub fn set_minimized(&self, minimized: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); let is_minimized = util::is_minimized(self.hwnd()); @@ -471,7 +642,7 @@ impl Window { WindowState::set_window_flags_in_place(&mut window_state.lock().unwrap(), |f| { f.set(WindowFlags::MINIMIZED, is_minimized) }); - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::MINIMIZED, minimized) }); }); @@ -484,12 +655,12 @@ impl Window { #[inline] pub fn set_maximized(&self, maximized: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::MAXIMIZED, maximized) }); }); @@ -509,14 +680,24 @@ impl Window { #[inline] pub fn set_fullscreen(&self, fullscreen: Option) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); let mut window_state_lock = window_state.lock().unwrap(); let old_fullscreen = window_state_lock.fullscreen.clone(); - if window_state_lock.fullscreen == fullscreen { - return; + + match (&old_fullscreen, &fullscreen) { + // Return if we already are in the same fullscreen mode + _ if old_fullscreen == fullscreen => return, + // Return if saved Borderless(monitor) is the same as current monitor when requested fullscreen is Borderless(None) + (Some(Fullscreen::Borderless(Some(monitor))), Some(Fullscreen::Borderless(None))) + if *monitor == monitor::current_monitor(window) => + { + return + } + _ => {} } + window_state_lock.fullscreen = fullscreen.clone(); drop(window_state_lock); @@ -579,7 +760,7 @@ impl Window { } // Update window style - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set( WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, matches!(fullscreen, Some(Fullscreen::Exclusive(_))), @@ -595,7 +776,7 @@ impl Window { // this needs to be called before the below fullscreen SetWindowPos as this itself // will generate WM_SIZE messages of the old window size that can race with what we set below unsafe { - taskbar_mark_fullscreen(window.0, fullscreen.is_some()); + taskbar_mark_fullscreen(window, fullscreen.is_some()); } // Update window bounds @@ -604,7 +785,7 @@ impl Window { // Save window bounds before entering fullscreen let placement = unsafe { let mut placement = mem::zeroed(); - GetWindowPlacement(window.0, &mut placement); + GetWindowPlacement(window, &mut placement); placement }; @@ -613,7 +794,7 @@ impl Window { let monitor = match &fullscreen { Fullscreen::Exclusive(video_mode) => video_mode.monitor(), Fullscreen::Borderless(Some(monitor)) => monitor.clone(), - Fullscreen::Borderless(None) => monitor::current_monitor(window.0), + Fullscreen::Borderless(None) => monitor::current_monitor(window), }; let position: (i32, i32) = monitor.position().into(); @@ -621,7 +802,7 @@ impl Window { unsafe { SetWindowPos( - window.0, + window, 0, position.0, position.1, @@ -629,7 +810,7 @@ impl Window { size.1 as i32, SWP_ASYNCWINDOWPOS | SWP_NOZORDER, ); - InvalidateRgn(window.0, 0, false.into()); + InvalidateRgn(window, 0, false.into()); } } None => { @@ -637,8 +818,8 @@ impl Window { if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() { drop(window_state_lock); unsafe { - SetWindowPlacement(window.0, &placement); - InvalidateRgn(window.0, 0, false.into()); + SetWindowPlacement(window, &placement); + InvalidateRgn(window, 0, false.into()); } } } @@ -648,12 +829,12 @@ impl Window { #[inline] pub fn set_decorations(&self, decorations: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::MARKER_DECORATIONS, decorations) }); }); @@ -669,12 +850,12 @@ impl Window { #[inline] pub fn set_window_level(&self, level: WindowLevel) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set( WindowFlags::ALWAYS_ON_TOP, level == WindowLevel::AlwaysOnTop, @@ -723,17 +904,22 @@ impl Window { #[inline] pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { - unsafe { - ImeContext::current(self.hwnd()).set_ime_cursor_area(spot, size, self.scale_factor()); - } + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + let scale_factor = state.lock().unwrap().scale_factor; + ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor); + }); } #[inline] pub fn set_ime_allowed(&self, allowed: bool) { - self.window_state_lock().ime_allowed = allowed; - unsafe { - ImeContext::set_ime_allowed(self.hwnd(), allowed); - } + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + state.lock().unwrap().ime_allowed = allowed; + ImeContext::set_ime_allowed(window, allowed); + }) } #[inline] @@ -741,14 +927,13 @@ impl Window { #[inline] pub fn request_user_attention(&self, request_type: Option) { - let window = self.window.clone(); + let window = self.window; let active_window_handle = unsafe { GetActiveWindow() }; - if window.0 == active_window_handle { + if window == active_window_handle { return; } self.thread_executor.execute_in_thread(move || unsafe { - let _ = &window; let (flags, count) = request_type .map(|ty| match ty { UserAttentionType::Critical => (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX), @@ -758,7 +943,7 @@ impl Window { let flash_info = FLASHWINFO { cbSize: mem::size_of::() as u32, - hwnd: window.0, + hwnd: window, dwFlags: flags, uCount: count, dwTimeout: 0, @@ -769,7 +954,7 @@ impl Window { #[inline] pub fn set_theme(&self, theme: Option) { - try_theme(self.window.0, theme); + try_theme(self.window, theme); } #[inline] @@ -784,9 +969,9 @@ impl Window { } pub fn title(&self) -> String { - let len = unsafe { GetWindowTextLengthW(self.window.0) } + 1; + let len = unsafe { GetWindowTextLengthW(self.window) } + 1; let mut buf = vec![0; len as usize]; - unsafe { GetWindowTextW(self.window.0, buf.as_mut_ptr(), len) }; + unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) }; util::decode_wide(&buf).to_string_lossy().to_string() } @@ -798,12 +983,12 @@ impl Window { #[inline] pub fn set_undecorated_shadow(&self, shadow: bool) { - let window = self.window.clone(); + let window = self.window; let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; - WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) }); }); @@ -811,15 +996,14 @@ impl Window { #[inline] pub fn focus_window(&self) { - let window = self.window.clone(); let window_flags = self.window_state_lock().window_flags(); let is_visible = window_flags.contains(WindowFlags::VISIBLE); let is_minimized = util::is_minimized(self.hwnd()); - let is_foreground = window.0 == unsafe { GetForegroundWindow() }; + let is_foreground = self.window == unsafe { GetForegroundWindow() }; if is_visible && !is_minimized && !is_foreground { - unsafe { force_window_active(window.0) }; + unsafe { force_window_active(self.window) }; } } @@ -869,18 +1053,6 @@ impl Drop for Window { } } -/// A simple non-owning wrapper around a window. -#[doc(hidden)] -#[derive(Clone)] -pub struct WindowWrapper(HWND); - -// Send and Sync are not implemented for HWND and HDC, we have to wrap it and implement them manually. -// For more info see: -// https://github.com/retep998/winapi-rs/issues/360 -// https://github.com/retep998/winapi-rs/issues/396 -unsafe impl Sync for WindowWrapper {} -unsafe impl Send for WindowWrapper {} - pub(super) struct InitData<'a, T: 'static> { // inputs pub event_loop: &'a EventLoopWindowTarget, @@ -895,13 +1067,13 @@ impl<'a, T: 'static> InitData<'a, T> { unsafe fn create_window(&self, window: HWND) -> Window { // Register for touch events if applicable { - let digitizer = GetSystemMetrics(SM_DIGITIZER) as u32; + let digitizer = unsafe { GetSystemMetrics(SM_DIGITIZER) as u32 }; if digitizer & NID_READY != 0 { - RegisterTouchWindow(window, TWF_WANTPALM); + unsafe { RegisterTouchWindow(window, TWF_WANTPALM) }; } } - let dpi = hwnd_dpi(window); + let dpi = unsafe { hwnd_dpi(window) }; let scale_factor = dpi_to_scale_factor(dpi); // If the system theme is dark, we need to set the window theme now @@ -925,10 +1097,10 @@ impl<'a, T: 'static> InitData<'a, T> { enable_non_client_dpi_scaling(window); - ImeContext::set_ime_allowed(window, false); + unsafe { ImeContext::set_ime_allowed(window, false) }; Window { - window: WindowWrapper(window), + window, window_state, thread_executor: self.event_loop.create_thread_executor(), } @@ -936,7 +1108,7 @@ impl<'a, T: 'static> InitData<'a, T> { unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { let file_drop_handler = if self.pl_attribs.drag_and_drop { - let ole_init_result = OleInitialize(ptr::null_mut()); + let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; // It is ok if the initialize result is `S_FALSE` because it might happen that // multiple windows are created on the same thread. if ole_init_result == OLE_E_WRONGCOMPOBJ { @@ -951,7 +1123,7 @@ impl<'a, T: 'static> InitData<'a, T> { let file_drop_runner = self.event_loop.runner_shared.clone(); let file_drop_handler = FileDropHandler::new( - win.window.0, + win.window, Box::new(move |event| { if let Ok(e) = event.map_nonuser_event() { file_drop_runner.send_event(e) @@ -960,16 +1132,17 @@ impl<'a, T: 'static> InitData<'a, T> { ); let handler_interface_ptr = - &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void; + unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void }; - assert_eq!(RegisterDragDrop(win.window.0, handler_interface_ptr), S_OK); + assert_eq!( + unsafe { RegisterDragDrop(win.window, handler_interface_ptr) }, + S_OK + ); Some(file_drop_handler) } else { None }; - self.event_loop.runner_shared.register_window(win.window.0); - event_loop::WindowData { window_state: win.window_state.clone(), event_loop_runner: self.event_loop.runner_shared.clone(), @@ -985,8 +1158,8 @@ impl<'a, T: 'static> InitData<'a, T> { pub unsafe fn on_nccreate(&mut self, window: HWND) -> Option { let runner = self.event_loop.runner_shared.clone(); let result = runner.catch_unwind(|| { - let window = self.create_window(window); - let window_data = self.create_window_data(&window); + let window = unsafe { self.create_window(window) }; + let window_data = unsafe { self.create_window_data(&window) }; (window, window_data) }); @@ -1003,7 +1176,7 @@ impl<'a, T: 'static> InitData<'a, T> { // making the window transparent if self.attributes.transparent && !self.pl_attribs.no_redirection_bitmap { // Empty region for the blur effect, so the window is fully transparent - let region = CreateRectRgn(0, 0, -1, -1); + let region = unsafe { CreateRectRgn(0, 0, -1, -1) }; let bb = DWM_BLURBEHIND { dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION, @@ -1011,14 +1184,14 @@ impl<'a, T: 'static> InitData<'a, T> { hRgnBlur: region, fTransitionOnMaximized: false.into(), }; - let hr = DwmEnableBlurBehindWindow(win.hwnd(), &bb); + let hr = unsafe { DwmEnableBlurBehindWindow(win.hwnd(), &bb) }; if hr < 0 { warn!( "Setting transparent window is failed. HRESULT Code: 0x{:X}", hr ); } - DeleteObject(region); + unsafe { DeleteObject(region) }; } win.set_skip_taskbar(self.pl_attribs.skip_taskbar); @@ -1037,28 +1210,17 @@ impl<'a, T: 'static> InitData<'a, T> { win.set_enabled_buttons(attributes.enabled_buttons); - if attributes.fullscreen.is_some() { - win.set_fullscreen(attributes.fullscreen.map(Into::into)); - force_window_active(win.window.0); - } else { - let size = attributes - .inner_size - .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); - 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 - // `Window::set_inner_size` changes MAXIMIZED to false. - win.set_maximized(true); - } - } + let size = attributes + .inner_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + 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.request_inner_size(clamped_size); // let margins = MARGINS { // cxLeftWidth: 1, @@ -1083,7 +1245,8 @@ where { let title = util::encode_wide(&attributes.title); - let class_name = register_window_class::(); + let class_name = util::encode_wide(&pl_attribs.class_name); + unsafe { register_window_class::(&class_name) }; let mut window_flags = WindowFlags::empty(); window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); @@ -1111,27 +1274,36 @@ where // so the diffing later can work. window_flags.set(WindowFlags::CLOSABLE, true); - let parent = match attributes.parent_window { - Some(RawWindowHandle::Win32(handle)) => { + let mut fallback_parent = || match pl_attribs.owner { + Some(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + } + None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + } + }; + + #[cfg(feature = "rwh_06")] + let parent = match attributes.parent_window.0 { + Some(rwh_06::RawWindowHandle::Win32(handle)) => { window_flags.set(WindowFlags::CHILD, true); if pl_attribs.menu.is_some() { warn!("Setting a menu on a child window is unsupported"); } - Some(handle.hwnd as HWND) + Some(handle.hwnd.get() as HWND) } Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"), - None => match pl_attribs.owner { - Some(parent) => { - window_flags.set(WindowFlags::POPUP, true); - Some(parent) - } - None => { - window_flags.set(WindowFlags::ON_TASKBAR, true); - None - } - }, + None => fallback_parent(), }; + #[cfg(not(feature = "rwh_06"))] + let parent = fallback_parent(); + + let fullscreen = attributes.fullscreen.clone(); + let maximized = attributes.maximized; + let mut initdata = InitData { event_loop, attributes, @@ -1141,20 +1313,22 @@ where }; let (style, ex_style) = window_flags.to_window_styles(); - let handle = CreateWindowExW( - ex_style, - class_name.as_ptr(), - title.as_ptr(), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - parent.unwrap_or(0), - pl_attribs.menu.unwrap_or(0), - util::get_instance_handle(), - &mut initdata as *mut _ as *mut _, - ); + let handle = unsafe { + CreateWindowExW( + ex_style, + class_name.as_ptr(), + title.as_ptr(), + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent.unwrap_or(0), + pl_attribs.menu.unwrap_or(0), + util::get_instance_handle(), + &mut initdata as *mut _ as *mut _, + ) + }; // If the window creation in `InitData` panicked, then should resume panicking here if let Err(panic_error) = event_loop.runner_shared.take_panic_error() { @@ -1167,12 +1341,21 @@ where // If the handle is non-null, then window creation must have succeeded, which means // that we *must* have populated the `InitData.window` field. - Ok(initdata.window.unwrap()) -} + let win = initdata.window.unwrap(); -unsafe fn register_window_class() -> Vec { - let class_name = util::encode_wide("Window Class"); + // Need to set FULLSCREEN or MAXIMIZED after CreateWindowEx + // This is because if the size is changed in WM_CREATE, the restored size will be stored in that size. + if fullscreen.0.is_some() { + win.set_fullscreen(fullscreen.0.map(Into::into)); + unsafe { force_window_active(win.window) }; + } else if maximized { + win.set_maximized(true); + } + + Ok(win) +} +unsafe fn register_window_class(class_name: &[u16]) { let class = WNDCLASSEXW { cbSize: mem::size_of::() as u32, style: CS_HREDRAW | CS_VREDRAW, @@ -1192,12 +1375,10 @@ unsafe fn register_window_class() -> Vec { // an error, and because errors here are detected during CreateWindowEx anyway. // Also since there is no weird element in the struct, there is no reason for this // call to fail. - RegisterClassExW(&class); - - class_name + unsafe { RegisterClassExW(&class) }; } -struct ComInitialized(*mut ()); +struct ComInitialized(#[allow(dead_code)] *mut ()); impl Drop for ComInitialized { fn drop(&mut self) { unsafe { CoUninitialize() }; @@ -1235,20 +1416,22 @@ unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { let mut task_bar_list2 = task_bar_list2_ptr.get(); if task_bar_list2.is_null() { - let hr = CoCreateInstance( - &CLSID_TaskbarList, - ptr::null_mut(), - CLSCTX_ALL, - &IID_ITaskbarList2, - &mut task_bar_list2 as *mut _ as *mut _, - ); + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList2, + &mut task_bar_list2 as *mut _ as *mut _, + ) + }; if hr != S_OK { // In visual studio retrieving the taskbar list fails return; } - let hr_init = (*(*task_bar_list2).lpVtbl).parent.HrInit; - if hr_init(task_bar_list2.cast()) != S_OK { + let hr_init = unsafe { (*(*task_bar_list2).lpVtbl).parent.HrInit }; + if unsafe { hr_init(task_bar_list2.cast()) } != S_OK { // In some old windows, the taskbar object could not be created, we just ignore it return; } @@ -1256,8 +1439,8 @@ unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { } task_bar_list2 = task_bar_list2_ptr.get(); - let mark_fullscreen_window = (*(*task_bar_list2).lpVtbl).MarkFullscreenWindow; - mark_fullscreen_window(task_bar_list2, handle, fullscreen.into()); + let mark_fullscreen_window = unsafe { (*(*task_bar_list2).lpVtbl).MarkFullscreenWindow }; + unsafe { mark_fullscreen_window(task_bar_list2, handle, fullscreen.into()) }; }) } @@ -1267,20 +1450,22 @@ pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { 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 = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList, + &mut task_bar_list as *mut _ as *mut _, + ) + }; if hr != S_OK { // In visual studio retrieving the taskbar list fails return; } - let hr_init = (*(*task_bar_list).lpVtbl).HrInit; - if hr_init(task_bar_list.cast()) != S_OK { + let hr_init = unsafe { (*(*task_bar_list).lpVtbl).HrInit }; + if unsafe { hr_init(task_bar_list.cast()) } != S_OK { // In some old windows, the taskbar object could not be created, we just ignore it return; } @@ -1289,11 +1474,11 @@ pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { task_bar_list = task_bar_list_ptr.get(); if skip { - let delete_tab = (*(*task_bar_list).lpVtbl).DeleteTab; - delete_tab(task_bar_list, hwnd); + let delete_tab = unsafe { (*(*task_bar_list).lpVtbl).DeleteTab }; + unsafe { delete_tab(task_bar_list, hwnd) }; } else { - let add_tab = (*(*task_bar_list).lpVtbl).AddTab; - add_tab(task_bar_list, hwnd); + let add_tab = unsafe { (*(*task_bar_list).lpVtbl).AddTab }; + unsafe { add_tab(task_bar_list, hwnd) }; } }); } @@ -1303,7 +1488,7 @@ unsafe fn force_window_active(handle: HWND) { // This is a little hack which can "steal" the foreground window permission // We only call this function in the window creation, so it should be fine. // See : https://stackoverflow.com/questions/10740346/setforegroundwindow-only-working-while-visual-studio-is-open - let alt_sc = MapVirtualKeyW(VK_MENU as u32, MAPVK_VK_TO_VSC); + let alt_sc = unsafe { MapVirtualKeyW(VK_MENU as u32, MAPVK_VK_TO_VSC) }; let inputs = [ INPUT { @@ -1333,11 +1518,13 @@ unsafe fn force_window_active(handle: HWND) { ]; // Simulate a key press and release - SendInput( - inputs.len() as u32, - inputs.as_ptr(), - mem::size_of::() as i32, - ); + unsafe { + SendInput( + inputs.len() as u32, + inputs.as_ptr(), + mem::size_of::() as i32, + ) + }; - SetForegroundWindow(handle); + unsafe { SetForegroundWindow(handle) }; } diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 233430ab51..cefaab6873 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -52,6 +52,9 @@ pub(crate) struct WindowState { pub is_active: bool, pub is_focused: bool, + // Flag whether redraw was requested. + pub redraw_requested: bool, + pub dragging: bool, pub skip_taskbar: bool, @@ -166,6 +169,7 @@ impl WindowState { is_active: false, is_focused: false, + redraw_requested: false, dragging: false, diff --git a/src/window.rs b/src/window.rs index c61b5d991d..333051eacd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,16 +1,12 @@ //! 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}, event_loop::EventLoopWindowTarget, monitor::{MonitorHandle, VideoMode}, - platform_impl, + platform_impl, SendSyncWrapper, }; pub use crate::icon::{BadIcon, Icon}; @@ -20,26 +16,37 @@ pub use cursor_icon::{CursorIcon, ParseError as CursorIconParseError}; /// Represents a window. /// +/// +/// # Threading +/// +/// This is `Send + Sync`, meaning that it can be freely used from other +/// threads. +/// +/// However, some platforms (macOS, Web and iOS) only allow user interface +/// interactions on the main thread, so on those platforms, if you use the +/// window from a thread other than the main, the code is scheduled to run on +/// the main thread, and your thread may be blocked until that completes. +/// +/// /// # Example /// /// ```no_run /// use winit::{ /// event::{Event, WindowEvent}, -/// event_loop::EventLoop, +/// event_loop::{ControlFlow, EventLoop}, /// window::Window, /// }; /// -/// let mut event_loop = EventLoop::new(); +/// let mut event_loop = EventLoop::new().unwrap(); +/// event_loop.set_control_flow(ControlFlow::Wait); /// let window = Window::new(&event_loop).unwrap(); /// -/// event_loop.run(move |event, _, control_flow| { -/// control_flow.set_wait(); -/// +/// event_loop.run(move |event, elwt| { /// match event { /// Event::WindowEvent { /// event: WindowEvent::CloseRequested, /// .. -/// } => control_flow.set_exit(), +/// } => elwt.exit(), /// _ => (), /// } /// }); @@ -56,13 +63,15 @@ impl fmt::Debug for Window { impl Drop for Window { fn drop(&mut self) { - // If the window is in exclusive fullscreen, we must restore the desktop - // video mode (generally this would be done on application exit, but - // closing the window doesn't necessarily always mean application exit, - // such as when there are multiple windows) - if let Some(Fullscreen::Exclusive(_)) = self.fullscreen() { - self.set_fullscreen(None); - } + self.window.maybe_wait_on_main(|w| { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) { + w.set_fullscreen(None); + } + }) } } @@ -86,7 +95,8 @@ impl WindowId { /// /// **Passing this into a winit function will result in undefined behavior.** pub const unsafe fn dummy() -> Self { - WindowId(platform_impl::WindowId::dummy()) + #[allow(unused_unsafe)] + WindowId(unsafe { platform_impl::WindowId::dummy() }) } } @@ -131,18 +141,20 @@ pub struct WindowAttributes { pub resizable: bool, pub enabled_buttons: WindowButtons, pub title: String, - pub fullscreen: Option, pub maximized: bool, pub visible: bool, pub transparent: bool, + pub blur: bool, pub decorations: bool, pub window_icon: Option, pub preferred_theme: Option, pub resize_increments: Option, pub content_protected: bool, pub window_level: WindowLevel, - pub parent_window: Option, pub active: bool, + #[cfg(feature = "rwh_06")] + pub(crate) parent_window: SendSyncWrapper>, + pub(crate) fullscreen: SendSyncWrapper>, } impl Default for WindowAttributes { @@ -157,28 +169,45 @@ impl Default for WindowAttributes { enabled_buttons: WindowButtons::all(), title: "winit window".to_owned(), maximized: false, - fullscreen: None, + fullscreen: SendSyncWrapper(None), visible: true, transparent: false, + blur: false, decorations: true, window_level: Default::default(), window_icon: None, preferred_theme: None, resize_increments: None, content_protected: false, - parent_window: None, + #[cfg(feature = "rwh_06")] + parent_window: SendSyncWrapper(None), active: true, } } } +impl WindowAttributes { + /// Get the parent window stored on the attributes. + #[cfg(feature = "rwh_06")] + pub fn parent_window(&self) -> Option<&rwh_06::RawWindowHandle> { + self.parent_window.0.as_ref() + } + + /// Get `Fullscreen` option stored on the attributes. + pub fn fullscreen(&self) -> Option<&Fullscreen> { + self.fullscreen.0.as_ref() + } +} + impl WindowBuilder { /// Initializes a new builder with default values. #[inline] pub fn new() -> Self { Default::default() } +} +impl WindowBuilder { /// Get the current window attributes. pub fn window_attributes(&self) -> &WindowAttributes { &self.window @@ -188,7 +217,7 @@ impl WindowBuilder { /// /// If this is not set, some platform-specific dimensions will be used. /// - /// See [`Window::set_inner_size`] for details. + /// See [`Window::request_inner_size`] for details. #[inline] pub fn with_inner_size>(mut self, size: S) -> Self { self.window.inner_size = Some(size.into()); @@ -287,7 +316,7 @@ impl WindowBuilder { /// See [`Window::set_fullscreen`] for details. #[inline] pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { - self.window.fullscreen = fullscreen; + self.window.fullscreen = SendSyncWrapper(fullscreen); self } @@ -327,6 +356,17 @@ impl WindowBuilder { self } + /// Sets whether the background of the window should be blurred by the system. + /// + /// The default is `false`. + /// + /// See [`Window::set_blur`] for details. + #[inline] + pub fn with_blur(mut self, blur: bool) -> Self { + self.window.blur = blur; + self + } + /// Get whether the window will support transparency. #[inline] pub fn transparent(&self) -> bool { @@ -377,8 +417,8 @@ impl WindowBuilder { /// ## Platform-specific /// /// - **macOS:** This is an app-wide setting. - /// - **Wayland:** This control only CSD. You can also use `WINIT_WAYLAND_CSD_THEME` env variable to set the theme. - /// Possible values for env variable are: "dark" and light". + /// - **Wayland:** This controls only CSD. When using `None` it'll try to use dbus to get the + /// system preference. When explicit theme is used, this will avoid dbus all together. /// - **x11:** Build window with `_GTK_THEME_VARIANT` hint set to `dark` or `light`. /// - **iOS / Android / Web / x11 / Orbital:** Ignored. #[inline] @@ -426,7 +466,7 @@ impl WindowBuilder { /// /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused. #[inline] - pub fn with_active(mut self, active: bool) -> WindowBuilder { + pub fn with_active(mut self, active: bool) -> Self { self.window.active = active; self } @@ -445,10 +485,14 @@ impl WindowBuilder { /// to the client area of its parent window. For more information, see /// /// - **X11**: A child window is confined to the client area of its parent window. - /// - **Android / iOS / Wayland:** Unsupported. - #[inline] - pub unsafe fn with_parent_window(mut self, parent_window: Option) -> Self { - self.window.parent_window = parent_window; + /// - **Android / iOS / Wayland / Web:** Unsupported. + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn with_parent_window( + mut self, + parent_window: Option, + ) -> Self { + self.window.parent_window = SendSyncWrapper(parent_window); self } @@ -465,12 +509,10 @@ impl WindowBuilder { self, window_target: &EventLoopWindowTarget, ) -> Result { - platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map( - |window| { - window.request_redraw(); - Window { window } - }, - ) + let window = + platform_impl::Window::new(&window_target.p, self.window, self.platform_specific)?; + window.maybe_queue_on_main(|w| w.request_redraw()); + Ok(Window { window }) } } @@ -498,17 +540,20 @@ impl Window { /// Returns an identifier unique to the window. #[inline] pub fn id(&self) -> WindowId { - WindowId(self.window.id()) + self.window.maybe_wait_on_main(|w| WindowId(w.id())) } - /// 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. + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and + /// vice versa. /// /// 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 /// the most robust way to track the DPI you need to use to draw. /// + /// This value may differ from [`MonitorHandle::scale_factor`]. + /// + /// See the [`dpi`](crate::dpi) module for more information. + /// /// ## Platform-specific /// /// - **X11:** This respects Xft.dpi, and can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. @@ -521,31 +566,75 @@ impl Window { /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc #[inline] pub fn scale_factor(&self) -> f64 { - self.window.scale_factor() + self.window.maybe_wait_on_main(|w| w.scale_factor()) } - /// Emits a [`Event::RedrawRequested`] event in the associated event loop after all OS - /// events have been processed by the event loop. + /// Queues a [`WindowEvent::RedrawRequested`] event to be emitted that aligns with the windowing + /// system drawing 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). + /// OS-requested redraws (e.g. when a window gets resized). To improve the event delivery + /// consider using [`Window::pre_present_notify`] as described in docs. + /// + /// Applications should always aim to redraw whenever they receive a `RedrawRequested` event. + /// + /// There are no strong guarantees about when exactly a `RedrawRequest` event will be emitted + /// with respect to other events, since the requirements can vary significantly between + /// windowing systems. /// - /// 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 - /// directly subsequent `RedrawRequested` event. + /// However as the event aligns with the windowing system drawing loop, it may not arrive in + /// same or even next event loop iteration. /// /// ## Platform-specific /// + /// - **Windows** This API uses `RedrawWindow` to request a `WM_PAINT` message and `RedrawRequested` + /// is emitted in sync with any `WM_PAINT` messages. /// - **iOS:** Can only be called on the main thread. - /// - **Android:** Subsequent calls after `MainEventsCleared` are not handled. + /// - **Wayland:** The events are aligned with the frame callbacks when [`Window::pre_present_notify`] + /// is used. + /// - **Web:** [`WindowEvent::RedrawRequested`] will be aligned with the `requestAnimationFrame`. /// - /// [`Event::RedrawRequested`]: crate::event::Event::RedrawRequested - /// [`Event::MainEventsCleared`]: crate::event::Event::MainEventsCleared + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested #[inline] pub fn request_redraw(&self) { - self.window.request_redraw() + self.window.maybe_queue_on_main(|w| w.request_redraw()) + } + + /// Notify the windowing system before presenting to the window. + /// + /// You should call this event after your drawing operations, but before you submit + /// the buffer to the display or commit your drawings. Doing so will help winit to properly + /// schedule and make assumptions about its internal state. For example, it could properly + /// throttle [`WindowEvent::RedrawRequested`]. + /// + /// ## Example + /// + /// This example illustrates how it looks with OpenGL, but it applies to other graphics + /// APIs and software rendering. + /// + /// ```no_run + /// # use winit::event_loop::EventLoop; + /// # use winit::window::Window; + /// # let mut event_loop = EventLoop::new().unwrap(); + /// # let window = Window::new(&event_loop).unwrap(); + /// # fn swap_buffers() {} + /// // Do the actual drawing with OpenGL. + /// + /// // Notify winit that we're about to submit buffer to the windowing system. + /// window.pre_present_notify(); + /// + /// // Sumbit buffer to the windowing system. + /// swap_buffers(); + /// ``` + /// + /// ## Platform-specific + /// + /// **Wayland:** - schedules a frame callback to throttle [`WindowEvent::RedrawRequested`]. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn pre_present_notify(&self) { + self.window.maybe_queue_on_main(|w| w.pre_present_notify()); } /// Reset the dead key state of the keyboard. @@ -561,7 +650,7 @@ impl Window { // at least, then this function should be provided through a platform specific // extension trait pub fn reset_dead_keys(&self) { - self.window.reset_dead_keys(); + self.window.maybe_queue_on_main(|w| w.reset_dead_keys()) } } @@ -583,7 +672,7 @@ impl Window { /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] pub fn inner_position(&self) -> Result, NotSupportedError> { - self.window.inner_position() + self.window.maybe_wait_on_main(|w| w.inner_position()) } /// Returns the position of the top-left hand corner of the window relative to the @@ -604,7 +693,7 @@ impl Window { /// - **Android / Wayland:** Always returns [`NotSupportedError`]. #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { - self.window.outer_position() + self.window.maybe_wait_on_main(|w| w.outer_position()) } /// Modifies the position of the window. @@ -616,7 +705,7 @@ impl Window { /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the position in logical dimensions like this: /// window.set_outer_position(LogicalPosition::new(400.0, 200.0)); @@ -629,11 +718,16 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the /// window in the screen space coordinate system. - /// - **Web:** Sets the top-left coordinates relative to the viewport. + /// - **Web:** Sets the top-left coordinates relative to the viewport. Doesn't account for CSS + /// [`transform`]. /// - **Android / Wayland:** Unsupported. + /// + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform #[inline] pub fn set_outer_position>(&self, position: P) { - self.window.set_outer_position(position.into()) + let position = position.into(); + self.window + .maybe_queue_on_main(move |w| w.set_outer_position(position)) } /// Returns the physical size of the window's client area. @@ -644,39 +738,56 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's /// [safe area] in screen space coordinates. - /// - **Web:** Returns the size of the canvas element. + /// - **Web:** Returns the size of the canvas element. Doesn't account for CSS [`transform`]. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform #[inline] pub fn inner_size(&self) -> PhysicalSize { - self.window.inner_size() + self.window.maybe_wait_on_main(|w| w.inner_size()) } - /// Modifies the inner size of the window. + /// Request the new size for the window. + /// + /// On platforms where the size is entirely controlled by the user the + /// applied size will be returned immediately, resize event in such case + /// may not be generated. + /// + /// On platforms where resizing is disallowed by the windowing system, the current + /// inner size is returned immidiatelly, and the user one is ignored. + /// + /// When `None` is returned, it means that the request went to the display system, + /// and the actual size will be delivered later with the [`WindowEvent::Resized`]. /// /// See [`Window::inner_size`] for more information about the values. - /// This automatically un-maximizes the window if it's maximized. + /// + /// The request could automatically un-maximize the window if it's maximized. /// /// ```no_run /// # use winit::dpi::{LogicalSize, PhysicalSize}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the size in logical dimensions like this: - /// window.set_inner_size(LogicalSize::new(400.0, 200.0)); + /// let _ = window.request_inner_size(LogicalSize::new(400.0, 200.0)); /// /// // Or specify the size in physical dimensions like this: - /// window.set_inner_size(PhysicalSize::new(400, 200)); + /// let _ = window.request_inner_size(PhysicalSize::new(400, 200)); /// ``` /// /// ## Platform-specific /// - /// - **iOS / Android:** Unsupported. - /// - **Web:** Sets the size of the canvas element. + /// - **Web:** Sets the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform #[inline] - pub fn set_inner_size>(&self, size: S) { - self.window.set_inner_size(size.into()) + #[must_use] + pub fn request_inner_size>(&self, size: S) -> Option> { + let size = size.into(); + self.window + .maybe_wait_on_main(|w| w.request_inner_size(size)) } /// Returns the physical size of the entire window. @@ -692,7 +803,7 @@ impl Window { /// [`Window::inner_size`]._ #[inline] pub fn outer_size(&self) -> PhysicalSize { - self.window.outer_size() + self.window.maybe_wait_on_main(|w| w.outer_size()) } /// Sets a minimum dimension size for the window. @@ -701,7 +812,7 @@ impl Window { /// # use winit::dpi::{LogicalSize, PhysicalSize}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the size in logical dimensions like this: /// window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0))); @@ -712,10 +823,12 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **iOS / Android / Orbital:** Unsupported. #[inline] pub fn set_min_inner_size>(&self, min_size: Option) { - self.window.set_min_inner_size(min_size.map(|s| s.into())) + let min_size = min_size.map(|s| s.into()); + self.window + .maybe_queue_on_main(move |w| w.set_min_inner_size(min_size)) } /// Sets a maximum dimension size for the window. @@ -724,7 +837,7 @@ impl Window { /// # use winit::dpi::{LogicalSize, PhysicalSize}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the size in logical dimensions like this: /// window.set_max_inner_size(Some(LogicalSize::new(400.0, 200.0))); @@ -735,10 +848,12 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **iOS / Android / Orbital:** Unsupported. #[inline] pub fn set_max_inner_size>(&self, max_size: Option) { - self.window.set_max_inner_size(max_size.map(|s| s.into())) + let max_size = max_size.map(|s| s.into()); + self.window + .maybe_queue_on_main(move |w| w.set_max_inner_size(max_size)) } /// Returns window resize increments if any were set. @@ -748,7 +863,7 @@ impl Window { /// - **iOS / Android / Web / Wayland / Windows / Orbital:** Always returns [`None`]. #[inline] pub fn resize_increments(&self) -> Option> { - self.window.resize_increments() + self.window.maybe_wait_on_main(|w| w.resize_increments()) } /// Sets window resize increments. @@ -763,8 +878,9 @@ impl Window { /// - **iOS / Android / Web / Orbital:** Unsupported. #[inline] pub fn set_resize_increments>(&self, increments: Option) { + let increments = increments.map(Into::into); self.window - .set_resize_increments(increments.map(Into::into)) + .maybe_queue_on_main(move |w| w.set_resize_increments(increments)) } } @@ -777,7 +893,7 @@ impl Window { /// - **iOS / Android:** Unsupported. #[inline] pub fn set_title(&self, title: &str) { - self.window.set_title(title) + self.window.maybe_wait_on_main(|w| w.set_title(title)) } /// Change the window transparency state. @@ -791,10 +907,25 @@ impl Window { /// /// ## Platform-specific /// - /// - **Windows / X11 / Web / iOS / Android / Orbital:** Unsupported. + /// - **Web / iOS / Android:** Unsupported. + /// - **X11:** Can only be set while building the window, with [`WindowBuilder::with_transparent`]. #[inline] pub fn set_transparent(&self, transparent: bool) { - self.window.set_transparent(transparent) + self.window + .maybe_queue_on_main(move |w| w.set_transparent(transparent)) + } + + /// Change the window blur state. + /// + /// If `true`, this will make the transparent window background blurry. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows:** Unsupported. + /// - **Wayland:** Only works with org_kde_kwin_blur_manager protocol. + #[inline] + pub fn set_blur(&self, blur: bool) { + self.window.maybe_queue_on_main(move |w| w.set_blur(blur)) } /// Modifies the window's visibility. @@ -807,7 +938,8 @@ impl Window { /// - **iOS:** Can only be called on the main thread. #[inline] pub fn set_visible(&self, visible: bool) { - self.window.set_visible(visible) + self.window + .maybe_queue_on_main(move |w| w.set_visible(visible)) } /// Gets the window's current visibility state. @@ -820,14 +952,14 @@ impl Window { /// - **Wayland / iOS / Android / Web:** Unsupported. #[inline] pub fn is_visible(&self) -> Option { - self.window.is_visible() + self.window.maybe_wait_on_main(|w| w.is_visible()) } /// Sets whether the window is resizable or not. /// /// 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`]. + /// window could still be resized by calling [`Window::request_inner_size`]. /// /// ## Platform-specific /// @@ -839,7 +971,8 @@ impl Window { /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized #[inline] pub fn set_resizable(&self, resizable: bool) { - self.window.set_resizable(resizable) + self.window + .maybe_queue_on_main(move |w| w.set_resizable(resizable)) } /// Gets the window's current resizable state. @@ -850,7 +983,7 @@ impl Window { /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn is_resizable(&self) -> bool { - self.window.is_resizable() + self.window.maybe_wait_on_main(|w| w.is_resizable()) } /// Sets the enabled window buttons. @@ -860,7 +993,8 @@ impl Window { /// - **Wayland / X11 / Orbital:** Not implemented. /// - **Web / iOS / Android:** Unsupported. pub fn set_enabled_buttons(&self, buttons: WindowButtons) { - self.window.set_enabled_buttons(buttons) + self.window + .maybe_queue_on_main(move |w| w.set_enabled_buttons(buttons)) } /// Gets the enabled window buttons. @@ -870,7 +1004,7 @@ impl Window { /// - **Wayland / X11 / Orbital:** Not implemented. Always returns [`WindowButtons::all`]. /// - **Web / iOS / Android:** Unsupported. Always returns [`WindowButtons::all`]. pub fn enabled_buttons(&self) -> WindowButtons { - self.window.enabled_buttons() + self.window.maybe_wait_on_main(|w| w.enabled_buttons()) } /// Sets the window to minimized or back @@ -881,7 +1015,8 @@ impl Window { /// - **Wayland:** Un-minimize is unsupported. #[inline] pub fn set_minimized(&self, minimized: bool) { - self.window.set_minimized(minimized); + self.window + .maybe_queue_on_main(move |w| w.set_minimized(minimized)) } /// Gets the window's current minimized state. @@ -898,27 +1033,28 @@ impl Window { /// - **iOS / Android / Web / Orbital:** Unsupported. #[inline] pub fn is_minimized(&self) -> Option { - self.window.is_minimized() + self.window.maybe_wait_on_main(|w| w.is_minimized()) } /// Sets the window to maximized or back. /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn set_maximized(&self, maximized: bool) { - self.window.set_maximized(maximized) + self.window + .maybe_queue_on_main(move |w| w.set_maximized(maximized)) } /// Gets the window's current maximized state. /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn is_maximized(&self) -> bool { - self.window.is_maximized() + self.window.maybe_wait_on_main(|w| w.is_maximized()) } /// Sets the window to fullscreen or back. @@ -947,7 +1083,8 @@ impl Window { /// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation #[inline] pub fn set_fullscreen(&self, fullscreen: Option) { - self.window.set_fullscreen(fullscreen.map(|f| f.into())) + self.window + .maybe_queue_on_main(move |w| w.set_fullscreen(fullscreen.map(|f| f.into()))) } /// Gets the window's current fullscreen state. @@ -957,9 +1094,11 @@ impl Window { /// - **iOS:** Can only be called on the main thread. /// - **Android / Orbital:** Will always return `None`. /// - **Wayland:** Can return `Borderless(None)` when there are no monitors. + /// - **Web:** Can only return `None` or `Borderless(None)`. #[inline] pub fn fullscreen(&self) -> Option { - self.window.fullscreen().map(|f| f.into()) + self.window + .maybe_wait_on_main(|w| w.fullscreen().map(|f| f.into())) } /// Turn window decorations on or off. @@ -973,7 +1112,8 @@ impl Window { /// - **iOS / Android / Web:** No effect. #[inline] pub fn set_decorations(&self, decorations: bool) { - self.window.set_decorations(decorations) + self.window + .maybe_queue_on_main(move |w| w.set_decorations(decorations)) } /// Gets the window's current decorations state. @@ -986,7 +1126,7 @@ impl Window { /// - **iOS / Android / Web:** Always returns `true`. #[inline] pub fn is_decorated(&self) -> bool { - self.window.is_decorated() + self.window.maybe_wait_on_main(|w| w.is_decorated()) } /// Change the window level. @@ -995,7 +1135,8 @@ impl Window { /// /// See [`WindowLevel`] for details. pub fn set_window_level(&self, level: WindowLevel) { - self.window.set_window_level(level) + self.window + .maybe_queue_on_main(move |w| w.set_window_level(level)) } /// Sets the window icon. @@ -1014,7 +1155,8 @@ impl Window { /// 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) + self.window + .maybe_queue_on_main(move |w| w.set_window_icon(window_icon)) } /// Set the IME cursor editing area, where the `position` is the top left corner of that area @@ -1036,7 +1178,7 @@ impl Window { /// # use winit::dpi::{LogicalPosition, PhysicalPosition, LogicalSize, PhysicalSize}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the position in logical dimensions like this: /// window.set_ime_cursor_area(LogicalPosition::new(400.0, 200.0), LogicalSize::new(100, 100)); @@ -1054,8 +1196,10 @@ impl Window { /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 #[inline] pub fn set_ime_cursor_area, S: Into>(&self, position: P, size: S) { + let position = position.into(); + let size = size.into(); self.window - .set_ime_cursor_area(position.into(), size.into()) + .maybe_queue_on_main(move |w| w.set_ime_cursor_area(position, size)) } /// Sets whether the window should get IME events @@ -1080,7 +1224,8 @@ impl Window { /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput #[inline] pub fn set_ime_allowed(&self, allowed: bool) { - self.window.set_ime_allowed(allowed); + self.window + .maybe_queue_on_main(move |w| w.set_ime_allowed(allowed)) } /// Sets the IME purpose for the window using [`ImePurpose`]. @@ -1090,7 +1235,8 @@ impl Window { /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. #[inline] pub fn set_ime_purpose(&self, purpose: ImePurpose) { - self.window.set_ime_purpose(purpose); + self.window + .maybe_queue_on_main(move |w| w.set_ime_purpose(purpose)) } /// Brings the window to the front and sets input focus. Has no effect if the window is @@ -1102,10 +1248,10 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Wayland / Orbital:** Unsupported. + /// - **iOS / Android / Wayland / Orbital:** Unsupported. #[inline] pub fn focus_window(&self) { - self.window.focus_window() + self.window.maybe_queue_on_main(|w| w.focus_window()) } /// Gets whether the window has keyboard focus. @@ -1115,7 +1261,7 @@ impl Window { /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused #[inline] pub fn has_focus(&self) -> bool { - self.window.has_focus() + self.window.maybe_wait_on_main(|w| w.has_focus()) } /// Requests user attention to the window, this has no effect if the application @@ -1133,7 +1279,8 @@ impl Window { /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. #[inline] pub fn request_user_attention(&self, request_type: Option) { - self.window.request_user_attention(request_type) + self.window + .maybe_queue_on_main(move |w| w.request_user_attention(request_type)) } /// Sets the current window theme. Use `None` to fallback to system default. @@ -1141,13 +1288,13 @@ impl Window { /// ## Platform-specific /// /// - **macOS:** This is an app-wide setting. - /// - **Wayland:** 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. + /// - **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus + /// to get the system preference. /// - **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, it will default to [`Theme::Dark`]. /// - **iOS / Android / Web / Orbital:** Unsupported. #[inline] pub fn set_theme(&self, theme: Option) { - self.window.set_theme(theme) + self.window.maybe_queue_on_main(move |w| w.set_theme(theme)) } /// Returns the current window theme. @@ -1158,7 +1305,7 @@ impl Window { /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. #[inline] pub fn theme(&self) -> Option { - self.window.theme() + self.window.maybe_wait_on_main(|w| w.theme()) } /// Prevents the window contents from being captured by other apps. @@ -1170,9 +1317,9 @@ impl Window { /// - **iOS / Android / x11 / Wayland / Web / Orbital:** Unsupported. /// /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone - pub fn set_content_protected(&self, _protected: bool) { - #[cfg(any(macos_platform, windows_platform))] - self.window.set_content_protected(_protected); + pub fn set_content_protected(&self, protected: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_content_protected(protected)) } /// Gets the current title of the window. @@ -1182,7 +1329,7 @@ impl Window { /// - **iOS / Android / x11 / Wayland / Web:** Unsupported. Always returns an empty string. #[inline] pub fn title(&self) -> String { - self.window.title() + self.window.maybe_wait_on_main(|w| w.title()) } } @@ -1195,7 +1342,8 @@ impl Window { /// - **iOS / Android / Orbital:** Unsupported. #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - self.window.set_cursor_icon(cursor); + self.window + .maybe_queue_on_main(move |w| w.set_cursor_icon(cursor)) } /// Changes the position of the cursor in window coordinates. @@ -1204,7 +1352,7 @@ impl Window { /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; /// # use winit::event_loop::EventLoop; /// # use winit::window::Window; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// // Specify the position in logical dimensions like this: /// window.set_cursor_position(LogicalPosition::new(400.0, 200.0)); @@ -1215,13 +1363,16 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Wayland / Orbital:** Always returns an [`ExternalError::NotSupported`]. + /// - **Wayland**: Cursor must be in [`CursorGrabMode::Locked`]. + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. #[inline] pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { - self.window.set_cursor_position(position.into()) + let position = position.into(); + self.window + .maybe_wait_on_main(|w| w.set_cursor_position(position)) } - /// Set grabbing [mode]([`CursorGrabMode`]) on the cursor preventing it from leaving the window. + /// Set grabbing [mode](CursorGrabMode) on the cursor preventing it from leaving the window. /// /// # Example /// @@ -1230,7 +1381,7 @@ impl Window { /// ```no_run /// # use winit::event_loop::EventLoop; /// # use winit::window::{CursorGrabMode, Window}; - /// # let mut event_loop = EventLoop::new(); + /// # let mut event_loop = EventLoop::new().unwrap(); /// # let window = Window::new(&event_loop).unwrap(); /// window.set_cursor_grab(CursorGrabMode::Confined) /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) @@ -1238,7 +1389,7 @@ impl Window { /// ``` #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - self.window.set_cursor_grab(mode) + self.window.maybe_wait_on_main(|w| w.set_cursor_grab(mode)) } /// Modifies the cursor's visibility. @@ -1252,10 +1403,11 @@ impl Window { /// - **Wayland:** The cursor is only hidden within the confines of the window. /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is /// outside of the window. - /// - **iOS / Android / Orbital:** Unsupported. + /// - **iOS / Android:** Unsupported. #[inline] pub fn set_cursor_visible(&self, visible: bool) { - self.window.set_cursor_visible(visible) + self.window + .maybe_queue_on_main(move |w| w.set_cursor_visible(visible)) } /// Moves the window with the left mouse button until the button is released. @@ -1268,10 +1420,10 @@ impl Window { /// - **X11:** Un-grabs the cursor. /// - **Wayland:** Requires the cursor to be inside the window to be dragged. /// - **macOS:** May prevent the button release event to be triggered. - /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. #[inline] pub fn drag_window(&self) -> Result<(), ExternalError> { - self.window.drag_window() + self.window.maybe_wait_on_main(|w| w.drag_window()) } /// Resizes the window with the left mouse button until the button is released. @@ -1281,10 +1433,27 @@ impl Window { /// /// ## Platform-specific /// - /// Only X11 is supported at this time. + /// - **macOS:** Always returns an [`ExternalError::NotSupported`] + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. #[inline] pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { - self.window.drag_resize_window(direction) + self.window + .maybe_wait_on_main(|w| w.drag_resize_window(direction)) + } + + /// Show [window menu] at a specified position . + /// + /// This is the context menu that is normally shown when interacting with + /// the title bar. This is useful when implementing custom decorations. + /// + /// ## Platform-specific + /// **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. + /// + /// [window menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu + pub fn show_window_menu(&self, position: impl Into) { + let position = position.into(); + self.window + .maybe_queue_on_main(move |w| w.show_window_menu(position)) } /// Modifies whether the window catches cursor events. @@ -1294,10 +1463,11 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / X11 / Orbital:** Always returns an [`ExternalError::NotSupported`]. + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { - self.window.set_cursor_hittest(hittest) + self.window + .maybe_wait_on_main(|w| w.set_cursor_hittest(hittest)) } } @@ -1306,33 +1476,24 @@ impl Window { /// Returns the monitor on which the window currently resides. /// /// Returns `None` if current monitor can't be detected. - /// - /// ## Platform-specific - /// - /// **iOS:** Can only be called on the main thread. #[inline] pub fn current_monitor(&self) -> Option { self.window - .current_monitor() - .map(|inner| MonitorHandle { inner }) + .maybe_wait_on_main(|w| w.current_monitor().map(|inner| MonitorHandle { inner })) } /// Returns the list of all the monitors available on the system. /// /// 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 { - #[allow(clippy::useless_conversion)] // false positive on some platforms - self.window - .available_monitors() - .into_iter() - .map(|inner| MonitorHandle { inner }) + self.window.maybe_wait_on_main(|w| { + w.available_monitors() + .into_iter() + .map(|inner| MonitorHandle { inner }) + }) } /// Returns the primary monitor of the system. @@ -1343,45 +1504,70 @@ impl Window { /// /// ## Platform-specific /// - /// **iOS:** Can only be called on the main thread. - /// **Wayland:** Always returns `None`. + /// **Wayland / Web:** Always returns `None`. /// /// [`EventLoopWindowTarget::primary_monitor`]: crate::event_loop::EventLoopWindowTarget::primary_monitor #[inline] pub fn primary_monitor(&self) -> Option { self.window - .primary_monitor() - .map(|inner| MonitorHandle { inner }) + .maybe_wait_on_main(|w| w.primary_monitor().map(|inner| MonitorHandle { inner })) } } -unsafe impl HasRawWindowHandle for Window { - /// Returns a [`raw_window_handle::RawWindowHandle`] for the Window - /// - /// ## 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) -> RawWindowHandle { - self.window.raw_window_handle() + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasWindowHandle for Window { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self + .window + .maybe_wait_on_main(|w| w.raw_window_handle_rwh_06().map(SendSyncWrapper))? + .0; + + // SAFETY: The window handle will never be deallocated while the window is alive. + Ok(unsafe { rwh_06::WindowHandle::borrow_raw(raw) }) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for Window { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self + .window + .maybe_wait_on_main(|w| w.raw_display_handle_rwh_06().map(SendSyncWrapper))? + .0; + + // SAFETY: The window handle will never be deallocated while the window is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) } } -unsafe impl HasRawDisplayHandle for Window { - /// Returns a [`raw_window_handle::RawDisplayHandle`] used by the [`EventLoop`] that +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_05::RawWindowHandle { + self.window + .maybe_wait_on_main(|w| SendSyncWrapper(w.raw_window_handle_rwh_05())) + .0 + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for Window { + /// Returns a [`rwh_05::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() + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.window + .maybe_wait_on_main(|w| SendSyncWrapper(w.raw_display_handle_rwh_05())) + .0 + } +} + +#[cfg(feature = "rwh_04")] +unsafe impl rwh_04::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_04::RawWindowHandle { + self.window + .maybe_wait_on_main(|w| SendSyncWrapper(w.raw_window_handle_rwh_04())) + .0 } } @@ -1402,7 +1588,7 @@ pub enum CursorGrabMode { /// ## Platform-specific /// /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. - /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. Confined, /// The cursor is locked inside the window area to the certain position. @@ -1413,7 +1599,7 @@ pub enum CursorGrabMode { /// ## Platform-specific /// /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. - /// - **iOS / Android / Orbital:** Always returns an [`ExternalError::NotSupported`]. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. Locked, } @@ -1546,3 +1732,17 @@ impl Default for ImePurpose { Self::Normal } } + +/// An opaque token used to activate the [`Window`]. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ActivationToken { + pub(crate) _token: String, +} + +impl ActivationToken { + pub(crate) fn _new(_token: String) -> Self { + Self { _token } + } +} diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 9462d0736f..7941b16769 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -16,6 +16,11 @@ fn window_send() { needs_send::(); } +#[test] +fn window_builder_send() { + needs_send::(); +} + #[test] fn ids_send() { // ensures that the various `..Id` types implement `Send` diff --git a/tests/serde_objects.rs b/tests/serde_objects.rs index fccc202b1e..02b249b555 100644 --- a/tests/serde_objects.rs +++ b/tests/serde_objects.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use winit::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase}, - keyboard::{Key, KeyCode, KeyLocation, ModifiersState}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey, PhysicalKey}, window::CursorIcon, }; @@ -23,7 +23,9 @@ fn events_serde() { needs_serde::(); needs_serde::(); needs_serde::(); + needs_serde::(); needs_serde::(); + needs_serde::(); needs_serde::(); needs_serde::(); } diff --git a/tests/sync_object.rs b/tests/sync_object.rs index dad6520248..23c6012545 100644 --- a/tests/sync_object.rs +++ b/tests/sync_object.rs @@ -6,3 +6,8 @@ fn window_sync() { // ensures that `winit::Window` implements `Sync` needs_sync::(); } + +#[test] +fn window_builder_sync() { + needs_sync::(); +}