diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..1c35451427 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + pull_request: + paths: + - '**.rs' + - '**.toml' + - '.github/workflows/ci.yml' + push: + branches: [master] + paths: + - '**.rs' + - '**.toml' + - '.github/workflows/ci.yml' + +jobs: + Check_Formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + components: rustfmt + - name: Check Formatting + run: cargo +stable fmt --all -- --check + + Tests: + strategy: + fail-fast: false + matrix: + rust_version: [stable, nightly] + platform: + - { 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 } + - { target: aarch64-linux-android, os: ubuntu-latest, cmd: 'apk --' } + - { 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, features: stdweb, cmd: web } + - { target: wasm32-unknown-unknown, os: windows-latest, features: web-sys, cmd: web } + + env: + RUST_BACKTRACE: 1 + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-C debuginfo=0" + OPTIONS: ${{ matrix.platform.options }} + FEATURES: ${{ format(',{0}', matrix.platform.features ) }} + CMD: ${{ matrix.platform.cmd }} + + runs-on: ${{ matrix.platform.os }} + steps: + - uses: actions/checkout@v2 + # 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 + with: + rust-version: ${{ matrix.rust_version }}${{ matrix.platform.host }} + targets: ${{ matrix.platform.target }} + + - 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 + if: contains(matrix.platform.target, 'android') + run: cargo install cargo-apk + - name: Install cargo-web + continue-on-error: true + if: contains(matrix.platform.target, 'wasm32') + run: cargo install cargo-web + + - name: Check documentation + shell: bash + if: matrix.platform.target != 'wasm32-unknown-unknown' + run: cargo $CMD doc --no-deps --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + + - name: Build + shell: bash + run: cargo $CMD build --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + + - name: Build tests + shell: bash + run: cargo $CMD test --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + - name: Run tests + shell: bash + if: ( + !contains(matrix.platform.target, 'android') && + !contains(matrix.platform.target, 'ios') && + !contains(matrix.platform.target, 'wasm32')) + run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + + + - name: Build with serde enabled + shell: bash + run: cargo $CMD build --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES + + - name: Build tests with serde enabled + shell: bash + run: cargo $CMD test --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES + - name: Run tests with serde enabled + shell: bash + if: ( + !contains(matrix.platform.target, 'android') && + !contains(matrix.platform.target, 'ios') && + !contains(matrix.platform.target, 'wasm32')) + run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features serde,$FEATURES diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..b7c059e6f6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: Publish + +on: + push: + branches: [master] + paths: "Cargo.toml" + +jobs: + Publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + components: rustfmt + - name: Publish to crates.io + run: cargo publish --token ${{ secrets.cratesio_token }} diff --git a/.gitignore b/.gitignore index 7cd4f7f91d..5e6640bc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ Cargo.lock target/ rls/ .vscode/ -util/ *~ *.wasm *.ts diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 649b0106c9..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,104 +0,0 @@ -language: rust - -matrix: - include: - # Linux 32bit - - env: TARGET=i686-unknown-linux-gnu - os: linux - rust: nightly - addons: - apt: - # Cross compiler and cross compiled C libraries - packages: &i686_packages - - gcc-multilib - - env: TARGET=i686-unknown-linux-gnu - os: linux - rust: stable - addons: - apt: - packages: *i686_packages - - # Linux 64bit - - env: TARGET=x86_64-unknown-linux-gnu - os: linux - rust: nightly - - env: TARGET=x86_64-unknown-linux-gnu - os: linux - rust: stable - - # macOS - - env: TARGET=x86_64-apple-darwin - os: osx - rust: nightly - - env: TARGET=x86_64-apple-darwin - os: osx - rust: stable - - # iOS x86_64 - - env: TARGET=x86_64-apple-ios - os: osx - rust: nightly - - env: TARGET=x86_64-apple-ios - os: osx - rust: stable - - # iOS armv7 - - env: TARGET=armv7-apple-ios - os: osx - rust: nightly - - env: TARGET=armv7-apple-ios - os: osx - rust: stable - - # iOS arm64 - - env: TARGET=aarch64-apple-ios - os: osx - rust: nightly - - env: TARGET=aarch64-apple-ios - os: osx - rust: stable - - # wasm stdweb - - env: TARGET=wasm32-unknown-unknown WEB=web FEATURES=stdweb - os: linux - rust: stable - - env: TARGET=wasm32-unknown-unknown WEB=web FEATURES=stdweb - os: linux - rust: nightly - # wasm web-sys - - env: TARGET=wasm32-unknown-unknown FEATURES=web-sys - os: linux - rust: stable - - env: TARGET=wasm32-unknown-unknown FEATURES=web-sys - os: linux - rust: nightly - -install: - - rustup self update - - rustup target add $TARGET; true - - rustup toolchain install stable - - rustup component add rustfmt --toolchain stable - -script: - - cargo +stable fmt --all -- --check - # Ensure that the documentation builds properly. - - cargo doc --no-deps - # Install cargo-web to build stdweb - - if [[ $WEB = "web" ]]; then cargo install -f cargo-web; fi - # Build without serde then with serde - - if [[ -z "$FEATURES" ]]; then - cargo $WEB build --target $TARGET --verbose; - else - cargo $WEB build --target $TARGET --features $FEATURES --verbose; - fi - - cargo $WEB build --target $TARGET --features serde,$FEATURES --verbose - # Running iOS apps on macOS requires the Simulator so we skip that for now - # The web targets also don't support running tests - - if [[ $TARGET != *-apple-ios && $TARGET != wasm32-* ]]; then cargo test --target $TARGET --verbose; fi - - if [[ $TARGET != *-apple-ios && $TARGET != wasm32-* ]]; then cargo test --target $TARGET --features serde --verbose; fi - -after_success: - - | - [ $TRAVIS_BRANCH = master ] && - [ $TRAVIS_PULL_REQUEST = false ] && - cargo publish --token ${CRATESIO_TOKEN} diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c609d072..f268bb34ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,220 @@ # Unreleased +- Added `is_maximized` method to `Window`. +- On Windows, fix bug where clicking the decoration bar would make the cursor blink. +- On Windows, fix bug causing newly created windows to erroneously display the "wait" (spinning) cursor. +- On Windows, change the default window size (1024x768) to match the default on other desktop platforms (800x600). +- On Windows, fix bug causing mouse capture to not be released. +- On Windows, fix fullscreen not preserving minimized/maximized state. +- On Android, unimplemented events are marked as unhandled on the native event loop. +- Overhaul device event API: + - **Breaking**: `Event::DeviceEvent` split into `MouseEvent`, `KeyboardEvent`, and `GamepadEvent`. + - **Breaking**: Remove `DeviceEvent::Text` variant. + - **Breaking**: `DeviceId` split into `MouseId`, `KeyboardId`, and `GamepadHandle`. + - **Breaking**: Removed device IDs from `WindowEvent` variants. + - Add `enumerate` function on device ID types to list all attached devices of that type. + - Add `is_connected` function on device ID types check if the specified device is still available. + - **Breaking**: On Windows, rename `DeviceIdExtWindows` to `DeviceExtWindows`. + - Add `handle` function to retrieve the underlying `HANDLE`. +- On Windows, fix duplicate device events getting sent if Winit managed multiple windows. +- On Windows, raw mouse events now report Mouse4 and Mouse5 presses and releases. +- Added gamepad support on Windows via raw input and XInput. +- On Windows, added `WindowBuilderExtWindows::with_menu` to set a custom menu at window creation time. +- On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. + +# 0.24.0 (2020-12-09) + +- On Windows, fix applications not exiting gracefully due to thread_event_target_callback accessing corrupted memory. +- On Windows, implement `Window::set_ime_position`. +- **Breaking:** On Windows, Renamed `WindowBuilderExtWindows`'s `is_dark_mode` to `theme`. +- **Breaking:** On Windows, renamed `WindowBuilderExtWindows::is_dark_mode` to `theme`. +- On Windows, add `WindowBuilderExtWindows::with_theme` to set a preferred theme. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, calling `WindowEvent::Focused` now works properly instead of always returning false. +- On Windows, fix Alt-Tab behaviour by removing borderless fullscreen "always on top" flag. +- On Windows, fix bug preventing windows with transparency enabled from having fully-opaque regions. +- **Breaking:** On Windows, include prefix byte in scancodes. +- On Wayland, fix window not being resizeable when using `WindowBuilder::with_min_inner_size`. +- On Unix, fix cross-compiling to wasm32 without enabling X11 or Wayland. +- On Windows, fix use-after-free crash during window destruction. +- On Web, fix `WindowEvent::ReceivedCharacter` never being sent on key input. +- On macOS, fix compilation when targeting aarch64. +- On X11, fix `Window::request_redraw` not waking the event loop. +- On Wayland, the keypad arrow keys are now recognized. +- **Breaking** Rename `desktop::EventLoopExtDesktop` to `run_return::EventLoopExtRunReturn`. +- Added `request_user_attention` method to `Window`. +- **Breaking:** On macOS, removed `WindowExt::request_user_attention`, use `Window::request_user_attention`. +- **Breaking:** On X11, removed `WindowExt::set_urgent`, use `Window::request_user_attention`. +- On Wayland, default font size in CSD increased from 11 to 17. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, support multi-touch. +- On Wayland, extra mouse buttons are not dropped anymore. +- **Breaking**: `MouseButton::Other` now uses `u16`. + +# 0.23.0 (2020-10-02) + +- On iOS, fixed support for the "Debug View Heirarchy" feature in Xcode. +- On all platforms, `available_monitors` and `primary_monitor` are now on `EventLoopWindowTarget` rather than `EventLoop` to list monitors event in the event loop. +- On Unix, X11 and Wayland are now optional features (enabled by default) +- On X11, fix deadlock when calling `set_fullscreen_inner`. +- On Web, prevent the webpage from scrolling when the user is focused on a winit canvas +- On Web, calling `window.set_cursor_icon` no longer breaks HiDPI scaling +- On Windows, drag and drop is now optional and must be enabled with `WindowBuilderExtWindows::with_drag_and_drop(true)`. +- On Wayland, fix deadlock when calling to `set_inner_size` from a callback. +- On macOS, add `hide__other_applications` to `EventLoopWindowTarget` via existing `EventLoopWindowTargetExtMacOS` trait. `hide_other_applications` will hide other applications by calling `-[NSApplication hideOtherApplications: nil]`. +- On android added support for `run_return`. +- On MacOS, Fixed fullscreen and dialog support for `run_return`. +- On Windows, fix bug where we'd try to emit `MainEventsCleared` events during nested win32 event loops. +- On Web, use mouse events if pointer events aren't supported. This affects Safari. +- On Windows, `set_ime_position` is now a no-op instead of a runtime crash. +- On Android, `set_fullscreen` is now a no-op instead of a runtime crash. +- On iOS and Android, `set_inner_size` is now a no-op instead of a runtime crash. +- On Android, fix `ControlFlow::Poll` not polling the Android event queue. +- On macOS, add `NSWindow.hasShadow` support. +- On Web, fix vertical mouse wheel scrolling being inverted. +- On Web, implement mouse capturing for click-dragging out of the canvas. +- On Web, fix `ControlFlow::Exit` not properly handled. +- On Web (web-sys only), send `WindowEvent::ScaleFactorChanged` event when `window.devicePixelRatio` is changed. +- **Breaking:** On Web, `set_cursor_position` and `set_cursor_grab` will now always return an error. +- **Breaking:** `PixelDelta` scroll events now return a `PhysicalPosition`. +- On NetBSD, fixed crash due to incorrect detection of the main thread. +- **Breaking:** On X11, `-` key is mapped to the `Minus` virtual key code, instead of `Subtract`. +- On macOS, fix inverted horizontal scroll. +- **Breaking:** `current_monitor` now returns `Option`. +- **Breaking:** `primary_monitor` now returns `Option`. +- On macOS, updated core-* dependencies and cocoa. +- Bump `parking_lot` to 0.11 +- On Android, bump `ndk`, `ndk-sys` and `ndk-glue` to 0.2. Checkout the new ndk-glue main proc attribute. +- On iOS, fixed starting the app in landscape where the view still had portrait dimensions. +- Deprecate the stdweb backend, to be removed in a future release +- **Breaking:** Prefixed virtual key codes `Add`, `Multiply`, `Divide`, `Decimal`, and `Subtract` with `Numpad`. +- Added `Asterisk` and `Plus` virtual key codes. +- On Web (web-sys only), the `Event::LoopDestroyed` event is correctly emitted when leaving the page. +- On Web, the `WindowEvent::Destroyed` event now gets emitted when a `Window` is dropped. +- On Web (web-sys only), the event listeners are now removed when a `Window` is dropped or when the event loop is destroyed. +- On Web, the event handler closure passed to `EventLoop::run` now gets dropped after the event loop is destroyed. +- **Breaking:** On Web, the canvas element associated to a `Window` is no longer removed from the DOM when the `Window` is dropped. +- On Web, `WindowEvent::Resized` is now emitted when `Window::set_inner_size` is called. +- **Breaking:** `Fullscreen` enum now uses `Borderless(Option)` instead of `Borderless(MonitorHandle)` to allow picking the current monitor. +- On MacOS, fix `WindowEvent::Moved` ignoring the scale factor. +- On Wayland, add missing virtual keycodes. +- On Wayland, implement proper `set_cursor_grab`. +- On Wayland, the cursor will use similar icons if the requested one isn't available. +- On Wayland, right clicking on client side decorations will request application menu. +- On Wayland, fix tracking of window size after state changes. +- On Wayland, fix client side decorations not being hidden properly in fullscreen. +- On Wayland, fix incorrect size event when entering fullscreen with client side decorations. +- On Wayland, fix `resizable` attribute not being applied properly on startup. +- On Wayland, fix disabled repeat rate not being handled. +- On Wayland, fix decoration buttons not working after tty switch. +- On Wayland, fix scaling not being applied on output re-enable. +- On Wayland, fix crash when `XCURSOR_SIZE` is `0`. +- On Wayland, fix pointer getting created in some cases without pointer capability. +- On Wayland, on kwin, fix space between window and decorations on startup. +- **Breaking:** On Wayland, `Theme` trait was reworked. +- On Wayland, disable maximize button for non-resizable window. +- On Wayland, added support for `set_ime_position`. +- On Wayland, fix crash on startup since GNOME 3.37.90. +- On X11, fix incorrect modifiers state on startup. + +# 0.22.2 (2020-05-16) + +- Added Clone implementation for 'static events. +- On Windows, fix window intermittently hanging when `ControlFlow` was set to `Poll`. +- On Windows, fix `WindowBuilder::with_maximized` being ignored. +- On Android, minimal platform support. +- On iOS, touch positions are now properly converted to physical pixels. +- On macOS, updated core-* dependencies and cocoa + +# 0.22.1 (2020-04-16) + +- On X11, fix `ResumeTimeReached` being fired too early. +- On Web, replaced zero timeout for `ControlFlow::Poll` with `requestAnimationFrame` +- On Web, fix a possible panic during event handling +- On macOS, fix `EventLoopProxy` leaking memory for every instance. +- On Windows, drag and drop can now be disabled with `WindowBuilderExtWindows::with_drag_and_drop(false)`. + +# 0.22.0 (2020-03-09) + +- On Windows, fix minor timing issue in wait_until_time_or_msg +- On Windows, rework handling of request_redraw() to address panics. +- On macOS, fix `set_simple_screen` to remember frame excluding title bar. +- On Wayland, fix coordinates in touch events when scale factor isn't 1. +- On Wayland, fix color from `close_button_icon_color` not applying. +- Ignore locale if unsupported by X11 backend +- On Wayland, Add HiDPI cursor support +- On Web, add the ability to query "Light" or "Dark" system theme send `ThemeChanged` on change. +- Fix `Event::to_static` returning `None` for user events. +- On Wayland, Hide CSD for fullscreen windows. +- On Windows, ignore spurious mouse move messages. +- **Breaking:** Move `ModifiersChanged` variant from `DeviceEvent` to `WindowEvent`. +- On Windows, add `IconExtWindows` trait which exposes creating an `Icon` from an external file or embedded resource +- Add `BadIcon::OsError` variant for when OS icon functionality fails +- On Windows, fix crash at startup on systems that do not properly support Windows' Dark Mode +- Revert On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- on macOS, fix incorrect ReceivedCharacter events for some key combinations. +- **Breaking:** Use `i32` instead of `u32` for position type in `WindowEvent::Moved`. +- On macOS, a mouse motion event is now generated before every mouse click. + +# 0.21.0 (2020-02-04) + +- On Windows, fixed "error: linking with `link.exe` failed: exit code: 1120" error on older versions of windows. +- On macOS, fix set_minimized(true) works only with decorations. +- On macOS, add `hide_application` to `EventLoopWindowTarget` via a new `EventLoopWindowTargetExtMacOS` trait. `hide_application` will hide the entire application by calling `-[NSApplication hide: nil]`. +- On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- On macOS, fix `CursorMoved` event reporting the cursor position using logical coordinates. +- On macOS, fix issue where unbundled applications would sometimes open without being focused. +- On macOS, fix `run_return` does not return unless it receives a message. +- On Windows, fix bug where `RedrawRequested` would only get emitted every other iteration of the event loop. +- On X11, fix deadlock on window state when handling certain window events. +- `WindowBuilder` now implements `Default`. +- **Breaking:** `WindowEvent::CursorMoved` changed to `f64` units, preserving high-precision data supplied by most backends +- On Wayland, fix coordinates in mouse events when scale factor isn't 1 +- On Web, add the ability to provide a custom canvas +- **Breaking:** On Wayland, the `WaylandTheme` struct has been replaced with a `Theme` trait, allowing for extra configuration + +# 0.20.0 (2020-01-05) + +- On X11, fix `ModifiersChanged` emitting incorrect modifier change events +- **Breaking**: Overhaul how Winit handles DPI: + + Window functions and events now return `PhysicalSize` instead of `LogicalSize`. + + Functions that take `Size` or `Position` types can now take either `Logical` or `Physical` types. + + `hidpi_factor` has been renamed to `scale_factor`. + + `HiDpiFactorChanged` has been renamed to `ScaleFactorChanged`, and lets you control how the OS + resizes the window in response to the change. + + On X11, deprecate `WINIT_HIDPI_FACTOR` environment variable in favor of `WINIT_X11_SCALE_FACTOR`. + + `Size` and `Position` types are now generic over their exact pixel type. + +# 0.20.0 Alpha 6 (2020-01-03) + +- On macOS, fix `set_cursor_visible` hides cursor outside of window. +- On macOS, fix `CursorEntered` and `CursorLeft` events fired at old window size. +- On macOS, fix error when `set_fullscreen` is called during fullscreen transition. +- On all platforms except mobile and WASM, implement `Window::set_minimized`. +- On X11, fix `CursorEntered` event being generated for non-winit windows. +- On macOS, fix crash when starting maximized without decorations. +- On macOS, fix application not terminating on `run_return`. +- On Wayland, fix cursor icon updates on window borders when using CSD. +- On Wayland, under mutter(GNOME Wayland), fix CSD being behind the status bar, when starting window in maximized mode. +- On Windows, theme the title bar according to whether the system theme is "Light" or "Dark". +- Added `WindowEvent::ThemeChanged` variant to handle changes to the system theme. Currently only implemented on Windows. +- **Breaking**: Changes to the `RedrawRequested` event (#1041): + - `RedrawRequested` has been moved from `WindowEvent` to `Event`. + - `EventsCleared` has been renamed to `MainEventsCleared`. + - `RedrawRequested` is now issued only after `MainEventsCleared`. + - `RedrawEventsCleared` is issued after each set of `RedrawRequested` events. +- Implement synthetic window focus key events on Windows. +- **Breaking**: Change `ModifiersState` to a `bitflags` struct. +- On Windows, implement `VirtualKeyCode` translation for `LWin` and `RWin`. +- On Windows, fix closing the last opened window causing `DeviceEvent`s to stop getting emitted. +- On Windows, fix `Window::set_visible` not setting internal flags correctly. This resulted in some weird behavior. +- Add `DeviceEvent::ModifiersChanged`. + - Deprecate `modifiers` fields in other events in favor of `ModifiersChanged`. +- On X11, `WINIT_HIDPI_FACTOR` now dominates `Xft.dpi` when picking DPI factor for output. +- On X11, add special value `randr` for `WINIT_HIDPI_FACTOR` to make winit use self computed DPI factor instead of the one from `Xft.dpi`. + +# 0.20.0 Alpha 5 (2019-12-09) + - On macOS, fix application termination on `ControlFlow::Exit` - On Windows, fix missing `ReceivedCharacter` events when Alt is held. - On macOS, stop emitting private corporate characters in `ReceivedCharacter` events. @@ -8,6 +223,13 @@ - On X11, fix key modifiers being incorrectly reported. - On X11, fix window creation hanging when another window is fullscreen. - On Windows, fix focusing unfocused windows when switching from fullscreen to windowed. +- On X11, fix reporting incorrect DPI factor when waking from suspend. +- Change `EventLoopClosed` to contain the original event. +- **Breaking**: Add `is_synthetic` field to `WindowEvent` variant `KeyboardInput`, + indicating that the event is generated by winit. +- On X11, generate synthetic key events for keys held when a window gains or loses focus. +- On X11, issue a `CursorMoved` event when a `Touch` event occurs, + as X11 implicitly moves the cursor for such events. # 0.20.0 Alpha 4 (2019-10-18) @@ -35,6 +257,7 @@ - On X11, return dummy monitor data to avoid panicking when no monitors exist. - On X11, prevent stealing input focus when creating a new window. Only steal input focus when entering fullscreen mode. +- On Wayland, fixed DeviceEvents for relative mouse movement is not always produced - On Wayland, add support for set_cursor_visible and set_cursor_grab. - On Wayland, fixed DeviceEvents for relative mouse movement is not always produced. - Removed `derivative` crate dependency. @@ -51,6 +274,7 @@ reduces the potential for cross-platform compatibility gotchyas. - On Windows and Linux X11/Wayland, add platform-specific functions for creating an `EventLoop` outside the main thread. - On Wayland, drop resize events identical to the current window size. +- On Windows, fix window rectangle not getting set correctly on high-DPI systems. # 0.20.0 Alpha 3 (2019-08-14) diff --git a/Cargo.toml b/Cargo.toml index 99586de807..30995a87ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.20.0-alpha4" +version = "0.24.0" authors = ["The winit contributors", "Pierre Krieger "] description = "Cross-platform window creation library." edition = "2018" @@ -12,11 +12,16 @@ documentation = "https://docs.rs/winit" categories = ["gui"] [package.metadata.docs.rs] -features = ["serde"] +features = ["serde", "web-sys"] +default-target = "x86_64-unknown-linux-gnu" +targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "i686-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "wasm32-unknown-unknown"] [features] +default = ["x11", "wayland"] web-sys = ["web_sys", "wasm-bindgen", "instant/wasm-bindgen"] stdweb = ["std_web", "instant/stdweb"] +x11 = ["x11-dl", "mio", "mio-extras", "percent-encoding", "parking_lot"] +wayland = ["wayland-client", "sctk"] [dependencies] instant = "0.1" @@ -25,31 +30,34 @@ libc = "0.2.64" log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } raw-window-handle = "0.3" +bitflags = "1" [dev-dependencies] -image = "0.21" -env_logger = "0.5" +image = "0.23.12" +simple_logger = "1.9" -[target.'cfg(target_os = "android")'.dependencies.android_glue] -version = "0.2" +[target.'cfg(target_os = "android")'.dependencies] +ndk = "0.3" +ndk-sys = "0.2.0" +ndk-glue = "0.3" -[target.'cfg(target_os = "ios")'.dependencies] -objc = "0.2.3" +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +objc = "0.2.7" [target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.19.1" -core-foundation = "0.6" -core-graphics = "0.17.3" -dispatch = "0.1.4" -objc = "0.2.3" +cocoa = "0.24" +core-foundation = "0.9" +core-graphics = "0.22" +dispatch = "0.2.0" [target.'cfg(target_os = "macos")'.dependencies.core-video-sys] -version = "0.1.3" +version = "0.1.4" default_features = false features = ["display_link"] -[target.'cfg(any(target_os = "ios", target_os = "windows"))'.dependencies] -bitflags = "1" +[target.'cfg(target_os = "windows")'.dependencies] +parking_lot = "0.11" +rusty-xinput = "1" [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" @@ -58,6 +66,8 @@ features = [ "commctrl", "dwmapi", "errhandlingapi", + "imm", + "hidpi", "hidusage", "libloaderapi", "objbase", @@ -73,17 +83,17 @@ features = [ "wingdi", "winnt", "winuser", + "xinput" ] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -wayland-client = { version = "0.23.0", features = [ "dlopen", "egl", "cursor", "eventloop"] } -calloop = "0.4.2" -smithay-client-toolkit = "0.6" -x11-dl = "2.18.3" -percent-encoding = "2.0" - -[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows"))'.dependencies.parking_lot] -version = "0.10" +wayland-client = { version = "0.28", features = [ "dlopen"] , optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.12", optional = true } +mio = { version = "0.6", optional = true } +mio-extras = { version = "2.0", optional = true } +x11-dl = { version = "2.18.5", optional = true } +percent-encoding = { version = "2.0", optional = true } +parking_lot = { version = "0.11.0", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] package = "web-sys" @@ -91,6 +101,8 @@ version = "0.3.22" optional = true features = [ 'console', + "AddEventListenerOptions", + 'CssStyleDeclaration', 'BeforeUnloadEvent', 'Document', 'DomRect', @@ -101,11 +113,28 @@ features = [ 'HtmlCanvasElement', 'HtmlElement', 'KeyboardEvent', + 'MediaQueryList', + 'MediaQueryListEvent', 'MouseEvent', 'Node', + 'Navigator', 'PointerEvent', 'Window', - 'WheelEvent' + 'WheelEvent', + 'Gamepad', + 'GamepadAxisMoveEvent', + 'GamepadAxisMoveEventInit', + 'GamepadButton', + 'GamepadButtonEvent', + 'GamepadButtonEventInit', + 'GamepadEvent', + 'GamepadEventInit', + 'GamepadHand', + 'GamepadHapticActuator', + 'GamepadHapticActuatorType', + 'GamepadMappingType', + 'GamepadPose', + 'GamepadServiceTest' ] [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] @@ -117,3 +146,6 @@ package = "stdweb" version = "=0.4.20" optional = true features = ["experimental_features_which_may_break_on_minor_version_bumps"] + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +console_log = "0.2" diff --git a/FEATURES.md b/FEATURES.md index b83caac0cc..4ebb64291a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -80,6 +80,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Window maximization**: The windows created by winit can be maximized upon creation. - **Window maximization toggle**: The windows created by winit can be maximized and unmaximized after creation. +- **Window minimization**: The windows created by winit can be minimized after creation. - **Fullscreen**: The windows created by winit can be put into fullscreen mode. - **Fullscreen toggle**: The windows created by winit can be switched to and from fullscreen after creation. @@ -108,14 +109,16 @@ If your PR makes notable changes to Winit's features, please update this section translating keypresses into UTF-8 characters, handling dead keys and IMEs. - **Drag & Drop**: Dragging content into winit, detecting when content enters, drops, or if the drop is cancelled. - **Raw Device Events**: Capturing input from input devices without any OS filtering. -- **Gamepad/Joystick events**: Capturing input from gampads and joysticks. -- **Device movement events:**: Capturing input from the device gyroscope and accelerometer. +- **Gamepad/Joystick events**: Capturing input from gamepads and joysticks. +- **Device movement events**: Capturing input from the device gyroscope and accelerometer. ## Platform ### Windows * Setting the taskbar icon * Setting the parent window +* Setting a menu bar * `WS_EX_NOREDIRECTIONBITMAP` support +* Theme the title bar according to Windows 10 Dark Mode setting or set a preferred theme ### macOS * Window activation policy @@ -147,6 +150,9 @@ If your PR makes notable changes to Winit's features, please update this section * Getting the device idiom * Getting the preferred video mode +### Web +* Get if systems preferred color scheme is "dark" + ## Usability * `serde`: Enables serialization/deserialization of certain types with Serde. (Maintainer: @Osspial) @@ -173,12 +179,15 @@ Legend: |Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|N/A | |Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| |Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| +|Window minimization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| |Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**| -|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |**N/A**| +|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ \*1| |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| +\*1: `WindowEvent::ScaleFactorChanged` is not sent on `stdweb` backend. + ### System information |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | @@ -192,9 +201,9 @@ Legend: |Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A**| |Cursor grab |✔️ |▢[#165] |▢[#242] |✔️ |**N/A**|**N/A**|❓ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | -|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ | -|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ | -|Multitouch |✔️ |❌ |✔️ |✔️ |❓ |✔️ |✔️ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❌ | +|Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | |Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |✔️ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ | |Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | diff --git a/README.md b/README.md index c0549ad428..8ba6518d4a 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ [![Crates.io](https://img.shields.io/crates/v/winit.svg)](https://crates.io/crates/winit) [![Docs.rs](https://docs.rs/winit/badge.svg)](https://docs.rs/winit) -[![Build Status](https://travis-ci.org/rust-windowing/winit.svg?branch=master)](https://travis-ci.org/rust-windowing/winit) -[![Build status](https://ci.appveyor.com/api/projects/status/hr89but4x1n3dphq/branch/master?svg=true)](https://ci.appveyor.com/project/Osspial/winit/branch/master) +[![CI Status](https://github.com/rust-windowing/winit/workflows/CI/badge.svg)](https://github.com/rust-windowing/winit/actions) ```toml [dependencies] -winit = "0.20.0-alpha4" +winit = "0.24.0" ``` ## [Documentation](https://docs.rs/winit) @@ -46,12 +45,14 @@ fn main() { 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, - _ => *control_flow = ControlFlow::Wait, + _ => (), } }); } @@ -63,15 +64,59 @@ Winit is only officially supported on the latest stable version of the Rust comp Winit provides the following features, which can be enabled in your `Cargo.toml` file: * `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). +* `x11` (enabled by default): On Unix platform, compiles with the X11 backend +* `wayland` (enabled by default): On Unix platform, compiles with the Wayland backend ### Platform-specific usage #### WebAssembly -Building a binary will yield a `.js` file. In order to use it in an HTML file, you need to: +Winit supports compiling to the `wasm32-unknown-unknown` target with either a +`stdweb` or a `web-sys` backend for use on web browsers. However, please note +that **the `stdweb` backend is being deprecated and may be removed in a future +release of Winit**. The `web-sys` backend is also more feature complete. + +On the web platform, a Winit window is backed by a `` element. You can +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 +information on using Rust on WebAssembly, check out the [Rust and WebAssembly +book]. + +[web with_canvas]: https://docs.rs/winit/latest/wasm32-unknown-unknown/winit/platform/web/trait.WindowBuilderExtWebSys.html#tymethod.with_canvas +[web canvas getter]: https://docs.rs/winit/latest/wasm32-unknown-unknown/winit/platform/web/trait.WindowExtWebSys.html#tymethod.canvas +[web example]: ./examples/web.rs +[Rust and WebAssembly book]: https://rustwasm.github.io/book/ + +#### Android + +This library makes use of the [ndk-rs](https://github.com/rust-windowing/android-ndk-rs) crates, refer to that repo for more documentation. + +Running on an Android device needs a dynamic system library, add this to Cargo.toml: +```toml +[[example]] +name = "request_redraw_threaded" +crate-type = ["cdylib"] +``` + +And add this to the example file to add the native activity glue: +```rust +#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] +fn main() { + ... +} +``` + +And run the application with `cargo apk run --example request_redraw_threaded` + +#### MacOS + +To ensure compatibility with older MacOS systems, winit links to +CGDisplayCreateUUIDFromDisplayID through the CoreGraphics framework. +However, under certain setups this function is only available to be linked +through the newer ColorSync framework. So, winit provides the +`WINIT_LINK_COLORSYNC` environment variable which can be set to `1` or `true` +while compiling to enable linking via ColorSync. -- Put a `` element somewhere. A canvas corresponds to a winit "window". -- Write a Javascript code that creates a global variable named `Module`. Set `Module.canvas` to - the element of the `` element (in the example you would retrieve it via `document.getElementById("my_id")`). - More information [here](https://kripken.github.io/emscripten-site/docs/api_reference/module.html). -- Make sure that you insert the `.js` file generated by Rust after the `Module` variable is created. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 20ee2cc7b4..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,35 +0,0 @@ -environment: - matrix: - - TARGET: x86_64-pc-windows-msvc - CHANNEL: stable - - TARGET: i686-pc-windows-msvc - CHANNEL: stable - - TARGET: x86_64-pc-windows-gnu - CHANNEL: stable - - TARGET: i686-pc-windows-gnu - CHANNEL: stable - - TARGET: x86_64-pc-windows-msvc - CHANNEL: nightly - - TARGET: i686-pc-windows-msvc - CHANNEL: nightly - - TARGET: x86_64-pc-windows-gnu - CHANNEL: nightly - - TARGET: i686-pc-windows-gnu - CHANNEL: nightly -matrix: - allow_failures: - - CHANNEL: nightly -install: - - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - - rustup-init -yv --default-toolchain %CHANNEL% --default-host %TARGET% - - SET PATH=%PATH%;%USERPROFILE%\.cargo\bin - - SET PATH=%PATH%;C:\MinGW\bin - - rustc -V - - cargo -V - -build: false - -test_script: - - cargo test --verbose - - cargo test --features serde --verbose - - cargo doc --no-deps diff --git a/build.rs b/build.rs new file mode 100644 index 0000000000..a092e63631 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +fn main() { + // If building for macos and WINIT_LINK_COLORSYNC is set to true + // use CGDisplayCreateUUIDFromDisplayID from ColorSync instead of CoreGraphics + if std::env::var("CARGO_CFG_TARGET_OS").map_or(false, |os| os == "macos") + && std::env::var("WINIT_LINK_COLORSYNC") + .map_or(false, |v| v == "1" || v.eq_ignore_ascii_case("true")) + { + println!("cargo:rustc-cfg=use_colorsync_cgdisplaycreateuuidfromdisplayid"); + } +} diff --git a/examples/control_flow.rs b/examples/control_flow.rs new file mode 100644 index 0000000000..64372a9bc8 --- /dev/null +++ b/examples/control_flow.rs @@ -0,0 +1,114 @@ +use std::{thread, time}; + +use simple_logger::SimpleLogger; +use winit::{ + event::{Event, KeyboardInput, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + Wait, + WaitUntil, + Poll, +} + +const WAIT_TIME: time::Duration = time::Duration::from_millis(100); +const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); + +fn main() { + SimpleLogger::new().init().unwrap(); + + println!("Press '1' to switch to Wait mode."); + println!("Press '2' to switch to WaitUntil mode."); + println!("Press '3' to switch to Poll mode."); + println!("Press 'R' to toggle request_redraw() calls."); + println!("Press 'Esc' to close the window."); + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.") + .build(&event_loop) + .unwrap(); + + let mut mode = Mode::Wait; + let mut request_redraw = false; + let mut wait_cancelled = false; + let mut close_requested = false; + + event_loop.run(move |event, _, control_flow| { + use winit::event::{ElementState, StartCause, VirtualKeyCode}; + println!("{:?}", event); + match event { + Event::NewEvents(start_cause) => { + wait_cancelled = match start_cause { + StartCause::WaitCancelled { .. } => mode == Mode::WaitUntil, + _ => false, + } + } + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => { + close_requested = true; + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + virtual_keycode: Some(virtual_code), + state: ElementState::Pressed, + .. + }, + .. + } => match virtual_code { + VirtualKeyCode::Key1 => { + mode = Mode::Wait; + println!("\nmode: {:?}\n", mode); + } + VirtualKeyCode::Key2 => { + mode = Mode::WaitUntil; + println!("\nmode: {:?}\n", mode); + } + VirtualKeyCode::Key3 => { + mode = Mode::Poll; + println!("\nmode: {:?}\n", mode); + } + VirtualKeyCode::R => { + request_redraw = !request_redraw; + println!("\nrequest_redraw: {}\n", request_redraw); + } + VirtualKeyCode::Escape => { + close_requested = true; + } + _ => (), + }, + _ => (), + }, + Event::MainEventsCleared => { + if request_redraw && !wait_cancelled && !close_requested { + window.request_redraw(); + } + if close_requested { + *control_flow = ControlFlow::Exit; + } + } + Event::RedrawRequested(_window_id) => {} + Event::RedrawEventsCleared => { + *control_flow = match mode { + Mode::Wait => ControlFlow::Wait, + Mode::WaitUntil => { + if wait_cancelled { + *control_flow + } else { + ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME) + } + } + Mode::Poll => { + thread::sleep(POLL_SLEEP_TIME); + ControlFlow::Poll + } + }; + } + _ => (), + } + }); +} diff --git a/examples/cursor.rs b/examples/cursor.rs index d5bd34a980..a466e889a8 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyboardInput, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -5,6 +6,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); @@ -12,35 +14,39 @@ fn main() { let mut cursor_idx = 0; - event_loop.run(move |event, _, control_flow| match event { - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - input: - KeyboardInput { - 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, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + input: + KeyboardInput { + 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::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + return; + } + _ => (), } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - *control_flow = ControlFlow::Exit; - return; - } - _ => (), }); } diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 726aba56bd..90a94764de 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -1,10 +1,12 @@ +use simple_logger::SimpleLogger; use winit::{ - event::{DeviceEvent, ElementState, Event, KeyboardInput, WindowEvent}, + event::{DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -12,8 +14,11 @@ fn main() { .build(&event_loop) .unwrap(); + 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, @@ -22,7 +27,6 @@ fn main() { KeyboardInput { state: ElementState::Released, virtual_keycode: Some(key), - modifiers, .. }, .. @@ -30,11 +34,12 @@ fn main() { use winit::event::VirtualKeyCode::*; match key { Escape => *control_flow = ControlFlow::Exit, - G => window.set_cursor_grab(!modifiers.shift).unwrap(), - H => window.set_cursor_visible(modifiers.shift), + G => window.set_cursor_grab(!modifiers.shift()).unwrap(), + H => window.set_cursor_visible(modifiers.shift()), _ => (), } } + WindowEvent::ModifiersChanged(m) => modifiers = m, _ => (), }, Event::DeviceEvent { event, .. } => match event { diff --git a/examples/custom_events.rs b/examples/custom_events.rs index dba4362470..016754b590 100644 --- a/examples/custom_events.rs +++ b/examples/custom_events.rs @@ -1,5 +1,6 @@ #[cfg(not(target_arch = "wasm32"))] fn main() { + use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -11,6 +12,7 @@ fn main() { Timer, } + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::::with_user_event(); let _window = WindowBuilder::new() @@ -31,13 +33,17 @@ fn main() { } }); - event_loop.run(move |event, _, control_flow| match event { - Event::UserEvent(event) => println!("user event: {:?}", event), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => *control_flow = ControlFlow::Wait, + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::UserEvent(event) => println!("user event: {:?}", event), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } }); } diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index d4b83bb0f7..83fbde30db 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,10 +1,13 @@ use std::io::{stdin, stdout, Write}; + +use simple_logger::SimpleLogger; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; use winit::monitor::{MonitorHandle, VideoMode}; use winit::window::{Fullscreen, WindowBuilder}; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); print!("Please choose the fullscreen mode: (1) exclusive, (2) borderless: "); @@ -16,11 +19,10 @@ fn main() { let fullscreen = Some(match num { 1 => Fullscreen::Exclusive(prompt_for_video_mode(&prompt_for_monitor(&event_loop))), - 2 => Fullscreen::Borderless(prompt_for_monitor(&event_loop)), + 2 => Fullscreen::Borderless(Some(prompt_for_monitor(&event_loop))), _ => panic!("Please enter a valid number"), }); - let mut is_maximized = false; let mut decorations = true; let window = WindowBuilder::new() @@ -56,8 +58,8 @@ fn main() { println!("window.fullscreen {:?}", window.fullscreen()); } (VirtualKeyCode::M, ElementState::Pressed) => { - is_maximized = !is_maximized; - window.set_maximized(is_maximized); + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); } (VirtualKeyCode::D, ElementState::Pressed) => { decorations = !decorations; diff --git a/examples/gamepad.rs b/examples/gamepad.rs new file mode 100644 index 0000000000..dd09ac85bd --- /dev/null +++ b/examples/gamepad.rs @@ -0,0 +1,48 @@ +use winit::event::device::{GamepadEvent, GamepadHandle}; +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; + +fn main() { + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("The world's worst video game") + .build(&event_loop) + .unwrap(); + + println!("enumerating gamepads:"); + for gamepad in GamepadHandle::enumerate(&event_loop) { + println!( + " gamepad={:?}\tport={:?}\tbattery level={:?}", + gamepad, + gamepad.port(), + gamepad.battery_level() + ); + } + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| { + match event { + Event::GamepadEvent(gamepad_handle, event) => { + match event { + // Discard any Axis events that has a corresponding Stick event. + GamepadEvent::Axis { stick: true, .. } => (), + + // Discard any Stick event that falls inside the stick's deadzone. + GamepadEvent::Stick { + x_value, y_value, .. + } if (x_value.powi(2) + y_value.powi(2)).sqrt() < deadzone => (), + + _ => println!("[{:?}] {:#?}", gamepad_handle, event), + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/examples/gamepad_rumble.rs b/examples/gamepad_rumble.rs new file mode 100644 index 0000000000..8ae6ccc8c9 --- /dev/null +++ b/examples/gamepad_rumble.rs @@ -0,0 +1,60 @@ +use std::time::Instant; +use winit::event_loop::EventLoop; + +#[derive(Debug, Clone)] +enum Rumble { + None, + Left, + Right, +} + +fn main() { + let event_loop = EventLoop::new(); + + // You should generally use `GamepadEvent::Added/Removed` to detect gamepads, as doing that will + // allow you to more easily support gamepad hotswapping. However, we're using `enumerate` here + // because it makes this example more concise. + let gamepads = winit::event::device::GamepadHandle::enumerate(&event_loop).collect::>(); + + let rumble_patterns = &[ + (0.5, Rumble::None), + (2.0, Rumble::Left), + (0.5, Rumble::None), + (2.0, Rumble::Right), + ]; + let mut rumble_iter = rumble_patterns.iter().cloned().cycle(); + + let mut active_pattern = rumble_iter.next().unwrap(); + let mut timeout = active_pattern.0; + let mut timeout_start = Instant::now(); + + event_loop.run(move |_, _, _| { + if timeout <= active_pattern.0 { + let t = (timeout / active_pattern.0) * std::f64::consts::PI; + let intensity = t.sin(); + + for g in &gamepads { + let result = match active_pattern.1 { + Rumble::Left => g.rumble(intensity, 0.0), + Rumble::Right => g.rumble(0.0, intensity), + Rumble::None => Ok(()), + }; + + if let Err(e) = result { + println!("Rumble failed: {:?}", e); + } + } + + timeout = (Instant::now() - timeout_start).as_millis() as f64 / 1000.0; + } else { + active_pattern = rumble_iter.next().unwrap(); + println!( + "Rumbling {:?} for {:?} seconds", + active_pattern.1, active_pattern.0 + ); + + timeout = 0.0; + timeout_start = Instant::now(); + } + }); +} diff --git a/examples/handling_close.rs b/examples/handling_close.rs index d67c0046a7..8334c1773f 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use winit::{ event::{Event, KeyboardInput, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -5,6 +6,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let _window = WindowBuilder::new() diff --git a/examples/min_max_size.rs b/examples/min_max_size.rs index 4b24d16191..9a58ed6c69 100644 --- a/examples/min_max_size.rs +++ b/examples/min_max_size.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use winit::{ dpi::LogicalSize, event::{Event, WindowEvent}, @@ -6,6 +7,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); @@ -14,6 +16,7 @@ fn main() { window.set_max_inner_size(Some(LogicalSize::new(800.0, 400.0))); event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; println!("{:?}", event); match event { @@ -21,7 +24,7 @@ fn main() { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, - _ => *control_flow = ControlFlow::Wait, + _ => (), } }); } diff --git a/examples/minimize.rs b/examples/minimize.rs new file mode 100644 index 0000000000..eb02a752c9 --- /dev/null +++ b/examples/minimize.rs @@ -0,0 +1,41 @@ +extern crate winit; + +use simple_logger::SimpleLogger; +use winit::event::{Event, VirtualKeyCode, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + + // Keyboard input event to handle minimize via a hotkey + Event::WindowEvent { + event: WindowEvent::KeyboardInput { input, .. }, + window_id, + } => { + if window_id == window.id() { + // Pressing the 'M' key will minimize the window + if input.virtual_keycode == Some(VirtualKeyCode::M) { + window.set_minimized(true); + } + } + } + _ => (), + } + }); +} diff --git a/examples/monitor_list.rs b/examples/monitor_list.rs index a6b24d2967..9c8b77e61c 100644 --- a/examples/monitor_list.rs +++ b/examples/monitor_list.rs @@ -1,6 +1,8 @@ +use simple_logger::SimpleLogger; use winit::{event_loop::EventLoop, window::WindowBuilder}; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 8ce3607799..68cdb60b78 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -1,28 +1,28 @@ #[cfg(not(target_arch = "wasm32"))] fn main() { - extern crate env_logger; - use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; + use simple_logger::SimpleLogger; use winit::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{CursorIcon, Fullscreen, WindowBuilder}, }; const WINDOW_COUNT: usize = 3; - const WINDOW_SIZE: (u32, u32) = (600, 400); + const WINDOW_SIZE: PhysicalSize = PhysicalSize::new(600, 400); - env_logger::init(); + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let mut window_senders = HashMap::with_capacity(WINDOW_COUNT); for _ in 0..WINDOW_COUNT { let window = WindowBuilder::new() - .with_inner_size(WINDOW_SIZE.into()) + .with_inner_size(WINDOW_SIZE) .build(&event_loop) .unwrap(); - let mut video_modes: Vec<_> = window.current_monitor().video_modes().collect(); + let mut video_modes: Vec<_> = window.current_monitor().unwrap().video_modes().collect(); let mut video_mode_id = 0usize; let (tx, rx) = mpsc::channel(); @@ -35,7 +35,7 @@ fn main() { // was moved to an another monitor, so that the window // appears on this monitor instead when we go fullscreen let previous_video_mode = video_modes.iter().cloned().nth(video_mode_id); - video_modes = window.current_monitor().video_modes().collect(); + video_modes = window.current_monitor().unwrap().video_modes().collect(); video_mode_id = video_mode_id.min(video_modes.len()); let video_mode = video_modes.iter().nth(video_mode_id); @@ -49,6 +49,7 @@ fn main() { ); } } + #[allow(deprecated)] WindowEvent::KeyboardInput { input: KeyboardInput { @@ -60,7 +61,7 @@ fn main() { .. } => { window.set_title(&format!("{:?}", key)); - let state = !modifiers.shift; + let state = !modifiers.shift(); use VirtualKeyCode::*; match key { A => window.set_always_on_top(state), @@ -81,10 +82,8 @@ fn main() { video_modes.iter().nth(video_mode_id).unwrap() ); } - F => window.set_fullscreen(match (state, modifiers.alt) { - (true, false) => { - Some(Fullscreen::Borderless(window.current_monitor())) - } + F => window.set_fullscreen(match (state, modifiers.alt()) { + (true, false) => Some(Fullscreen::Borderless(None)), (true, true) => Some(Fullscreen::Exclusive( video_modes.iter().nth(video_mode_id).unwrap().clone(), )), @@ -101,31 +100,38 @@ fn main() { println!("-> fullscreen : {:?}", window.fullscreen()); } L => window.set_min_inner_size(match state { - true => Some(WINDOW_SIZE.into()), + true => Some(WINDOW_SIZE), false => None, }), M => window.set_maximized(state), P => window.set_outer_position({ let mut position = window.outer_position().unwrap(); - let sign = if state { 1.0 } else { -1.0 }; - position.x += 10.0 * sign; - position.y += 10.0 * sign; + let sign = if state { 1 } else { -1 }; + position.x += 10 * sign; + position.y += 10 * sign; position }), Q => window.request_redraw(), R => window.set_resizable(state), - S => window.set_inner_size( - match state { - true => (WINDOW_SIZE.0 + 100, WINDOW_SIZE.1 + 100), - false => WINDOW_SIZE, + S => window.set_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 + .set_cursor_position(Position::Physical( + PhysicalPosition::new( + size.width as i32 / 2, + size.height as i32 / 2, + ), + )) + .unwrap() } - .into(), - ), - W => window - .set_cursor_position( - (WINDOW_SIZE.0 as i32 / 2, WINDOW_SIZE.1 as i32 / 2).into(), - ) - .unwrap(), + } Z => { window.set_visible(false); thread::sleep(Duration::from_secs(1)); @@ -161,7 +167,9 @@ fn main() { } _ => { if let Some(tx) = window_senders.get(&window_id) { - tx.send(event).unwrap(); + if let Some(event) = event.to_static() { + tx.send(event).unwrap(); + } } } }, diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 61dc1fd7ec..ec97eee097 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; + +use simple_logger::SimpleLogger; use winit::{ event::{ElementState, Event, KeyboardInput, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -6,6 +8,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let mut windows = HashMap::new(); @@ -16,6 +19,7 @@ fn main() { event_loop.run(move |event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; + match event { Event::WindowEvent { event, window_id } => { match event { diff --git a/examples/request_redraw.rs b/examples/request_redraw.rs index b55bd970d8..163f6a14d2 100644 --- a/examples/request_redraw.rs +++ b/examples/request_redraw.rs @@ -1,13 +1,12 @@ -use instant::Instant; -use std::time::Duration; - +use simple_logger::SimpleLogger; use winit::{ - event::{Event, WindowEvent}, + event::{ElementState, Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -15,21 +14,26 @@ fn main() { .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::EventsCleared => { - window.request_redraw(); - *control_flow = ControlFlow::WaitUntil(Instant::now() + Duration::new(1, 0)) - } - Event::WindowEvent { - event: WindowEvent::RedrawRequested, - .. - } => { - println!("{:?}", event); + event_loop.run(move |event, _, control_flow| { + println!("{:?}", event); + + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Released, + .. + } => { + window.request_redraw(); + } + _ => (), + }, + Event::RedrawRequested(_) => { + println!("\nredrawing!\n"); + } + _ => (), } - _ => (), }); } diff --git a/examples/request_redraw_threaded.rs b/examples/request_redraw_threaded.rs new file mode 100644 index 0000000000..1456f25448 --- /dev/null +++ b/examples/request_redraw_threaded.rs @@ -0,0 +1,40 @@ +use std::{thread, time}; + +use simple_logger::SimpleLogger; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + thread::spawn(move || loop { + thread::sleep(time::Duration::from_secs(1)); + window.request_redraw(); + }); + + event_loop.run(move |event, _, control_flow| { + println!("{:?}", event); + + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + _ => (), + }, + Event::RedrawRequested(_) => { + println!("\nredrawing!\n"); + } + _ => (), + } + }); +} diff --git a/examples/resizable.rs b/examples/resizable.rs index 8aa6f70631..17892d8741 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -1,23 +1,27 @@ +use simple_logger::SimpleLogger; use winit::{ + dpi::LogicalSize, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let mut resizable = false; let window = WindowBuilder::new() .with_title("Hit space to toggle resizability.") - .with_inner_size((400, 200).into()) + .with_inner_size(LogicalSize::new(400.0, 200.0)) .with_resizable(resizable) .build(&event_loop) .unwrap(); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; + match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, diff --git a/examples/set_ime_position.rs b/examples/set_ime_position.rs new file mode 100644 index 0000000000..1b2eecc373 --- /dev/null +++ b/examples/set_ime_position.rs @@ -0,0 +1,54 @@ +use simple_logger::SimpleLogger; +use winit::{ + dpi::PhysicalPosition, + event::{ElementState, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + window.set_title("A fantastic window!"); + + println!("Ime position will system default"); + println!("Click to set ime position to cursor's"); + + let mut cursor_position = PhysicalPosition::new(0.0, 0.0); + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + cursor_position = position; + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Released, + .. + }, + .. + } => { + println!( + "Setting ime position to {}, {}", + cursor_position.x, cursor_position.y + ); + window.set_ime_position(cursor_position); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + return; + } + _ => (), + } + }); +} diff --git a/examples/timer.rs b/examples/timer.rs index e1e3f29097..7bbb9685f4 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -1,5 +1,7 @@ use instant::Instant; use std::time::Duration; + +use simple_logger::SimpleLogger; use winit::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -7,6 +9,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let _window = WindowBuilder::new() diff --git a/examples/transparent.rs b/examples/transparent.rs index 8eddb90272..c9937cd4cd 100644 --- a/examples/transparent.rs +++ b/examples/transparent.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -5,6 +6,7 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -16,6 +18,7 @@ fn main() { window.set_title("A fantastic window!"); event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; println!("{:?}", event); match event { @@ -23,7 +26,7 @@ fn main() { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, - _ => *control_flow = ControlFlow::Wait, + _ => (), } }); } diff --git a/examples/video_modes.rs b/examples/video_modes.rs index f923fa9202..341f43855b 100644 --- a/examples/video_modes.rs +++ b/examples/video_modes.rs @@ -1,8 +1,16 @@ +use simple_logger::SimpleLogger; use winit::event_loop::EventLoop; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); - let monitor = event_loop.primary_monitor(); + let monitor = match event_loop.primary_monitor() { + Some(monitor) => monitor, + None => { + println!("No primary monitor detected."); + return; + } + }; println!("Listing available video modes:"); diff --git a/examples/web.rs b/examples/web.rs new file mode 100644 index 0000000000..46b024ccf2 --- /dev/null +++ b/examples/web.rs @@ -0,0 +1,74 @@ +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +pub fn main() { + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + #[cfg(feature = "web-sys")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window.canvas(); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + body.append_child(&canvas) + .expect("Append canvas to HTML body"); + } + + #[cfg(feature = "stdweb")] + { + use std_web::web::INode; + use winit::platform::web::WindowExtStdweb; + + let canvas = window.canvas(); + + let document = std_web::web::document(); + let body: std_web::web::Node = document.body().expect("Get HTML body").into(); + + body.append_child(&canvas); + } + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + #[cfg(feature = "web-sys")] + log::debug!("{:?}", event); + + #[cfg(feature = "stdweb")] + std_web::console!(log, "%s", format!("{:?}", event)); + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => *control_flow = ControlFlow::Exit, + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }); +} + +#[cfg(feature = "web-sys")] +mod wasm { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(start)] + pub fn run() { + console_log::init_with_level(log::Level::Debug); + + super::main(); + } +} diff --git a/examples/web/gamepad/stdweb/Cargo.toml b/examples/web/gamepad/stdweb/Cargo.toml new file mode 100644 index 0000000000..3d931fdca4 --- /dev/null +++ b/examples/web/gamepad/stdweb/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stdweb-gamepad" +version = "0.1.0" +authors = ["furiouzz "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +winit = { path = "../../../../", features = [ "stdweb" ] } +stdweb = "0.4.20" diff --git a/examples/web/gamepad/stdweb/src/main.rs b/examples/web/gamepad/stdweb/src/main.rs new file mode 100644 index 0000000000..7b545bc18d --- /dev/null +++ b/examples/web/gamepad/stdweb/src/main.rs @@ -0,0 +1,80 @@ +use winit::{ + event::{device::GamepadEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +use stdweb::js; + +/** + * Build example (from examples/web/gamepad/stdweb): + * cargo web build + * Run example (from examples/web/gamepad/stdweb): + * cargo web start + * Development (from project root): + * npx nodemon --watch src --watch examples/web/gamepad/stdweb/src -e rs --exec 'cargo web check' + */ + +pub fn main() { + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Gamepad tests") + .build(&event_loop) + .unwrap(); + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| match event { + Event::GamepadEvent(gamepad_handle, event) => match event { + GamepadEvent::Axis { + axis_id, + axis, + value, + stick, + } if value > deadzone => { + let string = format!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick); + js! { console.log( @{string} ); } + } + + GamepadEvent::Stick { + x_id, + y_id, + x_value, + y_value, + side, + } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { + let string = format!( + "Stick {:#?} {:#?} {:#?} {:#?} {:#?}", + x_id, y_id, x_value, y_value, side + ); + js! { console.log( @{string} ); } + } + + GamepadEvent::Button { + button_id, + button, + state, + } => { + let string = format!("Button {:#?} {:#?} {:#?}", button_id, button, state); + js! { console.log( @{string} ); } + } + + GamepadEvent::Added => { + let string = format!("[{:?}] {:#?}", gamepad_handle, event); + js! { console.log( @{string} ); } + } + GamepadEvent::Removed => { + let string = format!("[{:?}] {:#?}", gamepad_handle, event); + js! { console.log( @{string} ); } + } + + _ => {} + }, + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + }); +} diff --git a/examples/web/gamepad/websys/.gitignore b/examples/web/gamepad/websys/.gitignore new file mode 100644 index 0000000000..5968859b78 --- /dev/null +++ b/examples/web/gamepad/websys/.gitignore @@ -0,0 +1,7 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +.DS_Store diff --git a/examples/web/gamepad/websys/Cargo.toml b/examples/web/gamepad/websys/Cargo.toml new file mode 100644 index 0000000000..f4ad9c90d1 --- /dev/null +++ b/examples/web/gamepad/websys/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "websys-gamepad" +version = "0.0.1" +authors = ["The winit contributors", "Pierre Krieger "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +winit = { path = "../../../../", features = [ "web-sys" ] } +wasm-bindgen = "0.2.45" +wasm-bindgen-test = "0.3.8" +web-sys = { version = "0.3.22", features = [ "console" ] } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = "0.1.6" diff --git a/examples/web/gamepad/websys/files/gamepad.html b/examples/web/gamepad/websys/files/gamepad.html new file mode 100644 index 0000000000..acb70ea349 --- /dev/null +++ b/examples/web/gamepad/websys/files/gamepad.html @@ -0,0 +1,26 @@ + + + + + + + + Gamepad + + + + + + + + + diff --git a/examples/web/gamepad/websys/src/lib.rs b/examples/web/gamepad/websys/src/lib.rs new file mode 100644 index 0000000000..94a5b42fcb --- /dev/null +++ b/examples/web/gamepad/websys/src/lib.rs @@ -0,0 +1,85 @@ +mod utils; + +use wasm_bindgen::prelude::*; +use winit::{ + event::{device::GamepadEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +/** + * Build example (from examples/gamepad/websys): + * wasm-pack build --target web + * Run web server (from examples/gamepad/websys): + * npx http-server + * Open your browser at http://localhost:8000/files/${EXAMPLE}.html + * Development (from project root): + * npx nodemon --watch src --watch examples/web/gamepad/websys/src -e rs --exec 'cd examples/web/gamepad/websys && wasm-pack build --target web' + */ + +macro_rules! console_log { + ($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into())) +} + +#[wasm_bindgen(start)] +pub fn example_gamepad() { + utils::set_panic_hook(); // needed for error stack trace + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Gamepad tests") + .build(&event_loop) + .unwrap(); + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| match event { + Event::GamepadEvent(gamepad_handle, event) => match event { + GamepadEvent::Axis { + axis_id, + axis, + value, + stick, + } if value > deadzone => { + console_log!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick) + } + + GamepadEvent::Stick { + x_id, + y_id, + x_value, + y_value, + side, + } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { + console_log!( + "Stick {:#?} {:#?} {:#?} {:#?} {:#?}", + x_id, + y_id, + x_value, + y_value, + side + ) + } + + GamepadEvent::Button { + button_id, + button, + state, + } => { + console_log!("Button {:#?} {:#?} {:#?}", button_id, button, state) + } + + GamepadEvent::Added => { + console_log!("[{:?}] {:#?}", gamepad_handle, event) + } + GamepadEvent::Removed => console_log!("[{:?}] {:#?}", gamepad_handle, event), + + _ => {} + }, + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + }); +} diff --git a/examples/web/gamepad/websys/src/utils.rs b/examples/web/gamepad/websys/src/utils.rs new file mode 100644 index 0000000000..9aae24c65f --- /dev/null +++ b/examples/web/gamepad/websys/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + // #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/examples/window.rs b/examples/window.rs index 12e0593e93..783578de65 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -5,14 +6,17 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new() .with_title("A fantastic window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) .build(&event_loop) .unwrap(); event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; println!("{:?}", event); match event { @@ -20,7 +24,10 @@ fn main() { event: WindowEvent::CloseRequested, window_id, } if window_id == window.id() => *control_flow = ControlFlow::Exit, - _ => *control_flow = ControlFlow::Wait, + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), } }); } diff --git a/examples/window_debug.rs b/examples/window_debug.rs index 0ecd37b398..577ad5cd73 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -1,5 +1,6 @@ // This example is used by developers to test various window functions. +use simple_logger::SimpleLogger; use winit::{ dpi::{LogicalSize, PhysicalSize}, event::{DeviceEvent, ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, @@ -8,26 +9,25 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); let window = WindowBuilder::new() .with_title("A fantastic window!") - .with_inner_size(LogicalSize::from((100, 100))) + .with_inner_size(LogicalSize::new(100.0, 100.0)) .build(&event_loop) .unwrap(); eprintln!("debugging keys:"); eprintln!(" (E) Enter exclusive fullscreen"); eprintln!(" (F) Toggle borderless fullscreen"); - #[cfg(waiting_for_set_minimized)] + eprintln!(" (P) Toggle borderless fullscreen on system's preffered monitor"); eprintln!(" (M) Toggle minimized"); eprintln!(" (Q) Quit event loop"); eprintln!(" (V) Toggle visibility"); eprintln!(" (X) Toggle maximized"); - #[cfg(waiting_for_set_minimized)] let mut minimized = false; - let mut maximized = false; let mut visible = true; event_loop.run(move |event, _, control_flow| { @@ -43,7 +43,6 @@ fn main() { }), .. } => match key { - #[cfg(waiting_for_set_minimized)] VirtualKeyCode::M => { if minimized { minimized = !minimized; @@ -68,16 +67,15 @@ fn main() { .. } => match key { VirtualKeyCode::E => { - fn area(size: PhysicalSize) -> f64 { + fn area(size: PhysicalSize) -> u32 { size.width * size.height } - let monitor = window.current_monitor(); - if let Some(mode) = monitor.video_modes().max_by(|a, b| { - area(a.size()) - .partial_cmp(&area(b.size())) - .expect("NaN in video mode size") - }) { + 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"); @@ -91,7 +89,13 @@ fn main() { window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); } } - #[cfg(waiting_for_set_minimized)] + VirtualKeyCode::P => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + } VirtualKeyCode::M => { minimized = !minimized; window.set_minimized(minimized); @@ -104,8 +108,8 @@ fn main() { window.set_visible(visible); } VirtualKeyCode::X => { - maximized = !maximized; - window.set_maximized(maximized); + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); } _ => (), }, diff --git a/examples/window_icon.rs b/examples/window_icon.rs index 8fdcdd405e..6c79625505 100644 --- a/examples/window_icon.rs +++ b/examples/window_icon.rs @@ -1,5 +1,7 @@ extern crate image; use std::path::Path; + +use simple_logger::SimpleLogger; use winit::{ event::Event, event_loop::{ControlFlow, EventLoop}, @@ -7,6 +9,8 @@ use winit::{ }; fn main() { + SimpleLogger::new().init().unwrap(); + // You'll have to choose an icon size at your own discretion. On X11, the desired size varies // by WM, and on Windows, you still have to account for screen scaling. Here we use 32px, // since it seems to work well enough in most cases. Be careful about going too high, or @@ -27,6 +31,7 @@ fn main() { event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; + if let Event::WindowEvent { event, .. } = event { use winit::event::WindowEvent::*; match event { @@ -42,13 +47,11 @@ fn main() { fn load_icon(path: &Path) -> Icon { let (icon_rgba, icon_width, icon_height) = { - let image = image::open(path).expect("Failed to open icon path"); - use image::{GenericImageView, Pixel}; + let image = image::open(path) + .expect("Failed to open icon path") + .into_rgba8(); let (width, height) = image.dimensions(); - let mut rgba = Vec::with_capacity((width * height) as usize * 4); - for (_, _, pixel) in image.pixels() { - rgba.extend_from_slice(&pixel.to_rgba().data); - } + let rgba = image.into_raw(); (rgba, width, height) }; Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") diff --git a/examples/window_run_return.rs b/examples/window_run_return.rs index 5096ef315d..6bedfcd82c 100644 --- a/examples/window_run_return.rs +++ b/examples/window_run_return.rs @@ -10,14 +10,17 @@ ))] fn main() { use std::{thread::sleep, time::Duration}; + + use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - platform::desktop::EventLoopExtDesktop, + platform::run_return::EventLoopExtRunReturn, window::WindowBuilder, }; let mut event_loop = EventLoop::new(); + SimpleLogger::new().init().unwrap(); let _window = WindowBuilder::new() .with_title("A fantastic window!") .build(&event_loop) @@ -27,6 +30,8 @@ fn main() { while !quit { event_loop.run_return(|event, _, control_flow| { + *control_flow = ControlFlow::Wait; + if let Event::WindowEvent { event, .. } = &event { // Print only Window events to reduce noise println!("{:?}", event); @@ -38,16 +43,16 @@ fn main() { .. } => { quit = true; - *control_flow = ControlFlow::Exit; } - Event::EventsCleared => { + Event::MainEventsCleared => { *control_flow = ControlFlow::Exit; } - _ => *control_flow = ControlFlow::Wait, + _ => (), } }); // Sleep for 1/60 second to simulate rendering + println!("rendering"); sleep(Duration::from_millis(16)); } } diff --git a/src/dpi.rs b/src/dpi.rs index 8e6c80fa99..ed30abb79b 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -1,331 +1,504 @@ -//! DPI is important, so read the docs for this module if you don't want to be confused. +//! UI scaling is important, so read the docs for this module if you don't want to be confused. //! -//! Originally, `winit` dealt entirely in physical pixels (excluding unintentional inconsistencies), but now all -//! window-related functions both produce and consume logical pixels. Monitor-related functions still use physical -//! pixels, as do any context-related functions in `glutin`. +//! ## Why should I care about UI scaling? //! -//! If you've never heard of these terms before, then you're not alone, and this documentation will explain the -//! concepts. +//! 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 +//! mobile screens range from below 720p to above 1440p, and desktop screens range from 720p to 5K +//! and beyond. //! -//! Modern screens have a defined physical resolution, most commonly 1920x1080. Indepedent of that is the amount of -//! space the screen occupies, which is to say, the height and width in millimeters. The relationship between these two -//! measurements is the *pixel density*. Mobile screens require a high pixel density, as they're held close to the -//! eyes. Larger displays also require a higher pixel density, hence the growing presence of 1440p and 4K displays. +//! 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, +//! 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 +//! problem. //! -//! So, this presents a problem. Let's say we want to render a square 100px button. It will occupy 100x100 of the -//! screen's pixels, which in many cases, seems perfectly fine. However, because this size doesn't account for the -//! screen's dimensions or pixel density, the button's size can vary quite a bit. On a 4K display, it would be unusably -//! small. +//! Failure to account for the scale factor can create a significantly degraded user experience. +//! Most notably, it can make users feel like they have bad eyesight, which will potentially cause +//! them to think about growing elderly, resulting in them having an existential crisis. Once users +//! enter that state, they will no longer be focused on your application. //! -//! That's a description of what happens when the button is 100x100 *physical* pixels. Instead, let's try using 100x100 -//! *logical* pixels. To map logical pixels to physical pixels, we simply multiply by the DPI (dots per inch) factor. -//! On a "typical" desktop display, the DPI factor will be 1.0, so 100x100 logical pixels equates to 100x100 physical -//! pixels. However, a 1440p display may have a DPI factor of 1.25, so the button is rendered as 125x125 physical pixels. -//! Ideally, the button now has approximately the same perceived size across varying displays. +//! ## How should I handle it? //! -//! Failure to account for the DPI factor can create a badly degraded user experience. Most notably, it can make users -//! feel like they have bad eyesight, which will potentially cause them to think about growing elderly, resulting in -//! them entering an existential panic. Once users enter that state, they will no longer be focused on your application. +//! 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 +//! with a scale factor of `2.0`, or 75 pixels across with a scale factor of `1.5`. //! -//! There are two ways to get the DPI factor: -//! - You can track the [`HiDpiFactorChanged`](crate::event::WindowEvent::HiDpiFactorChanged) event of your -//! windows. This event is sent any time the DPI factor changes, either because the window moved to another monitor, -//! or because the user changed the configuration of their screen. -//! - You can also retrieve the DPI factor of a monitor by calling -//! [`MonitorHandle::hidpi_factor`](crate::monitor::MonitorHandle::hidpi_factor), or the -//! current DPI factor applied to a window by calling -//! [`Window::hidpi_factor`](crate::window::Window::hidpi_factor), which is roughly equivalent -//! to `window.current_monitor().hidpi_factor()`. +//! 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 +//! than any DPI-dependent units. //! -//! Depending on the platform, the window's actual DPI factor may only be known after -//! the event loop has started and your window has been drawn once. To properly handle these cases, -//! the most robust way is to monitor the [`HiDpiFactorChanged`](crate::event::WindowEvent::HiDpiFactorChanged) -//! event and dynamically adapt your drawing logic to follow the DPI factor. +//! ### Position and Size types //! -//! Here's an overview of what sort of DPI factors you can expect, and where they come from: -//! - **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" DPI factors, i.e. -//! 1.0, 1.25, 1.5... on Windows 7, the DPI factor is global and changing it requires logging out. -//! - **macOS:** The buzzword is "retina displays", which have a DPI factor of 2.0. Otherwise, the DPI factor is 1.0. -//! Intermediate DPI factors are never used, thus 1440p displays/etc. aren't properly supported. It's possible for any -//! display to use that 2.0 DPI factor, given the use of the command line. -//! - **X11:** On X11, we calculate the DPI factor based on the millimeter dimensions provided by XRandR. This can -//! result in a wide range of possible values, including some interesting ones like 1.0833333333333333. This can be -//! overridden using the `WINIT_HIDPI_FACTOR` environment variable, though that's not recommended. -//! - **Wayland:** On Wayland, DPI factors are set per-screen by the server, and are always integers (most often 1 or 2). -//! - **iOS:** DPI factors are both constant and device-specific on iOS. -//! - **Android:** This feature isn't yet implemented on Android, so the DPI factor will always be returned as 1.0. -//! - **Web:** DPI factors are handled by the browser and will always be 1.0 for your application. +//! Winit's `Physical(Position|Size)` types correspond with the actual pixels on the device, and the +//! `Logical(Position|Size)` types correspond to the physical pixels divided by the scale factor. +//! All of Winit's functions return physical types, but can take either logical or physical +//! coordinates as input, allowing you to use the most convenient coordinate system for your +//! particular application. //! -//! The window's logical size is conserved across DPI changes, resulting in the physical size changing instead. This -//! may be surprising on X11, but is quite standard elsewhere. Physical size changes always produce a -//! [`Resized`](crate::event::WindowEvent::Resized) event, even on platforms where no resize actually occurs, -//! such as macOS and Wayland. As a result, it's not necessary to separately handle -//! [`HiDpiFactorChanged`](crate::event::WindowEvent::HiDpiFactorChanged) if you're only listening for size. +//! Winit's position and size types 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 +//! 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. //! -//! Your GPU has no awareness of the concept of logical pixels, and unless you like wasting pixel density, your -//! framebuffer's size should be in physical pixels. +//! ### Events //! -//! `winit` will send [`Resized`](crate::event::WindowEvent::Resized) events whenever a window's logical size -//! changes, and [`HiDpiFactorChanged`](crate::event::WindowEvent::HiDpiFactorChanged) events -//! whenever the DPI factor changes. Receiving either of these events means that the physical size of your window has -//! changed, and you should recompute it using the latest values you received for each. If the logical size and the -//! DPI factor change simultaneously, `winit` will send both events together; thus, it's recommended to buffer -//! these events and process them at the end of the queue. +//! Winit will dispatch a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged) +//! event whenever a window's scale factor has changed. This can happen if the user drags their +//! window from a standard-resolution monitor to a high-DPI monitor, or if the user changes their +//! DPI settings. This gives you a chance to rescale your application's UI elements and adjust how +//! the platform changes the window's size to reflect the new scale factor. If a window hasn't +//! received a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged) event, +//! then its scale factor is `1.0`. //! -//! If you never received any [`HiDpiFactorChanged`](crate::event::WindowEvent::HiDpiFactorChanged) events, -//! then your window's DPI factor is 1. +//! ## How is the scale factor calculated? +//! +//! 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 +//! global and changing it requires logging out. See [this article][windows_1] for technical +//! details. +//! - **macOS:** "retina displays" have a scale factor of 2.0. Otherwise, the scale factor is 1.0. +//! Intermediate scale factors are never used. It's possible for any display to use that 2.0 scale +//! factor, given the use of the command line. +//! - **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. +//! + If not present, use the value set in `Xft.dpi` in Xresources. +//! + Otherwise, calcuate the scale factor based on the millimeter monitor dimensions provided by XRandR. +//! +//! 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). +//! - **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. +//! - **Android:** Scale factors are set by the manufacturer to the value that best suits the +//! device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. +//! - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. +//! In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by +//! both the screen scaling and the browser zoom level and can go below `1.0`. +//! +//! [points]: https://en.wikipedia.org/wiki/Point_(typography) +//! [picas]: https://en.wikipedia.org/wiki/Pica_(typography) +//! [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows +//! [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html +//! [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ +//! [android_1]: https://developer.android.com/training/multiscreen/screendensities +//! [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + +pub trait Pixel: Copy + Into { + fn from_f64(f: f64) -> Self; + fn cast(self) -> P { + P::from_f64(self.into()) + } +} + +impl Pixel for u8 { + fn from_f64(f: f64) -> Self { + f.round() as u8 + } +} +impl Pixel for u16 { + fn from_f64(f: f64) -> Self { + f.round() as u16 + } +} +impl Pixel for u32 { + fn from_f64(f: f64) -> Self { + f.round() as u32 + } +} +impl Pixel for i8 { + fn from_f64(f: f64) -> Self { + f.round() as i8 + } +} +impl Pixel for i16 { + fn from_f64(f: f64) -> Self { + f.round() as i16 + } +} +impl Pixel for i32 { + fn from_f64(f: f64) -> Self { + f.round() as i32 + } +} +impl Pixel for f32 { + fn from_f64(f: f64) -> Self { + f as f32 + } +} +impl Pixel for f64 { + fn from_f64(f: f64) -> Self { + f + } +} -/// Checks that the DPI factor is a normal positive `f64`. +/// Checks that the scale factor is a normal positive `f64`. /// -/// All functions that take a DPI factor assert that this will return `true`. If you're sourcing DPI factors from +/// All functions that take a scale factor assert that this will return `true`. If you're sourcing scale factors from /// anywhere other than winit, it's recommended to validate them using this function before passing them to winit; /// otherwise, you risk panics. #[inline] -pub fn validate_hidpi_factor(dpi_factor: f64) -> bool { - dpi_factor.is_sign_positive() && dpi_factor.is_normal() +pub fn validate_scale_factor(scale_factor: f64) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() } /// A position represented in logical pixels. /// -/// The position is stored as floats, so please be careful. Casting floats to integers truncates the fractional part, -/// which can cause noticable issues. To help with that, an `Into<(i32, i32)>` implementation is provided which -/// does the rounding for you. +/// The position is stored as floats, so please be careful. Casting floats to integers truncates the +/// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>` +/// implementation is provided which does the rounding for you. #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct LogicalPosition { - pub x: f64, - pub y: f64, +pub struct LogicalPosition

{ + pub x: P, + pub y: P, } -impl LogicalPosition { +impl

LogicalPosition

{ #[inline] - pub fn new(x: f64, y: f64) -> Self { + pub const fn new(x: P, y: P) -> Self { LogicalPosition { x, y } } +} +impl LogicalPosition

{ #[inline] - pub fn from_physical>(physical: T, dpi_factor: f64) -> Self { - physical.into().to_logical(dpi_factor) + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) } #[inline] - pub fn to_physical(&self, dpi_factor: f64) -> PhysicalPosition { - assert!(validate_hidpi_factor(dpi_factor)); - let x = self.x * dpi_factor; - let y = self.y * dpi_factor; - PhysicalPosition::new(x, y) + pub fn to_physical(&self, scale_factor: f64) -> PhysicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() * scale_factor; + let y = self.y.into() * scale_factor; + PhysicalPosition::new(x, y).cast() } -} -impl From<(f64, f64)> for LogicalPosition { #[inline] - fn from((x, y): (f64, f64)) -> Self { - Self::new(x, y) + pub fn cast(&self) -> LogicalPosition { + LogicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } } } -impl From<(i32, i32)> for LogicalPosition { - #[inline] - fn from((x, y): (i32, i32)) -> Self { - Self::new(x as f64, y as f64) +impl From<(X, X)> for LogicalPosition

{ + fn from((x, y): (X, X)) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) } } -impl Into<(f64, f64)> for LogicalPosition { - #[inline] - fn into(self) -> (f64, f64) { - (self.x, self.y) +impl Into<(X, X)> for LogicalPosition

{ + fn into(self: Self) -> (X, X) { + (self.x.cast(), self.y.cast()) } } -impl Into<(i32, i32)> for LogicalPosition { - /// Note that this rounds instead of truncating. - #[inline] - fn into(self) -> (i32, i32) { - (self.x.round() as _, self.y.round() as _) +impl From<[X; 2]> for LogicalPosition

{ + fn from([x, y]: [X; 2]) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) + } +} + +impl Into<[X; 2]> for LogicalPosition

{ + fn into(self: Self) -> [X; 2] { + [self.x.cast(), self.y.cast()] } } /// A position represented in physical pixels. -/// -/// The position is stored as floats, so please be careful. Casting floats to integers truncates the fractional part, -/// which can cause noticable issues. To help with that, an `Into<(i32, i32)>` implementation is provided which -/// does the rounding for you. #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct PhysicalPosition { - pub x: f64, - pub y: f64, +pub struct PhysicalPosition

{ + pub x: P, + pub y: P, } -impl PhysicalPosition { +impl

PhysicalPosition

{ #[inline] - pub fn new(x: f64, y: f64) -> Self { + pub const fn new(x: P, y: P) -> Self { PhysicalPosition { x, y } } +} +impl PhysicalPosition

{ #[inline] - pub fn from_logical>(logical: T, dpi_factor: f64) -> Self { - logical.into().to_physical(dpi_factor) + pub fn from_logical>, X: Pixel>( + logical: T, + scale_factor: f64, + ) -> Self { + logical.into().to_physical(scale_factor) } #[inline] - pub fn to_logical(&self, dpi_factor: f64) -> LogicalPosition { - assert!(validate_hidpi_factor(dpi_factor)); - let x = self.x / dpi_factor; - let y = self.y / dpi_factor; - LogicalPosition::new(x, y) + pub fn to_logical(&self, scale_factor: f64) -> LogicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() / scale_factor; + let y = self.y.into() / scale_factor; + LogicalPosition::new(x, y).cast() } -} -impl From<(f64, f64)> for PhysicalPosition { #[inline] - fn from((x, y): (f64, f64)) -> Self { - Self::new(x, y) + pub fn cast(&self) -> PhysicalPosition { + PhysicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } } } -impl From<(i32, i32)> for PhysicalPosition { - #[inline] - fn from((x, y): (i32, i32)) -> Self { - Self::new(x as f64, y as f64) +impl From<(X, X)> for PhysicalPosition

{ + fn from((x, y): (X, X)) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) } } -impl Into<(f64, f64)> for PhysicalPosition { - #[inline] - fn into(self) -> (f64, f64) { - (self.x, self.y) +impl Into<(X, X)> for PhysicalPosition

{ + fn into(self: Self) -> (X, X) { + (self.x.cast(), self.y.cast()) } } -impl Into<(i32, i32)> for PhysicalPosition { - /// Note that this rounds instead of truncating. - #[inline] - fn into(self) -> (i32, i32) { - (self.x.round() as _, self.y.round() as _) +impl From<[X; 2]> for PhysicalPosition

{ + fn from([x, y]: [X; 2]) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) + } +} + +impl Into<[X; 2]> for PhysicalPosition

{ + fn into(self: Self) -> [X; 2] { + [self.x.cast(), self.y.cast()] } } /// A size represented in logical pixels. -/// -/// The size is stored as floats, so please be careful. Casting floats to integers truncates the fractional part, -/// which can cause noticable issues. To help with that, an `Into<(u32, u32)>` implementation is provided which -/// does the rounding for you. #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct LogicalSize { - pub width: f64, - pub height: f64, +pub struct LogicalSize

{ + pub width: P, + pub height: P, } -impl LogicalSize { +impl

LogicalSize

{ #[inline] - pub fn new(width: f64, height: f64) -> Self { + pub const fn new(width: P, height: P) -> Self { LogicalSize { width, height } } +} +impl LogicalSize

{ #[inline] - pub fn from_physical>(physical: T, dpi_factor: f64) -> Self { - physical.into().to_logical(dpi_factor) + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) } #[inline] - pub fn to_physical(&self, dpi_factor: f64) -> PhysicalSize { - assert!(validate_hidpi_factor(dpi_factor)); - let width = self.width * dpi_factor; - let height = self.height * dpi_factor; - PhysicalSize::new(width, height) + pub fn to_physical(&self, scale_factor: f64) -> PhysicalSize { + assert!(validate_scale_factor(scale_factor)); + let width = self.width.into() * scale_factor; + let height = self.height.into() * scale_factor; + PhysicalSize::new(width, height).cast() } -} -impl From<(f64, f64)> for LogicalSize { #[inline] - fn from((width, height): (f64, f64)) -> Self { - Self::new(width, height) + pub fn cast(&self) -> LogicalSize { + LogicalSize { + width: self.width.cast(), + height: self.height.cast(), + } } } -impl From<(u32, u32)> for LogicalSize { - #[inline] - fn from((width, height): (u32, u32)) -> Self { - Self::new(width as f64, height as f64) +impl From<(X, X)> for LogicalSize

{ + fn from((x, y): (X, X)) -> LogicalSize

{ + LogicalSize::new(x.cast(), y.cast()) } } -impl Into<(f64, f64)> for LogicalSize { - #[inline] - fn into(self) -> (f64, f64) { - (self.width, self.height) +impl Into<(X, X)> for LogicalSize

{ + fn into(self: LogicalSize

) -> (X, X) { + (self.width.cast(), self.height.cast()) } } -impl Into<(u32, u32)> for LogicalSize { - /// Note that this rounds instead of truncating. - #[inline] - fn into(self) -> (u32, u32) { - (self.width.round() as _, self.height.round() as _) +impl From<[X; 2]> for LogicalSize

{ + fn from([x, y]: [X; 2]) -> LogicalSize

{ + LogicalSize::new(x.cast(), y.cast()) + } +} + +impl Into<[X; 2]> for LogicalSize

{ + fn into(self: Self) -> [X; 2] { + [self.width.cast(), self.height.cast()] } } /// A size represented in physical pixels. -/// -/// The size is stored as floats, so please be careful. Casting floats to integers truncates the fractional part, -/// which can cause noticable issues. To help with that, an `Into<(u32, u32)>` implementation is provided which -/// does the rounding for you. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct PhysicalSize { - pub width: f64, - pub height: f64, +pub struct PhysicalSize

{ + pub width: P, + pub height: P, } -impl PhysicalSize { +impl

PhysicalSize

{ #[inline] - pub fn new(width: f64, height: f64) -> Self { + pub const fn new(width: P, height: P) -> Self { PhysicalSize { width, height } } +} +impl PhysicalSize

{ #[inline] - pub fn from_logical>(logical: T, dpi_factor: f64) -> Self { - logical.into().to_physical(dpi_factor) + pub fn from_logical>, X: Pixel>(logical: T, scale_factor: f64) -> Self { + logical.into().to_physical(scale_factor) } #[inline] - pub fn to_logical(&self, dpi_factor: f64) -> LogicalSize { - assert!(validate_hidpi_factor(dpi_factor)); - let width = self.width / dpi_factor; - let height = self.height / dpi_factor; - LogicalSize::new(width, height) + pub fn to_logical(&self, scale_factor: f64) -> LogicalSize { + assert!(validate_scale_factor(scale_factor)); + let width = self.width.into() / scale_factor; + let height = self.height.into() / scale_factor; + LogicalSize::new(width, height).cast() + } + + #[inline] + pub fn cast(&self) -> PhysicalSize { + PhysicalSize { + width: self.width.cast(), + height: self.height.cast(), + } + } +} + +impl From<(X, X)> for PhysicalSize

{ + fn from((x, y): (X, X)) -> PhysicalSize

{ + PhysicalSize::new(x.cast(), y.cast()) + } +} + +impl Into<(X, X)> for PhysicalSize

{ + fn into(self: Self) -> (X, X) { + (self.width.cast(), self.height.cast()) + } +} + +impl From<[X; 2]> for PhysicalSize

{ + fn from([x, y]: [X; 2]) -> PhysicalSize

{ + PhysicalSize::new(x.cast(), y.cast()) + } +} + +impl Into<[X; 2]> for PhysicalSize

{ + fn into(self: Self) -> [X; 2] { + [self.width.cast(), self.height.cast()] } } -impl From<(f64, f64)> for PhysicalSize { +/// A size that's either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Size { + Physical(PhysicalSize), + Logical(LogicalSize), +} + +impl Size { + pub fn new>(size: S) -> Size { + size.into() + } + + pub fn to_logical(&self, scale_factor: f64) -> LogicalSize

{ + match *self { + Size::Physical(size) => size.to_logical(scale_factor), + Size::Logical(size) => size.cast(), + } + } + + pub fn to_physical(&self, scale_factor: f64) -> PhysicalSize

{ + match *self { + Size::Physical(size) => size.cast(), + Size::Logical(size) => size.to_physical(scale_factor), + } + } +} + +impl From> for Size { #[inline] - fn from((width, height): (f64, f64)) -> Self { - Self::new(width, height) + fn from(size: PhysicalSize

) -> Size { + Size::Physical(size.cast()) } } -impl From<(u32, u32)> for PhysicalSize { +impl From> for Size { #[inline] - fn from((width, height): (u32, u32)) -> Self { - Self::new(width as f64, height as f64) + fn from(size: LogicalSize

) -> Size { + Size::Logical(size.cast()) + } +} + +/// A position that's either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Position { + Physical(PhysicalPosition), + Logical(LogicalPosition), +} + +impl Position { + pub fn new>(position: S) -> Position { + position.into() + } + + pub fn to_logical(&self, scale_factor: f64) -> LogicalPosition

{ + match *self { + Position::Physical(position) => position.to_logical(scale_factor), + Position::Logical(position) => position.cast(), + } + } + + pub fn to_physical(&self, scale_factor: f64) -> PhysicalPosition

{ + match *self { + Position::Physical(position) => position.cast(), + Position::Logical(position) => position.to_physical(scale_factor), + } } } -impl Into<(f64, f64)> for PhysicalSize { +impl From> for Position { #[inline] - fn into(self) -> (f64, f64) { - (self.width, self.height) + fn from(position: PhysicalPosition

) -> Position { + Position::Physical(position.cast()) } } -impl Into<(u32, u32)> for PhysicalSize { - /// Note that this rounds instead of truncating. +impl From> for Position { #[inline] - fn into(self) -> (u32, u32) { - (self.width.round() as _, self.height.round() as _) + fn from(position: LogicalPosition

) -> Position { + Position::Logical(position.cast()) } } diff --git a/src/event.rs b/src/event.rs index ff94aa84e4..a6357d4af4 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,62 +3,189 @@ //! These are sent to the closure given to [`EventLoop::run(...)`][event_loop_run], where they get //! processed and used to modify the program state. For more details, see the root-level documentation. //! +//! Some of these events represent different "parts" of a traditional event-handling loop. You could +//! approximate the basic ordering loop of [`EventLoop::run(...)`][event_loop_run] like this: +//! +//! ```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); +//! +//! for e in (window events, user events, device events) { +//! event_handler(e, ..., &mut control_flow); +//! } +//! event_handler(MainEventsCleared, ..., &mut control_flow); +//! +//! for w in (redraw windows) { +//! event_handler(RedrawRequested(w), ..., &mut control_flow); +//! } +//! event_handler(RedrawEventsCleared, ..., &mut control_flow); +//! +//! start_cause = wait_if_necessary(control_flow); +//! } +//! +//! event_handler(LoopDestroyed, ..., &mut control_flow); +//! ``` +//! +//! This leaves out timing details like `ControlFlow::WaitUntil` but hopefully +//! describes what happens in what order. +//! //! [event_loop_run]: crate::event_loop::EventLoop::run use instant::Instant; use std::path::PathBuf; use crate::{ - dpi::{LogicalPosition, LogicalSize}, - platform_impl, - window::WindowId, + dpi::{PhysicalPosition, PhysicalSize}, + window::{Theme, WindowId}, }; +pub mod device; + /// Describes a generic event. -#[derive(Clone, Debug, PartialEq)] -pub enum 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> { + /// 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 + /// [`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, - }, - /// Emitted when the OS sends an event to a device. - DeviceEvent { - device_id: DeviceId, - event: DeviceEvent, + event: WindowEvent<'a>, }, + + MouseEvent(device::MouseId, device::MouseEvent), + /// Emitted when a keyboard device has generated input. + KeyboardEvent(device::KeyboardId, device::KeyboardEvent), + HidEvent(device::HidId, device::HidEvent), + /// Emitted when a gamepad/joystick device has generated input. + GamepadEvent(device::GamepadHandle, device::GamepadEvent), + /// Emitted when an event is sent from [`EventLoopProxy::send_event`](crate::event_loop::EventLoopProxy::send_event) UserEvent(T), - /// Emitted when new events arrive from the OS to be processed. - NewEvents(StartCause), - /// Emitted when all of the event loop's events have been processed and control flow is about - /// to be taken away from the program. - EventsCleared, - - /// Emitted when the event loop is being shut down. This is irreversable - if this event is - /// emitted, it is guaranteed to be the last event emitted. - LoopDestroyed, /// Emitted when the application has been suspended. Suspended, /// Emitted when the application has been resumed. Resumed, + + /// Emitted when all of the event loop's input events have been processed and redraw processing + /// is about to begin. + /// + /// 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, + + /// Emitted after `MainEventsCleared` when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via + /// [`Window::request_redraw`](crate::window::Window::request_redraw). + /// + /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests + /// into a single event, to help avoid duplicating rendering work. + /// + /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless + /// something changes, like most non-game GUIs. + 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`. + /// + /// This event is useful for doing any cleanup or bookkeeping work after all the rendering + /// tasks have been completed. + RedrawEventsCleared, + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversable - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as an "do on quit" event. + LoopDestroyed, +} + +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()), + MouseEvent(id, event) => MouseEvent(id.clone(), event.clone()), + KeyboardEvent(id, event) => KeyboardEvent(id.clone(), event.clone()), + HidEvent(id, event) => HidEvent(id.clone(), event.clone()), + GamepadEvent(id, event) => GamepadEvent(id.clone(), event.clone()), + NewEvents(cause) => NewEvents(cause.clone()), + MainEventsCleared => MainEventsCleared, + RedrawRequested(wid) => RedrawRequested(*wid), + RedrawEventsCleared => RedrawEventsCleared, + LoopDestroyed => LoopDestroyed, + Suspended => Suspended, + Resumed => Resumed, + } + } } -impl Event { - pub fn map_nonuser_event(self) -> Result, Event> { +impl<'a, T> Event<'a, T> { + pub fn map_nonuser_event(self) -> Result, Event<'a, T>> { 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 }), + MouseEvent(id, event) => Ok(MouseEvent(id, event)), + KeyboardEvent(id, event) => Ok(KeyboardEvent(id, event)), + HidEvent(id, event) => Ok(HidEvent(id, event)), + GamepadEvent(id, event) => Ok(GamepadEvent(id, event)), NewEvents(cause) => Ok(NewEvents(cause)), - EventsCleared => Ok(EventsCleared), + MainEventsCleared => Ok(MainEventsCleared), + RedrawRequested(wid) => Ok(RedrawRequested(wid)), + RedrawEventsCleared => Ok(RedrawEventsCleared), LoopDestroyed => Ok(LoopDestroyed), 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)), + MouseEvent(id, event) => Some(MouseEvent(id, event)), + KeyboardEvent(id, event) => Some(KeyboardEvent(id, event)), + HidEvent(id, event) => Some(HidEvent(id, event)), + GamepadEvent(id, event) => Some(GamepadEvent(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), + } + } } /// Describes the reason the event loop is resuming. @@ -88,13 +215,13 @@ pub enum StartCause { } /// Describes an event from a `Window`. -#[derive(Clone, Debug, PartialEq)] -pub enum WindowEvent { +#[derive(Debug, PartialEq)] +pub enum WindowEvent<'a> { /// The size of the window has changed. Contains the client area's new dimensions. - Resized(LogicalSize), + Resized(PhysicalSize), /// The position of the window has changed. Contains the window's new position. - Moved(LogicalPosition), + Moved(PhysicalPosition), /// The window has been requested to close. CloseRequested, @@ -130,40 +257,55 @@ pub enum WindowEvent { /// An event from the keyboard has been received. KeyboardInput { - device_id: DeviceId, input: KeyboardInput, + /// If `true`, the event was generated synthetically by winit + /// in one of the following circumstances: + /// + /// * Synthetic key press events are generated for all keys pressed + /// when a window gains focus. Likewise, synthetic key release events + /// are generated for all keys pressed when a window goes out of focus. + /// ***Currently, this is only functional on X11 and Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, }, + /// The keyboard modifiers have changed. + /// + /// Platform-specific behavior: + /// - **Web**: This API is currently unimplemented on the web. This isn't by design - it's an + /// issue, and it should get fixed - but it's the current state of the API. + ModifiersChanged(ModifiersState), + /// The cursor has moved on the window. CursorMoved { - device_id: DeviceId, - /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range of this data is /// limited by the display area and it may have been transformed by the OS to implement effects such as cursor /// acceleration, it should not be used to implement non-cursor-like interactions such as 3D camera control. - position: LogicalPosition, + position: PhysicalPosition, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] modifiers: ModifiersState, }, /// The cursor has entered the window. - CursorEntered { device_id: DeviceId }, + CursorEntered, /// The cursor has left the window. - CursorLeft { device_id: DeviceId }, + CursorLeft, /// A mouse wheel movement or touchpad scroll occurred. MouseWheel { - device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] modifiers: ModifiersState, }, /// An mouse button press has been received. MouseInput { - device_id: DeviceId, state: ElementState, button: MouseButton, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] modifiers: ModifiersState, }, @@ -172,108 +314,160 @@ pub enum WindowEvent { /// At the moment, only supported on Apple forcetouch-capable macbooks. /// The parameters are: pressure level (value between 0 and 1 representing how hard the touchpad /// is being pressed) and stage (integer representing the click level). - TouchpadPressure { - device_id: DeviceId, - pressure: f32, - stage: i64, - }, - - /// Motion on some analog axis. May report data redundant to other, more specific events. - AxisMotion { - device_id: DeviceId, - axis: AxisId, - value: f64, - }, - - /// The OS or application has requested that the window be redrawn. - RedrawRequested, + TouchpadPressure { pressure: f32, stage: i64 }, /// Touch event has been received Touch(Touch), - /// The DPI factor of the window has changed. + /// The window's scale factor has changed. /// /// The following user actions can cause DPI changes: /// /// * Changing the display's resolution. - /// * Changing the display's DPI factor (e.g. in Control Panel on Windows). - /// * Moving the window to a display with a different DPI factor. + /// * Changing the display's scale factor (e.g. in Control Panel on Windows). + /// * Moving the window to a display with a different scale factor. + /// + /// After this event callback has been processed, the window will be resized to whatever value + /// is pointed to by the `new_inner_size` reference. By default, this will contain the size suggested + /// by the OS, but it can be changed to any value. /// /// For more information about DPI in general, see the [`dpi`](crate::dpi) module. - HiDpiFactorChanged(f64), -} + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: &'a mut PhysicalSize, + }, -/// Identifier of an input device. -/// -/// Whenever you receive an event arising from a particular input device, this event contains a `DeviceId` which -/// identifies its origin. Note that devices may be virtual (representing an on-screen cursor and keyboard focus) or -/// physical. Virtual devices typically aggregate inputs from multiple physical devices. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(pub(crate) platform_impl::DeviceId); - -impl DeviceId { - /// Returns a dummy `DeviceId`, useful for unit testing. The only guarantee made about the return - /// value of this function is that it will always be equal to itself and to future values returned - /// by this function. No other guarantees are made. This may be equal to a real `DeviceId`. + /// The system window theme has changed. /// - /// **Passing this into a winit function will result in undefined behavior.** - pub unsafe fn dummy() -> Self { - DeviceId(platform_impl::DeviceId::dummy()) - } -} - -/// Represents raw hardware events that are not associated with any particular window. -/// -/// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera or first-person -/// game controls. Many physical actions, such as mouse movement, can produce both device and window events. Because -/// window events typically arise from virtual devices (corresponding to GUI cursors and keyboard focus) the device IDs -/// may not match. -/// -/// Note that these events are delivered regardless of input focus. -#[derive(Clone, Debug, PartialEq)] -pub enum DeviceEvent { - Added, - Removed, - - /// Change in physical position of a pointing device. + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. /// - /// This represents raw, unfiltered physical motion. Not to be confused with `WindowEvent::CursorMoved`. - MouseMotion { - /// (x, y) change in position in unspecified units. - /// - /// Different devices may use different units. - delta: (f64, f64), - }, - - /// Physical scroll event - MouseWheel { - delta: MouseScrollDelta, - }, - - /// Motion on some analog axis. This event will be reported for all arbitrary input devices - /// that winit supports on this platform, including mouse devices. If the device is a mouse - /// device then this will be reported alongside the MouseMotion event. - Motion { - axis: AxisId, - value: f64, - }, - - Button { - button: ButtonId, - state: ElementState, - }, - - Key(KeyboardInput), + /// At the moment this is only supported on Windows. + ThemeChanged(Theme), +} - /// Keyboard modifiers have changed - #[doc(hidden)] - ModifiersChanged { - modifiers: ModifiersState, - }, +impl Clone for WindowEvent<'static> { + fn clone(&self) -> Self { + use self::WindowEvent::*; + return match self { + Resized(size) => Resized(size.clone()), + Moved(pos) => Moved(pos.clone()), + CloseRequested => CloseRequested, + Destroyed => Destroyed, + DroppedFile(file) => DroppedFile(file.clone()), + HoveredFile(file) => HoveredFile(file.clone()), + HoveredFileCancelled => HoveredFileCancelled, + ReceivedCharacter(c) => ReceivedCharacter(*c), + Focused(f) => Focused(*f), + KeyboardInput { + input, + is_synthetic, + } => KeyboardInput { + input: *input, + is_synthetic: *is_synthetic, + }, + + ModifiersChanged(modifiers) => ModifiersChanged(modifiers.clone()), + #[allow(deprecated)] + CursorMoved { + position, + modifiers, + } => CursorMoved { + position: *position, + modifiers: *modifiers, + }, + CursorEntered => CursorEntered, + CursorLeft => CursorLeft, + #[allow(deprecated)] + MouseWheel { + delta, + phase, + modifiers, + } => MouseWheel { + delta: *delta, + phase: *phase, + modifiers: *modifiers, + }, + #[allow(deprecated)] + MouseInput { + state, + button, + modifiers, + } => MouseInput { + state: *state, + button: *button, + modifiers: *modifiers, + }, + TouchpadPressure { pressure, stage } => TouchpadPressure { + pressure: *pressure, + stage: *stage, + }, + Touch(touch) => Touch(*touch), + ThemeChanged(theme) => ThemeChanged(theme.clone()), + ScaleFactorChanged { .. } => { + unreachable!("Static event can't be about scale factor changing") + } + }; + } +} - Text { - codepoint: char, - }, +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), + ReceivedCharacter(c) => Some(ReceivedCharacter(c)), + Focused(focused) => Some(Focused(focused)), + KeyboardInput { + input, + is_synthetic, + } => Some(KeyboardInput { + input, + is_synthetic, + }), + ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)), + #[allow(deprecated)] + CursorMoved { + position, + modifiers, + } => Some(CursorMoved { + position, + modifiers, + }), + CursorEntered => Some(CursorEntered), + CursorLeft => Some(CursorLeft), + #[allow(deprecated)] + MouseWheel { + delta, + phase, + modifiers, + } => Some(MouseWheel { + delta, + phase, + modifiers, + }), + #[allow(deprecated)] + MouseInput { + state, + button, + modifiers, + } => Some(MouseInput { + state, + button, + modifiers, + }), + TouchpadPressure { pressure, stage } => Some(TouchpadPressure { pressure, stage }), + Touch(touch) => Some(Touch(touch)), + ThemeChanged(theme) => Some(ThemeChanged(theme)), + ScaleFactorChanged { .. } => None, + } + } } /// Describes a keyboard input event. @@ -299,6 +493,7 @@ pub struct KeyboardInput { /// /// This is tracked internally to avoid tracking errors arising from modifier key state changes when events from /// this device are not being delivered to the application, e.g. due to keyboard focus being elsewhere. + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] pub modifiers: ModifiersState, } @@ -330,9 +525,8 @@ pub enum TouchPhase { /// device against their face. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Touch { - pub device_id: DeviceId, pub phase: TouchPhase, - pub location: LogicalPosition, + pub location: PhysicalPosition, /// Describes how hard the screen was pressed. May be `None` if the platform /// does not support pressure sensitivity. /// @@ -410,7 +604,7 @@ pub type AxisId = u32; pub type ButtonId = u32; /// Describes the input state of a key. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ElementState { Pressed, @@ -424,7 +618,7 @@ pub enum MouseButton { Left, Right, Middle, - Other(u8), + Other(u16), } /// Describes a difference in the mouse scroll wheel state. @@ -443,7 +637,7 @@ pub enum MouseScrollDelta { /// Scroll events are expressed as a PixelDelta if /// supported by the device (eg. a touchpad) and /// platform. - PixelDelta(LogicalPosition), + PixelDelta(PhysicalPosition), } /// Symbolic name for a keyboard key. @@ -571,12 +765,20 @@ pub enum VirtualKeyCode { Numpad7, Numpad8, Numpad9, + NumpadAdd, + NumpadDivide, + NumpadDecimal, + NumpadComma, + NumpadEnter, + NumpadEquals, + NumpadMultiply, + NumpadSubtract, AbntC1, AbntC2, - Add, Apostrophe, Apps, + Asterisk, At, Ax, Backslash, @@ -585,8 +787,6 @@ pub enum VirtualKeyCode { Colon, Comma, Convert, - Decimal, - Divide, Equals, Grave, Kana, @@ -600,19 +800,18 @@ pub enum VirtualKeyCode { MediaSelect, MediaStop, Minus, - Multiply, Mute, MyComputer, - NavigateForward, // also called "Prior" - NavigateBackward, // also called "Next" + // also called "Next" + NavigateForward, + // also called "Prior" + NavigateBackward, NextTrack, NoConvert, - NumpadComma, - NumpadEnter, - NumpadEquals, OEM102, Period, PlayPause, + Plus, Power, PrevTrack, RAlt, @@ -624,7 +823,6 @@ pub enum VirtualKeyCode { Slash, Sleep, Stop, - Subtract, Sysrq, Tab, Underline, @@ -645,21 +843,99 @@ pub enum VirtualKeyCode { Cut, } -/// Represents the current state of the keyboard modifiers -/// -/// Each field of this struct represents a modifier and is `true` if this modifier is active. -#[derive(Default, Debug, Hash, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(default))] -pub struct ModifiersState { - /// The "shift" key - pub shift: bool, - /// The "control" key - pub ctrl: bool, - /// The "alt" key - pub alt: bool, - /// The "logo" key +impl ModifiersState { + /// Returns `true` if the shift key is pressed. + pub fn shift(&self) -> bool { + self.intersects(Self::SHIFT) + } + /// Returns `true` if the control key is pressed. + pub fn ctrl(&self) -> bool { + self.intersects(Self::CTRL) + } + /// Returns `true` if the alt key is pressed. + pub fn alt(&self) -> bool { + self.intersects(Self::ALT) + } + /// Returns `true` if the logo key is pressed. + pub fn logo(&self) -> bool { + self.intersects(Self::LOGO) + } +} + +bitflags! { + /// Represents the current state of the keyboard modifiers /// - /// This is the "windows" key on PC and "command" key on Mac. - pub logo: bool, + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Default)] + pub struct ModifiersState: u32 { + // left and right modifiers are currently commented out, but we should be able to support + // them in a future release + /// The "shift" key. + const SHIFT = 0b100 << 0; + // const LSHIFT = 0b010 << 0; + // const RSHIFT = 0b001 << 0; + /// The "control" key. + const CTRL = 0b100 << 3; + // const LCTRL = 0b010 << 3; + // const RCTRL = 0b001 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + // const LALT = 0b010 << 6; + // const RALT = 0b001 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const LOGO = 0b100 << 9; + // const LLOGO = 0b010 << 9; + // const RLOGO = 0b001 << 9; + } +} + +#[cfg(feature = "serde")] +mod modifiers_serde { + use super::ModifiersState; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Default, Serialize, Deserialize)] + #[serde(default)] + #[serde(rename = "ModifiersState")] + pub struct ModifiersStateSerialize { + pub shift: bool, + pub ctrl: bool, + pub alt: bool, + pub logo: bool, + } + + impl Serialize for ModifiersState { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = ModifiersStateSerialize { + shift: self.shift(), + ctrl: self.ctrl(), + alt: self.alt(), + logo: self.logo(), + }; + s.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for ModifiersState { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ModifiersStateSerialize { + shift, + ctrl, + alt, + logo, + } = ModifiersStateSerialize::deserialize(deserializer)?; + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, shift); + m.set(ModifiersState::CTRL, ctrl); + m.set(ModifiersState::ALT, alt); + m.set(ModifiersState::LOGO, logo); + Ok(m) + } + } } diff --git a/src/event/device.rs b/src/event/device.rs new file mode 100644 index 0000000000..3fd7c08393 --- /dev/null +++ b/src/event/device.rs @@ -0,0 +1,363 @@ +//! Raw hardware events that are not associated with any particular window. +//! +//! Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera or first-person +//! game controls. Many physical actions, such as mouse movement, can produce both device and window events. Because +//! window events typically arise from virtual devices (corresponding to GUI cursors and keyboard focus) the device IDs +//! may not match. +//! +//! All attached devices are guaranteed to emit an `Added` event upon the initialization of the event loop. +//! +//! Note that device events are always delivered regardless of window focus. + +use crate::{ + dpi::PhysicalPosition, + event::{AxisId, ButtonId, ElementState, KeyboardInput, ModifiersState, MouseButton}, + event_loop::EventLoop, + platform_impl, +}; +use std::{fmt, io}; + +/// A hint suggesting the type of button that was pressed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GamepadButton { + Start, + Select, + + /// The north face button. + /// + /// * Nintendo: X + /// * Playstation: Triangle + /// * XBox: Y + North, + /// The south face button. + /// + /// * Nintendo: B + /// * Playstation: X + /// * XBox: A + South, + /// The east face button. + /// + /// * Nintendo: A + /// * Playstation: Circle + /// * XBox: B + East, + /// The west face button. + /// + /// * Nintendo: Y + /// * Playstation: Square + /// * XBox: X + West, + + LeftStick, + RightStick, + + LeftTrigger, + RightTrigger, + + LeftShoulder, + RightShoulder, + + DPadUp, + DPadDown, + DPadLeft, + DPadRight, +} + +/// A hint suggesting the type of axis that moved. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GamepadAxis { + LeftStickX, + LeftStickY, + + RightStickX, + RightStickY, + + LeftTrigger, + RightTrigger, +} + +/// A given joystick's side on the gamepad. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Side { + Left, + Right, +} + +/// Raw mouse events. +/// +/// See the module-level docs for more information. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum MouseEvent { + /// A mouse device has been added. + Added, + /// A mouse device has been removed. + Removed, + /// A mouse button has been pressed or released. + Button { + state: ElementState, + button: MouseButton, + }, + /// Relative change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion, NOT the position of the mouse. Accordingly, + /// the values provided here are the change in position of the mouse since the previous + /// `MovedRelative` event. + MovedRelative(f64, f64), + /// Change in absolute position of a pointing device. + /// + /// The `PhysicalPosition` value is the new position of the cursor relative to the desktop. This + /// generally doesn't get output by standard mouse devices, but can get output from tablet devices. + MovedAbsolute(PhysicalPosition), + /// Change in rotation of mouse wheel. + Wheel(f64, f64), +} + +/// Raw keyboard events. +/// +/// See the module-level docs for more information. +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum KeyboardEvent { + /// A keyboard device has been added. + Added, + /// A keyboard device has been removed. + Removed, + /// A key has been pressed or released. + Input(KeyboardInput), + ModifiersChanged(ModifiersState), +} + +/// Raw HID event. +/// +/// See the module-level docs for more information. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum HidEvent { + /// A Human Interface Device device has been added. + Added, + /// A Human Interface Device device has been removed. + Removed, + /// A raw data packet has been received from the Human Interface Device. + Data(Box<[u8]>), +} + +/// Gamepad/joystick events. +/// +/// These can be generated by any of a variety of game controllers, including (but not limited to) +/// gamepads, joysicks, and HOTAS devices. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub enum GamepadEvent { + /// A gamepad/joystick device has been added. + Added, + /// A gamepad/joystick device has been removed. + Removed, + /// An analog axis value on the gamepad/joystick has changed. + /// + /// Winit does NOT provide [deadzone filtering](https://www.quora.com/What-does-the-term-joystick-deadzone-mean), + /// and such filtering may have to be provided by API users for joystick axes. + Axis { + axis_id: AxisId, + /// A hint regarding the physical axis that moved. + /// + /// On traditional gamepads (such as an X360 controller) this can be assumed to have a + /// non-`None` value; however, other joystick devices with more varied layouts generally won't + /// provide a value here. + /// + /// TODO: DISCUSS CONTROLLER MAPPING ONCE WE FIGURE OUT WHAT WE'RE DOING THERE. + axis: Option, + value: f64, + /// Whether or not this axis has also produced a [`GamepadEvent::Stick`] event. + stick: bool, + }, + /// A two-axis joystick's value has changed. + /// + /// This is mainly provided to assist with deadzone calculation, as proper deadzones should be + /// calculated via the combined distance of each joystick axis from the center of the joystick, + /// rather than per-axis. + /// + /// Note that this is only guaranteed to be emitted for traditionally laid out gamepads. More + /// complex joysticks generally don't report specifics of their layout to the operating system, + /// preventing Winit from automatically aggregating their axis input into two-axis stick events. + Stick { + /// The X axis' ID. + x_id: AxisId, + /// The Y axis' ID. + y_id: AxisId, + x_value: f64, + y_value: f64, + /// Which joystick side produced this event. + side: Side, + }, + Button { + button_id: ButtonId, + /// A hint regarding the location of the button. + /// + /// The caveats on the `Axis.hint` field also apply here. + button: Option, + state: ElementState, + }, +} + +/// Error reported if a rumble attempt unexpectedly failed. +#[derive(Debug)] +pub enum RumbleError { + /// The device is no longer connected. + DeviceNotConnected, + /// An unknown OS error has occured. + OsError(io::Error), +} + +/// A typed identifier for a mouse device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MouseId(pub(crate) platform_impl::MouseId); +/// A typed identifier for a keyboard device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct KeyboardId(pub(crate) platform_impl::KeyboardId); +/// A typed if for a Human Interface Device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HidId(pub(crate) platform_impl::HidId); +/// A handle to a gamepad/joystick device. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GamepadHandle(pub(crate) platform_impl::GamepadHandle); + +impl MouseId { + /// Returns a dummy `MouseId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `MouseId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + MouseId(platform_impl::MouseId::dummy()) + } + + /// Enumerate all attached mouse devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::MouseId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this mouse device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl KeyboardId { + /// Returns a dummy `KeyboardId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `KeyboardId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + KeyboardId(platform_impl::KeyboardId::dummy()) + } + + /// Enumerate all attached keyboard devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::KeyboardId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this keyboard device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl HidId { + /// Returns a dummy `HidId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `HidId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + HidId(platform_impl::HidId::dummy()) + } + + /// Enumerate all attached keyboard devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::HidId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this keyboard device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl GamepadHandle { + /// Returns a dummy `GamepadHandle`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `GamepadHandle`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + GamepadHandle(platform_impl::GamepadHandle::dummy()) + } + + /// Enumerate all attached gamepad/joystick devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::GamepadHandle::enumerate(&event_loop.event_loop) + } + + /// Check to see if this gamepad/joystick device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } + + /// Attempts to set the rumble values for an attached controller. Input values are automatically + /// bound to a [`0.0`, `1.0`] range. + /// + /// Certain gamepads assign different usages to the left and right motors - for example, X360 + /// controllers treat the left motor as a low-frequency rumble and the right motor as a + /// high-frequency rumble. However, this cannot necessarily be assumed for all gamepad devices. + /// + /// Note that, if the given gamepad does not support rumble, no error value gets thrown. + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + self.0.rumble(left_speed, right_speed) + } + + /// Gets the port number assigned to the gamepad. + pub fn port(&self) -> Option { + self.0.port() + } + + /// Gets the controller's battery level. + /// + /// If the controller doesn't report a battery level, this returns `None`. + pub fn battery_level(&self) -> Option { + self.0.battery_level() + } +} + +/// TODO: IS THIS THE RIGHT ABSTRACTION FOR ALL PLATFORMS? +/// This is exposed in its current form because it's what Microsoft does for XInput, and that's my +/// (@Osspial's) main point of reference. If you're implementing this on a different platform and +/// that platform exposes battery level differently, please bring it up in the tracking issue! +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum BatteryLevel { + Empty, + Low, + Medium, + Full, +} + +impl fmt::Debug for MouseId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for KeyboardId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for HidId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for GamepadHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} diff --git a/src/event_loop.rs b/src/event_loop.rs index fd9e034ff6..edc5670ac9 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -59,7 +59,7 @@ 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::EventsCleared`][events_cleared] +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`][events_cleared] /// is emitted. Defaults to `Poll`. /// /// ## Persistency @@ -68,11 +68,17 @@ impl fmt::Debug for EventLoopWindowTarget { /// are **not** persistent between multiple calls to `run_return` - issuing a new call will reset /// the control flow to `Poll`. /// -/// [events_cleared]: crate::event::Event::EventsCleared +/// [events_cleared]: crate::event::Event::RedrawEventsCleared #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ControlFlow { /// When the current loop iteration finishes, immediately begin a new iteration regardless of /// whether or not new events are available to process. + /// + /// ## Platform-specific + /// - **Web:** Events are queued and usually sent when `requestAnimationFrame` fires but sometimes + /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for + /// example when the scaling of the page has changed. This should be treated as an implementation + /// detail which should not be relied on. Poll, /// When the current loop iteration finishes, suspend the thread until another event arrives. Wait, @@ -143,7 +149,7 @@ impl EventLoop { #[inline] pub fn run(self, event_handler: F) -> ! where - F: 'static + FnMut(Event, &EventLoopWindowTarget, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), { self.event_loop.run(event_handler) } @@ -154,29 +160,35 @@ impl EventLoop { event_loop_proxy: self.event_loop.create_proxy(), } } +} + +impl Deref for EventLoop { + type Target = EventLoopWindowTarget; + fn deref(&self) -> &EventLoopWindowTarget { + self.event_loop.window_target() + } +} +impl EventLoopWindowTarget { /// Returns the list of all the monitors available on the system. #[inline] pub fn available_monitors(&self) -> impl Iterator { - self.event_loop + self.p .available_monitors() .into_iter() .map(|inner| MonitorHandle { inner }) } /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// ## Platform-specific + /// + /// **Wayland:** Always returns `None`. #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle { - inner: self.event_loop.primary_monitor(), - } - } -} - -impl Deref for EventLoop { - type Target = EventLoopWindowTarget; - fn deref(&self) -> &EventLoopWindowTarget { - self.event_loop.window_target() + pub fn primary_monitor(&self) -> Option { + self.p.primary_monitor() } } @@ -199,7 +211,7 @@ impl EventLoopProxy { /// function. /// /// Returns an `Err` if the associated `EventLoop` no longer exists. - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { self.event_loop_proxy.send_event(event) } } @@ -211,18 +223,14 @@ impl fmt::Debug for EventLoopProxy { } /// The error that is returned when an `EventLoopProxy` attempts to wake up an `EventLoop` that -/// no longer exists. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct EventLoopClosed; +/// no longer exists. Contains the original event given to `send_event`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct EventLoopClosed(pub T); -impl fmt::Display for EventLoopClosed { +impl fmt::Display for EventLoopClosed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", error::Error::description(self)) + f.write_str("Tried to wake up a closed `EventLoop`") } } -impl error::Error for EventLoopClosed { - fn description(&self) -> &str { - "Tried to wake up a closed `EventLoop`" - } -} +impl error::Error for EventLoopClosed {} diff --git a/src/icon.rs b/src/icon.rs index bbbb53f984..418ea03afa 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,4 +1,5 @@ -use std::{error::Error, fmt, mem}; +use crate::platform_impl::PlatformIcon; +use std::{error::Error, fmt, io, mem}; #[repr(C)] #[derive(Debug)] @@ -11,7 +12,7 @@ pub(crate) struct Pixel { pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug)] /// An error produced when using `Icon::from_rgba` with invalid arguments. pub enum BadIcon { /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be @@ -25,72 +26,110 @@ pub enum BadIcon { width_x_height: usize, pixel_count: usize, }, + /// Produced when underlying OS functionality failed to create the icon + OsError(io::Error), } impl fmt::Display for BadIcon { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let msg = match self { - &BadIcon::ByteCountNotDivisibleBy4 { byte_count } => format!( + match self { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!(f, "The length of the `rgba` argument ({:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.", byte_count, ), - &BadIcon::DimensionsVsPixelCount { + BadIcon::DimensionsVsPixelCount { width, height, width_x_height, pixel_count, - } => format!( + } => write!(f, "The specified dimensions ({:?}x{:?}) don't match the number of pixels supplied by the `rgba` argument ({:?}). For those dimensions, the expected pixel count is {:?}.", width, height, pixel_count, width_x_height, ), - }; - write!(f, "{}", msg) + BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {:?}", e), + } } } impl Error for BadIcon { - fn description(&self) -> &str { - "A valid icon cannot be created from these arguments" - } - - fn cause(&self) -> Option<&dyn Error> { + fn source(&self) -> Option<&(dyn Error + 'static)> { Some(self) } } #[derive(Debug, Clone, PartialEq, Eq)] -/// An icon used for the window titlebar, taskbar, etc. -pub struct Icon { +pub(crate) struct RgbaIcon { pub(crate) rgba: Vec, pub(crate) width: u32, pub(crate) height: u32, } +/// For platforms which don't have window icons (e.g. web) +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoIcon; + +#[allow(dead_code)] // These are not used on every platform +mod constructors { + use super::*; + + impl RgbaIcon { + /// Creates an `Icon` from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + pixel_count, + }) + } else { + Ok(RgbaIcon { + rgba, + width, + height, + }) + } + } + } + + impl NoIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + // Create the rgba icon anyway to validate the input + let _ = RgbaIcon::from_rgba(rgba, width, height)?; + Ok(NoIcon) + } + } +} + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Clone)] +pub struct Icon { + pub(crate) inner: PlatformIcon, +} + +impl fmt::Debug for Icon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.inner, formatter) + } +} + impl Icon { /// Creates an `Icon` from 32bpp RGBA data. /// /// The length of `rgba` must be divisible by 4, and `width * height` must equal /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { - if rgba.len() % PIXEL_SIZE != 0 { - return Err(BadIcon::ByteCountNotDivisibleBy4 { - byte_count: rgba.len(), - }); - } - let pixel_count = rgba.len() / PIXEL_SIZE; - if pixel_count != (width * height) as usize { - Err(BadIcon::DimensionsVsPixelCount { - width, - height, - width_x_height: (width * height) as usize, - pixel_count, - }) - } else { - Ok(Icon { - rgba, - width, - height, - }) - } + Ok(Icon { + inner: PlatformIcon::from_rgba(rgba, width, height)?, + }) } } diff --git a/src/lib.rs b/src/lib.rs index 92b35b410c..54d051e4a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -//! Winit allows you to build a window on as many platforms as possible. +//! Winit is a cross-platform window creation and event loop management library. //! -//! # Building a window +//! # Building windows //! //! Before you can build a [`Window`], you first need to build an [`EventLoop`]. This is done with the //! [`EventLoop::new()`] function. @@ -15,26 +15,31 @@ //! - 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 way is the simplest way and will give you default values for everything. -//! -//! The second way 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`]. +//! 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`]. //! //! # Event handling //! //! Once a [`Window`] has been created, it will generate different *events*. A [`Window`] object can -//! generate a [`WindowEvent`] when certain things happen, like whenever the user moves their mouse -//! or presses a key inside the [`Window`]. Devices can generate a [`DeviceEvent`] directly as well, -//! which contains 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. +//! generate [`WindowEvent`]s when certain input events occur, such as a cursor moving over the +//! 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. +//! +//! You can retrieve events by calling [`EventLoop::run`][event_loop_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`]`::`[`Exit`], at which point [`Event`]`::`[`LoopDestroyed`] is emitted and the +//! entire program terminates. //! -//! Events can be retreived by using an [`EventLoop`]. A [`Window`] will send its events to the -//! [`EventLoop`] object it was created with. +//! 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. //! -//! You do this by calling [`event_loop.run(...)`][event_loop_run]. This function will run forever -//! unless `control_flow` is set to [`ControlFlow`]`::`[`Exit`], at which point [`Event`]`::`[`LoopDestroyed`] -//! is emitted and the entire program terminates. //! //! ```no_run //! use winit::{ @@ -47,57 +52,70 @@ //! 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 = 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 = ControlFlow::Wait; +//! //! match event { -//! Event::EventsCleared => { +//! Event::WindowEvent { +//! event: WindowEvent::CloseRequested, +//! .. +//! } => { +//! println!("The close button was pressed; stopping"); +//! *control_flow = ControlFlow::Exit +//! }, +//! Event::MainEventsCleared => { //! // Application update code. //! //! // Queue a RedrawRequested event. +//! // +//! // 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. //! window.request_redraw(); //! }, -//! Event::WindowEvent { -//! event: WindowEvent::RedrawRequested, -//! .. -//! } => { +//! Event::RedrawRequested(_) => { //! // Redraw the application. //! // -//! // It's preferrable to render in this event rather than in EventsCleared, since -//! // rendering in here allows the program to gracefully handle redraws requested -//! // by the OS. +//! // It's preferable for applications that do not render continuously to render in +//! // this event rather than in MainEventsCleared, since rendering in here allows +//! // the program to gracefully handle redraws requested by the OS. //! }, -//! Event::WindowEvent { -//! event: WindowEvent::CloseRequested, -//! .. -//! } => { -//! println!("The close button was pressed; stopping"); -//! *control_flow = ControlFlow::Exit -//! }, -//! // 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 = 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 = ControlFlow::Wait, +//! _ => () //! } //! }); //! ``` //! -//! If you use multiple [`Window`]s, [`Event`]`::`[`WindowEvent`] has a member named `window_id`. You can -//! compare it with the value returned by the [`id()`][window_id_fn] method of [`Window`] in order to know which -//! [`Window`] has received the event. +//! [`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`] +//! dispatched the event. //! //! # Drawing on the window //! -//! Winit doesn't provide any function that allows drawing on a [`Window`]. However it allows you to -//! retrieve the raw handle of the window (see the [`platform`] module), which in turn allows you -//! to create an OpenGL/Vulkan/DirectX/Metal/etc. context that will draw on the [`Window`]. +//! Winit doesn't directly provide any methods for drawing on a [`Window`]. However it allows you to +//! retrieve the raw handle of the window (see the [`platform`] module and/or the +//! [`raw_window_handle`] method), which in turn allows you to create an +//! OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. +//! +//! 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 +//! display the window to the user. If you notice this happening, you should create the window with +//! [`visible` set to `false`](crate::window::WindowBuilder::with_visible) and explicitly make the +//! 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 //! [`Window`]: window::Window +//! [`WindowId`]: window::WindowId //! [`WindowBuilder`]: window::WindowBuilder //! [window_new]: window::Window::new //! [window_builder_new]: window::WindowBuilder::new @@ -109,20 +127,21 @@ //! [`UserEvent`]: event::Event::UserEvent //! [`LoopDestroyed`]: event::Event::LoopDestroyed //! [`platform`]: platform +//! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle #![deny(rust_2018_idioms)] -#![deny(intra_doc_link_resolution_failure)] +#![deny(broken_intra_doc_links)] #[allow(unused_imports)] #[macro_use] extern crate lazy_static; +#[allow(unused_imports)] #[macro_use] extern crate log; #[cfg(feature = "serde")] #[macro_use] extern crate serde; #[macro_use] -#[cfg(any(target_os = "ios", target_os = "windows"))] extern crate bitflags; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] @@ -137,7 +156,7 @@ pub mod event; pub mod event_loop; mod icon; pub mod monitor; +pub mod platform; mod platform_impl; +mod util; pub mod window; - -pub mod platform; diff --git a/src/monitor.rs b/src/monitor.rs index 417596c379..5cf680ff21 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,13 +1,13 @@ //! Types useful for interacting with a user's monitors. //! //! If you want to get basic information about a monitor, you can use the [`MonitorHandle`][monitor_handle] -//! type. This is retreived from one of the following methods, which return an iterator of +//! type. This is retrieved from one of the following methods, which return an iterator of //! [`MonitorHandle`][monitor_handle]: -//! - [`EventLoop::available_monitors`][loop_get] +//! - [`EventLoopWindowTarget::available_monitors`][loop_get] //! - [`Window::available_monitors`][window_get]. //! //! [monitor_handle]: crate::monitor::MonitorHandle -//! [loop_get]: crate::event_loop::EventLoop::available_monitors +//! [loop_get]: crate::event_loop::EventLoopWindowTarget::available_monitors //! [window_get]: crate::window::Window::available_monitors use crate::{ dpi::{PhysicalPosition, PhysicalSize}, @@ -58,7 +58,7 @@ impl Ord for VideoMode { impl VideoMode { /// Returns the resolution of this video mode. #[inline] - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.video_mode.size() } @@ -133,7 +133,7 @@ impl MonitorHandle { /// /// - **Web:** Always returns (0,0) #[inline] - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.inner.size() } @@ -144,22 +144,22 @@ impl MonitorHandle { /// /// - **Web:** Always returns (0,0) #[inline] - pub fn position(&self) -> PhysicalPosition { + pub fn position(&self) -> PhysicalPosition { self.inner.position() } - /// Returns the DPI factor that can be used to map logical pixels to physical pixels, and vice versa. + /// 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. /// /// ## Platform-specific /// - /// - **X11:** Can be overridden using the `WINIT_HIDPI_FACTOR` environment variable. + /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. /// - **Android:** Always returns 1.0. /// - **Web:** Always returns 1.0 #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.inner.hidpi_factor() + pub fn scale_factor(&self) -> f64 { + self.inner.scale_factor() } /// Returns all fullscreen video modes supported by this monitor. diff --git a/src/platform/android.rs b/src/platform/android.rs index dafb7a391a..b4e9917642 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -1,32 +1,39 @@ #![cfg(any(target_os = "android"))] -use crate::{EventLoop, Window, WindowBuilder}; -use std::os::raw::c_void; +use crate::{ + event_loop::{EventLoop, EventLoopWindowTarget}, + window::{Window, WindowBuilder}, +}; +use ndk::configuration::Configuration; +use ndk_glue::Rect; /// Additional methods on `EventLoop` that are specific to Android. -pub trait EventLoopExtAndroid { - /// Makes it possible for glutin to register a callback when a suspend event happens on Android - fn set_suspend_callback(&self, cb: Option ()>>); -} +pub trait EventLoopExtAndroid {} -impl EventLoopExtAndroid for EventLoop { - fn set_suspend_callback(&self, cb: Option ()>>) { - self.event_loop.set_suspend_callback(cb); - } -} +impl EventLoopExtAndroid for EventLoop {} + +/// Additional methods on `EventLoopWindowTarget` that are specific to Android. +pub trait EventLoopWindowTargetExtAndroid {} /// Additional methods on `Window` that are specific to Android. pub trait WindowExtAndroid { - fn native_window(&self) -> *const c_void; + fn content_rect(&self) -> Rect; + + fn config(&self) -> Configuration; } impl WindowExtAndroid for Window { - #[inline] - fn native_window(&self) -> *const c_void { - self.window.native_window() + fn content_rect(&self) -> Rect { + self.window.content_rect() + } + + fn config(&self) -> Configuration { + self.window.config() } } +impl EventLoopWindowTargetExtAndroid for EventLoopWindowTarget {} + /// Additional methods on `WindowBuilder` that are specific to Android. pub trait WindowBuilderExtAndroid {} diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 9218204fa6..9d982ac511 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -43,14 +43,14 @@ pub trait WindowExtIOS { /// [`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 `hidpi_factor`. + /// 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 - /// this to [`MonitorHandle::hidpi_factor()`]. + /// this to [`MonitorHandle::scale_factor()`]. /// /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc - fn set_hidpi_factor(&self, hidpi_factor: f64); + fn set_scale_factor(&self, scale_factor: f64); /// Sets the valid orientations for the [`Window`]. /// @@ -113,8 +113,8 @@ impl WindowExtIOS for Window { } #[inline] - fn set_hidpi_factor(&self, hidpi_factor: f64) { - self.window.set_hidpi_factor(hidpi_factor) + fn set_scale_factor(&self, scale_factor: f64) { + self.window.set_scale_factor(scale_factor) } #[inline] @@ -148,14 +148,14 @@ pub trait WindowBuilderExtIOS { /// [`UIView`]: https://developer.apple.com/documentation/uikit/uiview?language=objc fn with_root_view_class(self, root_view_class: *const c_void) -> WindowBuilder; - /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `hidpi_factor`. + /// 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 - /// this to [`MonitorHandle::hidpi_factor()`]. + /// this to [`MonitorHandle::scale_factor()`]. /// /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc - fn with_hidpi_factor(self, hidpi_factor: f64) -> WindowBuilder; + fn with_scale_factor(self, scale_factor: f64) -> WindowBuilder; /// Sets the valid orientations for the [`Window`]. /// @@ -204,8 +204,8 @@ impl WindowBuilderExtIOS for WindowBuilder { } #[inline] - fn with_hidpi_factor(mut self, hidpi_factor: f64) -> WindowBuilder { - self.platform_specific.hidpi_factor = Some(hidpi_factor); + fn with_scale_factor(mut self, scale_factor: f64) -> WindowBuilder { + self.platform_specific.scale_factor = Some(scale_factor); self } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 8101f0f7c3..f66076b9f5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -4,30 +4,11 @@ use std::os::raw::c_void; use crate::{ dpi::LogicalSize, + event_loop::EventLoopWindowTarget, monitor::MonitorHandle, window::{Window, WindowBuilder}, }; -/// Corresponds to `NSRequestUserAttentionType`. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum RequestUserAttentionType { - /// Corresponds to `NSCriticalRequest`. - /// - /// Dock icon will bounce until the application is focused. - Critical, - - /// Corresponds to `NSInformationalRequest`. - /// - /// Dock icon will bounce once. - Informational, -} - -impl Default for RequestUserAttentionType { - fn default() -> Self { - RequestUserAttentionType::Critical - } -} - /// 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. @@ -40,10 +21,6 @@ pub trait WindowExtMacOS { /// The pointer will become invalid when the `Window` is destroyed. fn ns_view(&self) -> *mut c_void; - /// Request user attention, causing the application's dock icon to bounce. - /// Note that this has no effect if the application is already focused. - fn request_user_attention(&self, request_type: RequestUserAttentionType); - /// Returns whether or not the window is in simple fullscreen mode. fn simple_fullscreen(&self) -> bool; @@ -55,6 +32,12 @@ pub trait WindowExtMacOS { /// And allows the user to have a fullscreen window without using another /// space or taking control over the entire monitor. fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; + + /// Returns whether or not the window has shadow. + fn has_shadow(&self) -> bool; + + /// Sets whether or not the window has shadow. + fn set_has_shadow(&self, has_shadow: bool); } impl WindowExtMacOS for Window { @@ -68,11 +51,6 @@ impl WindowExtMacOS for Window { self.window.ns_view() } - #[inline] - fn request_user_attention(&self, request_type: RequestUserAttentionType) { - self.window.request_user_attention(request_type) - } - #[inline] fn simple_fullscreen(&self) -> bool { self.window.simple_fullscreen() @@ -82,6 +60,16 @@ impl WindowExtMacOS for Window { fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { self.window.set_simple_fullscreen(fullscreen) } + + #[inline] + fn has_shadow(&self) -> bool { + self.window.has_shadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window.set_has_shadow(has_shadow) + } } /// Corresponds to `NSApplicationActivationPolicy`. @@ -128,8 +116,9 @@ pub trait WindowBuilderExtMacOS { /// Makes the window content appear behind the titlebar. fn with_fullsize_content_view(self, fullsize_content_view: bool) -> WindowBuilder; /// Build window with `resizeIncrements` property. Values must not be 0. - fn with_resize_increments(self, increments: LogicalSize) -> WindowBuilder; + fn with_resize_increments(self, increments: LogicalSize) -> WindowBuilder; fn with_disallow_hidpi(self, disallow_hidpi: bool) -> WindowBuilder; + fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder; } impl WindowBuilderExtMacOS for WindowBuilder { @@ -179,7 +168,7 @@ impl WindowBuilderExtMacOS for WindowBuilder { } #[inline] - fn with_resize_increments(mut self, increments: LogicalSize) -> WindowBuilder { + fn with_resize_increments(mut self, increments: LogicalSize) -> WindowBuilder { self.platform_specific.resize_increments = Some(increments.into()); self } @@ -189,6 +178,12 @@ impl WindowBuilderExtMacOS for WindowBuilder { self.platform_specific.disallow_hidpi = disallow_hidpi; self } + + #[inline] + fn with_has_shadow(mut self, has_shadow: bool) -> WindowBuilder { + self.platform_specific.has_shadow = has_shadow; + self + } } /// Additional methods on `MonitorHandle` that are specific to MacOS. @@ -209,3 +204,25 @@ impl MonitorHandleExtMacOS for MonitorHandle { self.inner.ns_screen().map(|s| s as *mut c_void) } } + +/// Additional methods on `EventLoopWindowTarget` that are specific to macOS. +pub trait EventLoopWindowTargetExtMacOS { + /// Hide the entire application. In most applications this is typically triggered with Command-H. + fn hide_application(&self); + /// Hide the other applications. In most applications this is typically triggered with Command+Option-H. + fn hide_other_applications(&self); +} + +impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { + fn hide_application(&self) { + let cls = objc::runtime::Class::get("NSApplication").unwrap(); + let app: cocoa::base::id = unsafe { msg_send![cls, sharedApplication] }; + unsafe { msg_send![app, hide: 0] } + } + + fn hide_other_applications(&self) { + let cls = objc::runtime::Class::get("NSApplication").unwrap(); + let app: cocoa::base::id = unsafe { msg_send![cls, sharedApplication] }; + unsafe { msg_send![app, hideOtherApplications: 0] } + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 27ecde1837..1ee5fce2bb 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -11,7 +11,7 @@ //! //! And the following platform-specific module: //! -//! - `desktop` (available on `windows`, `unix`, and `macos`) +//! - `run_return` (available on `windows`, `unix`, `macos`, and `android`) //! //! However only the module corresponding to the platform you're compiling to will be available. @@ -21,5 +21,5 @@ pub mod macos; pub mod unix; pub mod windows; -pub mod desktop; +pub mod run_return; pub mod web; diff --git a/src/platform/desktop.rs b/src/platform/run_return.rs similarity index 60% rename from src/platform/desktop.rs rename to src/platform/run_return.rs index 1ec2056230..932d9e38a3 100644 --- a/src/platform/desktop.rs +++ b/src/platform/run_return.rs @@ -1,7 +1,12 @@ #![cfg(any( target_os = "windows", target_os = "macos", - target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" + target_os = "android", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] use crate::{ @@ -9,8 +14,8 @@ use crate::{ event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; -/// Additional methods on `EventLoop` that are specific to desktop platforms. -pub trait EventLoopExtDesktop { +/// 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; @@ -20,25 +25,33 @@ pub trait EventLoopExtDesktop { /// control flow to the caller when `control_flow` is set to `ControlFlow::Exit`. /// /// # Caveats - /// Despite its apperance at first glance, this is *not* a perfect replacement for + /// 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 - /// underyling OS APIs, which cannot be hidden by Winit without severe stability reprecussions. + /// 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. fn run_return(&mut self, event_handler: F) where - F: FnMut(Event, &EventLoopWindowTarget, &mut ControlFlow); + F: FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ); } -impl EventLoopExtDesktop for EventLoop { +impl EventLoopExtRunReturn for EventLoop { type UserEvent = T; fn run_return(&mut self, event_handler: F) where - F: FnMut(Event, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ), { self.event_loop.run_return(event_handler) } diff --git a/src/platform/unix.rs b/src/platform/unix.rs index 61921e1d82..8662e45690 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -1,105 +1,49 @@ -#![cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] - -use std::{os::raw, ptr, sync::Arc}; - -use smithay_client_toolkit::window::{ButtonState, Theme}; +#![cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +use std::os::raw; +#[cfg(feature = "x11")] +use std::{ptr, sync::Arc}; use crate::{ - dpi::LogicalSize, event_loop::{EventLoop, EventLoopWindowTarget}, monitor::MonitorHandle, window::{Window, WindowBuilder}, }; +#[cfg(feature = "x11")] +use crate::dpi::Size; +#[cfg(feature = "x11")] +use crate::platform_impl::x11::{ffi::XVisualInfo, XConnection}; use crate::platform_impl::{ - x11::{ffi::XVisualInfo, XConnection}, EventLoop as LinuxEventLoop, EventLoopWindowTarget as LinuxEventLoopWindowTarget, Window as LinuxWindow, }; // TODO: stupid hack so that glutin can do its work #[doc(hidden)] +#[cfg(feature = "x11")] pub use crate::platform_impl::x11; - +#[cfg(feature = "x11")] pub use crate::platform_impl::{x11::util::WindowType as XWindowType, XNotSupported}; -/// Theme for wayland client side decorations -/// -/// Colors must be in ARGB8888 format -pub struct WaylandTheme { - /// Primary color when the window is focused - pub primary_active: [u8; 4], - /// Primary color when the window is unfocused - pub primary_inactive: [u8; 4], - /// Secondary color when the window is focused - pub secondary_active: [u8; 4], - /// Secondary color when the window is unfocused - pub secondary_inactive: [u8; 4], - /// Close button color when hovered over - pub close_button_hovered: [u8; 4], - /// Close button color - pub close_button: [u8; 4], - /// Close button color when hovered over - pub maximize_button_hovered: [u8; 4], - /// Maximize button color - pub maximize_button: [u8; 4], - /// Minimize button color when hovered over - pub minimize_button_hovered: [u8; 4], - /// Minimize button color - pub minimize_button: [u8; 4], -} - -struct WaylandThemeObject(WaylandTheme); - -impl Theme for WaylandThemeObject { - fn get_primary_color(&self, active: bool) -> [u8; 4] { - if active { - self.0.primary_active - } else { - self.0.primary_inactive - } - } - - // Used for division line - fn get_secondary_color(&self, active: bool) -> [u8; 4] { - if active { - self.0.secondary_active - } else { - self.0.secondary_inactive - } - } - - fn get_close_button_color(&self, state: ButtonState) -> [u8; 4] { - match state { - ButtonState::Hovered => self.0.close_button_hovered, - _ => self.0.close_button, - } - } - - fn get_maximize_button_color(&self, state: ButtonState) -> [u8; 4] { - match state { - ButtonState::Hovered => self.0.maximize_button_hovered, - _ => self.0.maximize_button, - } - } - - fn get_minimize_button_color(&self, state: ButtonState) -> [u8; 4] { - match state { - ButtonState::Hovered => self.0.minimize_button_hovered, - _ => self.0.minimize_button, - } - } -} - /// Additional methods on `EventLoopWindowTarget` that are specific to Unix. pub trait EventLoopWindowTargetExtUnix { /// True if the `EventLoopWindowTarget` uses Wayland. + #[cfg(feature = "wayland")] fn is_wayland(&self) -> bool; - /// + /// True if the `EventLoopWindowTarget` uses X11. + #[cfg(feature = "x11")] fn is_x11(&self) -> bool; #[doc(hidden)] + #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option>; /// Returns a pointer to the `wl_display` object of wayland that is used by this @@ -108,35 +52,42 @@ pub trait EventLoopWindowTargetExtUnix { /// 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. + #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void>; } impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { #[inline] + #[cfg(feature = "wayland")] fn is_wayland(&self) -> bool { self.p.is_wayland() } #[inline] + #[cfg(feature = "x11")] fn is_x11(&self) -> bool { !self.p.is_wayland() } #[inline] #[doc(hidden)] + #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option> { match self.p { LinuxEventLoopWindowTarget::X(ref e) => Some(e.x_connection().clone()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] + #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void> { match self.p { LinuxEventLoopWindowTarget::Wayland(ref p) => { Some(p.display().get_display_ptr() as *mut _) } + #[cfg(feature = "x11")] _ => None, } } @@ -150,6 +101,7 @@ pub trait EventLoopExtUnix { /// /// If called outside the main thread. To initialize an X11 event loop outside /// the main thread, use [`new_x11_any_thread`](#tymethod.new_x11_any_thread). + #[cfg(feature = "x11")] fn new_x11() -> Result where Self: Sized; @@ -160,6 +112,7 @@ pub trait EventLoopExtUnix { /// /// If called outside the main thread. To initialize a Wayland event loop outside /// the main thread, use [`new_wayland_any_thread`](#tymethod.new_wayland_any_thread). + #[cfg(feature = "wayland")] fn new_wayland() -> Self where Self: Sized; @@ -176,6 +129,7 @@ pub trait EventLoopExtUnix { /// /// This method bypasses the cross-platform compatibility requirement /// that `EventLoop` be created on the main thread. + #[cfg(feature = "x11")] fn new_x11_any_thread() -> Result where Self: Sized; @@ -184,6 +138,7 @@ pub trait EventLoopExtUnix { /// /// This method bypasses the cross-platform compatibility requirement /// that `EventLoop` be created on the main thread. + #[cfg(feature = "wayland")] fn new_wayland_any_thread() -> Self where Self: Sized; @@ -203,11 +158,13 @@ impl EventLoopExtUnix for EventLoop { } #[inline] + #[cfg(feature = "x11")] fn new_x11_any_thread() -> Result { LinuxEventLoop::new_x11_any_thread().map(wrap_ev) } #[inline] + #[cfg(feature = "wayland")] fn new_wayland_any_thread() -> Self { wrap_ev( LinuxEventLoop::new_wayland_any_thread() @@ -217,11 +174,13 @@ impl EventLoopExtUnix for EventLoop { } #[inline] + #[cfg(feature = "x11")] fn new_x11() -> Result { LinuxEventLoop::new_x11().map(wrap_ev) } #[inline] + #[cfg(feature = "wayland")] fn new_wayland() -> Self { wrap_ev( LinuxEventLoop::new_wayland() @@ -236,6 +195,7 @@ pub trait WindowExtUnix { /// Returns the ID of the `Window` xlib object that is used by this window. /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). + #[cfg(feature = "x11")] fn xlib_window(&self) -> Option; /// Returns a pointer to the `Display` object of xlib that is used by this window. @@ -243,21 +203,22 @@ pub trait WindowExtUnix { /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). /// /// The pointer will become invalid when the glutin `Window` is destroyed. + #[cfg(feature = "x11")] fn xlib_display(&self) -> Option<*mut raw::c_void>; + #[cfg(feature = "x11")] fn xlib_screen_id(&self) -> Option; #[doc(hidden)] + #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option>; - /// Set window urgency hint (`XUrgencyHint`). Only relevant on X. - fn set_urgent(&self, is_urgent: bool); - /// 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 glutin `Window` is destroyed. + #[cfg(feature = "x11")] fn xcb_connection(&self) -> Option<*mut raw::c_void>; /// Returns a pointer to the `wl_surface` object of wayland that is used by this window. @@ -265,6 +226,7 @@ pub trait WindowExtUnix { /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). /// /// The pointer will become invalid when the glutin `Window` is destroyed. + #[cfg(feature = "wayland")] 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. @@ -272,10 +234,12 @@ pub trait WindowExtUnix { /// Returns `None` if the window doesn't use wayland (if it uses xlib for example). /// /// The pointer will become invalid when the glutin `Window` is destroyed. + #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void>; /// Sets the color theme of the client side window decorations on wayland - fn set_wayland_theme(&self, theme: WaylandTheme); + #[cfg(feature = "wayland")] + fn set_wayland_theme(&self, theme: T); /// Check if the window is ready for drawing /// @@ -289,73 +253,82 @@ pub trait WindowExtUnix { impl WindowExtUnix for Window { #[inline] + #[cfg(feature = "x11")] fn xlib_window(&self) -> Option { match self.window { LinuxWindow::X(ref w) => Some(w.xlib_window()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] + #[cfg(feature = "x11")] fn xlib_display(&self) -> Option<*mut raw::c_void> { match self.window { LinuxWindow::X(ref w) => Some(w.xlib_display()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] + #[cfg(feature = "x11")] fn xlib_screen_id(&self) -> Option { match self.window { LinuxWindow::X(ref w) => Some(w.xlib_screen_id()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] #[doc(hidden)] + #[cfg(feature = "x11")] fn xlib_xconnection(&self) -> Option> { match self.window { LinuxWindow::X(ref w) => Some(w.xlib_xconnection()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] - fn set_urgent(&self, is_urgent: bool) { - if let LinuxWindow::X(ref w) = self.window { - w.set_urgent(is_urgent); - } - } - - #[inline] + #[cfg(feature = "x11")] fn xcb_connection(&self) -> Option<*mut raw::c_void> { match self.window { LinuxWindow::X(ref w) => Some(w.xcb_connection()), + #[cfg(feature = "wayland")] _ => None, } } #[inline] + #[cfg(feature = "wayland")] fn wayland_surface(&self) -> Option<*mut raw::c_void> { match self.window { LinuxWindow::Wayland(ref w) => Some(w.surface().as_ref().c_ptr() as *mut _), + #[cfg(feature = "x11")] _ => None, } } #[inline] + #[cfg(feature = "wayland")] fn wayland_display(&self) -> Option<*mut raw::c_void> { match self.window { - LinuxWindow::Wayland(ref w) => Some(w.display().as_ref().c_ptr() as *mut _), + LinuxWindow::Wayland(ref w) => Some(w.display().get_display_ptr() as *mut _), + #[cfg(feature = "x11")] _ => None, } } #[inline] - fn set_wayland_theme(&self, theme: WaylandTheme) { + #[cfg(feature = "wayland")] + fn set_wayland_theme(&self, theme: T) { match self.window { - LinuxWindow::Wayland(ref w) => w.set_theme(WaylandThemeObject(theme)), + LinuxWindow::Wayland(ref w) => w.set_theme(theme), + #[cfg(feature = "x11")] _ => {} } } @@ -368,81 +341,101 @@ impl WindowExtUnix for Window { /// Additional methods on `WindowBuilder` that are specific to Unix. pub trait WindowBuilderExtUnix { + #[cfg(feature = "x11")] fn with_x11_visual(self, visual_infos: *const T) -> Self; + #[cfg(feature = "x11")] fn with_x11_screen(self, screen_id: i32) -> Self; /// Build window with `WM_CLASS` hint; defaults to the name of the binary. Only relevant on X11. + #[cfg(feature = "x11")] fn with_class(self, class: String, instance: String) -> Self; /// Build window with override-redirect flag; defaults to false. Only relevant on X11. + #[cfg(feature = "x11")] fn with_override_redirect(self, override_redirect: bool) -> Self; /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. Only relevant on X11. + #[cfg(feature = "x11")] fn with_x11_window_type(self, x11_window_type: Vec) -> Self; /// Build window with `_GTK_THEME_VARIANT` hint set to the specified value. Currently only relevant on X11. + #[cfg(feature = "x11")] fn with_gtk_theme_variant(self, variant: String) -> Self; /// Build window with resize increment hint. Only implemented on X11. - fn with_resize_increments(self, increments: LogicalSize) -> Self; + #[cfg(feature = "x11")] + fn with_resize_increments>(self, increments: S) -> Self; /// Build window with base size hint. Only implemented on X11. - fn with_base_size(self, base_size: LogicalSize) -> Self; + #[cfg(feature = "x11")] + fn with_base_size>(self, base_size: S) -> Self; /// Build window with a given application ID. It should match the `.desktop` file distributed with /// your program. Only relevant on Wayland. /// /// For details about application ID conventions, see the /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + #[cfg(feature = "wayland")] fn with_app_id(self, app_id: String) -> Self; } impl WindowBuilderExtUnix for WindowBuilder { #[inline] + #[cfg(feature = "x11")] 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) }); + { + self.platform_specific.visual_infos = + Some(unsafe { ptr::read(visual_infos as *const XVisualInfo) }); + } self } #[inline] + #[cfg(feature = "x11")] fn with_x11_screen(mut self, screen_id: i32) -> Self { self.platform_specific.screen_id = Some(screen_id); self } #[inline] + #[cfg(feature = "x11")] fn with_class(mut self, instance: String, class: String) -> Self { self.platform_specific.class = Some((instance, class)); self } #[inline] + #[cfg(feature = "x11")] fn with_override_redirect(mut self, override_redirect: bool) -> Self { self.platform_specific.override_redirect = override_redirect; self } #[inline] + #[cfg(feature = "x11")] fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { self.platform_specific.x11_window_types = x11_window_types; self } #[inline] + #[cfg(feature = "x11")] fn with_gtk_theme_variant(mut self, variant: String) -> Self { self.platform_specific.gtk_theme_variant = Some(variant); self } #[inline] - fn with_resize_increments(mut self, increments: LogicalSize) -> Self { + #[cfg(feature = "x11")] + fn with_resize_increments>(mut self, increments: S) -> Self { self.platform_specific.resize_increments = Some(increments.into()); self } #[inline] - fn with_base_size(mut self, base_size: LogicalSize) -> Self { + #[cfg(feature = "x11")] + fn with_base_size>(mut self, base_size: S) -> Self { self.platform_specific.base_size = Some(base_size.into()); self } #[inline] + #[cfg(feature = "wayland")] fn with_app_id(mut self, app_id: String) -> Self { self.platform_specific.app_id = Some(app_id); self @@ -461,3 +454,78 @@ impl MonitorHandleExtUnix for MonitorHandle { self.inner.native_identifier() } } + +/// A theme for a Wayland's client side decorations. +#[cfg(feature = "wayland")] +pub trait Theme: Send + 'static { + /// Title bar color. + fn element_color(&self, element: Element, window_active: bool) -> ARGBColor; + + /// Color for a given button part. + fn button_color( + &self, + button: Button, + state: ButtonState, + foreground: bool, + window_active: bool, + ) -> ARGBColor; + + /// Font name and the size for the title bar. + /// + /// By default the font is `sans-serif` at the size of 17. + /// + /// Returning `None` means that title won't be drawn. + fn font(&self) -> Option<(String, f32)> { + // Not having any title isn't something desirable for the users, so setting it to + // something generic. + Some((String::from("sans-serif"), 17.)) + } +} + +/// A button on Wayland's client side decorations. +#[cfg(feature = "wayland")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Button { + /// Button that maximizes the window. + Maximize, + + /// Button that minimizes the window. + Minimize, + + /// Button that closes the window. + Close, +} + +/// A button state of the button on Wayland's client side decorations. +#[cfg(feature = "wayland")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ButtonState { + /// Button is being hovered over by pointer. + Hovered, + /// Button is not being hovered over by pointer. + Idle, + /// Button is disabled. + Disabled, +} + +#[cfg(feature = "wayland")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Element { + /// Bar itself. + Bar, + + /// Separator between window and title bar. + Separator, + + /// Title bar text. + Text, +} + +#[cfg(feature = "wayland")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ARGBColor { + pub a: u8, + pub r: u8, + pub g: u8, + pub b: u8, +} diff --git a/src/platform/web.rs b/src/platform/web.rs index 5fca0c422b..c5ceca90bb 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -3,7 +3,10 @@ //! The web target does not automatically insert the canvas element object into the web page, to //! allow end users to determine how the page should be laid out. Use the `WindowExtStdweb` or //! `WindowExtWebSys` traits (depending on your web backend) to retrieve the canvas from the -//! Window. +//! Window. Alternatively, use the `WindowBuilderExtStdweb` or `WindowBuilderExtWebSys` to provide +//! your own canvas. + +use crate::window::WindowBuilder; #[cfg(feature = "stdweb")] use stdweb::web::html_element::CanvasElement; @@ -11,6 +14,9 @@ use stdweb::web::html_element::CanvasElement; #[cfg(feature = "stdweb")] pub trait WindowExtStdweb { fn canvas(&self) -> CanvasElement; + + /// Whether the browser reports the preferred color scheme to be "dark". + fn is_dark_mode(&self) -> bool; } #[cfg(feature = "web-sys")] @@ -19,4 +25,35 @@ use web_sys::HtmlCanvasElement; #[cfg(feature = "web-sys")] pub trait WindowExtWebSys { fn canvas(&self) -> HtmlCanvasElement; + + /// Whether the browser reports the preferred color scheme to be "dark". + fn is_dark_mode(&self) -> bool; +} + +#[cfg(feature = "stdweb")] +pub trait WindowBuilderExtStdweb { + fn with_canvas(self, canvas: Option) -> Self; +} + +#[cfg(feature = "stdweb")] +impl WindowBuilderExtStdweb for WindowBuilder { + fn with_canvas(mut self, canvas: Option) -> Self { + self.platform_specific.canvas = canvas; + + self + } +} + +#[cfg(feature = "web-sys")] +pub trait WindowBuilderExtWebSys { + fn with_canvas(self, canvas: Option) -> Self; +} + +#[cfg(feature = "web-sys")] +impl WindowBuilderExtWebSys for WindowBuilder { + fn with_canvas(mut self, canvas: Option) -> Self { + self.platform_specific.canvas = canvas; + + self + } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 3ab9b1dde6..adbdec3975 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,16 +1,19 @@ #![cfg(target_os = "windows")] use std::os::raw::c_void; +use std::path::Path; use libc; -use winapi::shared::windef::HWND; +use winapi::shared::minwindef::WORD; +use winapi::shared::windef::{HMENU, HWND}; use crate::{ - event::DeviceId, + dpi::PhysicalSize, + event::device::{GamepadHandle, KeyboardId, MouseId}, event_loop::EventLoop, monitor::MonitorHandle, - platform_impl::EventLoop as WindowsEventLoop, - window::{Icon, Window, WindowBuilder}, + platform_impl::{EventLoop as WindowsEventLoop, WinIcon}, + window::{BadIcon, Icon, Theme, Window, WindowBuilder}, }; /// Additional methods on `EventLoop` that are specific to Windows. @@ -77,6 +80,9 @@ pub trait WindowExtWindows { /// This sets `ICON_BIG`. A good ceiling here is 256x256. fn set_taskbar_icon(&self, taskbar_icon: Option); + + /// Returns the current window theme. + fn theme(&self) -> Theme; } impl WindowExtWindows for Window { @@ -94,6 +100,11 @@ impl WindowExtWindows for Window { fn set_taskbar_icon(&self, taskbar_icon: Option) { self.window.set_taskbar_icon(taskbar_icon) } + + #[inline] + fn theme(&self) -> Theme { + self.window.theme() + } } /// Additional methods on `WindowBuilder` that are specific to Windows. @@ -101,11 +112,32 @@ pub trait WindowBuilderExtWindows { /// Sets a parent to the window to be created. fn with_parent_window(self, parent: HWND) -> WindowBuilder; + /// Sets a menu on the window to be created. + /// + /// Parent and menu are mutually exclusive; a child window cannot have a menu! + /// + /// The menu must have been manually created beforehand with [`winapi::um::winuser::CreateMenu`] or similar. + /// + /// Note: Dark mode cannot be supported for win32 menus, it's simply not possible to change how the menus look. + /// If you use this, it is recommended that you combine it with `with_theme(Some(Theme::Light))` to avoid a jarring effect. + fn with_menu(self, menu: HMENU) -> WindowBuilder; + /// This sets `ICON_BIG`. A good ceiling here is 256x256. fn with_taskbar_icon(self, taskbar_icon: Option) -> WindowBuilder; /// This sets `WS_EX_NOREDIRECTIONBITMAP`. fn with_no_redirection_bitmap(self, flag: bool) -> WindowBuilder; + + /// 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 + /// `COINIT_APARTMENTTHREADED`) on the same thread. Note that winit may still attempt to initialize + /// 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 https://docs.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitialize#remarks for more information. + fn with_drag_and_drop(self, flag: bool) -> WindowBuilder; + + /// Forces a theme or uses the system settings if `None` was provided. + fn with_theme(self, theme: Option) -> WindowBuilder; } impl WindowBuilderExtWindows for WindowBuilder { @@ -115,6 +147,12 @@ impl WindowBuilderExtWindows for WindowBuilder { self } + #[inline] + fn with_menu(mut self, menu: HMENU) -> WindowBuilder { + self.platform_specific.menu = Some(menu); + self + } + #[inline] fn with_taskbar_icon(mut self, taskbar_icon: Option) -> WindowBuilder { self.platform_specific.taskbar_icon = taskbar_icon; @@ -126,6 +164,18 @@ impl WindowBuilderExtWindows for WindowBuilder { self.platform_specific.no_redirection_bitmap = flag; self } + + #[inline] + fn with_drag_and_drop(mut self, flag: bool) -> WindowBuilder { + self.platform_specific.drag_and_drop = flag; + self + } + + #[inline] + fn with_theme(mut self, theme: Option) -> WindowBuilder { + self.platform_specific.preferred_theme = theme; + self + } } /// Additional methods on `MonitorHandle` that are specific to Windows. @@ -149,17 +199,86 @@ impl MonitorHandleExtWindows for MonitorHandle { } } -/// Additional methods on `DeviceId` that are specific to Windows. -pub trait DeviceIdExtWindows { +/// Additional methods on device types that are specific to Windows. +pub trait DeviceExtWindows { /// Returns an identifier that persistently refers to this specific device. /// /// Will return `None` if the device is no longer available. fn persistent_identifier(&self) -> Option; + + /// Returns the handle of the device - `HANDLE`. + fn handle(&self) -> *mut c_void; +} + +impl DeviceExtWindows for MouseId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } +} + +impl DeviceExtWindows for KeyboardId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } } -impl DeviceIdExtWindows for DeviceId { +impl DeviceExtWindows for GamepadHandle { #[inline] fn persistent_identifier(&self) -> Option { self.0.persistent_identifier() } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } +} + +/// Additional methods on `Icon` that are specific to Windows. +pub trait IconExtWindows: Sized { + /// Create an icon from a file path. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_path>(path: P, size: Option>) + -> Result; + + /// Create an icon from a resource embedded in this executable or library. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_resource(ordinal: WORD, size: Option>) -> Result; +} + +impl IconExtWindows for Icon { + fn from_path>( + path: P, + size: Option>, + ) -> Result { + let win_icon = WinIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource(ordinal: WORD, size: Option>) -> Result { + let win_icon = WinIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } } diff --git a/src/platform_impl/android/ffi.rs b/src/platform_impl/android/ffi.rs deleted file mode 100644 index 93a59b8247..0000000000 --- a/src/platform_impl/android/ffi.rs +++ /dev/null @@ -1,122 +0,0 @@ -#![allow(dead_code)] -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(non_upper_case_globals)] - -use libc; -use std::os::raw; - -#[link(name = "android")] -#[link(name = "EGL")] -#[link(name = "GLESv2")] -extern "C" {} - -/** - ** asset_manager.h - **/ -pub type AAssetManager = raw::c_void; - -/** - ** native_window.h - **/ -pub type ANativeWindow = raw::c_void; - -extern "C" { - pub fn ANativeWindow_getHeight(window: *const ANativeWindow) -> libc::int32_t; - pub fn ANativeWindow_getWidth(window: *const ANativeWindow) -> libc::int32_t; -} - -/** - ** native_activity.h - **/ -pub type JavaVM = (); -pub type JNIEnv = (); -pub type jobject = *const libc::c_void; - -pub type AInputQueue = (); // FIXME: wrong -pub type ARect = (); // FIXME: wrong - -#[repr(C)] -pub struct ANativeActivity { - pub callbacks: *mut ANativeActivityCallbacks, - pub vm: *mut JavaVM, - pub env: *mut JNIEnv, - pub clazz: jobject, - pub internalDataPath: *const libc::c_char, - pub externalDataPath: *const libc::c_char, - pub sdkVersion: libc::int32_t, - pub instance: *mut libc::c_void, - pub assetManager: *mut AAssetManager, - pub obbPath: *const libc::c_char, -} - -#[repr(C)] -pub struct ANativeActivityCallbacks { - pub onStart: extern "C" fn(*mut ANativeActivity), - pub onResume: extern "C" fn(*mut ANativeActivity), - pub onSaveInstanceState: extern "C" fn(*mut ANativeActivity, *mut libc::size_t), - pub onPause: extern "C" fn(*mut ANativeActivity), - pub onStop: extern "C" fn(*mut ANativeActivity), - pub onDestroy: extern "C" fn(*mut ANativeActivity), - pub onWindowFocusChanged: extern "C" fn(*mut ANativeActivity, libc::c_int), - pub onNativeWindowCreated: extern "C" fn(*mut ANativeActivity, *const ANativeWindow), - pub onNativeWindowResized: extern "C" fn(*mut ANativeActivity, *const ANativeWindow), - pub onNativeWindowRedrawNeeded: extern "C" fn(*mut ANativeActivity, *const ANativeWindow), - pub onNativeWindowDestroyed: extern "C" fn(*mut ANativeActivity, *const ANativeWindow), - pub onInputQueueCreated: extern "C" fn(*mut ANativeActivity, *mut AInputQueue), - pub onInputQueueDestroyed: extern "C" fn(*mut ANativeActivity, *mut AInputQueue), - pub onContentRectChanged: extern "C" fn(*mut ANativeActivity, *const ARect), - pub onConfigurationChanged: extern "C" fn(*mut ANativeActivity), - pub onLowMemory: extern "C" fn(*mut ANativeActivity), -} - -/** - ** looper.h - **/ -pub type ALooper = (); - -#[link(name = "android")] -extern "C" { - pub fn ALooper_forThread() -> *const ALooper; - pub fn ALooper_acquire(looper: *const ALooper); - pub fn ALooper_release(looper: *const ALooper); - pub fn ALooper_prepare(opts: libc::c_int) -> *const ALooper; - pub fn ALooper_pollOnce( - timeoutMillis: libc::c_int, - outFd: *mut libc::c_int, - outEvents: *mut libc::c_int, - outData: *mut *mut libc::c_void, - ) -> libc::c_int; - pub fn ALooper_pollAll( - timeoutMillis: libc::c_int, - outFd: *mut libc::c_int, - outEvents: *mut libc::c_int, - outData: *mut *mut libc::c_void, - ) -> libc::c_int; - pub fn ALooper_wake(looper: *const ALooper); - pub fn ALooper_addFd( - looper: *const ALooper, - fd: libc::c_int, - ident: libc::c_int, - events: libc::c_int, - callback: ALooper_callbackFunc, - data: *mut libc::c_void, - ) -> libc::c_int; - pub fn ALooper_removeFd(looper: *const ALooper, fd: libc::c_int) -> libc::c_int; -} - -pub const ALOOPER_PREPARE_ALLOW_NON_CALLBACKS: libc::c_int = 1 << 0; - -pub const ALOOPER_POLL_WAKE: libc::c_int = -1; -pub const ALOOPER_POLL_CALLBACK: libc::c_int = -2; -pub const ALOOPER_POLL_TIMEOUT: libc::c_int = -3; -pub const ALOOPER_POLL_ERROR: libc::c_int = -4; - -pub const ALOOPER_EVENT_INPUT: libc::c_int = 1 << 0; -pub const ALOOPER_EVENT_OUTPUT: libc::c_int = 1 << 1; -pub const ALOOPER_EVENT_ERROR: libc::c_int = 1 << 2; -pub const ALOOPER_EVENT_HANGUP: libc::c_int = 1 << 3; -pub const ALOOPER_EVENT_INVALID: libc::c_int = 1 << 4; - -pub type ALooper_callbackFunc = - extern "C" fn(libc::c_int, libc::c_int, *mut libc::c_void) -> libc::c_int; diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 2c05e4bbe7..a6d197b644 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -1,440 +1,642 @@ #![cfg(target_os = "android")] -extern crate android_glue; - -mod ffi; - +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + error, event, + event_loop::{self, ControlFlow}, + monitor, window, +}; +use ndk::{ + configuration::Configuration, + event::{InputEvent, MotionAction}, + looper::{ForeignLooper, Poll, ThreadLooper}, +}; +use ndk_glue::{Event, Rect}; use std::{ - cell::RefCell, collections::VecDeque, - fmt, - os::raw::c_void, - sync::mpsc::{channel, Receiver}, + sync::{Arc, Mutex, RwLock}, + time::{Duration, Instant}, }; -use crate::{ - error::{ExternalError, NotSupportedError}, - events::{Touch, TouchPhase}, - window::MonitorHandle as RootMonitorHandle, - CreationError, CursorIcon, Event, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, - WindowAttributes, WindowEvent, WindowId as RootWindowId, -}; -use raw_window_handle::{android::AndroidHandle, RawWindowHandle}; -use CreationError::OsError; +lazy_static! { + static ref CONFIG: RwLock = RwLock::new(Configuration::new()); +} -pub type OsError = std::io::Error; +enum EventSource { + Callback, + InputQueue, + User, +} -pub struct EventLoop { - event_rx: Receiver, - suspend_callback: RefCell ()>>>, +fn poll(poll: Poll) -> Option { + match poll { + Poll::Event { ident, .. } => match ident { + ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback), + ndk_glue::NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT => Some(EventSource::InputQueue), + _ => unreachable!(), + }, + Poll::Timeout => None, + Poll::Wake => Some(EventSource::User), + Poll::Callback => unreachable!(), + } } -#[derive(Clone)] -pub struct EventLoopProxy; +pub struct EventLoop { + window_target: event_loop::EventLoopWindowTarget, + user_queue: Arc>>, + first_event: Option, + start_cause: event::StartCause, + looper: ThreadLooper, + running: bool, +} -impl EventLoop { - pub fn new() -> EventLoop { - let (tx, rx) = channel(); - android_glue::add_sender(tx); - EventLoop { - event_rx: rx, - suspend_callback: Default::default(), +macro_rules! call_event_handler { + ( $event_handler:expr, $window_target:expr, $cf:expr, $event:expr ) => {{ + if $cf != ControlFlow::Exit { + $event_handler($event, $window_target, &mut $cf); + } else { + $event_handler($event, $window_target, &mut ControlFlow::Exit); } - } + }}; +} - #[inline] - pub fn available_monitors(&self) -> VecDeque { - let mut rb = VecDeque::with_capacity(1); - rb.push_back(MonitorHandle); - rb +impl EventLoop { + pub fn new() -> Self { + Self { + window_target: event_loop::EventLoopWindowTarget { + p: EventLoopWindowTarget { + _marker: std::marker::PhantomData, + }, + _marker: std::marker::PhantomData, + }, + user_queue: Default::default(), + first_event: None, + start_cause: event::StartCause::Init, + looper: ThreadLooper::for_thread().unwrap(), + running: false, + } } - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle + pub fn run(mut self, event_handler: F) -> ! + where + F: 'static + + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + self.run_return(event_handler); + ::std::process::exit(0); } - pub fn poll_events(&mut self, mut callback: F) + pub fn run_return(&mut self, mut event_handler: F) where - F: FnMut(::Event), + F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), { - while let Ok(event) = self.event_rx.try_recv() { - let e = match event { - android_glue::Event::EventMotion(motion) => { - let dpi_factor = MonitorHandle.hidpi_factor(); - let location = LogicalPosition::from_physical( - (motion.x as f64, motion.y as f64), - dpi_factor, - ); - Some(Event::WindowEvent { - window_id: RootWindowId(WindowId), - event: WindowEvent::Touch(Touch { - phase: match motion.action { - android_glue::MotionAction::Down => TouchPhase::Started, - android_glue::MotionAction::Move => TouchPhase::Moved, - android_glue::MotionAction::Up => TouchPhase::Ended, - android_glue::MotionAction::Cancel => TouchPhase::Cancelled, - }, - location, - force: None, // TODO - id: motion.pointer_id as u64, - device_id: DEVICE_ID, - }), - }) + let mut control_flow = ControlFlow::default(); + + 'event_loop: loop { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::NewEvents(self.start_cause) + ); + + let mut redraw = false; + let mut resized = false; + + match self.first_event.take() { + Some(EventSource::Callback) => match ndk_glue::poll_events().unwrap() { + Event::WindowCreated => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Resumed + ); + } + Event::WindowResized => resized = true, + Event::WindowRedrawNeeded => redraw = true, + Event::WindowDestroyed => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Suspended + ); + } + Event::Pause => self.running = false, + Event::Resume => self.running = true, + Event::ConfigChanged => { + let am = ndk_glue::native_activity().asset_manager(); + let config = Configuration::from_asset_manager(&am); + let old_scale_factor = MonitorHandle.scale_factor(); + *CONFIG.write().unwrap() = config; + let scale_factor = MonitorHandle.scale_factor(); + if (scale_factor - old_scale_factor).abs() < f64::EPSILON { + let mut size = MonitorHandle.size(); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::ScaleFactorChanged { + new_inner_size: &mut size, + scale_factor, + }, + }; + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event + ); + } + } + Event::WindowHasFocus => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(true), + } + ); + } + Event::WindowLostFocus => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(false), + } + ); + } + _ => {} + }, + Some(EventSource::InputQueue) => { + if let Some(input_queue) = ndk_glue::input_queue().as_ref() { + while let Some(event) = input_queue.get_event() { + if let Some(event) = input_queue.pre_dispatch(event) { + let mut handled = true; + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId); + match &event { + InputEvent::MotionEvent(motion_event) => { + 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) + } + _ => { + handled = false; + 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()) + } + }; + + for pointer in pointers { + let location = PhysicalPosition { + x: pointer.x() as _, + y: pointer.y() as _, + }; + 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, + }, + ), + }; + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event + ); + } + } + } + InputEvent::KeyEvent(_) => { + // TODO + handled = false; + } + }; + input_queue.finish_event(event, handled); + } + } + } } - android_glue::Event::InitWindow => { - // The activity went to foreground. - if let Some(cb) = self.suspend_callback.borrow().as_ref() { - (*cb)(false); + Some(EventSource::User) => { + let mut user_queue = self.user_queue.lock().unwrap(); + while let Some(event) = user_queue.pop_front() { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::UserEvent(event) + ); } - Some(Event::Resumed) } - android_glue::Event::TermWindow => { - // The activity went to background. - if let Some(cb) = self.suspend_callback.borrow().as_ref() { - (*cb)(true); + None => {} + } + + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::MainEventsCleared + ); + + if resized && self.running { + let size = MonitorHandle.size(); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Resized(size), + }; + call_event_handler!(event_handler, self.window_target(), control_flow, event); + } + + if redraw && self.running { + let event = event::Event::RedrawRequested(window::WindowId(WindowId)); + call_event_handler!(event_handler, self.window_target(), control_flow, event); + } + + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::RedrawEventsCleared + ); + + match control_flow { + ControlFlow::Exit => { + self.first_event = poll( + self.looper + .poll_once_timeout(Duration::from_millis(0)) + .unwrap(), + ); + self.start_cause = event::StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }; + break 'event_loop; + } + ControlFlow::Poll => { + self.first_event = poll( + self.looper + .poll_all_timeout(Duration::from_millis(0)) + .unwrap(), + ); + self.start_cause = event::StartCause::Poll; + } + ControlFlow::Wait => { + self.first_event = poll(self.looper.poll_all().unwrap()); + self.start_cause = event::StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, } - Some(Event::Suspended) } - android_glue::Event::WindowResized | android_glue::Event::ConfigChanged => { - // Activity Orientation changed or resized. - let native_window = unsafe { android_glue::native_window() }; - if native_window.is_null() { - None + ControlFlow::WaitUntil(instant) => { + let start = Instant::now(); + let duration = if instant <= start { + Duration::default() + } else { + instant - start + }; + self.first_event = poll(self.looper.poll_all_timeout(duration).unwrap()); + self.start_cause = if self.first_event.is_some() { + event::StartCause::WaitCancelled { + start, + requested_resume: Some(instant), + } } else { - let dpi_factor = MonitorHandle.hidpi_factor(); - let physical_size = MonitorHandle.size(); - let size = LogicalSize::from_physical(physical_size, dpi_factor); - Some(Event::WindowEvent { - window_id: RootWindowId(WindowId), - event: WindowEvent::Resized(size), - }) + event::StartCause::ResumeTimeReached { + start, + requested_resume: instant, + } } } - android_glue::Event::WindowRedrawNeeded => { - // The activity needs to be redrawn. - Some(Event::WindowEvent { - window_id: RootWindowId(WindowId), - event: WindowEvent::Redraw, - }) - } - android_glue::Event::Wake => Some(Event::Awakened), - _ => None, - }; - - if let Some(event) = e { - callback(event); } } } - pub fn set_suspend_callback(&self, cb: Option ()>>) { - *self.suspend_callback.borrow_mut() = cb; + pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { + &self.window_target } - pub fn run_forever(&mut self, mut callback: F) - where - F: FnMut(::Event) -> ::ControlFlow, - { - // Yeah that's a very bad implementation. - loop { - let mut control_flow = ::ControlFlow::Continue; - self.poll_events(|e| { - if let ::ControlFlow::Break = callback(e) { - control_flow = ::ControlFlow::Break; - } - }); - if let ::ControlFlow::Break = control_flow { - break; - } - ::std::thread::sleep(::std::time::Duration::from_millis(5)); + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + queue: self.user_queue.clone(), + looper: ForeignLooper::for_thread().expect("called from event loop thread"), } } +} - pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy - } +pub struct EventLoopProxy { + queue: Arc>>, + looper: ForeignLooper, } -impl EventLoopProxy { - pub fn wakeup(&self) -> Result<(), ::EventLoopClosed> { - android_glue::wake_event_loop(); +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.queue.lock().unwrap().push_back(event); + self.looper.wake(); Ok(()) } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId; - -impl WindowId { - pub unsafe fn dummy() -> Self { - WindowId +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + queue: self.queue.clone(), + looper: self.looper.clone(), + } } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId; +pub struct EventLoopWindowTarget { + _marker: std::marker::PhantomData, +} -impl DeviceId { - pub unsafe fn dummy() -> Self { - DeviceId +impl EventLoopWindowTarget { + pub fn primary_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, + }) } -} -pub struct Window { - native_window: *const c_void, + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } } -#[derive(Clone)] -pub struct MonitorHandle; - -impl fmt::Debug for MonitorHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[derive(Debug)] - struct MonitorHandle { - name: Option, - dimensions: PhysicalSize, - position: PhysicalPosition, - hidpi_factor: f64, - } - - let monitor_id_proxy = MonitorHandle { - name: self.name(), - dimensions: self.size(), - position: self.outer_position(), - hidpi_factor: self.hidpi_factor(), - }; +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId; - monitor_id_proxy.fmt(f) +impl WindowId { + pub fn dummy() -> Self { + WindowId } } -impl MonitorHandle { - #[inline] - pub fn name(&self) -> Option { - Some("Primary".to_string()) - } - - #[inline] - pub fn size(&self) -> PhysicalSize { - unsafe { - let window = android_glue::native_window(); - ( - ffi::ANativeWindow_getWidth(window) as f64, - ffi::ANativeWindow_getHeight(window) as f64, - ) - .into() - } - } - - #[inline] - pub fn outer_position(&self) -> PhysicalPosition { - // Android assumes single screen - (0, 0).into() - } +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId; - #[inline] - pub fn hidpi_factor(&self) -> f64 { - 1.0 +impl DeviceId { + pub fn dummy() -> Self { + DeviceId } } -#[derive(Clone, Default)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PlatformSpecificWindowBuilderAttributes; -#[derive(Clone, Default)] -pub struct PlatformSpecificHeadlessBuilderAttributes; + +pub struct Window; impl Window { - pub fn new( - _: &EventLoop, - win_attribs: WindowAttributes, + pub fn new( + _el: &EventLoopWindowTarget, + _window_attrs: window::WindowAttributes, _: PlatformSpecificWindowBuilderAttributes, - ) -> Result { - let native_window = unsafe { android_glue::native_window() }; - if native_window.is_null() { - return Err(OsError(format!("Android's native window is null"))); - } + ) -> Result { + // FIXME this ignores requested window attributes + Ok(Self) + } - android_glue::set_multitouch(true); + pub fn id(&self) -> WindowId { + WindowId + } - Ok(Window { - native_window: native_window as *const _, + pub fn primary_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, }) } - #[inline] - pub fn native_window(&self) -> *const c_void { - self.native_window + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v } - #[inline] - pub fn set_title(&self, _: &str) { - // N/A + pub fn current_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, + }) } - #[inline] - pub fn show(&self) { - // N/A + pub fn scale_factor(&self) -> f64 { + MonitorHandle.scale_factor() } - #[inline] - pub fn hide(&self) { - // N/A + pub fn request_redraw(&self) { + // TODO } - #[inline] - pub fn outer_position(&self) -> Option { - // N/A - None + pub fn inner_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) } - #[inline] - pub fn inner_position(&self) -> Option { - // N/A - None + pub fn outer_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) } - #[inline] - pub fn set_outer_position(&self, _position: LogicalPosition) { - // N/A + pub fn set_outer_position(&self, _position: Position) { + // no effect } - #[inline] - pub fn set_min_inner_size(&self, _dimensions: Option) { - // N/A + pub fn inner_size(&self) -> PhysicalSize { + self.outer_size() } - #[inline] - pub fn set_max_inner_size(&self, _dimensions: Option) { - // N/A + pub fn set_inner_size(&self, _size: Size) { + warn!("Cannot set window size on Android"); } - #[inline] - pub fn set_resizable(&self, _resizable: bool) { - // N/A + pub fn outer_size(&self) -> PhysicalSize { + MonitorHandle.size() } - #[inline] - pub fn inner_size(&self) -> Option { - if self.native_window.is_null() { - None - } else { - let dpi_factor = self.hidpi_factor(); - let physical_size = self.current_monitor().size(); - Some(LogicalSize::from_physical(physical_size, dpi_factor)) - } - } + pub fn set_min_inner_size(&self, _: Option) {} - #[inline] - pub fn outer_size(&self) -> Option { - self.inner_size() - } + pub fn set_max_inner_size(&self, _: Option) {} - #[inline] - pub fn set_inner_size(&self, _size: LogicalSize) { - // N/A - } + pub fn set_title(&self, _title: &str) {} - #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.current_monitor().hidpi_factor() - } + pub fn set_visible(&self, _visibility: bool) {} - #[inline] - pub fn set_cursor_icon(&self, _: CursorIcon) { - // N/A - } + pub fn set_resizable(&self, _resizeable: bool) {} - #[inline] - pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn set_minimized(&self, _minimized: bool) {} + + pub fn set_maximized(&self, _maximized: bool) {} + + pub fn is_maximized(&self) -> bool { + false } - #[inline] - pub fn hide_cursor(&self, _hide: bool) { - // N/A + pub fn set_fullscreen(&self, _monitor: Option) { + warn!("Cannot set fullscreen on Android"); } - #[inline] - pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn fullscreen(&self) -> Option { + None } - #[inline] - pub fn set_maximized(&self, _maximized: bool) { - // N/A - // Android has single screen maximized apps so nothing to do + pub fn set_decorations(&self, _decorations: bool) {} + + pub fn set_always_on_top(&self, _always_on_top: bool) {} + + pub fn set_window_icon(&self, _window_icon: Option) {} + + pub fn set_ime_position(&self, _position: Position) {} + + pub fn request_user_attention(&self, _request_type: Option) {} + + pub fn set_cursor_icon(&self, _: window::CursorIcon) {} + + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) } - #[inline] - pub fn fullscreen(&self) -> Option { - // N/A - // Android has single screen maximized apps so nothing to do - None + pub fn set_cursor_grab(&self, _: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) } - #[inline] - pub fn set_fullscreen(&self, _monitor: Option) { - // N/A - // Android has single screen maximized apps so nothing to do + pub fn set_cursor_visible(&self, _: bool) {} + + pub fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + let a_native_window = if let Some(native_window) = ndk_glue::native_window().as_ref() { + unsafe { native_window.ptr().as_mut() as *mut _ as *mut _ } + } 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."); + }; + let mut handle = raw_window_handle::android::AndroidHandle::empty(); + handle.a_native_window = a_native_window; + raw_window_handle::RawWindowHandle::Android(handle) } - #[inline] - pub fn set_decorations(&self, _decorations: bool) { - // N/A + pub fn config(&self) -> Configuration { + CONFIG.read().unwrap().clone() } - #[inline] - pub fn set_always_on_top(&self, _always_on_top: bool) { - // N/A + pub fn content_rect(&self) -> Rect { + ndk_glue::content_rect() } +} - #[inline] - pub fn set_window_icon(&self, _icon: Option<::Icon>) { - // N/A +#[derive(Default, Clone, Debug)] +pub struct OsError; + +use std::fmt::{self, Display, Formatter}; +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + write!(fmt, "Android OS Error") } +} - #[inline] - pub fn set_ime_position(&self, _spot: LogicalPosition) { - // N/A +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn name(&self) -> Option { + Some("Android Device".to_owned()) } - #[inline] - pub fn current_monitor(&self) -> RootMonitorHandle { - RootMonitorHandle { - inner: MonitorHandle, + pub fn size(&self) -> PhysicalSize { + if let Some(native_window) = ndk_glue::native_window().as_ref() { + let width = native_window.width() as _; + let height = native_window.height() as _; + PhysicalSize::new(width, height) + } else { + PhysicalSize::new(0, 0) } } - #[inline] - pub fn available_monitors(&self) -> VecDeque { - let mut rb = VecDeque::with_capacity(1); - rb.push_back(MonitorHandle); - rb + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() } - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle + pub fn scale_factor(&self) -> f64 { + let config = CONFIG.read().unwrap(); + config + .density() + .map(|dpi| dpi as f64 / 160.0) + .unwrap_or(1.0) + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + let mut v = Vec::new(); + // FIXME this is not the real refresh rate + // (it is guarunteed to support 32 bit color though) + v.push(monitor::VideoMode { + video_mode: VideoMode { + size, + bit_depth: 32, + refresh_rate: 60, + monitor: self.clone(), + }, + }); + v.into_iter() } +} - #[inline] - pub fn id(&self) -> WindowId { - WindowId +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoMode { + size: (u32, u32), + bit_depth: u16, + refresh_rate: u16, + monitor: MonitorHandle, +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() } - #[inline] - pub fn raw_window_handle(&self) -> RawWindowHandle { - let handle = AndroidHandle { - a_native_window: self.native_window, - ..WindowsHandle::empty() - }; - RawWindowHandle::Android(handle) + pub fn bit_depth(&self) -> u16 { + self.bit_depth } -} -unsafe impl Send for Window {} -unsafe impl Sync for Window {} + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } -// Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); + pub fn monitor(&self) -> monitor::MonitorHandle { + monitor::MonitorHandle { + inner: self.monitor.clone(), + } + } +} diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 6009a26a7e..0d388c509a 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -12,15 +12,16 @@ use std::{ use objc::runtime::{BOOL, YES}; use crate::{ + dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, platform_impl::platform::{ - event_loop::{EventHandler, Never}, + event_loop::{EventHandler, EventProxy, EventWrapper, Never}, ffi::{ id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, - CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSInteger, NSOperatingSystemVersion, - NSUInteger, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CGRect, CGSize, NSInteger, + NSOperatingSystemVersion, NSUInteger, }, }, window::WindowId as RootWindowId, @@ -45,17 +46,13 @@ enum UserCallbackTransitionResult<'a> { processing_redraws: bool, }, ReentrancyPrevented { - queued_events: &'a mut Vec>, + queued_events: &'a mut Vec, }, } -impl Event { +impl Event<'static, Never> { fn is_redraw(&self) -> bool { - if let Event::WindowEvent { - window_id: _, - event: WindowEvent::RedrawRequested, - } = self - { + if let Event::RedrawRequested(_) = self { true } else { false @@ -69,12 +66,12 @@ impl Event { enum AppStateImpl { NotLaunched { queued_windows: Vec, - queued_events: Vec>, + queued_events: Vec, queued_gpu_redraws: HashSet, }, Launching { queued_windows: Vec, - queued_events: Vec>, + queued_events: Vec, queued_event_handler: Box, queued_gpu_redraws: HashSet, }, @@ -85,7 +82,7 @@ enum AppStateImpl { }, // special state to deal with reentrancy and prevent mutable aliasing. InUserCallback { - queued_events: Vec>, + queued_events: Vec, queued_gpu_redraws: HashSet, }, ProcessingRedraws { @@ -226,7 +223,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, @@ -249,7 +246,7 @@ impl AppState { (windows, events) } - fn wakeup_transition(&mut self) -> Option> { + fn wakeup_transition(&mut self) -> Option { // before `AppState::did_finish_launching` is called, pretend there is no running // event loop. if !self.has_launched() { @@ -262,7 +259,10 @@ impl AppState { AppStateImpl::PollFinished { waiting_event_handler, }, - ) => (waiting_event_handler, Event::NewEvents(StartCause::Poll)), + ) => ( + waiting_event_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll)), + ), ( ControlFlow::Wait, AppStateImpl::Waiting { @@ -271,10 +271,10 @@ impl AppState { }, ) => ( waiting_event_handler, - Event::NewEvents(StartCause::WaitCancelled { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { start, requested_resume: None, - }), + })), ), ( ControlFlow::WaitUntil(requested_resume), @@ -284,15 +284,15 @@ impl AppState { }, ) => { let event = if Instant::now() >= requested_resume { - Event::NewEvents(StartCause::ResumeTimeReached { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, - }) + })) } else { - Event::NewEvents(StartCause::WaitCancelled { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { start, requested_resume: Some(requested_resume), - }) + })) }; (waiting_event_handler, event) } @@ -591,7 +591,10 @@ pub unsafe fn did_finish_launching() { let (windows, events) = AppState::get_mut().did_finish_launching_transition(); - let events = std::iter::once(Event::NewEvents(StartCause::Init)).chain(events); + let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents( + StartCause::Init, + ))) + .chain(events); handle_nonuser_events(events); // the above window dance hack, could possibly trigger new windows to be created. @@ -620,12 +623,12 @@ pub unsafe fn handle_wakeup_transition() { } // requires main thread -pub unsafe fn handle_nonuser_event(event: Event) { +pub unsafe fn handle_nonuser_event(event: EventWrapper) { handle_nonuser_events(std::iter::once(event)) } // requires main thread -pub unsafe fn handle_nonuser_events>>(events: I) { +pub unsafe fn handle_nonuser_events>(events: I) { let mut this = AppState::get_mut(); let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { @@ -642,16 +645,23 @@ pub unsafe fn handle_nonuser_events>>(events let mut control_flow = this.control_flow; drop(this); - for event in events { - if !processing_redraws && event.is_redraw() { - log::info!("processing `RedrawRequested` during the main event loop"); - } else if processing_redraws && !event.is_redraw() { - log::warn!( - "processing non `RedrawRequested` event after the main event loop: {:#?}", - event - ); + for wrapper in events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + log::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + log::warn!( + "processing non `RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + EventWrapper::EventProxy(proxy) => { + handle_event_proxy(&mut event_handler, control_flow, proxy) + } } - event_handler.handle_nonuser_event(event, &mut control_flow) } loop { @@ -692,16 +702,23 @@ pub unsafe fn handle_nonuser_events>>(events } drop(this); - for event in queued_events { - if !processing_redraws && event.is_redraw() { - log::info!("processing `RedrawRequested` during the main event loop"); - } else if processing_redraws && !event.is_redraw() { - log::warn!( - "processing non-`RedrawRequested` event after the main event loop: {:#?}", - event - ); + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + log::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + log::warn!( + "processing non-`RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + EventWrapper::EventProxy(proxy) => { + handle_event_proxy(&mut event_handler, control_flow, proxy) + } } - event_handler.handle_nonuser_event(event, &mut control_flow) } } } @@ -755,8 +772,15 @@ unsafe fn handle_user_events() { } drop(this); - for event in queued_events { - event_handler.handle_nonuser_event(event, &mut control_flow) + 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) + } + } } event_handler.handle_user_events(&mut control_flow); } @@ -776,16 +800,20 @@ pub unsafe fn handle_main_events_cleared() { // User events are always sent out at the end of the "MainEventLoop" handle_user_events(); - handle_nonuser_event(Event::EventsCleared); + handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); let mut this = AppState::get_mut(); - let redraw_events = this + let mut redraw_events: Vec = this .main_events_cleared_transition() .into_iter() - .map(|window| Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::RedrawRequested, - }); + .map(|window| { + EventWrapper::StaticEvent(Event::RedrawRequested(RootWindowId(window.into()))) + }) + .collect(); + + if !redraw_events.is_empty() { + redraw_events.push(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); + } drop(this); handle_nonuser_events(redraw_events); @@ -806,6 +834,66 @@ pub unsafe fn terminated() { event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) } +fn handle_event_proxy( + event_handler: &mut Box, + control_flow: ControlFlow, + proxy: EventProxy, +) { + match proxy { + EventProxy::DpiChangedProxy { + suggested_size, + scale_factor, + window_id, + } => handle_hidpi_proxy( + event_handler, + control_flow, + suggested_size, + scale_factor, + window_id, + ), + } +} + +fn handle_hidpi_proxy( + event_handler: &mut Box, + mut control_flow: ControlFlow, + suggested_size: LogicalSize, + scale_factor: f64, + window_id: id, +) { + let mut size = suggested_size.to_physical(scale_factor); + let new_inner_size = &mut size; + let event = Event::WindowEvent { + window_id: RootWindowId(window_id.into()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + }; + event_handler.handle_nonuser_event(event, &mut control_flow); + let (view, screen_frame) = get_view_and_screen_frame(window_id); + let physical_size = *new_inner_size; + let logical_size = physical_size.to_logical(scale_factor); + let size = CGSize::new(logical_size); + let new_frame: CGRect = CGRect::new(screen_frame.origin, size); + unsafe { + let () = msg_send![view, setFrame: new_frame]; + } +} + +fn get_view_and_screen_frame(window_id: id) -> (id, CGRect) { + unsafe { + let view_controller: id = msg_send![window_id, rootViewController]; + let view: id = msg_send![view_controller, view]; + let bounds: CGRect = msg_send![window_id, bounds]; + let screen: id = msg_send![window_id, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![window_id, convertRect:bounds toCoordinateSpace:screen_space]; + (view, screen_frame) + } +} + struct EventLoopWaker { timer: CFRunLoopTimerRef, } diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index b26b1eb646..1680c98a8e 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -8,10 +8,12 @@ use std::{ }; use crate::{ + dpi::LogicalSize, event::Event, event_loop::{ ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootEventLoopWindowTarget, }, + monitor::MonitorHandle as RootMonitorHandle, platform::ios::Idiom, }; @@ -23,16 +25,46 @@ use crate::platform_impl::platform::{ CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, - CFRunLoopSourceSignal, CFRunLoopWakeUp, NSString, UIApplicationMain, UIUserInterfaceIdiom, + CFRunLoopSourceSignal, CFRunLoopWakeUp, NSStringRust, UIApplicationMain, + UIUserInterfaceIdiom, }, monitor, view, MonitorHandle, }; +#[derive(Debug)] +pub enum EventWrapper { + StaticEvent(Event<'static, Never>), + EventProxy(EventProxy), +} + +#[derive(Debug, PartialEq)] +pub enum EventProxy { + DpiChangedProxy { + window_id: id, + suggested_size: LogicalSize, + scale_factor: f64, + }, +} + pub struct EventLoopWindowTarget { receiver: Receiver, sender_to_clone: Sender, } +impl EventLoopWindowTarget { + pub fn available_monitors(&self) -> VecDeque { + // guaranteed to be on main thread + unsafe { monitor::uiscreens() } + } + + pub fn primary_monitor(&self) -> Option { + // guaranteed to be on main thread + let monitor = unsafe { monitor::main_uiscreen() }; + + Some(RootMonitorHandle { inner: monitor }) + } +} + pub struct EventLoop { window_target: RootEventLoopWindowTarget, } @@ -69,7 +101,7 @@ impl EventLoop { pub fn run(self, event_handler: F) -> ! where - F: 'static + FnMut(Event, &RootEventLoopWindowTarget, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), { unsafe { let application: *mut c_void = msg_send![class!(UIApplication), sharedApplication]; @@ -89,7 +121,7 @@ impl EventLoop { 0, ptr::null(), nil, - NSString::alloc(nil).init_str("AppDelegate"), + NSStringRust::alloc(nil).init_str("AppDelegate"), ); unreachable!() } @@ -99,16 +131,6 @@ impl EventLoop { EventLoopProxy::new(self.window_target.p.sender_to_clone.clone()) } - pub fn available_monitors(&self) -> VecDeque { - // guaranteed to be on main thread - unsafe { monitor::uiscreens() } - } - - pub fn primary_monitor(&self) -> MonitorHandle { - // guaranteed to be on main thread - unsafe { monitor::main_uiscreen() } - } - pub fn window_target(&self) -> &RootEventLoopWindowTarget { &self.window_target } @@ -165,8 +187,10 @@ impl EventLoopProxy { } } - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.sender.send(event).map_err(|_| EventLoopClosed)?; + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender + .send(event) + .map_err(|::std::sync::mpsc::SendError(x)| EventLoopClosed(x))?; unsafe { // let the main thread know there's a new event CFRunLoopSourceSignal(self.source); @@ -197,10 +221,10 @@ fn setup_control_flow_observers() { // 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 EventsCleared before RedrawRequested. This value was + // priority to be 0, in order to send MainEventsCleared 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 `EventsCleared`. + // conservative, the main symptom would be non-redraw events coming in after `MainEventsCleared`. // // 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. @@ -275,7 +299,7 @@ fn setup_control_flow_observers() { pub enum Never {} pub trait EventHandler: Debug { - fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow); + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow); fn handle_user_events(&mut self, control_flow: &mut ControlFlow); } @@ -294,10 +318,10 @@ impl Debug for EventLoopHandler { impl EventHandler for EventLoopHandler where - F: 'static + FnMut(Event, &RootEventLoopWindowTarget, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), T: 'static, { - fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow) { + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { (self.f)( event.map_nonuser_event().unwrap(), &self.event_loop, diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 4ecd47f7b2..726723f30b 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -4,7 +4,10 @@ use std::{convert::TryInto, ffi::CString, ops::BitOr, os::raw::*}; use objc::{runtime::Object, Encode, Encoding}; -use crate::platform::ios::{Idiom, ScreenEdge, ValidOrientations}; +use crate::{ + dpi::LogicalSize, + platform::ios::{Idiom, ScreenEdge, ValidOrientations}, +}; pub type id = *mut Object; pub const nil: id = 0 as id; @@ -26,26 +29,41 @@ pub struct NSOperatingSystemVersion { } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct CGPoint { pub x: CGFloat, pub y: CGFloat, } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct CGSize { pub width: CGFloat, pub height: CGFloat, } +impl CGSize { + pub fn new(size: LogicalSize) -> CGSize { + CGSize { + width: size.width as _, + height: size.height as _, + } + } +} + #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct CGRect { pub origin: CGPoint, pub size: CGSize, } +impl CGRect { + pub fn new(origin: CGPoint, size: CGSize) -> CGRect { + CGRect { origin, size } + } +} + unsafe impl Encode for CGRect { fn encode() -> Encoding { unsafe { @@ -341,7 +359,10 @@ pub struct CFRunLoopSourceContext { pub perform: Option, } -pub trait NSString: Sized { +// This is named NSStringRust rather than NSString because the "Debug View Heirarchy" feature of +// Xcode requires a non-ambiguous reference to NSString for unclear reasons. This makes Xcode happy +// so please test if you change the name back to NSString. +pub trait NSStringRust: Sized { unsafe fn alloc(_: Self) -> id { msg_send![class!(NSString), alloc] } @@ -352,7 +373,7 @@ pub trait NSString: Sized { unsafe fn UTF8String(self) -> *const c_char; } -impl NSString for id { +impl NSStringRust for id { unsafe fn initWithUTF8String_(self, c_string: *const c_char) -> id { msg_send![self, initWithUTF8String: c_string as id] } diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index 3141dea4cc..da7b018572 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -83,6 +83,8 @@ pub use self::{ window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId { uiscreen: ffi::id, diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 14279f6643..42c71ffc7a 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -18,31 +18,44 @@ pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, pub(crate) refresh_rate: u16, - pub(crate) screen_mode: id, + pub(crate) screen_mode: NativeDisplayMode, pub(crate) monitor: MonitorHandle, } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct NativeDisplayMode(pub id); + +unsafe impl Send for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + let () = msg_send![self.0, release]; + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + let _: id = msg_send![self.0, retain]; + } + NativeDisplayMode(self.0) + } +} + impl Clone for VideoMode { fn clone(&self) -> VideoMode { VideoMode { size: self.size, bit_depth: self.bit_depth, refresh_rate: self.refresh_rate, - screen_mode: unsafe { msg_send![self.screen_mode, retain] }, + screen_mode: self.screen_mode.clone(), monitor: self.monitor.clone(), } } } -impl Drop for VideoMode { - fn drop(&mut self) { - unsafe { - assert_main_thread!("`VideoMode` can only be dropped on the main thread on iOS"); - let () = msg_send![self.screen_mode, release]; - } - } -} - impl VideoMode { unsafe fn retained_new(uiscreen: id, screen_mode: id) -> VideoMode { assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); @@ -64,16 +77,18 @@ impl VideoMode { 60 }; let size: CGSize = msg_send![screen_mode, size]; + let screen_mode: id = msg_send![screen_mode, retain]; + let screen_mode = NativeDisplayMode(screen_mode); VideoMode { size: (size.width as u32, size.height as u32), bit_depth: 32, refresh_rate: refresh_rate as u16, - screen_mode: msg_send![screen_mode, retain], + screen_mode, monitor: MonitorHandle::retained_new(uiscreen), } } - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.size.into() } @@ -156,16 +171,16 @@ impl fmt::Debug for MonitorHandle { #[derive(Debug)] struct MonitorHandle { name: Option, - size: PhysicalSize, - position: PhysicalPosition, - hidpi_factor: f64, + size: PhysicalSize, + position: PhysicalPosition, + scale_factor: f64, } let monitor_id_proxy = MonitorHandle { name: self.name(), size: self.size(), position: self.position(), - hidpi_factor: self.hidpi_factor(), + scale_factor: self.scale_factor(), }; monitor_id_proxy.fmt(f) @@ -201,21 +216,21 @@ impl Inner { } } - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { unsafe { let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; - (bounds.size.width as f64, bounds.size.height as f64).into() + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) } } - pub fn position(&self) -> PhysicalPosition { + pub fn position(&self) -> PhysicalPosition { unsafe { let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; (bounds.origin.x as f64, bounds.origin.y as f64).into() } } - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { unsafe { let scale: CGFloat = msg_send![self.ui_screen(), nativeScale]; scale as f64 diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index 5868059b47..ccdec6bfbf 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -6,11 +6,12 @@ use objc::{ }; use crate::{ + dpi::PhysicalPosition, event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, platform::ios::MonitorHandleExtIOS, platform_impl::platform::{ app_state::{self, OSCapabilities}, - event_loop, + event_loop::{self, EventProxy, EventWrapper}, ffi::{ id, nil, CGFloat, CGPoint, CGRect, UIForceTouchCapability, UIInterfaceOrientationMask, UIRectEdge, UITouchPhase, UITouchType, @@ -102,10 +103,14 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { unsafe { let window: id = msg_send![object, window]; assert!(!window.is_null()); - app_state::handle_nonuser_event(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::RedrawRequested, - }); + app_state::handle_nonuser_events( + std::iter::once(EventWrapper::StaticEvent(Event::RedrawRequested( + RootWindowId(window.into()), + ))) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::RedrawEventsCleared, + ))), + ); let superclass: &'static Class = msg_send![object, superclass]; let () = msg_send![super(object, superclass), drawRect: rect]; } @@ -118,32 +123,43 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { let window: id = msg_send![object, window]; assert!(!window.is_null()); - let bounds: CGRect = msg_send![window, bounds]; + let window_bounds: CGRect = msg_send![window, bounds]; let screen: id = msg_send![window, screen]; let screen_space: id = msg_send![screen, coordinateSpace]; let screen_frame: CGRect = - msg_send![object, convertRect:bounds toCoordinateSpace:screen_space]; + msg_send![object, convertRect:window_bounds toCoordinateSpace:screen_space]; + let scale_factor: CGFloat = msg_send![screen, scale]; let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, - }; - app_state::handle_nonuser_event(Event::WindowEvent { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + } + .to_physical(scale_factor.into()); + + // If the app is started in landscape, the view frame and window bounds can be mismatched. + // The view frame will be in portrait and the window bounds in landscape. So apply the + // window bounds to the view frame to make it consistent. + let view_frame: CGRect = msg_send![object, frame]; + if view_frame != window_bounds { + let () = msg_send![object, setFrame: window_bounds]; + } + + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.into()), event: WindowEvent::Resized(size), - }); + })); } } extern "C" fn set_content_scale_factor( object: &mut Object, _: Sel, - untrusted_hidpi_factor: CGFloat, + untrusted_scale_factor: CGFloat, ) { unsafe { let superclass: &'static Class = msg_send![object, superclass]; let () = msg_send![ super(object, superclass), - setContentScaleFactor: untrusted_hidpi_factor + setContentScaleFactor: untrusted_scale_factor ]; let window: id = msg_send![object, window]; @@ -156,14 +172,15 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { // `setContentScaleFactor` may be called with a value of 0, which means "reset the // content scale factor to a device-specific default value", so we can't use the // parameter here. We can query the actual factor using the getter - let hidpi_factor: CGFloat = msg_send![object, contentScaleFactor]; + let scale_factor: CGFloat = msg_send![object, contentScaleFactor]; assert!( - !hidpi_factor.is_nan() - && hidpi_factor.is_finite() - && hidpi_factor.is_sign_positive() - && hidpi_factor > 0.0, - "invalid hidpi_factor set on UIView", + !scale_factor.is_nan() + && scale_factor.is_finite() + && scale_factor.is_sign_positive() + && scale_factor > 0.0, + "invalid scale_factor set on UIView", ); + let scale_factor: f64 = scale_factor.into(); let bounds: CGRect = msg_send![object, bounds]; let screen: id = msg_send![window, screen]; let screen_space: id = msg_send![screen, coordinateSpace]; @@ -174,14 +191,17 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { height: screen_frame.size.height as _, }; app_state::handle_nonuser_events( - std::iter::once(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::HiDpiFactorChanged(hidpi_factor as _), - }) - .chain(std::iter::once(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::Resized(size), - })), + std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + window_id: window, + scale_factor, + suggested_size: size, + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), ); } } @@ -199,7 +219,7 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { if touch == nil { break; } - let location: CGPoint = msg_send![touch, locationInView: nil]; + let logical_location: CGPoint = msg_send![touch, locationInView: nil]; let touch_type: UITouchType = msg_send![touch, type]; let force = if os_supports_force { let trait_collection: id = msg_send![object, traitCollection]; @@ -238,16 +258,23 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { _ => panic!("unexpected touch phase: {:?}", phase as i32), }; - touch_events.push(Event::WindowEvent { + let physical_location = { + let scale_factor: CGFloat = msg_send![object, contentScaleFactor]; + PhysicalPosition::from_logical::<(f64, f64), f64>( + (logical_location.x as _, logical_location.y as _), + scale_factor, + ) + }; + touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.into()), event: WindowEvent::Touch(Touch { device_id: RootDeviceId(DeviceId { uiscreen }), id: touch_id, - location: (location.x as f64, location.y as f64).into(), + location: physical_location, force, phase, }), - }); + })); } app_state::handle_nonuser_events(touch_events); } @@ -367,20 +394,20 @@ unsafe fn get_window_class() -> &'static Class { extern "C" fn become_key_window(object: &Object, _: Sel) { unsafe { - app_state::handle_nonuser_event(Event::WindowEvent { + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(object.into()), event: WindowEvent::Focused(true), - }); + })); let () = msg_send![super(object, class!(UIWindow)), becomeKeyWindow]; } } extern "C" fn resign_key_window(object: &Object, _: Sel) { unsafe { - app_state::handle_nonuser_event(Event::WindowEvent { + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(object.into()), event: WindowEvent::Focused(false), - }); + })); let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; } } @@ -414,8 +441,8 @@ pub unsafe fn create_view( let view: id = msg_send![view, initWithFrame: frame]; assert!(!view.is_null(), "Failed to initialize `UIView` instance"); let () = msg_send![view, setMultipleTouchEnabled: YES]; - if let Some(hidpi_factor) = platform_attributes.hidpi_factor { - let () = msg_send![view, setContentScaleFactor: hidpi_factor as CGFloat]; + if let Some(scale_factor) = platform_attributes.scale_factor { + let () = msg_send![view, setContentScaleFactor: scale_factor as CGFloat]; } view @@ -497,11 +524,19 @@ pub unsafe fn create_window( match window_attributes.fullscreen { Some(Fullscreen::Exclusive(ref video_mode)) => { let uiscreen = video_mode.monitor().ui_screen() as id; - let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode]; + let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; msg_send![window, setScreen:video_mode.monitor().ui_screen()] } Some(Fullscreen::Borderless(ref monitor)) => { - msg_send![window, setScreen:monitor.ui_screen()] + let uiscreen: id = match &monitor { + Some(monitor) => monitor.ui_screen() as id, + None => { + let uiscreen: id = msg_send![window, screen]; + uiscreen + } + }; + + msg_send![window, setScreen: uiscreen] } None => (), } @@ -518,11 +553,11 @@ pub fn create_delegate_class() { } extern "C" fn did_become_active(_: &Object, _: Sel, _: id) { - unsafe { app_state::handle_nonuser_event(Event::Resumed) } + unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed)) } } extern "C" fn will_resign_active(_: &Object, _: Sel, _: id) { - unsafe { app_state::handle_nonuser_event(Event::Suspended) } + unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Suspended)) } } extern "C" fn will_enter_foreground(_: &Object, _: Sel, _: id) {} @@ -541,10 +576,10 @@ pub fn create_delegate_class() { } let is_winit_window: BOOL = msg_send![window, isKindOfClass: class!(WinitUIWindow)]; if is_winit_window == YES { - events.push(Event::WindowEvent { + events.push(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.into()), event: WindowEvent::Destroyed, - }); + })); } } app_state::handle_nonuser_events(events); diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index eea0c304e2..7a3665a2b9 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -7,21 +7,24 @@ use std::{ use objc::runtime::{Class, Object, BOOL, NO, YES}; use crate::{ - dpi::{self, LogicalPosition, LogicalSize}, + dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::{Event, WindowEvent}, icon::Icon, monitor::MonitorHandle as RootMonitorHandle, platform::ios::{MonitorHandleExtIOS, ScreenEdge, ValidOrientations}, platform_impl::platform::{ - app_state, event_loop, + app_state, + event_loop::{self, EventProxy, EventWrapper}, ffi::{ id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, UIRectEdge, UIScreenOverscanCompensation, }, monitor, view, EventLoopWindowTarget, MonitorHandle, }, - window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId}, + window::{ + CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + }, }; pub struct Inner { @@ -75,28 +78,34 @@ impl Inner { } } - pub fn inner_position(&self) -> Result { + pub fn inner_position(&self) -> Result, NotSupportedError> { unsafe { let safe_area = self.safe_area_screen_space(); - Ok(LogicalPosition { - x: safe_area.origin.x as _, - y: safe_area.origin.y as _, - }) + 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 { + pub fn outer_position(&self) -> Result, NotSupportedError> { unsafe { let screen_frame = self.screen_frame(); - Ok(LogicalPosition { - x: screen_frame.origin.x as _, - y: screen_frame.origin.y as _, - }) + 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, position: LogicalPosition) { + 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 { @@ -110,35 +119,39 @@ impl Inner { } } - pub fn inner_size(&self) -> LogicalSize { + pub fn inner_size(&self) -> PhysicalSize { unsafe { + let scale_factor = self.scale_factor(); let safe_area = self.safe_area_screen_space(); - LogicalSize { - width: safe_area.size.width as _, - height: safe_area.size.height as _, - } + 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) -> LogicalSize { + pub fn outer_size(&self) -> PhysicalSize { unsafe { + let scale_factor = self.scale_factor(); let screen_frame = self.screen_frame(); - LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, - } + 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: LogicalSize) { - unimplemented!("not clear what `Window::set_inner_size` means on iOS"); + pub fn set_inner_size(&self, _size: Size) { + warn!("not clear what `Window::set_inner_size` means on iOS"); } - pub fn set_min_inner_size(&self, _dimensions: Option) { + pub fn set_min_inner_size(&self, _dimensions: Option) { warn!("`Window::set_min_inner_size` is ignored on iOS") } - pub fn set_max_inner_size(&self, _dimensions: Option) { + pub fn set_max_inner_size(&self, _dimensions: Option) { warn!("`Window::set_max_inner_size` is ignored on iOS") } @@ -146,7 +159,7 @@ impl Inner { warn!("`Window::set_resizable` is ignored on iOS") } - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { unsafe { let hidpi: CGFloat = msg_send![self.view, contentScaleFactor]; hidpi as _ @@ -157,7 +170,7 @@ impl Inner { debug!("`Window::set_cursor_icon` ignored on iOS") } - pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), ExternalError> { + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } @@ -169,10 +182,19 @@ impl Inner { debug!("`Window::set_cursor_visible` is ignored on iOS") } + pub fn set_minimized(&self, _minimized: bool) { + warn!("`Window::set_minimized` is ignored on iOS") + } + pub fn set_maximized(&self, _maximized: bool) { warn!("`Window::set_maximized` is ignored on iOS") } + pub fn is_maximized(&self) -> bool { + warn!("`Window::is_maximized` is ignored on iOS"); + false + } + pub fn set_fullscreen(&self, monitor: Option) { unsafe { let uiscreen = match monitor { @@ -181,7 +203,9 @@ impl Inner { let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode]; uiscreen } - Some(Fullscreen::Borderless(monitor)) => monitor.ui_screen() as id, + Some(Fullscreen::Borderless(monitor)) => monitor + .unwrap_or_else(|| self.current_monitor_inner()) + .ui_screen() as id, None => { warn!("`Window::set_fullscreen(None)` ignored on iOS"); return; @@ -209,7 +233,7 @@ impl Inner { pub fn fullscreen(&self) -> Option { unsafe { - let monitor = self.current_monitor(); + let monitor = self.current_monitor_inner(); let uiscreen = monitor.inner.ui_screen(); let screen_space_bounds = self.screen_frame(); let screen_bounds: CGRect = msg_send![uiscreen, bounds]; @@ -220,7 +244,7 @@ impl Inner { && screen_space_bounds.size.width == screen_bounds.size.width && screen_space_bounds.size.height == screen_bounds.size.height { - Some(Fullscreen::Borderless(monitor)) + Some(Fullscreen::Borderless(Some(monitor))) } else { None } @@ -239,11 +263,16 @@ impl Inner { warn!("`Window::set_window_icon` is ignored on iOS") } - pub fn set_ime_position(&self, _position: LogicalPosition) { + pub fn set_ime_position(&self, _position: Position) { warn!("`Window::set_ime_position` is ignored on iOS") } - pub fn current_monitor(&self) -> RootMonitorHandle { + pub fn request_user_attention(&self, _request_type: Option) { + warn!("`Window::request_user_attention` is ignored on iOS") + } + + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> RootMonitorHandle { unsafe { let uiscreen: id = msg_send![self.window, screen]; RootMonitorHandle { @@ -252,12 +281,17 @@ impl Inner { } } + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + pub fn available_monitors(&self) -> VecDeque { unsafe { monitor::uiscreens() } } - pub fn primary_monitor(&self) -> MonitorHandle { - unsafe { monitor::main_uiscreen() } + pub fn primary_monitor(&self) -> Option { + let monitor = unsafe { monitor::main_uiscreen() }; + Some(RootMonitorHandle { inner: monitor }) } pub fn id(&self) -> WindowId { @@ -332,20 +366,26 @@ impl Window { Some(Fullscreen::Exclusive(ref video_mode)) => { video_mode.video_mode.monitor.ui_screen() as id } - Some(Fullscreen::Borderless(ref monitor)) => monitor.ui_screen() as id, - None => monitor::main_uiscreen().ui_screen(), + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.inner.ui_screen(), + Some(Fullscreen::Borderless(None)) | None => { + monitor::main_uiscreen().ui_screen() as id + } }; let screen_bounds: CGRect = msg_send![screen, bounds]; let frame = match window_attributes.inner_size { - Some(dim) => CGRect { - origin: screen_bounds.origin, - size: CGSize { - width: dim.width as _, - height: dim.height as _, - }, - }, + Some(dim) => { + let scale_factor = msg_send![screen, scale]; + let size = dim.to_logical::(scale_factor); + CGRect { + origin: screen_bounds.origin, + size: CGSize { + width: size.width as _, + height: size.height as _, + }, + } + } None => screen_bounds, }; @@ -379,10 +419,11 @@ impl Window { }; app_state::set_key_window(window); - // Like the Windows and macOS backends, we send a `HiDpiFactorChanged` and `Resized` + // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` // event on window creation if the DPI factor != 1.0 - let hidpi_factor: CGFloat = msg_send![view, contentScaleFactor]; - if hidpi_factor != 1.0 { + let scale_factor: CGFloat = msg_send![view, contentScaleFactor]; + let scale_factor: f64 = scale_factor.into(); + if scale_factor != 1.0 { let bounds: CGRect = msg_send![view, bounds]; let screen: id = msg_send![window, screen]; let screen_space: id = msg_send![screen, coordinateSpace]; @@ -393,14 +434,17 @@ impl Window { height: screen_frame.size.height as _, }; app_state::handle_nonuser_events( - std::iter::once(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::HiDpiFactorChanged(hidpi_factor as _), - }) - .chain(std::iter::once(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::Resized(size), - })), + std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + window_id: window, + scale_factor, + suggested_size: size, + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), ); } @@ -421,14 +465,14 @@ impl Inner { self.view } - pub fn set_hidpi_factor(&self, hidpi_factor: f64) { + pub fn set_scale_factor(&self, scale_factor: f64) { unsafe { assert!( - dpi::validate_hidpi_factor(hidpi_factor), - "`WindowExtIOS::set_hidpi_factor` received an invalid hidpi factor" + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" ); - let hidpi_factor = hidpi_factor as CGFloat; - let () = msg_send![self.view, setContentScaleFactor: hidpi_factor]; + let scale_factor = scale_factor as CGFloat; + let () = msg_send![self.view, setContentScaleFactor: scale_factor]; } } @@ -594,7 +638,7 @@ impl From for WindowId { #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub root_view_class: &'static Class, - pub hidpi_factor: Option, + pub scale_factor: Option, pub valid_orientations: ValidOrientations, pub prefers_home_indicator_hidden: bool, pub prefers_status_bar_hidden: bool, @@ -605,7 +649,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> PlatformSpecificWindowBuilderAttributes { PlatformSpecificWindowBuilderAttributes { root_view_class: class!(UIView), - hidpi_factor: None, + scale_factor: None, valid_orientations: Default::default(), prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 3e56b82498..e845b83be3 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -1,26 +1,43 @@ -#![cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] - -use std::{collections::VecDeque, env, ffi::CStr, fmt, mem::MaybeUninit, os::raw::*, sync::Arc}; - +#![cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +#[cfg(all(not(feature = "x11"), not(feature = "wayland")))] +compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); + +#[cfg(feature = "wayland")] +use std::error::Error; +use std::{collections::VecDeque, env, fmt}; +#[cfg(feature = "x11")] +use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Arc}; + +#[cfg(feature = "x11")] use parking_lot::Mutex; use raw_window_handle::RawWindowHandle; -use smithay_client_toolkit::reexports::client::ConnectError; +#[cfg(feature = "x11")] pub use self::x11::XNotSupported; -use self::x11::{ - ffi::XVisualInfo, get_xtarget, util::WindowType as XWindowType, XConnection, XError, -}; +#[cfg(feature = "x11")] +use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, XError}; use crate::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::Event, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - window::{CursorIcon, Fullscreen, WindowAttributes}, + window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, }; +pub(crate) use crate::icon::RgbaIcon as PlatformIcon; + +#[cfg(feature = "wayland")] pub mod wayland; +#[cfg(feature = "x11")] pub mod x11; /// Environment variable specifying which backend should be used on unix platform. @@ -34,175 +51,216 @@ const BACKEND_PREFERENCE_ENV_VAR: &str = "WINIT_UNIX_BACKEND"; #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { + #[cfg(feature = "x11")] pub visual_infos: Option, + #[cfg(feature = "x11")] pub screen_id: Option, - pub resize_increments: Option<(u32, u32)>, - pub base_size: Option<(u32, u32)>, + #[cfg(feature = "x11")] + pub resize_increments: Option, + #[cfg(feature = "x11")] + pub base_size: Option, + #[cfg(feature = "x11")] pub class: Option<(String, String)>, + #[cfg(feature = "x11")] pub override_redirect: bool, + #[cfg(feature = "x11")] pub x11_window_types: Vec, + #[cfg(feature = "x11")] pub gtk_theme_variant: Option, + #[cfg(feature = "wayland")] pub app_id: Option, } impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { + #[cfg(feature = "x11")] visual_infos: None, + #[cfg(feature = "x11")] screen_id: None, + #[cfg(feature = "x11")] resize_increments: None, + #[cfg(feature = "x11")] base_size: None, + #[cfg(feature = "x11")] class: None, + #[cfg(feature = "x11")] override_redirect: false, + #[cfg(feature = "x11")] x11_window_types: vec![XWindowType::Normal], + #[cfg(feature = "x11")] gtk_theme_variant: None, + #[cfg(feature = "wayland")] app_id: None, } } } +#[cfg(feature = "x11")] lazy_static! { pub static ref X11_BACKEND: Mutex, XNotSupported>> = - { Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new)) }; + Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new)); } #[derive(Debug, Clone)] pub enum OsError { + #[cfg(feature = "x11")] XError(XError), + #[cfg(feature = "x11")] XMisc(&'static str), + #[cfg(feature = "wayland")] + WaylandMisc(&'static str), } impl fmt::Display for OsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - OsError::XError(e) => f.pad(&e.description), - OsError::XMisc(e) => f.pad(e), + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match *self { + #[cfg(feature = "x11")] + OsError::XError(ref e) => _f.pad(&e.description), + #[cfg(feature = "x11")] + OsError::XMisc(ref e) => _f.pad(e), + #[cfg(feature = "wayland")] + OsError::WaylandMisc(ref e) => _f.pad(e), } } } pub enum Window { + #[cfg(feature = "x11")] X(x11::Window), + #[cfg(feature = "wayland")] Wayland(wayland::Window), } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WindowId { + #[cfg(feature = "x11")] X(x11::WindowId), + #[cfg(feature = "wayland")] Wayland(wayland::WindowId), } impl WindowId { pub unsafe fn dummy() -> Self { - WindowId::Wayland(wayland::WindowId::dummy()) + #[cfg(feature = "wayland")] + return WindowId::Wayland(wayland::WindowId::dummy()); + #[cfg(all(not(feature = "wayland"), feature = "x11"))] + return WindowId::X(x11::WindowId::dummy()); } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum DeviceId { + #[cfg(feature = "x11")] X(x11::DeviceId), + #[cfg(feature = "wayland")] Wayland(wayland::DeviceId), } impl DeviceId { pub unsafe fn dummy() -> Self { - DeviceId::Wayland(wayland::DeviceId::dummy()) + #[cfg(feature = "wayland")] + return DeviceId::Wayland(wayland::DeviceId::dummy()); + #[cfg(all(not(feature = "wayland"), feature = "x11"))] + return DeviceId::X(x11::DeviceId::dummy()); } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum MonitorHandle { + #[cfg(feature = "x11")] X(x11::MonitorHandle), + #[cfg(feature = "wayland")] Wayland(wayland::MonitorHandle), } +/// `x11_or_wayland!(match expr; Enum(foo) => foo.something())` +/// expands to the equivalent of +/// ```ignore +/// match self { +/// Enum::X(foo) => foo.something(), +/// Enum::Wayland(foo) => foo.something(), +/// } +/// ``` +/// The result can be converted to another enum by adding `; as AnotherEnum` +macro_rules! x11_or_wayland { + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr; as $enum2:ident ) => { + match $what { + #[cfg(feature = "x11")] + $enum::X($($c1)*) => $enum2::X($x), + #[cfg(feature = "wayland")] + $enum::Wayland($($c1)*) => $enum2::Wayland($x), + } + }; + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr) => { + match $what { + #[cfg(feature = "x11")] + $enum::X($($c1)*) => $x, + #[cfg(feature = "wayland")] + $enum::Wayland($($c1)*) => $x, + } + }; +} + impl MonitorHandle { #[inline] pub fn name(&self) -> Option { - match self { - &MonitorHandle::X(ref m) => m.name(), - &MonitorHandle::Wayland(ref m) => m.name(), - } + x11_or_wayland!(match self; MonitorHandle(m) => m.name()) } #[inline] pub fn native_identifier(&self) -> u32 { - match self { - &MonitorHandle::X(ref m) => m.native_identifier(), - &MonitorHandle::Wayland(ref m) => m.native_identifier(), - } + x11_or_wayland!(match self; MonitorHandle(m) => m.native_identifier()) } #[inline] - pub fn size(&self) -> PhysicalSize { - match self { - &MonitorHandle::X(ref m) => m.size(), - &MonitorHandle::Wayland(ref m) => m.size(), - } + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; MonitorHandle(m) => m.size()) } #[inline] - pub fn position(&self) -> PhysicalPosition { - match self { - &MonitorHandle::X(ref m) => m.position(), - &MonitorHandle::Wayland(ref m) => m.position(), - } + pub fn position(&self) -> PhysicalPosition { + x11_or_wayland!(match self; MonitorHandle(m) => m.position()) } #[inline] - pub fn hidpi_factor(&self) -> f64 { - match self { - &MonitorHandle::X(ref m) => m.hidpi_factor(), - &MonitorHandle::Wayland(ref m) => m.hidpi_factor() as f64, - } + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; MonitorHandle(m) => m.scale_factor() as f64) } #[inline] pub fn video_modes(&self) -> Box> { - match self { - MonitorHandle::X(m) => Box::new(m.video_modes()), - MonitorHandle::Wayland(m) => Box::new(m.video_modes()), - } + x11_or_wayland!(match self; MonitorHandle(m) => Box::new(m.video_modes())) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum VideoMode { + #[cfg(feature = "x11")] X(x11::VideoMode), + #[cfg(feature = "wayland")] Wayland(wayland::VideoMode), } impl VideoMode { #[inline] - pub fn size(&self) -> PhysicalSize { - match self { - &VideoMode::X(ref m) => m.size(), - &VideoMode::Wayland(ref m) => m.size(), - } + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; VideoMode(m) => m.size()) } #[inline] pub fn bit_depth(&self) -> u16 { - match self { - &VideoMode::X(ref m) => m.bit_depth(), - &VideoMode::Wayland(ref m) => m.bit_depth(), - } + x11_or_wayland!(match self; VideoMode(m) => m.bit_depth()) } #[inline] pub fn refresh_rate(&self) -> u16 { - match self { - &VideoMode::X(ref m) => m.refresh_rate(), - &VideoMode::Wayland(ref m) => m.refresh_rate(), - } + x11_or_wayland!(match self; VideoMode(m) => m.refresh_rate()) } #[inline] pub fn monitor(&self) -> RootMonitorHandle { - match self { - &VideoMode::X(ref m) => m.monitor(), - &VideoMode::Wayland(ref m) => m.monitor(), - } + x11_or_wayland!(match self; VideoMode(m) => m.monitor()) } } @@ -214,9 +272,11 @@ impl Window { pl_attribs: PlatformSpecificWindowBuilderAttributes, ) -> Result { match *window_target { + #[cfg(feature = "wayland")] EventLoopWindowTarget::Wayland(ref window_target) => { wayland::Window::new(window_target, attribs, pl_attribs).map(Window::Wayland) } + #[cfg(feature = "x11")] EventLoopWindowTarget::X(ref window_target) => { x11::Window::new(window_target, attribs, pl_attribs).map(Window::X) } @@ -225,224 +285,190 @@ impl Window { #[inline] pub fn id(&self) -> WindowId { - match self { - &Window::X(ref w) => WindowId::X(w.id()), - &Window::Wayland(ref w) => WindowId::Wayland(w.id()), - } + x11_or_wayland!(match self; Window(w) => w.id(); as WindowId) } #[inline] pub fn set_title(&self, title: &str) { - match self { - &Window::X(ref w) => w.set_title(title), - &Window::Wayland(ref w) => w.set_title(title), - } + x11_or_wayland!(match self; Window(w) => w.set_title(title)); } #[inline] pub fn set_visible(&self, visible: bool) { - match self { - &Window::X(ref w) => w.set_visible(visible), - &Window::Wayland(ref w) => w.set_visible(visible), - } + x11_or_wayland!(match self; Window(w) => w.set_visible(visible)) } #[inline] - pub fn outer_position(&self) -> Result { - match self { - &Window::X(ref w) => w.outer_position(), - &Window::Wayland(ref w) => w.outer_position(), - } + pub fn outer_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.outer_position()) } #[inline] - pub fn inner_position(&self) -> Result { - match self { - &Window::X(ref m) => m.inner_position(), - &Window::Wayland(ref m) => m.inner_position(), - } + pub fn inner_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.inner_position()) } #[inline] - pub fn set_outer_position(&self, position: LogicalPosition) { - match self { - &Window::X(ref w) => w.set_outer_position(position), - &Window::Wayland(ref w) => w.set_outer_position(position), - } + pub fn set_outer_position(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.set_outer_position(position)) } #[inline] - pub fn inner_size(&self) -> LogicalSize { - match self { - &Window::X(ref w) => w.inner_size(), - &Window::Wayland(ref w) => w.inner_size(), - } + pub fn inner_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.inner_size()) } #[inline] - pub fn outer_size(&self) -> LogicalSize { - match self { - &Window::X(ref w) => w.outer_size(), - &Window::Wayland(ref w) => w.outer_size(), - } + pub fn outer_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.outer_size()) } #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { - match self { - &Window::X(ref w) => w.set_inner_size(size), - &Window::Wayland(ref w) => w.set_inner_size(size), - } + pub fn set_inner_size(&self, size: Size) { + x11_or_wayland!(match self; Window(w) => w.set_inner_size(size)) } #[inline] - pub fn set_min_inner_size(&self, dimensions: Option) { - match self { - &Window::X(ref w) => w.set_min_inner_size(dimensions), - &Window::Wayland(ref w) => w.set_min_inner_size(dimensions), - } + pub fn set_min_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions)) } #[inline] - pub fn set_max_inner_size(&self, dimensions: Option) { - match self { - &Window::X(ref w) => w.set_max_inner_size(dimensions), - &Window::Wayland(ref w) => w.set_max_inner_size(dimensions), - } + pub fn set_max_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_max_inner_size(dimensions)) } #[inline] pub fn set_resizable(&self, resizable: bool) { - match self { - &Window::X(ref w) => w.set_resizable(resizable), - &Window::Wayland(ref w) => w.set_resizable(resizable), - } + x11_or_wayland!(match self; Window(w) => w.set_resizable(resizable)) } #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - match self { - &Window::X(ref w) => w.set_cursor_icon(cursor), - &Window::Wayland(ref w) => w.set_cursor_icon(cursor), - } + x11_or_wayland!(match self; Window(w) => w.set_cursor_icon(cursor)) } #[inline] pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - match self { - &Window::X(ref window) => window.set_cursor_grab(grab), - &Window::Wayland(ref window) => window.set_cursor_grab(grab), - } + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(grab)) } #[inline] pub fn set_cursor_visible(&self, visible: bool) { - match self { - &Window::X(ref window) => window.set_cursor_visible(visible), - &Window::Wayland(ref window) => window.set_cursor_visible(visible), - } + x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) } #[inline] - pub fn hidpi_factor(&self) -> f64 { - match self { - &Window::X(ref w) => w.hidpi_factor(), - &Window::Wayland(ref w) => w.hidpi_factor() as f64, - } + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; Window(w) => w.scale_factor() as f64) } #[inline] - pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { - match self { - &Window::X(ref w) => w.set_cursor_position(position), - &Window::Wayland(ref w) => w.set_cursor_position(position), - } + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_position(position)) } #[inline] pub fn set_maximized(&self, maximized: bool) { - match self { - &Window::X(ref w) => w.set_maximized(maximized), - &Window::Wayland(ref w) => w.set_maximized(maximized), - } + x11_or_wayland!(match self; Window(w) => w.set_maximized(maximized)) + } + + #[inline] + pub fn is_maximized(&self) -> bool { + // TODO: Not implemented + false + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_minimized(minimized)) } #[inline] pub fn fullscreen(&self) -> Option { - match self { - &Window::X(ref w) => w.fullscreen(), - &Window::Wayland(ref w) => w.fullscreen(), - } + x11_or_wayland!(match self; Window(w) => w.fullscreen()) } #[inline] pub fn set_fullscreen(&self, monitor: Option) { - match self { - &Window::X(ref w) => w.set_fullscreen(monitor), - &Window::Wayland(ref w) => w.set_fullscreen(monitor), - } + x11_or_wayland!(match self; Window(w) => w.set_fullscreen(monitor)) } #[inline] pub fn set_decorations(&self, decorations: bool) { - match self { - &Window::X(ref w) => w.set_decorations(decorations), - &Window::Wayland(ref w) => w.set_decorations(decorations), - } + x11_or_wayland!(match self; Window(w) => w.set_decorations(decorations)) } #[inline] - pub fn set_always_on_top(&self, always_on_top: bool) { + pub fn set_always_on_top(&self, _always_on_top: bool) { match self { - &Window::X(ref w) => w.set_always_on_top(always_on_top), - &Window::Wayland(_) => (), + #[cfg(feature = "x11")] + &Window::X(ref w) => w.set_always_on_top(_always_on_top), + #[cfg(feature = "wayland")] + _ => (), } } #[inline] - pub fn set_window_icon(&self, window_icon: Option) { + pub fn set_window_icon(&self, _window_icon: Option) { match self { - &Window::X(ref w) => w.set_window_icon(window_icon), - &Window::Wayland(_) => (), + #[cfg(feature = "x11")] + &Window::X(ref w) => w.set_window_icon(_window_icon), + #[cfg(feature = "wayland")] + _ => (), } } #[inline] - pub fn set_ime_position(&self, position: LogicalPosition) { + pub fn set_ime_position(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.set_ime_position(position)) + } + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) { match self { - &Window::X(ref w) => w.set_ime_position(position), - &Window::Wayland(_) => (), + #[cfg(feature = "x11")] + &Window::X(ref w) => w.request_user_attention(_request_type), + #[cfg(feature = "wayland")] + _ => (), } } #[inline] pub fn request_redraw(&self) { - match self { - &Window::X(ref w) => w.request_redraw(), - &Window::Wayland(ref w) => w.request_redraw(), - } + x11_or_wayland!(match self; Window(w) => w.request_redraw()) } #[inline] - pub fn current_monitor(&self) -> RootMonitorHandle { + pub fn current_monitor(&self) -> Option { match self { - &Window::X(ref window) => RootMonitorHandle { - inner: MonitorHandle::X(window.current_monitor()), - }, - &Window::Wayland(ref window) => RootMonitorHandle { - inner: MonitorHandle::Wayland(window.current_monitor()), - }, + #[cfg(feature = "x11")] + &Window::X(ref window) => { + let current_monitor = MonitorHandle::X(window.current_monitor()); + Some(RootMonitorHandle { + inner: current_monitor, + }) + } + #[cfg(feature = "wayland")] + &Window::Wayland(ref window) => { + let current_monitor = MonitorHandle::Wayland(window.current_monitor()?); + Some(RootMonitorHandle { + inner: current_monitor, + }) + } } } #[inline] pub fn available_monitors(&self) -> VecDeque { match self { + #[cfg(feature = "x11")] &Window::X(ref window) => window .available_monitors() .into_iter() .map(MonitorHandle::X) .collect(), + #[cfg(feature = "wayland")] &Window::Wayland(ref window) => window .available_monitors() .into_iter() @@ -452,21 +478,31 @@ impl Window { } #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { + pub fn primary_monitor(&self) -> Option { match self { - &Window::X(ref window) => MonitorHandle::X(window.primary_monitor()), - &Window::Wayland(ref window) => MonitorHandle::Wayland(window.primary_monitor()), + #[cfg(feature = "x11")] + &Window::X(ref window) => { + let primary_monitor = MonitorHandle::X(window.primary_monitor()); + Some(RootMonitorHandle { + inner: primary_monitor, + }) + } + #[cfg(feature = "wayland")] + &Window::Wayland(ref window) => window.primary_monitor(), } } pub fn raw_window_handle(&self) -> RawWindowHandle { match self { + #[cfg(feature = "x11")] &Window::X(ref window) => RawWindowHandle::Xlib(window.raw_window_handle()), + #[cfg(feature = "wayland")] &Window::Wayland(ref window) => RawWindowHandle::Wayland(window.raw_window_handle()), } } } +#[cfg(feature = "x11")] unsafe extern "C" fn x_error_callback( display: *mut x11::ffi::Display, event: *mut x11::ffi::XErrorEvent, @@ -500,21 +536,22 @@ unsafe extern "C" fn x_error_callback( } pub enum EventLoop { + #[cfg(feature = "wayland")] Wayland(wayland::EventLoop), + #[cfg(feature = "x11")] X(x11::EventLoop), } pub enum EventLoopProxy { + #[cfg(feature = "x11")] X(x11::EventLoopProxy), + #[cfg(feature = "wayland")] Wayland(wayland::EventLoopProxy), } impl Clone for EventLoopProxy { fn clone(&self) -> Self { - match self { - EventLoopProxy::X(proxy) => EventLoopProxy::X(proxy.clone()), - EventLoopProxy::Wayland(proxy) => EventLoopProxy::Wayland(proxy.clone()), - } + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.clone(); as EventLoopProxy) } } @@ -530,12 +567,18 @@ impl EventLoop { match env_var.as_str() { "x11" => { // TODO: propagate + #[cfg(feature = "x11")] return EventLoop::new_x11_any_thread() .expect("Failed to initialize X11 backend"); + #[cfg(not(feature = "x11"))] + panic!("x11 feature is not enabled") } "wayland" => { + #[cfg(feature = "wayland")] return EventLoop::new_wayland_any_thread() .expect("Failed to initialize Wayland backend"); + #[cfg(not(feature = "wayland"))] + panic!("wayland feature is not enabled"); } _ => panic!( "Unknown environment variable value for {}, try one of `x11`,`wayland`", @@ -544,16 +587,23 @@ impl EventLoop { } } + #[cfg(feature = "wayland")] let wayland_err = match EventLoop::new_wayland_any_thread() { Ok(event_loop) => return event_loop, Err(err) => err, }; + #[cfg(feature = "x11")] let x11_err = match EventLoop::new_x11_any_thread() { Ok(event_loop) => return event_loop, Err(err) => err, }; + #[cfg(not(feature = "wayland"))] + let wayland_err = "backend disabled"; + #[cfg(not(feature = "x11"))] + let x11_err = "backend disabled"; + let err_string = format!( "Failed to initialize any backend! Wayland status: {:?} X11 status: {:?}", wayland_err, x11_err, @@ -561,105 +611,68 @@ impl EventLoop { panic!(err_string); } - pub fn new_wayland() -> Result, ConnectError> { + #[cfg(feature = "wayland")] + pub fn new_wayland() -> Result, Box> { assert_is_main_thread("new_wayland_any_thread"); EventLoop::new_wayland_any_thread() } - pub fn new_wayland_any_thread() -> Result, ConnectError> { + #[cfg(feature = "wayland")] + pub fn new_wayland_any_thread() -> Result, Box> { wayland::EventLoop::new().map(EventLoop::Wayland) } + #[cfg(feature = "x11")] pub fn new_x11() -> Result, XNotSupported> { assert_is_main_thread("new_x11_any_thread"); EventLoop::new_x11_any_thread() } + #[cfg(feature = "x11")] pub fn new_x11_any_thread() -> Result, XNotSupported> { - X11_BACKEND - .lock() - .as_ref() - .map(Arc::clone) - .map(x11::EventLoop::new) - .map(EventLoop::X) - .map_err(|err| err.clone()) - } - - #[inline] - pub fn available_monitors(&self) -> VecDeque { - match *self { - EventLoop::Wayland(ref evlp) => evlp - .available_monitors() - .into_iter() - .map(MonitorHandle::Wayland) - .collect(), - EventLoop::X(ref evlp) => get_xtarget(&evlp.target) - .x_connection() - .available_monitors() - .into_iter() - .map(MonitorHandle::X) - .collect(), - } - } + let xconn = match X11_BACKEND.lock().as_ref() { + Ok(xconn) => xconn.clone(), + Err(err) => return Err(err.clone()), + }; - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - match *self { - EventLoop::Wayland(ref evlp) => MonitorHandle::Wayland(evlp.primary_monitor()), - EventLoop::X(ref evlp) => { - MonitorHandle::X(get_xtarget(&evlp.target).x_connection().primary_monitor()) - } - } + Ok(EventLoop::X(x11::EventLoop::new(xconn))) } pub fn create_proxy(&self) -> EventLoopProxy { - match *self { - EventLoop::Wayland(ref evlp) => EventLoopProxy::Wayland(evlp.create_proxy()), - EventLoop::X(ref evlp) => EventLoopProxy::X(evlp.create_proxy()), - } + x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy) } pub fn run_return(&mut self, callback: F) where - F: FnMut(crate::event::Event, &RootELW, &mut ControlFlow), + F: FnMut(crate::event::Event<'_, T>, &RootELW, &mut ControlFlow), { - match *self { - EventLoop::Wayland(ref mut evlp) => evlp.run_return(callback), - EventLoop::X(ref mut evlp) => evlp.run_return(callback), - } + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_return(callback)) } pub fn run(self, callback: F) -> ! where - F: 'static + FnMut(crate::event::Event, &RootELW, &mut ControlFlow), + F: 'static + FnMut(crate::event::Event<'_, T>, &RootELW, &mut ControlFlow), { - match self { - EventLoop::Wayland(evlp) => evlp.run(callback), - EventLoop::X(evlp) => evlp.run(callback), - } + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run(callback)) } pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget { - match *self { - EventLoop::Wayland(ref evl) => evl.window_target(), - EventLoop::X(ref evl) => evl.window_target(), - } + x11_or_wayland!(match self; EventLoop(evl) => evl.window_target()) } } impl EventLoopProxy { - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - match *self { - EventLoopProxy::Wayland(ref proxy) => proxy.send_event(event), - EventLoopProxy::X(ref proxy) => proxy.send_event(event), - } + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.send_event(event)) } } pub enum EventLoopWindowTarget { + #[cfg(feature = "wayland")] Wayland(wayland::EventLoopWindowTarget), + #[cfg(feature = "x11")] X(x11::EventLoopWindowTarget), } @@ -667,19 +680,55 @@ impl EventLoopWindowTarget { #[inline] pub fn is_wayland(&self) -> bool { match *self { + #[cfg(feature = "wayland")] EventLoopWindowTarget::Wayland(_) => true, - EventLoopWindowTarget::X(_) => false, + #[cfg(feature = "x11")] + _ => false, + } + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match *self { + #[cfg(feature = "wayland")] + EventLoopWindowTarget::Wayland(ref evlp) => evlp + .available_monitors() + .into_iter() + .map(MonitorHandle::Wayland) + .collect(), + #[cfg(feature = "x11")] + EventLoopWindowTarget::X(ref evlp) => evlp + .x_connection() + .available_monitors() + .into_iter() + .map(MonitorHandle::X) + .collect(), + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + match *self { + #[cfg(feature = "wayland")] + EventLoopWindowTarget::Wayland(ref evlp) => evlp.primary_monitor(), + #[cfg(feature = "x11")] + EventLoopWindowTarget::X(ref evlp) => { + let primary_monitor = MonitorHandle::X(evlp.x_connection().primary_monitor()); + Some(RootMonitorHandle { + inner: primary_monitor, + }) + } } } } fn sticky_exit_callback( - evt: Event, + evt: Event<'_, T>, target: &RootELW, control_flow: &mut ControlFlow, callback: &mut F, ) where - F: FnMut(Event, &RootELW, &mut ControlFlow), + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { // make ControlFlow::Exit sticky by providing a dummy // control flow reference if it is already Exit. @@ -720,7 +769,5 @@ fn is_main_thread() -> bool { #[cfg(target_os = "netbsd")] fn is_main_thread() -> bool { - use libc::_lwp_self; - - unsafe { _lwp_self() == 1 } + std::thread::current().name() == Some("main") } diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs new file mode 100644 index 0000000000..1cb2745b59 --- /dev/null +++ b/src/platform_impl/linux/wayland/env.rs @@ -0,0 +1,149 @@ +//! SCTK environment setup. + +use sctk::reexports::client::protocol::wl_compositor::WlCompositor; +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::protocols::unstable::xdg_shell::v6::client::zxdg_shell_v6::ZxdgShellV6; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::protocols::unstable::xdg_decoration::v1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use sctk::reexports::client::protocol::wl_shell::WlShell; +use sctk::reexports::client::protocol::wl_subcompositor::WlSubcompositor; +use sctk::reexports::client::{Attached, DispatchData}; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::protocols::xdg_shell::client::xdg_wm_base::XdgWmBase; +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; + +use sctk::environment::{Environment, SimpleGlobal}; +use sctk::output::{OutputHandler, OutputHandling, OutputInfo, OutputStatusListener}; +use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener}; +use sctk::shell::{Shell, ShellHandler, ShellHandling}; +use sctk::shm::ShmHandler; + +/// Set of extra features that are supported by the compositor. +#[derive(Debug, Clone, Copy)] +pub struct WindowingFeatures { + cursor_grab: bool, +} + +impl WindowingFeatures { + /// Create `WindowingFeatures` based on the presented interfaces. + pub fn new(env: &Environment) -> Self { + let cursor_grab = env.get_global::().is_some(); + Self { cursor_grab } + } + + pub fn cursor_grab(&self) -> bool { + self.cursor_grab + } +} + +sctk::environment!(WinitEnv, + singles = [ + WlShm => shm, + WlCompositor => compositor, + WlSubcompositor => subcompositor, + WlShell => shell, + XdgWmBase => shell, + ZxdgShellV6 => shell, + ZxdgDecorationManagerV1 => decoration_manager, + ZwpRelativePointerManagerV1 => relative_pointer_manager, + ZwpPointerConstraintsV1 => pointer_constraints, + ZwpTextInputManagerV3 => text_input_manager, + ], + multis = [ + WlSeat => seats, + WlOutput => outputs, + ] +); + +/// The environment that we utilize. +pub struct WinitEnv { + seats: SeatHandler, + + outputs: OutputHandler, + + shm: ShmHandler, + + compositor: SimpleGlobal, + + subcompositor: SimpleGlobal, + + shell: ShellHandler, + + relative_pointer_manager: SimpleGlobal, + + pointer_constraints: SimpleGlobal, + + text_input_manager: SimpleGlobal, + + decoration_manager: SimpleGlobal, +} + +impl WinitEnv { + pub fn new() -> Self { + // Output tracking for available_monitors, etc. + let outputs = OutputHandler::new(); + + // Keyboard/Pointer/Touch input. + let seats = SeatHandler::new(); + + // Essential globals. + let shm = ShmHandler::new(); + let compositor = SimpleGlobal::new(); + let subcompositor = SimpleGlobal::new(); + + // Gracefully handle shell picking, since SCTK automatically supports multiple + // backends. + let shell = ShellHandler::new(); + + // Server side decorations. + let decoration_manager = SimpleGlobal::new(); + + // Device events for pointer. + let relative_pointer_manager = SimpleGlobal::new(); + + // Pointer grab functionality. + let pointer_constraints = SimpleGlobal::new(); + + // IME handling. + let text_input_manager = SimpleGlobal::new(); + + Self { + seats, + outputs, + shm, + compositor, + subcompositor, + shell, + decoration_manager, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + } + } +} + +impl ShellHandling for WinitEnv { + fn get_shell(&self) -> Option { + self.shell.get_shell() + } +} + +impl SeatHandling for WinitEnv { + fn listen, &SeatData, DispatchData<'_>) + 'static>( + &mut self, + f: F, + ) -> SeatListener { + self.seats.listen(f) + } +} + +impl OutputHandling for WinitEnv { + fn listen) + 'static>( + &mut self, + f: F, + ) -> OutputStatusListener { + self.outputs.listen(f) + } +} diff --git a/src/platform_impl/linux/wayland/event_loop.rs b/src/platform_impl/linux/wayland/event_loop.rs deleted file mode 100644 index 146dcb373b..0000000000 --- a/src/platform_impl/linux/wayland/event_loop.rs +++ /dev/null @@ -1,1058 +0,0 @@ -use std::{ - cell::RefCell, - collections::VecDeque, - fmt, - rc::Rc, - sync::{Arc, Mutex}, - time::Instant, -}; - -use smithay_client_toolkit::reexports::protocols::unstable::pointer_constraints::v1::client::{ - zwp_locked_pointer_v1::ZwpLockedPointerV1, zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, -}; -use smithay_client_toolkit::reexports::protocols::unstable::relative_pointer::v1::client::{ - zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, - zwp_relative_pointer_v1::ZwpRelativePointerV1, -}; - -use smithay_client_toolkit::pointer::{AutoPointer, AutoThemer}; -use smithay_client_toolkit::reexports::client::protocol::{ - wl_compositor::WlCompositor, wl_shm::WlShm, wl_surface::WlSurface, -}; - -use crate::{ - dpi::{PhysicalPosition, PhysicalSize}, - event::ModifiersState, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, - monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - platform_impl::platform::{ - sticky_exit_callback, MonitorHandle as PlatformMonitorHandle, - VideoMode as PlatformVideoMode, - }, - window::CursorIcon, -}; - -use super::{window::WindowStore, DeviceId, WindowId}; - -use smithay_client_toolkit::{ - output::OutputMgr, - reexports::client::{ - protocol::{wl_keyboard, wl_output, wl_pointer, wl_registry, wl_seat, wl_touch}, - ConnectError, Display, EventQueue, GlobalEvent, - }, - Environment, -}; - -pub struct WindowEventsSink { - buffer: VecDeque>, -} - -impl WindowEventsSink { - pub fn new() -> WindowEventsSink { - WindowEventsSink { - buffer: VecDeque::new(), - } - } - - pub fn send_event(&mut self, evt: crate::event::Event) { - self.buffer.push_back(evt); - } - - pub fn send_window_event(&mut self, evt: crate::event::WindowEvent, wid: WindowId) { - self.buffer.push_back(crate::event::Event::WindowEvent { - event: evt, - window_id: crate::window::WindowId(crate::platform_impl::WindowId::Wayland(wid)), - }); - } - - pub fn send_device_event(&mut self, evt: crate::event::DeviceEvent, dev_id: DeviceId) { - self.buffer.push_back(crate::event::Event::DeviceEvent { - event: evt, - device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(dev_id)), - }); - } - - fn empty_with(&mut self, mut callback: F) - where - F: FnMut(crate::event::Event), - { - for evt in self.buffer.drain(..) { - callback(evt) - } - } -} - -pub struct CursorManager { - pointer_constraints_proxy: Arc>>, - auto_themer: Option, - pointers: Vec, - locked_pointers: Vec, - cursor_visible: bool, - current_cursor: CursorIcon, -} - -impl CursorManager { - fn new(constraints: Arc>>) -> CursorManager { - CursorManager { - pointer_constraints_proxy: constraints, - auto_themer: None, - pointers: Vec::new(), - locked_pointers: Vec::new(), - cursor_visible: true, - current_cursor: CursorIcon::default(), - } - } - - fn register_pointer(&mut self, pointer: wl_pointer::WlPointer) { - let auto_themer = self - .auto_themer - .as_ref() - .expect("AutoThemer not initialized. Server did not advertise shm or compositor?"); - self.pointers.push(auto_themer.theme_pointer(pointer)); - } - - fn set_auto_themer(&mut self, auto_themer: AutoThemer) { - self.auto_themer = Some(auto_themer); - } - - pub fn set_cursor_visible(&mut self, visible: bool) { - if !visible { - for pointer in self.pointers.iter() { - (**pointer).set_cursor(0, None, 0, 0); - } - } else { - self.set_cursor_icon_impl(self.current_cursor); - } - self.cursor_visible = visible; - } - - /// A helper function to restore cursor styles on PtrEvent::Enter. - pub fn reload_cursor_style(&mut self) { - if !self.cursor_visible { - self.set_cursor_visible(false); - } else { - self.set_cursor_icon_impl(self.current_cursor); - } - } - - pub fn set_cursor_icon(&mut self, cursor: CursorIcon) { - if cursor != self.current_cursor { - self.current_cursor = cursor; - if self.cursor_visible { - self.set_cursor_icon_impl(cursor); - } - } - } - - fn set_cursor_icon_impl(&mut self, cursor: CursorIcon) { - let cursor = match cursor { - CursorIcon::Alias => "link", - CursorIcon::Arrow => "arrow", - CursorIcon::Cell => "plus", - CursorIcon::Copy => "copy", - CursorIcon::Crosshair => "crosshair", - CursorIcon::Default => "left_ptr", - CursorIcon::Hand => "hand", - CursorIcon::Help => "question_arrow", - CursorIcon::Move => "move", - CursorIcon::Grab => "grab", - CursorIcon::Grabbing => "grabbing", - CursorIcon::Progress => "progress", - CursorIcon::AllScroll => "all-scroll", - CursorIcon::ContextMenu => "context-menu", - - CursorIcon::NoDrop => "no-drop", - CursorIcon::NotAllowed => "crossed_circle", - - // Resize cursors - CursorIcon::EResize => "right_side", - CursorIcon::NResize => "top_side", - CursorIcon::NeResize => "top_right_corner", - CursorIcon::NwResize => "top_left_corner", - CursorIcon::SResize => "bottom_side", - CursorIcon::SeResize => "bottom_right_corner", - CursorIcon::SwResize => "bottom_left_corner", - CursorIcon::WResize => "left_side", - CursorIcon::EwResize => "h_double_arrow", - CursorIcon::NsResize => "v_double_arrow", - CursorIcon::NwseResize => "bd_double_arrow", - CursorIcon::NeswResize => "fd_double_arrow", - CursorIcon::ColResize => "h_double_arrow", - CursorIcon::RowResize => "v_double_arrow", - - CursorIcon::Text => "text", - CursorIcon::VerticalText => "vertical-text", - - CursorIcon::Wait => "watch", - - CursorIcon::ZoomIn => "zoom-in", - CursorIcon::ZoomOut => "zoom-out", - }; - - for pointer in self.pointers.iter() { - // Ignore erros, since we don't want to fail hard in case we can't find a proper cursor - // in a given theme. - let _ = pointer.set_cursor(cursor, None); - } - } - - // This function can only be called from a thread on which `pointer_constraints_proxy` event - // queue is located, so calling it directly from a Window doesn't work well, in case - // you've sent your window to another thread, so we need to pass cursor grab updates to - // the event loop and call this function from there. - fn grab_pointer(&mut self, surface: Option<&WlSurface>) { - for locked_pointer in self.locked_pointers.drain(..) { - locked_pointer.destroy(); - } - - if let Some(surface) = surface { - for pointer in self.pointers.iter() { - let locked_pointer = self - .pointer_constraints_proxy - .try_lock() - .unwrap() - .as_ref() - .and_then(|pointer_constraints| { - super::pointer::implement_locked_pointer( - surface, - &**pointer, - pointer_constraints, - ) - .ok() - }); - - if let Some(locked_pointer) = locked_pointer { - self.locked_pointers.push(locked_pointer); - } - } - } - } -} - -pub struct EventLoop { - // The loop - inner_loop: ::calloop::EventLoop<()>, - // The wayland display - pub display: Arc, - // The output manager - pub outputs: OutputMgr, - // Our sink, shared with some handlers, buffering the events - sink: Arc>>, - pending_user_events: Rc>>, - // The cursor manager - cursor_manager: Arc>, - // Utility for grabbing the cursor and changing visibility - _user_source: ::calloop::Source<::calloop::channel::Channel>, - user_sender: ::calloop::channel::Sender, - _kbd_source: ::calloop::Source<::calloop::channel::Channel>>, - window_target: RootELW, -} - -// A handle that can be sent across threads and used to wake up the `EventLoop`. -// -// We should only try and wake up the `EventLoop` if it still exists, so we hold Weak ptrs. -pub struct EventLoopProxy { - user_sender: calloop::channel::Sender, -} - -pub struct EventLoopWindowTarget { - // The event queue - pub evq: RefCell<::calloop::Source>, - // The window store - pub store: Arc>, - // The cursor manager - pub cursor_manager: Arc>, - // The env - pub env: Environment, - // A cleanup switch to prune dead windows - pub cleanup_needed: Arc>, - // The wayland display - pub display: Arc, - // The list of seats - pub seats: Arc>>, - _marker: ::std::marker::PhantomData, -} - -impl Clone for EventLoopProxy { - fn clone(&self) -> Self { - EventLoopProxy { - user_sender: self.user_sender.clone(), - } - } -} - -impl EventLoopProxy { - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.user_sender.send(event).map_err(|_| EventLoopClosed) - } -} - -impl EventLoop { - pub fn new() -> Result, ConnectError> { - let (display, mut event_queue) = Display::connect_to_env()?; - - let display = Arc::new(display); - let sink = Arc::new(Mutex::new(WindowEventsSink::new())); - let store = Arc::new(Mutex::new(WindowStore::new())); - let seats = Arc::new(Mutex::new(Vec::new())); - - let inner_loop = ::calloop::EventLoop::new().unwrap(); - - let (kbd_sender, kbd_channel) = ::calloop::channel::channel::>(); - let kbd_sink = sink.clone(); - let kbd_source = inner_loop - .handle() - .insert_source(kbd_channel, move |evt, &mut ()| { - if let ::calloop::channel::Event::Msg(evt) = evt { - let evt = evt.map_nonuser_event().ok().unwrap(); - kbd_sink.lock().unwrap().send_event(evt); - } - }) - .unwrap(); - - let pointer_constraints_proxy = Arc::new(Mutex::new(None)); - - let mut seat_manager = SeatManager { - sink: sink.clone(), - relative_pointer_manager_proxy: Rc::new(RefCell::new(None)), - pointer_constraints_proxy: pointer_constraints_proxy.clone(), - store: store.clone(), - seats: seats.clone(), - kbd_sender, - cursor_manager: Arc::new(Mutex::new(CursorManager::new(pointer_constraints_proxy))), - }; - - let cursor_manager = seat_manager.cursor_manager.clone(); - let cursor_manager_clone = cursor_manager.clone(); - - let shm_cell = Rc::new(RefCell::new(None)); - let compositor_cell = Rc::new(RefCell::new(None)); - - let env = Environment::from_display_with_cb( - &display, - &mut event_queue, - move |event, registry| match event { - GlobalEvent::New { - id, - ref interface, - version, - } => { - if interface == "zwp_relative_pointer_manager_v1" { - let relative_pointer_manager_proxy = registry - .bind(version, id, move |pointer_manager| { - pointer_manager.implement_closure(|_, _| (), ()) - }) - .unwrap(); - - *seat_manager - .relative_pointer_manager_proxy - .try_borrow_mut() - .unwrap() = Some(relative_pointer_manager_proxy); - } - if interface == "zwp_pointer_constraints_v1" { - let pointer_constraints_proxy = registry - .bind(version, id, move |pointer_constraints| { - pointer_constraints.implement_closure(|_, _| (), ()) - }) - .unwrap(); - - *seat_manager.pointer_constraints_proxy.lock().unwrap() = - Some(pointer_constraints_proxy); - } - if interface == "wl_shm" { - let shm: WlShm = registry - .bind(version, id, move |shm| shm.implement_closure(|_, _| (), ())) - .unwrap(); - - (*shm_cell.borrow_mut()) = Some(shm); - } - if interface == "wl_compositor" { - let compositor: WlCompositor = registry - .bind(version, id, move |compositor| { - compositor.implement_closure(|_, _| (), ()) - }) - .unwrap(); - (*compositor_cell.borrow_mut()) = Some(compositor); - } - - if compositor_cell.borrow().is_some() && shm_cell.borrow().is_some() { - let compositor = compositor_cell.borrow_mut().take().unwrap(); - let shm = shm_cell.borrow_mut().take().unwrap(); - let auto_themer = AutoThemer::init(None, compositor, &shm); - cursor_manager_clone - .lock() - .unwrap() - .set_auto_themer(auto_themer); - } - - if interface == "wl_seat" { - seat_manager.add_seat(id, version, registry) - } - } - GlobalEvent::Removed { id, ref interface } => { - if interface == "wl_seat" { - seat_manager.remove_seat(id) - } - } - }, - ) - .unwrap(); - - let source = inner_loop - .handle() - .insert_source(event_queue, |(), &mut ()| {}) - .unwrap(); - - let pending_user_events = Rc::new(RefCell::new(VecDeque::new())); - let pending_user_events2 = pending_user_events.clone(); - - let (user_sender, user_channel) = ::calloop::channel::channel(); - - let user_source = inner_loop - .handle() - .insert_source(user_channel, move |evt, &mut ()| { - if let ::calloop::channel::Event::Msg(msg) = evt { - pending_user_events2.borrow_mut().push_back(msg); - } - }) - .unwrap(); - - let cursor_manager_clone = cursor_manager.clone(); - Ok(EventLoop { - inner_loop, - sink, - pending_user_events, - display: display.clone(), - outputs: env.outputs.clone(), - _user_source: user_source, - user_sender, - cursor_manager, - _kbd_source: kbd_source, - window_target: RootELW { - p: crate::platform_impl::EventLoopWindowTarget::Wayland(EventLoopWindowTarget { - evq: RefCell::new(source), - store, - env, - cursor_manager: cursor_manager_clone, - cleanup_needed: Arc::new(Mutex::new(false)), - seats, - display, - _marker: ::std::marker::PhantomData, - }), - _marker: ::std::marker::PhantomData, - }, - }) - } - - pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy { - user_sender: self.user_sender.clone(), - } - } - - pub fn run(mut self, callback: F) -> ! - where - F: 'static + FnMut(crate::event::Event, &RootELW, &mut ControlFlow), - { - self.run_return(callback); - std::process::exit(0); - } - - pub fn run_return(&mut self, mut callback: F) - where - F: FnMut(crate::event::Event, &RootELW, &mut ControlFlow), - { - // send pending events to the server - self.display.flush().expect("Wayland connection lost."); - - let mut control_flow = ControlFlow::default(); - - let sink = self.sink.clone(); - let user_events = self.pending_user_events.clone(); - - callback( - crate::event::Event::NewEvents(crate::event::StartCause::Init), - &self.window_target, - &mut control_flow, - ); - - loop { - self.post_dispatch_triggers(); - - // empty buffer of events - { - let mut guard = sink.lock().unwrap(); - guard.empty_with(|evt| { - sticky_exit_callback( - evt, - &self.window_target, - &mut control_flow, - &mut callback, - ); - }); - } - // empty user events - { - let mut guard = user_events.borrow_mut(); - for evt in guard.drain(..) { - sticky_exit_callback( - crate::event::Event::UserEvent(evt), - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - } - // do a second run of post-dispatch-triggers, to handle user-generated "request-redraw" - // in response of resize & friends - self.post_dispatch_triggers(); - { - let mut guard = sink.lock().unwrap(); - guard.empty_with(|evt| { - sticky_exit_callback( - evt, - &self.window_target, - &mut control_flow, - &mut callback, - ); - }); - } - // send Events cleared - { - sticky_exit_callback( - crate::event::Event::EventsCleared, - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - - // send pending events to the server - self.display.flush().expect("Wayland connection lost."); - - // 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 getting stuck. - let instant_wakeup = { - let window_target = match self.window_target.p { - crate::platform_impl::EventLoopWindowTarget::Wayland(ref wt) => wt, - _ => unreachable!(), - }; - let dispatched = window_target - .evq - .borrow_mut() - .dispatch_pending() - .expect("Wayland connection lost."); - dispatched > 0 - }; - - match control_flow { - ControlFlow::Exit => break, - ControlFlow::Poll => { - // non-blocking dispatch - self.inner_loop - .dispatch(Some(::std::time::Duration::from_millis(0)), &mut ()) - .unwrap(); - callback( - crate::event::Event::NewEvents(crate::event::StartCause::Poll), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::Wait => { - let timeout = if instant_wakeup { - Some(::std::time::Duration::from_millis(0)) - } else { - None - }; - self.inner_loop.dispatch(timeout, &mut ()).unwrap(); - callback( - crate::event::Event::NewEvents(crate::event::StartCause::WaitCancelled { - start: Instant::now(), - requested_resume: None, - }), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::WaitUntil(deadline) => { - let start = Instant::now(); - // compute the blocking duration - let duration = if deadline > start && !instant_wakeup { - deadline - start - } else { - ::std::time::Duration::from_millis(0) - }; - self.inner_loop.dispatch(Some(duration), &mut ()).unwrap(); - let now = Instant::now(); - if now < deadline { - callback( - crate::event::Event::NewEvents( - crate::event::StartCause::WaitCancelled { - start, - requested_resume: Some(deadline), - }, - ), - &self.window_target, - &mut control_flow, - ); - } else { - callback( - crate::event::Event::NewEvents( - crate::event::StartCause::ResumeTimeReached { - start, - requested_resume: deadline, - }, - ), - &self.window_target, - &mut control_flow, - ); - } - } - } - } - - callback( - crate::event::Event::LoopDestroyed, - &self.window_target, - &mut control_flow, - ); - } - - pub fn primary_monitor(&self) -> MonitorHandle { - primary_monitor(&self.outputs) - } - - pub fn available_monitors(&self) -> VecDeque { - available_monitors(&self.outputs) - } - - pub fn window_target(&self) -> &RootELW { - &self.window_target - } -} - -impl EventLoopWindowTarget { - pub fn display(&self) -> &Display { - &*self.display - } -} - -/* - * Private EventLoop Internals - */ - -impl EventLoop { - fn post_dispatch_triggers(&mut self) { - let mut sink = self.sink.lock().unwrap(); - let window_target = match self.window_target.p { - crate::platform_impl::EventLoopWindowTarget::Wayland(ref wt) => wt, - _ => unreachable!(), - }; - // prune possible dead windows - { - let mut cleanup_needed = window_target.cleanup_needed.lock().unwrap(); - if *cleanup_needed { - let pruned = window_target.store.lock().unwrap().cleanup(); - *cleanup_needed = false; - for wid in pruned { - sink.send_window_event(crate::event::WindowEvent::Destroyed, wid); - } - } - } - // process pending resize/refresh - window_target.store.lock().unwrap().for_each( - |newsize, - size, - new_dpi, - refresh, - frame_refresh, - closed, - grab_cursor, - surface, - wid, - frame| { - if let Some(frame) = frame { - if let Some(newsize) = newsize { - // Drop resize events equaled to the current size - if newsize != *size { - let (w, h) = newsize; - frame.resize(w, h); - frame.refresh(); - let logical_size = crate::dpi::LogicalSize::new(w as f64, h as f64); - sink.send_window_event( - crate::event::WindowEvent::Resized(logical_size), - wid, - ); - *size = (w, h); - } else { - // Refresh csd, etc, otherwise - frame.refresh(); - } - } else if frame_refresh { - frame.refresh(); - if !refresh { - frame.surface().commit() - } - } - } - if let Some(dpi) = new_dpi { - sink.send_window_event( - crate::event::WindowEvent::HiDpiFactorChanged(dpi as f64), - wid, - ); - } - if refresh { - sink.send_window_event(crate::event::WindowEvent::RedrawRequested, wid); - } - if closed { - sink.send_window_event(crate::event::WindowEvent::CloseRequested, wid); - } - - if let Some(grab_cursor) = grab_cursor { - let surface = if grab_cursor { Some(surface) } else { None }; - self.cursor_manager.lock().unwrap().grab_pointer(surface); - } - }, - ) - } -} - -/* - * Wayland protocol implementations - */ - -struct SeatManager { - sink: Arc>>, - store: Arc>, - seats: Arc>>, - kbd_sender: ::calloop::channel::Sender>, - relative_pointer_manager_proxy: Rc>>, - pointer_constraints_proxy: Arc>>, - cursor_manager: Arc>, -} - -impl SeatManager { - fn add_seat(&mut self, id: u32, version: u32, registry: wl_registry::WlRegistry) { - use std::cmp::min; - - let mut seat_data = SeatData { - sink: self.sink.clone(), - store: self.store.clone(), - pointer: None, - relative_pointer: None, - relative_pointer_manager_proxy: self.relative_pointer_manager_proxy.clone(), - keyboard: None, - touch: None, - kbd_sender: self.kbd_sender.clone(), - modifiers_tracker: Arc::new(Mutex::new(ModifiersState::default())), - cursor_manager: self.cursor_manager.clone(), - }; - let seat = registry - .bind(min(version, 5), id, move |seat| { - seat.implement_closure(move |event, seat| seat_data.receive(event, seat), ()) - }) - .unwrap(); - self.store.lock().unwrap().new_seat(&seat); - self.seats.lock().unwrap().push((id, seat)); - } - - fn remove_seat(&mut self, id: u32) { - let mut seats = self.seats.lock().unwrap(); - if let Some(idx) = seats.iter().position(|&(i, _)| i == id) { - let (_, seat) = seats.swap_remove(idx); - if seat.as_ref().version() >= 5 { - seat.release(); - } - } - } -} - -struct SeatData { - sink: Arc>>, - store: Arc>, - kbd_sender: ::calloop::channel::Sender>, - pointer: Option, - relative_pointer: Option, - relative_pointer_manager_proxy: Rc>>, - keyboard: Option, - touch: Option, - modifiers_tracker: Arc>, - cursor_manager: Arc>, -} - -impl SeatData { - fn receive(&mut self, evt: wl_seat::Event, seat: wl_seat::WlSeat) { - match evt { - wl_seat::Event::Name { .. } => (), - wl_seat::Event::Capabilities { capabilities } => { - // create pointer if applicable - if capabilities.contains(wl_seat::Capability::Pointer) && self.pointer.is_none() { - self.pointer = Some(super::pointer::implement_pointer( - &seat, - self.sink.clone(), - self.store.clone(), - self.modifiers_tracker.clone(), - self.cursor_manager.clone(), - )); - - self.cursor_manager - .lock() - .unwrap() - .register_pointer(self.pointer.as_ref().unwrap().clone()); - - self.relative_pointer = self - .relative_pointer_manager_proxy - .try_borrow() - .unwrap() - .as_ref() - .and_then(|manager| { - super::pointer::implement_relative_pointer( - self.sink.clone(), - self.pointer.as_ref().unwrap(), - manager, - ) - .ok() - }) - } - // destroy pointer if applicable - if !capabilities.contains(wl_seat::Capability::Pointer) { - if let Some(pointer) = self.pointer.take() { - if pointer.as_ref().version() >= 3 { - pointer.release(); - } - } - } - // create keyboard if applicable - if capabilities.contains(wl_seat::Capability::Keyboard) && self.keyboard.is_none() { - self.keyboard = Some(super::keyboard::init_keyboard( - &seat, - self.kbd_sender.clone(), - self.modifiers_tracker.clone(), - )) - } - // destroy keyboard if applicable - if !capabilities.contains(wl_seat::Capability::Keyboard) { - if let Some(kbd) = self.keyboard.take() { - if kbd.as_ref().version() >= 3 { - kbd.release(); - } - } - } - // create touch if applicable - if capabilities.contains(wl_seat::Capability::Touch) && self.touch.is_none() { - self.touch = Some(super::touch::implement_touch( - &seat, - self.sink.clone(), - self.store.clone(), - )) - } - // destroy touch if applicable - if !capabilities.contains(wl_seat::Capability::Touch) { - if let Some(touch) = self.touch.take() { - if touch.as_ref().version() >= 3 { - touch.release(); - } - } - } - } - _ => unreachable!(), - } - } -} - -impl Drop for SeatData { - fn drop(&mut self) { - if let Some(pointer) = self.pointer.take() { - if pointer.as_ref().version() >= 3 { - pointer.release(); - } - } - if let Some(kbd) = self.keyboard.take() { - if kbd.as_ref().version() >= 3 { - kbd.release(); - } - } - if let Some(touch) = self.touch.take() { - if touch.as_ref().version() >= 3 { - touch.release(); - } - } - } -} - -/* - * Monitor stuff - */ - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct VideoMode { - pub(crate) size: (u32, u32), - pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, - pub(crate) monitor: MonitorHandle, -} - -impl VideoMode { - #[inline] - pub fn size(&self) -> PhysicalSize { - self.size.into() - } - - #[inline] - pub fn bit_depth(&self) -> u16 { - self.bit_depth - } - - #[inline] - pub fn refresh_rate(&self) -> u16 { - self.refresh_rate - } - - #[inline] - pub fn monitor(&self) -> RootMonitorHandle { - RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(self.monitor.clone()), - } - } -} - -#[derive(Clone)] -pub struct MonitorHandle { - pub(crate) proxy: wl_output::WlOutput, - pub(crate) mgr: OutputMgr, -} - -impl PartialEq for MonitorHandle { - fn eq(&self, other: &Self) -> bool { - self.native_identifier() == other.native_identifier() - } -} - -impl Eq for MonitorHandle {} - -impl PartialOrd for MonitorHandle { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) - } -} - -impl Ord for MonitorHandle { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.native_identifier().cmp(&other.native_identifier()) - } -} - -impl std::hash::Hash for MonitorHandle { - fn hash(&self, state: &mut H) { - self.native_identifier().hash(state); - } -} - -impl fmt::Debug for MonitorHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[derive(Debug)] - struct MonitorHandle { - name: Option, - native_identifier: u32, - size: PhysicalSize, - position: PhysicalPosition, - hidpi_factor: i32, - } - - let monitor_id_proxy = MonitorHandle { - name: self.name(), - native_identifier: self.native_identifier(), - size: self.size(), - position: self.position(), - hidpi_factor: self.hidpi_factor(), - }; - - monitor_id_proxy.fmt(f) - } -} - -impl MonitorHandle { - pub fn name(&self) -> Option { - self.mgr.with_info(&self.proxy, |_, info| { - format!("{} ({})", info.model, info.make) - }) - } - - #[inline] - pub fn native_identifier(&self) -> u32 { - self.mgr.with_info(&self.proxy, |id, _| id).unwrap_or(0) - } - - pub fn size(&self) -> PhysicalSize { - match self.mgr.with_info(&self.proxy, |_, info| { - info.modes - .iter() - .find(|m| m.is_current) - .map(|m| m.dimensions) - }) { - Some(Some((w, h))) => (w as u32, h as u32), - _ => (0, 0), - } - .into() - } - - pub fn position(&self) -> PhysicalPosition { - self.mgr - .with_info(&self.proxy, |_, info| info.location) - .unwrap_or((0, 0)) - .into() - } - - #[inline] - pub fn hidpi_factor(&self) -> i32 { - self.mgr - .with_info(&self.proxy, |_, info| info.scale_factor) - .unwrap_or(1) - } - - #[inline] - pub fn video_modes(&self) -> impl Iterator { - let monitor = self.clone(); - - self.mgr - .with_info(&self.proxy, |_, info| info.modes.clone()) - .unwrap_or(vec![]) - .into_iter() - .map(move |x| RootVideoMode { - video_mode: PlatformVideoMode::Wayland(VideoMode { - size: (x.dimensions.0 as u32, x.dimensions.1 as u32), - refresh_rate: (x.refresh_rate as f32 / 1000.0).round() as u16, - bit_depth: 32, - monitor: monitor.clone(), - }), - }) - } -} - -pub fn primary_monitor(outputs: &OutputMgr) -> MonitorHandle { - outputs.with_all(|list| { - if let Some(&(_, ref proxy, _)) = list.first() { - MonitorHandle { - proxy: proxy.clone(), - mgr: outputs.clone(), - } - } else { - panic!("No monitor is available.") - } - }) -} - -pub fn available_monitors(outputs: &OutputMgr) -> VecDeque { - outputs.with_all(|list| { - list.iter() - .map(|&(_, ref proxy, _)| MonitorHandle { - proxy: proxy.clone(), - mgr: outputs.clone(), - }) - .collect() - }) -} diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 0000000000..8c2d86a5c9 --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -0,0 +1,535 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::error::Error; +use std::process; +use std::rc::Rc; +use std::time::{Duration, Instant}; + +use sctk::reexports::client::protocol::wl_compositor::WlCompositor; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::Display; + +use sctk::reexports::calloop; + +use sctk::environment::Environment; +use sctk::seat::pointer::{ThemeManager, ThemeSpec}; +use sctk::WaylandSource; + +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; +use crate::platform_impl::platform::sticky_exit_callback; + +use super::env::{WindowingFeatures, WinitEnv}; +use super::output::OutputManager; +use super::seat::SeatManager; +use super::window::shim::{self, WindowUpdate}; +use super::{DeviceId, WindowId}; + +mod proxy; +mod sink; +mod state; + +pub use proxy::EventLoopProxy; +pub use state::WinitState; + +use sink::EventSink; + +pub struct EventLoopWindowTarget { + /// Wayland display. + pub display: Display, + + /// Environment to handle object creation, etc. + pub env: Environment, + + /// Event loop handle. + pub event_loop_handle: calloop::LoopHandle, + + /// Output manager. + pub output_manager: OutputManager, + + /// State that we share across callbacks. + pub state: RefCell, + + /// Wayland source. + pub wayland_source: Rc>, + + /// A proxy to wake up event loop. + pub event_loop_awakener: calloop::ping::Ping, + + /// The available windowing features. + pub windowing_features: WindowingFeatures, + + /// Theme manager to manage cursors. + /// + /// It's being shared amoung all windows to avoid loading + /// multiple similar themes. + pub theme_manager: ThemeManager, + + _marker: std::marker::PhantomData, +} + +pub struct EventLoop { + /// Event loop. + event_loop: calloop::EventLoop, + + /// Wayland display. + display: Display, + + /// Pending user events. + pending_user_events: Rc>>, + + /// Sender of user events. + user_events_sender: calloop::channel::Sender, + + /// Wayland source of events. + wayland_source: Rc>, + + /// Window target. + window_target: RootEventLoopWindowTarget, + + /// Output manager. + _seat_manager: SeatManager, +} + +impl EventLoop { + pub fn new() -> Result, Box> { + // Connect to wayland server and setup event queue. + let display = Display::connect_to_env()?; + let mut event_queue = display.create_event_queue(); + let display_proxy = display.attach(event_queue.token()); + + // Setup environment. + let env = Environment::new(&display_proxy, &mut event_queue, WinitEnv::new())?; + + // Create event loop. + let event_loop = calloop::EventLoop::::new()?; + // Build windowing features. + let windowing_features = WindowingFeatures::new(&env); + + // Create a theme manager. + let compositor = env.require_global::(); + let shm = env.require_global::(); + let theme_manager = ThemeManager::init(ThemeSpec::System, compositor, shm); + + // Setup theme seat and output managers. + let seat_manager = SeatManager::new(&env, event_loop.handle(), theme_manager.clone()); + let output_manager = OutputManager::new(&env); + + // A source of events that we plug into our event loop. + let wayland_source = WaylandSource::new(event_queue).quick_insert(event_loop.handle())?; + let wayland_source = Rc::new(wayland_source); + + // A source of user events. + 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(); + + // User events channel. + 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); + } + })?; + + // 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()?; + + // Handler of window requests. + event_loop.handle().insert_source( + event_loop_awakener_source, + move |_, _, winit_state| { + shim::handle_window_requests(winit_state); + }, + )?; + + let event_loop_handle = event_loop.handle(); + let window_map = HashMap::new(); + let event_sink = EventSink::new(); + let window_updates = HashMap::new(); + + // Create event loop window target. + let event_loop_window_target = EventLoopWindowTarget { + display: display.clone(), + env, + state: RefCell::new(WinitState { + window_map, + event_sink, + window_updates, + }), + event_loop_handle, + output_manager, + event_loop_awakener, + wayland_source: wayland_source.clone(), + windowing_features, + theme_manager, + _marker: std::marker::PhantomData, + }; + + // Create event loop itself. + let event_loop = Self { + event_loop, + display, + pending_user_events, + wayland_source, + _seat_manager: seat_manager, + user_events_sender, + window_target: RootEventLoopWindowTarget { + p: crate::platform_impl::EventLoopWindowTarget::Wayland(event_loop_window_target), + _marker: std::marker::PhantomData, + }, + }; + + Ok(event_loop) + } + + pub fn run(mut self, callback: F) -> ! + where + F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow) + 'static, + { + self.run_return(callback); + process::exit(0) + } + + pub fn run_return(&mut self, mut callback: F) + where + F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + { + // Send pending events to the server. + let _ = self.display.flush(); + + let mut control_flow = ControlFlow::default(); + + let pending_user_events = self.pending_user_events.clone(); + + callback( + Event::NewEvents(StartCause::Init), + &self.window_target, + &mut control_flow, + ); + + let mut window_updates: Vec<(WindowId, WindowUpdate)> = Vec::new(); + let mut event_sink_back_buffer = Vec::new(); + + // NOTE 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. + loop { + // 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 pending_user_events.borrow_mut().drain(..) { + sticky_exit_callback( + Event::UserEvent(user_event), + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + + // Process 'new' pending updates. + self.with_state(|state| { + window_updates.clear(); + window_updates.extend( + state + .window_updates + .iter_mut() + .map(|(wid, window_update)| (*wid, window_update.take())), + ); + }); + + for (window_id, window_update) in window_updates.iter_mut() { + if let Some(scale_factor) = window_update.scale_factor.map(|f| f as f64) { + let mut physical_size = self.with_state(|state| { + let window_handle = state.window_map.get(&window_id).unwrap(); + let mut size = window_handle.size.lock().unwrap(); + + // Update the new logical size if it was changed. + let window_size = window_update.size.unwrap_or(*size); + *size = window_size; + + window_size.to_physical(scale_factor) + }); + + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId( + crate::platform_impl::WindowId::Wayland(*window_id), + ), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: &mut physical_size, + }, + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + + // We don't update size on a window handle since we'll do that later + // when handling size update. + let new_logical_size = physical_size.to_logical(scale_factor); + window_update.size = Some(new_logical_size); + } + + if let Some(size) = window_update.size.take() { + let physical_size = self.with_state(|state| { + let window_handle = state.window_map.get_mut(&window_id).unwrap(); + let mut window_size = window_handle.size.lock().unwrap(); + + // Always issue resize event on scale factor change. + let physical_size = + if window_update.scale_factor.is_none() && *window_size == size { + // The size hasn't changed, don't inform downstream about that. + None + } else { + *window_size = size; + let scale_factor = + sctk::get_surface_scale_factor(&window_handle.window.surface()); + let physical_size = size.to_physical(scale_factor as f64); + Some(physical_size) + }; + + // We still perform all of those resize related logic even if the size + // hasn't changed, since GNOME relies on `set_geometry` calls after + // configures. + window_handle.window.resize(size.width, size.height); + window_handle.window.refresh(); + + // Mark that refresh isn't required, since we've done it right now. + window_update.refresh_frame = false; + + physical_size + }); + + if let Some(physical_size) = physical_size { + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId( + crate::platform_impl::WindowId::Wayland(*window_id), + ), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + if window_update.close_window { + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId( + crate::platform_impl::WindowId::Wayland(*window_id), + ), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + // The purpose of the back buffer and that swap is to not hold borrow_mut when + // we're doing callback to the user, since we can double borrow if the user decides + // to create a window in one of those callbacks. + self.with_state(|state| { + std::mem::swap( + &mut event_sink_back_buffer, + &mut state.event_sink.window_events, + ) + }); + + // Handle pending window events. + for event in event_sink_back_buffer.drain(..) { + let event = event.map_nonuser_event().unwrap(); + sticky_exit_callback(event, &self.window_target, &mut control_flow, &mut callback); + } + + // Send events cleared. + sticky_exit_callback( + Event::MainEventsCleared, + &self.window_target, + &mut control_flow, + &mut callback, + ); + + // Handle RedrawRequested events. + for (window_id, window_update) in window_updates.iter() { + // Handle refresh of the frame. + if window_update.refresh_frame { + self.with_state(|state| { + let window_handle = state.window_map.get_mut(&window_id).unwrap(); + window_handle.window.refresh(); + if !window_update.redraw_requested { + window_handle.window.surface().commit(); + } + }); + } + + // Handle redraw request. + if window_update.redraw_requested { + sticky_exit_callback( + Event::RedrawRequested(crate::window::WindowId( + crate::platform_impl::WindowId::Wayland(*window_id), + )), + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + // Send RedrawEventCleared. + sticky_exit_callback( + Event::RedrawEventsCleared, + &self.window_target, + &mut control_flow, + &mut callback, + ); + + // Send pending events to the server. + let _ = self.display.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 handle = self.event_loop.handle(); + let source = self.wayland_source.clone(); + let dispatched = handle.with_source(&source, |wayland_source| { + let queue = wayland_source.queue(); + self.with_state(|state| { + queue.dispatch_pending(state, |_, _, _| unimplemented!()) + }) + }); + + if let Ok(dispatched) = dispatched { + dispatched > 0 + } else { + break; + } + }; + + match control_flow { + ControlFlow::Exit => break, + ControlFlow::Poll => { + // Non-blocking dispatch. + let timeout = Duration::from_millis(0); + if self.loop_dispatch(Some(timeout)).is_err() { + break; + } + + callback( + Event::NewEvents(StartCause::Poll), + &self.window_target, + &mut control_flow, + ); + } + ControlFlow::Wait => { + let timeout = if instant_wakeup { + Some(Duration::from_millis(0)) + } else { + None + }; + + if self.loop_dispatch(timeout).is_err() { + break; + } + + callback( + Event::NewEvents(StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }), + &self.window_target, + &mut control_flow, + ); + } + ControlFlow::WaitUntil(deadline) => { + let start = Instant::now(); + + // Compute the amount of time we'll block for. + let duration = if deadline > start && !instant_wakeup { + deadline - start + } else { + Duration::from_millis(0) + }; + + if self.loop_dispatch(Some(duration)).is_err() { + break; + } + + 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, + ) + } + } + } + } + + callback(Event::LoopDestroyed, &self.window_target, &mut control_flow); + } + + #[inline] + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.user_events_sender.clone()) + } + + #[inline] + pub fn window_target(&self) -> &RootEventLoopWindowTarget { + &self.window_target + } + + fn with_state U>(&mut self, f: F) -> U { + let state = match &mut self.window_target.p { + crate::platform_impl::EventLoopWindowTarget::Wayland(ref mut window_target) => { + window_target.state.get_mut() + } + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + f(state) + } + + fn loop_dispatch>>( + &mut self, + timeout: D, + ) -> std::io::Result<()> { + let mut state = match &mut self.window_target.p { + crate::platform_impl::EventLoopWindowTarget::Wayland(ref mut window_target) => { + window_target.state.get_mut() + } + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + self.event_loop.dispatch(timeout, &mut state) + } +} diff --git a/src/platform_impl/linux/wayland/event_loop/proxy.rs b/src/platform_impl/linux/wayland/event_loop/proxy.rs new file mode 100644 index 0000000000..dad64ef2c9 --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/proxy.rs @@ -0,0 +1,32 @@ +//! An event loop proxy. + +use std::sync::mpsc::SendError; + +use sctk::reexports::calloop::channel::Sender; + +use crate::event_loop::EventLoopClosed; + +/// A handle that can be sent across the threads and used to wake up the `EventLoop`. +pub struct EventLoopProxy { + user_events_sender: Sender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + } + } +} + +impl EventLoopProxy { + pub fn new(user_events_sender: Sender) -> Self { + Self { user_events_sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_events_sender + .send(event) + .map_err(|SendError(error)| EventLoopClosed(error)) + } +} diff --git a/src/platform_impl/linux/wayland/event_loop/sink.rs b/src/platform_impl/linux/wayland/event_loop/sink.rs new file mode 100644 index 0000000000..303ab826e4 --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -0,0 +1,36 @@ +//! An event loop's sink to deliver events from the Wayland event callbacks. + +use crate::event::{DeviceEvent, DeviceId as RootDeviceId, Event, WindowEvent}; +use crate::platform_impl::platform::{DeviceId as PlatformDeviceId, WindowId as PlatformWindowId}; +use crate::window::WindowId as RootWindowId; + +use super::{DeviceId, WindowId}; + +/// An event loop's sink to deliver events from the Wayland event callbacks +/// to the winit's user. +#[derive(Default)] +pub struct EventSink { + pub window_events: Vec>, +} + +impl EventSink { + pub fn new() -> Self { + Default::default() + } + + /// Add new device event to a queue. + pub fn push_device_event(&mut self, event: DeviceEvent, device_id: DeviceId) { + self.window_events.push(Event::DeviceEvent { + event, + device_id: RootDeviceId(PlatformDeviceId::Wayland(device_id)), + }); + } + + /// Add new window event to a queue. + pub fn push_window_event(&mut self, event: WindowEvent<'static>, window_id: WindowId) { + self.window_events.push(Event::WindowEvent { + event, + window_id: RootWindowId(PlatformWindowId::Wayland(window_id)), + }); + } +} diff --git a/src/platform_impl/linux/wayland/event_loop/state.rs b/src/platform_impl/linux/wayland/event_loop/state.rs new file mode 100644 index 0000000000..7aad9eccd2 --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/state.rs @@ -0,0 +1,25 @@ +//! A state that we pass around in a dispatch. + +use std::collections::HashMap; + +use super::EventSink; +use crate::platform_impl::wayland::window::shim::{WindowHandle, WindowUpdate}; +use crate::platform_impl::wayland::WindowId; + +/// Wrapper to carry winit's state. +pub struct WinitState { + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub event_sink: EventSink, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the winit's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub window_updates: HashMap, + + /// Window map containing all SCTK windows. Since those windows aren't allowed + /// to be sent to other threads, they live on the event loop's thread + /// and requests from winit's windows are being forwarded to them either via + /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. + pub window_map: HashMap, +} diff --git a/src/platform_impl/linux/wayland/keyboard.rs b/src/platform_impl/linux/wayland/keyboard.rs deleted file mode 100644 index 31811422c7..0000000000 --- a/src/platform_impl/linux/wayland/keyboard.rs +++ /dev/null @@ -1,419 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use super::{make_wid, DeviceId}; -use smithay_client_toolkit::{ - keyboard::{ - self, map_keyboard_auto_with_repeat, Event as KbEvent, KeyRepeatEvent, KeyRepeatKind, - }, - reexports::client::protocol::{wl_keyboard, wl_seat}, -}; - -use crate::event::{ - DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent, -}; - -pub fn init_keyboard( - seat: &wl_seat::WlSeat, - sink: ::calloop::channel::Sender>, - modifiers_tracker: Arc>, -) -> wl_keyboard::WlKeyboard { - // { variables to be captured by the closures - let target = Arc::new(Mutex::new(None)); - let my_sink = sink.clone(); - let repeat_sink = sink.clone(); - let repeat_target = target.clone(); - let my_modifiers = modifiers_tracker.clone(); - // } - let ret = map_keyboard_auto_with_repeat( - seat, - KeyRepeatKind::System, - move |evt: KbEvent<'_>, _| { - match evt { - KbEvent::Enter { surface, .. } => { - let wid = make_wid(&surface); - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::Focused(true), - }) - .unwrap(); - *target.lock().unwrap() = Some(wid); - } - KbEvent::Leave { surface, .. } => { - let wid = make_wid(&surface); - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::Focused(false), - }) - .unwrap(); - *target.lock().unwrap() = None; - } - KbEvent::Key { - rawkey, - keysym, - state, - utf8, - .. - } => { - if let Some(wid) = *target.lock().unwrap() { - let state = match state { - wl_keyboard::KeyState::Pressed => ElementState::Pressed, - wl_keyboard::KeyState::Released => ElementState::Released, - _ => unreachable!(), - }; - let vkcode = key_to_vkey(rawkey, keysym); - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::KeyboardInput { - device_id: device_id(), - input: KeyboardInput { - state, - scancode: rawkey, - virtual_keycode: vkcode, - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - }, - }) - .unwrap(); - // send char event only on key press, not release - if let ElementState::Released = state { - return; - } - if let Some(txt) = utf8 { - for chr in txt.chars() { - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::ReceivedCharacter(chr), - }) - .unwrap(); - } - } - } - } - KbEvent::RepeatInfo { .. } => { /* Handled by smithay client toolkit */ } - KbEvent::Modifiers { - modifiers: event_modifiers, - } => { - let modifiers = ModifiersState::from_wayland(event_modifiers); - - *modifiers_tracker.lock().unwrap() = modifiers; - - my_sink - .send(Event::DeviceEvent { - device_id: device_id(), - event: DeviceEvent::ModifiersChanged { modifiers }, - }) - .unwrap(); - } - } - }, - move |repeat_event: KeyRepeatEvent, _| { - if let Some(wid) = *repeat_target.lock().unwrap() { - let state = ElementState::Pressed; - let vkcode = key_to_vkey(repeat_event.rawkey, repeat_event.keysym); - repeat_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::KeyboardInput { - device_id: device_id(), - input: KeyboardInput { - state, - scancode: repeat_event.rawkey, - virtual_keycode: vkcode, - modifiers: my_modifiers.lock().unwrap().clone(), - }, - }, - }) - .unwrap(); - if let Some(txt) = repeat_event.utf8 { - for chr in txt.chars() { - repeat_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::ReceivedCharacter(chr), - }) - .unwrap(); - } - } - } - }, - ); - - match ret { - Ok(keyboard) => keyboard, - Err(_) => { - // This is a fallback impl if libxkbcommon was not available - // This case should probably never happen, as most wayland - // compositors _need_ libxkbcommon anyway... - // - // In this case, we don't have the keymap information (it is - // supposed to be serialized by the compositor using libxkbcommon) - - seat.get_keyboard(|keyboard| { - // { variables to be captured by the closure - let mut target = None; - let my_sink = sink; - // } - - keyboard.implement_closure( - move |evt, _| match evt { - wl_keyboard::Event::Enter { surface, .. } => { - let wid = make_wid(&surface); - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::Focused(true), - }) - .unwrap(); - target = Some(wid); - } - wl_keyboard::Event::Leave { surface, .. } => { - let wid = make_wid(&surface); - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::Focused(false), - }) - .unwrap(); - target = None; - } - wl_keyboard::Event::Key { key, state, .. } => { - if let Some(wid) = target { - let state = match state { - wl_keyboard::KeyState::Pressed => ElementState::Pressed, - wl_keyboard::KeyState::Released => ElementState::Released, - _ => unreachable!(), - }; - my_sink - .send(Event::WindowEvent { - window_id: mk_root_wid(wid), - event: WindowEvent::KeyboardInput { - device_id: device_id(), - input: KeyboardInput { - state, - scancode: key, - virtual_keycode: None, - modifiers: ModifiersState::default(), - }, - }, - }) - .unwrap(); - } - } - _ => (), - }, - (), - ) - }) - .unwrap() - } - } -} - -fn key_to_vkey(rawkey: u32, keysym: u32) -> Option { - match rawkey { - 1 => Some(VirtualKeyCode::Escape), - 2 => Some(VirtualKeyCode::Key1), - 3 => Some(VirtualKeyCode::Key2), - 4 => Some(VirtualKeyCode::Key3), - 5 => Some(VirtualKeyCode::Key4), - 6 => Some(VirtualKeyCode::Key5), - 7 => Some(VirtualKeyCode::Key6), - 8 => Some(VirtualKeyCode::Key7), - 9 => Some(VirtualKeyCode::Key8), - 10 => Some(VirtualKeyCode::Key9), - 11 => Some(VirtualKeyCode::Key0), - _ => keysym_to_vkey(keysym), - } -} - -fn keysym_to_vkey(keysym: u32) -> Option { - use smithay_client_toolkit::keyboard::keysyms; - match keysym { - // letters - keysyms::XKB_KEY_A | keysyms::XKB_KEY_a => Some(VirtualKeyCode::A), - keysyms::XKB_KEY_B | keysyms::XKB_KEY_b => Some(VirtualKeyCode::B), - keysyms::XKB_KEY_C | keysyms::XKB_KEY_c => Some(VirtualKeyCode::C), - keysyms::XKB_KEY_D | keysyms::XKB_KEY_d => Some(VirtualKeyCode::D), - keysyms::XKB_KEY_E | keysyms::XKB_KEY_e => Some(VirtualKeyCode::E), - keysyms::XKB_KEY_F | keysyms::XKB_KEY_f => Some(VirtualKeyCode::F), - keysyms::XKB_KEY_G | keysyms::XKB_KEY_g => Some(VirtualKeyCode::G), - keysyms::XKB_KEY_H | keysyms::XKB_KEY_h => Some(VirtualKeyCode::H), - keysyms::XKB_KEY_I | keysyms::XKB_KEY_i => Some(VirtualKeyCode::I), - keysyms::XKB_KEY_J | keysyms::XKB_KEY_j => Some(VirtualKeyCode::J), - keysyms::XKB_KEY_K | keysyms::XKB_KEY_k => Some(VirtualKeyCode::K), - keysyms::XKB_KEY_L | keysyms::XKB_KEY_l => Some(VirtualKeyCode::L), - keysyms::XKB_KEY_M | keysyms::XKB_KEY_m => Some(VirtualKeyCode::M), - keysyms::XKB_KEY_N | keysyms::XKB_KEY_n => Some(VirtualKeyCode::N), - keysyms::XKB_KEY_O | keysyms::XKB_KEY_o => Some(VirtualKeyCode::O), - keysyms::XKB_KEY_P | keysyms::XKB_KEY_p => Some(VirtualKeyCode::P), - keysyms::XKB_KEY_Q | keysyms::XKB_KEY_q => Some(VirtualKeyCode::Q), - keysyms::XKB_KEY_R | keysyms::XKB_KEY_r => Some(VirtualKeyCode::R), - keysyms::XKB_KEY_S | keysyms::XKB_KEY_s => Some(VirtualKeyCode::S), - keysyms::XKB_KEY_T | keysyms::XKB_KEY_t => Some(VirtualKeyCode::T), - keysyms::XKB_KEY_U | keysyms::XKB_KEY_u => Some(VirtualKeyCode::U), - keysyms::XKB_KEY_V | keysyms::XKB_KEY_v => Some(VirtualKeyCode::V), - keysyms::XKB_KEY_W | keysyms::XKB_KEY_w => Some(VirtualKeyCode::W), - keysyms::XKB_KEY_X | keysyms::XKB_KEY_x => Some(VirtualKeyCode::X), - keysyms::XKB_KEY_Y | keysyms::XKB_KEY_y => Some(VirtualKeyCode::Y), - keysyms::XKB_KEY_Z | keysyms::XKB_KEY_z => Some(VirtualKeyCode::Z), - // F-- - keysyms::XKB_KEY_F1 => Some(VirtualKeyCode::F1), - keysyms::XKB_KEY_F2 => Some(VirtualKeyCode::F2), - keysyms::XKB_KEY_F3 => Some(VirtualKeyCode::F3), - keysyms::XKB_KEY_F4 => Some(VirtualKeyCode::F4), - keysyms::XKB_KEY_F5 => Some(VirtualKeyCode::F5), - keysyms::XKB_KEY_F6 => Some(VirtualKeyCode::F6), - keysyms::XKB_KEY_F7 => Some(VirtualKeyCode::F7), - keysyms::XKB_KEY_F8 => Some(VirtualKeyCode::F8), - keysyms::XKB_KEY_F9 => Some(VirtualKeyCode::F9), - keysyms::XKB_KEY_F10 => Some(VirtualKeyCode::F10), - keysyms::XKB_KEY_F11 => Some(VirtualKeyCode::F11), - keysyms::XKB_KEY_F12 => Some(VirtualKeyCode::F12), - keysyms::XKB_KEY_F13 => Some(VirtualKeyCode::F13), - keysyms::XKB_KEY_F14 => Some(VirtualKeyCode::F14), - keysyms::XKB_KEY_F15 => Some(VirtualKeyCode::F15), - keysyms::XKB_KEY_F16 => Some(VirtualKeyCode::F16), - keysyms::XKB_KEY_F17 => Some(VirtualKeyCode::F17), - keysyms::XKB_KEY_F18 => Some(VirtualKeyCode::F18), - keysyms::XKB_KEY_F19 => Some(VirtualKeyCode::F19), - keysyms::XKB_KEY_F20 => Some(VirtualKeyCode::F20), - keysyms::XKB_KEY_F21 => Some(VirtualKeyCode::F21), - keysyms::XKB_KEY_F22 => Some(VirtualKeyCode::F22), - keysyms::XKB_KEY_F23 => Some(VirtualKeyCode::F23), - keysyms::XKB_KEY_F24 => Some(VirtualKeyCode::F24), - // flow control - keysyms::XKB_KEY_Print => Some(VirtualKeyCode::Snapshot), - keysyms::XKB_KEY_Scroll_Lock => Some(VirtualKeyCode::Scroll), - keysyms::XKB_KEY_Pause => Some(VirtualKeyCode::Pause), - keysyms::XKB_KEY_Insert => Some(VirtualKeyCode::Insert), - keysyms::XKB_KEY_Home => Some(VirtualKeyCode::Home), - keysyms::XKB_KEY_Delete => Some(VirtualKeyCode::Delete), - keysyms::XKB_KEY_End => Some(VirtualKeyCode::End), - keysyms::XKB_KEY_Page_Down => Some(VirtualKeyCode::PageDown), - keysyms::XKB_KEY_Page_Up => Some(VirtualKeyCode::PageUp), - // arrows - keysyms::XKB_KEY_Left => Some(VirtualKeyCode::Left), - keysyms::XKB_KEY_Up => Some(VirtualKeyCode::Up), - keysyms::XKB_KEY_Right => Some(VirtualKeyCode::Right), - keysyms::XKB_KEY_Down => Some(VirtualKeyCode::Down), - // - keysyms::XKB_KEY_BackSpace => Some(VirtualKeyCode::Back), - keysyms::XKB_KEY_Return => Some(VirtualKeyCode::Return), - keysyms::XKB_KEY_space => Some(VirtualKeyCode::Space), - // keypad - keysyms::XKB_KEY_Num_Lock => Some(VirtualKeyCode::Numlock), - keysyms::XKB_KEY_KP_0 => Some(VirtualKeyCode::Numpad0), - keysyms::XKB_KEY_KP_1 => Some(VirtualKeyCode::Numpad1), - keysyms::XKB_KEY_KP_2 => Some(VirtualKeyCode::Numpad2), - keysyms::XKB_KEY_KP_3 => Some(VirtualKeyCode::Numpad3), - keysyms::XKB_KEY_KP_4 => Some(VirtualKeyCode::Numpad4), - keysyms::XKB_KEY_KP_5 => Some(VirtualKeyCode::Numpad5), - keysyms::XKB_KEY_KP_6 => Some(VirtualKeyCode::Numpad6), - keysyms::XKB_KEY_KP_7 => Some(VirtualKeyCode::Numpad7), - keysyms::XKB_KEY_KP_8 => Some(VirtualKeyCode::Numpad8), - keysyms::XKB_KEY_KP_9 => Some(VirtualKeyCode::Numpad9), - // misc - // => Some(VirtualKeyCode::AbntC1), - // => Some(VirtualKeyCode::AbntC2), - keysyms::XKB_KEY_plus => Some(VirtualKeyCode::Add), - keysyms::XKB_KEY_apostrophe => Some(VirtualKeyCode::Apostrophe), - // => Some(VirtualKeyCode::Apps), - // => Some(VirtualKeyCode::At), - // => Some(VirtualKeyCode::Ax), - keysyms::XKB_KEY_backslash => Some(VirtualKeyCode::Backslash), - // => Some(VirtualKeyCode::Calculator), - // => Some(VirtualKeyCode::Capital), - keysyms::XKB_KEY_colon => Some(VirtualKeyCode::Colon), - keysyms::XKB_KEY_comma => Some(VirtualKeyCode::Comma), - // => Some(VirtualKeyCode::Convert), - // => Some(VirtualKeyCode::Decimal), - // => Some(VirtualKeyCode::Divide), - keysyms::XKB_KEY_equal => Some(VirtualKeyCode::Equals), - // => Some(VirtualKeyCode::Grave), - // => Some(VirtualKeyCode::Kana), - // => Some(VirtualKeyCode::Kanji), - keysyms::XKB_KEY_Alt_L => Some(VirtualKeyCode::LAlt), - // => Some(VirtualKeyCode::LBracket), - keysyms::XKB_KEY_Control_L => Some(VirtualKeyCode::LControl), - keysyms::XKB_KEY_Shift_L => Some(VirtualKeyCode::LShift), - // => Some(VirtualKeyCode::LWin), - // => Some(VirtualKeyCode::Mail), - // => Some(VirtualKeyCode::MediaSelect), - // => Some(VirtualKeyCode::MediaStop), - keysyms::XKB_KEY_minus => Some(VirtualKeyCode::Minus), - keysyms::XKB_KEY_asterisk => Some(VirtualKeyCode::Multiply), - // => Some(VirtualKeyCode::Mute), - // => Some(VirtualKeyCode::MyComputer), - // => Some(VirtualKeyCode::NextTrack), - // => Some(VirtualKeyCode::NoConvert), - keysyms::XKB_KEY_KP_Separator => Some(VirtualKeyCode::NumpadComma), - keysyms::XKB_KEY_KP_Enter => Some(VirtualKeyCode::NumpadEnter), - keysyms::XKB_KEY_KP_Equal => Some(VirtualKeyCode::NumpadEquals), - keysyms::XKB_KEY_KP_Add => Some(VirtualKeyCode::Add), - keysyms::XKB_KEY_KP_Subtract => Some(VirtualKeyCode::Subtract), - keysyms::XKB_KEY_KP_Divide => Some(VirtualKeyCode::Divide), - keysyms::XKB_KEY_KP_Page_Up => Some(VirtualKeyCode::PageUp), - keysyms::XKB_KEY_KP_Page_Down => Some(VirtualKeyCode::PageDown), - keysyms::XKB_KEY_KP_Home => Some(VirtualKeyCode::Home), - keysyms::XKB_KEY_KP_End => Some(VirtualKeyCode::End), - // => Some(VirtualKeyCode::OEM102), - // => Some(VirtualKeyCode::Period), - // => Some(VirtualKeyCode::Playpause), - // => Some(VirtualKeyCode::Power), - // => Some(VirtualKeyCode::Prevtrack), - keysyms::XKB_KEY_Alt_R => Some(VirtualKeyCode::RAlt), - // => Some(VirtualKeyCode::RBracket), - keysyms::XKB_KEY_Control_R => Some(VirtualKeyCode::RControl), - keysyms::XKB_KEY_Shift_R => Some(VirtualKeyCode::RShift), - // => Some(VirtualKeyCode::RWin), - keysyms::XKB_KEY_semicolon => Some(VirtualKeyCode::Semicolon), - keysyms::XKB_KEY_slash => Some(VirtualKeyCode::Slash), - // => Some(VirtualKeyCode::Sleep), - // => Some(VirtualKeyCode::Stop), - // => Some(VirtualKeyCode::Subtract), - // => Some(VirtualKeyCode::Sysrq), - keysyms::XKB_KEY_Tab => Some(VirtualKeyCode::Tab), - keysyms::XKB_KEY_ISO_Left_Tab => Some(VirtualKeyCode::Tab), - // => Some(VirtualKeyCode::Underline), - // => Some(VirtualKeyCode::Unlabeled), - keysyms::XKB_KEY_XF86AudioLowerVolume => Some(VirtualKeyCode::VolumeDown), - keysyms::XKB_KEY_XF86AudioRaiseVolume => Some(VirtualKeyCode::VolumeUp), - // => Some(VirtualKeyCode::Wake), - // => Some(VirtualKeyCode::Webback), - // => Some(VirtualKeyCode::WebFavorites), - // => Some(VirtualKeyCode::WebForward), - // => Some(VirtualKeyCode::WebHome), - // => Some(VirtualKeyCode::WebRefresh), - // => Some(VirtualKeyCode::WebSearch), - // => Some(VirtualKeyCode::WebStop), - // => Some(VirtualKeyCode::Yen), - keysyms::XKB_KEY_XF86Copy => Some(VirtualKeyCode::Copy), - keysyms::XKB_KEY_XF86Paste => Some(VirtualKeyCode::Paste), - keysyms::XKB_KEY_XF86Cut => Some(VirtualKeyCode::Cut), - // fallback - _ => None, - } -} - -impl ModifiersState { - pub(crate) fn from_wayland(mods: keyboard::ModifiersState) -> ModifiersState { - ModifiersState { - shift: mods.shift, - ctrl: mods.ctrl, - alt: mods.alt, - logo: mods.logo, - } - } -} - -fn device_id() -> crate::event::DeviceId { - crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)) -} - -fn mk_root_wid(wid: crate::platform_impl::wayland::WindowId) -> crate::window::WindowId { - crate::window::WindowId(crate::platform_impl::WindowId::Wayland(wid)) -} diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index 09cd66d15a..a63a6f1ef0 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -1,20 +1,21 @@ -#![cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", - target_os = "netbsd", target_os = "openbsd"))] +#![cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] -pub use self::{ - event_loop::{ - EventLoop, EventLoopProxy, EventLoopWindowTarget, MonitorHandle, VideoMode, - WindowEventsSink, - }, - window::Window, -}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; -use smithay_client_toolkit::reexports::client::protocol::wl_surface; +pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; +pub use output::{MonitorHandle, VideoMode}; +pub use window::Window; +mod env; mod event_loop; -mod keyboard; -mod pointer; -mod touch; +mod output; +mod seat; mod window; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -36,6 +37,6 @@ impl WindowId { } #[inline] -fn make_wid(s: &wl_surface::WlSurface) -> WindowId { - WindowId(s.as_ref().c_ptr() as usize) +fn make_wid(surface: &WlSurface) -> WindowId { + WindowId(surface.as_ref().c_ptr() as usize) } diff --git a/src/platform_impl/linux/wayland/output.rs b/src/platform_impl/linux/wayland/output.rs new file mode 100644 index 0000000000..fc4d08737b --- /dev/null +++ b/src/platform_impl/linux/wayland/output.rs @@ -0,0 +1,240 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::Display; + +use sctk::environment::Environment; +use sctk::output::OutputStatusListener; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}; +use crate::platform_impl::platform::{ + MonitorHandle as PlatformMonitorHandle, VideoMode as PlatformVideoMode, +}; + +use super::env::WinitEnv; +use super::event_loop::EventLoopWindowTarget; + +/// Output manager. +pub struct OutputManager { + /// A handle that actually performs all operations on outputs. + handle: OutputManagerHandle, + + _output_listener: OutputStatusListener, +} + +impl OutputManager { + pub fn new(env: &Environment) -> Self { + let handle = OutputManagerHandle::new(); + + // Handle existing outputs. + for output in env.get_all_outputs() { + match sctk::output::with_output_info(&output, |info| info.obsolete) { + Some(false) => (), + // The output is obsolete or we've failed to access its data, skipping. + _ => continue, + } + + // The output is present and unusable, add it to the output manager manager. + handle.add_output(output); + } + + let handle_for_listener = handle.clone(); + + let output_listener = env.listen_for_outputs(move |output, info, _| { + if info.obsolete { + handle_for_listener.remove_output(output) + } else { + handle_for_listener.add_output(output) + } + }); + + Self { + handle, + _output_listener: output_listener, + } + } + + pub fn handle(&self) -> OutputManagerHandle { + self.handle.clone() + } +} + +/// A handle to output manager. +#[derive(Debug, Clone)] +pub struct OutputManagerHandle { + outputs: Arc>>, +} + +impl OutputManagerHandle { + fn new() -> Self { + let outputs = Arc::new(Mutex::new(VecDeque::new())); + Self { outputs } + } + + /// Handle addition of the output. + fn add_output(&self, output: WlOutput) { + let mut outputs = self.outputs.lock().unwrap(); + let position = outputs.iter().position(|handle| handle.proxy == output); + if position.is_none() { + outputs.push_back(MonitorHandle::new(output)); + } + } + + /// Handle removal of the output. + fn remove_output(&self, output: WlOutput) { + let mut outputs = self.outputs.lock().unwrap(); + let position = outputs.iter().position(|handle| handle.proxy == output); + if let Some(position) = position { + outputs.remove(position); + } + } + + /// Get all observed outputs. + pub fn available_outputs(&self) -> VecDeque { + self.outputs.lock().unwrap().clone() + } +} + +#[derive(Clone, Debug)] +pub struct MonitorHandle { + pub(crate) proxy: WlOutput, +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.native_identifier() == other.native_identifier() + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.native_identifier().cmp(&other.native_identifier()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.native_identifier().hash(state); + } +} + +impl MonitorHandle { + #[inline] + pub(crate) fn new(proxy: WlOutput) -> Self { + Self { proxy } + } + + #[inline] + pub fn name(&self) -> Option { + sctk::output::with_output_info(&self.proxy, |info| { + format!("{} ({})", info.model, info.make) + }) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + sctk::output::with_output_info(&self.proxy, |info| info.id).unwrap_or(0) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + match sctk::output::with_output_info(&self.proxy, |info| { + info.modes + .iter() + .find(|mode| mode.is_current) + .map(|mode| mode.dimensions) + }) { + Some(Some((w, h))) => (w as u32, h as u32), + _ => (0, 0), + } + .into() + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + sctk::output::with_output_info(&self.proxy, |info| info.location) + .unwrap_or((0, 0)) + .into() + } + + #[inline] + pub fn scale_factor(&self) -> i32 { + sctk::output::with_output_info(&self.proxy, |info| info.scale_factor).unwrap_or(1) + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let modes = sctk::output::with_output_info(&self.proxy, |info| info.modes.clone()) + .unwrap_or_else(Vec::new); + + let monitor = self.clone(); + + modes.into_iter().map(move |mode| RootVideoMode { + video_mode: PlatformVideoMode::Wayland(VideoMode { + size: (mode.dimensions.0 as u32, mode.dimensions.1 as u32).into(), + refresh_rate: (mode.refresh_rate as f32 / 1000.0).round() as u16, + bit_depth: 32, + monitor: monitor.clone(), + }), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) size: PhysicalSize, + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, +} + +impl VideoMode { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(self.monitor.clone()), + } + } +} + +impl EventLoopWindowTarget { + #[inline] + pub fn display(&self) -> &Display { + &self.display + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + self.output_manager.handle.available_outputs() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // There's no primary monitor on Wayland. + None + } +} diff --git a/src/platform_impl/linux/wayland/pointer.rs b/src/platform_impl/linux/wayland/pointer.rs deleted file mode 100644 index cad299e470..0000000000 --- a/src/platform_impl/linux/wayland/pointer.rs +++ /dev/null @@ -1,268 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crate::event::{ - DeviceEvent, ElementState, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, - WindowEvent, -}; - -use super::{ - event_loop::{CursorManager, WindowEventsSink}, - window::WindowStore, - DeviceId, -}; - -use smithay_client_toolkit::reexports::client::protocol::{ - wl_pointer::{self, Event as PtrEvent, WlPointer}, - wl_seat, -}; - -use smithay_client_toolkit::reexports::protocols::unstable::relative_pointer::v1::client::{ - zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, zwp_relative_pointer_v1::Event, - zwp_relative_pointer_v1::ZwpRelativePointerV1, -}; - -use smithay_client_toolkit::reexports::protocols::unstable::pointer_constraints::v1::client::{ - zwp_locked_pointer_v1::ZwpLockedPointerV1, zwp_pointer_constraints_v1::Lifetime, - zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, -}; - -use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; - -pub fn implement_pointer( - seat: &wl_seat::WlSeat, - sink: Arc>>, - store: Arc>, - modifiers_tracker: Arc>, - cursor_manager: Arc>, -) -> WlPointer { - seat.get_pointer(|pointer| { - let mut mouse_focus = None; - let mut axis_buffer = None; - let mut axis_discrete_buffer = None; - let mut axis_state = TouchPhase::Ended; - - pointer.implement_closure( - move |evt, pointer| { - let mut sink = sink.lock().unwrap(); - let store = store.lock().unwrap(); - let mut cursor_manager = cursor_manager.lock().unwrap(); - match evt { - PtrEvent::Enter { - surface, - surface_x, - surface_y, - .. - } => { - let wid = store.find_wid(&surface); - if let Some(wid) = wid { - mouse_focus = Some(wid); - sink.send_window_event( - WindowEvent::CursorEntered { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - }, - wid, - ); - sink.send_window_event( - WindowEvent::CursorMoved { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - position: (surface_x, surface_y).into(), - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } - - cursor_manager.reload_cursor_style(); - } - PtrEvent::Leave { surface, .. } => { - mouse_focus = None; - let wid = store.find_wid(&surface); - if let Some(wid) = wid { - sink.send_window_event( - WindowEvent::CursorLeft { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - }, - wid, - ); - } - } - PtrEvent::Motion { - surface_x, - surface_y, - .. - } => { - if let Some(wid) = mouse_focus { - sink.send_window_event( - WindowEvent::CursorMoved { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - position: (surface_x, surface_y).into(), - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } - } - PtrEvent::Button { button, state, .. } => { - if let Some(wid) = mouse_focus { - let state = match state { - wl_pointer::ButtonState::Pressed => ElementState::Pressed, - wl_pointer::ButtonState::Released => ElementState::Released, - _ => unreachable!(), - }; - let button = match button { - 0x110 => MouseButton::Left, - 0x111 => MouseButton::Right, - 0x112 => MouseButton::Middle, - // TODO figure out the translation ? - _ => return, - }; - sink.send_window_event( - WindowEvent::MouseInput { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - state, - button, - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } - } - PtrEvent::Axis { axis, value, .. } => { - if let Some(wid) = mouse_focus { - if pointer.as_ref().version() < 5 { - let (mut x, mut y) = (0.0, 0.0); - // old seat compatibility - match axis { - // wayland vertical sign convention is the inverse of winit - wl_pointer::Axis::VerticalScroll => y -= value as f32, - wl_pointer::Axis::HorizontalScroll => x += value as f32, - _ => unreachable!(), - } - sink.send_window_event( - WindowEvent::MouseWheel { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - delta: MouseScrollDelta::PixelDelta( - (x as f64, y as f64).into(), - ), - phase: TouchPhase::Moved, - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } else { - let (mut x, mut y) = axis_buffer.unwrap_or((0.0, 0.0)); - match axis { - // wayland vertical sign convention is the inverse of winit - wl_pointer::Axis::VerticalScroll => y -= value as f32, - wl_pointer::Axis::HorizontalScroll => x += value as f32, - _ => unreachable!(), - } - axis_buffer = Some((x, y)); - axis_state = match axis_state { - TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, - _ => TouchPhase::Started, - } - } - } - } - PtrEvent::Frame => { - let axis_buffer = axis_buffer.take(); - let axis_discrete_buffer = axis_discrete_buffer.take(); - if let Some(wid) = mouse_focus { - if let Some((x, y)) = axis_discrete_buffer { - sink.send_window_event( - WindowEvent::MouseWheel { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - delta: MouseScrollDelta::LineDelta(x as f32, y as f32), - phase: axis_state, - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } else if let Some((x, y)) = axis_buffer { - sink.send_window_event( - WindowEvent::MouseWheel { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - delta: MouseScrollDelta::PixelDelta( - (x as f64, y as f64).into(), - ), - phase: axis_state, - modifiers: modifiers_tracker.lock().unwrap().clone(), - }, - wid, - ); - } - } - } - PtrEvent::AxisSource { .. } => (), - PtrEvent::AxisStop { .. } => { - axis_state = TouchPhase::Ended; - } - PtrEvent::AxisDiscrete { axis, discrete } => { - let (mut x, mut y) = axis_discrete_buffer.unwrap_or((0, 0)); - match axis { - // wayland vertical sign convention is the inverse of winit - wl_pointer::Axis::VerticalScroll => y -= discrete, - wl_pointer::Axis::HorizontalScroll => x += discrete, - _ => unreachable!(), - } - axis_discrete_buffer = Some((x, y)); - axis_state = match axis_state { - TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, - _ => TouchPhase::Started, - } - } - _ => unreachable!(), - } - }, - (), - ) - }) - .unwrap() -} - -pub fn implement_relative_pointer( - sink: Arc>>, - pointer: &WlPointer, - manager: &ZwpRelativePointerManagerV1, -) -> Result { - manager.get_relative_pointer(pointer, |rel_pointer| { - rel_pointer.implement_closure( - move |evt, _rel_pointer| { - let mut sink = sink.lock().unwrap(); - match evt { - Event::RelativeMotion { dx, dy, .. } => sink - .send_device_event(DeviceEvent::MouseMotion { delta: (dx, dy) }, DeviceId), - _ => unreachable!(), - } - }, - (), - ) - }) -} - -pub fn implement_locked_pointer( - surface: &WlSurface, - pointer: &WlPointer, - constraints: &ZwpPointerConstraintsV1, -) -> Result { - constraints.lock_pointer(surface, pointer, None, Lifetime::Persistent.to_raw(), |c| { - c.implement_closure(|_, _| (), ()) - }) -} diff --git a/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs b/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs new file mode 100644 index 0000000000..7c320973b8 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs @@ -0,0 +1,151 @@ +//! Handling of various keyboard events. + +use sctk::reexports::client::protocol::wl_keyboard::KeyState; + +use sctk::seat::keyboard::Event as KeyboardEvent; + +use crate::event::{ElementState, KeyboardInput, ModifiersState, WindowEvent}; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +use super::keymap; +use super::KeyboardInner; + +#[inline] +pub(super) fn handle_keyboard( + event: KeyboardEvent<'_>, + inner: &mut KeyboardInner, + winit_state: &mut WinitState, +) { + let event_sink = &mut winit_state.event_sink; + match event { + KeyboardEvent::Enter { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // Window gained focus. + event_sink.push_window_event(WindowEvent::Focused(true), window_id); + + // Dispatch modifers changes that we've received before getting `Enter` event. + if let Some(modifiers) = inner.pending_modifers_state.take() { + *inner.modifiers_state.borrow_mut() = modifiers; + event_sink.push_window_event(WindowEvent::ModifiersChanged(modifiers), window_id); + } + + inner.target_window_id = Some(window_id); + } + KeyboardEvent::Leave { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // Notify that no modifiers are being pressed. + if !inner.modifiers_state.borrow().is_empty() { + event_sink.push_window_event( + WindowEvent::ModifiersChanged(ModifiersState::empty()), + window_id, + ); + } + + // Window lost focus. + event_sink.push_window_event(WindowEvent::Focused(false), window_id); + + // Reset the id. + inner.target_window_id = None; + } + KeyboardEvent::Key { + rawkey, + keysym, + state, + utf8, + .. + } => { + let window_id = match inner.target_window_id { + Some(window_id) => window_id, + None => return, + }; + + let state = match state { + KeyState::Pressed => ElementState::Pressed, + KeyState::Released => ElementState::Released, + _ => unreachable!(), + }; + + let virtual_keycode = keymap::keysym_to_vkey(keysym); + + event_sink.push_window_event( + #[allow(deprecated)] + WindowEvent::KeyboardInput { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + input: KeyboardInput { + state, + scancode: rawkey, + virtual_keycode, + modifiers: *inner.modifiers_state.borrow(), + }, + is_synthetic: false, + }, + window_id, + ); + + // Send ReceivedCharacter event only on ElementState::Pressed. + if ElementState::Released == state { + return; + } + + if let Some(txt) = utf8 { + for ch in txt.chars() { + event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id); + } + } + } + KeyboardEvent::Repeat { + rawkey, + keysym, + utf8, + .. + } => { + let window_id = match inner.target_window_id { + Some(window_id) => window_id, + None => return, + }; + + let virtual_keycode = keymap::keysym_to_vkey(keysym); + + event_sink.push_window_event( + #[allow(deprecated)] + WindowEvent::KeyboardInput { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + input: KeyboardInput { + state: ElementState::Pressed, + scancode: rawkey, + virtual_keycode, + modifiers: *inner.modifiers_state.borrow(), + }, + is_synthetic: false, + }, + window_id, + ); + + if let Some(txt) = utf8 { + for ch in txt.chars() { + event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id); + } + } + } + KeyboardEvent::Modifiers { modifiers } => { + let modifiers = ModifiersState::from(modifiers); + if let Some(window_id) = inner.target_window_id { + *inner.modifiers_state.borrow_mut() = modifiers; + + event_sink.push_window_event(WindowEvent::ModifiersChanged(modifiers), window_id); + } else { + // Compositor must send modifiers after wl_keyboard::enter, however certain + // compositors are still sending it before, so stash such events and send + // them on wl_keyboard::enter. + inner.pending_modifers_state = Some(modifiers); + } + } + } +} diff --git a/src/platform_impl/linux/wayland/seat/keyboard/keymap.rs b/src/platform_impl/linux/wayland/seat/keyboard/keymap.rs new file mode 100644 index 0000000000..991afff2c9 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/keyboard/keymap.rs @@ -0,0 +1,192 @@ +//! Convert Wayland keys to winit keys. + +use crate::event::VirtualKeyCode; + +pub fn keysym_to_vkey(keysym: u32) -> Option { + use sctk::seat::keyboard::keysyms; + match keysym { + // Numbers. + keysyms::XKB_KEY_1 => Some(VirtualKeyCode::Key1), + keysyms::XKB_KEY_2 => Some(VirtualKeyCode::Key2), + keysyms::XKB_KEY_3 => Some(VirtualKeyCode::Key3), + keysyms::XKB_KEY_4 => Some(VirtualKeyCode::Key4), + keysyms::XKB_KEY_5 => Some(VirtualKeyCode::Key5), + keysyms::XKB_KEY_6 => Some(VirtualKeyCode::Key6), + keysyms::XKB_KEY_7 => Some(VirtualKeyCode::Key7), + keysyms::XKB_KEY_8 => Some(VirtualKeyCode::Key8), + keysyms::XKB_KEY_9 => Some(VirtualKeyCode::Key9), + keysyms::XKB_KEY_0 => Some(VirtualKeyCode::Key0), + // Letters. + keysyms::XKB_KEY_A | keysyms::XKB_KEY_a => Some(VirtualKeyCode::A), + keysyms::XKB_KEY_B | keysyms::XKB_KEY_b => Some(VirtualKeyCode::B), + keysyms::XKB_KEY_C | keysyms::XKB_KEY_c => Some(VirtualKeyCode::C), + keysyms::XKB_KEY_D | keysyms::XKB_KEY_d => Some(VirtualKeyCode::D), + keysyms::XKB_KEY_E | keysyms::XKB_KEY_e => Some(VirtualKeyCode::E), + keysyms::XKB_KEY_F | keysyms::XKB_KEY_f => Some(VirtualKeyCode::F), + keysyms::XKB_KEY_G | keysyms::XKB_KEY_g => Some(VirtualKeyCode::G), + keysyms::XKB_KEY_H | keysyms::XKB_KEY_h => Some(VirtualKeyCode::H), + keysyms::XKB_KEY_I | keysyms::XKB_KEY_i => Some(VirtualKeyCode::I), + keysyms::XKB_KEY_J | keysyms::XKB_KEY_j => Some(VirtualKeyCode::J), + keysyms::XKB_KEY_K | keysyms::XKB_KEY_k => Some(VirtualKeyCode::K), + keysyms::XKB_KEY_L | keysyms::XKB_KEY_l => Some(VirtualKeyCode::L), + keysyms::XKB_KEY_M | keysyms::XKB_KEY_m => Some(VirtualKeyCode::M), + keysyms::XKB_KEY_N | keysyms::XKB_KEY_n => Some(VirtualKeyCode::N), + keysyms::XKB_KEY_O | keysyms::XKB_KEY_o => Some(VirtualKeyCode::O), + keysyms::XKB_KEY_P | keysyms::XKB_KEY_p => Some(VirtualKeyCode::P), + keysyms::XKB_KEY_Q | keysyms::XKB_KEY_q => Some(VirtualKeyCode::Q), + keysyms::XKB_KEY_R | keysyms::XKB_KEY_r => Some(VirtualKeyCode::R), + keysyms::XKB_KEY_S | keysyms::XKB_KEY_s => Some(VirtualKeyCode::S), + keysyms::XKB_KEY_T | keysyms::XKB_KEY_t => Some(VirtualKeyCode::T), + keysyms::XKB_KEY_U | keysyms::XKB_KEY_u => Some(VirtualKeyCode::U), + keysyms::XKB_KEY_V | keysyms::XKB_KEY_v => Some(VirtualKeyCode::V), + keysyms::XKB_KEY_W | keysyms::XKB_KEY_w => Some(VirtualKeyCode::W), + keysyms::XKB_KEY_X | keysyms::XKB_KEY_x => Some(VirtualKeyCode::X), + keysyms::XKB_KEY_Y | keysyms::XKB_KEY_y => Some(VirtualKeyCode::Y), + keysyms::XKB_KEY_Z | keysyms::XKB_KEY_z => Some(VirtualKeyCode::Z), + // Escape. + keysyms::XKB_KEY_Escape => Some(VirtualKeyCode::Escape), + // Function keys. + keysyms::XKB_KEY_F1 => Some(VirtualKeyCode::F1), + keysyms::XKB_KEY_F2 => Some(VirtualKeyCode::F2), + keysyms::XKB_KEY_F3 => Some(VirtualKeyCode::F3), + keysyms::XKB_KEY_F4 => Some(VirtualKeyCode::F4), + keysyms::XKB_KEY_F5 => Some(VirtualKeyCode::F5), + keysyms::XKB_KEY_F6 => Some(VirtualKeyCode::F6), + keysyms::XKB_KEY_F7 => Some(VirtualKeyCode::F7), + keysyms::XKB_KEY_F8 => Some(VirtualKeyCode::F8), + keysyms::XKB_KEY_F9 => Some(VirtualKeyCode::F9), + keysyms::XKB_KEY_F10 => Some(VirtualKeyCode::F10), + keysyms::XKB_KEY_F11 => Some(VirtualKeyCode::F11), + keysyms::XKB_KEY_F12 => Some(VirtualKeyCode::F12), + keysyms::XKB_KEY_F13 => Some(VirtualKeyCode::F13), + keysyms::XKB_KEY_F14 => Some(VirtualKeyCode::F14), + keysyms::XKB_KEY_F15 => Some(VirtualKeyCode::F15), + keysyms::XKB_KEY_F16 => Some(VirtualKeyCode::F16), + keysyms::XKB_KEY_F17 => Some(VirtualKeyCode::F17), + keysyms::XKB_KEY_F18 => Some(VirtualKeyCode::F18), + keysyms::XKB_KEY_F19 => Some(VirtualKeyCode::F19), + keysyms::XKB_KEY_F20 => Some(VirtualKeyCode::F20), + keysyms::XKB_KEY_F21 => Some(VirtualKeyCode::F21), + keysyms::XKB_KEY_F22 => Some(VirtualKeyCode::F22), + keysyms::XKB_KEY_F23 => Some(VirtualKeyCode::F23), + keysyms::XKB_KEY_F24 => Some(VirtualKeyCode::F24), + // Flow control. + keysyms::XKB_KEY_Print => Some(VirtualKeyCode::Snapshot), + keysyms::XKB_KEY_Scroll_Lock => Some(VirtualKeyCode::Scroll), + keysyms::XKB_KEY_Pause => Some(VirtualKeyCode::Pause), + keysyms::XKB_KEY_Insert => Some(VirtualKeyCode::Insert), + keysyms::XKB_KEY_Home => Some(VirtualKeyCode::Home), + keysyms::XKB_KEY_Delete => Some(VirtualKeyCode::Delete), + keysyms::XKB_KEY_End => Some(VirtualKeyCode::End), + keysyms::XKB_KEY_Page_Down => Some(VirtualKeyCode::PageDown), + keysyms::XKB_KEY_Page_Up => Some(VirtualKeyCode::PageUp), + // Arrows. + keysyms::XKB_KEY_Left => Some(VirtualKeyCode::Left), + keysyms::XKB_KEY_Up => Some(VirtualKeyCode::Up), + keysyms::XKB_KEY_Right => Some(VirtualKeyCode::Right), + keysyms::XKB_KEY_Down => Some(VirtualKeyCode::Down), + + keysyms::XKB_KEY_BackSpace => Some(VirtualKeyCode::Back), + keysyms::XKB_KEY_Return => Some(VirtualKeyCode::Return), + keysyms::XKB_KEY_space => Some(VirtualKeyCode::Space), + + keysyms::XKB_KEY_Multi_key => Some(VirtualKeyCode::Compose), + keysyms::XKB_KEY_caret => Some(VirtualKeyCode::Caret), + + // Keypad. + keysyms::XKB_KEY_Num_Lock => Some(VirtualKeyCode::Numlock), + keysyms::XKB_KEY_KP_0 => Some(VirtualKeyCode::Numpad0), + keysyms::XKB_KEY_KP_1 => Some(VirtualKeyCode::Numpad1), + keysyms::XKB_KEY_KP_2 => Some(VirtualKeyCode::Numpad2), + keysyms::XKB_KEY_KP_3 => Some(VirtualKeyCode::Numpad3), + keysyms::XKB_KEY_KP_4 => Some(VirtualKeyCode::Numpad4), + keysyms::XKB_KEY_KP_5 => Some(VirtualKeyCode::Numpad5), + keysyms::XKB_KEY_KP_6 => Some(VirtualKeyCode::Numpad6), + keysyms::XKB_KEY_KP_7 => Some(VirtualKeyCode::Numpad7), + keysyms::XKB_KEY_KP_8 => Some(VirtualKeyCode::Numpad8), + keysyms::XKB_KEY_KP_9 => Some(VirtualKeyCode::Numpad9), + // Misc. + // => Some(VirtualKeyCode::AbntC1), + // => Some(VirtualKeyCode::AbntC2), + keysyms::XKB_KEY_plus => Some(VirtualKeyCode::Plus), + keysyms::XKB_KEY_apostrophe => Some(VirtualKeyCode::Apostrophe), + // => Some(VirtualKeyCode::Apps), + keysyms::XKB_KEY_at => Some(VirtualKeyCode::At), + // => Some(VirtualKeyCode::Ax), + keysyms::XKB_KEY_backslash => Some(VirtualKeyCode::Backslash), + keysyms::XKB_KEY_XF86Calculator => Some(VirtualKeyCode::Calculator), + // => Some(VirtualKeyCode::Capital), + keysyms::XKB_KEY_colon => Some(VirtualKeyCode::Colon), + keysyms::XKB_KEY_comma => Some(VirtualKeyCode::Comma), + // => Some(VirtualKeyCode::Convert), + keysyms::XKB_KEY_equal => Some(VirtualKeyCode::Equals), + keysyms::XKB_KEY_grave => Some(VirtualKeyCode::Grave), + // => Some(VirtualKeyCode::Kana), + keysyms::XKB_KEY_Kanji => Some(VirtualKeyCode::Kanji), + keysyms::XKB_KEY_Alt_L => Some(VirtualKeyCode::LAlt), + keysyms::XKB_KEY_bracketleft => Some(VirtualKeyCode::LBracket), + keysyms::XKB_KEY_Control_L => Some(VirtualKeyCode::LControl), + keysyms::XKB_KEY_Shift_L => Some(VirtualKeyCode::LShift), + keysyms::XKB_KEY_Super_L => Some(VirtualKeyCode::LWin), + keysyms::XKB_KEY_XF86Mail => Some(VirtualKeyCode::Mail), + // => Some(VirtualKeyCode::MediaSelect), + // => Some(VirtualKeyCode::MediaStop), + keysyms::XKB_KEY_minus => Some(VirtualKeyCode::Minus), + keysyms::XKB_KEY_asterisk => Some(VirtualKeyCode::Asterisk), + keysyms::XKB_KEY_XF86AudioMute => Some(VirtualKeyCode::Mute), + // => Some(VirtualKeyCode::MyComputer), + keysyms::XKB_KEY_XF86AudioNext => Some(VirtualKeyCode::NextTrack), + // => Some(VirtualKeyCode::NoConvert), + keysyms::XKB_KEY_KP_Separator => Some(VirtualKeyCode::NumpadComma), + keysyms::XKB_KEY_KP_Enter => Some(VirtualKeyCode::NumpadEnter), + keysyms::XKB_KEY_KP_Equal => Some(VirtualKeyCode::NumpadEquals), + keysyms::XKB_KEY_KP_Add => Some(VirtualKeyCode::NumpadAdd), + keysyms::XKB_KEY_KP_Subtract => Some(VirtualKeyCode::NumpadSubtract), + keysyms::XKB_KEY_KP_Multiply => Some(VirtualKeyCode::NumpadMultiply), + keysyms::XKB_KEY_KP_Divide => Some(VirtualKeyCode::NumpadDivide), + keysyms::XKB_KEY_KP_Decimal => Some(VirtualKeyCode::NumpadDecimal), + keysyms::XKB_KEY_KP_Page_Up => Some(VirtualKeyCode::PageUp), + keysyms::XKB_KEY_KP_Page_Down => Some(VirtualKeyCode::PageDown), + keysyms::XKB_KEY_KP_Home => Some(VirtualKeyCode::Home), + keysyms::XKB_KEY_KP_End => Some(VirtualKeyCode::End), + keysyms::XKB_KEY_KP_Left => Some(VirtualKeyCode::Left), + keysyms::XKB_KEY_KP_Up => Some(VirtualKeyCode::Up), + keysyms::XKB_KEY_KP_Right => Some(VirtualKeyCode::Right), + keysyms::XKB_KEY_KP_Down => Some(VirtualKeyCode::Down), + // => Some(VirtualKeyCode::OEM102), + keysyms::XKB_KEY_period => Some(VirtualKeyCode::Period), + // => Some(VirtualKeyCode::Playpause), + keysyms::XKB_KEY_XF86PowerOff => Some(VirtualKeyCode::Power), + keysyms::XKB_KEY_XF86AudioPrev => Some(VirtualKeyCode::PrevTrack), + keysyms::XKB_KEY_Alt_R => Some(VirtualKeyCode::RAlt), + keysyms::XKB_KEY_bracketright => Some(VirtualKeyCode::RBracket), + keysyms::XKB_KEY_Control_R => Some(VirtualKeyCode::RControl), + keysyms::XKB_KEY_Shift_R => Some(VirtualKeyCode::RShift), + keysyms::XKB_KEY_Super_R => Some(VirtualKeyCode::RWin), + keysyms::XKB_KEY_semicolon => Some(VirtualKeyCode::Semicolon), + keysyms::XKB_KEY_slash => Some(VirtualKeyCode::Slash), + keysyms::XKB_KEY_XF86Sleep => Some(VirtualKeyCode::Sleep), + // => Some(VirtualKeyCode::Stop), + // => Some(VirtualKeyCode::Sysrq), + keysyms::XKB_KEY_Tab => Some(VirtualKeyCode::Tab), + keysyms::XKB_KEY_ISO_Left_Tab => Some(VirtualKeyCode::Tab), + keysyms::XKB_KEY_underscore => Some(VirtualKeyCode::Underline), + // => Some(VirtualKeyCode::Unlabeled), + keysyms::XKB_KEY_XF86AudioLowerVolume => Some(VirtualKeyCode::VolumeDown), + keysyms::XKB_KEY_XF86AudioRaiseVolume => Some(VirtualKeyCode::VolumeUp), + // => Some(VirtualKeyCode::Wake), + // => Some(VirtualKeyCode::Webback), + // => Some(VirtualKeyCode::WebFavorites), + // => Some(VirtualKeyCode::WebForward), + // => Some(VirtualKeyCode::WebHome), + // => Some(VirtualKeyCode::WebRefresh), + // => Some(VirtualKeyCode::WebSearch), + // => Some(VirtualKeyCode::WebStop), + keysyms::XKB_KEY_yen => Some(VirtualKeyCode::Yen), + keysyms::XKB_KEY_XF86Copy => Some(VirtualKeyCode::Copy), + keysyms::XKB_KEY_XF86Paste => Some(VirtualKeyCode::Paste), + keysyms::XKB_KEY_XF86Cut => Some(VirtualKeyCode::Cut), + // Fallback. + _ => None, + } +} diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs new file mode 100644 index 0000000000..1362dcf797 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -0,0 +1,105 @@ +//! Wayland keyboard handling. + +use std::cell::RefCell; +use std::rc::Rc; + +use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::Attached; + +use sctk::reexports::calloop::{LoopHandle, Source}; + +use sctk::seat::keyboard::{self, RepeatSource}; + +use crate::event::ModifiersState; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::WindowId; + +mod handlers; +mod keymap; + +pub(crate) struct Keyboard { + pub keyboard: WlKeyboard, + + /// The source for repeat keys. + pub repeat_source: Option>, + + /// LoopHandle to drop `RepeatSource`, when dropping the keyboard. + pub loop_handle: LoopHandle, +} + +impl Keyboard { + pub fn new( + seat: &Attached, + loop_handle: LoopHandle, + modifiers_state: Rc>, + ) -> Option { + let mut inner = KeyboardInner::new(modifiers_state); + let keyboard_data = keyboard::map_keyboard_repeat( + loop_handle.clone(), + &seat, + None, + keyboard::RepeatKind::System, + move |event, _, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_keyboard(event, &mut inner, winit_state); + }, + ); + + let (keyboard, repeat_source) = keyboard_data.ok()?; + + Some(Self { + keyboard, + loop_handle, + repeat_source: Some(repeat_source), + }) + } +} + +impl Drop for Keyboard { + fn drop(&mut self) { + if self.keyboard.as_ref().version() >= 3 { + self.keyboard.release(); + } + + if let Some(repeat_source) = self.repeat_source.take() { + self.loop_handle.remove(repeat_source); + } + } +} + +struct KeyboardInner { + /// Currently focused surface. + target_window_id: Option, + + /// A pending state of modifiers. + /// + /// This state is getting set if we've got a modifiers update + /// before `Enter` event, which shouldn't happen in general, however + /// some compositors are still doing so. + pending_modifers_state: Option, + + /// Current state of modifiers keys. + modifiers_state: Rc>, +} + +impl KeyboardInner { + fn new(modifiers_state: Rc>) -> Self { + Self { + target_window_id: None, + pending_modifers_state: None, + modifiers_state, + } + } +} + +impl From for ModifiersState { + fn from(mods: keyboard::ModifiersState) -> ModifiersState { + let mut wl_mods = ModifiersState::empty(); + wl_mods.set(ModifiersState::SHIFT, mods.shift); + wl_mods.set(ModifiersState::CTRL, mods.ctrl); + wl_mods.set(ModifiersState::ALT, mods.alt); + wl_mods.set(ModifiersState::LOGO, mods.logo); + wl_mods + } +} diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 0000000000..23098d08c8 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -0,0 +1,208 @@ +//! Seat handling and managing. + +use std::cell::RefCell; +use std::rc::Rc; + +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; + +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::Attached; + +use sctk::environment::Environment; +use sctk::reexports::calloop::LoopHandle; +use sctk::seat::pointer::ThemeManager; +use sctk::seat::{SeatData, SeatListener}; + +use super::env::WinitEnv; +use super::event_loop::WinitState; +use crate::event::ModifiersState; + +mod keyboard; +pub mod pointer; +pub mod text_input; +mod touch; + +use keyboard::Keyboard; +use pointer::Pointers; +use text_input::TextInput; +use touch::Touch; + +pub struct SeatManager { + /// Listener for seats. + _seat_listener: SeatListener, +} + +impl SeatManager { + pub fn new( + env: &Environment, + loop_handle: LoopHandle, + theme_manager: ThemeManager, + ) -> Self { + let relative_pointer_manager = env.get_global::(); + let pointer_constraints = env.get_global::(); + let text_input_manager = env.get_global::(); + + let mut inner = SeatManagerInner::new( + theme_manager, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + loop_handle, + ); + + // Handle existing seats. + for seat in env.get_all_seats() { + let seat_data = match sctk::seat::clone_seat_data(&seat) { + Some(seat_data) => seat_data, + None => continue, + }; + + inner.process_seat_update(&seat, &seat_data); + } + + let seat_listener = env.listen_for_seats(move |seat, seat_data, _| { + inner.process_seat_update(&seat, &seat_data); + }); + + Self { + _seat_listener: seat_listener, + } + } +} + +/// Inner state of the seat manager. +struct SeatManagerInner { + /// Currently observed seats. + seats: Vec, + + /// Loop handle. + loop_handle: LoopHandle, + + /// Relative pointer manager. + relative_pointer_manager: Option>, + + /// Pointer constraints. + pointer_constraints: Option>, + + /// Text input manager. + text_input_manager: Option>, + + /// A theme manager. + theme_manager: ThemeManager, +} + +impl SeatManagerInner { + fn new( + theme_manager: ThemeManager, + relative_pointer_manager: Option>, + pointer_constraints: Option>, + text_input_manager: Option>, + loop_handle: LoopHandle, + ) -> Self { + Self { + seats: Vec::new(), + loop_handle, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + theme_manager, + } + } + + /// Handle seats update from the `SeatListener`. + pub fn process_seat_update(&mut self, seat: &Attached, seat_data: &SeatData) { + let detached_seat = seat.detach(); + + let position = self.seats.iter().position(|si| si.seat == detached_seat); + let index = position.unwrap_or_else(|| { + self.seats.push(SeatInfo::new(detached_seat)); + self.seats.len() - 1 + }); + + let seat_info = &mut self.seats[index]; + + // Pointer handling. + if seat_data.has_pointer && !seat_data.defunct { + if seat_info.pointer.is_none() { + seat_info.pointer = Some(Pointers::new( + &seat, + &self.theme_manager, + &self.relative_pointer_manager, + &self.pointer_constraints, + seat_info.modifiers_state.clone(), + )); + } + } else { + seat_info.pointer = None; + } + + // Handle keyboard. + if seat_data.has_keyboard && !seat_data.defunct { + if seat_info.keyboard.is_none() { + seat_info.keyboard = Keyboard::new( + &seat, + self.loop_handle.clone(), + seat_info.modifiers_state.clone(), + ); + } + } else { + seat_info.keyboard = None; + } + + // Handle touch. + if seat_data.has_touch && !seat_data.defunct { + if seat_info.touch.is_none() { + seat_info.touch = Some(Touch::new(&seat)); + } + } else { + seat_info.touch = None; + } + + // Handle text input. + if let Some(text_input_manager) = self.text_input_manager.as_ref() { + if seat_data.defunct { + seat_info.text_input = None; + } else if seat_info.text_input.is_none() { + seat_info.text_input = Some(TextInput::new(&seat, &text_input_manager)); + } + } + } +} + +/// Resources associtated with a given seat. +struct SeatInfo { + /// Seat to which this `SeatInfo` belongs. + seat: WlSeat, + + /// A keyboard handle with its repeat rate handling. + keyboard: Option, + + /// All pointers we're using on a seat. + pointer: Option, + + /// Touch handling. + touch: Option, + + /// Text input handling aka IME. + text_input: Option, + + /// The current state of modifiers observed in keyboard handler. + /// + /// We keep modifiers state on a seat, since it's being used by pointer events as well. + modifiers_state: Rc>, +} + +impl SeatInfo { + pub fn new(seat: WlSeat) -> Self { + Self { + seat, + keyboard: None, + pointer: None, + touch: None, + text_input: None, + modifiers_state: Rc::new(RefCell::new(ModifiersState::default())), + } + } +} diff --git a/src/platform_impl/linux/wayland/seat/pointer/data.rs b/src/platform_impl/linux/wayland/seat/pointer/data.rs new file mode 100644 index 0000000000..1da60d3526 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/pointer/data.rs @@ -0,0 +1,74 @@ +//! Data which is used in pointer callbacks. + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Attached; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1}; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; + +use crate::event::{ModifiersState, TouchPhase}; + +/// A data being used by pointer handlers. +pub(super) struct PointerData { + /// Winit's surface the pointer is currently over. + pub surface: Option, + + /// Current modifiers state. + /// + /// This refers a state of modifiers from `WlKeyboard` on + /// the given seat. + pub modifiers_state: Rc>, + + /// Pointer constraints. + pub pointer_constraints: Option>, + + pub confined_pointer: Rc>>, + + /// A latest event serial. + pub latest_serial: Rc>, + + /// The currently accumulated axis data on a pointer. + pub axis_data: AxisData, +} + +impl PointerData { + pub fn new( + confined_pointer: Rc>>, + pointer_constraints: Option>, + modifiers_state: Rc>, + ) -> Self { + Self { + surface: None, + latest_serial: Rc::new(Cell::new(0)), + confined_pointer, + modifiers_state, + pointer_constraints, + axis_data: AxisData::new(), + } + } +} + +/// Axis data. +#[derive(Clone, Copy)] +pub(super) struct AxisData { + /// Current state of the axis. + pub axis_state: TouchPhase, + + /// A buffer for `PixelDelta` event. + pub axis_buffer: Option<(f32, f32)>, + + /// A buffer for `LineDelta` event. + pub axis_discrete_buffer: Option<(f32, f32)>, +} + +impl AxisData { + pub fn new() -> Self { + Self { + axis_state: TouchPhase::Ended, + axis_buffer: None, + axis_discrete_buffer: None, + } + } +} diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs new file mode 100644 index 0000000000..7d291713fa --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -0,0 +1,301 @@ +//! Handlers for the pointers we're using. + +use std::cell::RefCell; +use std::rc::Rc; + +use sctk::reexports::client::protocol::wl_pointer::{self, Event as PointerEvent}; +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::Event as RelativePointerEvent; + +use sctk::seat::pointer::ThemedPointer; + +use crate::dpi::LogicalPosition; +use crate::event::{ + DeviceEvent, ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, +}; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +use super::{PointerData, WinitPointer}; + +// These values are comming from . +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; +const BTN_MIDDLE: u32 = 0x112; + +#[inline] +pub(super) fn handle_pointer( + pointer: ThemedPointer, + event: PointerEvent, + pointer_data: &Rc>, + winit_state: &mut WinitState, +) { + let event_sink = &mut winit_state.event_sink; + let mut pointer_data = pointer_data.borrow_mut(); + match event { + PointerEvent::Enter { + surface, + surface_x, + surface_y, + serial, + .. + } => { + pointer_data.latest_serial.replace(serial); + + let window_id = wayland::make_wid(&surface); + if !winit_state.window_map.contains_key(&window_id) { + return; + } + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + + let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; + pointer_data.surface = Some(surface); + + // Notify window that pointer entered the surface. + let winit_pointer = WinitPointer { + pointer, + confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + pointer_constraints: pointer_data.pointer_constraints.clone(), + latest_serial: pointer_data.latest_serial.clone(), + }; + window_handle.pointer_entered(winit_pointer); + + event_sink.push_window_event( + WindowEvent::CursorEntered { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + }, + window_id, + ); + + let position = LogicalPosition::new(surface_x, surface_y).to_physical(scale_factor); + + event_sink.push_window_event( + WindowEvent::CursorMoved { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + position, + modifiers: *pointer_data.modifiers_state.borrow(), + }, + window_id, + ); + } + PointerEvent::Leave { surface, serial } => { + pointer_data.surface = None; + pointer_data.latest_serial.replace(serial); + + let window_id = wayland::make_wid(&surface); + + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + + // Notify a window that pointer is no longer observing it. + let winit_pointer = WinitPointer { + pointer, + confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + pointer_constraints: pointer_data.pointer_constraints.clone(), + latest_serial: pointer_data.latest_serial.clone(), + }; + window_handle.pointer_left(winit_pointer); + + event_sink.push_window_event( + WindowEvent::CursorLeft { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + }, + window_id, + ); + } + PointerEvent::Motion { + surface_x, + surface_y, + .. + } => { + let surface = match pointer_data.surface.as_ref() { + Some(surface) => surface, + None => return, + }; + + let window_id = wayland::make_wid(surface); + + let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; + let position = LogicalPosition::new(surface_x, surface_y).to_physical(scale_factor); + + event_sink.push_window_event( + WindowEvent::CursorMoved { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + position, + modifiers: *pointer_data.modifiers_state.borrow(), + }, + window_id, + ); + } + PointerEvent::Button { + button, + state, + serial, + .. + } => { + pointer_data.latest_serial.replace(serial); + let window_id = match pointer_data.surface.as_ref().map(wayland::make_wid) { + Some(window_id) => window_id, + None => return, + }; + + let state = match state { + wl_pointer::ButtonState::Pressed => ElementState::Pressed, + wl_pointer::ButtonState::Released => ElementState::Released, + _ => unreachable!(), + }; + + let button = match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + button => MouseButton::Other(button as u16), + }; + + event_sink.push_window_event( + WindowEvent::MouseInput { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + state, + button, + modifiers: *pointer_data.modifiers_state.borrow(), + }, + window_id, + ); + } + PointerEvent::Axis { axis, value, .. } => { + let surface = match pointer_data.surface.as_ref() { + Some(surface) => surface, + None => return, + }; + + let window_id = wayland::make_wid(&surface); + + if pointer.as_ref().version() < 5 { + let (mut x, mut y) = (0.0, 0.0); + + // Old seat compatibility. + match axis { + // Wayland vertical sign convention is the inverse of winit. + wl_pointer::Axis::VerticalScroll => y -= value as f32, + wl_pointer::Axis::HorizontalScroll => x += value as f32, + _ => unreachable!(), + } + + let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; + let delta = LogicalPosition::new(x as f64, y as f64).to_physical(scale_factor); + + event_sink.push_window_event( + WindowEvent::MouseWheel { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + delta: MouseScrollDelta::PixelDelta(delta), + phase: TouchPhase::Moved, + modifiers: *pointer_data.modifiers_state.borrow(), + }, + window_id, + ); + } else { + let (mut x, mut y) = pointer_data.axis_data.axis_buffer.unwrap_or((0.0, 0.0)); + match axis { + // Wayland vertical sign convention is the inverse of winit. + wl_pointer::Axis::VerticalScroll => y -= value as f32, + wl_pointer::Axis::HorizontalScroll => x += value as f32, + _ => unreachable!(), + } + + pointer_data.axis_data.axis_buffer = Some((x, y)); + + pointer_data.axis_data.axis_state = match pointer_data.axis_data.axis_state { + TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, + _ => TouchPhase::Started, + } + } + } + PointerEvent::AxisDiscrete { axis, discrete } => { + let (mut x, mut y) = pointer_data + .axis_data + .axis_discrete_buffer + .unwrap_or((0., 0.)); + + match axis { + // Wayland vertical sign convention is the inverse of winit. + wl_pointer::Axis::VerticalScroll => y -= discrete as f32, + wl_pointer::Axis::HorizontalScroll => x += discrete as f32, + _ => unreachable!(), + } + + pointer_data.axis_data.axis_discrete_buffer = Some((x, y)); + + pointer_data.axis_data.axis_state = match pointer_data.axis_data.axis_state { + TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, + _ => TouchPhase::Started, + } + } + PointerEvent::AxisSource { .. } => (), + PointerEvent::AxisStop { .. } => { + pointer_data.axis_data.axis_state = TouchPhase::Ended; + } + PointerEvent::Frame => { + let axis_buffer = pointer_data.axis_data.axis_buffer.take(); + let axis_discrete_buffer = pointer_data.axis_data.axis_discrete_buffer.take(); + + let surface = match pointer_data.surface.as_ref() { + Some(surface) => surface, + None => return, + }; + let window_id = wayland::make_wid(&surface); + + let window_event = if let Some((x, y)) = axis_discrete_buffer { + WindowEvent::MouseWheel { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + delta: MouseScrollDelta::LineDelta(x, y), + phase: pointer_data.axis_data.axis_state, + modifiers: *pointer_data.modifiers_state.borrow(), + } + } else if let Some((x, y)) = axis_buffer { + let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; + let delta = LogicalPosition::new(x, y).to_physical(scale_factor); + + WindowEvent::MouseWheel { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + delta: MouseScrollDelta::PixelDelta(delta), + phase: pointer_data.axis_data.axis_state, + modifiers: *pointer_data.modifiers_state.borrow(), + } + } else { + return; + }; + + event_sink.push_window_event(window_event, window_id); + } + _ => (), + } +} + +#[inline] +pub(super) fn handle_relative_pointer(event: RelativePointerEvent, winit_state: &mut WinitState) { + if let RelativePointerEvent::RelativeMotion { dx, dy, .. } = event { + winit_state + .event_sink + .push_device_event(DeviceEvent::MouseMotion { delta: (dx, dy) }, DeviceId) + } +} diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs new file mode 100644 index 0000000000..5debc8cbcf --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -0,0 +1,242 @@ +//! All pointer related handling. + +use std::cell::{Cell, RefCell}; +use std::rc::{Rc, Weak}; + +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Attached; +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1, Lifetime}; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; + +use sctk::seat::pointer::{ThemeManager, ThemedPointer}; + +use crate::event::ModifiersState; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::window::CursorIcon; + +mod data; +mod handlers; + +use data::PointerData; + +/// A proxy to Wayland pointer, which serves requests from a `WindowHandle`. +pub struct WinitPointer { + pointer: ThemedPointer, + + /// Create confined pointers. + pointer_constraints: Option>, + + /// Cursor to handle confine requests. + confined_pointer: Weak>>, + + /// Latest observed serial in pointer events. + latest_serial: Rc>, +} + +impl PartialEq for WinitPointer { + fn eq(&self, other: &Self) -> bool { + *self.pointer == *other.pointer + } +} + +impl Eq for WinitPointer {} + +impl WinitPointer { + /// Set the cursor icon. + /// + /// Providing `None` will hide the cursor. + pub fn set_cursor(&self, cursor_icon: Option) { + let cursor_icon = match cursor_icon { + Some(cursor_icon) => cursor_icon, + None => { + // Hide the cursor. + (*self.pointer).set_cursor(self.latest_serial.get(), None, 0, 0); + return; + } + }; + + let cursors: &[&str] = match cursor_icon { + CursorIcon::Alias => &["link"], + CursorIcon::Arrow => &["arrow"], + CursorIcon::Cell => &["plus"], + CursorIcon::Copy => &["copy"], + CursorIcon::Crosshair => &["crosshair"], + CursorIcon::Default => &["left_ptr"], + CursorIcon::Hand => &["hand"], + CursorIcon::Help => &["question_arrow"], + CursorIcon::Move => &["move"], + CursorIcon::Grab => &["openhand", "grab"], + CursorIcon::Grabbing => &["closedhand", "grabbing"], + CursorIcon::Progress => &["progress"], + CursorIcon::AllScroll => &["all-scroll"], + CursorIcon::ContextMenu => &["context-menu"], + + CursorIcon::NoDrop => &["no-drop", "circle"], + CursorIcon::NotAllowed => &["crossed_circle"], + + // Resize cursors + CursorIcon::EResize => &["right_side"], + CursorIcon::NResize => &["top_side"], + CursorIcon::NeResize => &["top_right_corner"], + CursorIcon::NwResize => &["top_left_corner"], + CursorIcon::SResize => &["bottom_side"], + CursorIcon::SeResize => &["bottom_right_corner"], + CursorIcon::SwResize => &["bottom_left_corner"], + CursorIcon::WResize => &["left_side"], + CursorIcon::EwResize => &["h_double_arrow"], + CursorIcon::NsResize => &["v_double_arrow"], + CursorIcon::NwseResize => &["bd_double_arrow", "size_bdiag"], + CursorIcon::NeswResize => &["fd_double_arrow", "size_fdiag"], + CursorIcon::ColResize => &["split_h", "h_double_arrow"], + CursorIcon::RowResize => &["split_v", "v_double_arrow"], + CursorIcon::Text => &["text", "xterm"], + CursorIcon::VerticalText => &["vertical-text"], + + CursorIcon::Wait => &["watch"], + + CursorIcon::ZoomIn => &["zoom-in"], + CursorIcon::ZoomOut => &["zoom-out"], + }; + + let serial = Some(self.latest_serial.get()); + for cursor in cursors { + if self.pointer.set_cursor(cursor, serial).is_ok() { + break; + } + } + } + + /// Confine the pointer to a surface. + pub fn confine(&self, surface: &WlSurface) { + let pointer_constraints = match &self.pointer_constraints { + Some(pointer_constraints) => pointer_constraints, + None => return, + }; + + let confined_pointer = match self.confined_pointer.upgrade() { + Some(confined_pointer) => confined_pointer, + // A pointer is gone. + None => return, + }; + + *confined_pointer.borrow_mut() = Some(init_confined_pointer( + &pointer_constraints, + &surface, + &*self.pointer, + )); + } + + /// Tries to unconfine the pointer if the current pointer is confined. + pub fn unconfine(&self) { + let confined_pointer = match self.confined_pointer.upgrade() { + Some(confined_pointer) => confined_pointer, + // A pointer is gone. + None => return, + }; + + let mut confined_pointer = confined_pointer.borrow_mut(); + + if let Some(confined_pointer) = confined_pointer.take() { + confined_pointer.destroy(); + } + } +} + +/// A pointer wrapper for easy releasing and managing pointers. +pub(super) struct Pointers { + /// A pointer itself. + pointer: ThemedPointer, + + /// A relative pointer handler. + relative_pointer: Option, + + /// Confined pointer. + confined_pointer: Rc>>, +} + +impl Pointers { + pub(super) fn new( + seat: &Attached, + theme_manager: &ThemeManager, + relative_pointer_manager: &Option>, + pointer_constraints: &Option>, + modifiers_state: Rc>, + ) -> Self { + let confined_pointer = Rc::new(RefCell::new(None)); + let pointer_data = Rc::new(RefCell::new(PointerData::new( + confined_pointer.clone(), + pointer_constraints.clone(), + modifiers_state, + ))); + let pointer = theme_manager.theme_pointer_with_impl( + seat, + move |event, pointer, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_pointer(pointer, event, &pointer_data, winit_state); + }, + ); + + // Setup relative_pointer if it's available. + let relative_pointer = match relative_pointer_manager.as_ref() { + Some(relative_pointer_manager) => { + Some(init_relative_pointer(&relative_pointer_manager, &*pointer)) + } + None => None, + }; + + Self { + pointer, + relative_pointer, + confined_pointer, + } + } +} + +impl Drop for Pointers { + fn drop(&mut self) { + // Drop relative pointer. + if let Some(relative_pointer) = self.relative_pointer.take() { + relative_pointer.destroy(); + } + + // Drop confined pointer. + if let Some(confined_pointer) = self.confined_pointer.borrow_mut().take() { + confined_pointer.destroy(); + } + + // Drop the pointer itself in case it's possible. + if self.pointer.as_ref().version() >= 3 { + self.pointer.release(); + } + } +} + +pub(super) fn init_relative_pointer( + relative_pointer_manager: &ZwpRelativePointerManagerV1, + pointer: &WlPointer, +) -> ZwpRelativePointerV1 { + let relative_pointer = relative_pointer_manager.get_relative_pointer(&*pointer); + relative_pointer.quick_assign(move |_, event, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_relative_pointer(event, winit_state); + }); + + relative_pointer.detach() +} + +pub(super) fn init_confined_pointer( + pointer_constraints: &Attached, + surface: &WlSurface, + pointer: &WlPointer, +) -> ZwpConfinedPointerV1 { + let confined_pointer = + pointer_constraints.confine_pointer(surface, pointer, None, Lifetime::Persistent.to_raw()); + + confined_pointer.quick_assign(move |_, _, _| {}); + + confined_pointer.detach() +} diff --git a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs new file mode 100644 index 0000000000..4ba13d6715 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs @@ -0,0 +1,78 @@ +//! Handling of IME events. + +use sctk::reexports::client::Main; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_v3::{ + Event as TextInputEvent, ZwpTextInputV3, +}; + +use crate::event::WindowEvent; +use crate::platform_impl::wayland; +use crate::platform_impl::wayland::event_loop::WinitState; + +use super::{TextInputHandler, TextInputInner}; + +#[inline] +pub(super) fn handle_text_input( + text_input: Main, + inner: &mut TextInputInner, + event: TextInputEvent, + winit_state: &mut WinitState, +) { + let event_sink = &mut winit_state.event_sink; + match event { + TextInputEvent::Enter { surface } => { + let window_id = wayland::make_wid(&surface); + + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + inner.target_window_id = Some(window_id); + + // Enable text input on that surface. + text_input.enable(); + text_input.commit(); + + // Notify a window we're currently over about text input handler. + let text_input_handler = TextInputHandler { + text_input: text_input.detach(), + }; + window_handle.text_input_entered(text_input_handler); + } + TextInputEvent::Leave { surface } => { + // Always issue a disable. + text_input.disable(); + text_input.commit(); + + let window_id = wayland::make_wid(&surface); + + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + + inner.target_window_id = None; + + // Remove text input handler from the window we're leaving. + let text_input_handler = TextInputHandler { + text_input: text_input.detach(), + }; + window_handle.text_input_left(text_input_handler); + } + TextInputEvent::CommitString { text } => { + // Update currenly commited string. + inner.commit_string = text; + } + TextInputEvent::Done { .. } => { + let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) { + (Some(window_id), Some(text)) => (window_id, text), + _ => return, + }; + + for ch in text.chars() { + event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id); + } + } + _ => (), + } +} diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs new file mode 100644 index 0000000000..77f4ff0827 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -0,0 +1,66 @@ +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::Attached; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_v3::ZwpTextInputV3; + +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::WindowId; + +mod handlers; + +/// A handler for text input that we're advertising for `WindowHandle`. +#[derive(Eq, PartialEq)] +pub struct TextInputHandler { + text_input: ZwpTextInputV3, +} + +impl TextInputHandler { + #[inline] + pub fn set_ime_position(&self, x: i32, y: i32) { + self.text_input.set_cursor_rectangle(x, y, 0, 0); + self.text_input.commit(); + } +} + +/// A wrapper around text input to automatically destroy the object on `Drop`. +pub struct TextInput { + text_input: Attached, +} + +impl TextInput { + pub fn new(seat: &Attached, text_input_manager: &ZwpTextInputManagerV3) -> Self { + let text_input = text_input_manager.get_text_input(seat); + let mut text_input_inner = TextInputInner::new(); + text_input.quick_assign(move |text_input, event, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_text_input(text_input, &mut text_input_inner, event, winit_state); + }); + + let text_input: Attached = text_input.into(); + + Self { text_input } + } +} + +impl Drop for TextInput { + fn drop(&mut self) { + self.text_input.destroy(); + } +} + +struct TextInputInner { + /// Currently focused surface. + target_window_id: Option, + + /// Pending string to commit. + commit_string: Option, +} + +impl TextInputInner { + fn new() -> Self { + Self { + target_window_id: None, + commit_string: None, + } + } +} diff --git a/src/platform_impl/linux/wayland/seat/touch/handlers.rs b/src/platform_impl/linux/wayland/seat/touch/handlers.rs new file mode 100644 index 0000000000..8a17b39372 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/touch/handlers.rs @@ -0,0 +1,122 @@ +//! Various handlers for touch events. + +use sctk::reexports::client::protocol::wl_touch::Event as TouchEvent; + +use crate::dpi::LogicalPosition; +use crate::event::{TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +use super::{TouchInner, TouchPoint}; + +/// Handle WlTouch events. +#[inline] +pub(super) fn handle_touch( + event: TouchEvent, + inner: &mut TouchInner, + winit_state: &mut WinitState, +) { + let event_sink = &mut winit_state.event_sink; + + match event { + TouchEvent::Down { + surface, id, x, y, .. + } => { + let window_id = wayland::make_wid(&surface); + if !winit_state.window_map.contains_key(&window_id) { + return; + } + + let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; + let position = LogicalPosition::new(x, y); + + event_sink.push_window_event( + WindowEvent::Touch(crate::event::Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Started, + location: position.to_physical(scale_factor), + force: None, // TODO + id: id as u64, + }), + window_id, + ); + + inner + .touch_points + .push(TouchPoint::new(surface, position, id)); + } + TouchEvent::Up { id, .. } => { + let touch_point = match inner.touch_points.iter().find(|p| p.id == id) { + Some(touch_point) => touch_point, + None => return, + }; + + let scale_factor = sctk::get_surface_scale_factor(&touch_point.surface) as f64; + let location = touch_point.position.to_physical(scale_factor); + let window_id = wayland::make_wid(&touch_point.surface); + + event_sink.push_window_event( + WindowEvent::Touch(crate::event::Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Ended, + location, + force: None, // TODO + id: id as u64, + }), + window_id, + ); + } + TouchEvent::Motion { id, x, y, .. } => { + let touch_point = match inner.touch_points.iter_mut().find(|p| p.id == id) { + Some(touch_point) => touch_point, + None => return, + }; + + touch_point.position = LogicalPosition::new(x, y); + + let scale_factor = sctk::get_surface_scale_factor(&touch_point.surface) as f64; + let location = touch_point.position.to_physical(scale_factor); + let window_id = wayland::make_wid(&touch_point.surface); + + event_sink.push_window_event( + WindowEvent::Touch(crate::event::Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Moved, + location, + force: None, // TODO + id: id as u64, + }), + window_id, + ); + } + TouchEvent::Frame => (), + TouchEvent::Cancel => { + for touch_point in inner.touch_points.drain(..) { + let scale_factor = sctk::get_surface_scale_factor(&touch_point.surface) as f64; + let location = touch_point.position.to_physical(scale_factor); + let window_id = wayland::make_wid(&touch_point.surface); + + event_sink.push_window_event( + WindowEvent::Touch(crate::event::Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Cancelled, + location, + force: None, // TODO + id: touch_point.id as u64, + }), + window_id, + ); + } + } + _ => (), + } +} diff --git a/src/platform_impl/linux/wayland/seat/touch/mod.rs b/src/platform_impl/linux/wayland/seat/touch/mod.rs new file mode 100644 index 0000000000..197e9ac9c5 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -0,0 +1,78 @@ +//! Touch handling. + +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::Attached; + +use crate::dpi::LogicalPosition; + +use crate::platform_impl::wayland::event_loop::WinitState; + +mod handlers; + +/// Wrapper around touch to handle release. +pub struct Touch { + /// Proxy to touch. + touch: WlTouch, +} + +impl Touch { + pub fn new(seat: &Attached) -> Self { + let touch = seat.get_touch(); + let mut inner = TouchInner::new(); + + touch.quick_assign(move |_, event, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_touch(event, &mut inner, winit_state); + }); + + Self { + touch: touch.detach(), + } + } +} + +impl Drop for Touch { + fn drop(&mut self) { + if self.touch.as_ref().version() >= 3 { + self.touch.release(); + } + } +} + +/// The data used by touch handlers. +pub(super) struct TouchInner { + /// Current touch points. + touch_points: Vec, +} + +impl TouchInner { + fn new() -> Self { + Self { + touch_points: Vec::new(), + } + } +} + +/// Location of touch press. +pub(super) struct TouchPoint { + /// A surface where the touch point is located. + surface: WlSurface, + + /// Location of the touch point. + position: LogicalPosition, + + /// Id. + id: i32, +} + +impl TouchPoint { + pub fn new(surface: WlSurface, position: LogicalPosition, id: i32) -> Self { + Self { + surface, + position, + id, + } + } +} diff --git a/src/platform_impl/linux/wayland/touch.rs b/src/platform_impl/linux/wayland/touch.rs deleted file mode 100644 index 33644a863e..0000000000 --- a/src/platform_impl/linux/wayland/touch.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crate::event::{TouchPhase, WindowEvent}; - -use super::{event_loop::WindowEventsSink, window::WindowStore, DeviceId, WindowId}; - -use smithay_client_toolkit::reexports::client::protocol::{ - wl_seat, - wl_touch::{Event as TouchEvent, WlTouch}, -}; - -struct TouchPoint { - wid: WindowId, - location: (f64, f64), - id: i32, -} - -pub(crate) fn implement_touch( - seat: &wl_seat::WlSeat, - sink: Arc>>, - store: Arc>, -) -> WlTouch { - let mut pending_ids = Vec::new(); - seat.get_touch(|touch| { - touch.implement_closure( - move |evt, _| { - let mut sink = sink.lock().unwrap(); - let store = store.lock().unwrap(); - match evt { - TouchEvent::Down { - surface, id, x, y, .. - } => { - let wid = store.find_wid(&surface); - if let Some(wid) = wid { - sink.send_window_event( - WindowEvent::Touch(crate::event::Touch { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - phase: TouchPhase::Started, - location: (x, y).into(), - force: None, // TODO - id: id as u64, - }), - wid, - ); - pending_ids.push(TouchPoint { - wid, - location: (x, y), - id, - }); - } - } - TouchEvent::Up { id, .. } => { - let idx = pending_ids.iter().position(|p| p.id == id); - if let Some(idx) = idx { - let pt = pending_ids.remove(idx); - sink.send_window_event( - WindowEvent::Touch(crate::event::Touch { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - phase: TouchPhase::Ended, - location: pt.location.into(), - force: None, // TODO - id: id as u64, - }), - pt.wid, - ); - } - } - TouchEvent::Motion { id, x, y, .. } => { - let pt = pending_ids.iter_mut().find(|p| p.id == id); - if let Some(pt) = pt { - pt.location = (x, y); - sink.send_window_event( - WindowEvent::Touch(crate::event::Touch { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - phase: TouchPhase::Moved, - location: (x, y).into(), - force: None, // TODO - id: id as u64, - }), - pt.wid, - ); - } - } - TouchEvent::Frame => (), - TouchEvent::Cancel => { - for pt in pending_ids.drain(..) { - sink.send_window_event( - WindowEvent::Touch(crate::event::Touch { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - phase: TouchPhase::Cancelled, - location: pt.location.into(), - force: None, // TODO - id: pt.id as u64, - }), - pt.wid, - ); - } - } - _ => unreachable!(), - } - }, - (), - ) - }) - .unwrap() -} diff --git a/src/platform_impl/linux/wayland/window.rs b/src/platform_impl/linux/wayland/window.rs deleted file mode 100644 index 49849ba6cc..0000000000 --- a/src/platform_impl/linux/wayland/window.rs +++ /dev/null @@ -1,472 +0,0 @@ -use raw_window_handle::unix::WaylandHandle; -use std::{ - collections::VecDeque, - mem::replace, - sync::{Arc, Mutex, Weak}, -}; - -use crate::{ - dpi::{LogicalPosition, LogicalSize}, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - monitor::MonitorHandle as RootMonitorHandle, - platform_impl::{ - platform::wayland::event_loop::{available_monitors, primary_monitor}, - MonitorHandle as PlatformMonitorHandle, - PlatformSpecificWindowBuilderAttributes as PlAttributes, - }, - window::{CursorIcon, Fullscreen, WindowAttributes}, -}; - -use smithay_client_toolkit::{ - output::OutputMgr, - reexports::client::{ - protocol::{wl_seat, wl_surface}, - Display, - }, - surface::{get_dpi_factor, get_outputs}, - window::{ConceptFrame, Event as WEvent, State as WState, Theme, Window as SWindow}, -}; - -use super::{event_loop::CursorManager, make_wid, EventLoopWindowTarget, MonitorHandle, WindowId}; - -pub struct Window { - surface: wl_surface::WlSurface, - frame: Arc>>, - cursor_manager: Arc>, - outputs: OutputMgr, // Access to info for all monitors - size: Arc>, - kill_switch: (Arc>, Arc>), - display: Arc, - need_frame_refresh: Arc>, - need_refresh: Arc>, - fullscreen: Arc>, - cursor_grab_changed: Arc>>, // Update grab state -} - -impl Window { - pub fn new( - evlp: &EventLoopWindowTarget, - attributes: WindowAttributes, - pl_attribs: PlAttributes, - ) -> Result { - let (width, height) = attributes.inner_size.map(Into::into).unwrap_or((800, 600)); - // Create the window - let size = Arc::new(Mutex::new((width, height))); - let fullscreen = Arc::new(Mutex::new(false)); - - let window_store = evlp.store.clone(); - let cursor_manager = evlp.cursor_manager.clone(); - let surface = evlp.env.create_surface(move |dpi, surface| { - window_store.lock().unwrap().dpi_change(&surface, dpi); - surface.set_buffer_scale(dpi); - }); - - let window_store = evlp.store.clone(); - let my_surface = surface.clone(); - let mut frame = SWindow::::init_from_env( - &evlp.env, - surface.clone(), - (width, height), - move |event| match event { - WEvent::Configure { new_size, states } => { - let mut store = window_store.lock().unwrap(); - let is_fullscreen = states.contains(&WState::Fullscreen); - - for window in &mut store.windows { - if window.surface.as_ref().equals(&my_surface.as_ref()) { - window.newsize = new_size; - *(window.need_refresh.lock().unwrap()) = true; - *(window.fullscreen.lock().unwrap()) = is_fullscreen; - *(window.need_frame_refresh.lock().unwrap()) = true; - return; - } - } - } - WEvent::Refresh => { - let store = window_store.lock().unwrap(); - for window in &store.windows { - if window.surface.as_ref().equals(&my_surface.as_ref()) { - *(window.need_frame_refresh.lock().unwrap()) = true; - return; - } - } - } - WEvent::Close => { - let mut store = window_store.lock().unwrap(); - for window in &mut store.windows { - if window.surface.as_ref().equals(&my_surface.as_ref()) { - window.closed = true; - return; - } - } - } - }, - ) - .unwrap(); - - if let Some(app_id) = pl_attribs.app_id { - frame.set_app_id(app_id); - } - - for &(_, ref seat) in evlp.seats.lock().unwrap().iter() { - frame.new_seat(seat); - } - - // Check for fullscreen requirements - match attributes.fullscreen { - Some(Fullscreen::Exclusive(_)) => { - panic!("Wayland doesn't support exclusive fullscreen") - } - Some(Fullscreen::Borderless(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - })) => frame.set_fullscreen(Some(&monitor_id.proxy)), - Some(Fullscreen::Borderless(_)) => unreachable!(), - None => { - if attributes.maximized { - frame.set_maximized(); - } - } - } - - frame.set_resizable(attributes.resizable); - - // set decorations - frame.set_decorate(attributes.decorations); - - // set title - frame.set_title(attributes.title); - - // min-max dimensions - frame.set_min_size(attributes.min_inner_size.map(Into::into)); - frame.set_max_size(attributes.max_inner_size.map(Into::into)); - - let kill_switch = Arc::new(Mutex::new(false)); - let need_frame_refresh = Arc::new(Mutex::new(true)); - let frame = Arc::new(Mutex::new(frame)); - let need_refresh = Arc::new(Mutex::new(true)); - let cursor_grab_changed = Arc::new(Mutex::new(None)); - - evlp.store.lock().unwrap().windows.push(InternalWindow { - closed: false, - newsize: None, - size: size.clone(), - need_refresh: need_refresh.clone(), - fullscreen: fullscreen.clone(), - cursor_grab_changed: cursor_grab_changed.clone(), - need_frame_refresh: need_frame_refresh.clone(), - surface: surface.clone(), - kill_switch: kill_switch.clone(), - frame: Arc::downgrade(&frame), - current_dpi: 1, - new_dpi: None, - }); - evlp.evq.borrow_mut().sync_roundtrip().unwrap(); - - Ok(Window { - display: evlp.display.clone(), - surface, - frame, - outputs: evlp.env.outputs.clone(), - size, - kill_switch: (kill_switch, evlp.cleanup_needed.clone()), - need_frame_refresh, - need_refresh, - cursor_manager, - fullscreen, - cursor_grab_changed, - }) - } - - #[inline] - pub fn id(&self) -> WindowId { - make_wid(&self.surface) - } - - pub fn set_title(&self, title: &str) { - self.frame.lock().unwrap().set_title(title.into()); - } - - pub fn set_visible(&self, _visible: bool) { - // TODO - } - - #[inline] - pub fn outer_position(&self) -> Result { - Err(NotSupportedError::new()) - } - - #[inline] - pub fn inner_position(&self) -> Result { - Err(NotSupportedError::new()) - } - - #[inline] - pub fn set_outer_position(&self, _pos: LogicalPosition) { - // Not possible with wayland - } - - pub fn inner_size(&self) -> LogicalSize { - self.size.lock().unwrap().clone().into() - } - - pub fn request_redraw(&self) { - *self.need_refresh.lock().unwrap() = true; - } - - #[inline] - pub fn outer_size(&self) -> LogicalSize { - let (w, h) = self.size.lock().unwrap().clone(); - // let (w, h) = super::wayland_window::add_borders(w as i32, h as i32); - (w, h).into() - } - - #[inline] - // NOTE: This will only resize the borders, the contents must be updated by the user - pub fn set_inner_size(&self, size: LogicalSize) { - let (w, h) = size.into(); - self.frame.lock().unwrap().resize(w, h); - *(self.size.lock().unwrap()) = (w, h); - } - - #[inline] - pub fn set_min_inner_size(&self, dimensions: Option) { - self.frame - .lock() - .unwrap() - .set_min_size(dimensions.map(Into::into)); - } - - #[inline] - pub fn set_max_inner_size(&self, dimensions: Option) { - self.frame - .lock() - .unwrap() - .set_max_size(dimensions.map(Into::into)); - } - - #[inline] - pub fn set_resizable(&self, resizable: bool) { - self.frame.lock().unwrap().set_resizable(resizable); - } - - #[inline] - pub fn hidpi_factor(&self) -> i32 { - get_dpi_factor(&self.surface) - } - - pub fn set_decorations(&self, decorate: bool) { - self.frame.lock().unwrap().set_decorate(decorate); - *(self.need_frame_refresh.lock().unwrap()) = true; - } - - pub fn set_maximized(&self, maximized: bool) { - if maximized { - self.frame.lock().unwrap().set_maximized(); - } else { - self.frame.lock().unwrap().unset_maximized(); - } - } - - pub fn fullscreen(&self) -> Option { - if *(self.fullscreen.lock().unwrap()) { - Some(Fullscreen::Borderless(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(self.current_monitor()), - })) - } else { - None - } - } - - pub fn set_fullscreen(&self, fullscreen: Option) { - match fullscreen { - Some(Fullscreen::Exclusive(_)) => { - panic!("Wayland doesn't support exclusive fullscreen") - } - Some(Fullscreen::Borderless(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - })) => { - self.frame - .lock() - .unwrap() - .set_fullscreen(Some(&monitor_id.proxy)); - } - Some(Fullscreen::Borderless(_)) => unreachable!(), - None => self.frame.lock().unwrap().unset_fullscreen(), - } - } - - pub fn set_theme(&self, theme: T) { - self.frame.lock().unwrap().set_theme(theme) - } - - #[inline] - pub fn set_cursor_icon(&self, cursor: CursorIcon) { - let mut cursor_manager = self.cursor_manager.lock().unwrap(); - cursor_manager.set_cursor_icon(cursor); - } - - #[inline] - pub fn set_cursor_visible(&self, visible: bool) { - let mut cursor_manager = self.cursor_manager.lock().unwrap(); - cursor_manager.set_cursor_visible(visible); - } - - #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - *self.cursor_grab_changed.lock().unwrap() = Some(grab); - Ok(()) - } - - #[inline] - pub fn set_cursor_position(&self, _pos: LogicalPosition) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) - } - - pub fn display(&self) -> &Display { - &*self.display - } - - pub fn surface(&self) -> &wl_surface::WlSurface { - &self.surface - } - - pub fn current_monitor(&self) -> MonitorHandle { - let output = get_outputs(&self.surface).last().unwrap().clone(); - MonitorHandle { - proxy: output, - mgr: self.outputs.clone(), - } - } - - pub fn available_monitors(&self) -> VecDeque { - available_monitors(&self.outputs) - } - - pub fn primary_monitor(&self) -> MonitorHandle { - primary_monitor(&self.outputs) - } - - pub fn raw_window_handle(&self) -> WaylandHandle { - WaylandHandle { - surface: self.surface().as_ref().c_ptr() as *mut _, - display: self.display().as_ref().c_ptr() as *mut _, - ..WaylandHandle::empty() - } - } -} - -impl Drop for Window { - fn drop(&mut self) { - *(self.kill_switch.0.lock().unwrap()) = true; - *(self.kill_switch.1.lock().unwrap()) = true; - } -} - -/* - * Internal store for windows - */ - -struct InternalWindow { - surface: wl_surface::WlSurface, - newsize: Option<(u32, u32)>, - size: Arc>, - need_refresh: Arc>, - fullscreen: Arc>, - need_frame_refresh: Arc>, - cursor_grab_changed: Arc>>, - closed: bool, - kill_switch: Arc>, - frame: Weak>>, - current_dpi: i32, - new_dpi: Option, -} - -pub struct WindowStore { - windows: Vec, -} - -impl WindowStore { - pub fn new() -> WindowStore { - WindowStore { - windows: Vec::new(), - } - } - - pub fn find_wid(&self, surface: &wl_surface::WlSurface) -> Option { - for window in &self.windows { - if surface.as_ref().equals(&window.surface.as_ref()) { - return Some(make_wid(surface)); - } - } - None - } - - pub fn cleanup(&mut self) -> Vec { - let mut pruned = Vec::new(); - self.windows.retain(|w| { - if *w.kill_switch.lock().unwrap() { - // window is dead, cleanup - pruned.push(make_wid(&w.surface)); - w.surface.destroy(); - false - } else { - true - } - }); - pruned - } - - pub fn new_seat(&self, seat: &wl_seat::WlSeat) { - for window in &self.windows { - if let Some(w) = window.frame.upgrade() { - w.lock().unwrap().new_seat(seat); - } - } - } - - fn dpi_change(&mut self, surface: &wl_surface::WlSurface, new: i32) { - for window in &mut self.windows { - if surface.as_ref().equals(&window.surface.as_ref()) { - window.new_dpi = Some(new); - } - } - } - - pub fn for_each(&mut self, mut f: F) - where - F: FnMut( - Option<(u32, u32)>, - &mut (u32, u32), - Option, - bool, - bool, - bool, - Option, - &wl_surface::WlSurface, - WindowId, - Option<&mut SWindow>, - ), - { - for window in &mut self.windows { - let opt_arc = window.frame.upgrade(); - let mut opt_mutex_lock = opt_arc.as_ref().map(|m| m.lock().unwrap()); - f( - window.newsize.take(), - &mut *(window.size.lock().unwrap()), - window.new_dpi, - replace(&mut *window.need_refresh.lock().unwrap(), false), - replace(&mut *window.need_frame_refresh.lock().unwrap(), false), - window.closed, - window.cursor_grab_changed.lock().unwrap().take(), - &window.surface, - make_wid(&window.surface), - opt_mutex_lock.as_mut().map(|m| &mut **m), - ); - if let Some(dpi) = window.new_dpi.take() { - window.current_dpi = dpi; - } - // avoid re-spamming the event - window.closed = false; - } - } -} diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs new file mode 100644 index 0000000000..d51f47a7a2 --- /dev/null +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -0,0 +1,656 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Display; + +use sctk::reexports::calloop; + +use sctk::window::{ + ARGBColor, ButtonColorSpec, ColorSpec, ConceptConfig, ConceptFrame, Decorations, +}; + +use raw_window_handle::unix::WaylandHandle; + +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::monitor::MonitorHandle as RootMonitorHandle; +use crate::platform::unix::{ARGBColor as LocalARGBColor, Button, ButtonState, Element, Theme}; +use crate::platform_impl::{ + MonitorHandle as PlatformMonitorHandle, OsError, + PlatformSpecificWindowBuilderAttributes as PlatformAttributes, +}; +use crate::window::{CursorIcon, Fullscreen, WindowAttributes}; + +use super::env::WindowingFeatures; +use super::event_loop::WinitState; +use super::output::{MonitorHandle, OutputManagerHandle}; +use super::{EventLoopWindowTarget, WindowId}; + +pub mod shim; + +use shim::{WindowHandle, WindowRequest, WindowUpdate}; + +pub struct Window { + /// Window id. + window_id: WindowId, + + /// The Wayland display. + display: Display, + + /// The underlying wl_surface. + surface: WlSurface, + + /// The current window size. + size: Arc>>, + + /// A handle to output manager. + output_manager_handle: OutputManagerHandle, + + /// Event loop proxy to wake it up. + event_loop_awakener: calloop::ping::Ping, + + /// Fullscreen state. + fullscreen: Arc, + + /// Available windowing features. + windowing_features: WindowingFeatures, + + /// Requests that SCTK window should perform. + window_requests: Arc>>, +} + +impl Window { + pub fn new( + event_loop_window_target: &EventLoopWindowTarget, + attributes: WindowAttributes, + platform_attributes: PlatformAttributes, + ) -> Result { + let surface = event_loop_window_target + .env + .create_surface_with_scale_callback(move |scale, surface, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + + // Get the window that receiced the event. + let window_id = super::make_wid(&surface); + let mut window_update = winit_state.window_updates.get_mut(&window_id).unwrap(); + + // Set pending scale factor. + window_update.scale_factor = Some(scale); + window_update.redraw_requested = true; + + surface.set_buffer_scale(scale); + }) + .detach(); + + let scale_factor = sctk::get_surface_scale_factor(&surface); + + let window_id = super::make_wid(&surface); + let fullscreen = Arc::new(AtomicBool::new(false)); + let fullscreen_clone = fullscreen.clone(); + + let (width, height) = attributes + .inner_size + .map(|size| size.to_logical::(scale_factor as f64).into()) + .unwrap_or((800, 600)); + + let theme_manager = event_loop_window_target.theme_manager.clone(); + let mut window = event_loop_window_target + .env + .create_window::( + surface.clone(), + Some(theme_manager), + (width, height), + move |event, mut dispatch_data| { + use sctk::window::{Event, State}; + + let winit_state = dispatch_data.get::().unwrap(); + let mut window_update = winit_state.window_updates.get_mut(&window_id).unwrap(); + + match event { + Event::Refresh => { + window_update.refresh_frame = true; + } + Event::Configure { new_size, states } => { + let is_fullscreen = states.contains(&State::Fullscreen); + fullscreen_clone.store(is_fullscreen, Ordering::Relaxed); + + window_update.refresh_frame = true; + window_update.redraw_requested = true; + if let Some((w, h)) = new_size { + window_update.size = Some(LogicalSize::new(w, h)); + } + } + Event::Close => { + window_update.close_window = true; + } + } + }, + ) + .map_err(|_| os_error!(OsError::WaylandMisc("failed to create window.")))?; + + // Set decorations. + if attributes.decorations { + window.set_decorate(Decorations::FollowServer); + } else { + window.set_decorate(Decorations::None); + } + + // Min dimensions. + let min_size = attributes + .min_inner_size + .map(|size| size.to_logical::(scale_factor as f64).into()); + window.set_min_size(min_size); + + // Max dimensions. + let max_size = attributes + .max_inner_size + .map(|size| size.to_logical::(scale_factor as f64).into()); + window.set_max_size(max_size); + + // Set Wayland specific window attributes. + if let Some(app_id) = platform_attributes.app_id { + window.set_app_id(app_id); + } + + // Set common window attributes. + // + // We set resizable after other attributes, since it touches min and max size under + // the hood. + window.set_resizable(attributes.resizable); + window.set_title(attributes.title); + + // Set fullscreen/maximized if so was requested. + match attributes.fullscreen { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland") + } + Some(Fullscreen::Borderless(monitor)) => { + let monitor = + monitor.and_then(|RootMonitorHandle { inner: monitor }| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(feature = "x11")] + PlatformMonitorHandle::X(_) => None, + }); + + window.set_fullscreen(monitor.as_ref()); + } + None => { + if attributes.maximized { + window.set_maximized(); + } + } + } + + let size = Arc::new(Mutex::new(LogicalSize::new(width, height))); + + // We should trigger redraw and commit the surface for the newly created window. + let mut window_update = WindowUpdate::new(); + window_update.refresh_frame = true; + window_update.redraw_requested = true; + + let window_id = super::make_wid(&surface); + let window_requests = Arc::new(Mutex::new(Vec::with_capacity(64))); + + // Create a handle that performs all the requests on underlying sctk a window. + let window_handle = WindowHandle::new(window, size.clone(), window_requests.clone()); + + let mut winit_state = event_loop_window_target.state.borrow_mut(); + + winit_state.window_map.insert(window_id, window_handle); + + winit_state + .window_updates + .insert(window_id, WindowUpdate::new()); + + let windowing_features = event_loop_window_target.windowing_features; + + // Send all updates to the server. + let wayland_source = &event_loop_window_target.wayland_source; + let event_loop_handle = &event_loop_window_target.event_loop_handle; + + // To make our window usable for drawing right away we must `ack` a `configure` + // from the server, the acking part here is done by SCTK window frame, so we just + // need to sync with server so it'll be done automatically for us. + event_loop_handle.with_source(&wayland_source, |event_queue| { + let event_queue = event_queue.queue(); + let _ = event_queue.sync_roundtrip(&mut *winit_state, |_, _, _| unreachable!()); + }); + + // We all praise GNOME for these 3 lines of pure magic. If we don't do that, + // GNOME will shrink our window a bit for the size of the decorations. I guess it + // happens because we haven't committed them with buffers to the server. + let window_handle = winit_state.window_map.get_mut(&window_id).unwrap(); + window_handle.window.refresh(); + + let output_manager_handle = event_loop_window_target.output_manager.handle(); + + let window = Self { + window_id, + surface, + display: event_loop_window_target.display.clone(), + output_manager_handle, + size, + window_requests, + event_loop_awakener: event_loop_window_target.event_loop_awakener.clone(), + fullscreen, + windowing_features, + }; + + Ok(window) + } +} + +impl Window { + #[inline] + pub fn id(&self) -> WindowId { + self.window_id + } + + #[inline] + pub fn set_title(&self, title: &str) { + let title_request = WindowRequest::Title(title.to_owned()); + self.window_requests.lock().unwrap().push(title_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_visible(&self, _visible: bool) { + // Not possible on Wayland. + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn set_outer_position(&self, _: Position) { + // Not possible on Wayland. + } + + pub fn inner_size(&self) -> PhysicalSize { + self.size + .lock() + .unwrap() + .to_physical(self.scale_factor() as f64) + } + + #[inline] + pub fn request_redraw(&self) { + let redraw_request = WindowRequest::Redraw; + self.window_requests.lock().unwrap().push(redraw_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + self.size + .lock() + .unwrap() + .to_physical(self.scale_factor() as f64) + } + + #[inline] + pub fn set_inner_size(&self, size: Size) { + let scale_factor = self.scale_factor() as f64; + + let size = size.to_logical::(scale_factor); + *self.size.lock().unwrap() = size; + + let frame_size_request = WindowRequest::FrameSize(size); + self.window_requests + .lock() + .unwrap() + .push(frame_size_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + let scale_factor = self.scale_factor() as f64; + let size = dimensions.map(|size| size.to_logical::(scale_factor)); + + let min_size_request = WindowRequest::MinSize(size); + self.window_requests.lock().unwrap().push(min_size_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + let scale_factor = self.scale_factor() as f64; + let size = dimensions.map(|size| size.to_logical::(scale_factor)); + + let max_size_request = WindowRequest::MaxSize(size); + self.window_requests.lock().unwrap().push(max_size_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let resizeable_request = WindowRequest::Resizeable(resizable); + self.window_requests + .lock() + .unwrap() + .push(resizeable_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn scale_factor(&self) -> u32 { + // The scale factor from `get_surface_scale_factor` is always greater than zero, so + // u32 conversion is safe. + sctk::get_surface_scale_factor(&self.surface) as u32 + } + + #[inline] + pub fn set_decorations(&self, decorate: bool) { + let decorate_request = WindowRequest::Decorate(decorate); + self.window_requests.lock().unwrap().push(decorate_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + // You can't unminimize the window on Wayland. + if !minimized { + return; + } + + let minimize_request = WindowRequest::Minimize; + self.window_requests.lock().unwrap().push(minimize_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let maximize_request = WindowRequest::Maximize(maximized); + self.window_requests.lock().unwrap().push(maximize_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn fullscreen(&self) -> Option { + if self.fullscreen.load(Ordering::Relaxed) { + let current_monitor = self.current_monitor().map(|monitor| RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(monitor), + }); + + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + } + } + + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let fullscreen_request = match fullscreen { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + return; + } + Some(Fullscreen::Borderless(monitor)) => { + let monitor = + monitor.and_then(|RootMonitorHandle { inner: monitor }| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(feature = "x11")] + PlatformMonitorHandle::X(_) => None, + }); + + WindowRequest::Fullscreen(monitor) + } + None => WindowRequest::UnsetFullscreen, + }; + + self.window_requests + .lock() + .unwrap() + .push(fullscreen_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_theme(&self, theme: T) { + // First buttons is minimize, then maximize, and then close. + let buttons: Vec<(ButtonColorSpec, ButtonColorSpec)> = + [Button::Minimize, Button::Maximize, Button::Close] + .iter() + .map(|button| { + let button = *button; + let idle_active_bg = theme + .button_color(button, ButtonState::Idle, false, true) + .into(); + let idle_inactive_bg = theme + .button_color(button, ButtonState::Idle, false, false) + .into(); + let idle_active_icon = theme + .button_color(button, ButtonState::Idle, true, true) + .into(); + let idle_inactive_icon = theme + .button_color(button, ButtonState::Idle, true, false) + .into(); + let idle_bg = ColorSpec { + active: idle_active_bg, + inactive: idle_inactive_bg, + }; + let idle_icon = ColorSpec { + active: idle_active_icon, + inactive: idle_inactive_icon, + }; + + let hovered_active_bg = theme + .button_color(button, ButtonState::Hovered, false, true) + .into(); + let hovered_inactive_bg = theme + .button_color(button, ButtonState::Hovered, false, false) + .into(); + let hovered_active_icon = theme + .button_color(button, ButtonState::Hovered, true, true) + .into(); + let hovered_inactive_icon = theme + .button_color(button, ButtonState::Hovered, true, false) + .into(); + let hovered_bg = ColorSpec { + active: hovered_active_bg, + inactive: hovered_inactive_bg, + }; + let hovered_icon = ColorSpec { + active: hovered_active_icon, + inactive: hovered_inactive_icon, + }; + + let disabled_active_bg = theme + .button_color(button, ButtonState::Disabled, false, true) + .into(); + let disabled_inactive_bg = theme + .button_color(button, ButtonState::Disabled, false, false) + .into(); + let disabled_active_icon = theme + .button_color(button, ButtonState::Disabled, true, true) + .into(); + let disabled_inactive_icon = theme + .button_color(button, ButtonState::Disabled, true, false) + .into(); + let disabled_bg = ColorSpec { + active: disabled_active_bg, + inactive: disabled_inactive_bg, + }; + let disabled_icon = ColorSpec { + active: disabled_active_icon, + inactive: disabled_inactive_icon, + }; + + let button_bg = ButtonColorSpec { + idle: idle_bg, + hovered: hovered_bg, + disabled: disabled_bg, + }; + let button_icon = ButtonColorSpec { + idle: idle_icon, + hovered: hovered_icon, + disabled: disabled_icon, + }; + + (button_icon, button_bg) + }) + .collect(); + + let minimize_button = Some(buttons[0]); + let maximize_button = Some(buttons[1]); + let close_button = Some(buttons[2]); + + // The first color is bar, then separator, and then text color. + let titlebar_colors: Vec = [Element::Bar, Element::Separator, Element::Text] + .iter() + .map(|element| { + let element = *element; + let active = theme.element_color(element, true).into(); + let inactive = theme.element_color(element, false).into(); + + ColorSpec { active, inactive } + }) + .collect(); + + let primary_color = titlebar_colors[0]; + let secondary_color = titlebar_colors[1]; + let title_color = titlebar_colors[2]; + + let title_font = theme.font(); + + let concept_config = ConceptConfig { + primary_color, + secondary_color, + title_color, + title_font, + minimize_button, + maximize_button, + close_button, + }; + + let theme_request = WindowRequest::Theme(concept_config); + self.window_requests.lock().unwrap().push(theme_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + let cursor_icon_request = WindowRequest::NewCursorIcon(cursor); + self.window_requests + .lock() + .unwrap() + .push(cursor_icon_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let cursor_visible_request = WindowRequest::ShowCursor(visible); + self.window_requests + .lock() + .unwrap() + .push(cursor_visible_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + if !self.windowing_features.cursor_grab() { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + let cursor_grab_request = WindowRequest::GrabCursor(grab); + self.window_requests + .lock() + .unwrap() + .push(cursor_grab_request); + self.event_loop_awakener.ping(); + + Ok(()) + } + + #[inline] + pub fn set_cursor_position(&self, _: Position) -> Result<(), ExternalError> { + // XXX This is possible if the locked pointer is being used. We don't have any + // API for that right now, but it could be added in + // https://github.com/rust-windowing/winit/issues/1677. + // + // This function is essential for the locked pointer API. + // + // See pointer-constraints-unstable-v1.xml. + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_ime_position(&self, position: Position) { + let scale_factor = self.scale_factor() as f64; + let position = position.to_logical(scale_factor); + let ime_position_request = WindowRequest::IMEPosition(position); + self.window_requests + .lock() + .unwrap() + .push(ime_position_request); + self.event_loop_awakener.ping(); + } + + #[inline] + pub fn display(&self) -> &Display { + &self.display + } + + #[inline] + pub fn surface(&self) -> &WlSurface { + &self.surface + } + + #[inline] + pub fn current_monitor(&self) -> Option { + let output = sctk::get_surface_outputs(&self.surface).last()?.clone(); + Some(MonitorHandle::new(output)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + self.output_manager_handle.available_outputs() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + None + } + + #[inline] + pub fn raw_window_handle(&self) -> WaylandHandle { + let display = self.display.get_display_ptr() as *mut _; + let surface = self.surface.as_ref().c_ptr() as *mut _; + + WaylandHandle { + display, + surface, + ..WaylandHandle::empty() + } + } +} + +impl From for ARGBColor { + fn from(color: LocalARGBColor) -> Self { + let a = color.a; + let r = color.r; + let g = color.g; + let b = color.b; + Self { a, r, g, b } + } +} + +impl Drop for Window { + fn drop(&mut self) { + let close_request = WindowRequest::Close; + self.window_requests.lock().unwrap().push(close_request); + self.event_loop_awakener.ping(); + } +} diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs new file mode 100644 index 0000000000..59cf921a59 --- /dev/null +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -0,0 +1,388 @@ +use std::cell::Cell; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_output::WlOutput; + +use sctk::window::{ConceptConfig, ConceptFrame, Decorations, Window}; + +use crate::dpi::{LogicalPosition, LogicalSize}; + +use crate::event::WindowEvent; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::seat::pointer::WinitPointer; +use crate::platform_impl::wayland::seat::text_input::TextInputHandler; +use crate::platform_impl::wayland::WindowId; +use crate::window::CursorIcon; + +/// A request to SCTK window from Winit window. +#[derive(Debug, Clone)] +pub enum WindowRequest { + /// Set fullscreen. + /// + /// Passing `None` will set it on the current monitor. + Fullscreen(Option), + + /// Unset fullscreen. + UnsetFullscreen, + + /// Show cursor for the certain window or not. + ShowCursor(bool), + + /// Change the cursor icon. + NewCursorIcon(CursorIcon), + + /// Grab cursor. + GrabCursor(bool), + + /// Maximize the window. + Maximize(bool), + + /// Minimize the window. + Minimize, + + /// Request decorations change. + Decorate(bool), + + /// Make the window resizeable. + Resizeable(bool), + + /// Set the title for window. + Title(String), + + /// Min size. + MinSize(Option>), + + /// Max size. + MaxSize(Option>), + + /// New frame size. + FrameSize(LogicalSize), + + /// Set IME window position. + IMEPosition(LogicalPosition), + + /// Redraw was requested. + Redraw, + + /// A new theme for a concept frame was requested. + Theme(ConceptConfig), + + /// Window should be closed. + Close, +} + +/// Pending update to a window from SCTK window. +#[derive(Debug, Clone, Copy)] +pub struct WindowUpdate { + /// New window size. + pub size: Option>, + + /// New scale factor. + pub scale_factor: Option, + + /// Whether `redraw` was requested. + pub redraw_requested: bool, + + /// Wether the frame should be refreshed. + pub refresh_frame: bool, + + /// Close the window. + pub close_window: bool, +} + +impl WindowUpdate { + pub fn new() -> Self { + Self { + size: None, + scale_factor: None, + redraw_requested: false, + refresh_frame: false, + close_window: false, + } + } + + pub fn take(&mut self) -> Self { + let size = self.size.take(); + let scale_factor = self.scale_factor.take(); + + let redraw_requested = self.redraw_requested; + self.redraw_requested = false; + + let refresh_frame = self.refresh_frame; + self.refresh_frame = false; + + let close_window = self.close_window; + self.close_window = false; + + Self { + size, + scale_factor, + redraw_requested, + refresh_frame, + close_window, + } + } +} + +/// A handle to perform operations on SCTK window +/// and react to events. +pub struct WindowHandle { + /// An actual window. + pub window: Window, + + /// The current size of the window. + pub size: Arc>>, + + /// A pending requests to SCTK window. + pub pending_window_requests: Arc>>, + + /// Current cursor icon. + pub cursor_icon: Cell, + + /// Visible cursor or not. + cursor_visible: Cell, + + /// Cursor confined to the surface. + confined: Cell, + + /// Pointers over the current surface. + pointers: Vec, + + /// Text inputs on the current surface. + text_inputs: Vec, +} + +impl WindowHandle { + pub fn new( + window: Window, + size: Arc>>, + pending_window_requests: Arc>>, + ) -> Self { + Self { + window, + size, + pending_window_requests, + cursor_icon: Cell::new(CursorIcon::Default), + confined: Cell::new(false), + cursor_visible: Cell::new(true), + pointers: Vec::new(), + text_inputs: Vec::new(), + } + } + + pub fn set_cursor_grab(&self, grab: bool) { + // The new requested state matches the current confine status, return. + if self.confined.get() == grab { + return; + } + + self.confined.replace(grab); + + for pointer in self.pointers.iter() { + if self.confined.get() { + let surface = self.window.surface(); + pointer.confine(&surface); + } else { + pointer.unconfine(); + } + } + } + + /// Pointer appeared over the window. + pub fn pointer_entered(&mut self, pointer: WinitPointer) { + let position = self.pointers.iter().position(|p| *p == pointer); + + if position.is_none() { + if self.confined.get() { + let surface = self.window.surface(); + pointer.confine(&surface); + } + self.pointers.push(pointer); + } + + // Apply the current cursor style. + self.set_cursor_visible(self.cursor_visible.get()); + } + + /// Pointer left the window. + pub fn pointer_left(&mut self, pointer: WinitPointer) { + let position = self.pointers.iter().position(|p| *p == pointer); + + if let Some(position) = position { + let pointer = self.pointers.remove(position); + + // Drop the confined pointer. + if self.confined.get() { + pointer.unconfine(); + } + } + } + + pub fn text_input_entered(&mut self, text_input: TextInputHandler) { + if self + .text_inputs + .iter() + .find(|t| *t == &text_input) + .is_none() + { + self.text_inputs.push(text_input); + } + } + + pub fn text_input_left(&mut self, text_input: TextInputHandler) { + if let Some(position) = self.text_inputs.iter().position(|t| *t == text_input) { + self.text_inputs.remove(position); + } + } + + pub fn set_ime_position(&self, position: LogicalPosition) { + // XXX 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); + for text_input in self.text_inputs.iter() { + text_input.set_ime_position(x, y); + } + } + + pub fn set_cursor_visible(&self, visible: bool) { + self.cursor_visible.replace(visible); + let cursor_icon = match visible { + true => Some(self.cursor_icon.get()), + false => None, + }; + + for pointer in self.pointers.iter() { + pointer.set_cursor(cursor_icon) + } + } + + pub fn set_cursor_icon(&self, cursor_icon: CursorIcon) { + self.cursor_icon.replace(cursor_icon); + + if !self.cursor_visible.get() { + return; + } + + for pointer in self.pointers.iter() { + pointer.set_cursor(Some(cursor_icon)); + } + } +} + +#[inline] +pub fn handle_window_requests(winit_state: &mut WinitState) { + let window_map = &mut winit_state.window_map; + let window_updates = &mut winit_state.window_updates; + let mut windows_to_close: Vec = Vec::new(); + + // Process the rest of the events. + for (window_id, window_handle) in window_map.iter_mut() { + let mut requests = window_handle.pending_window_requests.lock().unwrap(); + for request in requests.drain(..) { + match request { + WindowRequest::Fullscreen(fullscreen) => { + window_handle.window.set_fullscreen(fullscreen.as_ref()); + } + WindowRequest::UnsetFullscreen => { + window_handle.window.unset_fullscreen(); + } + WindowRequest::ShowCursor(show_cursor) => { + window_handle.set_cursor_visible(show_cursor); + } + WindowRequest::NewCursorIcon(cursor_icon) => { + window_handle.set_cursor_icon(cursor_icon); + } + WindowRequest::IMEPosition(position) => { + window_handle.set_ime_position(position); + } + WindowRequest::GrabCursor(grab) => { + window_handle.set_cursor_grab(grab); + } + WindowRequest::Maximize(maximize) => { + if maximize { + window_handle.window.set_maximized(); + } else { + window_handle.window.unset_maximized(); + } + } + WindowRequest::Minimize => { + window_handle.window.set_minimized(); + } + WindowRequest::Decorate(decorate) => { + let decorations = match decorate { + true => Decorations::FollowServer, + false => Decorations::None, + }; + + window_handle.window.set_decorate(decorations); + + // We should refresh the frame to apply decorations change. + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.refresh_frame = true; + } + WindowRequest::Resizeable(resizeable) => { + window_handle.window.set_resizable(resizeable); + + // We should refresh the frame to update button state. + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.refresh_frame = true; + } + WindowRequest::Title(title) => { + window_handle.window.set_title(title); + + // We should refresh the frame to draw new title. + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.refresh_frame = true; + } + WindowRequest::MinSize(size) => { + let size = size.map(|size| (size.width, size.height)); + window_handle.window.set_min_size(size); + + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.redraw_requested = true; + } + WindowRequest::MaxSize(size) => { + let size = size.map(|size| (size.width, size.height)); + window_handle.window.set_max_size(size); + + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.redraw_requested = true; + } + WindowRequest::FrameSize(size) => { + // Set new size. + window_handle.window.resize(size.width, size.height); + + // We should refresh the frame after resize. + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.refresh_frame = true; + } + WindowRequest::Redraw => { + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.redraw_requested = true; + } + WindowRequest::Theme(concept_config) => { + window_handle.window.set_frame_config(concept_config); + + // We should refresh the frame to apply new theme. + let window_update = window_updates.get_mut(&window_id).unwrap(); + window_update.refresh_frame = true; + } + WindowRequest::Close => { + // The window was requested to be closed. + windows_to_close.push(*window_id); + + // Send event that the window was destroyed. + let event_sink = &mut winit_state.event_sink; + event_sink.push_window_event(WindowEvent::Destroyed, *window_id); + } + }; + } + } + + // Close the windows. + for window in windows_to_close { + let _ = window_map.remove(&window); + let _ = window_updates.remove(&window); + } +} diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 2758aa3581..65ed4d9ff1 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -1,7 +1,9 @@ -use std::{cell::RefCell, collections::HashMap, ptr, rc::Rc, slice}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, slice, sync::Arc}; use libc::{c_char, c_int, c_long, c_uint, c_ulong}; +use parking_lot::MutexGuard; + use super::{ events, ffi, get_xtarget, mkdid, mkwid, monitor, util, Device, DeviceId, DeviceInfo, Dnd, DndState, GenericEventCookie, ImeReceiver, ScrollOrientation, UnownedWindow, WindowId, @@ -11,11 +13,16 @@ use super::{ use util::modifiers::{ModifierKeyState, ModifierKeymap}; use crate::{ - dpi::{LogicalPosition, LogicalSize}, - event::{DeviceEvent, Event, KeyboardInput, ModifiersState, WindowEvent}, + dpi::{PhysicalPosition, PhysicalSize}, + event::{ + DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent, + }, event_loop::EventLoopWindowTarget as RootELW, }; +/// 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, @@ -25,6 +32,11 @@ pub(super) struct EventProcessor { pub(super) target: Rc>, pub(super) mod_keymap: ModifierKeymap, pub(super) device_mod_state: ModifierKeyState, + // Number of touch events currently in progress + pub(super) num_touch: u32, + pub(super) first_touch: Option, + // Currently focused window belonging to this process + pub(super) active_window: Option, } impl EventProcessor { @@ -40,7 +52,7 @@ impl EventProcessor { fn with_window(&self, window_id: ffi::Window, callback: F) -> Option where - F: Fn(&UnownedWindow) -> Ret, + F: Fn(&Arc) -> Ret, { let mut deleted = false; let window_id = WindowId(window_id); @@ -54,7 +66,7 @@ impl EventProcessor { deleted = arc.is_none(); arc }) - .map(|window| callback(&*window)); + .map(|window| callback(&window)); if deleted { // Garbage collection wt.windows.borrow_mut().remove(&window_id); @@ -102,7 +114,7 @@ impl EventProcessor { pub(super) fn process_event(&mut self, xev: &mut ffi::XEvent, mut callback: F) where - F: FnMut(Event), + F: FnMut(Event<'_, T>), { let wt = get_xtarget(&self.target); // XFilterEvent tells us when an event has been discarded by the input method. @@ -129,11 +141,12 @@ impl EventProcessor { if let Some(modifiers) = self.device_mod_state.update_state(&state, modifier) { - let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); - callback(Event::DeviceEvent { - device_id, - event: DeviceEvent::ModifiersChanged { modifiers }, - }); + if let Some(window_id) = self.active_window { + callback(Event::WindowEvent { + window_id: mkwid(window_id), + event: WindowEvent::ModifiersChanged(modifiers), + }); + } } } } @@ -316,16 +329,11 @@ impl EventProcessor { } ffi::ConfigureNotify => { - #[derive(Debug, Default)] - struct Events { - resized: Option, - moved: Option, - dpi_changed: Option, - } - let xev: &ffi::XConfigureEvent = xev.as_ref(); let xwindow = xev.window; - let events = self.with_window(xwindow, |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 @@ -339,7 +347,6 @@ impl EventProcessor { let new_inner_size = (xev.width as u32, xev.height as u32); let new_inner_position = (xev.x as i32, xev.y as i32); - let mut monitor = window.current_monitor(); // This must be done *before* locking! let mut shared_state_lock = window.shared_state.lock(); let (mut resized, moved) = { @@ -369,8 +376,6 @@ impl EventProcessor { (resized, moved) }; - let mut events = Events::default(); - let new_outer_position = if moved || shared_state_lock.position.is_none() { // We need to convert client area position to window position. let frame_extents = shared_state_lock @@ -387,9 +392,13 @@ impl EventProcessor { .inner_pos_to_outer(new_inner_position.0, new_inner_position.1); shared_state_lock.position = Some(outer); if moved { - let logical_position = - LogicalPosition::from_physical(outer, monitor.hidpi_factor); - events.moved = Some(WindowEvent::Moved(logical_position)); + // Temporarily unlock shared state to prevent deadlock + MutexGuard::unlocked(&mut shared_state_lock, || { + callback(Event::WindowEvent { + window_id, + event: WindowEvent::Moved(outer.into()), + }); + }); } outer } else { @@ -401,34 +410,54 @@ impl EventProcessor { // resizing by dragging across monitors *without* dropping the window. let (width, height) = shared_state_lock .dpi_adjusted - .unwrap_or_else(|| (xev.width as f64, xev.height as f64)); + .unwrap_or_else(|| (xev.width as u32, xev.height as u32)); - let last_hidpi_factor = shared_state_lock.last_monitor.hidpi_factor; - let new_hidpi_factor = { + 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); - monitor = wt.xconn.get_monitor_for_window(Some(window_rect)); - let new_hidpi_factor = monitor.hidpi_factor; + let monitor = wt.xconn.get_monitor_for_window(Some(window_rect)); - // Avoid caching an invalid dummy monitor handle - if monitor.id != 0 { + 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 } - new_hidpi_factor }; - if last_hidpi_factor != new_hidpi_factor { - events.dpi_changed = - Some(WindowEvent::HiDpiFactorChanged(new_hidpi_factor)); - let (new_width, new_height, flusher) = window.adjust_for_dpi( - last_hidpi_factor, - new_hidpi_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, ); - flusher.queue(); - shared_state_lock.dpi_adjusted = Some((new_width, new_height)); - // if the DPI factor changed, force a resize event to ensure the logical - // size is computed with the right DPI factor - resized = true; + + let old_inner_size = PhysicalSize::new(width, height); + let mut new_inner_size = PhysicalSize::new(new_width, new_height); + + // Temporarily unlock shared state to prevent deadlock + MutexGuard::unlocked(&mut 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, + ); + 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; + } } } @@ -437,44 +466,22 @@ impl EventProcessor { // 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 { - let rounded_size = ( - adjusted_size.0.round() as u32, - adjusted_size.1.round() as u32, - ); - if new_inner_size == rounded_size || !util::wm_name_is_one_of(&["Xfwm4"]) { + 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 { - unsafe { - (wt.xconn.xlib.XResizeWindow)( - wt.xconn.display, - xwindow, - rounded_size.0 as c_uint, - rounded_size.1 as c_uint, - ); - } + window.set_inner_size_physical(adjusted_size.0, adjusted_size.1); } } if resized { - let logical_size = - LogicalSize::from_physical(new_inner_size, monitor.hidpi_factor); - events.resized = Some(WindowEvent::Resized(logical_size)); - } + // Drop the shared state lock to prevent deadlock + drop(shared_state_lock); - events - }); - - if let Some(events) = events { - let window_id = mkwid(xwindow); - if let Some(event) = events.dpi_changed { - callback(Event::WindowEvent { window_id, event }); - } - if let Some(event) = events.resized { - callback(Event::WindowEvent { window_id, event }); - } - if let Some(event) = events.moved { - callback(Event::WindowEvent { window_id, event }); + callback(Event::WindowEvent { + window_id, + event: WindowEvent::Resized(new_inner_size.into()), + }); } } } @@ -527,13 +534,14 @@ impl EventProcessor { ffi::Expose => { let xev: &ffi::XExposeEvent = xev.as_ref(); - let window = xev.window; - let window_id = mkwid(window); + // 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); - callback(Event::WindowEvent { - window_id, - event: WindowEvent::RedrawRequested, - }); + callback(Event::RedrawRequested(window_id)); + } } ffi::KeyPress | ffi::KeyRelease => { @@ -555,22 +563,13 @@ impl EventProcessor { // value, though this should only be an issue under multiseat configurations. let device = util::VIRTUAL_CORE_KEYBOARD; let device_id = mkdid(device); + let keycode = xkev.keycode; // When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with // a keycode of 0. - if xkev.keycode != 0 { - let keysym = unsafe { - let mut keysym = 0; - (wt.xconn.xlib.XLookupString)( - xkev, - ptr::null_mut(), - 0, - &mut keysym, - ptr::null_mut(), - ); - wt.xconn.check_errors().expect("Failed to lookup keysym"); - keysym - }; + if keycode != 0 { + let scancode = keycode - KEYCODE_OFFSET as u32; + let keysym = wt.xconn.lookup_keysym(xkev); let virtual_keycode = events::keysym_to_element(keysym as c_uint); update_modifiers!( @@ -580,16 +579,18 @@ impl EventProcessor { let modifiers = self.device_mod_state.modifiers(); + #[allow(deprecated)] callback(Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { device_id, input: KeyboardInput { state, - scancode: xkev.keycode - 8, + scancode, virtual_keycode, modifiers, }, + is_synthetic: false, }, }); } @@ -626,7 +627,7 @@ impl EventProcessor { ElementState::{Pressed, Released}, MouseButton::{Left, Middle, Other, Right}, MouseScrollDelta::LineDelta, - Touch, TouchPhase, + Touch, WindowEvent::{ AxisMotion, CursorEntered, CursorLeft, CursorMoved, Focused, MouseInput, MouseWheel, @@ -708,7 +709,7 @@ impl EventProcessor { event: MouseInput { device_id, state, - button: Other(x as u8), + button: Other(x as u16), modifiers, }, }), @@ -728,24 +729,16 @@ impl EventProcessor { util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) }); if cursor_moved == Some(true) { - let dpi_factor = - self.with_window(xev.event, |window| window.hidpi_factor()); - if let Some(dpi_factor) = dpi_factor { - let position = LogicalPosition::from_physical( - (xev.event_x as f64, xev.event_y as f64), - dpi_factor, - ); - callback(Event::WindowEvent { - window_id, - event: CursorMoved { - device_id, - position, - modifiers, - }, - }); - } else { - return; - } + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + callback(Event::WindowEvent { + window_id, + event: CursorMoved { + device_id, + position, + modifiers, + }, + }); } else if cursor_moved.is_none() { return; } @@ -835,18 +828,14 @@ impl EventProcessor { } } } - callback(Event::WindowEvent { - window_id, - event: CursorEntered { device_id }, - }); - if let Some(dpi_factor) = - self.with_window(xev.event, |window| window.hidpi_factor()) - { - let position = LogicalPosition::from_physical( - (xev.event_x as f64, xev.event_y as f64), - dpi_factor, - ); + 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); // The mods field on this event isn't actually populated, so query the // pointer device. In the future, we can likely remove this round-trip by @@ -889,44 +878,61 @@ impl EventProcessor { ffi::XI_FocusIn => { let xev: &ffi::XIFocusInEvent = unsafe { &*(xev.data as *const _) }; - let dpi_factor = - match self.with_window(xev.event, |window| window.hidpi_factor()) { - Some(dpi_factor) => dpi_factor, - None => return, - }; - let window_id = mkwid(xev.event); - wt.ime .borrow_mut() .focus(xev.event) .expect("Failed to focus input context"); - callback(Event::WindowEvent { - window_id, - event: Focused(true), - }); + let modifiers = ModifiersState::from_x11(&xev.mods); - // 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); - - let position = LogicalPosition::from_physical( - (xev.event_x as f64, xev.event_y as f64), - dpi_factor, - ); - callback(Event::WindowEvent { - window_id, - event: CursorMoved { - device_id: mkdid(pointer_id), - position, - modifiers: ModifiersState::from_x11(&xev.mods), - }, - }); + self.device_mod_state.update_state(&modifiers, None); + + if self.active_window != Some(xev.event) { + self.active_window = Some(xev.event); + + let window_id = mkwid(xev.event); + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + callback(Event::WindowEvent { + window_id, + event: Focused(true), + }); + + if !modifiers.is_empty() { + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(modifiers), + }); + } + + // 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, + modifiers, + }, + }); + + // Issue key press events for all pressed keys + Self::handle_pressed_keys( + &wt, + window_id, + ElementState::Pressed, + &self.mod_keymap, + &mut self.device_mod_state, + &mut callback, + ); + } } ffi::XI_FocusOut => { let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) }; @@ -938,10 +944,29 @@ impl EventProcessor { .unfocus(xev.event) .expect("Failed to unfocus input context"); - callback(Event::WindowEvent { - window_id: mkwid(xev.event), - event: Focused(false), - }) + if self.active_window.take() == Some(xev.event) { + let window_id = mkwid(xev.event); + + // Issue key release events for all pressed keys + Self::handle_pressed_keys( + &wt, + window_id, + ElementState::Released, + &self.mod_keymap, + &mut self.device_mod_state, + &mut callback, + ); + + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(ModifiersState::empty()), + }); + + callback(Event::WindowEvent { + window_id, + event: Focused(false), + }) + } } ffi::XI_TouchBegin | ffi::XI_TouchUpdate | ffi::XI_TouchEnd => { @@ -953,13 +978,26 @@ impl EventProcessor { ffi::XI_TouchEnd => TouchPhase::Ended, _ => unreachable!(), }; - let dpi_factor = - self.with_window(xev.event, |window| window.hidpi_factor()); - if let Some(dpi_factor) = dpi_factor { - let location = LogicalPosition::from_physical( - (xev.event_x as f64, xev.event_y as f64), - dpi_factor, - ); + if self.window_exists(xev.event) { + let id = xev.detail as u64; + let modifiers = self.device_mod_state.modifiers(); + let location = + PhysicalPosition::new(xev.event_x as f64, xev.event_y as f64); + + // 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(), + modifiers, + }, + }); + } + callback(Event::WindowEvent { window_id, event: WindowEvent::Touch(Touch { @@ -967,7 +1005,7 @@ impl EventProcessor { phase, location, force: None, // TODO - id: xev.detail as u64, + id, }), }) } @@ -1052,30 +1090,19 @@ impl EventProcessor { let device_id = mkdid(xev.sourceid); let keycode = xev.detail; - if keycode < 8 { + let scancode = keycode - KEYCODE_OFFSET as i32; + if scancode < 0 { return; } - let scancode = (keycode - 8) as u32; - - let keysym = unsafe { - (wt.xconn.xlib.XKeycodeToKeysym)( - wt.xconn.display, - xev.detail as ffi::KeyCode, - 0, - ) - }; - wt.xconn - .check_errors() - .expect("Failed to lookup raw keysym"); - + let keysym = wt.xconn.keycode_to_keysym(keycode as ffi::KeyCode); let virtual_keycode = events::keysym_to_element(keysym as c_uint); - let modifiers = self.device_mod_state.modifiers(); + #[allow(deprecated)] callback(Event::DeviceEvent { device_id, event: DeviceEvent::Key(KeyboardInput { - scancode, + scancode: scancode as u32, virtual_keycode, state, modifiers, @@ -1094,12 +1121,12 @@ impl EventProcessor { let new_modifiers = self.device_mod_state.modifiers(); if modifiers != new_modifiers { - callback(Event::DeviceEvent { - device_id, - event: DeviceEvent::ModifiersChanged { - modifiers: new_modifiers, - }, - }); + if let Some(window_id) = self.active_window { + callback(Event::WindowEvent { + window_id: mkwid(window_id), + event: WindowEvent::ModifiersChanged(new_modifiers), + }); + } } } } @@ -1141,27 +1168,48 @@ impl EventProcessor { .iter() .find(|prev_monitor| prev_monitor.name == new_monitor.name) .map(|prev_monitor| { - if new_monitor.hidpi_factor != prev_monitor.hidpi_factor { + if new_monitor.scale_factor != prev_monitor.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 { - callback(Event::WindowEvent { - window_id: mkwid(window_id.0), - event: WindowEvent::HiDpiFactorChanged( - new_monitor.hidpi_factor, - ), - }); let (width, height) = window.inner_size_physical(); - let (_, _, flusher) = window.adjust_for_dpi( - prev_monitor.hidpi_factor, - new_monitor.hidpi_factor, - width as f64, - height as f64, + let (new_width, new_height) = window + .adjust_for_dpi( + prev_monitor.scale_factor, + new_monitor.scale_factor, + width, + height, + &*window.shared_state.lock(), + ); + + let window_id = crate::window::WindowId( + crate::platform_impl::platform::WindowId::X( + *window_id, + ), ); - flusher.queue(); + 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, + ); + } } } } @@ -1180,4 +1228,73 @@ impl EventProcessor { Err(_) => (), } } + + fn handle_pressed_keys( + wt: &super::EventLoopWindowTarget, + window_id: crate::window::WindowId, + state: ElementState, + mod_keymap: &ModifierKeymap, + device_mod_state: &mut ModifierKeyState, + callback: &mut F, + ) where + F: FnMut(Event<'_, T>), + { + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + let modifiers = device_mod_state.modifiers(); + + // Update modifiers state and emit key events based on which keys are currently pressed. + for keycode in wt + .xconn + .query_keymap() + .into_iter() + .filter(|k| *k >= KEYCODE_OFFSET) + { + let scancode = (keycode - KEYCODE_OFFSET) as u32; + let keysym = wt.xconn.keycode_to_keysym(keycode); + let virtual_keycode = events::keysym_to_element(keysym as c_uint); + + if let Some(modifier) = mod_keymap.get_modifier(keycode as ffi::KeyCode) { + device_mod_state.key_event( + ElementState::Pressed, + keycode as ffi::KeyCode, + modifier, + ); + } + + #[allow(deprecated)] + callback(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id, + input: KeyboardInput { + scancode, + state, + virtual_keycode, + modifiers, + }, + is_synthetic: true, + }, + }); + } + } +} + +fn is_first_touch(first: &mut Option, num: &mut u32, id: u64, phase: TouchPhase) -> bool { + match phase { + TouchPhase::Started => { + if *num == 0 { + *first = Some(id); + } + *num += 1; + } + TouchPhase::Cancelled | TouchPhase::Ended => { + if *first == Some(id) { + *first = None; + } + *num = num.saturating_sub(1); + } + _ => (), + } + + *first == Some(id) } diff --git a/src/platform_impl/linux/x11/events.rs b/src/platform_impl/linux/x11/events.rs index 1b4996e745..0c02ca0c82 100644 --- a/src/platform_impl/linux/x11/events.rs +++ b/src/platform_impl/linux/x11/events.rs @@ -81,12 +81,12 @@ pub fn keysym_to_element(keysym: libc::c_uint) -> Option { 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::Add, + ffi::XK_KP_Multiply => VirtualKeyCode::NumpadMultiply, + ffi::XK_KP_Add => VirtualKeyCode::NumpadAdd, //ffi::XK_KP_Separator => VirtualKeyCode::Kp_separator, - ffi::XK_KP_Subtract => VirtualKeyCode::Subtract, - //ffi::XK_KP_Decimal => VirtualKeyCode::Kp_decimal, - ffi::XK_KP_Divide => VirtualKeyCode::Divide, + 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, @@ -183,10 +183,10 @@ pub fn keysym_to_element(keysym: libc::c_uint) -> Option { //ffi::XK_quoteright => VirtualKeyCode::Quoteright, //ffi::XK_parenleft => VirtualKeyCode::Parenleft, //ffi::XK_parenright => VirtualKeyCode::Parenright, - //ffi::XK_asterisk => VirtualKeyCode::Asterisk, - ffi::XK_plus => VirtualKeyCode::Add, + ffi::XK_asterisk => VirtualKeyCode::Asterisk, + ffi::XK_plus => VirtualKeyCode::Plus, ffi::XK_comma => VirtualKeyCode::Comma, - ffi::XK_minus => VirtualKeyCode::Subtract, + ffi::XK_minus => VirtualKeyCode::Minus, ffi::XK_period => VirtualKeyCode::Period, ffi::XK_slash => VirtualKeyCode::Slash, ffi::XK_0 => VirtualKeyCode::Key0, diff --git a/src/platform_impl/linux/x11/ime/input_method.rs b/src/platform_impl/linux/x11/ime/input_method.rs index 42c4033efb..142c150199 100644 --- a/src/platform_impl/linux/x11/ime/input_method.rs +++ b/src/platform_impl/linux/x11/ime/input_method.rs @@ -21,7 +21,8 @@ unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { xconn: Arc, wm_delete_window: ffi::Atom, @@ -53,23 +68,21 @@ pub struct EventLoopWindowTarget { root: ffi::Window, ime: RefCell, windows: RefCell>>, - pending_redraws: Arc>>, + redraw_sender: Sender, _marker: ::std::marker::PhantomData, } pub struct EventLoop { - inner_loop: ::calloop::EventLoop<()>, - _x11_source: ::calloop::Source<::calloop::generic::Generic<::calloop::generic::EventedRawFd>>, - _user_source: ::calloop::Source<::calloop::channel::Channel>, - pending_user_events: Rc>>, - event_processor: Rc>>, - user_sender: ::calloop::channel::Sender, - pending_events: Rc>>>, - pub(crate) target: Rc>, + poll: Poll, + event_processor: EventProcessor, + redraw_channel: Receiver, + user_channel: Receiver, + user_sender: Sender, + target: Rc>, } pub struct EventLoopProxy { - user_sender: ::calloop::channel::Sender, + user_sender: Sender, } impl Clone for EventLoopProxy { @@ -95,7 +108,25 @@ impl EventLoop { // Input methods will open successfully without setting the locale, but it won't be // possible to actually commit pre-edit sequences. unsafe { + // Remember default locale to restore it if target locale is unsupported + // by Xlib + let default_locale = setlocale(LC_CTYPE, ptr::null()); setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); + + // Check if set locale is supported by Xlib. + // If not, calls to some Xlib functions like `XSetLocaleModifiers` + // will fail. + let locale_supported = (xconn.xlib.XSupportsLocale)() == 1; + if !locale_supported { + let unsupported_locale = setlocale(LC_CTYPE, ptr::null()); + warn!( + "Unsupported locale \"{}\". Restoring default locale \"{}\".", + CStr::from_ptr(unsupported_locale).to_string_lossy(), + CStr::from_ptr(default_locale).to_string_lossy() + ); + // Restore default locale + setlocale(LC_CTYPE, default_locale); + } } let ime = RefCell::new({ let result = Ime::new(Arc::clone(&xconn)); @@ -148,6 +179,35 @@ impl EventLoop { let mut mod_keymap = ModifierKeymap::new(); mod_keymap.reset_from_x_connection(&xconn); + let poll = Poll::new().unwrap(); + + let (user_sender, user_channel) = channel(); + let (redraw_sender, redraw_channel) = channel(); + + poll.register( + &EventedFd(&xconn.x11_fd), + X_TOKEN, + Ready::readable(), + PollOpt::level(), + ) + .unwrap(); + + poll.register( + &user_channel, + USER_TOKEN, + Ready::readable(), + PollOpt::level(), + ) + .unwrap(); + + poll.register( + &redraw_channel, + REDRAW_TOKEN, + Ready::readable(), + PollOpt::level(), + ) + .unwrap(); + let target = Rc::new(RootELW { p: super::EventLoopWindowTarget::X(EventLoopWindowTarget { ime, @@ -158,33 +218,12 @@ impl EventLoop { xconn, wm_delete_window, net_wm_ping, - pending_redraws: Default::default(), + redraw_sender, }), _marker: ::std::marker::PhantomData, }); - // A calloop event loop to drive us - let inner_loop = ::calloop::EventLoop::new().unwrap(); - - // Handle user events - let pending_user_events = Rc::new(RefCell::new(VecDeque::new())); - let pending_user_events2 = pending_user_events.clone(); - - let (user_sender, user_channel) = ::calloop::channel::channel(); - - let _user_source = inner_loop - .handle() - .insert_source(user_channel, move |evt, &mut ()| { - if let ::calloop::channel::Event::Msg(msg) = evt { - pending_user_events2.borrow_mut().push_back(msg); - } - }) - .unwrap(); - - // Handle X11 events - let pending_events: Rc>> = Default::default(); - - let processor = EventProcessor { + let event_processor = EventProcessor { target: target.clone(), dnd, devices: Default::default(), @@ -193,6 +232,9 @@ impl EventLoop { xi2ext, mod_keymap, device_mod_state: Default::default(), + num_touch: 0, + first_touch: None, + active_window: None, }; // Register for device hotplug events @@ -202,36 +244,13 @@ impl EventLoop { .select_xinput_events(root, ffi::XIAllDevices, ffi::XI_HierarchyChangedMask) .queue(); - processor.init_device(ffi::XIAllDevices); - - let processor = Rc::new(RefCell::new(processor)); - let event_processor = processor.clone(); - - // Setup the X11 event source - let mut x11_events = - ::calloop::generic::Generic::from_raw_fd(get_xtarget(&target).xconn.x11_fd); - x11_events.set_interest(::calloop::mio::Ready::readable()); - let _x11_source = inner_loop - .handle() - .insert_source(x11_events, { - let pending_events = pending_events.clone(); - move |evt, &mut ()| { - if evt.readiness.is_readable() { - let mut processor = processor.borrow_mut(); - let mut pending_events = pending_events.borrow_mut(); - drain_events(&mut processor, &mut pending_events); - } - } - }) - .unwrap(); + event_processor.init_device(ffi::XIAllDevices); let result = EventLoop { - inner_loop, - pending_events, - _x11_source, - _user_source, + poll, + redraw_channel, + user_channel, user_sender, - pending_user_events, event_processor, target, }; @@ -251,61 +270,65 @@ impl EventLoop { pub fn run_return(&mut self, mut callback: F) where - F: FnMut(Event, &RootELW, &mut ControlFlow), + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { let mut control_flow = ControlFlow::default(); - let wt = get_xtarget(&self.target); - - callback( - crate::event::Event::NewEvents(crate::event::StartCause::Init), - &self.target, - &mut control_flow, - ); + let mut events = Events::with_capacity(8); + let mut cause = StartCause::Init; loop { - self.drain_events(); + sticky_exit_callback( + crate::event::Event::NewEvents(cause), + &self.target, + &mut control_flow, + &mut callback, + ); - // Empty the event buffer - { - let mut guard = self.pending_events.borrow_mut(); - for evt in guard.drain(..) { - sticky_exit_callback(evt, &self.target, &mut control_flow, &mut callback); - } - } + // Process all pending events + self.drain_events(&mut callback, &mut control_flow); // Empty the user event buffer { - let mut guard = self.pending_user_events.borrow_mut(); - for evt in guard.drain(..) { + while let Ok(event) = self.user_channel.try_recv() { sticky_exit_callback( - crate::event::Event::UserEvent(evt), + crate::event::Event::UserEvent(event), &self.target, &mut control_flow, &mut callback, ); } } + // send MainEventsCleared + { + sticky_exit_callback( + crate::event::Event::MainEventsCleared, + &self.target, + &mut control_flow, + &mut callback, + ); + } // Empty the redraw requests { - // Release the lock to prevent deadlock - let windows: Vec<_> = wt.pending_redraws.lock().unwrap().drain().collect(); + let mut windows = HashSet::new(); + + while let Ok(window_id) = self.redraw_channel.try_recv() { + windows.insert(window_id); + } - for wid in windows { + for window_id in windows { + let window_id = crate::window::WindowId(super::WindowId::X(window_id)); sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(super::WindowId::X(wid)), - event: WindowEvent::RedrawRequested, - }, + Event::RedrawRequested(window_id), &self.target, &mut control_flow, &mut callback, ); } } - // send Events cleared + // send RedrawEventsCleared { sticky_exit_callback( - crate::event::Event::EventsCleared, + crate::event::Event::RedrawEventsCleared, &self.target, &mut control_flow, &mut callback, @@ -313,7 +336,7 @@ impl EventLoop { } let start = Instant::now(); - let (mut cause, deadline, mut timeout); + let (deadline, timeout); match control_flow { ControlFlow::Exit => break, @@ -344,26 +367,21 @@ impl EventLoop { } } - if self.events_waiting() { - timeout = Some(Duration::from_millis(0)); + // If the XConnection already contains buffered events, we don't + // need to wait for data on the socket. + if !self.event_processor.poll() { + self.poll.poll(&mut events, timeout).unwrap(); + events.clear(); } - self.inner_loop.dispatch(timeout, &mut ()).unwrap(); + let wait_cancelled = deadline.map_or(false, |deadline| Instant::now() < deadline); - if let Some(deadline) = deadline { - if deadline > Instant::now() { - cause = StartCause::WaitCancelled { - start, - requested_resume: Some(deadline), - }; - } + if wait_cancelled { + cause = StartCause::WaitCancelled { + start, + requested_resume: deadline, + }; } - - callback( - crate::event::Event::NewEvents(cause), - &self.target, - &mut control_flow, - ); } callback( @@ -375,43 +393,48 @@ impl EventLoop { pub fn run(mut self, callback: F) -> ! where - F: 'static + FnMut(Event, &RootELW, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { self.run_return(callback); ::std::process::exit(0); } - fn drain_events(&self) { - let mut processor = self.event_processor.borrow_mut(); - let mut pending_events = self.pending_events.borrow_mut(); - - drain_events(&mut processor, &mut pending_events); - } + fn drain_events(&mut self, callback: &mut F, control_flow: &mut ControlFlow) + where + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + { + let target = &self.target; + let mut xev = MaybeUninit::uninit(); - fn events_waiting(&self) -> bool { - !self.pending_events.borrow().is_empty() || self.event_processor.borrow().poll() - } -} + let wt = get_xtarget(&self.target); -fn drain_events( - processor: &mut EventProcessor, - pending_events: &mut VecDeque>, -) { - let mut callback = |event| { - pending_events.push_back(event); - }; - - // process all pending events - let mut xev = MaybeUninit::uninit(); - while unsafe { processor.poll_one_event(xev.as_mut_ptr()) } { - let mut xev = unsafe { xev.assume_init() }; - processor.process_event(&mut xev, &mut callback); + 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( + super::WindowId::X(wid), + )) = event + { + wt.redraw_sender.send(wid).unwrap(); + } else { + callback(event, window_target, control_flow); + } + }, + ); + }); + } } } pub(crate) fn get_xtarget(target: &RootELW) -> &EventLoopWindowTarget { match target.p { super::EventLoopWindowTarget::X(ref target) => target, + #[cfg(feature = "wayland")] _ => unreachable!(), } } @@ -425,8 +448,14 @@ impl EventLoopWindowTarget { } impl EventLoopProxy { - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.user_sender.send(event).map_err(|_| EventLoopClosed) + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_sender.send(event).map_err(|e| { + EventLoopClosed(if let SendError::Disconnected(x) = e { + x + } else { + unreachable!() + }) + }) } } diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 1fe2fd2000..274e0cda9f 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -38,7 +38,7 @@ pub struct VideoMode { impl VideoMode { #[inline] - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.size.into() } @@ -73,7 +73,7 @@ pub struct MonitorHandle { /// If the monitor is the primary one primary: bool, /// The DPI scale factor - pub(crate) hidpi_factor: f64, + pub(crate) scale_factor: f64, /// Used to determine which windows are on this monitor pub(crate) rect: util::AaRect, /// Supported video modes on this monitor @@ -114,14 +114,14 @@ impl MonitorHandle { crtc: *mut XRRCrtcInfo, primary: bool, ) -> Option { - let (name, hidpi_factor, video_modes) = unsafe { xconn.get_output_info(resources, crtc)? }; + let (name, scale_factor, video_modes) = unsafe { xconn.get_output_info(resources, crtc)? }; let dimensions = unsafe { ((*crtc).width as u32, (*crtc).height as u32) }; let position = unsafe { ((*crtc).x as i32, (*crtc).y as i32) }; let rect = util::AaRect::new(position, dimensions); Some(MonitorHandle { id, name, - hidpi_factor, + scale_factor, dimensions, position, primary, @@ -134,7 +134,7 @@ impl MonitorHandle { MonitorHandle { id: 0, name: "".into(), - hidpi_factor: 1.0, + scale_factor: 1.0, dimensions: (1, 1), position: (0, 0), primary: true, @@ -143,6 +143,11 @@ impl MonitorHandle { } } + pub(crate) fn is_dummy(&self) -> bool { + // Zero is an invalid XID value; no real monitor will have it + self.id == 0 + } + pub fn name(&self) -> Option { Some(self.name.clone()) } @@ -152,17 +157,17 @@ impl MonitorHandle { self.id as u32 } - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.dimensions.into() } - pub fn position(&self) -> PhysicalPosition { + pub fn position(&self) -> PhysicalPosition { self.position.into() } #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.hidpi_factor + pub fn scale_factor(&self) -> f64 { + self.scale_factor } #[inline] diff --git a/src/platform_impl/linux/x11/util/client_msg.rs b/src/platform_impl/linux/x11/util/client_msg.rs index 3fafcdf999..2f2b7edf9c 100644 --- a/src/platform_impl/linux/x11/util/client_msg.rs +++ b/src/platform_impl/linux/x11/util/client_msg.rs @@ -43,62 +43,4 @@ impl XConnection { }; self.send_event(target_window, event_mask, event) } - - // Prepare yourself for the ultimate in unsafety! - // You should favor `send_client_msg` whenever possible, but some protocols (i.e. startup notification) require you - // to send more than one message worth of data. - pub fn send_client_msg_multi( - &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: &[T], - ) -> Flusher<'_> { - let format = T::FORMAT; - let size_of_t = mem::size_of::(); - debug_assert_eq!(size_of_t, format.get_actual_size()); - let mut event = ffi::XClientMessageEvent { - type_: ffi::ClientMessage, - display: self.display, - window, - message_type, - format: format as c_int, - data: ffi::ClientMessageData::new(), - // These fields are ignored by `XSendEvent` - serial: 0, - send_event: 0, - }; - - let t_per_payload = format.get_payload_size() / size_of_t; - assert!(t_per_payload > 0); - let payload_count = data.len() / t_per_payload; - let payload_remainder = data.len() % t_per_payload; - let payload_ptr = data.as_ptr() as *const ClientMsgPayload; - - let mut payload_index = 0; - while payload_index < payload_count { - let payload = unsafe { payload_ptr.offset(payload_index as isize) }; - payload_index += 1; - event.data = unsafe { mem::transmute(*payload) }; - self.send_event(target_window, event_mask, &event).queue(); - } - - if payload_remainder > 0 { - let mut payload: ClientMsgPayload = [0; 5]; - let t_payload = payload.as_mut_ptr() as *mut T; - let invalid_payload = unsafe { payload_ptr.offset(payload_index as isize) }; - let invalid_t_payload = invalid_payload as *const T; - let mut t_index = 0; - while t_index < payload_remainder { - let valid_t = unsafe { invalid_t_payload.offset(t_index as isize) }; - unsafe { (*t_payload.offset(t_index as isize)) = (*valid_t).clone() }; - t_index += 1; - } - event.data = unsafe { mem::transmute(payload) }; - self.send_event(target_window, event_mask, &event).queue(); - } - - Flusher::new(self) - } } diff --git a/src/platform_impl/linux/x11/util/format.rs b/src/platform_impl/linux/x11/util/format.rs index 6090c65d5d..1ab5e01f7e 100644 --- a/src/platform_impl/linux/x11/util/format.rs +++ b/src/platform_impl/linux/x11/util/format.rs @@ -21,10 +21,6 @@ impl Format { } } - pub fn is_same_size_as(&self) -> bool { - mem::size_of::() == self.get_actual_size() - } - pub fn get_actual_size(&self) -> usize { match self { &Format::Char => mem::size_of::(), @@ -32,15 +28,6 @@ impl Format { &Format::Long => mem::size_of::(), } } - - pub fn get_payload_size(&self) -> usize { - match self { - // Due to the wonders of X11, half the space goes unused if you're not using longs (on 64-bit). - &Format::Char => mem::size_of::() * 20, - &Format::Short => mem::size_of::() * 10, - &Format::Long => mem::size_of::() * 5, - } - } } pub trait Formattable: Debug + Clone + Copy + PartialEq + PartialOrd { diff --git a/src/platform_impl/linux/x11/util/geometry.rs b/src/platform_impl/linux/x11/util/geometry.rs index 6b59d13a2a..d8f466fc2a 100644 --- a/src/platform_impl/linux/x11/util/geometry.rs +++ b/src/platform_impl/linux/x11/util/geometry.rs @@ -1,7 +1,6 @@ use std::cmp; use super::*; -use crate::dpi::{LogicalPosition, LogicalSize}; // Friendly neighborhood axis-aligned rectangle #[derive(Debug, Clone, PartialEq, Eq)] @@ -89,16 +88,6 @@ impl FrameExtents { pub fn from_border(border: c_ulong) -> Self { Self::new(border, border, border, border) } - - pub fn as_logical(&self, factor: f64) -> LogicalFrameExtents { - let logicalize = |value: c_ulong| value as f64 / factor; - LogicalFrameExtents { - left: logicalize(self.left), - right: logicalize(self.right), - top: logicalize(self.top), - bottom: logicalize(self.bottom), - } - } } #[derive(Debug, Clone)] @@ -135,20 +124,6 @@ impl FrameExtentsHeuristic { } } - pub fn inner_pos_to_outer_logical( - &self, - mut logical: LogicalPosition, - factor: f64, - ) -> LogicalPosition { - use self::FrameExtentsHeuristicPath::*; - if self.heuristic_path != UnsupportedBordered { - let frame_extents = self.frame_extents.as_logical(factor); - logical.x -= frame_extents.left; - logical.y -= frame_extents.top; - } - logical - } - pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { ( width.saturating_add( @@ -163,17 +138,6 @@ impl FrameExtentsHeuristic { ), ) } - - pub fn inner_size_to_outer_logical( - &self, - mut logical: LogicalSize, - factor: f64, - ) -> LogicalSize { - let frame_extents = self.frame_extents.as_logical(factor); - logical.width += frame_extents.left + frame_extents.right; - logical.height += frame_extents.top + frame_extents.bottom; - logical - } } impl XConnection { diff --git a/src/platform_impl/linux/x11/util/hint.rs b/src/platform_impl/linux/x11/util/hint.rs index 94c7e33a72..809f3063b0 100644 --- a/src/platform_impl/linux/x11/util/hint.rs +++ b/src/platform_impl/linux/x11/util/hint.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use super::*; #[derive(Debug)] +#[allow(dead_code)] pub enum StateOperation { Remove = 0, // _NET_WM_STATE_REMOVE Add = 1, // _NET_WM_STATE_ADD @@ -189,22 +190,6 @@ impl<'a> NormalHints<'a> { } } - pub fn has_flag(&self, flag: c_long) -> bool { - has_flag(self.size_hints.flags, flag) - } - - fn getter(&self, flag: c_long, field1: &c_int, field2: &c_int) -> Option<(u32, u32)> { - if self.has_flag(flag) { - Some((*field1 as _, *field2 as _)) - } else { - None - } - } - - pub fn get_size(&self) -> Option<(u32, u32)> { - self.getter(ffi::PSize, &self.size_hints.width, &self.size_hints.height) - } - // WARNING: This hint is obsolete pub fn set_size(&mut self, size: Option<(u32, u32)>) { if let Some((width, height)) = size { @@ -216,14 +201,6 @@ impl<'a> NormalHints<'a> { } } - pub fn get_max_size(&self) -> Option<(u32, u32)> { - self.getter( - ffi::PMaxSize, - &self.size_hints.max_width, - &self.size_hints.max_height, - ) - } - 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; @@ -234,14 +211,6 @@ impl<'a> NormalHints<'a> { } } - pub fn get_min_size(&self) -> Option<(u32, u32)> { - self.getter( - ffi::PMinSize, - &self.size_hints.min_width, - &self.size_hints.min_height, - ) - } - 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; @@ -252,14 +221,6 @@ impl<'a> NormalHints<'a> { } } - pub fn get_resize_increments(&self) -> Option<(u32, u32)> { - self.getter( - ffi::PResizeInc, - &self.size_hints.width_inc, - &self.size_hints.height_inc, - ) - } - 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; @@ -270,14 +231,6 @@ impl<'a> NormalHints<'a> { } } - pub fn get_base_size(&self) -> Option<(u32, u32)> { - self.getter( - ffi::PBaseSize, - &self.size_hints.base_width, - &self.size_hints.base_height, - ) - } - 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; diff --git a/src/platform_impl/linux/x11/util/icon.rs b/src/platform_impl/linux/x11/util/icon.rs index 5334d4a9c9..eb13d48b5b 100644 --- a/src/platform_impl/linux/x11/util/icon.rs +++ b/src/platform_impl/linux/x11/util/icon.rs @@ -1,5 +1,5 @@ use super::*; -use crate::window::{Icon, Pixel, PIXEL_SIZE}; +use crate::icon::{Icon, Pixel, PIXEL_SIZE}; impl Pixel { pub fn to_packed_argb(&self) -> Cardinal { @@ -18,13 +18,14 @@ impl Pixel { impl Icon { pub(crate) fn to_cardinals(&self) -> Vec { - 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 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); let mut data = Vec::with_capacity(pixel_count); - data.push(self.width as Cardinal); - data.push(self.height as Cardinal); - let pixels = self.rgba.as_ptr() as *const Pixel; + data.push(rgba_icon.width as Cardinal); + data.push(rgba_icon.height as Cardinal); + let pixels = rgba_icon.rgba.as_ptr() as *const Pixel; for pixel_index in 0..pixel_count { let pixel = unsafe { &*pixels.offset(pixel_index as isize) }; 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 2fbabe1e63..e9f45aee1c 100644 --- a/src/platform_impl/linux/x11/util/input.rs +++ b/src/platform_impl/linux/x11/util/input.rs @@ -17,12 +17,12 @@ impl ModifiersState { } pub(crate) fn from_x11_mask(mask: c_uint) -> Self { - ModifiersState { - alt: mask & ffi::Mod1Mask != 0, - shift: mask & ffi::ShiftMask != 0, - ctrl: mask & ffi::ControlMask != 0, - logo: mask & ffi::Mod4Mask != 0, - } + let mut m = ModifiersState::empty(); + m.set(ModifiersState::ALT, mask & ffi::Mod1Mask != 0); + m.set(ModifiersState::SHIFT, mask & ffi::ShiftMask != 0); + m.set(ModifiersState::CTRL, mask & ffi::ControlMask != 0); + m.set(ModifiersState::LOGO, mask & ffi::Mod4Mask != 0); + m } } diff --git a/src/platform_impl/linux/x11/util/keys.rs b/src/platform_impl/linux/x11/util/keys.rs new file mode 100644 index 0000000000..fc0c9d9062 --- /dev/null +++ b/src/platform_impl/linux/x11/util/keys.rs @@ -0,0 +1,92 @@ +use std::{iter::Enumerate, ptr, slice::Iter}; + +use super::*; + +pub struct Keymap { + keys: [u8; 32], +} + +pub struct KeymapIter<'a> { + iter: Enumerate>, + index: usize, + item: Option, +} + +impl Keymap { + pub fn iter(&self) -> KeymapIter<'_> { + KeymapIter { + iter: self.keys.iter().enumerate(), + index: 0, + item: None, + } + } +} + +impl<'a> IntoIterator for &'a Keymap { + type Item = ffi::KeyCode; + type IntoIter = KeymapIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Iterator for KeymapIter<'_> { + type Item = ffi::KeyCode; + + fn next(&mut self) -> Option { + if self.item.is_none() { + while let Some((index, &item)) = self.iter.next() { + if item != 0 { + self.index = index; + self.item = Some(item); + break; + } + } + } + + self.item.take().map(|item| { + debug_assert!(item != 0); + + let bit = first_bit(item); + + if item != bit { + // Remove the first bit; save the rest for further iterations + self.item = Some(item ^ bit); + } + + let shift = bit.trailing_zeros() + (self.index * 8) as u32; + shift as ffi::KeyCode + }) + } +} + +impl XConnection { + pub fn keycode_to_keysym(&self, keycode: ffi::KeyCode) -> ffi::KeySym { + unsafe { (self.xlib.XKeycodeToKeysym)(self.display, keycode, 0) } + } + + pub fn lookup_keysym(&self, xkev: &mut ffi::XKeyEvent) -> ffi::KeySym { + let mut keysym = 0; + + unsafe { + (self.xlib.XLookupString)(xkev, ptr::null_mut(), 0, &mut keysym, ptr::null_mut()); + } + + keysym + } + + pub fn query_keymap(&self) -> Keymap { + let mut keys = [0; 32]; + + unsafe { + (self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char); + } + + Keymap { keys } + } +} + +fn first_bit(b: u8) -> u8 { + 1 << b.trailing_zeros() +} diff --git a/src/platform_impl/linux/x11/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index 1410da28ff..4bd1420655 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -9,6 +9,7 @@ mod geometry; mod hint; mod icon; mod input; +pub mod keys; mod memory; pub mod modifiers; mod randr; @@ -22,18 +23,12 @@ pub use self::{ use std::{ mem::{self, MaybeUninit}, - ops::BitAnd, os::raw::*, ptr, }; use super::{ffi, XConnection, XError}; -pub fn reinterpret<'a, A, B>(a: &'a A) -> &'a B { - let b_ptr = a as *const _ as *const B; - unsafe { &*b_ptr } -} - pub fn maybe_change(field: &mut Option, value: T) -> bool { let wrapped = Some(value); if *field != wrapped { @@ -44,13 +39,6 @@ pub fn maybe_change(field: &mut Option, value: T) -> bool { } } -pub fn has_flag(bitset: T, flag: T) -> bool -where - T: Copy + PartialEq + BitAnd, -{ - 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 struct Flusher<'a> { xconn: &'a XConnection, diff --git a/src/platform_impl/linux/x11/util/modifiers.rs b/src/platform_impl/linux/x11/util/modifiers.rs index 9bdc6e99e5..7c951997ad 100644 --- a/src/platform_impl/linux/x11/util/modifiers.rs +++ b/src/platform_impl/linux/x11/util/modifiers.rs @@ -116,10 +116,10 @@ impl ModifierKeyState { let mut new_state = *state; match except { - Some(Modifier::Alt) => new_state.alt = self.state.alt, - Some(Modifier::Ctrl) => new_state.ctrl = self.state.ctrl, - Some(Modifier::Shift) => new_state.shift = self.state.shift, - Some(Modifier::Logo) => new_state.logo = self.state.logo, + Some(Modifier::Alt) => new_state.set(ModifiersState::ALT, self.state.alt()), + Some(Modifier::Ctrl) => new_state.set(ModifiersState::CTRL, self.state.ctrl()), + Some(Modifier::Shift) => new_state.set(ModifiersState::SHIFT, self.state.shift()), + Some(Modifier::Logo) => new_state.set(ModifiersState::LOGO, self.state.logo()), None => (), } @@ -170,18 +170,18 @@ impl ModifierKeyState { fn get_modifier(state: &ModifiersState, modifier: Modifier) -> bool { match modifier { - Modifier::Alt => state.alt, - Modifier::Ctrl => state.ctrl, - Modifier::Shift => state.shift, - Modifier::Logo => state.logo, + Modifier::Alt => state.alt(), + Modifier::Ctrl => state.ctrl(), + Modifier::Shift => state.shift(), + Modifier::Logo => state.logo(), } } fn set_modifier(state: &mut ModifiersState, modifier: Modifier, value: bool) { match modifier { - Modifier::Alt => state.alt = value, - Modifier::Ctrl => state.ctrl = value, - Modifier::Shift => state.shift = value, - Modifier::Logo => state.logo = value, + Modifier::Alt => state.set(ModifiersState::ALT, value), + Modifier::Ctrl => state.set(ModifiersState::CTRL, value), + Modifier::Shift => state.set(ModifiersState::SHIFT, value), + Modifier::Logo => state.set(ModifiersState::LOGO, value), } } diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 28fcb601ca..b9258c68f1 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -4,26 +4,19 @@ use super::{ ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, *, }; -use crate::{dpi::validate_hidpi_factor, platform_impl::platform::x11::VideoMode}; +use crate::{dpi::validate_scale_factor, platform_impl::platform::x11::VideoMode}; + +/// Represents values of `WINIT_HIDPI_FACTOR`. +pub enum EnvVarDPI { + Randr, + Scale(f64), + NotSet, +} pub fn calc_dpi_factor( (width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64), ) -> f64 { - // Override DPI if `WINIT_HIDPI_FACTOR` variable is set - let dpi_override = env::var("WINIT_HIDPI_FACTOR") - .ok() - .and_then(|var| f64::from_str(&var).ok()); - if let Some(dpi_override) = dpi_override { - if !validate_hidpi_factor(dpi_override) { - panic!( - "`WINIT_HIDPI_FACTOR` invalid; DPI factors must be normal floats greater than 0. Got `{}`", - dpi_override, - ); - } - return dpi_override; - } - // See http://xpra.org/trac/ticket/728 for more information. if width_mm == 0 || height_mm == 0 { warn!("XRandR reported that the display's 0mm in size, which is certifiably insane"); @@ -33,7 +26,7 @@ pub fn calc_dpi_factor( let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // Quantize 1/12 step size let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0); - assert!(validate_hidpi_factor(dpi_factor)); + assert!(validate_scale_factor(dpi_factor)); dpi_factor } @@ -107,20 +100,65 @@ impl XConnection { (*output_info).nameLen as usize, ); let name = String::from_utf8_lossy(name_slice).into(); - let hidpi_factor = if let Some(dpi) = self.get_xft_dpi() { - dpi / 96. - } else { - calc_dpi_factor( + // 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() { + warn!( + "The WINIT_HIDPI_FACTOR environment variable is deprecated; use WINIT_X11_SCALE_FACTOR" + ) + } + let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else( + || EnvVarDPI::NotSet, + |var| { + if var.to_lowercase() == "randr" { + EnvVarDPI::Randr + } else if let Ok(dpi) = f64::from_str(&var) { + EnvVarDPI::Scale(dpi) + } else if var.is_empty() { + EnvVarDPI::NotSet + } else { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal floats greater than 0, or `randr`. Got `{}`", + var + ); + } + }, + ); + + let scale_factor = match dpi_env { + EnvVarDPI::Randr => calc_dpi_factor( ((*crtc).width as u32, (*crtc).height as u32), ( (*output_info).mm_width as u64, (*output_info).mm_height as u64, ), - ) + ), + EnvVarDPI::Scale(dpi_override) => { + if !validate_scale_factor(dpi_override) { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal floats greater than 0, or `randr`. Got `{}`", + dpi_override, + ); + } + dpi_override + } + EnvVarDPI::NotSet => { + if let Some(dpi) = self.get_xft_dpi() { + dpi / 96. + } else { + calc_dpi_factor( + ((*crtc).width as u32, (*crtc).height as u32), + ( + (*output_info).mm_width as u64, + (*output_info).mm_height as u64, + ), + ) + } + } }; (self.xrandr.XRRFreeOutputInfo)(output_info); - Some((name, hidpi_factor, modes)) + Some((name, scale_factor, modes)) } pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Result<(), ()> { unsafe { diff --git a/src/platform_impl/linux/x11/util/window_property.rs b/src/platform_impl/linux/x11/util/window_property.rs index 0b9e7a71e3..34977f36d2 100644 --- a/src/platform_impl/linux/x11/util/window_property.rs +++ b/src/platform_impl/linux/x11/util/window_property.rs @@ -26,6 +26,7 @@ impl GetPropertyError { 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, diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index f967a10060..0fa64cf164 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1,8 +1,6 @@ use raw_window_handle::unix::XlibHandle; use std::{ - cmp, - collections::HashSet, - env, + cmp, env, ffi::CString, mem::{self, replace, MaybeUninit}, os::raw::*, @@ -12,10 +10,11 @@ use std::{ }; use libc; +use mio_extras::channel::Sender; use parking_lot::Mutex; use crate::{ - dpi::{LogicalPosition, LogicalSize}, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::{ @@ -23,7 +22,7 @@ use crate::{ MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, - window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Icon, UserAttentionType, WindowAttributes}, }; use super::{ffi, util, EventLoopWindowTarget, ImeSender, WindowId, XConnection, XError}; @@ -36,7 +35,7 @@ pub struct SharedState { pub inner_position: Option<(i32, i32)>, pub inner_position_rel_parent: Option<(i32, i32)>, pub last_monitor: X11MonitorHandle, - pub dpi_adjusted: Option<(f64, f64)>, + pub dpi_adjusted: Option<(u32, u32)>, pub fullscreen: Option, // Set when application calls `set_fullscreen` when window is not visible pub desired_fullscreen: Option>, @@ -45,8 +44,10 @@ pub struct SharedState { // Used to restore video mode after exiting fullscreen pub desktop_video_mode: Option<(ffi::RRCrtc, ffi::RRMode)>, pub frame_extents: Option, - pub min_inner_size: Option, - pub max_inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub resize_increments: Option, + pub base_size: Option, pub visibility: Visibility, } @@ -83,6 +84,8 @@ impl SharedState { frame_extents: None, min_inner_size: None, max_inner_size: None, + resize_increments: None, + base_size: None, }) } } @@ -100,7 +103,7 @@ pub struct UnownedWindow { cursor_visible: Mutex, ime_sender: Mutex, pub shared_state: Mutex, - pending_redraws: Arc<::std::sync::Mutex>>, + redraw_sender: Sender, } impl UnownedWindow { @@ -132,24 +135,24 @@ impl UnownedWindow { }) .unwrap_or_else(|| monitors.swap_remove(0)) }; - let dpi_factor = guessed_monitor.hidpi_factor(); + let scale_factor = guessed_monitor.scale_factor(); - info!("Guessed window DPI factor: {}", dpi_factor); + info!("Guessed window scale factor: {}", scale_factor); let max_inner_size: Option<(u32, u32)> = window_attrs .max_inner_size - .map(|size| size.to_physical(dpi_factor).into()); + .map(|size| size.to_physical::(scale_factor).into()); let min_inner_size: Option<(u32, u32)> = window_attrs .min_inner_size - .map(|size| size.to_physical(dpi_factor).into()); + .map(|size| size.to_physical::(scale_factor).into()); let dimensions = { // x11 only applies constraints when the window is actively resized // by the user, so we have to manually apply the initial constraints let mut dimensions: (u32, u32) = window_attrs .inner_size + .map(|size| size.to_physical::(scale_factor)) .or_else(|| Some((800, 600).into())) - .map(|size| size.to_physical(dpi_factor)) .map(Into::into) .unwrap(); if let Some(max) = max_inner_size { @@ -223,7 +226,7 @@ impl UnownedWindow { // is > 0, like we do in glutin. // // It is non obvious which masks, if any, we should pass to - // `XGetVisualInfo`. winit doesn't recieve any info about what + // `XGetVisualInfo`. winit doesn't receive any info about what // properties the user wants. Users should consider choosing the // visual themselves as glutin does. match pl_attribs.visual_infos { @@ -235,7 +238,7 @@ impl UnownedWindow { ) }; - let window = UnownedWindow { + let mut window = UnownedWindow { xconn: Arc::clone(xconn), xwindow, root, @@ -245,7 +248,7 @@ impl UnownedWindow { cursor_visible: Mutex::new(true), ime_sender: Mutex::new(event_loop.ime_sender.clone()), shared_state: SharedState::new(guessed_monitor, window_attrs.visible), - pending_redraws: event_loop.pending_redraws.clone(), + redraw_sender: event_loop.redraw_sender.clone(), }; // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window @@ -320,10 +323,11 @@ impl UnownedWindow { { let mut min_inner_size = window_attrs .min_inner_size - .map(|size| size.to_physical(dpi_factor)); + .map(|size| size.to_physical::(scale_factor)); let mut max_inner_size = window_attrs .max_inner_size - .map(|size| size.to_physical(dpi_factor)); + .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"); @@ -331,9 +335,11 @@ impl UnownedWindow { max_inner_size = Some(dimensions.into()); min_inner_size = Some(dimensions.into()); - let mut shared_state_lock = window.shared_state.lock(); - shared_state_lock.min_inner_size = window_attrs.min_inner_size; - shared_state_lock.max_inner_size = window_attrs.max_inner_size; + let mut shared_state = window.shared_state.get_mut(); + shared_state.min_inner_size = window_attrs.min_inner_size; + shared_state.max_inner_size = window_attrs.max_inner_size; + shared_state.resize_increments = pl_attribs.resize_increments; + shared_state.base_size = pl_attribs.base_size; } } @@ -341,8 +347,16 @@ impl UnownedWindow { 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(pl_attribs.resize_increments); - normal_hints.set_base_size(pl_attribs.base_size); + normal_hints.set_resize_increments( + pl_attribs + .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(); } @@ -440,16 +454,6 @@ impl UnownedWindow { .map_err(|x_err| os_error!(OsError::XError(x_err))) } - fn logicalize_coords(&self, (x, y): (i32, i32)) -> LogicalPosition { - let dpi = self.hidpi_factor(); - LogicalPosition::from_physical((x, y), dpi) - } - - fn logicalize_size(&self, (width, height): (u32, u32)) -> LogicalSize { - let dpi = self.hidpi_factor(); - LogicalSize::from_physical((width, height), dpi) - } - 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") }; @@ -519,23 +523,6 @@ impl UnownedWindow { ) } - #[inline] - pub fn set_urgent(&self, is_urgent: bool) { - let mut wm_hints = self - .xconn - .get_wm_hints(self.xwindow) - .expect("`XGetWMHints` failed"); - if is_urgent { - (*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"); - } - fn set_netwm( &self, operation: util::StateOperation, @@ -635,6 +622,7 @@ impl UnownedWindow { let flusher = self.set_fullscreen_hint(false); 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(); } Some(flusher) @@ -643,15 +631,17 @@ impl UnownedWindow { let (video_mode, monitor) = match fullscreen { Fullscreen::Exclusive(RootVideoMode { video_mode: PlatformVideoMode::X(ref video_mode), - }) => (Some(video_mode), video_mode.monitor.as_ref().unwrap()), - Fullscreen::Borderless(RootMonitorHandle { - inner: PlatformMonitorHandle::X(ref monitor), - }) => (None, monitor), + }) => (Some(video_mode), video_mode.monitor.clone().unwrap()), + Fullscreen::Borderless(Some(RootMonitorHandle { + inner: PlatformMonitorHandle::X(monitor), + })) => (None, monitor), + Fullscreen::Borderless(None) => (None, self.current_monitor()), + #[cfg(feature = "wayland")] _ => unreachable!(), }; // Don't set fullscreen on an invalid dummy monitor handle - if monitor.id == 0 { + if monitor.is_dummy() { return None; } @@ -749,6 +739,35 @@ impl UnownedWindow { self.xconn.primary_monitor() } + fn set_minimized_inner(&self, minimized: bool) -> util::Flusher<'_> { + unsafe { + if minimized { + let screen = (self.xconn.xlib.XDefaultScreen)(self.xconn.display); + + (self.xconn.xlib.XIconifyWindow)(self.xconn.display, self.xwindow, screen); + + util::Flusher::new(&self.xconn) + } else { + let atom = self.xconn.get_atom_unchecked(b"_NET_ACTIVE_WINDOW\0"); + + self.xconn.send_client_msg( + self.xwindow, + self.root, + atom, + Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), + [1, ffi::CurrentTime as c_long, 0, 0, 0], + ) + } + } + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.set_minimized_inner(minimized) + .flush() + .expect("Failed to change window minimization"); + } + fn set_maximized_inner(&self, maximized: bool) -> util::Flusher<'_> { let horz_atom = unsafe { self.xconn @@ -922,11 +941,11 @@ impl UnownedWindow { } #[inline] - pub fn outer_position(&self) -> Result { + pub fn outer_position(&self) -> Result, NotSupportedError> { let extents = (*self.shared_state.lock()).frame_extents.clone(); if let Some(extents) = extents { - let logical = self.inner_position().unwrap(); - Ok(extents.inner_pos_to_outer_logical(logical, self.hidpi_factor())) + let (x, y) = self.inner_position_physical(); + Ok(extents.inner_pos_to_outer(x, y).into()) } else { self.update_cached_frame_extents(); self.outer_position() @@ -943,8 +962,8 @@ impl UnownedWindow { } #[inline] - pub fn inner_position(&self) -> Result { - Ok(self.logicalize_coords(self.inner_position_physical())) + pub fn inner_position(&self) -> Result, NotSupportedError> { + Ok(self.inner_position_physical().into()) } pub(crate) fn set_position_inner(&self, mut x: i32, mut y: i32) -> util::Flusher<'_> { @@ -973,8 +992,8 @@ impl UnownedWindow { } #[inline] - pub fn set_outer_position(&self, logical_position: LogicalPosition) { - let (x, y) = logical_position.to_physical(self.hidpi_factor()).into(); + pub fn set_outer_position(&self, position: Position) { + let (x, y) = position.to_physical::(self.scale_factor()).into(); self.set_position_physical(x, y); } @@ -988,16 +1007,16 @@ impl UnownedWindow { } #[inline] - pub fn inner_size(&self) -> LogicalSize { - self.logicalize_size(self.inner_size_physical()) + pub fn inner_size(&self) -> PhysicalSize { + self.inner_size_physical().into() } #[inline] - pub fn outer_size(&self) -> LogicalSize { + pub fn outer_size(&self) -> PhysicalSize { let extents = self.shared_state.lock().frame_extents.clone(); if let Some(extents) = extents { - let logical = self.inner_size(); - extents.inner_size_to_outer_logical(logical, self.hidpi_factor()) + let (width, height) = self.inner_size_physical(); + extents.inner_size_to_outer(width, height).into() } else { self.update_cached_frame_extents(); self.outer_size() @@ -1018,9 +1037,9 @@ impl UnownedWindow { } #[inline] - pub fn set_inner_size(&self, logical_size: LogicalSize) { - let dpi_factor = self.hidpi_factor(); - let (width, height) = logical_size.to_physical(dpi_factor).into(); + pub fn set_inner_size(&self, size: Size) { + let scale_factor = self.scale_factor(); + let (width, height) = size.to_physical::(scale_factor).into(); self.set_inner_size_physical(width, height); } @@ -1041,10 +1060,10 @@ impl UnownedWindow { } #[inline] - pub fn set_min_inner_size(&self, logical_dimensions: Option) { - self.shared_state.lock().min_inner_size = logical_dimensions; - let physical_dimensions = logical_dimensions - .map(|logical_dimensions| logical_dimensions.to_physical(self.hidpi_factor()).into()); + pub fn set_min_inner_size(&self, dimensions: Option) { + self.shared_state.lock().min_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); self.set_min_inner_size_physical(physical_dimensions); } @@ -1054,48 +1073,40 @@ impl UnownedWindow { } #[inline] - pub fn set_max_inner_size(&self, logical_dimensions: Option) { - self.shared_state.lock().max_inner_size = logical_dimensions; - let physical_dimensions = logical_dimensions - .map(|logical_dimensions| logical_dimensions.to_physical(self.hidpi_factor()).into()); + pub fn set_max_inner_size(&self, dimensions: Option) { + self.shared_state.lock().max_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); self.set_max_inner_size_physical(physical_dimensions); } pub(crate) fn adjust_for_dpi( &self, - old_dpi_factor: f64, - new_dpi_factor: f64, - width: f64, - height: f64, - ) -> (f64, f64, util::Flusher<'_>) { - let scale_factor = new_dpi_factor / old_dpi_factor; - let new_width = width * scale_factor; - let new_height = height * scale_factor; + old_scale_factor: f64, + new_scale_factor: f64, + width: u32, + height: u32, + shared_state: &SharedState, + ) -> (u32, u32) { + let scale_factor = new_scale_factor / old_scale_factor; self.update_normal_hints(|normal_hints| { - let dpi_adjuster = |(width, height): (u32, u32)| -> (u32, u32) { - let new_width = width as f64 * scale_factor; - let new_height = height as f64 * scale_factor; - (new_width.round() as u32, new_height.round() as u32) - }; - let max_size = normal_hints.get_max_size().map(&dpi_adjuster); - let min_size = normal_hints.get_min_size().map(&dpi_adjuster); - let resize_increments = normal_hints.get_resize_increments().map(&dpi_adjuster); - let base_size = normal_hints.get_base_size().map(&dpi_adjuster); + let dpi_adjuster = + |size: Size| -> (u32, u32) { size.to_physical::(new_scale_factor).into() }; + 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); }) .expect("Failed to update normal hints"); - unsafe { - (self.xconn.xlib.XResizeWindow)( - self.xconn.display, - self.xwindow, - new_width.round() as c_uint, - new_height.round() as c_uint, - ); - } - (new_width, new_height, util::Flusher::new(&self.xconn)) + + let new_width = (width as f64 * scale_factor).round() as u32; + let new_height = (height as f64 * scale_factor).round() as u32; + + (new_width, new_height) } pub fn set_resizable(&self, resizable: bool) { @@ -1107,25 +1118,25 @@ impl UnownedWindow { return; } - let (logical_min, logical_max) = if resizable { + let (min_size, max_size) = if resizable { let shared_state_lock = self.shared_state.lock(); ( shared_state_lock.min_inner_size, shared_state_lock.max_inner_size, ) } else { - let window_size = Some(self.inner_size()); + let window_size = Some(Size::from(self.inner_size())); (window_size.clone(), window_size) }; self.set_maximizable_inner(resizable).queue(); - let dpi_factor = self.hidpi_factor(); - let min_inner_size = logical_min - .map(|logical_size| logical_size.to_physical(dpi_factor)) + 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 = logical_max - .map(|logical_size| logical_size.to_physical(dpi_factor)) + let max_inner_size = max_size + .map(|size| size.to_physical::(scale_factor)) .map(Into::into); self.update_normal_hints(|normal_hints| { normal_hints.set_min_size(min_inner_size); @@ -1246,8 +1257,8 @@ impl UnownedWindow { } #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.current_monitor().hidpi_factor + pub fn scale_factor(&self) -> f64 { + self.current_monitor().scale_factor } pub fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { @@ -1260,11 +1271,8 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_position( - &self, - logical_position: LogicalPosition, - ) -> Result<(), ExternalError> { - let (x, y) = logical_position.to_physical(self.hidpi_factor()).into(); + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let (x, y) = position.to_physical::(self.scale_factor()).into(); self.set_cursor_position_physical(x, y) } @@ -1276,11 +1284,28 @@ impl UnownedWindow { } #[inline] - pub fn set_ime_position(&self, logical_spot: LogicalPosition) { - let (x, y) = logical_spot.to_physical(self.hidpi_factor()).into(); + pub fn set_ime_position(&self, spot: Position) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); self.set_ime_position_physical(x, y); } + #[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"); + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.xwindow) @@ -1288,10 +1313,7 @@ impl UnownedWindow { #[inline] pub fn request_redraw(&self) { - self.pending_redraws - .lock() - .unwrap() - .insert(WindowId(self.xwindow)); + self.redraw_sender.send(WindowId(self.xwindow)).unwrap(); } #[inline] diff --git a/src/platform_impl/linux/x11/xdisplay.rs b/src/platform_impl/linux/x11/xdisplay.rs index 176323ec9c..25065f045d 100644 --- a/src/platform_impl/linux/x11/xdisplay.rs +++ b/src/platform_impl/linux/x11/xdisplay.rs @@ -111,12 +111,7 @@ pub struct XError { pub minor_code: u8, } -impl Error for XError { - #[inline] - fn description(&self) -> &str { - &self.description - } -} +impl Error for XError {} impl fmt::Display for XError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { @@ -144,17 +139,18 @@ impl From for XNotSupported { } } -impl Error for XNotSupported { - #[inline] - fn description(&self) -> &str { - match *self { +impl XNotSupported { + fn description(&self) -> &'static str { + match self { XNotSupported::LibraryOpenError(_) => "Failed to load one of xlib's shared libraries", XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server", } } +} +impl Error for XNotSupported { #[inline] - fn cause(&self) -> Option<&dyn Error> { + fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { XNotSupported::LibraryOpenError(ref err) => Some(err), _ => None, diff --git a/src/platform_impl/macos/activation_hack.rs b/src/platform_impl/macos/activation_hack.rs new file mode 100644 index 0000000000..6cf1960cfc --- /dev/null +++ b/src/platform_impl/macos/activation_hack.rs @@ -0,0 +1,208 @@ +// Normally when you run or distribute a macOS app, it's bundled: it's in one +// of those fun little folders that you have to right click "Show Package +// Contents" on, and usually contains myriad delights including, but not +// limited to, plists, icons, and of course, your beloved executable. However, +// when you use `cargo run`, your app is unbundled - it's just a lonely, bare +// executable. +// +// Apple isn't especially fond of unbundled apps, which is to say, they seem to +// barely be supported. If you move the mouse while opening a winit window from +// an unbundled app, the window will fail to activate and be in a grayed-out +// uninteractable state. Switching to another app and back is the only way to +// get the winit window into a normal state. None of this happens if the app is +// bundled, i.e. when running via Xcode. +// +// To workaround this, we just switch focus to the Dock and then switch back to +// our app. We only do this for unbundled apps, and only when they fail to +// become active on their own. +// +// This solution was derived from this Godot PR: +// https://github.com/godotengine/godot/pull/17187 +// (which appears to be based on https://stackoverflow.com/a/7602677) +// The curious specialness of mouse motions is touched upon here: +// https://github.com/godotengine/godot/issues/8653#issuecomment-358130512 +// +// We omit the 2nd step of the solution used in Godot, since it appears to have +// no effect - I speculate that it's just technical debt picked up from the SO +// answer; the API used is fairly exotic, and was historically used for very +// old versions of macOS that didn't support `activateIgnoringOtherApps`, i.e. +// in previous versions of SDL: +// https://hg.libsdl.org/SDL/file/c0bcc39a3491/src/video/cocoa/SDL_cocoaevents.m#l322 +// +// The `performSelector` delays in the Godot solution are used for sequencing, +// since refocusing the app will fail if the call is made before it finishes +// unfocusing. The delays used there are much smaller than the ones in the +// original SO answer, presumably because they found the fastest delay that +// works reliably through trial and error. Instead of using delays, we just +// handle `applicationDidResignActive`; despite the app not activating reliably, +// that still triggers when we switch focus to the Dock. +// +// The Godot solution doesn't appear to skip the hack when an unbundled app +// activates normally. Checking for this is difficult, since if you call +// `isActive` too early, it will always be `NO`. Even though we receive +// `applicationDidResignActive` when switching focus to the Dock, we never +// receive a preceding `applicationDidBecomeActive` if the app fails to +// activate normally. I wasn't able to find a proper point in time to perform +// the `isActive` check, so we instead check for the cause of the quirk: if +// any mouse motion occurs prior to us receiving `applicationDidResignActive`, +// we assume the app failed to become active. +// +// Fun fact: this issue is still present in GLFW +// (https://github.com/glfw/glfw/issues/1515) +// +// A similar issue was found in SDL, but the resolution doesn't seem to work +// for us: https://bugzilla.libsdl.org/show_bug.cgi?id=3051 + +use super::util; +use cocoa::{ + appkit::{NSApp, NSApplicationActivateIgnoringOtherApps}, + base::id, + foundation::NSUInteger, +}; +use objc::runtime::{Object, Sel, BOOL, NO, YES}; +use std::{ + os::raw::c_void, + sync::atomic::{AtomicBool, Ordering}, +}; + +#[derive(Debug, Default)] +pub struct State { + // Indicates that the hack has either completed or been skipped. + activated: AtomicBool, + // Indicates that the mouse has moved at some point in time. + mouse_moved: AtomicBool, + // Indicates that the hack is in progress, and that we should refocus when + // the app resigns active. + needs_refocus: AtomicBool, +} + +impl State { + pub fn name() -> &'static str { + "activationHackState" + } + + pub fn new() -> *mut c_void { + let this = Box::new(Self::default()); + Box::into_raw(this) as *mut c_void + } + + pub unsafe fn free(this: *mut Self) { + Box::from_raw(this); + } + + pub unsafe fn get_ptr(obj: &Object) -> *mut Self { + let this: *mut c_void = *(*obj).get_ivar(Self::name()); + assert!(!this.is_null(), "`activationHackState` pointer was null"); + this as *mut Self + } + + pub unsafe fn set_activated(obj: &Object, value: bool) { + let this = Self::get_ptr(obj); + (*this).activated.store(value, Ordering::Release); + } + + unsafe fn get_activated(obj: &Object) -> bool { + let this = Self::get_ptr(obj); + (*this).activated.load(Ordering::Acquire) + } + + pub unsafe fn set_mouse_moved(obj: &Object, value: bool) { + let this = Self::get_ptr(obj); + (*this).mouse_moved.store(value, Ordering::Release); + } + + pub unsafe fn get_mouse_moved(obj: &Object) -> bool { + let this = Self::get_ptr(obj); + (*this).mouse_moved.load(Ordering::Acquire) + } + + pub unsafe fn set_needs_refocus(obj: &Object, value: bool) { + let this = Self::get_ptr(obj); + (*this).needs_refocus.store(value, Ordering::Release); + } + + unsafe fn get_needs_refocus(obj: &Object) -> bool { + let this = Self::get_ptr(obj); + (*this).needs_refocus.load(Ordering::Acquire) + } +} + +// This is the entry point for the hack - if the app is unbundled and a mouse +// movement occurs before the app activates, it will trigger the hack. Because +// mouse movements prior to activation are the cause of this quirk, they should +// be a reliable way to determine if the hack needs to be performed. +pub extern "C" fn mouse_moved(this: &Object, _: Sel, _: id) { + trace!("Triggered `activationHackMouseMoved`"); + unsafe { + if !State::get_activated(this) { + // We check if `CFBundleName` is undefined to determine if the + // app is unbundled. + if let None = util::app_name() { + info!("App detected as unbundled"); + unfocus(this); + } else { + info!("App detected as bundled"); + } + } + } + trace!("Completed `activationHackMouseMoved`"); +} + +// Switch focus to the dock. +unsafe fn unfocus(this: &Object) { + // We only perform the hack if the app failed to activate, since otherwise, + // there'd be a gross (but fast) flicker as it unfocused and then refocused. + // However, we only enter this function if we detect mouse movement prior + // to activation, so this should always be `NO`. + // + // Note that this check isn't necessarily reliable in detecting a violation + // of the invariant above, since it's not guaranteed that activation will + // resolve before this point. In other words, it can spuriously return `NO`. + // This is also why the mouse motion approach was chosen, since it's not + // obvious how to sequence this check - if someone knows how to, then that + // would almost surely be a cleaner approach. + let active: BOOL = msg_send![NSApp(), isActive]; + if active == YES { + error!("Unbundled app activation hack triggered on an app that's already active; this shouldn't happen!"); + } else { + info!("Performing unbundled app activation hack"); + let dock_bundle_id = util::ns_string_id_ref("com.apple.dock"); + let dock_array: id = msg_send![ + class!(NSRunningApplication), + runningApplicationsWithBundleIdentifier: *dock_bundle_id + ]; + let dock_array_len: NSUInteger = msg_send![dock_array, count]; + if dock_array_len == 0 { + error!("The Dock doesn't seem to be running, so switching focus to it is impossible"); + } else { + State::set_needs_refocus(this, true); + let dock: id = msg_send![dock_array, objectAtIndex: 0]; + // This will trigger `applicationDidResignActive`, which will in + // turn call `refocus`. + let status: BOOL = msg_send![ + dock, + activateWithOptions: NSApplicationActivateIgnoringOtherApps + ]; + if status == NO { + error!("Failed to switch focus to Dock"); + } + } + } +} + +// Switch focus back to our app, causing the user to rejoice! +pub unsafe fn refocus(this: &Object) { + if State::get_needs_refocus(this) { + State::set_needs_refocus(this, false); + let app: id = msg_send![class!(NSRunningApplication), currentApplication]; + // Simply calling `NSApp activateIgnoringOtherApps` doesn't work. The + // nuanced difference isn't clear to me, but hey, I tried. + let success: BOOL = msg_send![ + app, + activateWithOptions: NSApplicationActivateIgnoringOtherApps + ]; + if success == NO { + error!("Failed to refocus app"); + } + } +} diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index 3278cb13a8..4cec95124c 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -2,17 +2,15 @@ use std::collections::VecDeque; use cocoa::{ appkit::{self, NSEvent}, - base::id, + base::{id, nil}, }; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; -use crate::{ - event::{DeviceEvent, ElementState, Event}, - platform_impl::platform::{app_state::AppState, util, DEVICE_ID}, -}; +use super::{activation_hack, app_state::AppState, event::EventWrapper, util, DEVICE_ID}; +use crate::event::{DeviceEvent, ElementState, Event}; pub struct AppClass(pub *const Class); unsafe impl Send for AppClass {} @@ -51,14 +49,14 @@ extern "C" fn send_event(this: &Object, _sel: Sel, event: id) { let key_window: id = msg_send![this, keyWindow]; let _: () = msg_send![key_window, sendEvent: event]; } else { - maybe_dispatch_device_event(event); + maybe_dispatch_device_event(this, event); let superclass = util::superclass(this); let _: () = msg_send![super(this, superclass), sendEvent: event]; } } } -unsafe fn maybe_dispatch_device_event(event: id) { +unsafe fn maybe_dispatch_device_event(this: &Object, event: id) { let event_type = event.eventType(); match event_type { appkit::NSMouseMoved @@ -71,59 +69,74 @@ unsafe fn maybe_dispatch_device_event(event: id) { let delta_y = event.deltaY() as f64; if delta_x != 0.0 { - events.push_back(Event::DeviceEvent { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { device_id: DEVICE_ID, event: DeviceEvent::Motion { axis: 0, value: delta_x, }, - }); + })); } if delta_y != 0.0 { - events.push_back(Event::DeviceEvent { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { device_id: DEVICE_ID, event: DeviceEvent::Motion { axis: 1, value: delta_y, }, - }); + })); } if delta_x != 0.0 || delta_y != 0.0 { - events.push_back(Event::DeviceEvent { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { device_id: DEVICE_ID, event: DeviceEvent::MouseMotion { delta: (delta_x, delta_y), }, - }); + })); } AppState::queue_events(events); + + // Notify the delegate when the first mouse move occurs. This is + // used for the unbundled app activation hack, which needs to know + // if any mouse motions occurred prior to the app activating. + let delegate: id = msg_send![this, delegate]; + assert_ne!(delegate, nil); + if !activation_hack::State::get_mouse_moved(&*delegate) { + activation_hack::State::set_mouse_moved(&*delegate, true); + let () = msg_send![ + delegate, + performSelector: sel!(activationHackMouseMoved:) + withObject: nil + afterDelay: 0.0 + ]; + } } appkit::NSLeftMouseDown | appkit::NSRightMouseDown | appkit::NSOtherMouseDown => { let mut events = VecDeque::with_capacity(1); - events.push_back(Event::DeviceEvent { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { device_id: DEVICE_ID, event: DeviceEvent::Button { button: event.buttonNumber() as u32, state: ElementState::Pressed, }, - }); + })); AppState::queue_events(events); } appkit::NSLeftMouseUp | appkit::NSRightMouseUp | appkit::NSOtherMouseUp => { let mut events = VecDeque::with_capacity(1); - events.push_back(Event::DeviceEvent { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { device_id: DEVICE_ID, event: DeviceEvent::Button { button: event.buttonNumber() as u32, state: ElementState::Released, }, - }); + })); AppState::queue_events(events); } diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index c0c3a121d7..9263cc121f 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,10 +1,10 @@ +use super::{activation_hack, app_state::AppState}; use cocoa::base::id; use objc::{ declare::ClassDecl, - runtime::{Class, Object, Sel, BOOL, YES}, + runtime::{Class, Object, Sel}, }; - -use crate::platform_impl::platform::app_state::AppState; +use std::os::raw::c_void; pub struct AppDelegateClass(pub *const Class); unsafe impl Send for AppDelegateClass {} @@ -15,90 +15,67 @@ lazy_static! { let superclass = class!(NSResponder); let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); + decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); decl.add_method( sel!(applicationDidFinishLaunching:), - did_finish_launching as extern "C" fn(&Object, Sel, id) -> BOOL, + did_finish_launching as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(applicationDidBecomeActive:), did_become_active as extern "C" fn(&Object, Sel, id), ); decl.add_method( - sel!(applicationWillResignActive:), - will_resign_active as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(applicationWillEnterForeground:), - will_enter_foreground as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(applicationDidEnterBackground:), - did_enter_background as extern "C" fn(&Object, Sel, id), + sel!(applicationDidResignActive:), + did_resign_active as extern "C" fn(&Object, Sel, id), ); + + decl.add_ivar::<*mut c_void>(activation_hack::State::name()); decl.add_method( - sel!(applicationWillTerminate:), - will_terminate as extern "C" fn(&Object, Sel, id), + sel!(activationHackMouseMoved:), + activation_hack::mouse_moved as extern "C" fn(&Object, Sel, id), ); AppDelegateClass(decl.register()) }; } -extern "C" fn did_finish_launching(_: &Object, _: Sel, _: id) -> BOOL { - trace!("Triggered `didFinishLaunching`"); - AppState::launched(); - trace!("Completed `didFinishLaunching`"); - YES -} - -extern "C" fn did_become_active(_: &Object, _: Sel, _: id) { - trace!("Triggered `didBecomeActive`"); - /*unsafe { - HANDLER.lock().unwrap().handle_nonuser_event(Event::Resumed) - }*/ - trace!("Completed `didBecomeActive`"); +extern "C" fn new(class: &Class, _: Sel) -> id { + unsafe { + let this: id = msg_send![class, alloc]; + let this: id = msg_send![this, init]; + (*this).set_ivar( + activation_hack::State::name(), + activation_hack::State::new(), + ); + this + } } -extern "C" fn will_resign_active(_: &Object, _: Sel, _: id) { - trace!("Triggered `willResignActive`"); - /*unsafe { - HANDLER.lock().unwrap().handle_nonuser_event(Event::Suspended) - }*/ - trace!("Completed `willResignActive`"); +extern "C" fn dealloc(this: &Object, _: Sel) { + unsafe { + activation_hack::State::free(activation_hack::State::get_ptr(this)); + } } -extern "C" fn will_enter_foreground(_: &Object, _: Sel, _: id) { - trace!("Triggered `willEnterForeground`"); - trace!("Completed `willEnterForeground`"); +extern "C" fn did_finish_launching(_: &Object, _: Sel, _: id) { + trace!("Triggered `applicationDidFinishLaunching`"); + AppState::launched(); + trace!("Completed `applicationDidFinishLaunching`"); } -extern "C" fn did_enter_background(_: &Object, _: Sel, _: id) { - trace!("Triggered `didEnterBackground`"); - trace!("Completed `didEnterBackground`"); +extern "C" fn did_become_active(this: &Object, _: Sel, _: id) { + trace!("Triggered `applicationDidBecomeActive`"); + unsafe { + activation_hack::State::set_activated(this, true); + } + trace!("Completed `applicationDidBecomeActive`"); } -extern "C" fn will_terminate(_: &Object, _: Sel, _: id) { - trace!("Triggered `willTerminate`"); - /*unsafe { - let app: id = msg_send![class!(UIApplication), sharedApplication]; - let windows: id = msg_send![app, windows]; - let windows_enum: id = msg_send![windows, objectEnumerator]; - let mut events = Vec::new(); - loop { - let window: id = msg_send![windows_enum, nextObject]; - if window == nil { - break - } - let is_winit_window: BOOL = msg_send![window, isKindOfClass:class!(WinitUIWindow)]; - if is_winit_window == YES { - events.push(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::Destroyed, - }); - } - } - HANDLER.lock().unwrap().handle_nonuser_events(events); - HANDLER.lock().unwrap().terminated(); - }*/ - trace!("Completed `willTerminate`"); +extern "C" fn did_resign_active(this: &Object, _: Sel, _: id) { + trace!("Triggered `applicationDidResignActive`"); + unsafe { + activation_hack::refocus(this); + } + trace!("Completed `applicationDidResignActive`"); } diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index bc585a52a5..29fafbe5fa 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -11,12 +11,24 @@ use std::{ time::Instant, }; -use cocoa::{appkit::NSApp, base::nil}; +use cocoa::{ + appkit::{NSApp, NSEventType::NSApplicationDefined, NSWindow}, + base::{id, nil}, + foundation::{NSAutoreleasePool, NSPoint, NSSize}, +}; + +use objc::runtime::YES; use crate::{ + dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, - platform_impl::platform::{observer::EventLoopWaker, util::Never}, + platform_impl::platform::{ + event::{EventProxy, EventWrapper}, + observer::EventLoopWaker, + util::{IdRef, Never}, + window::get_window_id, + }, window::WindowId, }; @@ -24,8 +36,8 @@ lazy_static! { static ref HANDLER: Handler = Default::default(); } -impl Event { - fn userify(self) -> Event { +impl<'a, Never> Event<'a, Never> { + fn userify(self) -> Event<'a, T> { self.map_nonuser_event() // `Never` can't be constructed, so the `UserEvent` variant can't // be present here. @@ -34,12 +46,13 @@ impl Event { } pub trait EventHandler: Debug { - fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow); + // 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); } struct EventLoopHandler { - callback: Box, &RootWindowTarget, &mut ControlFlow)>, + callback: Box, &RootWindowTarget, &mut ControlFlow)>, will_exit: bool, window_target: Rc>, } @@ -54,7 +67,7 @@ impl Debug for EventLoopHandler { } impl EventHandler for EventLoopHandler { - fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow) { + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { (self.callback)(event.userify(), &self.window_target, control_flow); self.will_exit |= *control_flow == ControlFlow::Exit; if self.will_exit { @@ -79,11 +92,12 @@ impl EventHandler for EventLoopHandler { struct Handler { ready: AtomicBool, in_callback: AtomicBool, + dialog_is_closing: AtomicBool, control_flow: Mutex, control_flow_prev: Mutex, start_time: Mutex>, callback: Mutex>>, - pending_events: Mutex>>, + pending_events: Mutex>, pending_redraw: Mutex>, waker: Mutex, } @@ -92,7 +106,7 @@ unsafe impl Send for Handler {} unsafe impl Sync for Handler {} impl Handler { - fn events<'a>(&'a self) -> MutexGuard<'a, VecDeque>> { + fn events(&self) -> MutexGuard<'_, VecDeque> { self.pending_events.lock().unwrap() } @@ -100,7 +114,7 @@ impl Handler { self.pending_redraw.lock().unwrap() } - fn waker<'a>(&'a self) -> MutexGuard<'a, EventLoopWaker> { + fn waker(&self) -> MutexGuard<'_, EventLoopWaker> { self.waker.lock().unwrap() } @@ -136,7 +150,7 @@ impl Handler { *self.start_time.lock().unwrap() = Some(Instant::now()); } - fn take_events(&self) -> VecDeque> { + fn take_events(&self) -> VecDeque { mem::replace(&mut *self.events(), Default::default()) } @@ -152,9 +166,14 @@ impl Handler { self.in_callback.store(in_callback, Ordering::Release); } - fn handle_nonuser_event(&self, event: Event) { + fn handle_nonuser_event(&self, wrapper: EventWrapper) { if let Some(ref mut callback) = *self.callback.lock().unwrap() { - callback.handle_nonuser_event(event, &mut *self.control_flow.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), + } } } @@ -163,15 +182,57 @@ impl Handler { callback.handle_user_events(&mut *self.control_flow.lock().unwrap()); } } + + fn handle_scale_factor_changed_event( + &self, + callback: &mut Box, + ns_window: IdRef, + suggested_size: LogicalSize, + 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(get_window_id(*ns_window)), + 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); + unsafe { NSWindow::setContentSize_(*ns_window, size) }; + } + + fn handle_proxy(&self, proxy: EventProxy, callback: &mut Box) { + match proxy { + EventProxy::DpiChangedProxy { + ns_window, + suggested_size, + scale_factor, + } => self.handle_scale_factor_changed_event( + callback, + ns_window, + suggested_size, + scale_factor, + ), + } + } } +pub static INTERRUPT_EVENT_LOOP_EXIT: AtomicBool = AtomicBool::new(false); + pub enum AppState {} impl AppState { // This function extends lifetime of `callback` to 'static as its side effect pub unsafe fn set_callback(callback: F, window_target: Rc>) where - F: FnMut(Event, &RootWindowTarget, &mut ControlFlow), + F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { // This transmute is always safe, in case it was reached through `run`, since our @@ -179,8 +240,8 @@ impl AppState { // 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. callback: mem::transmute::< - Box, &RootWindowTarget, &mut ControlFlow)>, - Box, &RootWindowTarget, &mut ControlFlow)>, + Box, &RootWindowTarget, &mut ControlFlow)>, + Box, &RootWindowTarget, &mut ControlFlow)>, >(Box::new(callback)), will_exit: false, window_target, @@ -189,7 +250,7 @@ impl AppState { pub fn exit() { HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::LoopDestroyed); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::LoopDestroyed)); HANDLER.set_in_callback(false); HANDLER.callback.lock().unwrap().take(); } @@ -198,7 +259,9 @@ impl AppState { HANDLER.set_ready(); HANDLER.waker().start(); HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::NewEvents(StartCause::Init)); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents( + StartCause::Init, + ))); HANDLER.set_in_callback(false); } @@ -229,7 +292,7 @@ impl AppState { ControlFlow::Exit => StartCause::Poll, //panic!("unexpected `ControlFlow::Exit`"), }; HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::NewEvents(cause)); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(cause))); HANDLER.set_in_callback(false); } @@ -241,18 +304,18 @@ impl AppState { } } - pub fn queue_event(event: Event) { + pub fn queue_event(wrapper: EventWrapper) { if !unsafe { msg_send![class!(NSThread), isMainThread] } { - panic!("Event queued from different thread: {:#?}", event); + panic!("Event queued from different thread: {:#?}", wrapper); } - HANDLER.events().push_back(event); + HANDLER.events().push_back(wrapper); } - pub fn queue_events(mut events: VecDeque>) { + pub fn queue_events(mut wrappers: VecDeque) { if !unsafe { msg_send![class!(NSThread), isMainThread] } { - panic!("Events queued from different thread: {:#?}", events); + panic!("Events queued from different thread: {:#?}", wrappers); } - HANDLER.events().append(&mut events); + HANDLER.events().append(&mut wrappers); } pub fn cleared() { @@ -265,18 +328,63 @@ impl AppState { for event in HANDLER.take_events() { HANDLER.handle_nonuser_event(event); } + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); for window_id in HANDLER.should_redraw() { - HANDLER.handle_nonuser_event(Event::WindowEvent { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested( window_id, - event: WindowEvent::RedrawRequested, - }); + ))); } - HANDLER.handle_nonuser_event(Event::EventsCleared); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); HANDLER.set_in_callback(false); } if HANDLER.should_exit() { - let _: () = unsafe { msg_send![NSApp(), terminate: nil] }; - return; + unsafe { + let app: id = NSApp(); + let windows: id = msg_send![app, windows]; + let window: id = msg_send![windows, objectAtIndex:0]; + let window_count: usize = msg_send![windows, count]; + assert_ne!(window, nil); + + let dialog_open = if window_count > 1 { + let dialog: id = msg_send![windows, lastObject]; + let is_main_window: bool = msg_send![dialog, isMainWindow]; + msg_send![dialog, isVisible] && !is_main_window + } else { + false + }; + + let dialog_is_closing = HANDLER.dialog_is_closing.load(Ordering::SeqCst); + let pool = NSAutoreleasePool::new(nil); + if !INTERRUPT_EVENT_LOOP_EXIT.load(Ordering::SeqCst) + && !dialog_open + && !dialog_is_closing + { + let _: () = msg_send![app, stop: nil]; + + let dummy_event: id = msg_send![class!(NSEvent), + otherEventWithType: NSApplicationDefined + location: NSPoint::new(0.0, 0.0) + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: 0 + data1: 0 + data2: 0 + ]; + // To stop event loop immediately, we need to post some event here. + let _: () = msg_send![window, postEvent: dummy_event atStart: YES]; + } + pool.drain(); + + let window_has_focus = msg_send![window, isKeyWindow]; + if !dialog_open && window_has_focus && dialog_is_closing { + HANDLER.dialog_is_closing.store(false, Ordering::SeqCst); + } + if dialog_open { + HANDLER.dialog_is_closing.store(true, Ordering::SeqCst); + } + }; } HANDLER.update_start_time(); match HANDLER.get_old_and_new_control_flow() { diff --git a/src/platform_impl/macos/event.rs b/src/platform_impl/macos/event.rs index ebad248213..343aaeb286 100644 --- a/src/platform_impl/macos/event.rs +++ b/src/platform_impl/macos/event.rs @@ -6,10 +6,29 @@ use cocoa::{ }; use crate::{ - event::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent}, - platform_impl::platform::DEVICE_ID, + dpi::LogicalSize, + event::{ElementState, Event, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent}, + platform_impl::platform::{ + util::{IdRef, Never}, + DEVICE_ID, + }, }; +#[derive(Debug)] +pub enum EventWrapper { + StaticEvent(Event<'static, Never>), + EventProxy(EventProxy), +} + +#[derive(Debug, PartialEq)] +pub enum EventProxy { + DpiChangedProxy { + ns_window: IdRef, + suggested_size: LogicalSize, + scale_factor: f64, + }, +} + pub fn char_to_keycode(c: char) -> Option { // We only translate keys that are affected by keyboard layout. // @@ -137,20 +156,20 @@ pub fn scancode_to_keycode(scancode: c_ushort) -> Option { 0x3e => VirtualKeyCode::RControl, //0x3f => Fn key, 0x40 => VirtualKeyCode::F17, - 0x41 => VirtualKeyCode::Decimal, + 0x41 => VirtualKeyCode::NumpadDecimal, //0x42 -> unkown, - 0x43 => VirtualKeyCode::Multiply, + 0x43 => VirtualKeyCode::NumpadMultiply, //0x44 => unkown, - 0x45 => VirtualKeyCode::Add, + 0x45 => VirtualKeyCode::NumpadAdd, //0x46 => unkown, 0x47 => VirtualKeyCode::Numlock, //0x48 => KeypadClear, 0x49 => VirtualKeyCode::VolumeUp, 0x4a => VirtualKeyCode::VolumeDown, - 0x4b => VirtualKeyCode::Divide, + 0x4b => VirtualKeyCode::NumpadDivide, 0x4c => VirtualKeyCode::NumpadEnter, //0x4d => unkown, - 0x4e => VirtualKeyCode::Subtract, + 0x4e => VirtualKeyCode::NumpadSubtract, 0x4f => VirtualKeyCode::F18, 0x50 => VirtualKeyCode::F19, 0x51 => VirtualKeyCode::NumpadEquals, @@ -224,12 +243,24 @@ pub fn check_function_keys(string: &str) -> Option { pub fn event_mods(event: id) -> ModifiersState { let flags = unsafe { NSEvent::modifierFlags(event) }; - ModifiersState { - shift: flags.contains(NSEventModifierFlags::NSShiftKeyMask), - ctrl: flags.contains(NSEventModifierFlags::NSControlKeyMask), - alt: flags.contains(NSEventModifierFlags::NSAlternateKeyMask), - logo: flags.contains(NSEventModifierFlags::NSCommandKeyMask), - } + let mut m = ModifiersState::empty(); + m.set( + ModifiersState::SHIFT, + flags.contains(NSEventModifierFlags::NSShiftKeyMask), + ); + m.set( + ModifiersState::CTRL, + flags.contains(NSEventModifierFlags::NSControlKeyMask), + ); + m.set( + ModifiersState::ALT, + flags.contains(NSEventModifierFlags::NSAlternateKeyMask), + ); + m.set( + ModifiersState::LOGO, + flags.contains(NSEventModifierFlags::NSCommandKeyMask), + ); + m } pub fn get_scancode(event: cocoa::base::id) -> c_ushort { @@ -244,7 +275,7 @@ pub unsafe fn modifier_event( ns_event: id, keymask: NSEventModifierFlags, was_key_pressed: bool, -) -> Option { +) -> Option> { if !was_key_pressed && NSEvent::modifierFlags(ns_event).contains(keymask) || was_key_pressed && !NSEvent::modifierFlags(ns_event).contains(keymask) { @@ -256,6 +287,7 @@ pub unsafe fn modifier_event( let scancode = get_scancode(ns_event); let virtual_keycode = scancode_to_keycode(scancode); + #[allow(deprecated)] Some(WindowEvent::KeyboardInput { device_id: DEVICE_ID, input: KeyboardInput { @@ -264,6 +296,7 @@ pub unsafe fn modifier_event( virtual_keycode, modifiers: event_mods(ns_event), }, + is_synthetic: false, }) } else { None diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index d6d89092a8..5b3d96b068 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -12,6 +12,7 @@ use cocoa::{ use crate::{ event::Event, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, + monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ app::APP_CLASS, app_delegate::APP_DELEGATE_CLASS, @@ -34,6 +35,19 @@ impl Default for EventLoopWindowTarget { } } +impl EventLoopWindowTarget { + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } +} + pub struct EventLoop { window_target: Rc>, _delegate: IdRef, @@ -68,23 +82,13 @@ impl EventLoop { } } - #[inline] - pub fn available_monitors(&self) -> VecDeque { - monitor::available_monitors() - } - - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - monitor::primary_monitor() - } - pub fn window_target(&self) -> &RootWindowTarget { &self.window_target } pub fn run(mut self, callback: F) -> ! where - F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { self.run_return(callback); process::exit(0); @@ -92,15 +96,16 @@ impl EventLoop { pub fn run_return(&mut self, callback: F) where - F: FnMut(Event, &RootWindowTarget, &mut ControlFlow), + F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { unsafe { - let _pool = NSAutoreleasePool::new(nil); + let pool = NSAutoreleasePool::new(nil); let app = NSApp(); assert_ne!(app, nil); AppState::set_callback(callback, Rc::clone(&self.window_target)); let _: () = msg_send![app, run]; AppState::exit(); + pool.drain(); } } @@ -116,6 +121,14 @@ pub struct Proxy { unsafe impl Send for Proxy {} +impl Drop for Proxy { + fn drop(&mut self) { + unsafe { + CFRelease(self.source as _); + } + } +} + impl Clone for Proxy { fn clone(&self) -> Self { Proxy::new(self.sender.clone()) @@ -142,8 +155,10 @@ impl Proxy { } } - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.sender.send(event).map_err(|_| EventLoopClosed)?; + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender + .send(event) + .map_err(|mpsc::SendError(x)| EventLoopClosed(x))?; unsafe { // let the main thread know there's a new event CFRunLoopSourceSignal(self.source); diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index aec0fc974e..1bcdafbaa4 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -161,6 +161,18 @@ pub const IO8BitOverlayPixels: &str = "O8"; pub type CGWindowLevel = i32; pub type CGDisplayModeRef = *mut libc::c_void; +#[cfg_attr( + not(use_colorsync_cgdisplaycreateuuidfromdisplayid), + link(name = "CoreGraphics", kind = "framework") +)] +#[cfg_attr( + use_colorsync_cgdisplaycreateuuidfromdisplayid, + link(name = "ColorSync", kind = "framework") +)] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + #[link(name = "CoreGraphics", kind = "framework")] extern "C" { pub fn CGRestorePermanentDisplayConfiguration(); @@ -189,7 +201,6 @@ extern "C" { synchronous: Boolean, ) -> CGError; pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError; - pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; pub fn CGShieldingWindowLevel() -> CGWindowLevel; pub fn CGDisplaySetDisplayMode( display: CGDirectDisplayID, diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index c26385da89..72b8e0a332 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,5 +1,6 @@ #![cfg(target_os = "macos")] +mod activation_hack; mod app; mod app_delegate; mod app_state; @@ -24,6 +25,8 @@ use crate::{ error::OsError as RootOsError, event::DeviceId as RootDeviceId, window::WindowAttributes, }; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId; diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index 976f6c9048..817d38ee57 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -1,15 +1,14 @@ use std::{collections::VecDeque, fmt}; -use super::ffi; +use super::{ffi, util}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - platform_impl::platform::util::IdRef, }; use cocoa::{ appkit::NSScreen, base::{id, nil}, - foundation::{NSString, NSUInteger}, + foundation::NSUInteger, }; use core_foundation::{ array::{CFArrayGetCount, CFArrayGetValueAtIndex}, @@ -84,7 +83,7 @@ impl Clone for NativeDisplayMode { } impl VideoMode { - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.size.into() } @@ -166,9 +165,9 @@ impl fmt::Debug for MonitorHandle { struct MonitorHandle { name: Option, native_identifier: u32, - size: PhysicalSize, - position: PhysicalPosition, - hidpi_factor: f64, + size: PhysicalSize, + position: PhysicalPosition, + scale_factor: f64, } let monitor_id_proxy = MonitorHandle { @@ -176,7 +175,7 @@ impl fmt::Debug for MonitorHandle { native_identifier: self.native_identifier(), size: self.size(), position: self.position(), - hidpi_factor: self.hidpi_factor(), + scale_factor: self.scale_factor(), }; monitor_id_proxy.fmt(f) @@ -199,24 +198,24 @@ impl MonitorHandle { self.0 } - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { let MonitorHandle(display_id) = *self; let display = CGDisplay::new(display_id); let height = display.pixels_high(); let width = display.pixels_wide(); - PhysicalSize::from_logical((width as f64, height as f64), self.hidpi_factor()) + PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor()) } #[inline] - pub fn position(&self) -> PhysicalPosition { + pub fn position(&self) -> PhysicalPosition { let bounds = unsafe { CGDisplayBounds(self.native_identifier()) }; - PhysicalPosition::from_logical( + PhysicalPosition::from_logical::<_, f64>( (bounds.origin.x as f64, bounds.origin.y as f64), - self.hidpi_factor(), + self.scale_factor(), ) } - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { let screen = match self.ns_screen() { Some(screen) => screen, None => return 1.0, // default to 1.0 when we can't find the screen @@ -303,7 +302,7 @@ impl MonitorHandle { let uuid = ffi::CGDisplayCreateUUIDFromDisplayID(self.0); let screens = NSScreen::screens(nil); let count: NSUInteger = msg_send![screens, count]; - let key = IdRef::new(NSString::alloc(nil).init_str("NSScreenNumber")); + let key = util::ns_string_id_ref("NSScreenNumber"); for i in 0..count { let screen = msg_send![screens, objectAtIndex: i as NSUInteger]; let device_description = NSScreen::deviceDescription(screen); diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index edd8e9948d..aa7f5362c8 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -122,7 +122,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 EventsCleared +// without that, LoopDestroyed would get sent after MainEventsCleared extern "C" fn control_flow_end_handler( _: CFRunLoopObserverRef, activity: CFRunLoopActivity, diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index ad1a891bc4..96f8e5b099 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -1,20 +1,37 @@ use std::{ - os::raw::c_void, + ops::Deref, sync::{Mutex, Weak}, }; use cocoa::{ appkit::{CGFloat, NSScreen, NSWindow, NSWindowStyleMask}, base::{id, nil}, - foundation::{NSAutoreleasePool, NSPoint, NSSize, NSString}, + foundation::{NSPoint, NSSize, NSString}, }; -use dispatch::ffi::{dispatch_async_f, dispatch_get_main_queue, dispatch_sync_f}; +use dispatch::Queue; +use objc::rc::autoreleasepool; +use objc::runtime::NO; use crate::{ dpi::LogicalSize, platform_impl::platform::{ffi, util::IdRef, window::SharedState}, }; +// 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 + } +} + unsafe fn set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { ns_window.setStyleMask_(mask); // If we don't do this, key handling will break @@ -22,199 +39,55 @@ unsafe fn set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { ns_window.makeFirstResponder_(ns_view); } -struct SetStyleMaskData { - ns_window: id, - ns_view: id, - mask: NSWindowStyleMask, -} -impl SetStyleMaskData { - fn new_ptr(ns_window: id, ns_view: id, mask: NSWindowStyleMask) -> *mut Self { - Box::into_raw(Box::new(SetStyleMaskData { - ns_window, - ns_view, - mask, - })) - } -} -extern "C" fn set_style_mask_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetStyleMaskData; - { - let context = &*context_ptr; - set_style_mask(context.ns_window, context.ns_view, context.mask); - } - Box::from_raw(context_ptr); - } -} // 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 unsafe fn set_style_mask_async(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { - let context = SetStyleMaskData::new_ptr(ns_window, ns_view, mask); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_style_mask_callback, - ); + let ns_window = MainThreadSafe(ns_window); + let ns_view = MainThreadSafe(ns_view); + Queue::main().exec_async(move || { + set_style_mask(*ns_window, *ns_view, mask); + }); } pub unsafe fn set_style_mask_sync(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { - let context = SetStyleMaskData::new_ptr(ns_window, ns_view, mask); - dispatch_sync_f( - dispatch_get_main_queue(), - context as *mut _, - set_style_mask_callback, - ); -} - -struct SetContentSizeData { - ns_window: id, - size: LogicalSize, -} -impl SetContentSizeData { - fn new_ptr(ns_window: id, size: LogicalSize) -> *mut Self { - Box::into_raw(Box::new(SetContentSizeData { ns_window, size })) - } -} -extern "C" fn set_content_size_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetContentSizeData; - { - let context = &*context_ptr; - NSWindow::setContentSize_( - context.ns_window, - NSSize::new( - context.size.width as CGFloat, - context.size.height as CGFloat, - ), - ); - } - Box::from_raw(context_ptr); + if msg_send![class!(NSThread), isMainThread] { + set_style_mask(ns_window, ns_view, mask); + } else { + let ns_window = MainThreadSafe(ns_window); + let ns_view = MainThreadSafe(ns_view); + Queue::main().exec_sync(move || { + set_style_mask(*ns_window, *ns_view, mask); + }) } } + // `setContentSize:` isn't thread-safe either, though it doesn't log any errors // and just fails silently. Anyway, GCD to the rescue! -pub unsafe fn set_content_size_async(ns_window: id, size: LogicalSize) { - let context = SetContentSizeData::new_ptr(ns_window, size); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_content_size_callback, - ); +pub unsafe fn set_content_size_async(ns_window: id, size: LogicalSize) { + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.setContentSize_(NSSize::new(size.width as CGFloat, size.height as CGFloat)); + }); } -struct SetFrameTopLeftPointData { - ns_window: id, - point: NSPoint, -} -impl SetFrameTopLeftPointData { - fn new_ptr(ns_window: id, point: NSPoint) -> *mut Self { - Box::into_raw(Box::new(SetFrameTopLeftPointData { ns_window, point })) - } -} -extern "C" fn set_frame_top_left_point_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetFrameTopLeftPointData; - { - let context = &*context_ptr; - NSWindow::setFrameTopLeftPoint_(context.ns_window, context.point); - } - Box::from_raw(context_ptr); - } -} // `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy // to log errors. pub unsafe fn set_frame_top_left_point_async(ns_window: id, point: NSPoint) { - let context = SetFrameTopLeftPointData::new_ptr(ns_window, point); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_frame_top_left_point_callback, - ); + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.setFrameTopLeftPoint_(point); + }); } -struct SetLevelData { - ns_window: id, - level: ffi::NSWindowLevel, -} -impl SetLevelData { - fn new_ptr(ns_window: id, level: ffi::NSWindowLevel) -> *mut Self { - Box::into_raw(Box::new(SetLevelData { ns_window, level })) - } -} -extern "C" fn set_level_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetLevelData; - { - let context = &*context_ptr; - context.ns_window.setLevel_(context.level as _); - } - Box::from_raw(context_ptr); - } -} // `setFrameTopLeftPoint:` isn't thread-safe, and fails silently. pub unsafe fn set_level_async(ns_window: id, level: ffi::NSWindowLevel) { - let context = SetLevelData::new_ptr(ns_window, level); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_level_callback, - ); -} - -struct ToggleFullScreenData { - ns_window: id, - ns_view: id, - not_fullscreen: bool, - shared_state: Weak>, -} -impl ToggleFullScreenData { - fn new_ptr( - ns_window: id, - ns_view: id, - not_fullscreen: bool, - shared_state: Weak>, - ) -> *mut Self { - Box::into_raw(Box::new(ToggleFullScreenData { - ns_window, - ns_view, - not_fullscreen, - shared_state, - })) - } + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.setLevel_(level as _); + }); } -extern "C" fn toggle_full_screen_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut ToggleFullScreenData; - { - let context = &*context_ptr; - // `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 context.not_fullscreen { - let curr_mask = context.ns_window.styleMask(); - let required = NSWindowStyleMask::NSTitledWindowMask - | NSWindowStyleMask::NSResizableWindowMask; - if !curr_mask.contains(required) { - set_style_mask(context.ns_window, context.ns_view, required); - if let Some(shared_state) = context.shared_state.upgrade() { - trace!("Locked shared state in `toggle_full_screen_callback`"); - let mut shared_state_lock = shared_state.lock().unwrap(); - (*shared_state_lock).saved_style = Some(curr_mask); - trace!("Unlocked shared state in `toggle_full_screen_callback`"); - } - } - } - // Window level must be restored from `CGShieldingWindowLevel() - // + 1` back to normal in order for `toggleFullScreen` to do - // anything - context.ns_window.setLevel_(0); - context.ns_window.toggleFullScreen_(nil); - } - Box::from_raw(context_ptr); - } -} // `toggleFullScreen` is thread-safe, but our additional logic to account for // window styles isn't. pub unsafe fn toggle_full_screen_async( @@ -223,91 +96,42 @@ pub unsafe fn toggle_full_screen_async( not_fullscreen: bool, shared_state: Weak>, ) { - let context = ToggleFullScreenData::new_ptr(ns_window, ns_view, not_fullscreen, shared_state); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - toggle_full_screen_callback, - ); + let ns_window = MainThreadSafe(ns_window); + let ns_view = MainThreadSafe(ns_view); + let shared_state = MainThreadSafe(shared_state); + Queue::main().exec_async(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 = ns_window.styleMask(); + let required = + NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask; + if !curr_mask.contains(required) { + set_style_mask(*ns_window, *ns_view, required); + if let Some(shared_state) = shared_state.upgrade() { + trace!("Locked shared state in `toggle_full_screen_callback`"); + let mut shared_state_lock = shared_state.lock().unwrap(); + (*shared_state_lock).saved_style = Some(curr_mask); + trace!("Unlocked shared state in `toggle_full_screen_callback`"); + } + } + } + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + ns_window.setLevel_(0); + ns_window.toggleFullScreen_(nil); + }); } -extern "C" fn restore_display_mode_callback(screen: *mut c_void) { - unsafe { - let screen = Box::from_raw(screen as *mut u32); - ffi::CGRestorePermanentDisplayConfiguration(); - assert_eq!(ffi::CGDisplayRelease(*screen), ffi::kCGErrorSuccess); - } -} pub unsafe fn restore_display_mode_async(ns_screen: u32) { - dispatch_async_f( - dispatch_get_main_queue(), - Box::into_raw(Box::new(ns_screen)) as *mut _, - restore_display_mode_callback, - ); -} - -struct SetMaximizedData { - ns_window: id, - is_zoomed: bool, - maximized: bool, - shared_state: Weak>, -} -impl SetMaximizedData { - fn new_ptr( - ns_window: id, - is_zoomed: bool, - maximized: bool, - shared_state: Weak>, - ) -> *mut Self { - Box::into_raw(Box::new(SetMaximizedData { - ns_window, - is_zoomed, - maximized, - shared_state, - })) - } + Queue::main().exec_async(move || { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!(ffi::CGDisplayRelease(ns_screen), ffi::kCGErrorSuccess); + }); } -extern "C" fn set_maximized_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetMaximizedData; - { - let context = &*context_ptr; - - if let Some(shared_state) = context.shared_state.upgrade() { - trace!("Locked shared state in `set_maximized`"); - let mut shared_state_lock = shared_state.lock().unwrap(); - - // Save the standard frame sized if it is not zoomed - if !context.is_zoomed { - shared_state_lock.standard_frame = Some(NSWindow::frame(context.ns_window)); - } - - shared_state_lock.maximized = context.maximized; - let curr_mask = context.ns_window.styleMask(); - if shared_state_lock.fullscreen.is_some() { - // Handle it in window_did_exit_fullscreen - return; - } else if curr_mask.contains(NSWindowStyleMask::NSResizableWindowMask) { - // Just use the native zoom if resizable - context.ns_window.zoom_(nil); - } else { - // if it's not resizable, we set the frame directly - let new_rect = if context.maximized { - let screen = NSScreen::mainScreen(nil); - NSScreen::visibleFrame(screen) - } else { - shared_state_lock.saved_standard_frame() - }; - context.ns_window.setFrame_display_(new_rect, 0); - } - - trace!("Unlocked shared state in `set_maximized`"); - } - } - Box::from_raw(context_ptr); - } -} // `setMaximized` is not thread-safe pub unsafe fn set_maximized_async( ns_window: id, @@ -315,127 +139,79 @@ pub unsafe fn set_maximized_async( maximized: bool, shared_state: Weak>, ) { - let context = SetMaximizedData::new_ptr(ns_window, is_zoomed, maximized, shared_state); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_maximized_callback, - ); -} + let ns_window = MainThreadSafe(ns_window); + let shared_state = MainThreadSafe(shared_state); + Queue::main().exec_async(move || { + if let Some(shared_state) = shared_state.upgrade() { + trace!("Locked shared state in `set_maximized`"); + let mut shared_state_lock = shared_state.lock().unwrap(); + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + shared_state_lock.standard_frame = Some(NSWindow::frame(*ns_window)); + } -struct OrderOutData { - ns_window: id, -} -impl OrderOutData { - fn new_ptr(ns_window: id) -> *mut Self { - Box::into_raw(Box::new(OrderOutData { ns_window })) - } -} -extern "C" fn order_out_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut OrderOutData; - { - let context = &*context_ptr; - context.ns_window.orderOut_(nil); + shared_state_lock.maximized = maximized; + + let curr_mask = ns_window.styleMask(); + if shared_state_lock.fullscreen.is_some() { + // Handle it in window_did_exit_fullscreen + return; + } else if curr_mask.contains(NSWindowStyleMask::NSResizableWindowMask) { + // Just use the native zoom if resizable + ns_window.zoom_(nil); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let screen = NSScreen::mainScreen(nil); + NSScreen::visibleFrame(screen) + } else { + shared_state_lock.saved_standard_frame() + }; + ns_window.setFrame_display_(new_rect, NO); + } + + trace!("Unlocked shared state in `set_maximized`"); } - Box::from_raw(context_ptr); - } + }); } + // `orderOut:` isn't thread-safe. Calling it from another thread actually works, // but with an odd delay. pub unsafe fn order_out_async(ns_window: id) { - let context = OrderOutData::new_ptr(ns_window); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - order_out_callback, - ); + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.orderOut_(nil); + }); } -struct MakeKeyAndOrderFrontData { - ns_window: id, -} -impl MakeKeyAndOrderFrontData { - fn new_ptr(ns_window: id) -> *mut Self { - Box::into_raw(Box::new(MakeKeyAndOrderFrontData { ns_window })) - } -} -extern "C" fn make_key_and_order_front_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut MakeKeyAndOrderFrontData; - { - let context = &*context_ptr; - context.ns_window.makeKeyAndOrderFront_(nil); - } - Box::from_raw(context_ptr); - } -} // `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread // actually works, but with an odd delay. pub unsafe fn make_key_and_order_front_async(ns_window: id) { - let context = MakeKeyAndOrderFrontData::new_ptr(ns_window); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - make_key_and_order_front_callback, - ); + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.makeKeyAndOrderFront_(nil); + }); } -struct SetTitleData { - ns_window: id, - title: String, -} -impl SetTitleData { - fn new_ptr(ns_window: id, title: String) -> *mut Self { - Box::into_raw(Box::new(SetTitleData { ns_window, title })) - } -} -extern "C" fn set_title_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut SetTitleData; - { - let context = &*context_ptr; - let title = IdRef::new(NSString::alloc(nil).init_str(&context.title)); - context.ns_window.setTitle_(*title); - } - Box::from_raw(context_ptr); - } -} // `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 unsafe fn set_title_async(ns_window: id, title: String) { - let context = SetTitleData::new_ptr(ns_window, title); - dispatch_async_f( - dispatch_get_main_queue(), - context as *mut _, - set_title_callback, - ); + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + let title = IdRef::new(NSString::alloc(nil).init_str(&title)); + ns_window.setTitle_(*title); + }); } -struct CloseData { - ns_window: id, -} -impl CloseData { - fn new_ptr(ns_window: id) -> *mut Self { - Box::into_raw(Box::new(CloseData { ns_window })) - } -} -extern "C" fn close_callback(context: *mut c_void) { - unsafe { - let context_ptr = context as *mut CloseData; - { - let context = &*context_ptr; - let pool = NSAutoreleasePool::new(nil); - context.ns_window.close(); - pool.drain(); - } - Box::from_raw(context_ptr); - } -} // `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 unsafe fn close_async(ns_window: id) { - let context = CloseData::new_ptr(ns_window); - dispatch_async_f(dispatch_get_main_queue(), context as *mut _, close_callback); + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + autoreleasepool(move || { + ns_window.close(); + }); + }); } diff --git a/src/platform_impl/macos/util/cursor.rs b/src/platform_impl/macos/util/cursor.rs index 7f0b57fe1b..5c4d1537a7 100644 --- a/src/platform_impl/macos/util/cursor.rs +++ b/src/platform_impl/macos/util/cursor.rs @@ -3,7 +3,8 @@ use cocoa::{ base::{id, nil}, foundation::{NSDictionary, NSPoint, NSString}, }; -use objc::runtime::Sel; +use objc::{runtime::Sel, runtime::NO}; +use std::cell::RefCell; use crate::window::CursorIcon; @@ -126,3 +127,38 @@ pub unsafe fn load_webkit_cursor(cursor_name: &str) -> id { hotSpot:point ] } + +pub unsafe fn invisible_cursor() -> id { + // 16x16 GIF data for invisible cursor + // You can reproduce this via ImageMagick. + // $ convert -size 16x16 xc:none cursor.gif + static CURSOR_BYTES: &[u8] = &[ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x84, 0x8F, 0xA9, 0xCB, 0xED, 0x0F, + 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B, + ]; + + thread_local! { + // We can't initialize this at startup. + static CURSOR_OBJECT: RefCell = RefCell::new(nil); + } + + CURSOR_OBJECT.with(|cursor_obj| { + if *cursor_obj.borrow() == nil { + // Create a cursor from `CURSOR_BYTES` + let cursor_data: id = msg_send![class!(NSData), + dataWithBytesNoCopy:CURSOR_BYTES as *const [u8] + length:CURSOR_BYTES.len() + freeWhenDone:NO + ]; + + let ns_image: id = msg_send![class!(NSImage), alloc]; + let _: id = msg_send![ns_image, initWithData: cursor_data]; + let cursor: id = msg_send![class!(NSCursor), alloc]; + *cursor_obj.borrow_mut() = + msg_send![cursor, initWithImage:ns_image hotSpot: NSPoint::new(0.0, 0.0)]; + } + *cursor_obj.borrow() + }) +} diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index 238c9e5ddc..39a97c9832 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -8,7 +8,7 @@ use std::ops::{BitAnd, Deref}; use cocoa::{ appkit::{NSApp, NSWindowStyleMask}, base::{id, nil}, - foundation::{NSAutoreleasePool, NSRect, NSUInteger}, + foundation::{NSAutoreleasePool, NSRect, NSString, NSUInteger}, }; use core_graphics::display::CGDisplay; use objc::runtime::{Class, Object, Sel, BOOL, YES}; @@ -31,6 +31,7 @@ pub const EMPTY_RANGE: ffi::NSRange = ffi::NSRange { length: 0, }; +#[derive(Debug, PartialEq)] pub struct IdRef(id); impl IdRef { @@ -90,6 +91,22 @@ pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) } +pub unsafe fn ns_string_id_ref(s: &str) -> IdRef { + IdRef::new(NSString::alloc(nil).init_str(s)) +} + +pub unsafe fn app_name() -> Option { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let dict: id = msg_send![bundle, infoDictionary]; + let key = ns_string_id_ref("CFBundleName"); + let app_name: id = msg_send![dict, objectForKey:*key]; + if app_name != nil { + Some(app_name) + } else { + None + } +} + pub unsafe fn superclass<'a>(this: &'a Object) -> &'a Class { let superclass: id = msg_send![this, superclass]; &*(superclass as *const _) diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 307573db98..523cb665d5 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -9,7 +9,7 @@ use std::{ use cocoa::{ appkit::{NSApp, NSEvent, NSEventModifierFlags, NSEventPhase, NSView, NSWindow}, base::{id, nil}, - foundation::{NSPoint, NSRect, NSSize, NSString, NSUInteger}, + foundation::{NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger}, }; use objc::{ declare::ClassDecl, @@ -17,15 +17,16 @@ use objc::{ }; use crate::{ + dpi::LogicalPosition, event::{ - DeviceEvent, ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, TouchPhase, - VirtualKeyCode, WindowEvent, + DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton, + MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent, }, platform_impl::platform::{ app_state::AppState, event::{ char_to_keycode, check_function_keys, event_mods, get_scancode, modifier_event, - scancode_to_keycode, + scancode_to_keycode, EventWrapper, }, ffi::*, util::{self, IdRef}, @@ -35,33 +36,47 @@ use crate::{ window::WindowId, }; -#[derive(Default)] -struct Modifiers { - shift_pressed: bool, - ctrl_pressed: bool, - win_pressed: bool, - alt_pressed: bool, +pub struct CursorState { + pub visible: bool, + pub cursor: util::Cursor, } -struct ViewState { +impl Default for CursorState { + fn default() -> Self { + Self { + visible: true, + cursor: Default::default(), + } + } +} + +pub(super) struct ViewState { ns_window: id, - pub cursor: Arc>, + pub cursor_state: Arc>, ime_spot: Option<(f64, f64)>, raw_characters: Option, is_key_down: bool, - modifiers: Modifiers, + pub(super) modifiers: ModifiersState, + tracking_rect: Option, +} + +impl ViewState { + fn get_scale_factor(&self) -> f64 { + (unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as f64 + } } -pub fn new_view(ns_window: id) -> (IdRef, Weak>) { - let cursor = Default::default(); - let cursor_access = Arc::downgrade(&cursor); +pub fn new_view(ns_window: id) -> (IdRef, Weak>) { + let cursor_state = Default::default(); + let cursor_access = Arc::downgrade(&cursor_state); let state = ViewState { ns_window, - cursor, + cursor_state, ime_spot: None, raw_characters: None, is_key_down: false, modifiers: Default::default(), + tracking_rect: None, }; unsafe { // This is free'd in `dealloc` @@ -236,6 +251,10 @@ lazy_static! { sel!(cancelOperation:), cancel_operation as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(frameDidChange:), + frame_did_change as extern "C" fn(&Object, Sel, id), + ); decl.add_ivar::<*mut c_void>("winitState"); decl.add_ivar::("markedText"); let protocol = Protocol::get("NSTextInputClient").unwrap(); @@ -261,6 +280,19 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i let marked_text = ::init(NSMutableAttributedString::alloc(nil)); (*this).set_ivar("markedText", marked_text); + let _: () = msg_send![this, setPostsFrameChangedNotifications: YES]; + + let notification_center: &Object = + msg_send![class!(NSNotificationCenter), defaultCenter]; + let notification_name = + NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification"); + let _: () = msg_send![ + notification_center, + addObserver: this + selector: sel!(frameDidChange:) + name: notification_name + object: this + ]; } this } @@ -269,17 +301,46 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i extern "C" fn view_did_move_to_window(this: &Object, _sel: Sel) { trace!("Triggered `viewDidMoveToWindow`"); unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![this, removeTrackingRect: tracking_rect]; + } + let rect: NSRect = msg_send![this, visibleRect]; - let _: () = msg_send![this, + let tracking_rect: NSInteger = msg_send![this, addTrackingRect:rect owner:this userData:nil assumeInside:NO ]; + state.tracking_rect = Some(tracking_rect); } trace!("Completed `viewDidMoveToWindow`"); } +extern "C" fn frame_did_change(this: &Object, _sel: Sel, _event: id) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![this, removeTrackingRect: tracking_rect]; + } + + let rect: NSRect = msg_send![this, visibleRect]; + let tracking_rect: NSInteger = msg_send![this, + addTrackingRect:rect + owner:this + userData:nil + assumeInside:NO + ]; + + state.tracking_rect = Some(tracking_rect); + } +} + extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); @@ -309,7 +370,12 @@ extern "C" fn reset_cursor_rects(this: &Object, _sel: Sel) { let state = &mut *(state_ptr as *mut ViewState); let bounds: NSRect = msg_send![this, bounds]; - let cursor = state.cursor.lock().unwrap().load(); + let cursor_state = state.cursor_state.lock().unwrap(); + let cursor = if cursor_state.visible { + cursor_state.cursor.load() + } else { + util::invisible_cursor() + }; let _: () = msg_send![this, addCursorRect:bounds cursor:cursor @@ -322,7 +388,7 @@ extern "C" fn has_marked_text(this: &Object, _sel: Sel) -> BOOL { trace!("Triggered `hasMarkedText`"); let marked_text: id = *this.get_ivar("markedText"); trace!("Completed `hasMarkedText`"); - (marked_text.length() > 0) as i8 + (marked_text.length() > 0) as BOOL } } @@ -453,10 +519,10 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran let mut events = VecDeque::with_capacity(characters.len()); for character in string.chars().filter(|c| !is_corporate_character(*c)) { - events.push_back(Event::WindowEvent { + events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::ReceivedCharacter(character), - }); + })); } AppState::queue_events(events); @@ -477,10 +543,10 @@ extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { // The `else` condition would emit the same character, but I'm keeping this here both... // 1) as a reminder for how `doCommandBySelector` works // 2) to make our use of carriage return explicit - events.push_back(Event::WindowEvent { + events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::ReceivedCharacter('\r'), - }); + })); } else { let raw_characters = state.raw_characters.take(); if let Some(raw_characters) = raw_characters { @@ -488,10 +554,10 @@ extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { .chars() .filter(|c| !is_corporate_character(*c)) { - events.push_back(Event::WindowEvent { + events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::ReceivedCharacter(character), - }); + })); } } }; @@ -557,6 +623,19 @@ fn retrieve_keycode(event: id) -> Option { }) } +// Update `state.modifiers` if `event` has something different +fn update_potentially_stale_modifiers(state: &mut ViewState, event: id) { + let event_modifiers = event_mods(event); + if state.modifiers != event_modifiers { + state.modifiers = event_modifiers; + + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::ModifiersChanged(state.modifiers), + })); + } +} + extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { trace!("Triggered `keyDown`"); unsafe { @@ -572,6 +651,9 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { let is_repeat = msg_send![event, isARepeat]; + update_potentially_stale_modifiers(state, event); + + #[allow(deprecated)] let window_event = Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { @@ -582,18 +664,19 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { virtual_keycode, modifiers: event_mods(event), }, + is_synthetic: false, }, }; let pass_along = { - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); // Emit `ReceivedCharacter` for key repeats if is_repeat && state.is_key_down { for character in characters.chars().filter(|c| !is_corporate_character(*c)) { - AppState::queue_event(Event::WindowEvent { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id, event: WindowEvent::ReceivedCharacter(character), - }); + })); } false } else { @@ -623,6 +706,9 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { let scancode = get_scancode(event) as u32; let virtual_keycode = retrieve_keycode(event); + update_potentially_stale_modifiers(state, event); + + #[allow(deprecated)] let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::KeyboardInput { @@ -633,10 +719,11 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { virtual_keycode, modifiers: event_mods(event), }, + is_synthetic: false, }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `keyUp`"); } @@ -652,45 +739,52 @@ extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) { if let Some(window_event) = modifier_event( event, NSEventModifierFlags::NSShiftKeyMask, - state.modifiers.shift_pressed, + state.modifiers.shift(), ) { - state.modifiers.shift_pressed = !state.modifiers.shift_pressed; + state.modifiers.toggle(ModifiersState::SHIFT); events.push_back(window_event); } if let Some(window_event) = modifier_event( event, NSEventModifierFlags::NSControlKeyMask, - state.modifiers.ctrl_pressed, + state.modifiers.ctrl(), ) { - state.modifiers.ctrl_pressed = !state.modifiers.ctrl_pressed; + state.modifiers.toggle(ModifiersState::CTRL); events.push_back(window_event); } if let Some(window_event) = modifier_event( event, NSEventModifierFlags::NSCommandKeyMask, - state.modifiers.win_pressed, + state.modifiers.logo(), ) { - state.modifiers.win_pressed = !state.modifiers.win_pressed; + state.modifiers.toggle(ModifiersState::LOGO); events.push_back(window_event); } if let Some(window_event) = modifier_event( event, NSEventModifierFlags::NSAlternateKeyMask, - state.modifiers.alt_pressed, + state.modifiers.alt(), ) { - state.modifiers.alt_pressed = !state.modifiers.alt_pressed; + state.modifiers.toggle(ModifiersState::ALT); events.push_back(window_event); } + let window_id = WindowId(get_window_id(state.ns_window)); + for event in events { - AppState::queue_event(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, event, - }); + })); } + + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(state.modifiers), + })); } trace!("Completed `flagsChanged`"); } @@ -731,6 +825,9 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let event: id = msg_send![NSApp(), currentEvent]; + update_potentially_stale_modifiers(state, event); + + #[allow(deprecated)] let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::KeyboardInput { @@ -741,10 +838,11 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { virtual_keycode, modifiers: event_mods(event), }, + is_synthetic: false, }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `cancelOperation`"); } @@ -754,6 +852,8 @@ fn mouse_click(this: &Object, event: id, button: MouseButton, button_state: Elem let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); + update_potentially_stale_modifiers(state, event); + let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::MouseInput { @@ -764,31 +864,37 @@ fn mouse_click(this: &Object, event: id, button: MouseButton, button_state: Elem }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } } extern "C" fn mouse_down(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Left, ElementState::Pressed); } extern "C" fn mouse_up(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Left, ElementState::Released); } extern "C" fn right_mouse_down(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Right, ElementState::Pressed); } extern "C" fn right_mouse_up(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Right, ElementState::Released); } extern "C" fn other_mouse_down(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Middle, ElementState::Pressed); } extern "C" fn other_mouse_up(this: &Object, _sel: Sel, event: id) { + mouse_motion(this, event); mouse_click(this, event, MouseButton::Middle, ElementState::Released); } @@ -809,23 +915,29 @@ fn mouse_motion(this: &Object, event: id) { || view_point.x > view_rect.size.width || view_point.y > view_rect.size.height { - // Point is outside of the client area (view) - return; + let mouse_buttons_down: NSInteger = msg_send![class!(NSEvent), pressedMouseButtons]; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } } let x = view_point.x as f64; let y = view_rect.size.height as f64 - view_point.y as f64; + let logical_position = LogicalPosition::new(x, y); + + update_potentially_stale_modifiers(state, event); let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::CursorMoved { device_id: DEVICE_ID, - position: (x, y).into(), + position: logical_position.to_physical(state.get_scale_factor()), modifiers: event_mods(event), }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } } @@ -845,7 +957,7 @@ extern "C" fn other_mouse_dragged(this: &Object, _sel: Sel, event: id) { mouse_motion(this, event); } -extern "C" fn mouse_entered(this: &Object, _sel: Sel, event: id) { +extern "C" fn mouse_entered(this: &Object, _sel: Sel, _event: id) { trace!("Triggered `mouseEntered`"); unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); @@ -858,27 +970,7 @@ extern "C" fn mouse_entered(this: &Object, _sel: Sel, event: id) { }, }; - let move_event = { - let window_point = event.locationInWindow(); - let view_point: NSPoint = msg_send![this, - convertPoint:window_point - fromView:nil // convert from window coordinates - ]; - let view_rect: NSRect = msg_send![this, frame]; - let x = view_point.x as f64; - let y = (view_rect.size.height - view_point.y) as f64; - Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::CursorMoved { - device_id: DEVICE_ID, - position: (x, y).into(), - modifiers: event_mods(event), - }, - } - }; - - AppState::queue_event(enter_event); - AppState::queue_event(move_event); + AppState::queue_event(EventWrapper::StaticEvent(enter_event)); } trace!("Completed `mouseEntered`"); } @@ -896,18 +988,26 @@ extern "C" fn mouse_exited(this: &Object, _sel: Sel, _event: id) { }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `mouseExited`"); } extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { trace!("Triggered `scrollWheel`"); + + mouse_motion(this, event); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let delta = { - let (x, y) = (event.scrollingDeltaX(), event.scrollingDeltaY()); + // macOS horizontal sign convention is the inverse of winit. + let (x, y) = (event.scrollingDeltaX() * -1.0, event.scrollingDeltaY()); if event.hasPreciseScrollingDeltas() == YES { - MouseScrollDelta::PixelDelta((x as f64, y as f64).into()) + let delta = LogicalPosition::new(x, y).to_physical(state.get_scale_factor()); + MouseScrollDelta::PixelDelta(delta) } else { MouseScrollDelta::LineDelta(x as f32, y as f32) } @@ -928,6 +1028,8 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); + update_potentially_stale_modifiers(state, event); + let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::MouseWheel { @@ -938,14 +1040,17 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { }, }; - AppState::queue_event(device_event); - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(device_event)); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `scrollWheel`"); } extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { trace!("Triggered `pressureChangeWithEvent`"); + + mouse_motion(this, event); + unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); @@ -962,7 +1067,7 @@ extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { }, }; - AppState::queue_event(window_event); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `pressureChangeWithEvent`"); } diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index ab19fe7a67..94a2f541cf 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -10,21 +10,27 @@ use std::{ }; use crate::{ - dpi::{LogicalPosition, LogicalSize}, + dpi::{ + LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, + }, error::{ExternalError, NotSupportedError, OsError as RootOsError}, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS}, + platform::macos::{ActivationPolicy, WindowExtMacOS}, platform_impl::platform::{ app_state::AppState, + app_state::INTERRUPT_EVENT_LOOP_EXIT, ffi, monitor::{self, MonitorHandle, VideoMode}, util::{self, IdRef}, + view::CursorState, view::{self, new_view}, window_delegate::new_delegate, OsError, }, - window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId}, + window::{ + CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + }, }; use cocoa::{ appkit::{ @@ -33,7 +39,7 @@ use cocoa::{ NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, - foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString}, + foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize}, }; use core_graphics::display::{CGDisplay, CGDisplayMode}; use objc::{ @@ -56,7 +62,7 @@ pub fn get_window_id(window_cocoa_id: id) -> Id { Id(window_cocoa_id as *const Object as _) } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub activation_policy: ActivationPolicy, pub movable_by_window_background: bool, @@ -65,8 +71,27 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub titlebar_hidden: bool, pub titlebar_buttons_hidden: bool, pub fullsize_content_view: bool, - pub resize_increments: Option, + pub resize_increments: Option>, pub disallow_hidpi: bool, + pub has_shadow: bool, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + #[inline] + fn default() -> Self { + Self { + activation_policy: Default::default(), + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + resize_increments: None, + disallow_hidpi: false, + has_shadow: true, + } + } } fn create_app(activation_policy: ActivationPolicy) -> Option { @@ -90,8 +115,8 @@ fn create_app(activation_policy: ActivationPolicy) -> Option { unsafe fn create_view( ns_window: id, pl_attribs: &PlatformSpecificWindowBuilderAttributes, -) -> Option<(IdRef, Weak>)> { - let (ns_view, cursor) = new_view(ns_window); +) -> Option<(IdRef, Weak>)> { + let (ns_view, cursor_state) = new_view(ns_window); ns_view.non_nil().map(|ns_view| { if !pl_attribs.disallow_hidpi { ns_view.setWantsBestResolutionOpenGLSurface_(YES); @@ -108,7 +133,7 @@ unsafe fn create_view( ns_window.setContentView_(*ns_view); ns_window.makeFirstResponder_(*ns_view); - (ns_view, cursor) + (ns_view, cursor_state) }) } @@ -119,22 +144,28 @@ fn create_window( unsafe { let pool = NSAutoreleasePool::new(nil); let screen = match attrs.fullscreen { - Some(Fullscreen::Borderless(RootMonitorHandle { inner: ref monitor })) + Some(Fullscreen::Borderless(Some(RootMonitorHandle { inner: ref monitor }))) | Some(Fullscreen::Exclusive(RootVideoMode { video_mode: VideoMode { ref monitor, .. }, })) => { let monitor_screen = monitor.ns_screen(); Some(monitor_screen.unwrap_or(appkit::NSScreen::mainScreen(nil))) } + Some(Fullscreen::Borderless(None)) => Some(appkit::NSScreen::mainScreen(nil)), None => None, }; let frame = match screen { - Some(screen) => appkit::NSScreen::frame(screen), + Some(screen) => NSScreen::frame(screen), None => { - let (width, height) = attrs - .inner_size - .map(|logical| (logical.width, logical.height)) - .unwrap_or_else(|| (800.0, 600.0)); + let screen = NSScreen::mainScreen(nil); + let scale_factor = NSScreen::backingScaleFactor(screen) as f64; + let (width, height) = match attrs.inner_size { + Some(size) => { + let logical = size.to_logical(scale_factor); + (logical.width, logical.height) + } + None => (800.0, 600.0), + }; NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(width, height)) } }; @@ -142,10 +173,14 @@ fn create_window( let mut masks = if !attrs.decorations && !screen.is_some() { // Resizable UnownedWindow without a titlebar or borders // if decorations is set to false, ignore pl_attrs - NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask + NSWindowStyleMask::NSBorderlessWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask } else if pl_attrs.titlebar_hidden { // if the titlebar is hidden, ignore other pl_attrs - NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask + NSWindowStyleMask::NSBorderlessWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask } else { // default case, resizable window with titlebar and titlebar buttons NSWindowStyleMask::NSClosableWindowMask @@ -170,7 +205,7 @@ fn create_window( NO, )); let res = ns_window.non_nil().map(|ns_window| { - let title = IdRef::new(NSString::alloc(nil).init_str(&attrs.title)); + let title = util::ns_string_id_ref(&attrs.title); ns_window.setReleasedWhenClosed_(NO); ns_window.setTitle_(*title); ns_window.setAcceptsMouseMovedEvents_(YES); @@ -211,6 +246,10 @@ fn create_window( } } + if !pl_attrs.has_shadow { + ns_window.setHasShadow_(NO); + } + ns_window.center(); ns_window }); @@ -243,6 +282,13 @@ lazy_static! { pub struct SharedState { pub resizable: bool, pub fullscreen: Option, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + pub in_fullscreen_transition: bool, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + pub target_fullscreen: Option>, pub maximized: bool, pub standard_frame: Option, is_simple_fullscreen: bool, @@ -283,8 +329,8 @@ pub struct UnownedWindow { input_context: IdRef, // never changes pub shared_state: Arc>, decorations: AtomicBool, - cursor: Weak>, - cursor_visible: AtomicBool, + cursor_state: Weak>, + pub inner_rect: Option>, } unsafe impl Send for UnownedWindow {} @@ -313,7 +359,7 @@ impl UnownedWindow { os_error!(OsError::CreationError("Couldn't create `NSWindow`")) })?; - let (ns_view, cursor) = + let (ns_view, cursor_state) = unsafe { create_view(*ns_window, &pl_attribs) }.ok_or_else(|| { unsafe { pool.drain() }; os_error!(OsError::CreationError("Couldn't create `NSView`")) @@ -321,6 +367,8 @@ impl UnownedWindow { let input_context = unsafe { util::create_input_context(*ns_view) }; + let scale_factor = unsafe { NSWindow::backingScaleFactor(*ns_window) as f64 }; + unsafe { if win_attribs.transparent { ns_window.setOpaque_(NO); @@ -328,13 +376,14 @@ impl UnownedWindow { } ns_app.activateIgnoringOtherApps_(YES); - - win_attribs - .min_inner_size - .map(|dim| set_min_inner_size(*ns_window, dim)); - win_attribs - .max_inner_size - .map(|dim| set_max_inner_size(*ns_window, dim)); + win_attribs.min_inner_size.map(|dim| { + let logical_dim = dim.to_logical(scale_factor); + set_min_inner_size(*ns_window, logical_dim) + }); + win_attribs.max_inner_size.map(|dim| { + let logical_dim = dim.to_logical(scale_factor); + set_max_inner_size(*ns_window, logical_dim) + }); use cocoa::foundation::NSArray; // register for drag and drop operations. @@ -354,6 +403,9 @@ impl UnownedWindow { let maximized = win_attribs.maximized; let visible = win_attribs.visible; let decorations = win_attribs.decorations; + let inner_rect = win_attribs + .inner_size + .map(|size| size.to_physical(scale_factor)); let window = Arc::new(UnownedWindow { ns_view, @@ -361,8 +413,8 @@ impl UnownedWindow { input_context, shared_state: Arc::new(Mutex::new(win_attribs.into())), decorations: AtomicBool::new(decorations), - cursor, - cursor_visible: AtomicBool::new(true), + cursor_state, + inner_rect, }); let delegate = new_delegate(&window, fullscreen.is_some()); @@ -419,27 +471,31 @@ impl UnownedWindow { AppState::queue_redraw(RootWindowId(self.id())); } - pub fn outer_position(&self) -> Result { + pub fn outer_position(&self) -> Result, NotSupportedError> { let frame_rect = unsafe { NSWindow::frame(*self.ns_window) }; - Ok(( + let position = LogicalPosition::new( frame_rect.origin.x as f64, util::bottom_left_to_top_left(frame_rect), - ) - .into()) + ); + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) } - pub fn inner_position(&self) -> Result { + pub fn inner_position(&self) -> Result, NotSupportedError> { let content_rect = unsafe { NSWindow::contentRectForFrameRect_(*self.ns_window, NSWindow::frame(*self.ns_window)) }; - Ok(( + let position = LogicalPosition::new( content_rect.origin.x as f64, util::bottom_left_to_top_left(content_rect), - ) - .into()) + ); + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) } - pub fn set_outer_position(&self, position: LogicalPosition) { + pub fn set_outer_position(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); let dummy = NSRect::new( NSPoint::new( position.x, @@ -455,35 +511,50 @@ impl UnownedWindow { } #[inline] - pub fn inner_size(&self) -> LogicalSize { + pub fn inner_size(&self) -> PhysicalSize { let view_frame = unsafe { NSView::frame(*self.ns_view) }; - (view_frame.size.width as f64, view_frame.size.height as f64).into() + let logical: LogicalSize = + (view_frame.size.width as f64, view_frame.size.height as f64).into(); + let scale_factor = self.scale_factor(); + logical.to_physical(scale_factor) } #[inline] - pub fn outer_size(&self) -> LogicalSize { + pub fn outer_size(&self) -> PhysicalSize { let view_frame = unsafe { NSWindow::frame(*self.ns_window) }; - (view_frame.size.width as f64, view_frame.size.height as f64).into() + let logical: LogicalSize = + (view_frame.size.width as f64, view_frame.size.height as f64).into(); + let scale_factor = self.scale_factor(); + logical.to_physical(scale_factor) } #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { + pub fn set_inner_size(&self, size: Size) { unsafe { - util::set_content_size_async(*self.ns_window, size); + let scale_factor = self.scale_factor(); + util::set_content_size_async(*self.ns_window, size.to_logical(scale_factor)); } } - pub fn set_min_inner_size(&self, dimensions: Option) { + pub fn set_min_inner_size(&self, dimensions: Option) { unsafe { - let dimensions = dimensions.unwrap_or_else(|| (0, 0).into()); - set_min_inner_size(*self.ns_window, dimensions); + let dimensions = dimensions.unwrap_or(Logical(LogicalSize { + width: 0.0, + height: 0.0, + })); + let scale_factor = self.scale_factor(); + set_min_inner_size(*self.ns_window, dimensions.to_logical(scale_factor)); } } - pub fn set_max_inner_size(&self, dimensions: Option) { + pub fn set_max_inner_size(&self, dimensions: Option) { unsafe { - let dimensions = dimensions.unwrap_or_else(|| (!0, !0).into()); - set_max_inner_size(*self.ns_window, dimensions); + let dimensions = dimensions.unwrap_or(Logical(LogicalSize { + width: std::f32::MAX as f64, + height: std::f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + set_max_inner_size(*self.ns_window, dimensions.to_logical(scale_factor)); } } @@ -509,8 +580,8 @@ impl UnownedWindow { pub fn set_cursor_icon(&self, cursor: CursorIcon) { let cursor = util::Cursor::from(cursor); - if let Some(cursor_access) = self.cursor.upgrade() { - *cursor_access.lock().unwrap() = cursor; + if let Some(cursor_access) = self.cursor_state.upgrade() { + cursor_access.lock().unwrap().cursor = cursor; } unsafe { let _: () = msg_send![*self.ns_window, @@ -528,33 +599,34 @@ impl UnownedWindow { #[inline] pub fn set_cursor_visible(&self, visible: bool) { - let cursor_class = class!(NSCursor); - // macOS uses a "hide counter" like Windows does, so we avoid incrementing it more than once. - // (otherwise, `hide_cursor(false)` would need to be called n times!) - if visible != self.cursor_visible.load(Ordering::Acquire) { - if visible { - let _: () = unsafe { msg_send![cursor_class, unhide] }; - } else { - let _: () = unsafe { msg_send![cursor_class, hide] }; + if let Some(cursor_access) = self.cursor_state.upgrade() { + let mut cursor_state = cursor_access.lock().unwrap(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + drop(cursor_state); + unsafe { + let _: () = msg_send![*self.ns_window, + invalidateCursorRectsForView:*self.ns_view + ]; + } } - self.cursor_visible.store(visible, Ordering::Release); } } #[inline] - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { unsafe { NSWindow::backingScaleFactor(*self.ns_window) as _ } } #[inline] - pub fn set_cursor_position( - &self, - cursor_position: LogicalPosition, - ) -> Result<(), ExternalError> { - let window_position = self.inner_position().unwrap(); + pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), ExternalError> { + let physical_window_position = self.inner_position().unwrap(); + let scale_factor = self.scale_factor(); + let window_position = physical_window_position.to_logical::(scale_factor); + let logical_cursor_position = cursor_position.to_logical::(scale_factor); let point = appkit::CGPoint { - x: (cursor_position.x + window_position.x) as CGFloat, - y: (cursor_position.y + window_position.y) as CGFloat, + x: logical_cursor_position.x + window_position.x, + y: logical_cursor_position.y + window_position.y, }; CGDisplay::warp_mouse_cursor_position(point) .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; @@ -583,7 +655,7 @@ impl UnownedWindow { self.set_style_mask_async(curr_mask); } - is_zoomed != 0 + is_zoomed != NO } fn saved_style(&self, shared_state: &mut SharedState) -> NSWindowStyleMask { @@ -617,6 +689,25 @@ impl UnownedWindow { self.set_maximized(maximized); } + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] }; + let is_minimized: bool = is_minimized == YES; + if is_minimized == minimized { + return; + } + + if minimized { + unsafe { + NSWindow::miniaturize_(*self.ns_window, *self.ns_window); + } + } else { + unsafe { + NSWindow::deminiaturize_(*self.ns_window, *self.ns_window); + } + } + } + #[inline] pub fn set_maximized(&self, maximized: bool) { let is_zoomed = self.is_zoomed(); @@ -639,14 +730,26 @@ impl UnownedWindow { shared_state_lock.fullscreen.clone() } + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + #[inline] pub fn set_fullscreen(&self, fullscreen: Option) { trace!("Locked shared state in `set_fullscreen`"); - let shared_state_lock = self.shared_state.lock().unwrap(); + let mut shared_state_lock = self.shared_state.lock().unwrap(); if shared_state_lock.is_simple_fullscreen { trace!("Unlocked shared state in `set_fullscreen`"); return; } + if shared_state_lock.in_fullscreen_transition { + // We can't set fullscreen here. + // Set fullscreen after transition. + shared_state_lock.target_fullscreen = Some(fullscreen); + trace!("Unlocked shared state in `set_fullscreen`"); + return; + } let old_fullscreen = shared_state_lock.fullscreen.clone(); if fullscreen == old_fullscreen { trace!("Unlocked shared state in `set_fullscreen`"); @@ -660,10 +763,15 @@ impl UnownedWindow { // does not take a screen parameter, but uses the current screen) if let Some(ref fullscreen) = fullscreen { let new_screen = match fullscreen { - Fullscreen::Borderless(RootMonitorHandle { inner: ref monitor }) => monitor, + Fullscreen::Borderless(borderless) => { + let RootMonitorHandle { inner: monitor } = borderless + .clone() + .unwrap_or_else(|| self.current_monitor_inner()); + monitor + } Fullscreen::Exclusive(RootVideoMode { video_mode: VideoMode { ref monitor, .. }, - }) => monitor, + }) => monitor.clone(), } .ns_screen() .unwrap(); @@ -749,6 +857,8 @@ impl UnownedWindow { shared_state_lock.fullscreen = fullscreen.clone(); trace!("Unlocked shared state in `set_fullscreen`"); + INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); + match (&old_fullscreen, &fullscreen) { (&None, &Some(_)) => unsafe { util::toggle_full_screen_async( @@ -794,7 +904,7 @@ impl UnownedWindow { ) => unsafe { util::restore_display_mode_async(video_mode.monitor().inner.native_identifier()); }, - _ => (), + _ => INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst), } } @@ -861,7 +971,9 @@ impl UnownedWindow { } #[inline] - pub fn set_ime_position(&self, logical_spot: LogicalPosition) { + pub fn set_ime_position(&self, spot: Position) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); unsafe { view::set_ime_position( *self.ns_view, @@ -873,11 +985,25 @@ impl UnownedWindow { } #[inline] - pub fn current_monitor(&self) -> RootMonitorHandle { + pub fn request_user_attention(&self, request_type: Option) { + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest, + UserAttentionType::Informational => NSRequestUserAttentionType::NSInformationalRequest, + }); + unsafe { + if let Some(ty) = ns_request_type { + NSApp().requestUserAttention_(ty); + } + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> RootMonitorHandle { unsafe { let screen: id = msg_send![*self.ns_window, screen]; let desc = NSScreen::deviceDescription(screen); - let key = IdRef::new(NSString::alloc(nil).init_str("NSScreenNumber")); + let key = util::ns_string_id_ref("NSScreenNumber"); let value = NSDictionary::valueForKey_(desc, *key); let display_id = msg_send![value, unsignedIntegerValue]; RootMonitorHandle { @@ -886,14 +1012,20 @@ impl UnownedWindow { } } + #[inline] + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + #[inline] pub fn available_monitors(&self) -> VecDeque { monitor::available_monitors() } #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - monitor::primary_monitor() + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) } #[inline] @@ -918,18 +1050,6 @@ impl WindowExtMacOS for UnownedWindow { *self.ns_view as *mut _ } - #[inline] - fn request_user_attention(&self, request_type: RequestUserAttentionType) { - unsafe { - NSApp().requestUserAttention_(match request_type { - RequestUserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest, - RequestUserAttentionType::Informational => { - NSRequestUserAttentionType::NSInformationalRequest - } - }); - } - } - #[inline] fn simple_fullscreen(&self) -> bool { let shared_state_lock = self.shared_state.lock().unwrap(); @@ -955,7 +1075,11 @@ impl WindowExtMacOS for UnownedWindow { if fullscreen { // Remember the original window's settings - shared_state_lock.standard_frame = Some(NSWindow::frame(*self.ns_window)); + // Exclude title bar + shared_state_lock.standard_frame = Some(NSWindow::contentRectForFrameRect_( + *self.ns_window, + NSWindow::frame(*self.ns_window), + )); shared_state_lock.saved_style = Some(self.ns_window.styleMask()); shared_state_lock.save_presentation_opts = Some(app.presentationOptions_()); @@ -1014,6 +1138,19 @@ impl WindowExtMacOS for UnownedWindow { } } } + + #[inline] + fn has_shadow(&self) -> bool { + unsafe { self.ns_window.hasShadow() == YES } + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + unsafe { + self.ns_window + .setHasShadow_(if has_shadow { YES } else { NO }) + } + } } impl Drop for UnownedWindow { @@ -1026,7 +1163,7 @@ impl Drop for UnownedWindow { } } -unsafe fn set_min_inner_size(window: V, mut min_size: LogicalSize) { +unsafe fn set_min_inner_size(window: V, mut min_size: LogicalSize) { let mut current_rect = NSWindow::frame(window); let content_rect = NSWindow::contentRectForFrameRect_(window, NSWindow::frame(window)); // Convert from client area size to window size @@ -1039,18 +1176,18 @@ unsafe fn set_min_inner_size(window: V, mut min_size: Logica // If necessary, resize the window to match constraint if current_rect.size.width < min_size.width { current_rect.size.width = min_size.width; - window.setFrame_display_(current_rect, 0) + window.setFrame_display_(current_rect, NO) } if current_rect.size.height < min_size.height { // The origin point of a rectangle is at its bottom left in Cocoa. // To ensure the window's top-left point remains the same: current_rect.origin.y += current_rect.size.height - min_size.height; current_rect.size.height = min_size.height; - window.setFrame_display_(current_rect, 0) + window.setFrame_display_(current_rect, NO) } } -unsafe fn set_max_inner_size(window: V, mut max_size: LogicalSize) { +unsafe fn set_max_inner_size(window: V, mut max_size: LogicalSize) { let mut current_rect = NSWindow::frame(window); let content_rect = NSWindow::contentRectForFrameRect_(window, NSWindow::frame(window)); // Convert from client area size to window size @@ -1063,13 +1200,13 @@ unsafe fn set_max_inner_size(window: V, mut max_size: Logica // If necessary, resize the window to match constraint if current_rect.size.width > max_size.width { current_rect.size.width = max_size.width; - window.setFrame_display_(current_rect, 0) + window.setFrame_display_(current_rect, NO) } if current_rect.size.height > max_size.height { // The origin point of a rectangle is at its bottom left in Cocoa. // To ensure the window's top-left point remains the same: current_rect.origin.y += current_rect.size.height - max_size.height; current_rect.size.height = max_size.height; - window.setFrame_display_(current_rect, 0) + window.setFrame_display_(current_rect, NO) } } diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index cb1bf3c26e..c226a66d7a 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -1,7 +1,7 @@ use std::{ f64, os::raw::c_void, - sync::{Arc, Weak}, + sync::{atomic::Ordering, Arc, Weak}, }; use cocoa::{ @@ -15,11 +15,14 @@ use objc::{ }; use crate::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, + dpi::{LogicalPosition, LogicalSize}, + event::{Event, ModifiersState, WindowEvent}, platform_impl::platform::{ app_state::AppState, + app_state::INTERRUPT_EVENT_LOOP_EXIT, + event::{EventProxy, EventWrapper}, util::{self, IdRef}, + view::ViewState, window::{get_window_id, UnownedWindow}, }, window::{Fullscreen, WindowId}, @@ -42,25 +45,23 @@ pub struct WindowDelegateState { previous_position: Option<(f64, f64)>, // Used to prevent redundant events. - previous_dpi_factor: f64, + previous_scale_factor: f64, } impl WindowDelegateState { pub fn new(window: &Arc, initial_fullscreen: bool) -> Self { - let dpi_factor = window.hidpi_factor(); - + let scale_factor = window.scale_factor(); let mut delegate_state = WindowDelegateState { ns_window: window.ns_window.clone(), ns_view: window.ns_view.clone(), window: Arc::downgrade(&window), initial_fullscreen, previous_position: None, - previous_dpi_factor: dpi_factor, + previous_scale_factor: scale_factor, }; - if dpi_factor != 1.0 { - delegate_state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); - delegate_state.emit_resize_event(); + if scale_factor != 1.0 { + delegate_state.emit_static_scale_factor_changed_event(); } delegate_state @@ -73,17 +74,34 @@ impl WindowDelegateState { self.window.upgrade().map(|ref window| callback(window)) } - pub fn emit_event(&mut self, event: WindowEvent) { + pub fn emit_event(&mut self, event: WindowEvent<'static>) { let event = Event::WindowEvent { window_id: WindowId(get_window_id(*self.ns_window)), event, }; - AppState::queue_event(event); + AppState::queue_event(EventWrapper::StaticEvent(event)); + } + + pub fn emit_static_scale_factor_changed_event(&mut self) { + let scale_factor = self.get_scale_factor(); + if scale_factor == self.previous_scale_factor { + return (); + }; + + self.previous_scale_factor = scale_factor; + let wrapper = EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + ns_window: IdRef::retain(*self.ns_window), + suggested_size: self.view_size(), + scale_factor, + }); + AppState::queue_event(wrapper); } pub fn emit_resize_event(&mut self) { let rect = unsafe { NSView::frame(*self.ns_view) }; - let size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let scale_factor = self.get_scale_factor(); + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical(scale_factor); self.emit_event(WindowEvent::Resized(size)); } @@ -94,9 +112,20 @@ impl WindowDelegateState { let moved = self.previous_position != Some((x, y)); if moved { self.previous_position = Some((x, y)); - self.emit_event(WindowEvent::Moved((x, y).into())); + let scale_factor = self.get_scale_factor(); + let physical_pos = LogicalPosition::::from((x, y)).to_physical(scale_factor); + self.emit_event(WindowEvent::Moved(physical_pos)); } } + + fn get_scale_factor(&self) -> f64 { + (unsafe { NSWindow::backingScaleFactor(*self.ns_window) }) as f64 + } + + fn view_size(&self) -> LogicalSize { + let ns_size = unsafe { NSView::frame(*self.ns_view).size }; + LogicalSize::new(ns_size.width as f64, ns_size.height as f64) + } } pub fn new_delegate(window: &Arc, initial_fullscreen: bool) -> IdRef { @@ -140,10 +169,6 @@ lazy_static! { sel!(windowDidMove:), window_did_move as extern "C" fn(&Object, Sel, id), ); - decl.add_method( - sel!(windowDidChangeScreen:), - window_did_change_screen as extern "C" fn(&Object, Sel, id), - ); decl.add_method( sel!(windowDidChangeBackingProperties:), window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), @@ -195,6 +220,10 @@ lazy_static! { sel!(windowDidExitFullScreen:), window_did_exit_fullscreen as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowWillExitFullScreen:), + window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(windowDidFailToEnterFullScreen:), window_did_fail_to_enter_fullscreen as extern "C" fn(&Object, Sel, id), @@ -273,29 +302,10 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { trace!("Completed `windowDidMove:`"); } -extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { - trace!("Triggered `windowDidChangeScreen:`"); - with_state(this, |state| { - let dpi_factor = unsafe { NSWindow::backingScaleFactor(*state.ns_window) } as f64; - if state.previous_dpi_factor != dpi_factor { - state.previous_dpi_factor = dpi_factor; - state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); - state.emit_resize_event(); - } - }); - trace!("Completed `windowDidChangeScreen:`"); -} - -// This will always be called before `window_did_change_screen`. extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidChangeBackingProperties:`"); with_state(this, |state| { - let dpi_factor = unsafe { NSWindow::backingScaleFactor(*state.ns_window) } as f64; - if state.previous_dpi_factor != dpi_factor { - state.previous_dpi_factor = dpi_factor; - state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); - state.emit_resize_event(); - } + state.emit_static_scale_factor_changed_event(); }); trace!("Completed `windowDidChangeBackingProperties:`"); } @@ -313,6 +323,29 @@ extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) { extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidResignKey:`"); with_state(this, |state| { + // It happens rather often, e.g. when the user is Cmd+Tabbing, that the + // NSWindowDelegate will receive a didResignKey event despite no event + // being received when the modifiers are released. This is because + // flagsChanged events are received by the NSView instead of the + // 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. + // + // Here we (very unsafely) acquire the winitState (a ViewState) from the + // Object referenced by state.ns_view (an IdRef, which is dereferenced + // to an id) + let view_state: &mut ViewState = unsafe { + let ns_view: &Object = (*state.ns_view).as_ref().expect("failed to deref"); + let state_ptr: *mut c_void = *ns_view.get_ivar("winitState"); + &mut *(state_ptr as *mut ViewState) + }; + + // Both update the state and emit a ModifiersChanged event. + if !view_state.modifiers.is_empty() { + view_state.modifiers = ModifiersState::empty(); + state.emit_event(WindowEvent::ModifiersChanged(view_state.modifiers)); + } + state.emit_event(WindowEvent::Focused(false)); }); trace!("Completed `windowDidResignKey:`"); @@ -399,6 +432,9 @@ extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { /// Invoked when before enter fullscreen extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowWillEnterFullscreen:`"); + + INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); + with_state(this, |state| { state.with_window(|window| { trace!("Locked shared state in `window_will_enter_fullscreen`"); @@ -416,16 +452,34 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { // Otherwise, we must've reached fullscreen by the user clicking // on the green fullscreen button. Update state! None => { - shared_state.fullscreen = Some(Fullscreen::Borderless(window.current_monitor())) + let current_monitor = Some(window.current_monitor_inner()); + shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor)) } } - + shared_state.in_fullscreen_transition = true; trace!("Unlocked shared state in `window_will_enter_fullscreen`"); }) }); trace!("Completed `windowWillEnterFullscreen:`"); } +/// Invoked when before exit fullscreen +extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillExitFullScreen:`"); + + INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst); + + with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_will_exit_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = true; + trace!("Unlocked shared state in `window_will_exit_fullscreen`"); + }); + }); + trace!("Completed `windowWillExitFullScreen:`"); +} + extern "C" fn window_will_use_fullscreen_presentation_options( _this: &Object, _: Sel, @@ -448,19 +502,43 @@ extern "C" fn window_will_use_fullscreen_presentation_options( /// Invoked when entered fullscreen extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { + INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst); + trace!("Triggered `windowDidEnterFullscreen:`"); with_state(this, |state| { state.initial_fullscreen = false; + state.with_window(|window| { + trace!("Locked shared state in `window_did_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + let target_fullscreen = shared_state.target_fullscreen.take(); + trace!("Unlocked shared state in `window_did_enter_fullscreen`"); + drop(shared_state); + if let Some(target_fullscreen) = target_fullscreen { + window.set_fullscreen(target_fullscreen); + } + }); }); trace!("Completed `windowDidEnterFullscreen:`"); } /// Invoked when exited fullscreen extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { + INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst); + trace!("Triggered `windowDidExitFullscreen:`"); with_state(this, |state| { state.with_window(|window| { window.restore_state_from_fullscreen(); + trace!("Locked shared state in `window_did_exit_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + let target_fullscreen = shared_state.target_fullscreen.take(); + trace!("Unlocked shared state in `window_did_exit_fullscreen`"); + drop(shared_state); + if let Some(target_fullscreen) = target_fullscreen { + window.set_fullscreen(target_fullscreen); + } }) }); trace!("Completed `windowDidExitFullscreen:`"); @@ -485,6 +563,13 @@ extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidFailToEnterFullscreen:`"); with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_did_fail_to_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + shared_state.target_fullscreen = None; + trace!("Unlocked shared state in `window_did_fail_to_enter_fullscreen`"); + }); if state.initial_fullscreen { let _: () = unsafe { msg_send![*state.ns_window, diff --git a/src/platform_impl/web/device.rs b/src/platform_impl/web/device.rs deleted file mode 100644 index a2f00b69c4..0000000000 --- a/src/platform_impl/web/device.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id(pub i32); - -impl Id { - pub unsafe fn dummy() -> Self { - Id(0) - } -} diff --git a/src/platform_impl/web/device/gamepad/constants.rs b/src/platform_impl/web/device/gamepad/constants.rs new file mode 100644 index 0000000000..c2c589adc1 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/constants.rs @@ -0,0 +1,37 @@ +use crate::event::device::{GamepadAxis, GamepadButton}; + +pub(crate) static BUTTONS: [GamepadButton; 16] = [ + GamepadButton::South, + GamepadButton::East, + GamepadButton::West, + GamepadButton::North, + GamepadButton::LeftTrigger, + GamepadButton::RightTrigger, + GamepadButton::LeftShoulder, + GamepadButton::RightShoulder, + GamepadButton::Select, + GamepadButton::Start, + GamepadButton::LeftStick, + GamepadButton::RightStick, + GamepadButton::DPadUp, + GamepadButton::DPadDown, + GamepadButton::DPadLeft, + GamepadButton::DPadRight, +]; + +pub(crate) static AXES: [GamepadAxis; 6] = [ + GamepadAxis::LeftStickX, + GamepadAxis::LeftStickY, + GamepadAxis::RightStickX, + GamepadAxis::RightStickY, + GamepadAxis::LeftTrigger, + GamepadAxis::RightTrigger, +]; + +pub(crate) fn button_code(index: usize) -> Option { + BUTTONS.get(index).map(|ev| ev.clone()) +} + +pub(crate) fn axis_code(index: usize) -> Option { + AXES.get(index).map(|ev| ev.clone()) +} diff --git a/src/platform_impl/web/device/gamepad/manager.rs b/src/platform_impl/web/device/gamepad/manager.rs new file mode 100644 index 0000000000..aeaba59fa4 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/manager.rs @@ -0,0 +1,158 @@ +use super::utils; +use crate::event::device; +use crate::platform_impl::platform::{backend, device::gamepad, event_loop::global, GamepadHandle}; +use std::collections::VecDeque; + +pub struct Manager { + pub(crate) gamepads: Vec, + pub(crate) events: VecDeque<(backend::gamepad::Gamepad, device::GamepadEvent)>, + pub(crate) global_window: Option, +} + +impl Manager { + pub fn new() -> Self { + Self { + gamepads: Vec::new(), + events: VecDeque::new(), + global_window: None, + } + } + + // Register global window to fetch gamepads. + // Due to Chrome issue, I prefer to use its gamepad list + pub fn set_global_window(&mut self, global_window: global::Shared) { + self.global_window.replace(global_window); + } + + // Get an updated raw gamepad and generate a new mapping + pub fn collect_gamepads(&self) -> Option> { + self.global_window.as_ref().map(|w| w.get_gamepads()) + } + + // Collect gamepad events (buttons/axes/sticks) + // dispatch to handler and update gamepads + pub fn collect_events(&mut self, mut handler: F) + where + F: 'static + FnMut((device::GamepadHandle, device::GamepadEvent)), + { + let opt_new_gamepads = self.collect_gamepads(); + if opt_new_gamepads.is_none() { + return; + } + + let new_gamepads = opt_new_gamepads.unwrap(); + let old_gamepads = &self.gamepads; + + let mut old_index = 0; + let mut new_index = 0; + + // Collect events + loop { + match (old_gamepads.get(old_index), new_gamepads.get(new_index)) { + (Some(old), Some(new)) if old.index() == new.index() => { + // Button events + let buttons = old.mapping.buttons().zip(new.mapping.buttons()).enumerate(); + for (btn_index, (old_button, new_button)) in buttons { + match (old_button, new_button) { + (false, true) => self + .events + .push_back((new.clone(), utils::gamepad_button(btn_index, true))), + (true, false) => self + .events + .push_back((new.clone(), utils::gamepad_button(btn_index, false))), + _ => (), + } + } + + // Axis events + let axes = old.mapping.axes().zip(new.mapping.axes()).enumerate(); + for (axis_index, (old_axis, new_axis)) in axes { + if old_axis != new_axis { + self.events + .push_back((new.clone(), utils::gamepad_axis(axis_index, new_axis))) + } + } + + // Stick events + let mut old_axes = old.mapping.axes(); + let mut new_axes = new.mapping.axes(); + + let old_left = (old_axes.next(), old_axes.next()); + let new_left = (new_axes.next(), new_axes.next()); + if old_left != new_left { + if let (Some(x), Some(y)) = (new_left.0, new_left.1) { + self.events.push_back(( + new.clone(), + utils::gamepad_stick(0, 1, x, y, device::Side::Left), + )); + } + } + + let old_right = (old_axes.next(), old_axes.next()); + let new_right = (new_axes.next(), new_axes.next()); + if old_right != new_right { + if let (Some(x), Some(y)) = (new_right.0, new_right.1) { + self.events.push_back(( + new.clone(), + utils::gamepad_stick(2, 3, x, y, device::Side::Right), + )); + } + } + + // Increment indices + old_index += 1; + new_index += 1; + } + + // Connect + (None, Some(new)) => { + self.events + .push_back((new.clone(), device::GamepadEvent::Added)); + new_index += 1; + } + + // Connect + (Some(old), Some(new)) if old.index > new.index => { + self.events + .push_back((new.clone(), device::GamepadEvent::Added)); + new_index += 1; + } + + // Disconnect + (Some(old), Some(_new)) => { + self.events + .push_back((old.clone(), device::GamepadEvent::Removed)); + old_index += 1; + } + + // Disconnect + (Some(old), None) => { + self.events + .push_back((old.clone(), device::GamepadEvent::Removed)); + old_index += 1; + } + + // Break loop + (None, None) => break, + } + } + + // Dispatch events and drain events vec + loop { + if let Some((gamepad, event)) = self.events.pop_front() { + handler(( + device::GamepadHandle(GamepadHandle { + id: gamepad.index, + gamepad: gamepad::Shared::Raw(gamepad), + }), + event, + )); + } else { + break; + } + } + + // Update gamepads + self.gamepads = new_gamepads; + } +} diff --git a/src/platform_impl/web/device/gamepad/mapping.rs b/src/platform_impl/web/device/gamepad/mapping.rs new file mode 100644 index 0000000000..c424b73789 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/mapping.rs @@ -0,0 +1,23 @@ +#[derive(Debug, Clone)] +pub enum Mapping { + Standard { buttons: [bool; 16], axes: [f64; 6] }, + NoMapping { buttons: Vec, axes: Vec }, +} + +impl Mapping { + pub(crate) fn buttons<'a>(&'a self) -> impl Iterator + 'a { + match self { + Mapping::Standard { buttons, .. } => buttons.iter(), + Mapping::NoMapping { buttons, .. } => buttons.iter(), + } + .cloned() + } + + pub(crate) fn axes<'a>(&'a self) -> impl Iterator + 'a { + match self { + Mapping::Standard { axes, .. } => axes.iter(), + Mapping::NoMapping { axes, .. } => axes.iter(), + } + .cloned() + } +} diff --git a/src/platform_impl/web/device/gamepad/mod.rs b/src/platform_impl/web/device/gamepad/mod.rs new file mode 100644 index 0000000000..7ed8e7abce --- /dev/null +++ b/src/platform_impl/web/device/gamepad/mod.rs @@ -0,0 +1,99 @@ +mod manager; +mod mapping; +mod utils; + +pub mod constants; +pub use manager::Manager; +pub use mapping::Mapping; + +use crate::event::device::{BatteryLevel, RumbleError}; +use crate::platform_impl::platform::backend; +use std::fmt; + +pub enum Shared { + Raw(backend::gamepad::Gamepad), + Dummy, +} + +impl Shared { + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn id(&self) -> i32 { + match self { + Shared::Raw(g) => g.index() as i32, + Shared::Dummy => -1, + } + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn info(&self) -> String { + match self { + Shared::Raw(g) => g.id(), + Shared::Dummy => String::new(), + } + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + match self { + Shared::Raw(g) => g.connected(), + Shared::Dummy => false, + } + } + + // [EXPERIMENTAL] An array containing GamepadHapticActuator objects, + // each of which represents haptic feedback hardware available on the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators + pub fn rumble(&self, left_speed: f64, _right_speed: f64) -> Result<(), RumbleError> { + match self { + Shared::Dummy => Ok(()), + Shared::Raw(g) => { + g.vibrate(left_speed, 1000f64); + Ok(()) + } + } + } + + pub fn is_dummy(&self) -> bool { + match self { + Shared::Dummy => true, + _ => false, + } + } + + pub fn port(&self) -> Option { + None + } + + pub fn battery_level(&self) -> Option { + None + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + match self { + Shared::Raw(g) => Shared::Raw(g.clone()), + Shared::Dummy => Shared::Dummy, + } + } +} + +impl Default for Shared { + fn default() -> Self { + Shared::Dummy + } +} + +impl fmt::Debug for Shared { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + if self.is_dummy() { + write!(f, "Gamepad (Dummy)") + } else { + write!(f, "Gamepad ({}#{})", self.id(), self.info()) + } + } +} diff --git a/src/platform_impl/web/device/gamepad/utils.rs b/src/platform_impl/web/device/gamepad/utils.rs new file mode 100644 index 0000000000..3bc8cb973d --- /dev/null +++ b/src/platform_impl/web/device/gamepad/utils.rs @@ -0,0 +1,50 @@ +use super::constants; +use crate::event::{device, ElementState}; + +pub fn gamepad_button(code: usize, pressed: bool) -> device::GamepadEvent { + let button_id = code as u32; + let button = constants::button_code(code); + + let state = if pressed { + ElementState::Pressed + } else { + ElementState::Released + }; + + device::GamepadEvent::Button { + button_id, + button, + state, + } +} + +pub fn gamepad_axis(code: usize, value: f64) -> device::GamepadEvent { + let axis_id = code as u32; + let axis = constants::axis_code(code); + + device::GamepadEvent::Axis { + axis_id, + axis, + value, + stick: true, + } +} + +pub fn gamepad_stick( + x_code: usize, + y_code: usize, + x_value: f64, + y_value: f64, + side: device::Side, +) -> device::GamepadEvent { + let x_id = x_code as u32; + let y_id = y_code as u32; + + device::GamepadEvent::Stick { + x_id, + y_id, + x_value, + y_value, + side, + } +} diff --git a/src/platform_impl/web/device/mod.rs b/src/platform_impl/web/device/mod.rs new file mode 100644 index 0000000000..eb4108bade --- /dev/null +++ b/src/platform_impl/web/device/mod.rs @@ -0,0 +1,161 @@ +pub mod gamepad; + +use super::event_loop::EventLoop; +use crate::event::device; + +use std::{ + cmp::{Eq, Ordering, PartialEq, PartialOrd}, + hash::{Hash, Hasher}, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct MouseId(pub i32); + +unsafe impl Send for MouseId {} +unsafe impl Sync for MouseId {} + +impl MouseId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.mice() + } +} + +impl From for device::MouseId { + fn from(platform_id: MouseId) -> Self { + Self(platform_id) + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct KeyboardId(pub i32); + +unsafe impl Send for KeyboardId {} +unsafe impl Sync for KeyboardId {} + +impl KeyboardId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.keyboards() + } +} + +impl From for device::KeyboardId { + fn from(platform_id: KeyboardId) -> Self { + Self(platform_id) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct HidId(pub i32); + +unsafe impl Send for HidId {} +unsafe impl Sync for HidId {} + +impl HidId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.hids() + } +} + +impl From for device::HidId { + fn from(platform_id: HidId) -> Self { + Self(platform_id) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GamepadHandle { + pub(crate) id: i32, + pub(crate) gamepad: gamepad::Shared, +} + +unsafe impl Send for GamepadHandle {} +unsafe impl Sync for GamepadHandle {} + +impl GamepadHandle { + pub unsafe fn dummy() -> Self { + Self { + id: -1, + gamepad: gamepad::Shared::default(), + } + } + + pub fn is_connected(&self) -> bool { + self.gamepad.connected() + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.gamepads() + } + + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), device::RumbleError> { + self.gamepad.rumble(left_speed, right_speed) + } + + pub fn port(&self) -> Option { + self.gamepad.port() + } + + pub fn battery_level(&self) -> Option { + self.gamepad.battery_level() + } +} + +impl Eq for GamepadHandle {} + +impl PartialEq for GamepadHandle { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.id == othr.id + } +} + +impl Ord for GamepadHandle { + #[inline(always)] + fn cmp(&self, othr: &Self) -> Ordering { + self.id.cmp(&othr.id) + } +} +impl PartialOrd for GamepadHandle { + #[inline(always)] + fn partial_cmp(&self, othr: &Self) -> Option { + self.id.partial_cmp(&othr.id) + } +} + +impl Hash for GamepadHandle { + #[inline(always)] + fn hash(&self, state: &mut H) { + self.id.hash(state) + } +} diff --git a/src/platform_impl/web/event_loop/global.rs b/src/platform_impl/web/event_loop/global.rs new file mode 100644 index 0000000000..8ec797009a --- /dev/null +++ b/src/platform_impl/web/event_loop/global.rs @@ -0,0 +1,81 @@ +use super::super::device::{gamepad, GamepadHandle}; +use super::backend; +use crate::event::device; +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +#[derive(Debug)] +pub struct Window { + raw: RefCell>, + gamepads: Rc>>, +} + +#[derive(Debug)] +pub struct Shared(Rc); + +impl Shared { + pub fn new() -> Self { + Self(Rc::new(Window { + raw: RefCell::new(None), + gamepads: Rc::new(RefCell::new(HashSet::new())), + })) + } + + // Request window object and listen global events + pub fn register_events(&self) -> Result<(), crate::error::OsError> { + if (*self.0.raw.borrow()).is_none() { + let shared = backend::window::Shared::create()?; + let mut window = shared.0.borrow_mut(); + + let shared_gamepads = self.0.gamepads.clone(); + window.on_gamepad_connected(move |gamepad: backend::gamepad::Gamepad| { + let mut gamepads = shared_gamepads.borrow_mut(); + let index = gamepad.index(); + if !gamepads.contains(&index) { + gamepads.insert(index); + } + }); + + let shared_gamepads = self.0.gamepads.clone(); + window.on_gamepad_disconnected(move |gamepad: backend::gamepad::Gamepad| { + let mut gamepads = shared_gamepads.borrow_mut(); + let index = gamepad.index(); + if gamepads.contains(&index) { + gamepads.remove(&index); + } + }); + + self.0.raw.replace(Some(shared.clone())); + } + + Ok(()) + } + + // Google Chrome create an array of [null, null, null, null]. + // To fix that issue, I create my own list of gamepads + // by listening "gamepadconnected" and "gamepaddisconnected" + pub fn get_gamepads(&self) -> Vec { + let gamepads = self.0.gamepads.borrow_mut(); + backend::get_gamepads() + .filter(|g| gamepads.contains(&g.index())) + .collect() + } + + // Return gamepads handles required for EventLoop::gamepads() + pub fn get_gamepad_handles(&self) -> Vec { + self.get_gamepads() + .iter() + .map(|gamepad| { + device::GamepadHandle(GamepadHandle { + id: gamepad.index, + gamepad: gamepad::Shared::Raw(gamepad.clone()), + }) + }) + .collect() + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 7124cee7e1..ccdef374d6 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod global; mod proxy; mod runner; mod state; @@ -6,11 +7,10 @@ mod window_target; pub use self::proxy::Proxy; pub use self::window_target::WindowTarget; -use super::{backend, device, monitor, window}; +use super::{backend, device, window}; use crate::event::Event; use crate::event_loop as root; -use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; use std::marker::PhantomData; pub struct EventLoop { @@ -27,17 +27,9 @@ impl EventLoop { } } - pub fn available_monitors(&self) -> VecDequeIter { - VecDeque::new().into_iter() - } - - pub fn primary_monitor(&self) -> monitor::Handle { - monitor::Handle - } - pub fn run(self, mut event_handler: F) -> ! where - F: 'static + FnMut(Event, &root::EventLoopWindowTarget, &mut root::ControlFlow), + F: 'static + FnMut(Event<'_, T>, &root::EventLoopWindowTarget, &mut root::ControlFlow), { let target = root::EventLoopWindowTarget { p: self.elw.p.clone(), @@ -64,4 +56,20 @@ impl EventLoop { pub fn window_target(&self) -> &root::EventLoopWindowTarget { &self.elw } + + pub fn mice(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn keyboards(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn hids(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn gamepads(&self) -> impl '_ + Iterator { + self.elw.p.collect_gamepads().into_iter() + } } diff --git a/src/platform_impl/web/event_loop/proxy.rs b/src/platform_impl/web/event_loop/proxy.rs index e98585a3b3..6e569c054c 100644 --- a/src/platform_impl/web/event_loop/proxy.rs +++ b/src/platform_impl/web/event_loop/proxy.rs @@ -11,7 +11,7 @@ impl Proxy { Proxy { runner } } - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { self.runner.send_event(Event::UserEvent(event)); Ok(()) } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index b3889fad2a..f98cfcf0bf 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,6 +1,7 @@ -use super::{backend, state::State}; -use crate::event::{Event, StartCause, WindowEvent}; +use super::{super::ScaleChangeArgs, backend, state::State}; +use crate::event::{Event, StartCause}; use crate::event_loop as root; +use crate::platform_impl::platform::device::gamepad; use crate::window::WindowId; use instant::{Duration, Instant}; @@ -8,10 +9,12 @@ use std::{ cell::RefCell, clone::Clone, collections::{HashSet, VecDeque}, - rc::Rc, + iter, + ops::Deref, + rc::{Rc, Weak}, }; -pub struct Shared(Rc>); +pub struct Shared(Rc>); impl Clone for Shared { fn clone(&self) -> Self { @@ -19,48 +22,139 @@ impl Clone for Shared { } } -pub struct Execution { - runner: RefCell>>, - events: RefCell>>, +pub struct Execution { + runner: RefCell>, + events: RefCell>>, id: RefCell, + all_canvases: RefCell>)>>, redraw_pending: RefCell>, + destroy_pending: RefCell>, + scale_change_detector: RefCell>, + unload_event_handle: RefCell>, + gamepad_manager: RefCell, } -struct Runner { +enum RunnerEnum { + /// The `EventLoop` is created but not being run. + Pending, + /// The `EventLoop` is being run. + 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> { + match self { + RunnerEnum::Running(runner) => Some(runner), + _ => None, + } + } +} + +struct Runner { state: State, - is_busy: bool, - event_handler: Box, &mut root::ControlFlow)>, + event_handler: Box, &mut root::ControlFlow)>, } impl Runner { - pub fn new(event_handler: Box, &mut root::ControlFlow)>) -> Self { + pub fn new(event_handler: Box, &mut root::ControlFlow)>) -> Self { Runner { state: State::Init, - is_busy: false, event_handler, } } + + /// Returns the cooresponding `StartCause` for the current `state`, or `None` + /// when in `Exit` state. + fn maybe_start_cause(&self) -> Option { + Some(match self.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { + start, + requested_resume: Some(end), + }, + State::Exit => return None, + }) + } + + fn handle_single_event(&mut self, event: Event<'_, T>, control: &mut root::ControlFlow) { + let is_closed = *control == root::ControlFlow::Exit; + + (self.event_handler)(event, control); + + // Maintain closed state, even if the callback changes it + if is_closed { + *control = root::ControlFlow::Exit; + } + } } impl Shared { pub fn new() -> Self { Shared(Rc::new(Execution { - runner: RefCell::new(None), + runner: RefCell::new(RunnerEnum::Pending), events: RefCell::new(VecDeque::new()), id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), + destroy_pending: RefCell::new(VecDeque::new()), + scale_change_detector: RefCell::new(None), + unload_event_handle: RefCell::new(None), + gamepad_manager: RefCell::new(gamepad::Manager::new()), })) } + pub fn set_global_window(&self, global_window: super::global::Shared) { + self.0 + .gamepad_manager + .borrow_mut() + .set_global_window(global_window); + } + + pub fn add_canvas(&self, id: WindowId, canvas: &Rc>) { + self.0 + .all_canvases + .borrow_mut() + .push((id, Rc::downgrade(canvas))); + } + + pub fn notify_destroy_window(&self, id: WindowId) { + self.0.destroy_pending.borrow_mut().push_back(id); + } + // 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, &mut root::ControlFlow)>) { - self.0.runner.replace(Some(Runner::new(event_handler))); - self.send_event(Event::NewEvents(StartCause::Init)); + pub fn set_listener( + &self, + event_handler: Box, &mut root::ControlFlow)>, + ) { + { + let mut runner = self.0.runner.borrow_mut(); + assert!(matches!(*runner, RunnerEnum::Pending)); + *runner = RunnerEnum::Running(Runner::new(event_handler)); + } + self.init(); let close_instance = self.clone(); - backend::on_unload(move || close_instance.handle_unload()); + *self.0.unload_event_handle.borrow_mut() = + Some(backend::on_unload(move || close_instance.handle_unload())); + } + + pub(crate) fn set_on_scale_change(&self, handler: F) + where + F: 'static + FnMut(ScaleChangeArgs), + { + *self.0.scale_change_detector.borrow_mut() = + Some(backend::ScaleChangeDetector::new(handler)); } // Generate a strictly increasing ID @@ -76,114 +170,272 @@ impl Shared { self.0.redraw_pending.borrow_mut().insert(id); } - // Add an event to the event loop runner + pub fn init(&self) { + let start_cause = Event::NewEvents(StartCause::Init); + self.run_until_cleared(iter::once(start_cause)); + } + + // Run the polling logic for the Poll ControlFlow, which involves clearing the queue + pub fn poll(&self) { + let start_cause = Event::NewEvents(StartCause::Poll); + self.run_until_cleared(iter::once(start_cause)); + } + + // Run the logic for waking from a WaitUntil, which involves clearing the queue + // Generally there shouldn't be events built up when this is called + pub fn resume_time_reached(&self, start: Instant, requested_resume: Instant) { + let start_cause = Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + }); + self.run_until_cleared(iter::once(start_cause)); + } + + // 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 fn send_event(&self, event: Event<'static, T>) { + 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 fn send_event(&self, event: Event) { + pub fn send_events(&self, events: impl Iterator>) { // If the event loop is closed, it should discard any new events if self.is_closed() { return; } - - // Determine if event handling is in process, and then release the borrow on the runner - let (start_cause, event_is_start) = match *self.0.runner.borrow() { - Some(ref runner) if !runner.is_busy => { - if let Event::NewEvents(cause) = event { - (cause, true) - } else { - ( - match runner.state { - State::Init => StartCause::Init, - State::Poll { .. } => StartCause::Poll, - State::Wait { start } => StartCause::WaitCancelled { - start, - requested_resume: None, - }, - State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { - start, - requested_resume: Some(end), - }, - State::Exit => { - return; - } - }, - false, - ) + // If we can run the event processing right now, or need to queue this and wait for later + let mut process_immediately = true; + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(ref runner)) => { + // If we're currently polling, queue this and wait for the poll() method to be called + if let State::Poll { .. } = runner.state { + process_immediately = false; } } - _ => { - // Events are currently being handled, so queue this one and don't try to - // double-process the event queue - self.0.events.borrow_mut().push_back(event); - return; + Ok(RunnerEnum::Pending) => { + // The runner still hasn't been attached: queue this event and wait for it to be + process_immediately = false; + } + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. So we queue this event for + // it to be processed later. + Err(_) => { + process_immediately = false; } + // This is unreachable since `self.is_closed() == true`. + Ok(RunnerEnum::Destroyed) => unreachable!(), + } + if !process_immediately { + // Queue these events to look at later + self.0.events.borrow_mut().extend(events); + return; + } + // At this point, we know this is a fresh set of events + // Now we determine why new events are incoming, and handle the events + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .unwrap_or_else(|| { + unreachable!("The runner cannot process events when it is not attached") + }) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, }; + // Take the start event, then the events provided to this function, and run an iteration of + // the event loop + let start_event = Event::NewEvents(start_cause); + let events = iter::once(start_event).chain(events); + self.run_until_cleared(events); + } + + // Process the destroy-pending windows. This should only be called from + // `run_until_cleared` and `handle_scale_changed`, somewhere between emitting + // `NewEvents` and `MainEventsCleared`. + fn process_destroy_pending_windows(&self, control: &mut root::ControlFlow) { + 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, + ); + self.0.redraw_pending.borrow_mut().remove(&id); + } + } + + // Given the set of new events, run the event loop until the main events and redraw events are + // 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(); - // Handle starting a new batch of events - // - // The user is informed via Event::NewEvents that there is a batch of events to process - // However, there is only one of these per batch of events - self.handle_event(Event::NewEvents(start_cause), &mut control); - if !event_is_start { + for event in events { self.handle_event(event, &mut control); } + self.process_destroy_pending_windows(&mut control); + self.handle_event(Event::MainEventsCleared, &mut control); + // 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( + self.handle_event(Event::RedrawRequested(window_id), &mut control); + } + + // Collect all global events + let mut gamepad_manager = self.0.gamepad_manager.borrow_mut(); + let instance = self.clone(); + gamepad_manager.collect_events(move |(handle, event)| { + instance.handle_event(Event::GamepadEvent(handle, event), &mut control); + }); + + self.handle_event(Event::RedrawEventsCleared, &mut control); + + self.apply_control_flow(control); + // 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); + } + } + + pub fn handle_scale_changed(&self, old_scale: f64, new_scale: f64) { + // If there aren't any windows, then there is nothing to do here. + if self.0.all_canvases.borrow().is_empty() { + return; + } + + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .unwrap_or_else(|| unreachable!("`scale_changed` should not happen without a runner")) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, + }; + let mut control = self.current_control_flow(); + + // Handle the start event and all other events in the queue. + self.handle_event(Event::NewEvents(start_cause), &mut control); + + // It is possible for windows to be dropped before this point. We don't + // want to send `ScaleFactorChanged` for destroyed windows, so we process + // the destroy-pending windows here. + self.process_destroy_pending_windows(&mut control); + + // Now handle the `ScaleFactorChanged` events. + for &(id, ref canvas) in &*self.0.all_canvases.borrow() { + let canvas = match canvas.upgrade() { + Some(rc) => rc.borrow().raw().clone(), + // This shouldn't happen, but just in case... + None => continue, + }; + // First, we send the `ScaleFactorChanged` event: + let current_size = crate::dpi::PhysicalSize { + width: canvas.width() as u32, + height: canvas.height() as u32, + }; + let logical_size = current_size.to_logical::(old_scale); + let mut new_size = logical_size.to_physical(new_scale); + self.handle_single_event_sync( Event::WindowEvent { - window_id, - event: WindowEvent::RedrawRequested, + window_id: id, + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: new_scale, + new_inner_size: &mut new_size, + }, }, &mut control, ); + + // Then we resize the canvas to the new size and send a `Resized` event: + backend::set_canvas_size(&canvas, crate::dpi::Size::Physical(new_size)); + self.handle_single_event_sync( + Event::WindowEvent { + window_id: id, + event: crate::event::WindowEvent::Resized(new_size), + }, + &mut control, + ); + } + + // Process the destroy-pending windows again. + self.process_destroy_pending_windows(&mut control); + self.handle_event(Event::MainEventsCleared, &mut control); + + // Discard all the pending redraw as we shall just redraw all windows. + self.0.redraw_pending.borrow_mut().clear(); + for &(window_id, _) in &*self.0.all_canvases.borrow() { + self.handle_event(Event::RedrawRequested(window_id), &mut control); } - self.handle_event(Event::EventsCleared, &mut control); + self.handle_event(Event::RedrawEventsCleared, &mut control); + self.apply_control_flow(control); // 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_event(Event::LoopDestroyed, &mut control); + self.handle_loop_destroyed(&mut control); } } fn handle_unload(&self) { self.apply_control_flow(root::ControlFlow::Exit); let mut control = self.current_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); } - // handle_event takes in events and either queues them or applies a callback + // handle_single_event_sync takes in an event and handles it synchronously. // - // It should only ever be called from send_event - fn handle_event(&self, event: Event, control: &mut root::ControlFlow) { - let is_closed = self.is_closed(); - + // It should only ever be called from `scale_changed`. + fn handle_single_event_sync(&self, event: Event<'_, T>, control: &mut root::ControlFlow) { + if self.is_closed() { + *control = root::ControlFlow::Exit; + } match *self.0.runner.borrow_mut() { - Some(ref mut runner) => { - // An event is being processed, so the runner should be marked busy - runner.is_busy = true; - - (runner.event_handler)(event, control); - - // Maintain closed state, even if the callback changes it - if is_closed { - *control = root::ControlFlow::Exit; - } + RunnerEnum::Running(ref mut runner) => { + runner.handle_single_event(event, control); + } + _ => panic!("Cannot handle event synchronously without a runner"), + } + } - // An event is no longer being processed - runner.is_busy = false; + // handle_event takes in events and either queues them or applies a callback + // + // It should only ever be called from `run_until_cleared` and `scale_changed`. + fn handle_event(&self, event: Event<'static, T>, control: &mut root::ControlFlow) { + if self.is_closed() { + *control = root::ControlFlow::Exit; + } + match *self.0.runner.borrow_mut() { + RunnerEnum::Running(ref mut runner) => { + runner.handle_single_event(event, control); } // If an event is being handled without a runner somehow, add it to the event queue so // it will eventually be processed - _ => self.0.events.borrow_mut().push_back(event), + RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event), + // If the Runner has been destroyed, there is nothing to do. + RunnerEnum::Destroyed => return, } + let is_closed = *control == root::ControlFlow::Exit; + // 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().is_some() { + if !is_closed && self.0.runner.borrow().maybe_runner().is_some() { // Take an event out of the queue and handle it - if let Some(event) = self.0.events.borrow_mut().pop_front() { + // Make sure not to let the borrow_mut live during the next handle_event + let event = { self.0.events.borrow_mut().pop_front() }; + if let Some(event) = event { self.handle_event(event, control); } } @@ -196,10 +448,7 @@ impl Shared { root::ControlFlow::Poll => { let cloned = self.clone(); State::Poll { - timeout: backend::Timeout::new( - move || cloned.send_event(Event::NewEvents(StartCause::Poll)), - Duration::from_millis(0), - ), + request: backend::AnimationFrameRequest::new(move || cloned.poll()), } } root::ControlFlow::Wait => State::Wait { @@ -220,7 +469,7 @@ impl Shared { start, end, timeout: backend::Timeout::new( - move || cloned.send_event(Event::NewEvents(StartCause::Poll)), + move || cloned.resume_time_reached(start, end), delay, ), } @@ -229,26 +478,61 @@ impl Shared { }; match *self.0.runner.borrow_mut() { - Some(ref mut runner) => { + RunnerEnum::Running(ref mut runner) => { runner.state = new_state; } - None => (), + _ => (), + } + } + + fn handle_loop_destroyed(&self, control: &mut root::ControlFlow) { + self.handle_event(Event::LoopDestroyed, control); + let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); + *self.0.scale_change_detector.borrow_mut() = None; + *self.0.unload_event_handle.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 { + // 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() { + let mut canvas = canvas.borrow_mut(); + canvas.remove_listeners(); + } } + // At this point, the `self.0` `Rc` should only be strongly referenced + // by the following: + // * `self`, i.e. the item which triggered this event loop wakeup, which + // is usually a `wasm-bindgen` `Closure`, which will be dropped after + // returning to the JS glue code. + // * The `EventLoopWindowTarget` leaked inside `EventLoop::run` due to the + // JS exception thrown at the end. + // * For each undropped `Window`: + // * The `register_redraw_request` closure. + // * The `destroy_fn` closure. } // Check if the event loop is currently closed fn is_closed(&self) -> bool { - match *self.0.runner.borrow() { - Some(ref runner) => runner.state.is_exit(), - None => false, // If the event loop is None, it has not been intialised yet, so it cannot be closed + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(runner)) => runner.state.is_exit(), + // 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. + Ok(RunnerEnum::Destroyed) => true, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. + Err(_) => false, } } // Get the current control flow state fn current_control_flow(&self) -> root::ControlFlow { match *self.0.runner.borrow() { - Some(ref runner) => runner.state.control_flow(), - None => root::ControlFlow::Poll, + RunnerEnum::Running(ref runner) => runner.state.control_flow(), + RunnerEnum::Pending => root::ControlFlow::Poll, + RunnerEnum::Destroyed => root::ControlFlow::Exit, } } } diff --git a/src/platform_impl/web/event_loop/state.rs b/src/platform_impl/web/event_loop/state.rs index 23e8045fc3..16b6e6232c 100644 --- a/src/platform_impl/web/event_loop/state.rs +++ b/src/platform_impl/web/event_loop/state.rs @@ -15,7 +15,7 @@ pub enum State { start: Instant, }, Poll { - timeout: backend::Timeout, + request: backend::AnimationFrameRequest, }, Exit, } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index d302edb87b..4e74ba4ac6 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,18 +1,25 @@ -use super::{backend, device, proxy::Proxy, runner, window}; -use crate::dpi::LogicalSize; -use crate::event::{DeviceId, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent}; +use super::{super::monitor, backend, global, proxy::Proxy, runner, window}; +use crate::dpi::{PhysicalSize, Size}; +use crate::event::{device, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent}; use crate::event_loop::ControlFlow; -use crate::window::WindowId; +use crate::monitor::MonitorHandle as RootMH; +use crate::platform_impl::platform::device::{KeyboardId, MouseId}; +use crate::window::{Theme, WindowId}; +use std::cell::RefCell; use std::clone::Clone; +use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; +use std::rc::Rc; pub struct WindowTarget { pub(crate) runner: runner::Shared, + pub(crate) global_window: global::Shared, } impl Clone for WindowTarget { fn clone(&self) -> Self { WindowTarget { runner: self.runner.clone(), + global_window: self.global_window.clone(), } } } @@ -21,6 +28,7 @@ impl WindowTarget { pub fn new() -> Self { WindowTarget { runner: runner::Shared::new(), + global_window: global::Shared::new(), } } @@ -28,18 +36,33 @@ impl WindowTarget { Proxy::new(self.runner.clone()) } - pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { + pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { + self.runner.set_global_window(self.global_window.clone()); self.runner.set_listener(event_handler); + let runner = self.runner.clone(); + self.runner.set_on_scale_change(move |arg| { + runner.handle_scale_changed(arg.old_scale, arg.new_scale) + }); } pub fn generate_id(&self) -> window::Id { window::Id(self.runner.generate_id()) } - pub fn register(&self, canvas: &mut backend::Canvas, id: window::Id) { - let runner = self.runner.clone(); + pub fn collect_gamepads(&self) -> Vec { + self.global_window.get_gamepad_handles() + } + + pub fn register_global_events(&self) -> Result<(), crate::error::OsError> { + self.global_window.register_events() + } + + pub fn register(&self, canvas: &Rc>, id: window::Id) { + self.runner.add_canvas(WindowId(id), canvas); + let mut canvas = canvas.borrow_mut(); canvas.set_attribute("data-raw-handle", &id.0.to_string()); + let runner = self.runner.clone(); canvas.on_blur(move || { runner.send_event(Event::WindowEvent { window_id: WindowId(id), @@ -57,34 +80,28 @@ impl WindowTarget { let runner = self.runner.clone(); canvas.on_keyboard_press(move |scancode, virtual_keycode, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::KeyboardInput { - device_id: DeviceId(unsafe { device::Id::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Pressed, - virtual_keycode, - modifiers, - }, - }, - }); + runner.send_event(Event::KeyboardEvent( + device::KeyboardId(unsafe { KeyboardId::dummy() }), + device::KeyboardEvent::Input(KeyboardInput { + scancode, + state: ElementState::Pressed, + virtual_keycode, + modifiers, + }), + )); }); let runner = self.runner.clone(); canvas.on_keyboard_release(move |scancode, virtual_keycode, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::KeyboardInput { - device_id: DeviceId(unsafe { device::Id::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Released, - virtual_keycode, - modifiers, - }, - }, - }); + runner.send_event(Event::KeyboardEvent( + device::KeyboardId(unsafe { KeyboardId::dummy() }), + device::KeyboardEvent::Input(KeyboardInput { + scancode, + state: ElementState::Released, + virtual_keycode, + modifiers, + }), + )); }); let runner = self.runner.clone(); @@ -96,31 +113,26 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_cursor_leave(move |pointer_id| { + canvas.on_cursor_leave(move || { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorLeft { - device_id: DeviceId(device::Id(pointer_id)), - }, + event: WindowEvent::CursorLeft, }); }); let runner = self.runner.clone(); - canvas.on_cursor_enter(move |pointer_id| { + canvas.on_cursor_enter(move || { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorEntered { - device_id: DeviceId(device::Id(pointer_id)), - }, + event: WindowEvent::CursorEntered, }); }); let runner = self.runner.clone(); - canvas.on_cursor_move(move |pointer_id, position, modifiers| { + canvas.on_cursor_move(move |position, modifiers| { runner.send_event(Event::WindowEvent { window_id: WindowId(id), event: WindowEvent::CursorMoved { - device_id: DeviceId(device::Id(pointer_id)), position, modifiers, }, @@ -128,70 +140,98 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_mouse_press(move |pointer_id, button, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseInput { - device_id: DeviceId(device::Id(pointer_id)), - state: ElementState::Pressed, - button, - modifiers, - }, - }); + canvas.on_mouse_press(move |pointer_id, position, button, modifiers| { + // 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( + std::iter::once(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::CursorMoved { + position, + modifiers, + }, + }) + .chain(std::iter::once(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Button { + state: ElementState::Pressed, + button, + }, + ))), + ); }); let runner = self.runner.clone(); - canvas.on_mouse_release(move |pointer_id, button, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseInput { - device_id: DeviceId(device::Id(pointer_id)), + canvas.on_mouse_release(move |pointer_id, button| { + runner.send_event(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Button { state: ElementState::Released, button, - modifiers, }, - }); + )); }); let runner = self.runner.clone(); - canvas.on_mouse_wheel(move |pointer_id, delta, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseWheel { - device_id: DeviceId(device::Id(pointer_id)), - delta, - phase: TouchPhase::Moved, - modifiers, - }, - }); + canvas.on_mouse_wheel(move |pointer_id, delta| { + runner.send_event(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Wheel(delta.0, delta.1), + )); }); let runner = self.runner.clone(); let raw = canvas.raw().clone(); - let mut intended_size = LogicalSize { - width: raw.width() as f64, - height: raw.height() as f64, + + // The size to restore to after exiting fullscreen. + let mut intended_size = PhysicalSize { + width: raw.width() as u32, + height: raw.height() as u32, }; canvas.on_fullscreen_change(move || { // If the canvas is marked as fullscreen, it is moving *into* fullscreen // If it is not, it is moving *out of* fullscreen let new_size = if backend::is_fullscreen(&raw) { - intended_size = LogicalSize { - width: raw.width() as f64, - height: raw.height() as f64, + intended_size = PhysicalSize { + width: raw.width() as u32, + height: raw.height() as u32, }; - backend::window_size() + backend::window_size().to_physical(backend::scale_factor()) } else { intended_size }; - raw.set_width(new_size.width as u32); - raw.set_height(new_size.height as u32); + + backend::set_canvas_size(&raw, Size::Physical(new_size)); runner.send_event(Event::WindowEvent { window_id: WindowId(id), event: WindowEvent::Resized(new_size), }); runner.request_redraw(WindowId(id)); }); + + let runner = self.runner.clone(); + canvas.on_dark_mode(move |is_dark_mode| { + let theme = if is_dark_mode { + Theme::Dark + } else { + Theme::Light + }; + runner.send_event(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::ThemeChanged(theme), + }); + }); + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> Option { + Some(RootMH { + inner: monitor::Handle, + }) } } diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 029761b957..f56890a023 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -1,6 +1,24 @@ -// TODO: close events (port from old stdweb branch) -// TODO: pointer locking (stdweb PR required) -// TODO: fullscreen API (stdweb PR required) +// Brief introduction to the internals of the web backend: +// Currently, the web backend supports both wasm-bindgen and stdweb as methods of binding to the +// environment. Because they are both supporting the same underlying APIs, the actual web bindings +// are cordoned off into backend abstractions, which present the thinnest unifying layer possible. +// +// When adding support for new events or interactions with the browser, first consult trusted +// documentation (such as MDN) to ensure it is well-standardised and supported across many browsers. +// Once you have decided on the relevant web APIs, add support to both backends. +// +// The backend is used by the rest of the module to implement Winit's business logic, which forms +// the rest of the code. 'device', 'error', 'monitor', and 'window' define web-specific structures +// for winit's cross-platform structures. They are all relatively simple translations. +// +// The event_loop module handles listening for and processing events. 'Proxy' implements +// EventLoopProxy and 'WindowTarget' implements EventLoopWindowTarget. WindowTarget also handles +// registering the event handlers. The 'Execution' struct in the 'runner' module handles taking +// incoming events (from the registered handlers) and ensuring they are passed to the user in a +// compliant way. + +// Silence warnings from use of deprecated stdweb backend +#![allow(deprecated)] mod device; mod error; @@ -19,7 +37,8 @@ mod backend; #[cfg(not(any(feature = "web-sys", feature = "stdweb")))] compile_error!("Please select a feature to build for web: `web-sys`, `stdweb`"); -pub use self::device::Id as DeviceId; +pub(crate) use self::device::*; + pub use self::error::OsError; pub use self::event_loop::{ EventLoop, Proxy as EventLoopProxy, WindowTarget as EventLoopWindowTarget, @@ -29,3 +48,11 @@ pub use self::window::{ Id as WindowId, PlatformSpecificBuilderAttributes as PlatformSpecificWindowBuilderAttributes, Window, }; + +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Copy)] +pub(crate) struct ScaleChangeArgs { + old_scale: f64, + new_scale: f64, +} diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 4889c88db9..d2b3c364ae 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -5,22 +5,22 @@ use crate::monitor::{MonitorHandle, VideoMode}; pub struct Handle; impl Handle { - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { 1.0 } - pub fn position(&self) -> PhysicalPosition { - PhysicalPosition { x: 0.0, y: 0.0 } + pub fn position(&self) -> PhysicalPosition { + PhysicalPosition { x: 0, y: 0 } } pub fn name(&self) -> Option { None } - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { PhysicalSize { - width: 0.0, - height: 0.0, + width: 0, + height: 0, } } @@ -33,7 +33,7 @@ impl Handle { pub struct Mode; impl Mode { - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { unimplemented!(); } diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs index a0ff5af2e8..8aeba9e5f6 100644 --- a/src/platform_impl/web/stdweb/canvas.rs +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -1,24 +1,24 @@ -use super::event; -use crate::dpi::{LogicalPosition, LogicalSize}; +use super::utils; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; -use crate::platform_impl::OsError; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; +use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use std::cell::RefCell; use std::rc::Rc; +use stdweb::js; use stdweb::traits::IPointerEvent; use stdweb::unstable::TryInto; use stdweb::web::event::{ - BlurEvent, ConcreteEvent, FocusEvent, FullscreenChangeEvent, KeyDownEvent, KeyPressEvent, - KeyUpEvent, MouseWheelEvent, PointerDownEvent, PointerMoveEvent, PointerOutEvent, - PointerOverEvent, PointerUpEvent, + BlurEvent, ConcreteEvent, FocusEvent, FullscreenChangeEvent, IEvent, IKeyboardEvent, + KeyDownEvent, KeyPressEvent, KeyUpEvent, ModifierKey, MouseWheelEvent, PointerDownEvent, + PointerMoveEvent, PointerOutEvent, PointerOverEvent, PointerUpEvent, }; use stdweb::web::html_element::CanvasElement; -use stdweb::web::{ - document, EventListenerHandle, IChildNode, IElement, IEventTarget, IHtmlElement, -}; +use stdweb::web::{document, EventListenerHandle, IElement, IEventTarget, IHtmlElement}; pub struct Canvas { + /// Note: resizing the CanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. raw: CanvasElement, on_focus: Option, on_blur: Option, @@ -35,19 +35,16 @@ pub struct Canvas { wants_fullscreen: Rc>, } -impl Drop for Canvas { - fn drop(&mut self) { - self.raw.remove(); - } -} - impl Canvas { - pub fn create() -> Result { - let canvas: CanvasElement = document() - .create_element("canvas") - .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? - .try_into() - .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?; + pub fn create(attr: PlatformSpecificWindowBuilderAttributes) -> Result { + let canvas = match attr.canvas { + Some(canvas) => canvas, + None => document() + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .try_into() + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?, + }; // A tabindex is needed in order to capture local keyboard events. // A "0" value means that the element should be focusable in @@ -82,23 +79,20 @@ impl Canvas { .expect(&format!("Set attribute: {}", attribute)); } - pub fn position(&self) -> (f64, f64) { + pub fn position(&self) -> LogicalPosition { let bounds = self.raw.get_bounding_client_rect(); - (bounds.get_x(), bounds.get_y()) - } - - pub fn width(&self) -> f64 { - self.raw.width() as f64 - } - - pub fn height(&self) -> f64 { - self.raw.height() as f64 + LogicalPosition { + x: bounds.get_x(), + y: bounds.get_y(), + } } - pub fn set_size(&self, size: LogicalSize) { - self.raw.set_width(size.width as u32); - self.raw.set_height(size.height as u32); + pub fn size(&self) -> PhysicalSize { + PhysicalSize { + width: self.raw.width() as u32, + height: self.raw.height() as u32, + } } pub fn raw(&self) -> &CanvasElement { @@ -128,10 +122,11 @@ impl Canvas { F: 'static + FnMut(ScanCode, Option, ModifiersState), { self.on_keyboard_release = Some(self.add_user_event(move |event: KeyUpEvent| { + event.prevent_default(); handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); })); } @@ -141,10 +136,21 @@ impl Canvas { F: 'static + FnMut(ScanCode, Option, ModifiersState), { self.on_keyboard_press = Some(self.add_user_event(move |event: KeyDownEvent| { + // event.prevent_default() would suppress subsequent on_received_character() calls. That + // supression is correct for key sequences like Tab/Shift-Tab, Ctrl+R, PgUp/Down to + // scroll, etc. We should not do it for key sequences that result in meaningful character + // input though. + let event_key = &event.key(); + let is_key_string = event_key.len() == 1 || !event_key.is_ascii(); + let is_shortcut_modifiers = (event.ctrl_key() || event.alt_key()) + && !event.get_modifier_state(ModifierKey::AltGr); + if !is_key_string || is_shortcut_modifiers { + event.prevent_default(); + } handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); })); } @@ -159,75 +165,79 @@ impl Canvas { // viable/compatible alternative as of now. `beforeinput` is still widely // unsupported. self.on_received_character = Some(self.add_user_event(move |event: KeyPressEvent| { - handler(event::codepoint(&event)); + // Supress further handling to stop keys like the space key from scrolling the page. + event.prevent_default(); + handler(utils::codepoint(&event)); })); } pub fn on_cursor_leave(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_leave = Some(self.add_event(move |event: PointerOutEvent| { - handler(event.pointer_id()); + self.on_cursor_leave = Some(self.add_event(move |_event: PointerOutEvent| { + handler(); })); } pub fn on_cursor_enter(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_enter = Some(self.add_event(move |event: PointerOverEvent| { - handler(event.pointer_id()); + self.on_cursor_enter = Some(self.add_event(move |_event: PointerOverEvent| { + handler(); })); } pub fn on_mouse_release(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, MouseButton), { self.on_mouse_release = Some(self.add_user_event(move |event: PointerUpEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + handler(event.pointer_id(), utils::mouse_button(&event)); })); } pub fn on_mouse_press(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), { + let canvas = self.raw.clone(); self.on_mouse_press = Some(self.add_user_event(move |event: PointerDownEvent| { handler( event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor()), event::mouse_button(&event), event::mouse_modifiers(&event), ); + canvas + .set_pointer_capture(event.pointer_id()) + .expect("Failed to set pointer capture"); })); } pub fn on_cursor_move(&mut self, mut handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(PhysicalPosition, ModifiersState), { + // todo self.on_cursor_move = Some(self.add_event(move |event: PointerMoveEvent| { handler( event.pointer_id(), - event::mouse_position(&event), - event::mouse_modifiers(&event), + utils::mouse_position(&event).to_physical(super::scale_factor()), + utils::mouse_modifiers(&event), ); })); } pub fn on_mouse_wheel(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + F: 'static + FnMut(i32, (f64, f64)), { self.on_mouse_wheel = Some(self.add_event(move |event: MouseWheelEvent| { - if let Some(delta) = event::mouse_scroll_delta(&event) { - handler(0, delta, event::mouse_modifiers(&event)); - } + event.prevent_default(); + let delta = utils::mouse_scroll_delta(&event); + handler(0, delta); })); } @@ -238,6 +248,22 @@ impl Canvas { self.on_fullscreen_change = Some(self.add_event(move |_: FullscreenChangeEvent| handler())); } + pub fn on_dark_mode(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + // TODO: upstream to stdweb + js! { + var handler = @{handler}; + + if (window.matchMedia) { + window.matchMedia("(prefers-color-scheme: dark)").addListener(function(e) { + handler(event.matches) + }); + } + } + } + fn add_event(&self, mut handler: F) -> EventListenerHandle where E: ConcreteEvent, @@ -279,4 +305,8 @@ impl Canvas { pub fn is_fullscreen(&self) -> bool { super::is_fullscreen(&self.raw) } + + pub fn remove_listeners(&mut self) { + // TODO: Stub, unimplemented (see web_sys for reference). + } } diff --git a/src/platform_impl/web/stdweb/gamepad.rs b/src/platform_impl/web/stdweb/gamepad.rs new file mode 100644 index 0000000000..f54d0c1528 --- /dev/null +++ b/src/platform_impl/web/stdweb/gamepad.rs @@ -0,0 +1,82 @@ +use super::utils; +use crate::platform_impl::platform::device; +use std::cmp::PartialEq; +use stdweb::js; + +#[derive(Debug)] +pub struct Gamepad { + pub(crate) index: i32, + pub(crate) raw: stdweb::web::Gamepad, + pub(crate) mapping: device::gamepad::Mapping, +} + +impl Gamepad { + pub fn new(raw: stdweb::web::Gamepad) -> Self { + let mapping = utils::create_mapping(&raw); + + Self { + index: raw.index(), + raw, + mapping, + } + } + + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn index(&self) -> i32 { + self.raw.index() + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn id(&self) -> String { + self.raw.id() + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + self.raw.connected() + } + + // EXPERIMENTAL + #[allow(dead_code)] + pub fn vibrate(&self, value: f64, duration: f64) { + let index = self.index; + js! { + const gamepads = navigator.getGamepads(); + let gamepad = null; + for (let i = 0; i < gamepads.length; i++) { + if (gamepads[i] && gamepads[i].index == @{index}) { + gamepad = gamepads[i]; + break + } + } + if (!gamepad || !gamepad.hapticActuators) return; + for (let i = 0; i < gamepad.hapticActuators.length; i++) { + const actuator = gamepad.hapticActuators[i]; + if (actuator && actuator.type === "vibration") { + actuator.pulse(@{value}, @{duration}); + } + } + } + } +} + +impl Clone for Gamepad { + fn clone(&self) -> Self { + Self { + index: self.index, + raw: self.raw.clone(), + mapping: self.mapping.clone(), + } + } +} + +impl PartialEq for Gamepad { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.raw.index() == othr.raw.index() + } +} diff --git a/src/platform_impl/web/stdweb/mod.rs b/src/platform_impl/web/stdweb/mod.rs index d49dbf02ac..5e4d67043b 100644 --- a/src/platform_impl/web/stdweb/mod.rs +++ b/src/platform_impl/web/stdweb/mod.rs @@ -1,15 +1,22 @@ +#![deprecated(since = "0.23.0", note = "Please migrate to web-sys over stdweb")] + mod canvas; -mod event; +pub mod gamepad; +mod scaling; mod timeout; +mod utils; +pub mod window; pub use self::canvas::Canvas; -pub use self::timeout::Timeout; +pub use self::scaling::ScaleChangeDetector; +pub use self::timeout::{AnimationFrameRequest, Timeout}; -use crate::dpi::LogicalSize; +use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtStdweb; use crate::window::Window; use stdweb::js; +use stdweb::unstable::TryInto; use stdweb::web::event::BeforeUnloadEvent; use stdweb::web::window; use stdweb::web::IEventTarget; @@ -23,7 +30,9 @@ pub fn exit_fullscreen() { document().exit_fullscreen(); } -pub fn on_unload(mut handler: impl FnMut() + 'static) { +pub type UnloadEventHandle = (); + +pub fn on_unload(mut handler: impl FnMut() + 'static) -> UnloadEventHandle { window().add_event_listener(move |_: BeforeUnloadEvent| handler()); } @@ -31,9 +40,18 @@ impl WindowExtStdweb for Window { fn canvas(&self) -> CanvasElement { self.window.canvas().raw().clone() } + + fn is_dark_mode(&self) -> bool { + // TODO: upstream to stdweb + let is_dark_mode = js! { + return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) + }; + + is_dark_mode.try_into().expect("should return a bool") + } } -pub fn window_size() -> LogicalSize { +pub fn window_size() -> LogicalSize { let window = window(); let width = window.inner_width() as f64; let height = window.inner_height() as f64; @@ -41,6 +59,30 @@ pub fn window_size() -> LogicalSize { LogicalSize { width, height } } +pub fn scale_factor() -> f64 { + let window = window(); + window.device_pixel_ratio() +} + +pub fn set_canvas_size(raw: &CanvasElement, size: Size) { + let scale_factor = scale_factor(); + + let physical_size = size.to_physical::(scale_factor); + let logical_size = size.to_logical::(scale_factor); + + raw.set_width(physical_size.width); + raw.set_height(physical_size.height); + + set_canvas_style_property(raw, "width", &format!("{}px", logical_size.width)); + set_canvas_style_property(raw, "height", &format!("{}px", logical_size.height)); +} + +pub fn set_canvas_style_property(raw: &CanvasElement, style_attribute: &str, value: &str) { + js! { + @{raw.as_ref()}.style[@{style_attribute}] = @{value}; + } +} + pub fn is_fullscreen(canvas: &CanvasElement) -> bool { match document().fullscreen_element() { Some(elem) => { @@ -50,3 +92,11 @@ pub fn is_fullscreen(canvas: &CanvasElement) -> bool { None => false, } } + +pub fn get_gamepads() -> impl Iterator { + stdweb::web::Gamepad::get_all() + .into_iter() + .filter_map(|gamepad| gamepad.map(|gamepad| gamepad::Gamepad::new(gamepad))) +} + +pub type RawCanvasType = CanvasElement; diff --git a/src/platform_impl/web/stdweb/scaling.rs b/src/platform_impl/web/stdweb/scaling.rs new file mode 100644 index 0000000000..28024735f1 --- /dev/null +++ b/src/platform_impl/web/stdweb/scaling.rs @@ -0,0 +1,13 @@ +use super::super::ScaleChangeArgs; + +pub struct ScaleChangeDetector(()); + +impl ScaleChangeDetector { + pub(crate) fn new(_handler: F) -> Self + where + F: 'static + FnMut(ScaleChangeArgs), + { + // TODO: Stub, unimplemented (see web_sys for reference). + Self(()) + } +} diff --git a/src/platform_impl/web/stdweb/timeout.rs b/src/platform_impl/web/stdweb/timeout.rs index fceb111374..72bfb72bb1 100644 --- a/src/platform_impl/web/stdweb/timeout.rs +++ b/src/platform_impl/web/stdweb/timeout.rs @@ -1,9 +1,11 @@ +use std::cell::Cell; +use std::rc::Rc; use std::time::Duration; -use stdweb::web::{window, IWindowOrWorker, TimeoutHandle}; +use stdweb::web::{window, IWindowOrWorker, RequestAnimationFrameHandle, TimeoutHandle}; #[derive(Debug)] pub struct Timeout { - handle: TimeoutHandle, + handle: Option, } impl Timeout { @@ -12,14 +14,50 @@ impl Timeout { F: 'static + FnMut(), { Timeout { - handle: window().set_clearable_timeout(f, duration.as_millis() as u32), + handle: Some(window().set_clearable_timeout(f, duration.as_millis() as u32)), } } } impl Drop for Timeout { fn drop(&mut self) { - let handle = std::mem::replace(&mut self.handle, unsafe { std::mem::uninitialized() }); + let handle = self.handle.take().unwrap(); handle.clear(); } } + +#[derive(Debug)] +pub struct AnimationFrameRequest { + handle: Option, + // track callback state, because `cancelAnimationFrame` is slow + fired: Rc>, +} + +impl AnimationFrameRequest { + pub fn new(mut f: F) -> AnimationFrameRequest + where + F: 'static + FnMut(), + { + let fired = Rc::new(Cell::new(false)); + let c_fired = fired.clone(); + let handle = window().request_animation_frame(move |_| { + (*c_fired).set(true); + f(); + }); + + AnimationFrameRequest { + handle: Some(handle), + fired, + } + } +} + +impl Drop for AnimationFrameRequest { + fn drop(&mut self) { + if !(*self.fired).get() { + if let Some(handle) = self.handle.take() { + handle.cancel(); + } + } + } +} diff --git a/src/platform_impl/web/stdweb/event.rs b/src/platform_impl/web/stdweb/utils.rs similarity index 80% rename from src/platform_impl/web/stdweb/event.rs rename to src/platform_impl/web/stdweb/utils.rs index 14456dc054..913f697995 100644 --- a/src/platform_impl/web/stdweb/event.rs +++ b/src/platform_impl/web/stdweb/utils.rs @@ -1,7 +1,10 @@ use crate::dpi::LogicalPosition; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; -use stdweb::web::event::{IKeyboardEvent, IMouseEvent, MouseWheelDeltaMode, MouseWheelEvent}; +use stdweb::web::{ + event::{IKeyboardEvent, IMouseEvent, MouseWheelEvent}, + Gamepad, GamepadMappingType, +}; use stdweb::{js, unstable::TryInto, JsSerialize}; pub fn mouse_button(event: &impl IMouseEvent) -> MouseButton { @@ -15,30 +18,26 @@ pub fn mouse_button(event: &impl IMouseEvent) -> MouseButton { } pub fn mouse_modifiers(event: &impl IMouseEvent) -> ModifiersState { - ModifiersState { - shift: event.shift_key(), - ctrl: event.ctrl_key(), - alt: event.alt_key(), - logo: event.meta_key(), - } + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, event.shift_key()); + m.set(ModifiersState::CTRL, event.ctrl_key()); + m.set(ModifiersState::ALT, event.alt_key()); + m.set(ModifiersState::LOGO, event.meta_key()); + m } -pub fn mouse_position(event: &impl IMouseEvent) -> LogicalPosition { +pub fn mouse_position(event: &impl IMouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x() as f64, y: event.offset_y() as f64, } } -pub fn mouse_scroll_delta(event: &MouseWheelEvent) -> Option { +pub fn mouse_scroll_delta(event: &MouseWheelEvent) -> (f64, f64) { let x = event.delta_x(); - let y = event.delta_y(); + let y = -event.delta_y(); - match event.delta_mode() { - MouseWheelDeltaMode::Line => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), - MouseWheelDeltaMode::Pixel => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), - MouseWheelDeltaMode::Page => None, - } + (x, y); } pub fn scan_code(event: &T) -> ScanCode { @@ -143,7 +142,7 @@ pub fn virtual_key_code(event: &impl IKeyboardEvent) -> Option { "Numpad9" => VirtualKeyCode::Numpad9, "AbntC1" => VirtualKeyCode::AbntC1, "AbntC2" => VirtualKeyCode::AbntC2, - "NumpadAdd" => VirtualKeyCode::Add, + "NumpadAdd" => VirtualKeyCode::NumpadAdd, "Quote" => VirtualKeyCode::Apostrophe, "Apps" => VirtualKeyCode::Apps, "At" => VirtualKeyCode::At, @@ -154,8 +153,8 @@ pub fn virtual_key_code(event: &impl IKeyboardEvent) -> Option { "Semicolon" => VirtualKeyCode::Semicolon, "Comma" => VirtualKeyCode::Comma, "Convert" => VirtualKeyCode::Convert, - "NumpadDecimal" => VirtualKeyCode::Decimal, - "NumpadDivide" => VirtualKeyCode::Divide, + "NumpadDecimal" => VirtualKeyCode::NumpadDecimal, + "NumpadDivide" => VirtualKeyCode::NumpadDivide, "Equal" => VirtualKeyCode::Equals, "Backquote" => VirtualKeyCode::Grave, "Kana" => VirtualKeyCode::Kana, @@ -169,7 +168,7 @@ pub fn virtual_key_code(event: &impl IKeyboardEvent) -> Option { "MediaSelect" => VirtualKeyCode::MediaSelect, "MediaStop" => VirtualKeyCode::MediaStop, "Minus" => VirtualKeyCode::Minus, - "NumpadMultiply" => VirtualKeyCode::Multiply, + "NumpadMultiply" => VirtualKeyCode::NumpadMultiply, "Mute" => VirtualKeyCode::Mute, "LaunchMyComputer" => VirtualKeyCode::MyComputer, "NavigateForward" => VirtualKeyCode::NavigateForward, @@ -192,7 +191,7 @@ pub fn virtual_key_code(event: &impl IKeyboardEvent) -> Option { "Slash" => VirtualKeyCode::Slash, "Sleep" => VirtualKeyCode::Sleep, "Stop" => VirtualKeyCode::Stop, - "NumpadSubtract" => VirtualKeyCode::Subtract, + "NumpadSubtract" => VirtualKeyCode::NumpadSubtract, "Sysrq" => VirtualKeyCode::Sysrq, "Tab" => VirtualKeyCode::Tab, "Underline" => VirtualKeyCode::Underline, @@ -213,12 +212,12 @@ pub fn virtual_key_code(event: &impl IKeyboardEvent) -> Option { } pub fn keyboard_modifiers(event: &impl IKeyboardEvent) -> ModifiersState { - ModifiersState { - shift: event.shift_key(), - ctrl: event.ctrl_key(), - alt: event.alt_key(), - logo: event.meta_key(), - } + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, event.shift_key()); + m.set(ModifiersState::CTRL, event.ctrl_key()); + m.set(ModifiersState::ALT, event.alt_key()); + m.set(ModifiersState::LOGO, event.meta_key()); + m } pub fn codepoint(event: &impl IKeyboardEvent) -> char { @@ -227,3 +226,31 @@ pub fn codepoint(event: &impl IKeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } + +pub fn create_mapping(raw: &Gamepad) -> gamepad::Mapping { + match raw.mapping() { + GamepadMappingType::Standard => { + let mut buttons = [false; 16]; + let mut axes = [0.0; 6]; + + for (index, button) in raw.buttons().into_iter().enumerate().take(buttons.len()) { + buttons[index] = button.pressed(); + } + + for (index, axis) in raw.axes().into_iter().enumerate().take(axes.len()) { + axes[index] = axis; + } + + gamepad::Mapping::Standard { buttons, axes } + } + _ => { + let buttons = raw + .buttons() + .into_iter() + .map(|button| button.pressed()) + .collect(); + let axes = raw.axes(); + gamepad::Mapping::NoMapping { buttons, axes } + } + } +} diff --git a/src/platform_impl/web/stdweb/windows.rs b/src/platform_impl/web/stdweb/windows.rs new file mode 100644 index 0000000000..6fc5c5a8d5 --- /dev/null +++ b/src/platform_impl/web/stdweb/windows.rs @@ -0,0 +1,77 @@ +use super::gamepad; +use crate::error::OsError as RootOE; +use std::{cell::RefCell, rc::Rc}; +use stdweb::web; +use stdweb::web::{event::IGamepadEvent, IEventTarget}; + +#[derive(Debug)] +pub struct Shared(pub Rc>); + +#[derive(Debug)] +pub struct Window { + raw: web::Window, + on_gamepad_connected: Option, + on_gamepad_disconnected: Option, +} + +impl Shared { + pub fn create() -> Result { + let global = Window::create()?; + Ok(Shared(Rc::new(RefCell::new(global)))) + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +impl Window { + pub fn create() -> Result { + let raw = stdweb::web::window(); + + Ok(Window { + raw, + on_gamepad_connected: None, + on_gamepad_disconnected: None, + }) + } + + pub fn on_gamepad_connected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + move |event: stdweb::web::event::GamepadConnectedEvent| { + let gamepad = event.gamepad(); + handler(gamepad::Gamepad::new(gamepad)); + }, + )); + } + + pub fn on_gamepad_disconnected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + move |event: stdweb::web::event::GamepadDisconnectedEvent| { + let gamepad = event.gamepad(); + handler(gamepad::Gamepad::new(gamepad)); + }, + )); + } + + fn add_event(&self, mut handler: F) -> web::EventListenerHandle + where + E: web::event::ConcreteEvent, + F: 'static + FnMut(E), + { + self.raw.add_event_listener(move |event: E| { + event.stop_propagation(); + event.cancel_bubble(); + + handler(event); + }) + } +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 0543055eb6..94b1d8307a 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,51 +1,60 @@ -use super::event; -use crate::dpi::{LogicalPosition, LogicalSize}; +use super::event_handle::EventListenerHandle; +use super::media_query_handle::MediaQueryListHandle; +use super::utils; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; -use crate::platform_impl::OsError; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; +use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; +use web_sys::{ + AddEventListenerOptions, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + MediaQueryListEvent, MouseEvent, WheelEvent, +}; + +mod mouse_handler; +mod pointer_handler; pub struct Canvas { - raw: HtmlCanvasElement, - on_focus: Option>, - on_blur: Option>, - on_keyboard_release: Option>, - on_keyboard_press: Option>, - on_received_character: Option>, - on_cursor_leave: Option>, - on_cursor_enter: Option>, - on_cursor_move: Option>, - on_mouse_press: Option>, - on_mouse_release: Option>, - on_mouse_wheel: Option>, - on_fullscreen_change: Option>, - wants_fullscreen: Rc>, + common: Common, + on_focus: Option>, + on_blur: Option>, + on_keyboard_release: Option>, + on_keyboard_press: Option>, + on_received_character: Option>, + on_mouse_wheel: Option>, + on_fullscreen_change: Option>, + on_dark_mode: Option, + mouse_state: MouseState, } -impl Drop for Canvas { - fn drop(&mut self) { - self.raw.remove(); - } +struct Common { + /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. + raw: HtmlCanvasElement, + wants_fullscreen: Rc>, } impl Canvas { - pub fn create() -> Result { - let window = - web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; - - let document = window - .document() - .ok_or(os_error!(OsError("Failed to obtain document".to_owned())))?; - - let canvas: HtmlCanvasElement = document - .create_element("canvas") - .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? - .unchecked_into(); + pub fn create(attr: PlatformSpecificWindowBuilderAttributes) -> Result { + let canvas = match attr.canvas { + Some(canvas) => canvas, + None => { + let window = web_sys::window() + .ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + + let document = window + .document() + .ok_or(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() + } + }; // A tabindex is needed in order to capture local keyboard events. // A "0" value means that the element should be focusable in @@ -56,58 +65,61 @@ impl Canvas { .set_attribute("tabindex", "0") .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + let mouse_state = if has_pointer_event() { + MouseState::HasPointerEvent(pointer_handler::PointerHandler::new()) + } else { + MouseState::NoPointerEvent(mouse_handler::MouseHandler::new()) + }; + Ok(Canvas { - raw: canvas, + common: Common { + raw: canvas, + wants_fullscreen: Rc::new(RefCell::new(false)), + }, on_blur: None, on_focus: None, on_keyboard_release: None, on_keyboard_press: None, on_received_character: None, - on_cursor_leave: None, - on_cursor_enter: None, - on_cursor_move: None, - on_mouse_release: None, - on_mouse_press: None, on_mouse_wheel: None, on_fullscreen_change: None, - wants_fullscreen: Rc::new(RefCell::new(false)), + on_dark_mode: None, + mouse_state, }) } pub fn set_attribute(&self, attribute: &str, value: &str) { - self.raw + self.common + .raw .set_attribute(attribute, value) .expect(&format!("Set attribute: {}", attribute)); } - pub fn position(&self) -> (f64, f64) { - let bounds = self.raw.get_bounding_client_rect(); + pub fn position(&self) -> LogicalPosition { + let bounds = self.common.raw.get_bounding_client_rect(); - (bounds.x(), bounds.y()) + LogicalPosition { + x: bounds.x(), + y: bounds.y(), + } } - pub fn width(&self) -> f64 { - self.raw.width() as f64 - } - - pub fn height(&self) -> f64 { - self.raw.height() as f64 - } - - pub fn set_size(&self, size: LogicalSize) { - self.raw.set_width(size.width as u32); - self.raw.set_height(size.height as u32); + pub fn size(&self) -> PhysicalSize { + PhysicalSize { + width: self.common.raw.width(), + height: self.common.raw.height(), + } } pub fn raw(&self) -> &HtmlCanvasElement { - &self.raw + &self.common.raw } pub fn on_blur(&mut self, mut handler: F) where F: 'static + FnMut(), { - self.on_blur = Some(self.add_event("blur", move |_: FocusEvent| { + self.on_blur = Some(self.common.add_event("blur", move |_: FocusEvent| { handler(); })); } @@ -116,7 +128,7 @@ impl Canvas { where F: 'static + FnMut(), { - self.on_focus = Some(self.add_event("focus", move |_: FocusEvent| { + self.on_focus = Some(self.common.add_event("focus", move |_: FocusEvent| { handler(); })); } @@ -125,28 +137,44 @@ impl Canvas { where F: 'static + FnMut(ScanCode, Option, ModifiersState), { - self.on_keyboard_release = - Some(self.add_user_event("keyup", move |event: KeyboardEvent| { + self.on_keyboard_release = Some(self.common.add_user_event( + "keyup", + move |event: KeyboardEvent| { + event.prevent_default(); handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); - })); + }, + )); } pub fn on_keyboard_press(&mut self, mut handler: F) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { - self.on_keyboard_press = - Some(self.add_user_event("keydown", move |event: KeyboardEvent| { + self.on_keyboard_press = Some(self.common.add_user_event( + "keydown", + move |event: KeyboardEvent| { + // event.prevent_default() would suppress subsequent on_received_character() calls. That + // supression is correct for key sequences like Tab/Shift-Tab, Ctrl+R, PgUp/Down to + // scroll, etc. We should not do it for key sequences that result in meaningful character + // input though. + let event_key = &event.key(); + let is_key_string = event_key.len() == 1 || !event_key.is_ascii(); + let is_shortcut_modifiers = + (event.ctrl_key() || event.alt_key()) && !event.get_modifier_state("AltGr"); + if !is_key_string || is_shortcut_modifiers { + event.prevent_default(); + } handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); - })); + }, + )); } pub fn on_received_character(&mut self, mut handler: F) @@ -158,84 +186,74 @@ impl Canvas { // The `keypress` event is deprecated, but there does not seem to be a // viable/compatible alternative as of now. `beforeinput` is still widely // unsupported. - self.on_received_character = Some(self.add_user_event( + self.on_received_character = Some(self.common.add_user_event( "keypress", move |event: KeyboardEvent| { - handler(event::codepoint(&event)); + // Supress further handling to stop keys like the space key from scrolling the page. + event.prevent_default(); + handler(utils::codepoint(&event)); }, )); } - pub fn on_cursor_leave(&mut self, mut handler: F) + pub fn on_cursor_leave(&mut self, handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_leave = Some(self.add_event("pointerout", move |event: PointerEvent| { - handler(event.pointer_id()); - })); + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.on_cursor_leave(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_cursor_leave(&self.common, handler), + } } - pub fn on_cursor_enter(&mut self, mut handler: F) + pub fn on_cursor_enter(&mut self, handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_enter = Some(self.add_event("pointerover", move |event: PointerEvent| { - handler(event.pointer_id()); - })); + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.on_cursor_enter(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_cursor_enter(&self.common, handler), + } } - pub fn on_mouse_release(&mut self, mut handler: F) + pub fn on_mouse_release(&mut self, handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, MouseButton), { - self.on_mouse_release = Some(self.add_user_event( - "pointerup", - move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); - }, - )); + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.on_mouse_release(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_mouse_release(&self.common, handler), + } } - pub fn on_mouse_press(&mut self, mut handler: F) + pub fn on_mouse_press(&mut self, handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), { - self.on_mouse_press = Some(self.add_user_event( - "pointerdown", - move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); - }, - )); + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.on_mouse_press(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_mouse_press(&self.common, handler), + } } - pub fn on_cursor_move(&mut self, mut handler: F) + pub fn on_cursor_move(&mut self, handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(PhysicalPosition, ModifiersState), { - self.on_cursor_move = Some(self.add_event("pointermove", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_position(&event), - event::mouse_modifiers(&event), - ); - })); + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.on_cursor_move(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_cursor_move(&self.common, handler), + } } pub fn on_mouse_wheel(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + F: 'static + FnMut(i32, (f64, f64)), { - self.on_mouse_wheel = Some(self.add_event("wheel", move |event: WheelEvent| { - if let Some(delta) = event::mouse_scroll_delta(&event) { - handler(0, delta, event::mouse_modifiers(&event)); + self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { + event.prevent_default(); + if let delta = utils::mouse_scroll_delta(&event) { + handler(0, delta); } })); } @@ -244,11 +262,54 @@ impl Canvas { where F: 'static + FnMut(), { - self.on_fullscreen_change = - Some(self.add_event("fullscreenchange", move |_: Event| handler())); + self.on_fullscreen_change = Some( + self.common + .add_event("fullscreenchange", move |_: Event| handler()), + ); } - fn add_event(&self, event_name: &str, mut handler: F) -> Closure + pub fn on_dark_mode(&mut self, mut handler: F) + where + F: 'static + FnMut(bool), + { + let closure = + Closure::wrap( + Box::new(move |event: MediaQueryListEvent| handler(event.matches())) + as Box, + ); + self.on_dark_mode = MediaQueryListHandle::new("(prefers-color-scheme: dark)", closure); + } + + pub fn request_fullscreen(&self) { + self.common.request_fullscreen() + } + + pub fn is_fullscreen(&self) -> bool { + self.common.is_fullscreen() + } + + pub fn remove_listeners(&mut self) { + self.on_focus = None; + self.on_blur = None; + self.on_keyboard_release = None; + self.on_keyboard_press = None; + self.on_received_character = None; + self.on_mouse_wheel = None; + self.on_fullscreen_change = None; + self.on_dark_mode = None; + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => h.remove_listeners(), + MouseState::NoPointerEvent(h) => h.remove_listeners(), + } + } +} + +impl Common { + fn add_event( + &self, + event_name: &'static str, + mut handler: F, + ) -> EventListenerHandle where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), @@ -263,17 +324,19 @@ impl Canvas { handler(event); }) as Box); - self.raw - .add_event_listener_with_callback(event_name, &closure.as_ref().unchecked_ref()) - .expect("Failed to add event listener with callback"); + let listener = EventListenerHandle::new(&self.raw, event_name, closure); - closure + listener } // 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.) - fn add_user_event(&self, event_name: &str, mut handler: F) -> Closure + fn add_user_event( + &self, + event_name: &'static str, + mut handler: F, + ) -> EventListenerHandle where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), @@ -293,6 +356,43 @@ impl Canvas { }) } + // This function is used exclusively for mouse events (not pointer events). + // Due to the need for mouse capturing, the mouse event handlers are added + // to the window instead of the canvas element, which requires special + // handling to control event propagation. + fn add_window_mouse_event( + &self, + event_name: &'static str, + mut handler: F, + ) -> EventListenerHandle + where + F: 'static + FnMut(MouseEvent), + { + let wants_fullscreen = self.wants_fullscreen.clone(); + let canvas = self.raw.clone(); + let window = web_sys::window().expect("Failed to obtain window"); + + let closure = Closure::wrap(Box::new(move |event: MouseEvent| { + handler(event); + + if *wants_fullscreen.borrow() { + canvas + .request_fullscreen() + .expect("Failed to enter fullscreen"); + *wants_fullscreen.borrow_mut() = false; + } + }) as Box); + + let listener = EventListenerHandle::with_options( + &window, + event_name, + closure, + AddEventListenerOptions::new().capture(true), + ); + + listener + } + pub fn request_fullscreen(&self) { *self.wants_fullscreen.borrow_mut() = true; } @@ -301,3 +401,20 @@ impl Canvas { super::is_fullscreen(&self.raw) } } + +enum MouseState { + HasPointerEvent(pointer_handler::PointerHandler), + NoPointerEvent(mouse_handler::MouseHandler), +} + +/// Returns whether pointer events are supported. +/// Used to decide whether to use pointer events +/// or plain mouse events. Note that Safari +/// doesn't support pointer events now. +fn has_pointer_event() -> bool { + if let Some(window) = web_sys::window() { + window.get("PointerEvent").is_some() + } else { + false + } +} diff --git a/src/platform_impl/web/web_sys/canvas/mouse_handler.rs b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs new file mode 100644 index 0000000000..847275eb7d --- /dev/null +++ b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs @@ -0,0 +1,207 @@ +use super::utils; +use super::EventListenerHandle; +use crate::dpi::PhysicalPosition; +use crate::event::{ModifiersState, MouseButton}; + +use std::cell::RefCell; +use std::rc::Rc; + +use web_sys::{EventTarget, MouseEvent}; + +pub(super) struct MouseHandler { + on_mouse_leave: Option>, + on_mouse_enter: Option>, + on_mouse_move: Option>, + on_mouse_press: Option>, + on_mouse_release: Option>, + on_mouse_leave_handler: Rc>>>, + mouse_capture_state: Rc>, +} + +#[derive(PartialEq, Eq)] +pub(super) enum MouseCaptureState { + NotCaptured, + Captured, + OtherElement, +} + +impl MouseHandler { + pub fn new() -> Self { + Self { + on_mouse_leave: None, + on_mouse_enter: None, + on_mouse_move: None, + on_mouse_press: None, + on_mouse_release: None, + on_mouse_leave_handler: Rc::new(RefCell::new(None)), + mouse_capture_state: Rc::new(RefCell::new(MouseCaptureState::NotCaptured)), + } + } + pub fn on_cursor_leave(&mut self, canvas_common: &super::Common, handler: F) + where + F: 'static + FnMut(), + { + *self.on_mouse_leave_handler.borrow_mut() = Some(Box::new(handler)); + let on_mouse_leave_handler = self.on_mouse_leave_handler.clone(); + let mouse_capture_state = self.mouse_capture_state.clone(); + self.on_mouse_leave = Some(canvas_common.add_event("mouseout", move |_: MouseEvent| { + // If the mouse is being captured, it is always considered + // to be "within" the the canvas, until the capture has been + // released, therefore we don't send cursor leave events. + if *mouse_capture_state.borrow() != MouseCaptureState::Captured { + if let Some(handler) = on_mouse_leave_handler.borrow_mut().as_mut() { + handler(); + } + } + })); + } + + pub fn on_cursor_enter(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(), + { + let mouse_capture_state = self.mouse_capture_state.clone(); + self.on_mouse_enter = Some(canvas_common.add_event("mouseover", move |_: MouseEvent| { + // We don't send cursor leave events when the mouse is being + // captured, therefore we do the same with cursor enter events. + if *mouse_capture_state.borrow() != MouseCaptureState::Captured { + handler(); + } + })); + } + + pub fn on_mouse_release(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(i32, MouseButton), + { + let on_mouse_leave_handler = self.on_mouse_leave_handler.clone(); + let mouse_capture_state = self.mouse_capture_state.clone(); + let canvas = canvas_common.raw.clone(); + self.on_mouse_release = Some(canvas_common.add_window_mouse_event( + "mouseup", + move |event: MouseEvent| { + let canvas = canvas.clone(); + let mut mouse_capture_state = mouse_capture_state.borrow_mut(); + match &*mouse_capture_state { + // Shouldn't happen but we'll just ignore it. + MouseCaptureState::NotCaptured => return, + MouseCaptureState::OtherElement => { + if event.buttons() == 0 { + // No buttons are pressed anymore so reset + // the capturing state. + *mouse_capture_state = MouseCaptureState::NotCaptured; + } + return; + } + MouseCaptureState::Captured => {} + } + event.stop_propagation(); + handler(0, utils::mouse_button(&event)); + if event + .target() + .map_or(false, |target| target != EventTarget::from(canvas)) + { + // Since we do not send cursor leave events while the + // cursor is being captured, we instead send it after + // the capture has been released. + if let Some(handler) = on_mouse_leave_handler.borrow_mut().as_mut() { + handler(); + } + } + if event.buttons() == 0 { + // No buttons are pressed anymore so reset + // the capturing state. + *mouse_capture_state = MouseCaptureState::NotCaptured; + } + }, + )); + } + + pub fn on_mouse_press(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), + { + let mouse_capture_state = self.mouse_capture_state.clone(); + let canvas = canvas_common.raw.clone(); + self.on_mouse_press = Some(canvas_common.add_window_mouse_event( + "mousedown", + move |event: MouseEvent| { + let canvas = canvas.clone(); + let mut mouse_capture_state = mouse_capture_state.borrow_mut(); + match &*mouse_capture_state { + MouseCaptureState::NotCaptured + if event + .target() + .map_or(false, |target| target != EventTarget::from(canvas)) => + { + // The target isn't our canvas which means the + // mouse is pressed outside of it. + *mouse_capture_state = MouseCaptureState::OtherElement; + return; + } + MouseCaptureState::OtherElement => return, + _ => {} + } + *mouse_capture_state = MouseCaptureState::Captured; + event.stop_propagation(); + handler( + 0, + utils::mouse_position(&event).to_physical(super::super::scale_factor()), + utils::mouse_button(&event), + utils::mouse_modifiers(&event), + ); + }, + )); + } + + pub fn on_cursor_move(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(PhysicalPosition, ModifiersState), + { + let mouse_capture_state = self.mouse_capture_state.clone(); + let canvas = canvas_common.raw.clone(); + self.on_mouse_move = Some(canvas_common.add_window_mouse_event( + "mousemove", + move |event: MouseEvent| { + let canvas = canvas.clone(); + let mouse_capture_state = mouse_capture_state.borrow(); + let is_over_canvas = event + .target() + .map_or(false, |target| target == EventTarget::from(canvas.clone())); + match &*mouse_capture_state { + // Don't handle hover events outside of canvas. + MouseCaptureState::NotCaptured if !is_over_canvas => return, + MouseCaptureState::OtherElement if !is_over_canvas => return, + // If hovering over the canvas, just send the cursor move event. + MouseCaptureState::NotCaptured + | MouseCaptureState::OtherElement + | MouseCaptureState::Captured => { + if *mouse_capture_state == MouseCaptureState::Captured { + event.stop_propagation(); + } + let mouse_pos = if is_over_canvas { + utils::mouse_position(&event) + } else { + // Since the mouse is not on the canvas, we cannot + // use `offsetX`/`offsetY`. + utils::mouse_position_by_client(&event, &canvas) + }; + handler( + mouse_pos.to_physical(super::super::scale_factor()), + utils::mouse_modifiers(&event), + ); + } + } + }, + )); + } + + pub fn remove_listeners(&mut self) { + self.on_mouse_leave = None; + self.on_mouse_enter = None; + self.on_mouse_move = None; + self.on_mouse_press = None; + self.on_mouse_release = None; + *self.on_mouse_leave_handler.borrow_mut() = None; + } +} diff --git a/src/platform_impl/web/web_sys/canvas/pointer_handler.rs b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs new file mode 100644 index 0000000000..a31b552b7e --- /dev/null +++ b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs @@ -0,0 +1,106 @@ +use super::utils; +use super::EventListenerHandle; +use crate::dpi::PhysicalPosition; +use crate::event::{ModifiersState, MouseButton}; + +use web_sys::PointerEvent; + +pub(super) struct PointerHandler { + on_cursor_leave: Option>, + on_cursor_enter: Option>, + on_cursor_move: Option>, + on_pointer_press: Option>, + on_pointer_release: Option>, +} + +impl PointerHandler { + pub fn new() -> Self { + Self { + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_pointer_press: None, + on_pointer_release: None, + } + } + + pub fn on_cursor_leave(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_cursor_leave = Some(canvas_common.add_event( + "pointerout", + move |event: PointerEvent| { + handler(); + }, + )); + } + + pub fn on_cursor_enter(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_cursor_enter = Some(canvas_common.add_event( + "pointerover", + move |event: PointerEvent| { + handler(); + }, + )); + } + + pub fn on_mouse_release(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(i32, MouseButton), + { + self.on_pointer_release = Some(canvas_common.add_user_event( + "pointerup", + move |event: PointerEvent| { + handler(event.pointer_id(), utils::mouse_button(&event)); + }, + )); + } + + pub fn on_mouse_press(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), + { + let canvas = canvas_common.raw.clone(); + self.on_pointer_press = Some(canvas_common.add_user_event( + "pointerdown", + move |event: PointerEvent| { + handler( + event.pointer_id(), + utils::mouse_position(&event).to_physical(super::super::scale_factor()), + utils::mouse_button(&event), + utils::mouse_modifiers(&event), + ); + canvas + .set_pointer_capture(event.pointer_id()) + .expect("Failed to set pointer capture"); + }, + )); + } + + pub fn on_cursor_move(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(PhysicalPosition, ModifiersState), + { + self.on_cursor_move = Some(canvas_common.add_event( + "pointermove", + move |event: PointerEvent| { + handler( + utils::mouse_position(&event).to_physical(super::super::scale_factor()), + utils::mouse_modifiers(&event), + ); + }, + )); + } + + pub fn remove_listeners(&mut self) { + self.on_cursor_leave = None; + self.on_cursor_enter = None; + self.on_cursor_move = None; + self.on_pointer_press = None; + self.on_pointer_release = None; + } +} diff --git a/src/platform_impl/web/web_sys/event_handle.rs b/src/platform_impl/web/web_sys/event_handle.rs new file mode 100644 index 0000000000..08130d7598 --- /dev/null +++ b/src/platform_impl/web/web_sys/event_handle.rs @@ -0,0 +1,65 @@ +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{AddEventListenerOptions, EventTarget}; + +pub(super) struct EventListenerHandle { + target: EventTarget, + event_type: &'static str, + listener: Closure, +} + +impl EventListenerHandle { + pub fn new(target: &U, event_type: &'static str, listener: Closure) -> Self + where + U: Clone + Into, + { + let target = target.clone().into(); + target + .add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref()) + .expect("Failed to add event listener"); + EventListenerHandle { + target, + event_type, + listener, + } + } + + pub fn with_options( + target: &U, + event_type: &'static str, + listener: Closure, + options: &AddEventListenerOptions, + ) -> Self + where + U: Clone + Into, + { + let target = target.clone().into(); + target + .add_event_listener_with_callback_and_add_event_listener_options( + event_type, + listener.as_ref().unchecked_ref(), + options, + ) + .expect("Failed to add event listener"); + EventListenerHandle { + target, + event_type, + listener, + } + } +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_type, + self.listener.as_ref().unchecked_ref(), + ) + .unwrap_or_else(|e| { + web_sys::console::error_2( + &format!("Error removing event listener {}", self.event_type).into(), + &e, + ) + }); + } +} diff --git a/src/platform_impl/web/web_sys/gamepad.rs b/src/platform_impl/web/web_sys/gamepad.rs new file mode 100644 index 0000000000..922bd645e0 --- /dev/null +++ b/src/platform_impl/web/web_sys/gamepad.rs @@ -0,0 +1,75 @@ +use super::utils; +use crate::platform_impl::platform::device; +use std::cmp::PartialEq; + +#[derive(Debug)] +pub struct Gamepad { + pub(crate) index: i32, + pub(crate) raw: web_sys::Gamepad, + pub(crate) mapping: device::gamepad::Mapping, +} + +impl Gamepad { + pub fn new(raw: web_sys::Gamepad) -> Self { + let mapping = utils::create_mapping(&raw); + + Self { + index: raw.index() as i32, + raw, + mapping, + } + } + + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn index(&self) -> i32 { + self.raw.index() as i32 + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn id(&self) -> String { + self.raw.id() + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + self.raw.connected() + } + + // An array containing GamepadHapticActuator objects, + // each of which represents haptic feedback hardware available on the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators + pub fn vibrate(&self, value: f64, duration: f64) { + for actuator in self.raw.haptic_actuators().values() { + actuator.ok().and_then(|a| { + let actuator: web_sys::GamepadHapticActuator = a.into(); + match actuator.type_() { + web_sys::GamepadHapticActuatorType::Vibration => { + actuator.pulse(value, duration).ok() + } + _ => None, + } + }); + } + } +} + +impl Clone for Gamepad { + fn clone(&self) -> Self { + Self { + index: self.index, + raw: self.raw.clone(), + mapping: self.mapping.clone(), + } + } +} + +impl PartialEq for Gamepad { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.raw.index() == othr.raw.index() + } +} diff --git a/src/platform_impl/web/web_sys/media_query_handle.rs b/src/platform_impl/web/web_sys/media_query_handle.rs new file mode 100644 index 0000000000..c2ab40da77 --- /dev/null +++ b/src/platform_impl/web/web_sys/media_query_handle.rs @@ -0,0 +1,56 @@ +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{MediaQueryList, MediaQueryListEvent}; + +pub(super) struct MediaQueryListHandle { + mql: MediaQueryList, + listener: Option>, +} + +impl MediaQueryListHandle { + pub fn new( + media_query: &str, + listener: Closure, + ) -> Option { + let window = web_sys::window().expect("Failed to obtain window"); + let mql = window + .match_media(media_query) + .ok() + .flatten() + .and_then(|mql| { + mql.add_listener_with_opt_callback(Some(&listener.as_ref().unchecked_ref())) + .map(|_| mql) + .ok() + }); + mql.map(|mql| Self { + mql, + listener: Some(listener), + }) + } + + pub fn mql(&self) -> &MediaQueryList { + &self.mql + } + + /// Removes the listener and returns the original listener closure, which + /// can be reused. + pub fn remove(mut self) -> Closure { + let listener = self.listener.take().unwrap_or_else(|| unreachable!()); + remove_listener(&self.mql, &listener); + listener + } +} + +impl Drop for MediaQueryListHandle { + fn drop(&mut self) { + if let Some(listener) = self.listener.take() { + remove_listener(&self.mql, &listener); + } + } +} + +fn remove_listener(mql: &MediaQueryList, listener: &Closure) { + mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())) + .unwrap_or_else(|e| { + web_sys::console::error_2(&"Error removing media query listener".into(), &e) + }); +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 205519d1ea..9b55642935 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,14 +1,20 @@ mod canvas; -mod event; +mod event_handle; +pub mod gamepad; +mod media_query_handle; +mod scaling; mod timeout; +mod utils; +pub mod window; pub use self::canvas::Canvas; -pub use self::timeout::Timeout; +pub use self::scaling::ScaleChangeDetector; +pub use self::timeout::{AnimationFrameRequest, Timeout}; -use crate::dpi::LogicalSize; +use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtWebSys; use crate::window::Window; -use wasm_bindgen::{closure::Closure, JsCast}; +use wasm_bindgen::closure::Closure; use web_sys::{window, BeforeUnloadEvent, Element, HtmlCanvasElement}; pub fn throw(msg: &str) { @@ -22,25 +28,41 @@ pub fn exit_fullscreen() { document.exit_fullscreen(); } -pub fn on_unload(mut handler: impl FnMut() + 'static) { +pub struct UnloadEventHandle { + _listener: event_handle::EventListenerHandle, +} + +pub fn on_unload(mut handler: impl FnMut() + 'static) -> UnloadEventHandle { let window = web_sys::window().expect("Failed to obtain window"); let closure = Closure::wrap( Box::new(move |_: BeforeUnloadEvent| handler()) as Box ); - window - .add_event_listener_with_callback("beforeunload", &closure.as_ref().unchecked_ref()) - .expect("Failed to add close listener"); + let listener = event_handle::EventListenerHandle::new(&window, "beforeunload", closure); + UnloadEventHandle { + _listener: listener, + } } impl WindowExtWebSys for Window { fn canvas(&self) -> HtmlCanvasElement { self.window.canvas().raw().clone() } + + fn is_dark_mode(&self) -> bool { + let window = web_sys::window().expect("Failed to obtain window"); + + window + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|media| media.matches()) + .unwrap_or(false) + } } -pub fn window_size() -> LogicalSize { +pub fn window_size() -> LogicalSize { let window = web_sys::window().expect("Failed to obtain window"); let width = window .inner_width() @@ -56,6 +78,31 @@ pub fn window_size() -> LogicalSize { LogicalSize { width, height } } +pub fn scale_factor() -> f64 { + let window = web_sys::window().expect("Failed to obtain window"); + window.device_pixel_ratio() +} + +pub fn set_canvas_size(raw: &HtmlCanvasElement, size: Size) { + let scale_factor = scale_factor(); + + let physical_size = size.to_physical::(scale_factor); + let logical_size = size.to_logical::(scale_factor); + + raw.set_width(physical_size.width); + raw.set_height(physical_size.height); + + set_canvas_style_property(raw, "width", &format!("{}px", logical_size.width)); + set_canvas_style_property(raw, "height", &format!("{}px", logical_size.height)); +} + +pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) { + let style = raw.style(); + style + .set_property(property, value) + .expect(&format!("Failed to set {}", property)); +} + pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { let window = window().expect("Failed to obtain window"); let document = window.document().expect("Failed to obtain document"); @@ -68,3 +115,23 @@ pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { None => false, } } + +pub type RawCanvasType = HtmlCanvasElement; + +pub fn get_gamepads() -> impl Iterator { + let mut gamepads: Vec = Vec::new(); + let web_gamepads = web_sys::window() + .unwrap() + .navigator() + .get_gamepads() + .ok() + .unwrap(); + for index in 0..web_gamepads.length() { + let jsvalue = web_gamepads.get(index); + if !jsvalue.is_null() { + let gamepad: web_sys::Gamepad = jsvalue.into(); + gamepads.push(gamepad::Gamepad::new(gamepad)); + } + } + gamepads.into_iter() +} diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs new file mode 100644 index 0000000000..08b52ae306 --- /dev/null +++ b/src/platform_impl/web/web_sys/scaling.rs @@ -0,0 +1,89 @@ +use super::super::ScaleChangeArgs; +use super::media_query_handle::MediaQueryListHandle; + +use std::{cell::RefCell, rc::Rc}; +use wasm_bindgen::prelude::Closure; +use web_sys::MediaQueryListEvent; + +pub struct ScaleChangeDetector(Rc>); + +impl ScaleChangeDetector { + pub(crate) fn new(handler: F) -> Self + where + F: 'static + FnMut(ScaleChangeArgs), + { + Self(ScaleChangeDetectorInternal::new(handler)) + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ScaleChangeDetectorInternal { + callback: Box, + mql: Option, + last_scale: f64, +} + +impl ScaleChangeDetectorInternal { + fn new(handler: F) -> Rc> + where + F: 'static + FnMut(ScaleChangeArgs), + { + let current_scale = super::scale_factor(); + let new_self = Rc::new(RefCell::new(Self { + callback: Box::new(handler), + mql: None, + last_scale: current_scale, + })); + + let weak_self = Rc::downgrade(&new_self); + let closure = Closure::wrap(Box::new(move |event: MediaQueryListEvent| { + if let Some(rc_self) = weak_self.upgrade() { + rc_self.borrow_mut().handler(event); + } + }) as Box); + + let mql = Self::create_mql(closure); + { + let mut borrowed_self = new_self.borrow_mut(); + borrowed_self.mql = mql; + } + new_self + } + + fn create_mql( + closure: Closure, + ) -> Option { + let current_scale = super::scale_factor(); + // This media query initially matches the current `devicePixelRatio`. + // We add 0.0001 to the lower and upper bounds such that it won't fail + // due to floating point precision limitations. + let media_query = format!( + "(min-resolution: {min_scale:.4}dppx) and (max-resolution: {max_scale:.4}dppx), + (-webkit-min-device-pixel-ratio: {min_scale:.4}) and (-webkit-max-device-pixel-ratio: {max_scale:.4})", + min_scale = current_scale - 0.0001, max_scale= current_scale + 0.0001, + ); + let mql = MediaQueryListHandle::new(&media_query, closure); + if let Some(mql) = &mql { + assert_eq!(mql.mql().matches(), true); + } + mql + } + + fn handler(&mut self, event: MediaQueryListEvent) { + assert_eq!(event.matches(), false); + let mql = self + .mql + .take() + .expect("DevicePixelRatioChangeDetector::mql should not be None"); + let closure = mql.remove(); + let new_scale = super::scale_factor(); + (self.callback)(ScaleChangeArgs { + old_scale: self.last_scale, + new_scale, + }); + let new_mql = Self::create_mql(closure); + self.mql = new_mql; + self.last_scale = new_scale; + } +} diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs index e7ce69a083..e95c54ed51 100644 --- a/src/platform_impl/web/web_sys/timeout.rs +++ b/src/platform_impl/web/web_sys/timeout.rs @@ -1,3 +1,5 @@ +use std::cell::Cell; +use std::rc::Rc; use std::time::Duration; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; @@ -38,3 +40,48 @@ impl Drop for Timeout { window.clear_timeout_with_handle(self.handle); } } + +#[derive(Debug)] +pub struct AnimationFrameRequest { + handle: i32, + // track callback state, because `cancelAnimationFrame` is slow + fired: Rc>, + _closure: Closure, +} + +impl AnimationFrameRequest { + pub fn new(mut f: F) -> AnimationFrameRequest + where + F: 'static + FnMut(), + { + let window = web_sys::window().expect("Failed to obtain window"); + + 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 = window + .request_animation_frame(&closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + AnimationFrameRequest { + handle, + fired, + _closure: closure, + } + } +} + +impl Drop for AnimationFrameRequest { + fn drop(&mut self) { + if !(*self.fired).get() { + let window = web_sys::window().expect("Failed to obtain window"); + window + .cancel_animation_frame(self.handle) + .expect("Failed to cancel animation frame"); + } + } +} diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/utils.rs similarity index 72% rename from src/platform_impl/web/web_sys/event.rs rename to src/platform_impl/web/web_sys/utils.rs index af557b9941..e39c3aed30 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/utils.rs @@ -1,8 +1,12 @@ use crate::dpi::LogicalPosition; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; +use crate::platform_impl::platform; use std::convert::TryInto; -use web_sys::{KeyboardEvent, MouseEvent, WheelEvent}; +use web_sys::{ + Gamepad, GamepadButton, GamepadMappingType, HtmlCanvasElement, KeyboardEvent, MouseEvent, + WheelEvent, +}; pub fn mouse_button(event: &MouseEvent) -> MouseButton { match event.button() { @@ -14,30 +18,37 @@ pub fn mouse_button(event: &MouseEvent) -> MouseButton { } pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState { - ModifiersState { - shift: event.shift_key(), - ctrl: event.ctrl_key(), - alt: event.alt_key(), - logo: event.meta_key(), - } + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, event.shift_key()); + m.set(ModifiersState::CTRL, event.ctrl_key()); + m.set(ModifiersState::ALT, event.alt_key()); + m.set(ModifiersState::LOGO, event.meta_key()); + m } -pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { +pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x() as f64, y: event.offset_y() as f64, } } -pub fn mouse_scroll_delta(event: &WheelEvent) -> Option { +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(event: &WheelEvent) -> (f64, f64) { let x = event.delta_x(); - let y = event.delta_y(); + let y = -event.delta_y(); - match event.delta_mode() { - WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), - WheelEvent::DOM_DELTA_PIXEL => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), - _ => None, - } + (x, y) } pub fn scan_code(event: &KeyboardEvent) -> ScanCode { @@ -141,7 +152,7 @@ pub fn virtual_key_code(event: &KeyboardEvent) -> Option { "Numpad9" => VirtualKeyCode::Numpad9, "AbntC1" => VirtualKeyCode::AbntC1, "AbntC2" => VirtualKeyCode::AbntC2, - "NumpadAdd" => VirtualKeyCode::Add, + "NumpadAdd" => VirtualKeyCode::NumpadAdd, "Quote" => VirtualKeyCode::Apostrophe, "Apps" => VirtualKeyCode::Apps, "At" => VirtualKeyCode::At, @@ -152,8 +163,8 @@ pub fn virtual_key_code(event: &KeyboardEvent) -> Option { "Semicolon" => VirtualKeyCode::Semicolon, "Comma" => VirtualKeyCode::Comma, "Convert" => VirtualKeyCode::Convert, - "NumpadDecimal" => VirtualKeyCode::Decimal, - "NumpadDivide" => VirtualKeyCode::Divide, + "NumpadDecimal" => VirtualKeyCode::NumpadDecimal, + "NumpadDivide" => VirtualKeyCode::NumpadDivide, "Equal" => VirtualKeyCode::Equals, "Backquote" => VirtualKeyCode::Grave, "Kana" => VirtualKeyCode::Kana, @@ -167,7 +178,7 @@ pub fn virtual_key_code(event: &KeyboardEvent) -> Option { "MediaSelect" => VirtualKeyCode::MediaSelect, "MediaStop" => VirtualKeyCode::MediaStop, "Minus" => VirtualKeyCode::Minus, - "NumpadMultiply" => VirtualKeyCode::Multiply, + "NumpadMultiply" => VirtualKeyCode::NumpadMultiply, "Mute" => VirtualKeyCode::Mute, "LaunchMyComputer" => VirtualKeyCode::MyComputer, "NavigateForward" => VirtualKeyCode::NavigateForward, @@ -190,7 +201,7 @@ pub fn virtual_key_code(event: &KeyboardEvent) -> Option { "Slash" => VirtualKeyCode::Slash, "Sleep" => VirtualKeyCode::Sleep, "Stop" => VirtualKeyCode::Stop, - "NumpadSubtract" => VirtualKeyCode::Subtract, + "NumpadSubtract" => VirtualKeyCode::NumpadSubtract, "Sysrq" => VirtualKeyCode::Sysrq, "Tab" => VirtualKeyCode::Tab, "Underline" => VirtualKeyCode::Underline, @@ -211,12 +222,12 @@ pub fn virtual_key_code(event: &KeyboardEvent) -> Option { } pub fn keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState { - ModifiersState { - shift: event.shift_key(), - ctrl: event.ctrl_key(), - alt: event.alt_key(), - logo: event.meta_key(), - } + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, event.shift_key()); + m.set(ModifiersState::CTRL, event.ctrl_key()); + m.set(ModifiersState::ALT, event.alt_key()); + m.set(ModifiersState::LOGO, event.meta_key()); + m } pub fn codepoint(event: &KeyboardEvent) -> char { @@ -225,3 +236,44 @@ pub fn codepoint(event: &KeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } + +pub fn create_mapping(raw: &Gamepad) -> platform::device::gamepad::Mapping { + match raw.mapping() { + GamepadMappingType::Standard => { + let mut buttons = [false; 16]; + let mut axes = [0.0; 6]; + + let gbuttons = raw.buttons(); + for index in 0..buttons.len() { + let button: GamepadButton = gbuttons.get(index as u32).into(); + buttons[index] = button.pressed(); + } + + let gaxes = raw.axes(); + for index in 0..axes.len() { + let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); + axes[index] = axe; + } + + platform::device::gamepad::Mapping::Standard { buttons, axes } + } + _ => { + let mut buttons: Vec = Vec::new(); + let mut axes: Vec = Vec::new(); + + let gbuttons = raw.buttons(); + for index in 0..gbuttons.length() { + let button: GamepadButton = gbuttons.get(index as u32).into(); + buttons.push(button.pressed()); + } + + let gaxes = raw.axes(); + for index in 0..gaxes.length() { + let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); + axes.push(axe); + } + + platform::device::gamepad::Mapping::NoMapping { buttons, axes } + } + } +} diff --git a/src/platform_impl/web/web_sys/window.rs b/src/platform_impl/web/web_sys/window.rs new file mode 100644 index 0000000000..9bb4cee450 --- /dev/null +++ b/src/platform_impl/web/web_sys/window.rs @@ -0,0 +1,88 @@ +use super::gamepad; +use crate::error::OsError as RootOE; +use crate::platform_impl::OsError; +use std::{cell::RefCell, rc::Rc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::GamepadEvent; + +#[derive(Debug)] +pub struct Shared(pub Rc>); + +#[derive(Debug)] +pub struct Window { + raw: web_sys::Window, + on_gamepad_connected: Option>, + on_gamepad_disconnected: Option>, +} + +impl Shared { + pub fn create() -> Result { + let global = Window::create()?; + Ok(Shared(Rc::new(RefCell::new(global)))) + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +impl Window { + pub fn create() -> Result { + let raw = + web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + + Ok(Window { + raw, + on_gamepad_connected: None, + on_gamepad_disconnected: None, + }) + } + + pub fn on_gamepad_connected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + "gamepadconnected", + move |event: GamepadEvent| { + let gamepad = event + .gamepad() + .expect("[gamepadconnected] expected gamepad"); + handler(gamepad::Gamepad::new(gamepad)); + }, + )) + } + + pub fn on_gamepad_disconnected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_disconnected = Some(self.add_event( + "gamepaddisconnected", + move |event: GamepadEvent| { + let gamepad = event + .gamepad() + .expect("[gamepaddisconnected] expected gamepad"); + handler(gamepad::Gamepad::new(gamepad)); + }, + )) + } + + fn add_event(&self, event_name: &str, mut handler: F) -> Closure + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + let closure = Closure::wrap(Box::new(move |event: E| { + handler(event); + }) as Box); + + self.raw + .add_event_listener_with_callback(event_name, &closure.as_ref().unchecked_ref()) + .expect("Failed to add event listener with callback"); + + closure + } +} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 8752d8b639..0d8b436000 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,53 +1,75 @@ -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::event; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMH; -use crate::window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWI}; +use crate::window::{ + CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWI, +}; use raw_window_handle::web::WebHandle; use super::{backend, monitor, EventLoopWindowTarget}; -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::collections::vec_deque::IntoIter as VecDequeIter; use std::collections::VecDeque; +use std::rc::Rc; pub struct Window { - canvas: backend::Canvas, + canvas: Rc>, previous_pointer: RefCell<&'static str>, - position: RefCell, id: Id, register_redraw_request: Box, + resize_notify_fn: Box)>, + destroy_fn: Option>, } impl Window { pub fn new( target: &EventLoopWindowTarget, attr: WindowAttributes, - _: PlatformSpecificBuilderAttributes, + platform_attr: PlatformSpecificBuilderAttributes, ) -> Result { let runner = target.runner.clone(); let id = target.generate_id(); - let mut canvas = backend::Canvas::create()?; + let canvas = backend::Canvas::create(platform_attr)?; + let mut canvas = Rc::new(RefCell::new(canvas)); let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); + target.register_global_events()?; target.register(&mut canvas, id); + let runner = target.runner.clone(); + let resize_notify_fn = Box::new(move |new_size| { + runner.send_event(event::Event::WindowEvent { + window_id: RootWI(id), + event: event::WindowEvent::Resized(new_size), + }); + }); + + let runner = target.runner.clone(); + let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + let window = Window { canvas, previous_pointer: RefCell::new("auto"), - position: RefCell::new(LogicalPosition { x: 0.0, y: 0.0 }), id, register_redraw_request, + resize_notify_fn, + destroy_fn: Some(destroy_fn), }; - window.set_inner_size(attr.inner_size.unwrap_or(LogicalSize { - width: 1024.0, - height: 768.0, - })); + backend::set_canvas_size( + window.canvas.borrow().raw(), + attr.inner_size.unwrap_or(Size::Logical(LogicalSize { + width: 1024.0, + height: 768.0, + })), + ); window.set_title(&attr.title); window.set_maximized(attr.maximized); window.set_visible(attr.visible); @@ -56,12 +78,12 @@ impl Window { Ok(window) } - pub fn canvas(&self) -> &backend::Canvas { - &self.canvas + pub fn canvas<'a>(&'a self) -> Ref<'a, backend::Canvas> { + self.canvas.borrow() } pub fn set_title(&self, title: &str) { - self.canvas.set_attribute("alt", title); + self.canvas.borrow().set_attribute("alt", title); } pub fn set_visible(&self, _visible: bool) { @@ -72,52 +94,56 @@ impl Window { (self.register_redraw_request)(); } - pub fn outer_position(&self) -> Result { - let (x, y) = self.canvas.position(); - - Ok(LogicalPosition { x, y }) + pub fn outer_position(&self) -> Result, NotSupportedError> { + Ok(self + .canvas + .borrow() + .position() + .to_physical(self.scale_factor())) } - pub fn inner_position(&self) -> Result { - Ok(*self.position.borrow()) + pub fn inner_position(&self) -> Result, NotSupportedError> { + // Note: the canvas element has no window decorations, so this is equal to `outer_position`. + self.outer_position() } - pub fn set_outer_position(&self, position: LogicalPosition) { - *self.position.borrow_mut() = position; + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical::(self.scale_factor()); - self.canvas.set_attribute("position", "fixed"); - self.canvas.set_attribute("left", &position.x.to_string()); - self.canvas.set_attribute("top", &position.y.to_string()); + let canvas = self.canvas.borrow(); + canvas.set_attribute("position", "fixed"); + canvas.set_attribute("left", &position.x.to_string()); + canvas.set_attribute("top", &position.y.to_string()); } #[inline] - pub fn inner_size(&self) -> LogicalSize { - LogicalSize { - width: self.canvas.width() as f64, - height: self.canvas.height() as f64, - } + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.borrow().size() } #[inline] - pub fn outer_size(&self) -> LogicalSize { - LogicalSize { - width: self.canvas.width() as f64, - height: self.canvas.height() as f64, - } + pub fn outer_size(&self) -> PhysicalSize { + // Note: the canvas element has no window decorations, so this is equal to `inner_size`. + self.inner_size() } #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { - self.canvas.set_size(size); + pub fn set_inner_size(&self, size: Size) { + let old_size = self.inner_size(); + backend::set_canvas_size(self.canvas.borrow().raw(), size); + let new_size = self.inner_size(); + if old_size != new_size { + (self.resize_notify_fn)(new_size); + } } #[inline] - pub fn set_min_inner_size(&self, _dimensions: Option) { + pub fn set_min_inner_size(&self, _dimensions: Option) { // Intentionally a no-op: users can't resize canvas elements } #[inline] - pub fn set_max_inner_size(&self, _dimensions: Option) { + pub fn set_max_inner_size(&self, _dimensions: Option) { // Intentionally a no-op: users can't resize canvas elements } @@ -127,8 +153,8 @@ impl Window { } #[inline] - pub fn hidpi_factor(&self) -> f64 { - 1.0 + pub fn scale_factor(&self) -> f64 { + super::backend::scale_factor() } #[inline] @@ -173,41 +199,50 @@ impl Window { CursorIcon::RowResize => "row-resize", }; *self.previous_pointer.borrow_mut() = text; - self.canvas - .set_attribute("style", &format!("cursor: {}", text)); + backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", text); } #[inline] - pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), ExternalError> { - // Intentionally a no-op, as the web does not support setting cursor positions - Ok(()) + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) } #[inline] pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { - // Intentionally a no-op, as the web does not (properly) support grabbing the cursor - Ok(()) + Err(ExternalError::NotSupported(NotSupportedError::new())) } #[inline] pub fn set_cursor_visible(&self, visible: bool) { if !visible { - self.canvas.set_attribute("cursor", "none"); + self.canvas.borrow().set_attribute("cursor", "none"); } else { self.canvas + .borrow() .set_attribute("cursor", *self.previous_pointer.borrow()); } } + #[inline] + pub fn set_minimized(&self, _minimized: bool) { + // Intentionally a no-op, as canvases cannot be 'minimized' + } + #[inline] pub fn set_maximized(&self, _maximized: bool) { // Intentionally a no-op, as canvases cannot be 'maximized' } + #[inline] + pub fn is_maximized(&self) -> bool { + // Canvas cannot be 'maximized' + false + } + #[inline] pub fn fullscreen(&self) -> Option { - if self.canvas.is_fullscreen() { - Some(Fullscreen::Borderless(self.current_monitor())) + if self.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(Some(self.current_monitor_inner()))) } else { None } @@ -216,8 +251,8 @@ impl Window { #[inline] pub fn set_fullscreen(&self, monitor: Option) { if monitor.is_some() { - self.canvas.request_fullscreen(); - } else if self.canvas.is_fullscreen() { + self.canvas.borrow().request_fullscreen(); + } else if self.canvas.borrow().is_fullscreen() { backend::exit_fullscreen(); } } @@ -238,25 +273,38 @@ impl Window { } #[inline] - pub fn set_ime_position(&self, _position: LogicalPosition) { + pub fn set_ime_position(&self, _position: Position) { // Currently a no-op as it does not seem there is good support for this on web } #[inline] - pub fn current_monitor(&self) -> RootMH { + pub fn request_user_attention(&self, _request_type: Option) { + // Currently an intentional no-op + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> RootMH { RootMH { inner: monitor::Handle, } } + #[inline] + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + #[inline] pub fn available_monitors(&self) -> VecDequeIter { VecDeque::new().into_iter() } #[inline] - pub fn primary_monitor(&self) -> monitor::Handle { - monitor::Handle + pub fn primary_monitor(&self) -> Option { + Some(RootMH { + inner: monitor::Handle, + }) } #[inline] @@ -275,6 +323,14 @@ impl Window { } } +impl Drop for Window { + fn drop(&mut self) { + if let Some(destroy_fn) = self.destroy_fn.take() { + destroy_fn(); + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Id(pub(crate) u32); @@ -284,5 +340,7 @@ impl Id { } } -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PlatformSpecificBuilderAttributes; +#[derive(Default, Clone)] +pub struct PlatformSpecificBuilderAttributes { + pub(crate) canvas: Option, +} diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 0000000000..a229e555dc --- /dev/null +++ b/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,217 @@ +/// This is a simple implementation of support for Windows Dark Mode, +/// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; + +use winapi::{ + shared::{ + basetsd::SIZE_T, + minwindef::{BOOL, DWORD, FALSE, UINT, ULONG, WORD}, + ntdef::{LPSTR, NTSTATUS, NT_SUCCESS, PVOID, WCHAR}, + windef::HWND, + winerror::S_OK, + }, + um::{libloaderapi, uxtheme, winuser}, +}; + +use crate::window::Theme; + +lazy_static! { + static ref WIN10_BUILD_VERSION: Option = { + // FIXME: RtlGetVersion is a documented windows API, + // should be part of winapi! + + #[allow(non_snake_case)] + #[repr(C)] + struct OSVERSIONINFOW { + dwOSVersionInfoSize: ULONG, + dwMajorVersion: ULONG, + dwMinorVersion: ULONG, + dwBuildNumber: ULONG, + dwPlatformId: ULONG, + szCSDVersion: [WCHAR; 128], + } + + type RtlGetVersion = unsafe extern "system" fn (*mut OSVERSIONINFOW) -> NTSTATUS; + let handle = get_function!("ntdll.dll", RtlGetVersion); + + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi as _); + + if NT_SUCCESS(status) && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } + } else { + None + } + }; + + static ref DARK_MODE_SUPPORTED: bool = { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false + } + }; + + static ref DARK_THEME_NAME: Vec = widestring("DarkMode_Explorer"); + static ref LIGHT_THEME_NAME: Vec = widestring(""); +} + +/// Attempt to set a theme on a window, if necessary. +/// Returns the theme that was picked +pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), + }; + + let theme_name = if is_dark_mode { + DARK_THEME_NAME.as_ptr() + } else { + LIGHT_THEME_NAME.as_ptr() + }; + + let status = unsafe { uxtheme::SetWindowTheme(hwnd, theme_name as _, std::ptr::null()) }; + + if status == S_OK && set_dark_mode_for_window(hwnd, is_dark_mode) { + return Theme::Dark; + } + } + + Theme::Light +} + +fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { + // Uses Windows undocumented API SetWindowCompositionAttribute, + // as seen in win32-darkmode example linked at top of file. + + type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + + #[allow(non_snake_case)] + type WINDOWCOMPOSITIONATTRIB = u32; + const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + + #[allow(non_snake_case)] + #[repr(C)] + struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: PVOID, + cbData: SIZE_T, + } + + lazy_static! { + static ref SET_WINDOW_COMPOSITION_ATTRIBUTE: Option = + get_function!("user32.dll", SetWindowCompositionAttribute); + } + + if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { + unsafe { + // SetWindowCompositionAttribute needs a bigbool (i32), not bool. + let mut is_dark_mode_bigbool = is_dark_mode as BOOL; + + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WCA_USEDARKMODECOLORS, + pvData: &mut is_dark_mode_bigbool as *mut _ as _, + cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _, + }; + + let status = set_window_composition_attribute(hwnd, &mut data as *mut _); + + status != FALSE + } + } else { + false + } +} + +fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + lazy_static! { + static ref SHOULD_APPS_USE_DARK_MODE: Option = { + unsafe { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: WORD = 132; + + let module = libloaderapi::LoadLibraryA("uxtheme.dll\0".as_ptr() as _); + + if module.is_null() { + return None; + } + + let handle = libloaderapi::GetProcAddress( + module, + winuser::MAKEINTRESOURCEA(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL), + ); + + if handle.is_null() { + None + } else { + Some(std::mem::transmute(handle)) + } + } + }; + } + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +// FIXME: This definition was missing from winapi. Can remove from +// here and use winapi once the following PR is released: +// https://github.com/retep998/winapi-rs/pull/815 +#[repr(C)] +#[allow(non_snake_case)] +struct HIGHCONTRASTA { + cbSize: UINT, + dwFlags: DWORD, + lpszDefaultScheme: LPSTR, +} + +const HCF_HIGHCONTRASTON: DWORD = 1; + +fn is_high_contrast() -> bool { + let mut hc = HIGHCONTRASTA { + cbSize: 0, + dwFlags: 0, + lpszDefaultScheme: std::ptr::null_mut(), + }; + + let ok = unsafe { + winuser::SystemParametersInfoA( + winuser::SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + 0, + ) + }; + + ok != FALSE && (HCF_HIGHCONTRASTON & hc.dwFlags) == 1 +} + +fn widestring(src: &'static str) -> Vec { + OsStr::new(src) + .encode_wide() + .chain(Some(0).into_iter()) + .collect() +} diff --git a/src/platform_impl/windows/dpi.rs b/src/platform_impl/windows/dpi.rs index 05af503e25..f7ec439135 100644 --- a/src/platform_impl/windows/dpi.rs +++ b/src/platform_impl/windows/dpi.rs @@ -2,54 +2,30 @@ use std::sync::Once; +use crate::platform_impl::platform::util::{ + ENABLE_NON_CLIENT_DPI_SCALING, GET_DPI_FOR_MONITOR, GET_DPI_FOR_WINDOW, SET_PROCESS_DPI_AWARE, + SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT, +}; use winapi::{ shared::{ - minwindef::{BOOL, FALSE, UINT}, + minwindef::FALSE, windef::{DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, HMONITOR, HWND}, winerror::S_OK, }, um::{ - shellscalingapi::{ - MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, - PROCESS_PER_MONITOR_DPI_AWARE, - }, + shellscalingapi::{MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE}, wingdi::{GetDeviceCaps, LOGPIXELSX}, - winnt::HRESULT, winuser::{self, MONITOR_DEFAULTTONEAREST}, }, }; const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2: DPI_AWARENESS_CONTEXT = -4isize as _; -type SetProcessDPIAware = unsafe extern "system" fn() -> BOOL; -type SetProcessDpiAwareness = unsafe extern "system" fn(value: PROCESS_DPI_AWARENESS) -> HRESULT; -type SetProcessDpiAwarenessContext = - unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; -type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> UINT; -type GetDpiForMonitor = unsafe extern "system" fn( - hmonitor: HMONITOR, - dpi_type: MONITOR_DPI_TYPE, - dpi_x: *mut UINT, - dpi_y: *mut UINT, -) -> HRESULT; -type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; - -lazy_static! { - static ref GET_DPI_FOR_WINDOW: Option = - get_function!("user32.dll", GetDpiForWindow); - static ref GET_DPI_FOR_MONITOR: Option = - get_function!("shcore.dll", GetDpiForMonitor); - static ref ENABLE_NON_CLIENT_DPI_SCALING: Option = - get_function!("user32.dll", EnableNonClientDpiScaling); -} - pub fn become_dpi_aware() { static ENABLE_DPI_AWARENESS: Once = Once::new(); ENABLE_DPI_AWARENESS.call_once(|| { unsafe { - if let Some(SetProcessDpiAwarenessContext) = - get_function!("user32.dll", SetProcessDpiAwarenessContext) - { + if let Some(SetProcessDpiAwarenessContext) = *SET_PROCESS_DPI_AWARENESS_CONTEXT { // We are on Windows 10 Anniversary Update (1607) or later. if SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) == FALSE @@ -58,13 +34,10 @@ pub fn become_dpi_aware() { // V1 if we can't set V2. SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); } - } else if let Some(SetProcessDpiAwareness) = - get_function!("shcore.dll", SetProcessDpiAwareness) - { + } else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS { // We are on Windows 8.1 or later. SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); - } else if let Some(SetProcessDPIAware) = get_function!("user32.dll", SetProcessDPIAware) - { + } else if let Some(SetProcessDPIAware) = *SET_PROCESS_DPI_AWARE { // We are on Vista or later. SetProcessDPIAware(); } @@ -141,7 +114,3 @@ pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { } } } - -pub fn hwnd_scale_factor(hwnd: HWND) -> f64 { - dpi_to_scale_factor(unsafe { hwnd_dpi(hwnd) }) -} diff --git a/src/platform_impl/windows/drop_handler.rs b/src/platform_impl/windows/drop_handler.rs index feec263964..f6c7a044d4 100644 --- a/src/platform_impl/windows/drop_handler.rs +++ b/src/platform_impl/windows/drop_handler.rs @@ -31,7 +31,7 @@ pub struct FileDropHandlerData { pub interface: IDropTarget, refcount: AtomicUsize, window: HWND, - send_event: Box)>, + send_event: Box)>, cursor_effect: DWORD, hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ } @@ -42,7 +42,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, @@ -227,7 +227,7 @@ impl FileDropHandler { } impl FileDropHandlerData { - fn send_event(&self, event: Event<()>) { + fn send_event(&self, event: Event<'static, ()>) { (self.send_event)(event); } } diff --git a/src/platform_impl/windows/event.rs b/src/platform_impl/windows/event.rs index 89171874b1..161cf6b2de 100644 --- a/src/platform_impl/windows/event.rs +++ b/src/platform_impl/windows/event.rs @@ -17,16 +17,84 @@ fn key_pressed(vkey: c_int) -> bool { } pub fn get_key_mods() -> ModifiersState { - let mut mods = ModifiersState::default(); let filter_out_altgr = layout_uses_altgr() && key_pressed(winuser::VK_RMENU); - mods.shift = key_pressed(winuser::VK_SHIFT); - mods.ctrl = key_pressed(winuser::VK_CONTROL) && !filter_out_altgr; - mods.alt = key_pressed(winuser::VK_MENU) && !filter_out_altgr; - mods.logo = key_pressed(winuser::VK_LWIN) || key_pressed(winuser::VK_RWIN); + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(winuser::VK_SHIFT)); + mods.set( + ModifiersState::CTRL, + key_pressed(winuser::VK_CONTROL) && !filter_out_altgr, + ); + mods.set( + ModifiersState::ALT, + key_pressed(winuser::VK_MENU) && !filter_out_altgr, + ); + mods.set( + ModifiersState::LOGO, + key_pressed(winuser::VK_LWIN) || key_pressed(winuser::VK_RWIN), + ); mods } +bitflags! { + #[derive(Default)] + pub struct ModifiersStateSide: u32 { + const LSHIFT = 0b010 << 0; + const RSHIFT = 0b001 << 0; + + const LCTRL = 0b010 << 3; + const RCTRL = 0b001 << 3; + + const LALT = 0b010 << 6; + const RALT = 0b001 << 6; + + const LLOGO = 0b010 << 9; + const RLOGO = 0b001 << 9; + } +} + +impl ModifiersStateSide { + pub fn filter_out_altgr(&self) -> ModifiersStateSide { + match layout_uses_altgr() && self.contains(Self::RALT) { + false => *self, + true => *self & !(Self::LCTRL | Self::RCTRL | Self::LALT | Self::RALT), + } + } +} + +impl From for ModifiersState { + fn from(side: ModifiersStateSide) -> Self { + let mut state = ModifiersState::default(); + state.set( + Self::SHIFT, + side.intersects(ModifiersStateSide::LSHIFT | ModifiersStateSide::RSHIFT), + ); + state.set( + Self::CTRL, + side.intersects(ModifiersStateSide::LCTRL | ModifiersStateSide::RCTRL), + ); + state.set( + Self::ALT, + side.intersects(ModifiersStateSide::LALT | ModifiersStateSide::RALT), + ); + state.set( + Self::LOGO, + side.intersects(ModifiersStateSide::LLOGO | ModifiersStateSide::RLOGO), + ); + state + } +} + +pub fn get_pressed_keys() -> impl Iterator { + let mut keyboard_state = vec![0u8; 256]; + unsafe { winuser::GetKeyboardState(keyboard_state.as_mut_ptr()) }; + keyboard_state + .into_iter() + .enumerate() + .filter(|(_, p)| (*p & (1 << 7)) != 0) // whether or not a key is pressed is communicated via the high-order bit + .map(|(i, _)| i as c_int) +} + unsafe fn get_char(keyboard_state: &[u8; 256], v_key: u32, hkl: HKL) -> Option { let mut unicode_bytes = [0u16; 5]; let len = winuser::ToUnicodeEx( @@ -39,7 +107,7 @@ unsafe fn get_char(keyboard_state: &[u8; 256], v_key: u32, hkl: HKL) -> Option= 1 { - char::decode_utf16(unicode_bytes.into_iter().cloned()) + char::decode_utf16(unicode_bytes.iter().cloned()) .next() .and_then(|c| c.ok()) } else { @@ -177,8 +245,8 @@ pub fn vkey_to_winit_vkey(vkey: c_int) -> Option { 0x58 => Some(VirtualKeyCode::X), 0x59 => Some(VirtualKeyCode::Y), 0x5A => Some(VirtualKeyCode::Z), - //winuser::VK_LWIN => Some(VirtualKeyCode::Lwin), - //winuser::VK_RWIN => Some(VirtualKeyCode::Rwin), + winuser::VK_LWIN => Some(VirtualKeyCode::LWin), + winuser::VK_RWIN => Some(VirtualKeyCode::RWin), winuser::VK_APPS => Some(VirtualKeyCode::Apps), winuser::VK_SLEEP => Some(VirtualKeyCode::Sleep), winuser::VK_NUMPAD0 => Some(VirtualKeyCode::Numpad0), @@ -191,12 +259,12 @@ pub fn vkey_to_winit_vkey(vkey: c_int) -> Option { winuser::VK_NUMPAD7 => Some(VirtualKeyCode::Numpad7), winuser::VK_NUMPAD8 => Some(VirtualKeyCode::Numpad8), winuser::VK_NUMPAD9 => Some(VirtualKeyCode::Numpad9), - winuser::VK_MULTIPLY => Some(VirtualKeyCode::Multiply), - winuser::VK_ADD => Some(VirtualKeyCode::Add), + winuser::VK_MULTIPLY => Some(VirtualKeyCode::NumpadMultiply), + winuser::VK_ADD => Some(VirtualKeyCode::NumpadAdd), //winuser::VK_SEPARATOR => Some(VirtualKeyCode::Separator), - winuser::VK_SUBTRACT => Some(VirtualKeyCode::Subtract), - winuser::VK_DECIMAL => Some(VirtualKeyCode::Decimal), - winuser::VK_DIVIDE => Some(VirtualKeyCode::Divide), + winuser::VK_SUBTRACT => Some(VirtualKeyCode::NumpadSubtract), + winuser::VK_DECIMAL => Some(VirtualKeyCode::NumpadDecimal), + winuser::VK_DIVIDE => Some(VirtualKeyCode::NumpadDivide), winuser::VK_F1 => Some(VirtualKeyCode::F1), winuser::VK_F2 => Some(VirtualKeyCode::F2), winuser::VK_F3 => Some(VirtualKeyCode::F3), @@ -275,6 +343,7 @@ pub fn handle_extended_keys( extended: bool, ) -> Option<(c_int, UINT)> { // Welcome to hell https://blog.molecular-matters.com/2011/09/05/properly-handling-keyboard-input/ + scancode = if extended { 0xE000 } else { 0x0000 } | scancode; let vkey = match vkey { winuser::VK_SHIFT => unsafe { winuser::MapVirtualKeyA(scancode, winuser::MAPVK_VSC_TO_VK_EX) as _ @@ -295,20 +364,23 @@ pub fn handle_extended_keys( } _ => { match scancode { - // This is only triggered when using raw input. Without this check, we get two events whenever VK_PAUSE is - // pressed, the first one having scancode 0x1D but vkey VK_PAUSE... - 0x1D if vkey == winuser::VK_PAUSE => return None, - // ...and the second having scancode 0x45 but an unmatched vkey! - 0x45 => winuser::VK_PAUSE, - // VK_PAUSE and VK_SCROLL have the same scancode when using modifiers, alongside incorrect vkey values. - 0x46 => { - if extended { - scancode = 0x45; - winuser::VK_PAUSE - } else { - winuser::VK_SCROLL - } + // When VK_PAUSE is pressed it emits a LeftControl + NumLock scancode event sequence, but reports VK_PAUSE + // as the virtual key on both events, or VK_PAUSE on the first event or 0xFF when using raw input. + // Don't emit anything for the LeftControl event in the pair... + 0xE01D if vkey == winuser::VK_PAUSE => return None, + // ...and emit the Pause event for the second event in the pair. + 0x45 if vkey == winuser::VK_PAUSE || vkey == 0xFF as _ => { + scancode = 0xE059; + winuser::VK_PAUSE + } + // VK_PAUSE has an incorrect vkey value when used with modifiers. VK_PAUSE also reports a different + // scancode when used with modifiers than when used without + 0xE046 => { + scancode = 0xE059; + winuser::VK_PAUSE } + // VK_SCROLL has an incorrect vkey value when used with modifiers. + 0x46 => winuser::VK_SCROLL, _ => vkey, } } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 0968db83a1..86c9e5fde7 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -1,39 +1,26 @@ #![allow(non_snake_case)] -//! An events loop on Win32 is a background thread. -//! -//! Creating an events loop spawns a thread and blocks it in a permanent Win32 events loop. -//! Destroying the events loop stops the thread. -//! -//! You can use the `execute_in_thread` method to execute some code in the background thread. -//! Since Win32 requires you to create a window in the right thread, you must use this method -//! to create a window. -//! -//! If you create a window whose class is set to `callback`, the window's events will be -//! propagated with `run_forever` and `poll_events`. -//! The closure passed to the `execute_in_thread` method takes an `Inserter` that you can use to -//! add a `WindowState` entry to a list of window to be used by the callback. + +mod runner; use parking_lot::Mutex; use std::{ - any::Any, - cell::RefCell, - collections::VecDeque, + cell::{Cell, RefCell}, + collections::{HashMap, VecDeque}, marker::PhantomData, mem, panic, ptr, rc::Rc, sync::{ - atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, Sender}, Arc, }, + thread, time::{Duration, Instant}, }; use winapi::shared::basetsd::{DWORD_PTR, UINT_PTR}; use winapi::{ - ctypes::c_int, shared::{ - minwindef::{BOOL, DWORD, HIWORD, INT, LOWORD, LPARAM, LRESULT, UINT, WPARAM}, + minwindef::{BOOL, DWORD, HIWORD, INT, LOWORD, LPARAM, LRESULT, UINT, WORD, WPARAM}, windef::{HWND, POINT, RECT}, windowsx, winerror, }, @@ -45,24 +32,28 @@ use winapi::{ }; use crate::{ - dpi::{LogicalPosition, LogicalSize, PhysicalSize}, - event::{DeviceEvent, Event, Force, KeyboardInput, StartCause, Touch, TouchPhase, WindowEvent}, + dpi::{PhysicalPosition, PhysicalSize}, + event::{ + device::{GamepadEvent, HidEvent, KeyboardEvent, MouseEvent}, + Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent, + }, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ - dpi::{ - become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_scale_factor, - }, + dark_mode::try_theme, + dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, drop_handler::FileDropHandler, event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey}, - monitor, - raw_input::{get_raw_input_data, get_raw_mouse_button_state}, + gamepad::Gamepad, + monitor::{self, MonitorHandle}, + raw_input::{self, get_raw_input_data, get_raw_mouse_button_state, RawInputData}, util, - window::adjust_size, window_state::{CursorFlags, WindowFlags, WindowState}, - wrap_device_id, WindowId, DEVICE_ID, + GamepadHandle, HidId, KeyboardId, MouseId, WindowId, }, window::{Fullscreen, WindowId as RootWindowId}, }; +use runner::{EventLoopRunner, EventLoopRunnerShared}; type GetPointerFrameInfoHistory = unsafe extern "system" fn( pointerId: UINT, @@ -97,26 +88,41 @@ lazy_static! { get_function!("user32.dll", GetPointerPenInfo); } -pub(crate) struct SubclassInput { +#[derive(Debug)] +pub(crate) enum DeviceId { + Mouse(MouseId), + Keyboard(KeyboardId), + Hid(HidId), + Gamepad(GamepadHandle, Gamepad), +} + +pub(crate) struct SubclassSharedData { + pub runner_shared: EventLoopRunnerShared, + pub active_device_ids: RefCell>, +} + +pub(crate) struct SubclassInput { pub window_state: Arc>, - pub event_loop_runner: EventLoopRunnerShared, - pub file_drop_handler: FileDropHandler, + pub shared_data: Rc>, + pub file_drop_handler: Option, + pub subclass_removed: Cell, + pub recurse_depth: Cell, } impl SubclassInput { - unsafe fn send_event(&self, event: Event) { - self.event_loop_runner.send_event(event); + unsafe fn send_event(&self, event: Event<'_, T>) { + self.shared_data.runner_shared.send_event(event); } } -struct ThreadMsgTargetSubclassInput { - event_loop_runner: EventLoopRunnerShared, +struct ThreadMsgTargetSubclassInput { + shared_data: Rc>, user_event_receiver: Receiver, } impl ThreadMsgTargetSubclassInput { - unsafe fn send_event(&self, event: Event) { - self.event_loop_runner.send_event(event); + unsafe fn send_event(&self, event: Event<'_, T>) { + self.shared_data.runner_shared.send_event(event); } } @@ -125,11 +131,10 @@ pub struct EventLoop { window_target: RootELW, } -pub struct EventLoopWindowTarget { +pub struct EventLoopWindowTarget { thread_id: DWORD, - trigger_newevents_on_redraw: Arc, thread_msg_target: HWND, - pub(crate) runner_shared: EventLoopRunnerShared, + pub(crate) shared_data: Rc>, } macro_rules! main_thread_check { @@ -167,21 +172,29 @@ impl EventLoop { pub fn new_dpi_unaware_any_thread() -> EventLoop { let thread_id = unsafe { processthreadsapi::GetCurrentThreadId() }; - let runner_shared = Rc::new(ELRShared { - runner: RefCell::new(None), - buffer: RefCell::new(VecDeque::new()), + + let thread_msg_target = create_event_target_window(); + + let send_thread_msg_target = thread_msg_target as usize; + thread::spawn(move || wait_thread(thread_id, send_thread_msg_target as HWND)); + let wait_thread_id = get_wait_thread_id(); + + let shared_data = Rc::new(SubclassSharedData { + runner_shared: Rc::new(EventLoopRunner::new(thread_msg_target, wait_thread_id)), + active_device_ids: RefCell::new(HashMap::default()), }); - let (thread_msg_target, thread_msg_sender) = - thread_event_target_window(runner_shared.clone()); + + let thread_msg_sender = + subclass_event_target_window(thread_msg_target, shared_data.clone()); + raw_input::register_for_raw_input(thread_msg_target); EventLoop { thread_msg_sender, window_target: RootELW { p: EventLoopWindowTarget { thread_id, - trigger_newevents_on_redraw: Arc::new(AtomicBool::new(true)), thread_msg_target, - runner_shared, + shared_data, }, _marker: PhantomData, }, @@ -194,7 +207,7 @@ impl EventLoop { pub fn run(mut self, event_handler: F) -> ! where - F: 'static + FnMut(Event, &RootELW, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { self.run_return(event_handler); ::std::process::exit(0); @@ -202,85 +215,48 @@ impl EventLoop { pub fn run_return(&mut self, mut event_handler: F) where - F: FnMut(Event, &RootELW, &mut ControlFlow), + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { let event_loop_windows_ref = &self.window_target; - let mut runner = unsafe { - EventLoopRunner::new(self, move |event, control_flow| { - event_handler(event, event_loop_windows_ref, control_flow) - }) - }; - { - let runner_shared = self.window_target.p.runner_shared.clone(); - let mut runner_ref = runner_shared.runner.borrow_mut(); - loop { - let event = runner_shared.buffer.borrow_mut().pop_front(); - match event { - Some(e) => { - runner.process_event(e); - } - None => break, - } - } - *runner_ref = Some(runner); - } - - macro_rules! runner { - () => { - self.window_target - .p - .runner_shared - .runner - .borrow_mut() - .as_mut() - .unwrap() - }; + unsafe { + self.window_target + .p + .shared_data + .runner_shared + .set_event_handler(move |event, control_flow| { + event_handler(event, event_loop_windows_ref, control_flow) + }); } + let runner = &self.window_target.p.shared_data.runner_shared; + unsafe { let mut msg = mem::zeroed(); - let mut msg_unprocessed = false; + runner.poll(); 'main: loop { - runner!().new_events(); - loop { - if !msg_unprocessed { - if 0 == winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 1) { - break; - } - } - winuser::TranslateMessage(&mut msg); - winuser::DispatchMessageW(&mut msg); - - msg_unprocessed = false; + if 0 == winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) { + break 'main; } - runner!().events_cleared(); - if let Some(payload) = runner!().panic_error.take() { + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); panic::resume_unwind(payload); } - if !msg_unprocessed { - let control_flow = runner!().control_flow; - match control_flow { - ControlFlow::Exit => break 'main, - ControlFlow::Wait => { - if 0 == winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) { - break 'main; - } - msg_unprocessed = true; - } - ControlFlow::WaitUntil(resume_time) => { - wait_until_time_or_msg(resume_time); - } - ControlFlow::Poll => (), - } + if runner.control_flow() == ControlFlow::Exit && !runner.handling_events() { + break 'main; } } } - runner!().call_event_handler(Event::LoopDestroyed); - *self.window_target.p.runner_shared.runner.borrow_mut() = None; + unsafe { + runner.loop_destroyed(); + } + runner.reset_runner(); } pub fn create_proxy(&self) -> EventLoopProxy { @@ -289,6 +265,70 @@ impl EventLoop { event_send: self.thread_msg_sender.clone(), } } + + fn devices( + &self, + f: impl FnMut(&DeviceId) -> Option, + ) -> impl '_ + Iterator { + // Flush WM_INPUT and WM_INPUT_DEVICE_CHANGE events so that the active_device_ids list is + // accurate. This is essential to make this function work if called before calling `run` or + // `run_return`. + unsafe { + let mut msg = mem::zeroed(); + loop { + let result = winuser::PeekMessageW( + &mut msg, + self.window_target.p.thread_msg_target, + winuser::WM_INPUT_DEVICE_CHANGE, + winuser::WM_INPUT, + 1, + ); + if 0 == result { + break; + } + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + } + } + + self.window_target + .p + .shared_data + .active_device_ids + .borrow() + .values() + .filter_map(f) + .collect::>() + .into_iter() + } + + pub fn mouses(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Mouse(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn keyboards(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Keyboard(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn hids(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Hid(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn gamepads(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Gamepad(handle, _) => Some(handle.clone().into()), + _ => None, + }) + } } impl EventLoopWindowTarget { @@ -296,10 +336,19 @@ impl EventLoopWindowTarget { pub(crate) fn create_thread_executor(&self) -> EventLoopThreadExecutor { EventLoopThreadExecutor { thread_id: self.thread_id, - trigger_newevents_on_redraw: self.trigger_newevents_on_redraw.clone(), target_window: self.thread_msg_target, } } + + // TODO: Investigate opportunities for caching + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } } fn main_thread_id() -> DWORD { @@ -317,317 +366,89 @@ fn main_thread_id() -> DWORD { unsafe { MAIN_THREAD_ID } } -pub(crate) type EventLoopRunnerShared = Rc>; -pub(crate) struct ELRShared { - runner: RefCell>>, - buffer: RefCell>>, -} -pub(crate) struct EventLoopRunner { - trigger_newevents_on_redraw: Arc, - control_flow: ControlFlow, - runner_state: RunnerState, - modal_redraw_window: HWND, - in_modal_loop: bool, - in_repaint: bool, - event_handler: Box, &mut ControlFlow)>, - panic_error: Option, -} -type PanicError = Box; - -impl ELRShared { - pub(crate) unsafe fn send_event(&self, event: Event) { - if let Ok(mut runner_ref) = self.runner.try_borrow_mut() { - if let Some(ref mut runner) = *runner_ref { - runner.process_event(event); - - // Dispatch any events that were buffered during the call to `process_event`. - 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 - // of the loop's body and attempts to add events to the event buffer while in - // `process_event` will fail. - let buffered_event_opt = self.buffer.borrow_mut().pop_front(); - match buffered_event_opt { - Some(event) => runner.process_event(event), - None => break, - } - } - - return; - } - } - - // 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. - self.buffer.borrow_mut().push_back(event) +fn get_wait_thread_id() -> DWORD { + unsafe { + let mut msg = mem::zeroed(); + let result = winuser::GetMessageW( + &mut msg, + -1 as _, + *SEND_WAIT_THREAD_ID_MSG_ID, + *SEND_WAIT_THREAD_ID_MSG_ID, + ); + assert_eq!( + msg.message, *SEND_WAIT_THREAD_ID_MSG_ID, + "this shouldn't be possible. please open an issue with Winit. error code: {}", + result + ); + msg.lParam as DWORD } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RunnerState { - /// The event loop has just been created, and an `Init` event must be sent. - New, - /// The event loop is idling, and began idling at the given instant. - Idle(Instant), - /// The event loop has received a signal from the OS that the loop may resume, but no winit - /// events have been generated yet. We're waiting for an event to be processed or the events - /// to be marked as cleared to send `NewEvents`, depending on the current `ControlFlow`. - DeferredNewEvents(Instant), - /// The event loop is handling the OS's events and sending them to the user's callback. - /// `NewEvents` has been sent, and `EventsCleared` hasn't. - HandlingEvents, -} - -impl EventLoopRunner { - unsafe fn new(event_loop: &EventLoop, f: F) -> EventLoopRunner - where - F: FnMut(Event, &mut ControlFlow), - { - EventLoopRunner { - trigger_newevents_on_redraw: event_loop - .window_target - .p - .trigger_newevents_on_redraw - .clone(), - control_flow: ControlFlow::default(), - runner_state: RunnerState::New, - in_modal_loop: false, - in_repaint: false, - modal_redraw_window: event_loop.window_target.p.thread_msg_target, - event_handler: mem::transmute::< - Box, &mut ControlFlow)>, - Box, &mut ControlFlow)>, - >(Box::new(f)), - panic_error: None, - } - } - - fn new_events(&mut self) { - self.runner_state = match self.runner_state { - // If we're already handling events or have deferred `NewEvents`, we don't need to do - // do any processing. - RunnerState::HandlingEvents | RunnerState::DeferredNewEvents(..) => self.runner_state, - - // Send the `Init` `NewEvents` and immediately move into event processing. - RunnerState::New => { - self.call_event_handler(Event::NewEvents(StartCause::Init)); - RunnerState::HandlingEvents - } - - // When `NewEvents` gets sent after an idle depends on the control flow... - RunnerState::Idle(wait_start) => { - match self.control_flow { - // If we're polling, send `NewEvents` and immediately move into event processing. - ControlFlow::Poll => { - self.call_event_handler(Event::NewEvents(StartCause::Poll)); - RunnerState::HandlingEvents - }, - // If the user was waiting until a specific time, the `NewEvents` call gets sent - // at varying times depending on the current time. - ControlFlow::WaitUntil(resume_time) => { - match Instant::now() >= resume_time { - // If the current time is later than the requested resume time, we can tell the - // user that the resume time has been reached with `NewEvents` and immdiately move - // into event processing. - true => { - self.call_event_handler(Event::NewEvents(StartCause::ResumeTimeReached { - start: wait_start, - requested_resume: resume_time, - })); - RunnerState::HandlingEvents - }, - // However, if the current time is EARLIER than the requested resume time, we - // don't want to send the `WaitCancelled` event until we know an event is being - // sent. Defer. - false => RunnerState::DeferredNewEvents(wait_start) - } - }, - // If we're waiting, `NewEvents` doesn't get sent until winit gets an event, so - // we defer. - ControlFlow::Wait | - // `Exit` shouldn't really ever get sent here, but if it does do something somewhat sane. - ControlFlow::Exit => RunnerState::DeferredNewEvents(wait_start), - } - } - }; - } +fn wait_thread(parent_thread_id: DWORD, msg_window_id: HWND) { + unsafe { + let mut msg: winuser::MSG; - fn process_event(&mut self, event: Event) { - // If we're in the modal loop, we need to have some mechanism for finding when the event - // queue has been cleared so we can call `events_cleared`. Windows doesn't give any utilities - // for doing this, but it DOES guarantee that WM_PAINT will only occur after input events have - // been processed. So, we send WM_PAINT to a dummy window which calls `events_cleared` when - // the events queue has been emptied. - if self.in_modal_loop { - unsafe { - winuser::RedrawWindow( - self.modal_redraw_window, - ptr::null(), - ptr::null_mut(), - winuser::RDW_INTERNALPAINT, - ); - } - } + let cur_thread_id = processthreadsapi::GetCurrentThreadId(); + winuser::PostThreadMessageW( + parent_thread_id, + *SEND_WAIT_THREAD_ID_MSG_ID, + 0, + cur_thread_id as LPARAM, + ); - // If new event processing has to be done (i.e. call NewEvents or defer), do it. If we're - // already in processing nothing happens with this call. - self.new_events(); + 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(); - // Now that an event has been received, we have to send any `NewEvents` calls that were - // deferred. - if let RunnerState::DeferredNewEvents(wait_start) = self.runner_state { - match self.control_flow { - ControlFlow::Exit | ControlFlow::Wait => { - self.call_event_handler(Event::NewEvents(StartCause::WaitCancelled { - start: wait_start, - requested_resume: None, - })) + if wait_until_opt.is_some() { + if 0 != winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, winuser::PM_REMOVE) { + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); } - ControlFlow::WaitUntil(resume_time) => { - let start_cause = match Instant::now() >= resume_time { - // If the current time is later than the requested resume time, the resume time - // has been reached. - true => StartCause::ResumeTimeReached { - start: wait_start, - requested_resume: resume_time, - }, - // Otherwise, the requested resume time HASN'T been reached and we send a WaitCancelled. - false => StartCause::WaitCancelled { - start: wait_start, - requested_resume: Some(resume_time), - }, - }; - self.call_event_handler(Event::NewEvents(start_cause)); + } else { + if 0 == winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) { + break 'main; + } else { + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); } - // This can be reached if the control flow is changed to poll during a `RedrawRequested` - // that was sent after `EventsCleared`. - ControlFlow::Poll => self.call_event_handler(Event::NewEvents(StartCause::Poll)), - } - } - - self.runner_state = RunnerState::HandlingEvents; - match (self.in_repaint, &event) { - ( - true, - Event::WindowEvent { - event: WindowEvent::RedrawRequested, - .. - }, - ) - | (false, _) => self.call_event_handler(event), - (true, _) => { - self.events_cleared(); - self.new_events(); - self.process_event(event); } - } - } - fn events_cleared(&mut self) { - self.in_repaint = false; - - match self.runner_state { - // If we were handling events, send the EventsCleared message. - RunnerState::HandlingEvents => { - self.call_event_handler(Event::EventsCleared); - self.runner_state = RunnerState::Idle(Instant::now()); + if msg.message == *WAIT_UNTIL_MSG_ID { + wait_until_opt = Some(*WaitUntilInstantBox::from_raw(msg.lParam as *mut _)); + } else if msg.message == *CANCEL_WAIT_UNTIL_MSG_ID { + wait_until_opt = None; } - // If we *weren't* handling events, we don't have to do anything. - RunnerState::New | RunnerState::Idle(..) => (), - - // Some control flows require a NewEvents call even if no events were received. This - // branch handles those. - RunnerState::DeferredNewEvents(wait_start) => { - match self.control_flow { - // If we had deferred a Poll, send the Poll NewEvents and EventsCleared. - ControlFlow::Poll => { - self.call_event_handler(Event::NewEvents(StartCause::Poll)); - self.call_event_handler(Event::EventsCleared); - } - // If we had deferred a WaitUntil and the resume time has since been reached, - // send the resume notification and EventsCleared event. - ControlFlow::WaitUntil(resume_time) => { - if Instant::now() >= resume_time { - self.call_event_handler(Event::NewEvents( - StartCause::ResumeTimeReached { - start: wait_start, - requested_resume: resume_time, - }, - )); - self.call_event_handler(Event::EventsCleared); - } + if let Some(wait_until) = wait_until_opt { + let now = Instant::now(); + if now < wait_until { + // MsgWaitForMultipleObjects tends to overshoot just a little bit. We subtract + // 1 millisecond from the requested time and spinlock for the remainder to + // compensate for that. + let resume_reason = winuser::MsgWaitForMultipleObjectsEx( + 0, + ptr::null(), + dur2timeout(wait_until - now).saturating_sub(1), + winuser::QS_ALLEVENTS, + winuser::MWMO_INPUTAVAILABLE, + ); + if resume_reason == winerror::WAIT_TIMEOUT { + winuser::PostMessageW(msg_window_id, *PROCESS_NEW_EVENTS_MSG_ID, 0, 0); + wait_until_opt = None; } - // If we deferred a wait and no events were received, the user doesn't have to - // get an event. - ControlFlow::Wait | ControlFlow::Exit => (), - } - // Mark that we've entered an idle state. - self.runner_state = RunnerState::Idle(wait_start) - } - } - } - - fn call_event_handler(&mut self, event: Event) { - match event { - Event::NewEvents(_) => self - .trigger_newevents_on_redraw - .store(true, Ordering::Relaxed), - Event::EventsCleared => self - .trigger_newevents_on_redraw - .store(false, Ordering::Relaxed), - Event::WindowEvent { - event: WindowEvent::RedrawRequested, - .. - } => self.in_repaint = true, - _ => (), - } - - if self.panic_error.is_none() { - let EventLoopRunner { - ref mut panic_error, - ref mut event_handler, - ref mut control_flow, - .. - } = self; - *panic_error = panic::catch_unwind(panic::AssertUnwindSafe(|| { - if *control_flow != ControlFlow::Exit { - (*event_handler)(event, control_flow); } else { - (*event_handler)(event, &mut ControlFlow::Exit); - } - })) - .err(); - } - } -} - -// Returns true if the wait time was reached, and false if a message must be processed. -unsafe fn wait_until_time_or_msg(wait_until: Instant) -> bool { - let mut msg = mem::zeroed(); - let now = Instant::now(); - if now <= wait_until { - // MsgWaitForMultipleObjects tends to overshoot just a little bit. We subtract 1 millisecond - // from the requested time and spinlock for the remainder to compensate for that. - let resume_reason = winuser::MsgWaitForMultipleObjectsEx( - 0, - ptr::null(), - dur2timeout(wait_until - now).saturating_sub(1), - winuser::QS_ALLEVENTS, - winuser::MWMO_INPUTAVAILABLE, - ); - - if resume_reason == winerror::WAIT_TIMEOUT { - while Instant::now() < wait_until { - if 0 != winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0) { - return false; + winuser::PostMessageW(msg_window_id, *PROCESS_NEW_EVENTS_MSG_ID, 0, 0); + wait_until_opt = None; } } } } - - return true; } + // Implementation taken from https://github.com/rust-lang/rust/blob/db5476571d9b27c862b95c1e64764b0ac8980e23/src/libstd/sys/windows/mod.rs fn dur2timeout(dur: Duration) -> DWORD { // Note that a duration is a (u64, u32) (seconds, nanoseconds) pair, and the @@ -667,7 +488,6 @@ impl Drop for EventLoop { pub(crate) struct EventLoopThreadExecutor { thread_id: DWORD, - trigger_newevents_on_redraw: Arc, target_window: HWND, } @@ -681,10 +501,6 @@ impl EventLoopThreadExecutor { self.thread_id == cur_thread_id } - pub(super) fn trigger_newevents_on_redraw(&self) -> bool { - !self.in_event_loop_thread() || self.trigger_newevents_on_redraw.load(Ordering::Relaxed) - } - /// Executes a function in the event loop thread. If we're already in the event loop thread, /// we just call the function directly. /// @@ -742,18 +558,20 @@ impl Clone for EventLoopProxy { } impl EventLoopProxy { - pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { unsafe { if winuser::PostMessageW(self.target_window, *USER_EVENT_MSG_ID, 0, 0) != 0 { self.event_send.send(event).ok(); Ok(()) } else { - Err(EventLoopClosed) + Err(EventLoopClosed(event)) } } } } +type WaitUntilInstantBox = Box; + lazy_static! { // Message sent by the `EventLoopProxy` when we want to wake up the thread. // WPARAM and LPARAM are unused. @@ -770,24 +588,34 @@ lazy_static! { winuser::RegisterWindowMessageA("Winit::ExecMsg\0".as_ptr() as *const i8) } }; - // Message sent by a `Window` when it wants to be destroyed by the main thread. - // WPARAM and LPARAM are unused. - pub static ref DESTROY_MSG_ID: u32 = { + static ref PROCESS_NEW_EVENTS_MSG_ID: u32 = { unsafe { - winuser::RegisterWindowMessageA("Winit::DestroyMsg\0".as_ptr() as LPCSTR) + winuser::RegisterWindowMessageA("Winit::ProcessNewEvents\0".as_ptr() as *const i8) } }; - // Message sent by a `Window` after creation if it has a DPI != 96. - // WPARAM is the the DPI (u32). LOWORD of LPARAM is width, and HIWORD is height. - pub static ref INITIAL_DPI_MSG_ID: u32 = { + /// lparam is the wait thread's message id. + static ref SEND_WAIT_THREAD_ID_MSG_ID: u32 = { unsafe { - winuser::RegisterWindowMessageA("Winit::InitialDpiMsg\0".as_ptr() as LPCSTR) + winuser::RegisterWindowMessageA("Winit::SendWaitThreadId\0".as_ptr() as *const i8) } }; - // Message sent by a `Window` if it's requesting a redraw without sending a NewEvents. - pub static ref REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID: u32 = { + /// lparam points to a `Box` signifying the time `PROCESS_NEW_EVENTS_MSG_ID` should + /// be sent. + static ref WAIT_UNTIL_MSG_ID: u32 = { unsafe { - winuser::RegisterWindowMessageA("Winit::RequestRedrawNoNewevents\0".as_ptr() as LPCSTR) + winuser::RegisterWindowMessageA("Winit::WaitUntil\0".as_ptr() as *const i8) + } + }; + static ref CANCEL_WAIT_UNTIL_MSG_ID: u32 = { + unsafe { + winuser::RegisterWindowMessageA("Winit::CancelWaitUntil\0".as_ptr() as *const i8) + } + }; + // Message sent by a `Window` when it wants to be destroyed by the main thread. + // WPARAM and LPARAM are unused. + pub static ref DESTROY_MSG_ID: u32 = { + unsafe { + winuser::RegisterWindowMessageA("Winit::DestroyMsg\0".as_ptr() as LPCSTR) } }; // WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the @@ -825,7 +653,7 @@ lazy_static! { }; } -fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> (HWND, Sender) { +fn create_event_target_window() -> HWND { unsafe { let window = winuser::CreateWindowExW( winuser::WS_EX_NOACTIVATE | winuser::WS_EX_TRANSPARENT | winuser::WS_EX_LAYERED, @@ -849,11 +677,19 @@ fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> // the LAYERED style. (winuser::WS_VISIBLE | winuser::WS_POPUP) as _, ); + window + } +} +fn subclass_event_target_window( + window: HWND, + shared_data: Rc>, +) -> Sender { + unsafe { let (tx, rx) = mpsc::channel(); let subclass_input = ThreadMsgTargetSubclassInput { - event_loop_runner, + shared_data, user_event_receiver: rx, }; let input_ptr = Box::into_raw(Box::new(subclass_input)); @@ -865,22 +701,35 @@ fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> ); assert_eq!(subclass_result, 1); - (window, tx) + tx } } +fn remove_event_target_window_subclass(window: HWND) { + let removal_result = unsafe { + commctrl::RemoveWindowSubclass( + window, + Some(thread_event_target_callback::), + THREAD_EVENT_TARGET_SUBCLASS_ID, + ) + }; + assert_eq!(removal_result, 1); +} + /// Capture mouse input, allowing `window` to receive mouse events when the cursor is outside of /// the window. unsafe fn capture_mouse(window: HWND, window_state: &mut WindowState) { - window_state.mouse.buttons_down += 1; + window_state.mouse.capture_count += 1; winuser::SetCapture(window); } /// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor /// is outside the window. -unsafe fn release_mouse(window_state: &mut WindowState) { - window_state.mouse.buttons_down = window_state.mouse.buttons_down.saturating_sub(1); - if window_state.mouse.buttons_down == 0 { +unsafe fn release_mouse(mut window_state: parking_lot::MutexGuard<'_, WindowState>) { + window_state.mouse.capture_count = window_state.mouse.capture_count.saturating_sub(1); + if window_state.mouse.capture_count == 0 { + // ReleaseCapture() causes a WM_CAPTURECHANGED where we lock the window_state. + drop(window_state); winuser::ReleaseCapture(); } } @@ -888,6 +737,10 @@ unsafe fn release_mouse(window_state: &mut WindowState) { const WINDOW_SUBCLASS_ID: UINT_PTR = 0; const THREAD_EVENT_TARGET_SUBCLASS_ID: UINT_PTR = 1; pub(crate) fn subclass_window(window: HWND, subclass_input: SubclassInput) { + subclass_input + .shared_data + .runner_shared + .register_window(window); let input_ptr = Box::into_raw(Box::new(subclass_input)); let subclass_result = unsafe { commctrl::SetWindowSubclass( @@ -900,6 +753,17 @@ pub(crate) fn subclass_window(window: HWND, subclass_input: SubclassInput) assert_eq!(subclass_result, 1); } +fn remove_window_subclass(window: HWND) { + let removal_result = unsafe { + commctrl::RemoveWindowSubclass( + window, + Some(public_window_callback::), + WINDOW_SUBCLASS_ID, + ) + }; + assert_eq!(removal_result, 1); +} + fn normalize_pointer_pressure(pressure: u32) -> Option { match pressure { 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), @@ -907,6 +771,89 @@ 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 0 == winuser::PeekMessageW( + &mut msg, + redraw_window, + winuser::WM_PAINT, + winuser::WM_PAINT, + winuser::PM_REMOVE | winuser::PM_QS_PAINT, + ) { + return; + } + + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + }); + true + } else { + false + } +} + +unsafe fn process_control_flow(runner: &EventLoopRunner) { + match runner.control_flow() { + ControlFlow::Poll => { + winuser::PostMessageW(runner.thread_msg_target(), *PROCESS_NEW_EVENTS_MSG_ID, 0, 0); + } + ControlFlow::Wait => (), + ControlFlow::WaitUntil(until) => { + winuser::PostThreadMessageW( + runner.wait_thread_id(), + *WAIT_UNTIL_MSG_ID, + 0, + Box::into_raw(WaitUntilInstantBox::new(until)) as LPARAM, + ); + } + ControlFlow::Exit => (), + } +} + +/// Emit a `ModifiersChanged` event whenever modifiers have changed. +fn update_modifiers(window: HWND, subclass_input: &SubclassInput) { + use crate::event::WindowEvent::ModifiersChanged; + + let modifiers = event::get_key_mods(); + let mut window_state = subclass_input.window_state.lock(); + if window_state.modifiers_state != modifiers { + window_state.modifiers_state = modifiers; + + // Drop lock + drop(window_state); + + unsafe { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(modifiers), + }); + } + } +} + /// Any window whose callback is configured to this function will have its events propagated /// through the events loop of the thread the window was created in. // @@ -914,46 +861,80 @@ fn normalize_pointer_pressure(pressure: u32) -> Option { // // Returning 0 tells the Win32 API that the message has been processed. // FIXME: detect WM_DWMCOMPOSITIONCHANGED and call DwmEnableBlurBehindWindow if necessary -unsafe extern "system" fn public_window_callback( +unsafe extern "system" fn public_window_callback( window: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, - _: UINT_PTR, + uidsubclass: UINT_PTR, subclass_input_ptr: DWORD_PTR, ) -> LRESULT { - let subclass_input = &mut *(subclass_input_ptr as *mut SubclassInput); + let subclass_input_ptr = subclass_input_ptr as *mut SubclassInput; + let (result, subclass_removed, recurse_depth) = { + let subclass_input = &*subclass_input_ptr; + subclass_input + .recurse_depth + .set(subclass_input.recurse_depth.get() + 1); + + let result = + public_window_callback_inner(window, msg, wparam, lparam, uidsubclass, subclass_input); + + let subclass_removed = subclass_input.subclass_removed.get(); + let recurse_depth = subclass_input.recurse_depth.get() - 1; + subclass_input.recurse_depth.set(recurse_depth); + + (result, subclass_removed, recurse_depth) + }; + + if subclass_removed && recurse_depth == 0 { + Box::from_raw(subclass_input_ptr); + } + + result +} - match msg { +unsafe fn public_window_callback_inner( + window: HWND, + msg: UINT, + wparam: WPARAM, + lparam: LPARAM, + _: UINT_PTR, + subclass_input: &SubclassInput, +) -> LRESULT { + winuser::RedrawWindow( + subclass_input.shared_data.runner_shared.thread_msg_target(), + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indendation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { winuser::WM_ENTERSIZEMOVE => { - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); - if let Some(ref mut runner) = *runner { - runner.in_modal_loop = true; - } + subclass_input + .window_state + .lock() + .set_window_flags_in_place(|f| f.insert(WindowFlags::MARKER_IN_SIZE_MOVE)); 0 } + winuser::WM_EXITSIZEMOVE => { - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); - if let Some(ref mut runner) = *runner { - runner.in_modal_loop = false; - } + subclass_input + .window_state + .lock() + .set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); 0 } + winuser::WM_NCCREATE => { enable_non_client_dpi_scaling(window); commctrl::DefSubclassProc(window, msg, wparam, lparam) } - winuser::WM_NCLBUTTONDOWN => { - // jumpstart the modal loop - winuser::RedrawWindow( - window, - ptr::null(), - ptr::null_mut(), - winuser::RDW_INTERNALPAINT, - ); if wparam == winuser::HTCAPTION as _ { - winuser::PostMessageW(window, winuser::WM_MOUSEMOVE, 0, 0); + winuser::PostMessageW(window, winuser::WM_MOUSEMOVE, 0, lparam); } commctrl::DefSubclassProc(window, msg, wparam, lparam) } @@ -974,49 +955,42 @@ unsafe extern "system" fn public_window_callback( window_id: RootWindowId(WindowId(window)), event: Destroyed, }); - - Box::from_raw(subclass_input); - drop(subclass_input); + subclass_input + .shared_data + .runner_shared + .remove_window(window); 0 } - _ if msg == *REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID => { - use crate::event::WindowEvent::RedrawRequested; - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); - subclass_input.window_state.lock().queued_out_of_band_redraw = false; - if let Some(ref mut runner) = *runner { - // This check makes sure that calls to `request_redraw()` during `EventsCleared` - // handling dispatch `RedrawRequested` immediately after `EventsCleared`, without - // spinning up a new event loop iteration. We do this because that's what the API - // says to do. - let runner_state = runner.runner_state; - let mut request_redraw = || { - runner.call_event_handler(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: RedrawRequested, - }); - }; - match runner_state { - RunnerState::Idle(..) | RunnerState::DeferredNewEvents(..) => request_redraw(), - RunnerState::HandlingEvents => { - winuser::RedrawWindow( - window, - ptr::null(), - ptr::null_mut(), - winuser::RDW_INTERNALPAINT, - ); - } - _ => (), - } - } + winuser::WM_NCDESTROY => { + remove_window_subclass::(window); + subclass_input.subclass_removed.set(true); 0 } + winuser::WM_PAINT => { - use crate::event::WindowEvent::RedrawRequested; - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: RedrawRequested, - }); + if subclass_input.shared_data.runner_shared.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. + winuser::RedrawWindow( + window, + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); + } else { + let managing_redraw = + flush_paint_messages(Some(window), &subclass_input.shared_data.runner_shared); + subclass_input.send_event(Event::RedrawRequested(RootWindowId(WindowId(window)))); + if managing_redraw { + subclass_input + .shared_data + .runner_shared + .redraw_events_cleared(); + process_control_flow(&subclass_input.shared_data.runner_shared); + } + } + commctrl::DefSubclassProc(window, msg, wparam, lparam) } @@ -1034,8 +1008,11 @@ unsafe extern "system" fn public_window_callback( winuser::MonitorFromRect(&new_rect, winuser::MONITOR_DEFAULTTONULL); match fullscreen { Fullscreen::Borderless(ref mut fullscreen_monitor) => { - if new_monitor != fullscreen_monitor.inner.hmonitor() - && new_monitor != ptr::null_mut() + if new_monitor != ptr::null_mut() + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.inner.hmonitor()) + .unwrap_or(true) { if let Ok(new_monitor_info) = monitor::get_monitor_info(new_monitor) { let new_monitor_rect = new_monitor_info.rcMonitor; @@ -1044,9 +1021,9 @@ unsafe extern "system" fn public_window_callback( window_pos.cx = new_monitor_rect.right - new_monitor_rect.left; window_pos.cy = new_monitor_rect.bottom - new_monitor_rect.top; } - *fullscreen_monitor = crate::monitor::MonitorHandle { - inner: monitor::MonitorHandle::new(new_monitor), - }; + *fullscreen_monitor = Some(crate::monitor::MonitorHandle { + inner: MonitorHandle::new(new_monitor), + }); } } Fullscreen::Exclusive(ref video_mode) => { @@ -1071,12 +1048,11 @@ unsafe extern "system" fn public_window_callback( let windowpos = lparam as *const winuser::WINDOWPOS; if (*windowpos).flags & winuser::SWP_NOMOVE != winuser::SWP_NOMOVE { - let dpi_factor = hwnd_scale_factor(window); - let logical_position = - LogicalPosition::from_physical(((*windowpos).x, (*windowpos).y), dpi_factor); + let physical_position = + PhysicalPosition::new((*windowpos).x as i32, (*windowpos).y as i32); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: Moved(logical_position), + event: Moved(physical_position), }); } @@ -1089,11 +1065,10 @@ unsafe extern "system" fn public_window_callback( let w = LOWORD(lparam as DWORD) as u32; let h = HIWORD(lparam as DWORD) as u32; - let dpi_factor = hwnd_scale_factor(window); - let logical_size = LogicalSize::from_physical((w, h), dpi_factor); + let physical_size = PhysicalSize::new(w, h); let event = Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: Resized(logical_size), + event: Resized(physical_size), }; { @@ -1145,7 +1120,18 @@ unsafe extern "system" fn public_window_callback( 0 } + // this is necessary for us to maintain minimize/restore state winuser::WM_SYSCOMMAND => { + if wparam == winuser::SC_RESTORE { + let mut w = subclass_input.window_state.lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, false)); + } + if wparam == winuser::SC_MINIMIZE { + let mut w = subclass_input.window_state.lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, true)); + } + // Send `WindowEvent::Minimized` here if we decide to implement one + if wparam == winuser::SC_SCREENSAVE { let window_state = subclass_input.window_state.lock(); if window_state.fullscreen.is_some() { @@ -1171,9 +1157,7 @@ unsafe extern "system" fn public_window_callback( if mouse_was_outside_window { subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: CursorEntered { - device_id: DEVICE_ID, - }, + event: CursorEntered, }); // Calling TrackMouseEvent in order to receive mouse leave events. @@ -1187,17 +1171,27 @@ unsafe extern "system" fn public_window_callback( let x = windowsx::GET_X_LPARAM(lparam) as f64; let y = windowsx::GET_Y_LPARAM(lparam) as f64; - let dpi_factor = hwnd_scale_factor(window); - let position = LogicalPosition::from_physical((x, y), dpi_factor); + 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 + let mut w = subclass_input.window_state.lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + if cursor_moved { + update_modifiers(window, subclass_input); - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: CursorMoved { - device_id: DEVICE_ID, - position, - modifiers: event::get_key_mods(), - }, - }); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { + position, + modifiers: event::get_key_mods(), + }, + }); + } 0 } @@ -1213,9 +1207,7 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: CursorLeft { - device_id: DEVICE_ID, - }, + event: CursorLeft, }); 0 @@ -1228,10 +1220,11 @@ unsafe extern "system" fn public_window_callback( let value = value as i32; let value = value as f32 / winuser::WHEEL_DELTA as f32; + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::MouseWheel { - device_id: DEVICE_ID, delta: LineDelta(0.0, value), phase: TouchPhase::Moved, modifiers: event::get_key_mods(), @@ -1248,10 +1241,11 @@ unsafe extern "system" fn public_window_callback( let value = value as i32; let value = value as f32 / winuser::WHEEL_DELTA as f32; + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::MouseWheel { - device_id: DEVICE_ID, delta: LineDelta(value, 0.0), phase: TouchPhase::Moved, modifiers: event::get_key_mods(), @@ -1267,16 +1261,19 @@ unsafe extern "system" fn public_window_callback( commctrl::DefSubclassProc(window, msg, wparam, lparam) } else { if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { + update_modifiers(window, subclass_input); + + #[allow(deprecated)] subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, input: KeyboardInput { state: Pressed, scancode, virtual_keycode: vkey, modifiers: event::get_key_mods(), }, + is_synthetic: false, }, }); // Windows doesn't emit a delete character by default, but in order to make it @@ -1295,16 +1292,19 @@ unsafe extern "system" fn public_window_callback( winuser::WM_KEYUP | winuser::WM_SYSKEYUP => { use crate::event::ElementState::Released; if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { + update_modifiers(window, subclass_input); + + #[allow(deprecated)] subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, input: KeyboardInput { state: Released, scancode, virtual_keycode: vkey, modifiers: event::get_key_mods(), }, + is_synthetic: false, }, }); } @@ -1316,10 +1316,11 @@ unsafe extern "system" fn public_window_callback( capture_mouse(window, &mut *subclass_input.window_state.lock()); + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, button: Left, modifiers: event::get_key_mods(), @@ -1333,12 +1334,13 @@ unsafe extern "system" fn public_window_callback( ElementState::Released, MouseButton::Left, WindowEvent::MouseInput, }; - release_mouse(&mut *subclass_input.window_state.lock()); + release_mouse(subclass_input.window_state.lock()); + + update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, button: Left, modifiers: event::get_key_mods(), @@ -1354,10 +1356,11 @@ unsafe extern "system" fn public_window_callback( capture_mouse(window, &mut *subclass_input.window_state.lock()); + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, button: Right, modifiers: event::get_key_mods(), @@ -1371,12 +1374,13 @@ unsafe extern "system" fn public_window_callback( ElementState::Released, MouseButton::Right, WindowEvent::MouseInput, }; - release_mouse(&mut *subclass_input.window_state.lock()); + release_mouse(subclass_input.window_state.lock()); + + update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, button: Right, modifiers: event::get_key_mods(), @@ -1392,10 +1396,11 @@ unsafe extern "system" fn public_window_callback( capture_mouse(window, &mut *subclass_input.window_state.lock()); + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, button: Middle, modifiers: event::get_key_mods(), @@ -1409,12 +1414,13 @@ unsafe extern "system" fn public_window_callback( ElementState::Released, MouseButton::Middle, WindowEvent::MouseInput, }; - release_mouse(&mut *subclass_input.window_state.lock()); + release_mouse(subclass_input.window_state.lock()); + + update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, button: Middle, modifiers: event::get_key_mods(), @@ -1431,12 +1437,13 @@ unsafe extern "system" fn public_window_callback( capture_mouse(window, &mut *subclass_input.window_state.lock()); + update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, - button: Other(xbutton as u8), + button: Other(xbutton), modifiers: event::get_key_mods(), }, }); @@ -1449,132 +1456,30 @@ unsafe extern "system" fn public_window_callback( }; let xbutton = winuser::GET_XBUTTON_WPARAM(wparam); - release_mouse(&mut *subclass_input.window_state.lock()); + release_mouse(subclass_input.window_state.lock()); + + update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, - button: Other(xbutton as u8), + button: Other(xbutton), modifiers: event::get_key_mods(), }, }); 0 } - winuser::WM_INPUT_DEVICE_CHANGE => { - let event = match wparam as _ { - winuser::GIDC_ARRIVAL => DeviceEvent::Added, - winuser::GIDC_REMOVAL => DeviceEvent::Removed, - _ => unreachable!(), - }; - - subclass_input.send_event(Event::DeviceEvent { - device_id: wrap_device_id(lparam as _), - event, - }); - - 0 - } - - winuser::WM_INPUT => { - use crate::event::{ - DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}, - ElementState::{Pressed, Released}, - MouseScrollDelta::LineDelta, - }; - - if let Some(data) = get_raw_input_data(lparam as _) { - let device_id = wrap_device_id(data.header.hDevice as _); - - if data.header.dwType == winuser::RIM_TYPEMOUSE { - let mouse = data.data.mouse(); - - if util::has_flag(mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { - let x = mouse.lLastX as f64; - let y = mouse.lLastY as f64; - - if x != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 0, value: x }, - }); - } - - if y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 1, value: y }, - }); - } - - if x != 0.0 || y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseMotion { delta: (x, y) }, - }); - } - } - - if util::has_flag(mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { - let delta = mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseWheel { - delta: LineDelta(0.0, delta as f32), - }, - }); - } - - let button_state = get_raw_mouse_button_state(mouse.usButtonFlags); - // Left, middle, and right, respectively. - for (index, 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 _; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Button { button, state }, - }); - } - } - } else if data.header.dwType == winuser::RIM_TYPEKEYBOARD { - let keyboard = data.data.keyboard(); - - let pressed = keyboard.Message == winuser::WM_KEYDOWN - || keyboard.Message == winuser::WM_SYSKEYDOWN; - let released = keyboard.Message == winuser::WM_KEYUP - || keyboard.Message == winuser::WM_SYSKEYUP; - - if pressed || released { - let state = if pressed { Pressed } else { Released }; - - let scancode = keyboard.MakeCode as _; - let extended = util::has_flag(keyboard.Flags, winuser::RI_KEY_E0 as _) - | util::has_flag(keyboard.Flags, winuser::RI_KEY_E1 as _); - if let Some((vkey, scancode)) = - handle_extended_keys(keyboard.VKey as _, scancode, extended) - { - let virtual_keycode = vkey_to_winit_vkey(vkey); - - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Key(KeyboardInput { - scancode, - state, - virtual_keycode, - modifiers: event::get_key_mods(), - }), - }); - } - } - } + winuser::WM_CAPTURECHANGED => { + // lparam here is a handle to the window which is gaining mouse capture. + // If it is the same as our window, then we're essentially retaining the capture. This + // can happen if `SetCapture` is called on our window when it already has the mouse + // capture. + if lparam != window as isize { + subclass_input.window_state.lock().mouse.capture_count = 0; } - - commctrl::DefSubclassProc(window, msg, wparam, lparam) + 0 } winuser::WM_TOUCH => { @@ -1589,7 +1494,6 @@ unsafe extern "system" fn public_window_callback( mem::size_of::() as INT, ) > 0 { - let dpi_factor = hwnd_scale_factor(window); for input in &inputs { let mut location = POINT { x: input.x / 100, @@ -1602,7 +1506,7 @@ unsafe extern "system" fn public_window_callback( let x = location.x as f64 + (input.x % 100) as f64 / 100f64; let y = location.y as f64 + (input.y % 100) as f64 / 100f64; - let location = LogicalPosition::from_physical((x, y), dpi_factor); + let location = PhysicalPosition::new(x, y); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::Touch(Touch { @@ -1618,7 +1522,6 @@ unsafe extern "system" fn public_window_callback( location, force: None, // WM_TOUCH doesn't support pressure information id: input.dwID as u64, - device_id: DEVICE_ID, }), }); } @@ -1663,7 +1566,6 @@ unsafe extern "system" fn public_window_callback( return 0; } - let dpi_factor = hwnd_scale_factor(window); // 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 // row of the returned array @@ -1741,7 +1643,7 @@ unsafe extern "system" fn public_window_callback( let x = location.x as f64 + x.fract(); let y = location.y as f64 + y.fract(); - let location = LogicalPosition::from_physical((x, y), dpi_factor); + let location = PhysicalPosition::new(x, y); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::Touch(Touch { @@ -1758,7 +1660,6 @@ unsafe extern "system" fn public_window_callback( location, force, id: pointer_info.pointerId as u64, - device_id: DEVICE_ID, }), }); } @@ -1769,7 +1670,29 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_SETFOCUS => { - use crate::event::WindowEvent::Focused; + use crate::event::{ElementState::Released, WindowEvent::Focused}; + for windows_keycode in event::get_pressed_keys() { + let scancode = + winuser::MapVirtualKeyA(windows_keycode as _, winuser::MAPVK_VK_TO_VSC); + let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); + + update_modifiers(window, subclass_input); + + #[allow(deprecated)] + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::KeyboardInput { + input: KeyboardInput { + scancode, + virtual_keycode, + state: Released, + modifiers: event::get_key_mods(), + }, + is_synthetic: true, + }, + }) + } + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: Focused(true), @@ -1779,7 +1702,37 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_KILLFOCUS => { - use crate::event::WindowEvent::Focused; + use crate::event::{ + ElementState::Released, + ModifiersState, + WindowEvent::{Focused, ModifiersChanged}, + }; + for windows_keycode in event::get_pressed_keys() { + let scancode = + winuser::MapVirtualKeyA(windows_keycode as _, winuser::MAPVK_VK_TO_VSC); + let virtual_keycode = event::vkey_to_winit_vkey(windows_keycode); + + #[allow(deprecated)] + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::KeyboardInput { + input: KeyboardInput { + scancode, + virtual_keycode, + state: Released, + modifiers: event::get_key_mods(), + }, + is_synthetic: true, + }, + }) + } + + subclass_input.window_state.lock().modifiers_state = ModifiersState::empty(); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(ModifiersState::empty()), + }); + subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: Focused(false), @@ -1790,11 +1743,11 @@ unsafe extern "system" fn public_window_callback( winuser::WM_SETCURSOR => { let set_cursor_to = { let window_state = subclass_input.window_state.lock(); - if window_state - .mouse - .cursor_flags() - .contains(CursorFlags::IN_WINDOW) - { + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement. + let in_client_area = LOWORD(lparam as DWORD) == winuser::HTCLIENT as WORD; + if in_client_area { Some(window_state.mouse.cursor) } else { None @@ -1822,19 +1775,17 @@ unsafe extern "system" fn public_window_callback( let window_state = subclass_input.window_state.lock(); if window_state.min_size.is_some() || window_state.max_size.is_some() { - let style = winuser::GetWindowLongA(window, winuser::GWL_STYLE) as DWORD; - let ex_style = winuser::GetWindowLongA(window, winuser::GWL_EXSTYLE) as DWORD; if let Some(min_size) = window_state.min_size { - let min_size = min_size.to_physical(window_state.dpi_factor); - let (width, height) = adjust_size(min_size, style, ex_style); + let min_size = min_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = util::adjust_size(window, min_size).into(); (*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.dpi_factor); - let (width, height) = adjust_size(max_size, style, ex_style); + let max_size = max_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = util::adjust_size(window, max_size).into(); (*mmi).ptMaxTrackSize = POINT { x: width as i32, y: height as i32, @@ -1848,47 +1799,242 @@ unsafe extern "system" fn public_window_callback( // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change // DPI, therefore all applications are closed while DPI is changing. winuser::WM_DPICHANGED => { - use crate::event::WindowEvent::HiDpiFactorChanged; + use crate::event::WindowEvent::ScaleFactorChanged; // This message actually provides two DPI values - x and y. However MSDN says that // "you only need to use either the X-axis or the Y-axis value when scaling your // application since they are the same". // https://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx let new_dpi_x = u32::from(LOWORD(wparam as DWORD)); - let new_dpi_factor = dpi_to_scale_factor(new_dpi_x); + let new_scale_factor = dpi_to_scale_factor(new_dpi_x); + let old_scale_factor: f64; let allow_resize = { let mut window_state = subclass_input.window_state.lock(); - let old_dpi_factor = window_state.dpi_factor; - window_state.dpi_factor = new_dpi_factor; + old_scale_factor = window_state.scale_factor; + window_state.scale_factor = new_scale_factor; - new_dpi_factor != old_dpi_factor && window_state.fullscreen.is_none() + if new_scale_factor == old_scale_factor { + return 0; + } + + window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED) }; - // This prevents us from re-applying DPI adjustment to the restored size after exiting - // fullscreen (the restored size is already DPI adjusted). - if allow_resize { - // Resize window to the size suggested by Windows. - let rect = &*(lparam as *const RECT); - winuser::SetWindowPos( - window, - ptr::null_mut(), - rect.left, - rect.top, - rect.right - rect.left, - rect.bottom - rect.top, - winuser::SWP_NOZORDER | winuser::SWP_NOACTIVATE, - ); + let style = winuser::GetWindowLongW(window, winuser::GWL_STYLE) as _; + let style_ex = winuser::GetWindowLongW(window, winuser::GWL_EXSTYLE) as _; + + // New size as suggested by Windows. + let suggested_rect = *(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 + // the outer rect, so we instead adjust the window rect to get the decoration margins + // and remove them from the outer size. + let margin_left: i32; + let margin_top: i32; + // let margin_right: i32; + // let margin_bottom: i32; + { + let adjusted_rect = + util::adjust_window_rect_with_styles(window, style, style_ex, suggested_rect) + .unwrap_or(suggested_rect); + margin_left = suggested_rect.left - adjusted_rect.left; + margin_top = suggested_rect.top - adjusted_rect.top; + // margin_right = adjusted_rect.right - suggested_rect.right; + // margin_bottom = adjusted_rect.bottom - suggested_rect.bottom; } - subclass_input.send_event(Event::WindowEvent { + let old_physical_inner_rect = { + let mut old_physical_inner_rect = mem::zeroed(); + winuser::GetClientRect(window, &mut old_physical_inner_rect); + let mut origin = mem::zeroed(); + winuser::ClientToScreen(window, &mut origin); + + old_physical_inner_rect.left += origin.x; + old_physical_inner_rect.right += origin.x; + old_physical_inner_rect.top += origin.y; + old_physical_inner_rect.bottom += origin.y; + + old_physical_inner_rect + }; + let old_physical_inner_size = PhysicalSize::new( + (old_physical_inner_rect.right - old_physical_inner_rect.left) as u32, + (old_physical_inner_rect.bottom - old_physical_inner_rect.top) as u32, + ); + + // `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 { + // 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 + .to_logical::(old_scale_factor) + .to_physical::(new_scale_factor), + false => old_physical_inner_size, + }; + + let _ = subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: HiDpiFactorChanged(new_dpi_factor), + event: ScaleFactorChanged { + scale_factor: new_scale_factor, + new_inner_size: &mut new_physical_inner_size, + }, }); + let dragging_window: bool; + + { + let window_state = subclass_input.window_state.lock(); + dragging_window = window_state + .window_flags() + .contains(WindowFlags::MARKER_IN_SIZE_MOVE); + // Unset maximized if we're changing the window's size. + if new_physical_inner_size != old_physical_inner_size { + WindowState::set_window_flags(window_state, window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + } + } + + let new_outer_rect: RECT; + { + let suggested_ul = ( + suggested_rect.left + margin_left, + suggested_rect.top + margin_top, + ); + + let mut conservative_rect = RECT { + left: suggested_ul.0, + top: suggested_ul.1, + right: suggested_ul.0 + new_physical_inner_size.width as LONG, + bottom: suggested_ul.1 + new_physical_inner_size.height as LONG, + }; + + conservative_rect = util::adjust_window_rect_with_styles( + window, + style, + style_ex, + conservative_rect, + ) + .unwrap_or(conservative_rect); + + // If we're dragging the window, offset the window so that the cursor's + // relative horizontal position in the title bar is preserved. + if dragging_window { + let bias = { + let cursor_pos = { + let mut pos = mem::zeroed(); + winuser::GetCursorPos(&mut pos); + pos + }; + let suggested_cursor_horizontal_ratio = (cursor_pos.x - suggested_rect.left) + as f64 + / (suggested_rect.right - suggested_rect.left) as f64; + + (cursor_pos.x + - (suggested_cursor_horizontal_ratio + * (conservative_rect.right - conservative_rect.left) as f64) + as LONG) + - conservative_rect.left + }; + conservative_rect.left += bias; + conservative_rect.right += bias; + } + + // 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 = winuser::MonitorFromWindow(window, 0); + let conservative_rect_monitor = winuser::MonitorFromRect(&conservative_rect, 0); + new_outer_rect = if conservative_rect_monitor == new_dpi_monitor { + conservative_rect + } else { + let get_monitor_rect = |monitor| { + let mut monitor_info = winuser::MONITORINFO { + cbSize: mem::size_of::() as _, + ..mem::zeroed() + }; + winuser::GetMonitorInfoW(monitor, &mut monitor_info); + monitor_info.rcMonitor + }; + let wrong_monitor = conservative_rect_monitor; + let wrong_monitor_rect = get_monitor_rect(wrong_monitor); + let new_monitor_rect = get_monitor_rect(new_dpi_monitor); + + // The direction to nudge the window in to get the window onto the monitor with + // the new DPI factor. We calculate this by seeing which monitor edges are + // shared and nudging away from the wrong monitor based on those. + let delta_nudge_to_dpi_monitor = ( + if wrong_monitor_rect.left == new_monitor_rect.right { + -1 + } else if wrong_monitor_rect.right == new_monitor_rect.left { + 1 + } else { + 0 + }, + if wrong_monitor_rect.bottom == new_monitor_rect.top { + 1 + } else if wrong_monitor_rect.top == new_monitor_rect.bottom { + -1 + } else { + 0 + }, + ); + + let abort_after_iterations = new_monitor_rect.right - new_monitor_rect.left + + new_monitor_rect.bottom + - new_monitor_rect.top; + for _ in 0..abort_after_iterations { + conservative_rect.left += delta_nudge_to_dpi_monitor.0; + conservative_rect.right += delta_nudge_to_dpi_monitor.0; + conservative_rect.top += delta_nudge_to_dpi_monitor.1; + conservative_rect.bottom += delta_nudge_to_dpi_monitor.1; + + if winuser::MonitorFromRect(&conservative_rect, 0) == new_dpi_monitor { + break; + } + } + + conservative_rect + }; + } + + winuser::SetWindowPos( + window, + ptr::null_mut(), + new_outer_rect.left, + new_outer_rect.top, + new_outer_rect.right - new_outer_rect.left, + new_outer_rect.bottom - new_outer_rect.top, + winuser::SWP_NOZORDER | winuser::SWP_NOACTIVATE, + ); + 0 } + winuser::WM_SETTINGCHANGE => { + use crate::event::WindowEvent::ThemeChanged; + + let preferred_theme = subclass_input.window_state.lock().preferred_theme; + + if preferred_theme == None { + let new_theme = try_theme(window, preferred_theme); + let mut window_state = subclass_input.window_state.lock(); + + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + mem::drop(window_state); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ThemeChanged(new_theme), + }); + } + } + + commctrl::DefSubclassProc(window, msg, wparam, lparam) + } + _ => { if msg == *DESTROY_MSG_ID { winuser::DestroyWindow(window); @@ -1899,52 +2045,20 @@ unsafe extern "system" fn public_window_callback( f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) }); 0 - } else if msg == *INITIAL_DPI_MSG_ID { - use crate::event::WindowEvent::HiDpiFactorChanged; - let scale_factor = dpi_to_scale_factor(wparam as u32); - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: HiDpiFactorChanged(scale_factor), - }); - // Automatically resize for actual DPI - let width = LOWORD(lparam as DWORD) as u32; - let height = HIWORD(lparam as DWORD) as u32; - let (adjusted_width, adjusted_height): (u32, u32) = - PhysicalSize::from_logical((width, height), scale_factor).into(); - // We're not done yet! `SetWindowPos` needs the window size, not the client area size. - let mut rect = RECT { - top: 0, - left: 0, - bottom: adjusted_height as LONG, - right: adjusted_width as LONG, - }; - let dw_style = winuser::GetWindowLongA(window, winuser::GWL_STYLE) as DWORD; - let b_menu = !winuser::GetMenu(window).is_null() as BOOL; - let dw_style_ex = winuser::GetWindowLongA(window, winuser::GWL_EXSTYLE) as DWORD; - winuser::AdjustWindowRectEx(&mut rect, dw_style, b_menu, dw_style_ex); - let outer_x = (rect.right - rect.left).abs() as c_int; - let outer_y = (rect.top - rect.bottom).abs() as c_int; - winuser::SetWindowPos( - window, - ptr::null_mut(), - 0, - 0, - outer_x, - outer_y, - winuser::SWP_NOMOVE - | winuser::SWP_NOREPOSITION - | winuser::SWP_NOZORDER - | winuser::SWP_NOACTIVATE, - ); - 0 } else { commctrl::DefSubclassProc(window, msg, wparam, lparam) } } - } + }; + + subclass_input + .shared_data + .runner_shared + .catch_unwind(callback) + .unwrap_or(-1) } -unsafe extern "system" fn thread_event_target_callback( +unsafe extern "system" fn thread_event_target_callback( window: HWND, msg: UINT, wparam: WPARAM, @@ -1952,80 +2066,297 @@ unsafe extern "system" fn thread_event_target_callback( _: UINT_PTR, subclass_input_ptr: DWORD_PTR, ) -> LRESULT { - let subclass_input = &mut *(subclass_input_ptr as *mut ThreadMsgTargetSubclassInput); - match msg { - winuser::WM_DESTROY => { - Box::from_raw(subclass_input); - drop(subclass_input); + let subclass_input = Box::from_raw(subclass_input_ptr as *mut ThreadMsgTargetSubclassInput); + + if msg != winuser::WM_PAINT { + winuser::RedrawWindow( + window, + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); + } + + let mut subclass_removed = false; + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indendation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + winuser::WM_NCDESTROY => { + remove_event_target_window_subclass::(window); + subclass_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. winuser::WM_PAINT => { winuser::ValidateRect(window, ptr::null()); - let queue_call_again = || { - winuser::RedrawWindow( - window, - ptr::null(), - ptr::null_mut(), - winuser::RDW_INTERNALPAINT, - ); - }; - let in_modal_loop = { - let runner = subclass_input.event_loop_runner.runner.borrow_mut(); - if let Some(ref runner) = *runner { - runner.in_modal_loop + // 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 subclass_input.shared_data.runner_shared.handling_events() { + if subclass_input.shared_data.runner_shared.should_buffer() { + // This branch can be triggered when a nested win32 event loop is triggered + // inside of the `event_handler` callback. + winuser::RedrawWindow( + window, + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); } else { - false + // 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, + &subclass_input.shared_data.runner_shared + )); + subclass_input + .shared_data + .runner_shared + .redraw_events_cleared(); + process_control_flow(&subclass_input.shared_data.runner_shared); } - }; - if in_modal_loop { - let mut msg = mem::zeroed(); - loop { - if 0 == winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0) { - break; + } + + // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediatly when opening them. + commctrl::DefSubclassProc(window, msg, wparam, lparam) + } + + winuser::WM_INPUT_DEVICE_CHANGE => { + use super::raw_input::RawDeviceInfo; + + let handle = lparam as HANDLE; + + match wparam as _ { + winuser::GIDC_ARRIVAL => { + if let Some(handle_info) = raw_input::get_raw_input_device_info(handle) { + let (device, event) = match handle_info { + RawDeviceInfo::Mouse(_) => { + let mouse_id = MouseId(handle); + ( + DeviceId::Mouse(mouse_id), + Event::MouseEvent(mouse_id.into(), MouseEvent::Added), + ) + } + RawDeviceInfo::Keyboard(_) => { + let keyboard_id = KeyboardId(handle); + ( + DeviceId::Keyboard(keyboard_id), + Event::KeyboardEvent(keyboard_id.into(), KeyboardEvent::Added), + ) + } + RawDeviceInfo::Hid(_) => match Gamepad::new(handle) { + Some(gamepad) => { + let gamepad_handle = GamepadHandle { + handle, + shared_data: gamepad.shared_data(), + }; + + ( + DeviceId::Gamepad(gamepad_handle.clone(), gamepad), + Event::GamepadEvent( + gamepad_handle.into(), + GamepadEvent::Added, + ), + ) + } + None => { + let hid_id = HidId(handle); + ( + DeviceId::Hid(hid_id.into()), + Event::HidEvent(hid_id.into(), HidEvent::Added), + ) + } + }, + }; + + subclass_input + .shared_data + .active_device_ids + .borrow_mut() + .insert(handle, device); + subclass_input.send_event(event); } - // Clear all paint/timer messages from the queue before sending the events cleared message. - match msg.message { - // Flush the event queue of WM_PAINT messages. - winuser::WM_PAINT | winuser::WM_TIMER => { - // Remove the message from the message queue. - winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 1); - - if msg.hwnd != window { - winuser::TranslateMessage(&mut msg); - winuser::DispatchMessageW(&mut msg); + } + winuser::GIDC_REMOVAL => { + let removed_device = subclass_input + .shared_data + .active_device_ids + .borrow_mut() + .remove(&handle); + if let Some(device_id) = removed_device { + let event = match device_id { + DeviceId::Mouse(mouse_id) => { + Event::MouseEvent(mouse_id.into(), MouseEvent::Removed) + } + DeviceId::Keyboard(keyboard_id) => { + Event::KeyboardEvent(keyboard_id.into(), KeyboardEvent::Removed) } + DeviceId::Hid(hid_id) => { + Event::HidEvent(hid_id.into(), HidEvent::Removed) + } + DeviceId::Gamepad(gamepad_handle, _) => { + Event::GamepadEvent(gamepad_handle.into(), GamepadEvent::Removed) + } + }; + subclass_input.send_event(event); + } + } + _ => unreachable!(), + } + + 0 + } + + winuser::WM_INPUT => { + use crate::event::{ + ElementState::{Pressed, Released}, + MouseButton, + }; + + match get_raw_input_data(lparam as _) { + Some(RawInputData::Mouse { + device_handle, + raw_mouse, + }) => { + let mouse_handle = MouseId(device_handle).into(); + + if util::has_flag(raw_mouse.usFlags, winuser::MOUSE_MOVE_ABSOLUTE) { + let x = raw_mouse.lLastX as f64; + let y = raw_mouse.lLastY as f64; + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::MovedAbsolute(PhysicalPosition { x, y }), + )); + } + } else if util::has_flag(raw_mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { + let x = raw_mouse.lLastX as f64; + let y = raw_mouse.lLastY as f64; + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::MovedRelative(x, y), + )); } - // If the message isn't one of those three, it may be handled by the modal - // loop so we should return control flow to it. - _ => { - queue_call_again(); - return 0; + } + + if util::has_flag(raw_mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { + // TODO: HOW IS RAW WHEEL DELTA HANDLED ON OTHER PLATFORMS? + let delta = raw_mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Wheel(0.0, delta as f64), + )); + } + // Check if there's horizontal wheel movement. + if util::has_flag(raw_mouse.usButtonFlags, 0x0800) { + // TODO: HOW IS RAW WHEEL DELTA HANDLED ON OTHER PLATFORMS? + let delta = raw_mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Wheel(delta as f64, 0.0), + )); + } + + let button_state = get_raw_mouse_button_state(raw_mouse.usButtonFlags); + for (index, state) in button_state + .iter() + .cloned() + .enumerate() + .filter_map(|(i, state)| state.map(|s| (i, s))) + { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Button { + state, + button: match index { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + _ => MouseButton::Other(index as u16 - 2), + }, + }, + )); + } + } + Some(RawInputData::Keyboard { + device_handle, + raw_keyboard, + }) => { + let keyboard_id = KeyboardId(device_handle).into(); + + let pressed = raw_keyboard.Message == winuser::WM_KEYDOWN + || raw_keyboard.Message == winuser::WM_SYSKEYDOWN; + let released = raw_keyboard.Message == winuser::WM_KEYUP + || raw_keyboard.Message == winuser::WM_SYSKEYUP; + + if pressed || released { + let state = if pressed { Pressed } else { Released }; + + let scancode = raw_keyboard.MakeCode as _; + let extended = util::has_flag(raw_keyboard.Flags, winuser::RI_KEY_E0 as _) + | util::has_flag(raw_keyboard.Flags, winuser::RI_KEY_E1 as _); + if let Some((vkey, scancode)) = + handle_extended_keys(raw_keyboard.VKey as _, scancode, extended) + { + let virtual_keycode = vkey_to_winit_vkey(vkey); + + #[allow(deprecated)] + subclass_input.send_event(Event::KeyboardEvent( + keyboard_id, + KeyboardEvent::Input(KeyboardInput { + scancode, + state, + virtual_keycode, + modifiers: event::get_key_mods(), + }), + )); } } } + Some(RawInputData::Hid { + device_handle, + mut raw_hid, + }) => { + let mut gamepad_handle_opt: Option = None; + let mut gamepad_events = vec![]; - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); - if let Some(ref mut runner) = *runner { - runner.events_cleared(); - match runner.control_flow { - // Waiting is handled by the modal loop. - ControlFlow::Exit | ControlFlow::Wait => runner.new_events(), - ControlFlow::WaitUntil(resume_time) => { - wait_until_time_or_msg(resume_time); - runner.new_events(); - queue_call_again(); + { + let mut devices = subclass_input.shared_data.active_device_ids.borrow_mut(); + let device_id = devices.get_mut(&device_handle); + if let Some(DeviceId::Gamepad(gamepad_handle, ref mut gamepad)) = device_id + { + gamepad.update_state(&mut raw_hid.raw_input); + gamepad_events = gamepad.get_gamepad_events(); + gamepad_handle_opt = Some(gamepad_handle.clone().into()); } - ControlFlow::Poll => { - runner.new_events(); - queue_call_again(); + } + + if let Some(gamepad_handle) = gamepad_handle_opt { + for gamepad_event in gamepad_events { + subclass_input.send_event(Event::GamepadEvent( + gamepad_handle.clone(), + gamepad_event, + )); } + } else { + subclass_input.send_event(Event::HidEvent( + HidId(device_handle).into(), + HidEvent::Data(raw_hid.raw_input), + )); } } + None => (), } - 0 + + commctrl::DefSubclassProc(window, msg, wparam, lparam) } + _ if msg == *USER_EVENT_MSG_ID => { if let Ok(event) = subclass_input.user_event_receiver.recv() { subclass_input.send_event(Event::UserEvent(event)); @@ -2037,6 +2368,59 @@ unsafe extern "system" fn thread_event_target_callback( function(); 0 } + _ if msg == *PROCESS_NEW_EVENTS_MSG_ID => { + winuser::PostThreadMessageW( + subclass_input.shared_data.runner_shared.wait_thread_id(), + *CANCEL_WAIT_UNTIL_MSG_ID, + 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) = + subclass_input.shared_data.runner_shared.control_flow() + { + let mut msg = mem::zeroed(); + while Instant::now() < wait_until { + if 0 != winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0) { + // 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 == winuser::WM_PAINT { + let mut rect = mem::zeroed(); + if 0 == winuser::GetUpdateRect(msg.hwnd, &mut rect, 0) { + winuser::RedrawWindow( + msg.hwnd, + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); + } + } + + break; + } + } + } + subclass_input.shared_data.runner_shared.poll(); + 0 + } _ => commctrl::DefSubclassProc(window, msg, wparam, lparam), + }; + + let result = subclass_input + .shared_data + .runner_shared + .catch_unwind(callback) + .unwrap_or(-1); + if subclass_removed { + mem::drop(subclass_input); + } else { + Box::into_raw(subclass_input); } + result } diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 0000000000..4f90966c43 --- /dev/null +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -0,0 +1,443 @@ +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::{HashSet, VecDeque}, + mem, panic, ptr, + rc::Rc, + time::Instant, +}; + +use winapi::{ + shared::{minwindef::DWORD, windef::HWND}, + um::winuser, +}; + +use crate::{ + dpi::PhysicalSize, + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, + platform_impl::platform::util, + window::WindowId, +}; + +pub(crate) type EventLoopRunnerShared = Rc>; +pub(crate) struct EventLoopRunner { + // The event loop's win32 handles + thread_msg_target: HWND, + wait_thread_id: DWORD, + + control_flow: Cell, + runner_state: Cell, + last_events_cleared: Cell, + + event_handler: Cell, &mut ControlFlow)>>>, + event_buffer: RefCell>>, + + owned_windows: Cell>, + + panic_error: Cell>, +} + +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 { + /// 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. + 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>), + ScaleFactorChanged(WindowId, f64, PhysicalSize), +} + +impl EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND, wait_thread_id: DWORD) -> EventLoopRunner { + EventLoopRunner { + thread_msg_target, + wait_thread_id, + runner_state: Cell::new(RunnerState::Uninitialized), + control_flow: Cell::new(ControlFlow::Poll), + 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()), + } + } + + pub(crate) unsafe fn set_event_handler(&self, f: F) + where + F: FnMut(Event<'_, T>, &mut ControlFlow), + { + let old_event_handler = self.event_handler.replace(mem::transmute::< + Option, &mut ControlFlow)>>, + Option, &mut ControlFlow)>>, + >(Some(Box::new(f)))); + assert!(old_event_handler.is_none()); + } + + pub(crate) fn reset_runner(&self) { + let EventLoopRunner { + thread_msg_target: _, + wait_thread_id: _, + runner_state, + panic_error, + control_flow, + last_events_cleared: _, + event_handler, + event_buffer: _, + owned_windows: _, + } = self; + runner_state.set(RunnerState::Uninitialized); + panic_error.set(None); + control_flow.set(ControlFlow::Poll); + event_handler.set(None); + } +} + +/// State retrieval functions. +impl EventLoopRunner { + pub fn thread_msg_target(&self) -> HWND { + self.thread_msg_target + } + + pub fn wait_thread_id(&self) -> DWORD { + 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), + None => Ok(()), + } + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub fn handling_events(&self) -> bool { + self.runner_state.get() != RunnerState::Idle + } + + pub fn should_buffer(&self) -> bool { + let handler = self.event_handler.take(); + let should_buffer = handler.is_none(); + self.event_handler.set(handler); + should_buffer + } +} + +/// Misc. functions +impl EventLoopRunner { + pub fn catch_unwind(&self, f: impl FnOnce() -> R) -> Option { + let panic_error = self.panic_error.take(); + if panic_error.is_none() { + let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); + + // Check to see if the panic error was set in a re-entrant call to catch_unwind inside + // of `f`. If it was, that error takes priority. If it wasn't, check if our call to + // catch_unwind caught any panics and set panic_error appropriately. + match self.panic_error.take() { + None => match result { + Ok(r) => Some(r), + Err(e) => { + self.panic_error.set(Some(e)); + None + } + }, + Some(e) => { + self.panic_error.set(Some(e)); + None + } + } + } else { + self.panic_error.set(panic_error); + 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) { + 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); + } + self.call_event_handler(event); + } 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. + self.event_buffer + .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) { + self.move_state_to(RunnerState::Destroyed); + } + + unsafe fn call_event_handler(&self, event: Event<'_, T>) { + 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 control_flow != ControlFlow::Exit { + event_handler(event, &mut control_flow); + } else { + event_handler(event, &mut ControlFlow::Exit); + } + + assert!(self.event_handler.replace(Some(event_handler)).is_none()); + self.control_flow.set(control_flow); + }); + } + + unsafe 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 + // of the loop's body and attempts to add events to the event buffer while in + // `process_event` will fail. + let buffered_event_opt = self.event_buffer.borrow_mut().pop_front(); + match buffered_event_opt { + Some(e) => e.dispatch_event(|e| self.call_event_handler(e)), + None => break, + } + } + } + + /// Dispatch control flow events (`NewEvents`, `MainEventsCleared`, `RedrawEventsCleared`, and + /// `LoopDestroyed`) as necessary to bring the internal `RunnerState` to the new runner state. + /// + /// The state transitions are defined as follows: + /// + /// ```text + /// Uninitialized + /// | + /// V + /// HandlingMainEvents + /// ^ | + /// | V + /// Idle <--- HandlingRedrawEvents + /// | + /// 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 + /// 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, + }; + + match ( + self.runner_state.replace(new_runner_state), + new_runner_state, + ) { + (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(); + } + (Uninitialized, Destroyed) => { + self.call_new_events(true); + self.call_event_handler(Event::MainEventsCleared); + self.call_redraw_events_cleared(); + self.call_event_handler(Event::LoopDestroyed); + } + (_, Uninitialized) => panic!("cannot move state to Uninitialized"), + + // State transitions that start the event handling process. + (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); + } + + (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(); + } + (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); + } + + (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::Exit) | (false, ControlFlow::Wait) => StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + }, + (false, ControlFlow::WaitUntil(requested_resume)) => { + if Instant::now() < requested_resume { + StartCause::WaitCancelled { + requested_resume: Some(requested_resume), + start: self.last_events_cleared.get(), + } + } else { + StartCause::ResumeTimeReached { + requested_resume, + start: self.last_events_cleared.get(), + } + } + } + }; + self.call_event_handler(Event::NewEvents(start_cause)); + self.dispatch_buffered_events(); + winuser::RedrawWindow( + self.thread_msg_target, + ptr::null(), + ptr::null_mut(), + winuser::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 { + match event { + Event::WindowEvent { + event: + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + window_id, + } => BufferedEvent::ScaleFactorChanged(window_id, scale_factor, *new_inner_size), + event => BufferedEvent::Event(event.to_static().unwrap()), + } + } + + pub fn dispatch_event(self, dispatch: impl FnOnce(Event<'_, T>)) { + match self { + Self::Event(event) => dispatch(event), + Self::ScaleFactorChanged(window_id, scale_factor, mut new_inner_size) => { + dispatch(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: &mut new_inner_size, + }, + }); + util::set_inner_size_physical( + (window_id.0).0, + new_inner_size.width as _, + new_inner_size.height as _, + ); + } + } + } +} diff --git a/src/platform_impl/windows/gamepad.rs b/src/platform_impl/windows/gamepad.rs new file mode 100644 index 0000000000..f4d3fb501d --- /dev/null +++ b/src/platform_impl/windows/gamepad.rs @@ -0,0 +1,87 @@ +use std::sync::Weak; + +use winapi::um::winnt::HANDLE; + +use crate::{ + event::device::{BatteryLevel, GamepadEvent, RumbleError}, + platform_impl::platform::raw_input::{get_raw_input_device_name, RawGamepad}, + platform_impl::platform::xinput::{self, XInputGamepad, XInputGamepadShared}, +}; + +#[derive(Debug)] +pub enum GamepadType { + Raw(RawGamepad), + XInput(XInputGamepad), +} + +#[derive(Clone)] +pub enum GamepadShared { + Raw(()), + XInput(Weak), + Dummy, +} + +#[derive(Debug)] +pub struct Gamepad { + handle: HANDLE, + backend: GamepadType, +} + +impl Gamepad { + pub fn new(handle: HANDLE) -> Option { + // TODO: Verify that this is an HID device + let name = get_raw_input_device_name(handle)?; + xinput::id_from_name(&name) + .and_then(XInputGamepad::new) + .map(GamepadType::XInput) + .or_else(|| RawGamepad::new(handle).map(GamepadType::Raw)) + .map(|backend| Gamepad { handle, backend }) + } + + pub unsafe fn update_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + match self.backend { + GamepadType::Raw(ref mut gamepad) => gamepad.update_state(raw_input_report), + GamepadType::XInput(ref mut gamepad) => gamepad.update_state(), + } + } + + pub fn get_gamepad_events(&self) -> Vec { + match self.backend { + GamepadType::Raw(ref gamepad) => gamepad.get_gamepad_events(), + GamepadType::XInput(ref gamepad) => gamepad.get_gamepad_events(), + } + } + + pub fn shared_data(&self) -> GamepadShared { + match self.backend { + GamepadType::Raw(_) => GamepadShared::Raw(()), + GamepadType::XInput(ref gamepad) => GamepadShared::XInput(gamepad.shared_data()), + } + } +} + +impl GamepadShared { + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => Ok(()), + GamepadShared::XInput(ref data) => data + .upgrade() + .map(|r| r.rumble(left_speed, right_speed)) + .unwrap_or(Err(RumbleError::DeviceNotConnected)), + } + } + + pub fn port(&self) -> Option { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => None, + GamepadShared::XInput(ref data) => data.upgrade().map(|r| r.port()), + } + } + + pub fn battery_level(&self) -> Option { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => None, + GamepadShared::XInput(ref data) => data.upgrade().and_then(|r| r.battery_level()), + } + } +} diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index c865053dbc..1308c7467e 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -1,15 +1,17 @@ -use std::{io, mem, os::windows::ffi::OsStrExt, path::Path, ptr}; +use std::{fmt, io, iter::once, mem, os::windows::ffi::OsStrExt, path::Path, ptr, sync::Arc}; use winapi::{ ctypes::{c_int, wchar_t}, shared::{ - minwindef::{BYTE, LPARAM, WPARAM}, + minwindef::{BYTE, LPARAM, WORD, WPARAM}, windef::{HICON, HWND}, }, + um::libloaderapi, um::winuser, }; -use crate::icon::{Icon, Pixel, PIXEL_SIZE}; +use crate::dpi::PhysicalSize; +use crate::icon::*; impl Pixel { fn to_bgra(&mut self) { @@ -17,92 +19,149 @@ impl Pixel { } } +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let mut rgba = self.rgba; + let pixel_count = rgba.len() / PIXEL_SIZE; + let mut and_mask = Vec::with_capacity(pixel_count); + let pixels = + unsafe { std::slice::from_raw_parts_mut(rgba.as_mut_ptr() as *mut Pixel, pixel_count) }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(std::u8::MAX)); // invert alpha channel + pixel.to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + winuser::CreateIcon( + ptr::null_mut(), + self.width as c_int, + self.height as c_int, + 1, + (PIXEL_SIZE * 8) as BYTE, + and_mask.as_ptr() as *const BYTE, + rgba.as_ptr() as *const BYTE, + ) as HICON + }; + if !handle.is_null() { + Ok(WinIcon::from_handle(handle)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } +} + #[derive(Debug)] pub enum IconType { Small = winuser::ICON_SMALL as isize, Big = winuser::ICON_BIG as isize, } -#[derive(Clone, Debug)] +#[derive(Debug)] +struct RaiiIcon { + handle: HICON, +} + +#[derive(Clone)] pub struct WinIcon { - pub handle: HICON, + inner: Arc, } unsafe impl Send for WinIcon {} impl WinIcon { - #[allow(dead_code)] - pub fn from_path>(path: P) -> Result { - let wide_path: Vec = path.as_ref().as_os_str().encode_wide().collect(); + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + pub fn from_path>( + path: P, + size: Option>, + ) -> Result { + let wide_path: Vec = path + .as_ref() + .as_os_str() + .encode_wide() + .chain(once(0)) + .collect(); + + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + let handle = unsafe { winuser::LoadImageW( ptr::null_mut(), wide_path.as_ptr() as *const wchar_t, winuser::IMAGE_ICON, - 0, // 0 indicates that we want to use the actual width - 0, // and height - winuser::LR_LOADFROMFILE, + width as c_int, + height as c_int, + winuser::LR_DEFAULTSIZE | winuser::LR_LOADFROMFILE, ) as HICON }; if !handle.is_null() { - Ok(WinIcon { handle }) + Ok(WinIcon::from_handle(handle)) } else { - Err(io::Error::last_os_error()) + Err(BadIcon::OsError(io::Error::last_os_error())) } } - pub fn from_icon(icon: Icon) -> Result { - Self::from_rgba(icon.rgba, icon.width, icon.height) - } - - pub fn from_rgba(mut rgba: Vec, width: u32, height: u32) -> Result { - assert_eq!(rgba.len() % PIXEL_SIZE, 0); - let pixel_count = rgba.len() / PIXEL_SIZE; - assert_eq!(pixel_count, (width * height) as usize); - let mut and_mask = Vec::with_capacity(pixel_count); - let pixels = rgba.as_mut_ptr() as *mut Pixel; // how not to write idiomatic Rust - for pixel_index in 0..pixel_count { - let pixel = unsafe { &mut *pixels.offset(pixel_index as isize) }; - and_mask.push(pixel.a.wrapping_sub(std::u8::MAX)); // invert alpha channel - pixel.to_bgra(); - } - assert_eq!(and_mask.len(), pixel_count); + pub fn from_resource( + resource_id: WORD, + size: Option>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); let handle = unsafe { - winuser::CreateIcon( - ptr::null_mut(), + winuser::LoadImageW( + libloaderapi::GetModuleHandleW(ptr::null_mut()), + winuser::MAKEINTRESOURCEW(resource_id), + winuser::IMAGE_ICON, width as c_int, height as c_int, - 1, - (PIXEL_SIZE * 8) as BYTE, - and_mask.as_ptr() as *const BYTE, - rgba.as_ptr() as *const BYTE, + winuser::LR_DEFAULTSIZE, ) as HICON }; if !handle.is_null() { - Ok(WinIcon { handle }) + Ok(WinIcon::from_handle(handle)) } else { - Err(io::Error::last_os_error()) + Err(BadIcon::OsError(io::Error::last_os_error())) } } + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?; + rgba_icon.into_windows_icon() + } + pub fn set_for_window(&self, hwnd: HWND, icon_type: IconType) { unsafe { winuser::SendMessageW( hwnd, winuser::WM_SETICON, icon_type as WPARAM, - self.handle as LPARAM, + self.as_raw_handle() as LPARAM, ); } } + + fn from_handle(handle: HICON) -> Self { + Self { + inner: Arc::new(RaiiIcon { handle }), + } + } } -impl Drop for WinIcon { +impl Drop for RaiiIcon { fn drop(&mut self) { unsafe { winuser::DestroyIcon(self.handle) }; } } +impl fmt::Debug for WinIcon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + (*self.inner).fmt(formatter) + } +} + pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { unsafe { winuser::SendMessageW(hwnd, winuser::WM_SETICON, icon_type as WPARAM, 0 as LPARAM); diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 34e9327d96..221c0fd551 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -1,20 +1,53 @@ #![cfg(target_os = "windows")] -use winapi::{self, shared::windef::HWND}; +use std::{ + cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, + fmt, + hash::{Hash, Hasher}, + ptr, +}; + +use winapi::{ + self, + shared::windef::{HMENU, HWND}, + um::winnt::HANDLE, +}; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + gamepad::GamepadShared, + icon::WinIcon, monitor::{MonitorHandle, VideoMode}, window::Window, }; -use crate::{event::DeviceId as RootDeviceId, window::Icon}; +pub use self::icon::WinIcon as PlatformIcon; + +use crate::event::device; +use crate::icon::Icon; +use crate::window::Theme; -#[derive(Clone, Default)] +#[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub parent: Option, + pub menu: Option, pub taskbar_icon: Option, pub no_redirection_bitmap: bool, + pub drag_and_drop: bool, + pub preferred_theme: Option, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> Self { + Self { + parent: None, + menu: None, + taskbar_icon: None, + no_redirection_bitmap: false, + drag_and_drop: true, + preferred_theme: None, + } + } } unsafe impl Send for PlatformSpecificWindowBuilderAttributes {} @@ -26,55 +59,166 @@ pub struct Cursor(pub *const winapi::ctypes::wchar_t); unsafe impl Send for Cursor {} unsafe impl Sync for Cursor {} +pub type OsError = std::io::Error; + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(u32); +pub struct WindowId(HWND); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} -impl DeviceId { +impl WindowId { pub unsafe fn dummy() -> Self { - DeviceId(0) + use std::ptr::null_mut; + + WindowId(null_mut()) } } -impl DeviceId { - pub fn persistent_identifier(&self) -> Option { - if self.0 != 0 { - raw_input::get_raw_input_device_name(self.0 as _) - } else { - None +macro_rules! device_id { + ($name:ident, $enumerate:ident) => { + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct $name(HANDLE); + + unsafe impl Send for $name {} + unsafe impl Sync for $name {} + + impl $name { + pub unsafe fn dummy() -> Self { + Self(ptr::null_mut()) + } + + pub fn persistent_identifier(&self) -> Option { + raw_input::get_raw_input_device_name(self.0) + } + + pub fn is_connected(&self) -> bool { + raw_input::get_raw_input_device_info(self.0).is_some() + } + + #[inline(always)] + pub fn handle(&self) -> HANDLE { + self.0 + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.$enumerate() + } } - } + + impl From<$name> for device::$name { + fn from(platform_id: $name) -> Self { + Self(platform_id) + } + } + }; } -// Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); +device_id!(MouseId, mouses); +device_id!(KeyboardId, keyboards); +device_id!(HidId, hids); -fn wrap_device_id(id: u32) -> RootDeviceId { - RootDeviceId(DeviceId(id)) +#[derive(Clone)] +pub(crate) struct GamepadHandle { + handle: HANDLE, + shared_data: GamepadShared, } -pub type OsError = std::io::Error; +unsafe impl Send for GamepadHandle where GamepadShared: Send {} +unsafe impl Sync for GamepadHandle where GamepadShared: Sync {} -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId(HWND); -unsafe impl Send for WindowId {} -unsafe impl Sync for WindowId {} - -impl WindowId { +impl GamepadHandle { pub unsafe fn dummy() -> Self { - use std::ptr::null_mut; + Self { + handle: ptr::null_mut(), + shared_data: GamepadShared::Dummy, + } + } - WindowId(null_mut()) + pub fn persistent_identifier(&self) -> Option { + raw_input::get_raw_input_device_name(self.handle) + } + + pub fn is_connected(&self) -> bool { + raw_input::get_raw_input_device_info(self.handle).is_some() + } + + #[inline(always)] + pub fn handle(&self) -> HANDLE { + self.handle + } + + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), device::RumbleError> { + self.shared_data.rumble(left_speed, right_speed) + } + + pub fn port(&self) -> Option { + self.shared_data.port() + } + + pub fn battery_level(&self) -> Option { + self.shared_data.battery_level() + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.gamepads() + } +} + +impl From for device::GamepadHandle { + fn from(platform_id: GamepadHandle) -> Self { + Self(platform_id) + } +} + +impl fmt::Debug for GamepadHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_tuple("GamepadHandle").field(&self.handle).finish() + } +} + +impl Eq for GamepadHandle {} +impl PartialEq for GamepadHandle { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + +impl Ord for GamepadHandle { + #[inline(always)] + fn cmp(&self, other: &Self) -> Ordering { + self.handle.cmp(&other.handle) + } +} +impl PartialOrd for GamepadHandle { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + self.handle.partial_cmp(&other.handle) + } +} + +impl Hash for GamepadHandle { + #[inline(always)] + fn hash(&self, state: &mut H) { + self.handle.hash(state); } } #[macro_use] mod util; +mod dark_mode; mod dpi; mod drop_handler; mod event; mod event_loop; +mod gamepad; mod icon; mod monitor; mod raw_input; mod window; mod window_state; +mod xinput; diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index 6705f334fe..ff88d102db 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -11,7 +11,7 @@ use std::{ io, mem, ptr, }; -use super::{util, EventLoop}; +use super::util; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, @@ -62,7 +62,7 @@ impl std::fmt::Debug for VideoMode { } impl VideoMode { - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { self.size.into() } @@ -126,24 +126,14 @@ pub fn current_monitor(hwnd: HWND) -> MonitorHandle { MonitorHandle::new(hmonitor) } -impl EventLoop { - // TODO: Investigate opportunities for caching - pub fn available_monitors(&self) -> VecDeque { - available_monitors() - } - - pub fn primary_monitor(&self) -> MonitorHandle { - primary_monitor() - } -} - impl Window { pub fn available_monitors(&self) -> VecDeque { available_monitors() } - pub fn primary_monitor(&self) -> MonitorHandle { - primary_monitor() + pub fn primary_monitor(&self) -> Option { + let monitor = primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) } } @@ -168,14 +158,6 @@ impl MonitorHandle { MonitorHandle(hmonitor) } - pub(crate) fn contains_point(&self, point: &POINT) -> bool { - let monitor_info = get_monitor_info(self.0).unwrap(); - point.x >= monitor_info.rcMonitor.left - && point.x <= monitor_info.rcMonitor.right - && point.y >= monitor_info.rcMonitor.top - && point.y <= monitor_info.rcMonitor.bottom - } - #[inline] pub fn name(&self) -> Option { let monitor_info = get_monitor_info(self.0).unwrap(); @@ -193,25 +175,25 @@ impl MonitorHandle { } #[inline] - pub fn size(&self) -> PhysicalSize { + pub fn size(&self) -> PhysicalSize { let monitor_info = get_monitor_info(self.0).unwrap(); PhysicalSize { - width: (monitor_info.rcMonitor.right - monitor_info.rcMonitor.left) as f64, - height: (monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top) as f64, + width: (monitor_info.rcMonitor.right - monitor_info.rcMonitor.left) as u32, + height: (monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top) as u32, } } #[inline] - pub fn position(&self) -> PhysicalPosition { + pub fn position(&self) -> PhysicalPosition { let monitor_info = get_monitor_info(self.0).unwrap(); PhysicalPosition { - x: monitor_info.rcMonitor.left as f64, - y: monitor_info.rcMonitor.top as f64, + x: monitor_info.rcMonitor.left, + y: monitor_info.rcMonitor.top, } } #[inline] - pub fn hidpi_factor(&self) -> f64 { + pub fn scale_factor(&self) -> f64 { dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) } diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 73b136a82f..a602392735 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -1,27 +1,43 @@ use std::{ + cmp::max, + fmt, mem::{self, size_of}, - ptr, + ptr, slice, }; use winapi::{ ctypes::wchar_t, shared::{ - hidusage::{HID_USAGE_GENERIC_KEYBOARD, HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC}, - minwindef::{TRUE, UINT, USHORT}, + hidpi::{ + HidP_GetButtonCaps, HidP_GetCaps, HidP_GetScaledUsageValue, HidP_GetUsageValue, + HidP_GetUsagesEx, HidP_GetValueCaps, HidP_Input, HIDP_STATUS_SUCCESS, HIDP_VALUE_CAPS, + PHIDP_PREPARSED_DATA, + }, + hidusage::{ + HID_USAGE_GENERIC_GAMEPAD, HID_USAGE_GENERIC_JOYSTICK, HID_USAGE_GENERIC_KEYBOARD, + HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, + }, + minwindef::{INT, TRUE, UINT, USHORT}, windef::HWND, }, um::{ - winnt::HANDLE, + winnt::{HANDLE, PCHAR}, winuser::{ self, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, RAWINPUTHEADER, - RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDI_DEVICEINFO, RIDI_DEVICENAME, RID_DEVICE_INFO, - RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, RID_INPUT, - RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, + RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDI_DEVICEINFO, RIDI_DEVICENAME, RIDI_PREPARSEDDATA, + RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, + RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }, }, }; -use crate::{event::ElementState, platform_impl::platform::util}; +use crate::{ + event::{ + device::{GamepadAxis, GamepadEvent}, + ElementState, + }, + platform_impl::platform::util, +}; #[allow(dead_code)] pub fn get_raw_input_device_list() -> Option> { @@ -51,8 +67,6 @@ pub fn get_raw_input_device_list() -> Option> { Some(buffer) } - -#[allow(dead_code)] pub enum RawDeviceInfo { Mouse(RID_DEVICE_INFO_MOUSE), Keyboard(RID_DEVICE_INFO_KEYBOARD), @@ -72,28 +86,27 @@ impl From for RawDeviceInfo { } } -#[allow(dead_code)] pub fn get_raw_input_device_info(handle: HANDLE) -> Option { let mut info: RID_DEVICE_INFO = unsafe { mem::zeroed() }; let info_size = size_of::() as UINT; info.cbSize = info_size; - let mut minimum_size = 0; + let mut data_size = info_size; let status = unsafe { winuser::GetRawInputDeviceInfoW( handle, RIDI_DEVICEINFO, &mut info as *mut _ as _, - &mut minimum_size, + &mut data_size, ) - }; + } as INT; - if status == UINT::max_value() || status == 0 { + if status <= 0 { return None; } - debug_assert_eq!(info_size, status); + debug_assert_eq!(info_size, status as _); Some(info.into()) } @@ -130,6 +143,43 @@ pub fn get_raw_input_device_name(handle: HANDLE) -> Option { Some(util::wchar_to_string(&name)) } +pub fn get_raw_input_pre_parse_info(handle: HANDLE) -> Option> { + let mut minimum_size = 0; + let status = unsafe { + winuser::GetRawInputDeviceInfoW( + handle, + RIDI_PREPARSEDDATA, + ptr::null_mut(), + &mut minimum_size, + ) + }; + + if status != 0 { + return None; + } + + let mut buf: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + winuser::GetRawInputDeviceInfoW( + handle, + RIDI_PREPARSEDDATA, + buf.as_ptr() as _, + &mut minimum_size, + ) + }; + + if status == UINT::max_value() || status == 0 { + return None; + } + + debug_assert_eq!(minimum_size, status); + + unsafe { buf.set_len(minimum_size as _) }; + + Some(buf) +} + pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { let device_size = size_of::() as UINT; @@ -140,12 +190,12 @@ pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { success == TRUE } -pub fn register_all_mice_and_keyboards_for_raw_input(window_handle: HWND) -> bool { - // RIDEV_DEVNOTIFY: receive hotplug events - // RIDEV_INPUTSINK: receive events even if we're not in the foreground +pub fn register_for_raw_input(window_handle: HWND) -> bool { + // `RIDEV_DEVNOTIFY`: receive hotplug events + // `RIDEV_INPUTSINK`: receive events even if we're not in the foreground let flags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; - let devices: [RAWINPUTDEVICE; 2] = [ + let devices: [RAWINPUTDEVICE; 5] = [ RAWINPUTDEVICE { usUsagePage: HID_USAGE_PAGE_GENERIC, usUsage: HID_USAGE_GENERIC_MOUSE, @@ -158,27 +208,182 @@ pub fn register_all_mice_and_keyboards_for_raw_input(window_handle: HWND) -> boo dwFlags: flags, hwndTarget: window_handle, }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_JOYSTICK, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_GAMEPAD, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: 0x08, // multi-axis + dwFlags: flags, + hwndTarget: window_handle, + }, ]; register_raw_input_devices(&devices) } -pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { - let mut data: RAWINPUT = unsafe { mem::zeroed() }; - let mut data_size = size_of::() as UINT; +pub enum RawInputData { + Mouse { + device_handle: HANDLE, + raw_mouse: winuser::RAWMOUSE, + }, + Keyboard { + device_handle: HANDLE, + raw_keyboard: winuser::RAWKEYBOARD, + }, + Hid { + device_handle: HANDLE, + raw_hid: RawHidData, + }, +} + +pub struct RawHidData { + pub hid_input_size: u32, + pub hid_input_count: u32, + pub raw_input: Box<[u8]>, +} + +pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { + let mut data_size = 0; let header_size = size_of::() as UINT; - let status = unsafe { + // There are two classes of data this function can output: + // - Raw mouse and keyboard data + // - Raw HID data + // The first class (mouse and keyboard) is always going to write data formatted like the + // `RAWINPUT` struct, with no other data, and can be placed on the stack into `RAWINPUT`. + // The second class (raw HID data) writes the struct, and then a buffer of data appended to + // the end. That data needs to be heap-allocated so we can store all of it. + unsafe { winuser::GetRawInputData( handle, RID_INPUT, - &mut data as *mut _ as _, + ptr::null_mut(), &mut data_size, header_size, ) }; - if status == UINT::max_value() || status == 0 { + let (status, data): (INT, RawInputData); + + if data_size <= size_of::() as UINT { + // Since GetRawInputData is going to write... well, a buffer that's `RAWINPUT` bytes long + // and structured like `RAWINPUT`, we're just going to cut to the chase and write directly into + // a `RAWINPUT` struct. + let mut rawinput_data: RAWINPUT = unsafe { mem::zeroed() }; + + status = unsafe { + winuser::GetRawInputData( + handle, + RID_INPUT, + &mut rawinput_data as *mut RAWINPUT as *mut _, + &mut data_size, + header_size, + ) + } as INT; + + assert_ne!(-1, status); + + let device_handle = rawinput_data.header.hDevice; + + data = match rawinput_data.header.dwType { + winuser::RIM_TYPEMOUSE => { + let raw_mouse = unsafe { rawinput_data.data.mouse().clone() }; + RawInputData::Mouse { + device_handle, + raw_mouse, + } + } + winuser::RIM_TYPEKEYBOARD => { + let raw_keyboard = unsafe { rawinput_data.data.keyboard().clone() }; + RawInputData::Keyboard { + device_handle, + raw_keyboard, + } + } + winuser::RIM_TYPEHID => { + let hid_data = unsafe { rawinput_data.data.hid() }; + let buf_len = hid_data.dwSizeHid as usize * hid_data.dwCount as usize; + let data = unsafe { slice::from_raw_parts(hid_data.bRawData.as_ptr(), buf_len) }; + RawInputData::Hid { + device_handle, + raw_hid: RawHidData { + hid_input_size: hid_data.dwSizeHid, + hid_input_count: hid_data.dwCount, + raw_input: Box::from(data), + }, + } + } + _ => unreachable!(), + }; + } else { + let mut buf = vec![0u8; data_size as usize]; + + status = unsafe { + winuser::GetRawInputData( + handle, + RID_INPUT, + buf.as_mut_ptr() as *mut _, + &mut data_size, + header_size, + ) + } as INT; + + let rawinput_data = buf.as_ptr() as *const RAWINPUT; + + let device_handle = unsafe { (&*rawinput_data).header.hDevice }; + + data = match unsafe { *rawinput_data }.header.dwType { + winuser::RIM_TYPEMOUSE => { + let raw_mouse = unsafe { (&*rawinput_data).data.mouse().clone() }; + RawInputData::Mouse { + device_handle, + raw_mouse, + } + } + winuser::RIM_TYPEKEYBOARD => { + let raw_keyboard = unsafe { (&*rawinput_data).data.keyboard().clone() }; + RawInputData::Keyboard { + device_handle, + raw_keyboard, + } + } + winuser::RIM_TYPEHID => { + let hid_data: winuser::RAWHID = unsafe { (&*rawinput_data).data.hid().clone() }; + + let hid_data_index = { + let hid_data_start = + unsafe { &((&*rawinput_data).data.hid().bRawData) } as *const _; + hid_data_start as usize - buf.as_ptr() as usize + }; + + buf.drain(..hid_data_index); + + RawInputData::Hid { + device_handle, + raw_hid: RawHidData { + hid_input_size: hid_data.dwSizeHid, + hid_input_count: hid_data.dwCount, + raw_input: buf.into_boxed_slice(), + }, + } + } + _ => unreachable!(), + }; + + assert_ne!(-1, status); + } + + if status == 0 { return None; } @@ -200,7 +405,7 @@ fn button_flags_to_element_state( } } -pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option; 3] { +pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option; 5] { [ button_flags_to_element_state( button_flags, @@ -217,5 +422,264 @@ pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option winuser::RI_MOUSE_RIGHT_BUTTON_DOWN, winuser::RI_MOUSE_RIGHT_BUTTON_UP, ), + button_flags_to_element_state( + button_flags, + winuser::RI_MOUSE_BUTTON_4_DOWN, + winuser::RI_MOUSE_BUTTON_4_UP, + ), + button_flags_to_element_state( + button_flags, + winuser::RI_MOUSE_BUTTON_5_DOWN, + winuser::RI_MOUSE_BUTTON_5_UP, + ), ] } + +pub struct Axis { + caps: HIDP_VALUE_CAPS, + value: f64, + prev_value: f64, + axis: Option, +} + +impl fmt::Debug for Axis { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[derive(Debug)] + struct Axis { + value: f64, + prev_value: f64, + axis: Option, + } + + let axis_proxy = Axis { + value: self.value, + prev_value: self.prev_value, + axis: self.axis, + }; + + axis_proxy.fmt(f) + } +} + +#[derive(Debug)] +pub struct RawGamepad { + handle: HANDLE, + pre_parsed_data: Vec, + button_count: usize, + pub button_state: Vec, + pub prev_button_state: Vec, + axis_count: usize, + pub axis_state: Vec, +} + +// Reference: https://chromium.googlesource.com/chromium/chromium/+/trunk/content/browser/gamepad/raw_input_data_fetcher_win.cc +impl RawGamepad { + pub fn new(handle: HANDLE) -> Option { + let pre_parsed_data = get_raw_input_pre_parse_info(handle)?; + let data_ptr = pre_parsed_data.as_ptr() as PHIDP_PREPARSED_DATA; + let mut caps = unsafe { mem::zeroed() }; + let status = unsafe { HidP_GetCaps(data_ptr, &mut caps) }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + let mut button_caps_len = caps.NumberInputButtonCaps; + let mut button_caps = Vec::with_capacity(button_caps_len as _); + let status = unsafe { + HidP_GetButtonCaps( + HidP_Input, + button_caps.as_mut_ptr(), + &mut button_caps_len, + data_ptr, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { button_caps.set_len(button_caps_len as _) }; + let mut button_count = 0; + for button_cap in button_caps { + let range = unsafe { button_cap.u.Range() }; + button_count = max(button_count, range.UsageMax); + } + let button_state = vec![false; button_count as usize]; + let mut axis_caps_len = caps.NumberInputValueCaps; + let mut axis_caps = Vec::with_capacity(axis_caps_len as _); + let status = unsafe { + HidP_GetValueCaps( + HidP_Input, + axis_caps.as_mut_ptr(), + &mut axis_caps_len, + data_ptr, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { axis_caps.set_len(axis_caps_len as _) }; + let mut axis_state = Vec::with_capacity(axis_caps_len as _); + let mut axis_count = 0; + for (axis_index, axis_cap) in axis_caps.drain(0..).enumerate() { + axis_state.push(Axis { + caps: axis_cap, + value: 0.0, + prev_value: 0.0, + axis: None, + }); + axis_count = max(axis_count, axis_index + 1); + } + Some(RawGamepad { + handle, + pre_parsed_data, + button_count: button_count as usize, + button_state: button_state.clone(), + prev_button_state: button_state, + axis_count, + axis_state, + }) + } + + fn pre_parsed_data_ptr(&mut self) -> PHIDP_PREPARSED_DATA { + self.pre_parsed_data.as_mut_ptr() as PHIDP_PREPARSED_DATA + } + + fn update_button_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + let pre_parsed_data_ptr = self.pre_parsed_data_ptr(); + self.prev_button_state = + mem::replace(&mut self.button_state, vec![false; self.button_count]); + let mut usages_len = 0; + // This is the officially documented way to get the required length, but it nonetheless returns + // `HIDP_STATUS_BUFFER_TOO_SMALL`... + unsafe { + HidP_GetUsagesEx( + HidP_Input, + 0, + ptr::null_mut(), + &mut usages_len, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + let mut usages = Vec::with_capacity(usages_len as _); + let status = unsafe { + HidP_GetUsagesEx( + HidP_Input, + 0, + usages.as_mut_ptr(), + &mut usages_len, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { usages.set_len(usages_len as _) }; + for usage in usages { + if usage.UsagePage != 0xFF << 8 { + let button_index = (usage.Usage - 1) as usize; + self.button_state[button_index] = true; + } + } + Some(()) + } + + fn update_axis_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + let pre_parsed_data_ptr = self.pre_parsed_data_ptr(); + for axis in &mut self.axis_state { + let (status, axis_value) = if axis.caps.LogicalMin < 0 { + let mut scaled_axis_value = 0; + let status = unsafe { + HidP_GetScaledUsageValue( + HidP_Input, + axis.caps.UsagePage, + 0, + axis.caps.u.Range().UsageMin, + &mut scaled_axis_value, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + (status, scaled_axis_value as f64) + } else { + let mut axis_value = 0; + let status = unsafe { + HidP_GetUsageValue( + HidP_Input, + axis.caps.UsagePage, + 0, + axis.caps.u.Range().UsageMin, + &mut axis_value, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + (status, axis_value as f64) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + axis.prev_value = axis.value; + axis.value = util::normalize_symmetric( + axis_value, + axis.caps.LogicalMin as f64, + axis.caps.LogicalMax as f64, + ); + } + Some(()) + } + + pub unsafe fn update_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + self.update_button_state(raw_input_report)?; + self.update_axis_state(raw_input_report)?; + Some(()) + } + + pub fn get_changed_buttons(&self) -> impl '_ + Iterator { + self.button_state + .iter() + .zip(self.prev_button_state.iter()) + .enumerate() + .filter(|&(_, (button, prev_button))| button != prev_button) + .map(|(index, (button, _))| { + let state = if *button { + ElementState::Pressed + } else { + ElementState::Released + }; + GamepadEvent::Button { + button_id: index as _, + button: None, + state, + } + }) + } + + pub fn get_changed_axes(&self) -> impl '_ + Iterator { + self.axis_state + .iter() + .enumerate() + .filter(|&(_, axis)| axis.value != axis.prev_value) + .map(|(index, axis)| GamepadEvent::Axis { + axis_id: index as _, + axis: axis.axis, + value: axis.value, + stick: false, + }) + } + + pub fn get_gamepad_events(&self) -> Vec { + self.get_changed_axes() + .chain(self.get_changed_buttons()) + .collect() + } + + // pub fn rumble(&mut self, _left_speed: u16, _right_speed: u16) { + // // Even though I can't read German, this is still the most useful resource I found: + // // https://zfx.info/viewtopic.php?t=3574&f=7 + // // I'm not optimistic about it being possible to implement this. + // } +} diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 96347849cf..4a761d2fd2 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,62 +1,27 @@ use std::{ io, mem, - ops::BitAnd, os::raw::c_void, ptr, slice, sync::atomic::{AtomicBool, Ordering}, }; -use crate::window::CursorIcon; +use crate::{dpi::PhysicalSize, window::CursorIcon}; use winapi::{ ctypes::wchar_t, shared::{ - minwindef::{BOOL, DWORD}, - windef::{HWND, POINT, RECT}, + minwindef::{BOOL, DWORD, UINT}, + windef::{DPI_AWARENESS_CONTEXT, HMONITOR, HWND, LPRECT, RECT}, }, um::{ libloaderapi::{GetProcAddress, LoadLibraryA}, + shellscalingapi::{MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS}, winbase::lstrlenW, - winnt::LPCSTR, + winnt::{HRESULT, LONG, LPCSTR}, winuser, }, }; -// Helper function to dynamically load function pointer. -// `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')); - assert_eq!(function.chars().last(), Some('\0')); - - // Library names we will use are ASCII so we can use the A version to avoid string conversion. - let module = unsafe { LoadLibraryA(library.as_ptr() as LPCSTR) }; - if module.is_null() { - return None; - } - - let function_ptr = unsafe { GetProcAddress(module, function.as_ptr() as LPCSTR) }; - if function_ptr.is_null() { - return None; - } - - Some(function_ptr as _) -} - -macro_rules! get_function { - ($lib:expr, $func:ident) => { - crate::platform_impl::platform::util::get_function_impl( - concat!($lib, '\0'), - concat!(stringify!($func), '\0'), - ) - .map(|f| unsafe { std::mem::transmute::<*const _, $func>(f) }) - }; -} - -pub fn has_flag(bitset: T, flag: T) -> bool -where - T: Copy + PartialEq + BitAnd, -{ - bitset & flag == flag -} +pub use crate::util::*; pub fn wchar_to_string(wchar: &[wchar_t]) -> String { String::from_utf16_lossy(wchar).to_string() @@ -85,10 +50,6 @@ fn win_to_err BOOL>(f: F) -> Result<(), io::Error> { } } -pub fn get_cursor_pos() -> Option { - unsafe { status_map(|cursor_pos| winuser::GetCursorPos(cursor_pos)) } -} - pub fn get_window_rect(hwnd: HWND) -> Option { unsafe { status_map(|rect| winuser::GetWindowRect(hwnd, rect)) } } @@ -109,6 +70,50 @@ pub fn get_client_rect(hwnd: HWND) -> Result { } } +pub fn adjust_size(hwnd: HWND, size: PhysicalSize) -> PhysicalSize { + let (width, height): (u32, u32) = size.into(); + let rect = RECT { + left: 0, + right: width as LONG, + top: 0, + bottom: height as LONG, + }; + let rect = adjust_window_rect(hwnd, rect).unwrap_or(rect); + PhysicalSize::new((rect.right - rect.left) as _, (rect.bottom - rect.top) as _) +} + +pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32) { + unsafe { + let rect = adjust_window_rect( + window, + RECT { + top: 0, + left: 0, + bottom: y as LONG, + right: x as LONG, + }, + ) + .expect("adjust_window_rect failed"); + + let outer_x = (rect.right - rect.left).abs() as _; + let outer_y = (rect.top - rect.bottom).abs() as _; + winuser::SetWindowPos( + window, + ptr::null_mut(), + 0, + 0, + outer_x, + outer_y, + winuser::SWP_ASYNCWINDOWPOS + | winuser::SWP_NOZORDER + | winuser::SWP_NOREPOSITION + | winuser::SWP_NOMOVE + | winuser::SWP_NOACTIVATE, + ); + winuser::InvalidateRgn(window, ptr::null_mut(), 0); + } +} + pub fn adjust_window_rect(hwnd: HWND, rect: RECT) -> Option { unsafe { let style = winuser::GetWindowLongW(hwnd, winuser::GWL_STYLE); @@ -128,7 +133,14 @@ pub fn adjust_window_rect_with_styles( *r = rect; let b_menu = !winuser::GetMenu(hwnd).is_null() as BOOL; - winuser::AdjustWindowRectEx(r, style as _, b_menu, style_ex as _) + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = + (*GET_DPI_FOR_WINDOW, *ADJUST_WINDOW_RECT_EX_FOR_DPI) + { + let dpi = get_dpi_for_window(hwnd); + adjust_window_rect_ex_for_dpi(r, style as _, b_menu, style_ex as _, dpi) + } else { + winuser::AdjustWindowRectEx(r, style as _, b_menu, style_ex as _) + } }) } } @@ -210,3 +222,71 @@ impl CursorIcon { } } } + +// Helper function to dynamically load function pointer. +// `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')); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryA(library.as_ptr() as LPCSTR) }; + if module.is_null() { + return None; + } + + let function_ptr = unsafe { GetProcAddress(module, function.as_ptr() as LPCSTR) }; + if function_ptr.is_null() { + return None; + } + + Some(function_ptr as _) +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl( + concat!($lib, '\0'), + concat!(stringify!($func), '\0'), + ) + .map(|f| unsafe { std::mem::transmute::<*const _, $func>(f) }) + }; +} + +pub type SetProcessDPIAware = unsafe extern "system" fn() -> BOOL; +pub type SetProcessDpiAwareness = + unsafe extern "system" fn(value: PROCESS_DPI_AWARENESS) -> HRESULT; +pub type SetProcessDpiAwarenessContext = + unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; +pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> UINT; +pub type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut UINT, + dpi_y: *mut UINT, +) -> HRESULT; +pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; +pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( + rect: LPRECT, + dwStyle: DWORD, + bMenu: BOOL, + dwExStyle: DWORD, + dpi: UINT, +) -> BOOL; + +lazy_static! { + pub static ref GET_DPI_FOR_WINDOW: Option = + get_function!("user32.dll", GetDpiForWindow); + pub static ref ADJUST_WINDOW_RECT_EX_FOR_DPI: Option = + get_function!("user32.dll", AdjustWindowRectExForDpi); + pub static ref GET_DPI_FOR_MONITOR: Option = + get_function!("shcore.dll", GetDpiForMonitor); + pub static ref ENABLE_NON_CLIENT_DPI_SCALING: Option = + get_function!("user32.dll", EnableNonClientDpiScaling); + pub static ref SET_PROCESS_DPI_AWARENESS_CONTEXT: Option = + get_function!("user32.dll", SetProcessDpiAwarenessContext); + pub static ref SET_PROCESS_DPI_AWARENESS: Option = + get_function!("shcore.dll", SetProcessDpiAwareness); + pub static ref SET_PROCESS_DPI_AWARE: Option = + get_function!("user32.dll", SetProcessDPIAware); +} diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 783ad5e7a8..e9c785f9d9 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -14,40 +14,38 @@ use std::{ use winapi::{ ctypes::c_int, shared::{ - minwindef::{DWORD, HINSTANCE, LPARAM, UINT, WORD, WPARAM}, + minwindef::{HINSTANCE, UINT}, windef::{HWND, POINT, RECT}, }, um::{ - combaseapi, dwmapi, libloaderapi, + combaseapi, dwmapi, + imm::{CFS_POINT, COMPOSITIONFORM}, + libloaderapi, objbase::COINIT_APARTMENTTHREADED, ole2, oleidl::LPDROPTARGET, shobjidl_core::{CLSID_TaskbarList, ITaskbarList2}, - wingdi::{CreateRectRgn, DeleteObject}, - winnt::{LONG, LPCWSTR}, + winnt::LPCWSTR, winuser, }, }; use crate::{ - dpi::{LogicalPosition, LogicalSize, PhysicalSize}, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ + dark_mode::try_theme, dpi::{dpi_to_scale_factor, hwnd_dpi}, drop_handler::FileDropHandler, - event_loop::{ - self, EventLoopWindowTarget, DESTROY_MSG_ID, INITIAL_DPI_MSG_ID, - REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID, - }, - icon::{self, IconType, WinIcon}, - monitor, - raw_input::register_all_mice_and_keyboards_for_raw_input, - util, + event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, + icon::{self, IconType}, + monitor, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, }; /// The Win32 implementation of the main `Window` object. @@ -73,8 +71,9 @@ impl Window { // // done. you owe me -- ossi unsafe { + let drag_and_drop = pl_attr.drag_and_drop; init(w_attr, pl_attr, event_loop).map(|win| { - let file_drop_handler = { + let file_drop_handler = if drag_and_drop { use winapi::shared::winerror::{OLE_E_WRONGCOMPOBJ, RPC_E_CHANGED_MODE, S_OK}; let ole_init_result = ole2::OleInitialize(ptr::null_mut()); @@ -83,15 +82,19 @@ impl Window { if ole_init_result == OLE_E_WRONGCOMPOBJ { panic!("OleInitialize failed! Result was: `OLE_E_WRONGCOMPOBJ`"); } else if ole_init_result == RPC_E_CHANGED_MODE { - panic!("OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`"); + panic!( + "OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`. \ + Make sure other crates are not using multithreaded COM library \ + on the same thread or disable drag and drop support." + ); } - let file_drop_runner = event_loop.runner_shared.clone(); + let shared_data = event_loop.shared_data.clone(); let file_drop_handler = FileDropHandler::new( win.window.0, Box::new(move |event| { if let Ok(e) = event.map_nonuser_event() { - file_drop_runner.send_event(e) + shared_data.runner_shared.send_event(e) } }), ); @@ -102,13 +105,17 @@ impl Window { ole2::RegisterDragDrop(win.window.0, handler_interface_ptr), S_OK ); - file_drop_handler + Some(file_drop_handler) + } else { + None }; let subclass_input = event_loop::SubclassInput { window_state: win.window_state.clone(), - event_loop_runner: event_loop.runner_shared.clone(), + shared_data: event_loop.shared_data.clone(), file_drop_handler, + subclass_removed: Cell::new(false), + recurse_depth: Cell::new(0), }; event_loop::subclass_window(win.window.0, subclass_input); @@ -129,71 +136,55 @@ impl Window { #[inline] pub fn set_visible(&self, visible: bool) { - match visible { - true => unsafe { - winuser::ShowWindow(self.window.0, winuser::SW_SHOW); - }, - false => unsafe { - winuser::ShowWindow(self.window.0, winuser::SW_HIDE); - }, - } + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::VISIBLE, visible) + }); + }); } #[inline] pub fn request_redraw(&self) { unsafe { - if self.thread_executor.trigger_newevents_on_redraw() { - winuser::RedrawWindow( - self.window.0, - ptr::null(), - ptr::null_mut(), - winuser::RDW_INTERNALPAINT, - ); - } else { - let mut window_state = self.window_state.lock(); - if !window_state.queued_out_of_band_redraw { - window_state.queued_out_of_band_redraw = true; - winuser::PostMessageW(self.window.0, *REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID, 0, 0); - } - } + winuser::RedrawWindow( + self.window.0, + ptr::null(), + ptr::null_mut(), + winuser::RDW_INTERNALPAINT, + ); } } - pub(crate) fn outer_position_physical(&self) -> (i32, i32) { + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { util::get_window_rect(self.window.0) - .map(|rect| (rect.left as i32, rect.top as i32)) - .unwrap() + .map(|rect| Ok(PhysicalPosition::new(rect.left as i32, rect.top as i32))) + .expect("Unexpected GetWindowRect failure; please report this error to https://github.com/rust-windowing/winit") } #[inline] - pub fn outer_position(&self) -> Result { - let physical_position = self.outer_position_physical(); - let dpi_factor = self.hidpi_factor(); - Ok(LogicalPosition::from_physical( - physical_position, - dpi_factor, - )) - } - - pub(crate) fn inner_position_physical(&self) -> (i32, i32) { + pub fn inner_position(&self) -> Result, NotSupportedError> { let mut position: POINT = unsafe { mem::zeroed() }; if unsafe { winuser::ClientToScreen(self.window.0, &mut position) } == 0 { panic!("Unexpected ClientToScreen failure: please report this error to https://github.com/rust-windowing/winit") } - (position.x, position.y) + Ok(PhysicalPosition::new(position.x as i32, position.y as i32)) } #[inline] - pub fn inner_position(&self) -> Result { - let physical_position = self.inner_position_physical(); - let dpi_factor = self.hidpi_factor(); - Ok(LogicalPosition::from_physical( - physical_position, - dpi_factor, - )) - } + pub fn set_outer_position(&self, position: Position) { + 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(); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); - pub(crate) fn set_position_physical(&self, x: i32, y: i32) { unsafe { winuser::SetWindowPos( self.window.0, @@ -207,48 +198,27 @@ impl Window { | winuser::SWP_NOSIZE | winuser::SWP_NOACTIVATE, ); - winuser::UpdateWindow(self.window.0); + winuser::InvalidateRgn(self.window.0, ptr::null_mut(), 0); } } #[inline] - pub fn set_outer_position(&self, logical_position: LogicalPosition) { - let dpi_factor = self.hidpi_factor(); - let (x, y) = logical_position.to_physical(dpi_factor).into(); - - let window_state = Arc::clone(&self.window_state); - let window = self.window.clone(); - self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, |f| { - f.set(WindowFlags::MAXIMIZED, false) - }); - }); - - self.set_position_physical(x, y); - } - - pub(crate) fn inner_size_physical(&self) -> (u32, u32) { + pub fn inner_size(&self) -> PhysicalSize { let mut rect: RECT = unsafe { mem::zeroed() }; if unsafe { winuser::GetClientRect(self.window.0, &mut rect) } == 0 { panic!("Unexpected GetClientRect failure: please report this error to https://github.com/rust-windowing/winit") } - ( + PhysicalSize::new( (rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32, ) } #[inline] - pub fn inner_size(&self) -> LogicalSize { - let physical_size = self.inner_size_physical(); - let dpi_factor = self.hidpi_factor(); - LogicalSize::from_physical(physical_size, dpi_factor) - } - - pub(crate) fn outer_size_physical(&self) -> (u32, u32) { + pub fn outer_size(&self) -> PhysicalSize { util::get_window_rect(self.window.0) .map(|rect| { - ( + PhysicalSize::new( (rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32, ) @@ -257,48 +227,9 @@ impl Window { } #[inline] - pub fn outer_size(&self) -> LogicalSize { - let physical_size = self.outer_size_physical(); - let dpi_factor = self.hidpi_factor(); - LogicalSize::from_physical(physical_size, dpi_factor) - } - - pub(crate) fn set_inner_size_physical(&self, x: u32, y: u32) { - unsafe { - let rect = util::adjust_window_rect( - self.window.0, - RECT { - top: 0, - left: 0, - bottom: y as LONG, - right: x as LONG, - }, - ) - .expect("adjust_window_rect failed"); - - let outer_x = (rect.right - rect.left).abs() as c_int; - let outer_y = (rect.top - rect.bottom).abs() as c_int; - winuser::SetWindowPos( - self.window.0, - ptr::null_mut(), - 0, - 0, - outer_x, - outer_y, - winuser::SWP_ASYNCWINDOWPOS - | winuser::SWP_NOZORDER - | winuser::SWP_NOREPOSITION - | winuser::SWP_NOMOVE - | winuser::SWP_NOACTIVATE, - ); - winuser::UpdateWindow(self.window.0); - } - } - - #[inline] - pub fn set_inner_size(&self, logical_size: LogicalSize) { - let dpi_factor = self.hidpi_factor(); - let (width, height) = logical_size.to_physical(dpi_factor).into(); + pub fn set_inner_size(&self, size: Size) { + let scale_factor = self.scale_factor(); + let (width, height) = size.to_physical::(scale_factor).into(); let window_state = Arc::clone(&self.window_state); let window = self.window.clone(); @@ -308,39 +239,23 @@ impl Window { }); }); - self.set_inner_size_physical(width, height); - } - - pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { - self.window_state.lock().min_size = dimensions.map(Into::into); - // Make windows re-check the window size bounds. - let (width, height) = self.inner_size_physical(); - self.set_inner_size_physical(width, height); + util::set_inner_size_physical(self.window.0, width, height); } #[inline] - pub fn set_min_inner_size(&self, logical_size: Option) { - let physical_size = logical_size.map(|logical_size| { - let dpi_factor = self.hidpi_factor(); - logical_size.to_physical(dpi_factor).into() - }); - self.set_min_inner_size_physical(physical_size); - } - - pub fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { - self.window_state.lock().max_size = dimensions.map(Into::into); + pub fn set_min_inner_size(&self, size: Option) { + self.window_state.lock().min_size = size; // Make windows re-check the window size bounds. - let (width, height) = self.inner_size_physical(); - self.set_inner_size_physical(width, height); + let size = self.inner_size(); + self.set_inner_size(size.into()); } #[inline] - pub fn set_max_inner_size(&self, logical_size: Option) { - let physical_size = logical_size.map(|logical_size| { - let dpi_factor = self.hidpi_factor(); - logical_size.to_physical(dpi_factor).into() - }); - self.set_max_inner_size_physical(physical_size); + pub fn set_max_inner_size(&self, size: Option) { + 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()); } #[inline] @@ -363,7 +278,7 @@ impl Window { #[inline] pub fn hinstance(&self) -> HINSTANCE { - unsafe { winuser::GetWindowLongW(self.hwnd(), winuser::GWL_HINSTANCE) as *mut _ } + unsafe { winuser::GetWindowLongPtrW(self.hwnd(), winuser::GWLP_HINSTANCE) as *mut _ } } #[inline] @@ -420,11 +335,15 @@ impl Window { } #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.window_state.lock().dpi_factor + pub fn scale_factor(&self) -> f64 { + self.window_state.lock().scale_factor } - fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + let mut point = POINT { x, y }; unsafe { if winuser::ClientToScreen(self.window.0, &mut point) == 0 { @@ -438,18 +357,20 @@ impl Window { } #[inline] - pub fn set_cursor_position( - &self, - logical_position: LogicalPosition, - ) -> Result<(), ExternalError> { - let dpi_factor = self.hidpi_factor(); - let (x, y) = logical_position.to_physical(dpi_factor).into(); - self.set_cursor_position_physical(x, y) + pub fn id(&self) -> WindowId { + WindowId(self.window.0) } #[inline] - pub fn id(&self) -> WindowId { - WindowId(self.window.0) + pub fn set_minimized(&self, minimized: bool) { + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::MINIMIZED, minimized) + }); + }); } #[inline] @@ -464,6 +385,12 @@ impl Window { }); } + #[inline] + pub fn is_maximized(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::MAXIMIZED) + } + #[inline] pub fn fullscreen(&self) -> Option { let window_state = self.window_state.lock(); @@ -484,20 +411,6 @@ impl Window { drop(window_state_lock); self.thread_executor.execute_in_thread(move || { - let mut window_state_lock = window_state.lock(); - - // Save window bounds before entering fullscreen - match (&old_fullscreen, &fullscreen) { - (&None, &Some(_)) => { - let client_rect = util::get_client_rect(window.0).unwrap(); - window_state_lock.saved_window = Some(SavedWindow { - client_rect, - dpi_factor: window_state_lock.dpi_factor, - }); - } - _ => (), - } - // Change video mode if we're transitioning to or from exclusive // fullscreen match (&old_fullscreen, &fullscreen) { @@ -571,16 +484,35 @@ impl Window { } // Update window style - WindowState::set_window_flags(window_state_lock, window.0, |f| { - f.set(WindowFlags::MARKER_FULLSCREEN, fullscreen.is_some()) + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Exclusive(_))), + ); + f.set( + WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Borderless(_))), + ); }); // Update window bounds match &fullscreen { Some(fullscreen) => { - let monitor = match fullscreen { - Fullscreen::Exclusive(ref video_mode) => video_mode.monitor(), - Fullscreen::Borderless(ref monitor) => monitor.clone(), + // Save window bounds before entering fullscreen + let placement = unsafe { + let mut placement = mem::zeroed(); + winuser::GetWindowPlacement(window.0, &mut placement); + placement + }; + + window_state.lock().saved_window = Some(SavedWindow { placement }); + + let monitor = match &fullscreen { + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => RootMonitorHandle { + inner: monitor::current_monitor(window.0), + }, }; let position: (i32, i32) = monitor.position().into(); @@ -596,33 +528,16 @@ impl Window { size.1 as i32, winuser::SWP_ASYNCWINDOWPOS | winuser::SWP_NOZORDER, ); - winuser::UpdateWindow(window.0); + winuser::InvalidateRgn(window.0, ptr::null_mut(), 0); } } None => { let mut window_state_lock = window_state.lock(); - if let Some(SavedWindow { - client_rect, - dpi_factor, - }) = window_state_lock.saved_window.take() - { - window_state_lock.dpi_factor = dpi_factor; + if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() { drop(window_state_lock); - let client_rect = util::adjust_window_rect(window.0, client_rect).unwrap(); - unsafe { - winuser::SetWindowPos( - window.0, - ptr::null_mut(), - client_rect.left, - client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, - winuser::SWP_ASYNCWINDOWPOS - | winuser::SWP_NOZORDER - | winuser::SWP_NOACTIVATE, - ); - winuser::UpdateWindow(window.0); + winuser::SetWindowPlacement(window.0, &placement); + winuser::InvalidateRgn(window.0, ptr::null_mut(), 0); } } } @@ -659,19 +574,18 @@ impl Window { } #[inline] - pub fn current_monitor(&self) -> RootMonitorHandle { - RootMonitorHandle { + pub fn current_monitor(&self) -> Option { + Some(RootMonitorHandle { inner: monitor::current_monitor(self.window.0), - } + }) } #[inline] - pub fn set_window_icon(&self, mut window_icon: Option) { - let window_icon = window_icon - .take() - .map(|icon| WinIcon::from_icon(icon).expect("Failed to create `ICON_SMALL`")); + pub fn set_window_icon(&self, window_icon: Option) { if let Some(ref window_icon) = window_icon { - window_icon.set_for_window(self.window.0, IconType::Small); + window_icon + .inner + .set_for_window(self.window.0, IconType::Small); } else { icon::unset_for_window(self.window.0, IconType::Small); } @@ -679,21 +593,72 @@ impl Window { } #[inline] - pub fn set_taskbar_icon(&self, mut taskbar_icon: Option) { - let taskbar_icon = taskbar_icon - .take() - .map(|icon| WinIcon::from_icon(icon).expect("Failed to create `ICON_BIG`")); + pub fn set_taskbar_icon(&self, taskbar_icon: Option) { if let Some(ref taskbar_icon) = taskbar_icon { - taskbar_icon.set_for_window(self.window.0, IconType::Big); + taskbar_icon + .inner + .set_for_window(self.window.0, IconType::Big); } else { icon::unset_for_window(self.window.0, IconType::Big); } self.window_state.lock().taskbar_icon = taskbar_icon; } + pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { + if unsafe { winuser::GetSystemMetrics(winuser::SM_IMMENABLED) } != 0 { + let mut composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y }, + rcArea: unsafe { mem::zeroed() }, + }; + unsafe { + let himc = winapi::um::imm::ImmGetContext(self.window.0); + winapi::um::imm::ImmSetCompositionWindow(himc, &mut composition_form); + winapi::um::imm::ImmReleaseContext(self.window.0, himc); + } + } + } + + #[inline] + pub fn set_ime_position(&self, spot: Position) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); + self.set_ime_position_physical(x, y); + } + #[inline] - pub fn set_ime_position(&self, _logical_spot: LogicalPosition) { - unimplemented!(); + pub fn request_user_attention(&self, request_type: Option) { + let window = self.window.clone(); + let active_window_handle = unsafe { winuser::GetActiveWindow() }; + if window.0 == active_window_handle { + return; + } + + self.thread_executor.execute_in_thread(move || unsafe { + let (flags, count) = request_type + .map(|ty| match ty { + UserAttentionType::Critical => { + (winuser::FLASHW_ALL | winuser::FLASHW_TIMERNOFG, u32::MAX) + } + UserAttentionType::Informational => { + (winuser::FLASHW_TRAY | winuser::FLASHW_TIMERNOFG, 0) + } + }) + .unwrap_or((winuser::FLASHW_STOP, 0)); + + let mut flash_info = winuser::FLASHWINFO { + cbSize: mem::size_of::() as UINT, + hwnd: window.0, + dwFlags: flags, + uCount: count, + dwTimeout: 0, + }; + winuser::FlashWindowEx(&mut flash_info); + }); + } + + #[inline] + pub fn theme(&self) -> Theme { + self.window_state.lock().current_theme } } @@ -720,24 +685,8 @@ pub struct WindowWrapper(HWND); unsafe impl Sync for WindowWrapper {} unsafe impl Send for WindowWrapper {} -pub unsafe fn adjust_size( - physical_size: PhysicalSize, - style: DWORD, - ex_style: DWORD, -) -> (LONG, LONG) { - let (width, height): (u32, u32) = physical_size.into(); - let mut rect = RECT { - left: 0, - right: width as LONG, - top: 0, - bottom: height as LONG, - }; - winuser::AdjustWindowRectEx(&mut rect, style, 0, ex_style); - (rect.right - rect.left, rect.bottom - rect.top) -} - unsafe fn init( - mut attributes: WindowAttributes, + attributes: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, event_loop: &EventLoopWindowTarget, ) -> Result { @@ -746,60 +695,8 @@ unsafe fn init( .chain(Some(0).into_iter()) .collect::>(); - let window_icon = { - let icon = attributes.window_icon.take().map(WinIcon::from_icon); - if let Some(icon) = icon { - Some(icon.map_err(|e| os_error!(e))?) - } else { - None - } - }; - let taskbar_icon = { - let icon = attributes.window_icon.take().map(WinIcon::from_icon); - if let Some(icon) = icon { - Some(icon.map_err(|e| os_error!(e))?) - } else { - None - } - }; - // registering the window class - let class_name = register_window_class(&window_icon, &taskbar_icon); - - let guessed_dpi_factor = { - let monitors = monitor::available_monitors(); - let dpi_factor = if !monitors.is_empty() { - let mut dpi_factor = Some(monitors[0].hidpi_factor()); - for monitor in &monitors { - if Some(monitor.hidpi_factor()) != dpi_factor { - dpi_factor = None; - } - } - dpi_factor - } else { - return Err(os_error!(io::Error::new( - io::ErrorKind::NotFound, - "No monitors were detected." - ))); - }; - dpi_factor.unwrap_or_else(|| { - util::get_cursor_pos() - .and_then(|cursor_pos| { - let mut dpi_factor = None; - for monitor in &monitors { - if monitor.contains_point(&cursor_pos) { - dpi_factor = Some(monitor.hidpi_factor()); - break; - } - } - dpi_factor - }) - .unwrap_or(1.0) - }) - }; - info!("Guessed window DPI factor: {}", guessed_dpi_factor); - - let dimensions = attributes.inner_size.unwrap_or_else(|| (1024, 768).into()); + let class_name = register_window_class(&attributes.window_icon, &pl_attribs.taskbar_icon); let mut window_flags = WindowFlags::empty(); window_flags.set(WindowFlags::DECORATIONS, attributes.decorations); @@ -814,6 +711,10 @@ unsafe fn init( window_flags.set(WindowFlags::CHILD, pl_attribs.parent.is_some()); window_flags.set(WindowFlags::ON_TASKBAR, true); + if pl_attribs.parent.is_some() && pl_attribs.menu.is_some() { + warn!("Setting a menu on windows that have a parent is unsupported"); + } + // creating the real window this time, by using the functions in `extra_functions` let real_window = { let (style, ex_style) = window_flags.to_window_styles(); @@ -827,7 +728,7 @@ unsafe fn init( winuser::CW_USEDEFAULT, winuser::CW_USEDEFAULT, pl_attribs.parent.unwrap_or(ptr::null_mut()), - ptr::null_mut(), + pl_attribs.menu.unwrap_or(ptr::null_mut()), libloaderapi::GetModuleHandleW(ptr::null()), ptr::null_mut(), ); @@ -839,9 +740,6 @@ unsafe fn init( WindowWrapper(handle) }; - // Set up raw input - register_all_mice_and_keyboards_for_raw_input(real_window.0); - // Register for touch events if applicable { let digitizer = winuser::GetSystemMetrics(winuser::SM_DIGITIZER) as u32; @@ -851,59 +749,39 @@ unsafe fn init( } let dpi = hwnd_dpi(real_window.0); - let dpi_factor = dpi_to_scale_factor(dpi); - if dpi_factor != guessed_dpi_factor { - let (width, height): (u32, u32) = dimensions.into(); - let mut packed_dimensions = 0; - // MAKELPARAM isn't provided by winapi yet. - let ptr = &mut packed_dimensions as *mut LPARAM as *mut WORD; - *ptr.offset(0) = width as WORD; - *ptr.offset(1) = height as WORD; - winuser::PostMessageW( - real_window.0, - *INITIAL_DPI_MSG_ID, - dpi as WPARAM, - packed_dimensions, - ); - } + let scale_factor = dpi_to_scale_factor(dpi); // making the window transparent if attributes.transparent && !pl_attribs.no_redirection_bitmap { - let region = CreateRectRgn(0, 0, -1, -1); // makes the window transparent - let bb = dwmapi::DWM_BLURBEHIND { - dwFlags: dwmapi::DWM_BB_ENABLE | dwmapi::DWM_BB_BLURREGION, + dwFlags: dwmapi::DWM_BB_ENABLE, fEnable: 1, - hRgnBlur: region, + hRgnBlur: ptr::null_mut(), fTransitionOnMaximized: 0, }; dwmapi::DwmEnableBlurBehindWindow(real_window.0, &bb); - DeleteObject(region as _); if attributes.decorations { - // HACK: When opaque (opacity 255), there is a trail whenever - // the transparent window is moved. By reducing it to 254, - // the window is rendered properly. - let opacity = 254; - - // The color key can be any value except for black (0x0). - let color_key = 0x0030c100; - - winuser::SetLayeredWindowAttributes( - real_window.0, - color_key, - opacity, - winuser::LWA_ALPHA, - ); + let opacity = 255; + + winuser::SetLayeredWindowAttributes(real_window.0, 0, opacity, winuser::LWA_ALPHA); } } - window_flags.set(WindowFlags::VISIBLE, attributes.visible); - window_flags.set(WindowFlags::MAXIMIZED, attributes.maximized); + // If the system theme is dark, we need to set the window theme now + // before we update the window flags (and possibly show the + // window for the first time). + let current_theme = try_theme(real_window.0, pl_attribs.preferred_theme); let window_state = { - let window_state = WindowState::new(&attributes, window_icon, taskbar_icon, dpi_factor); + let window_state = WindowState::new( + &attributes, + pl_attribs.taskbar_icon, + scale_factor, + current_theme, + pl_attribs.preferred_theme, + ); let window_state = Arc::new(Mutex::new(window_state)); WindowState::set_window_flags(window_state.lock(), real_window.0, |f| *f = window_flags); window_state @@ -915,21 +793,28 @@ unsafe fn init( thread_executor: event_loop.create_thread_executor(), }; + let dimensions = attributes + .inner_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + win.set_inner_size(dimensions); + if attributes.maximized { + // Need to set MAXIMIZED after setting `inner_size` as + // `Window::set_inner_size` changes MAXIMIZED to false. + win.set_maximized(true); + } + win.set_visible(attributes.visible); + if let Some(_) = attributes.fullscreen { win.set_fullscreen(attributes.fullscreen); force_window_active(win.window.0); } - if let Some(dimensions) = attributes.inner_size { - win.set_inner_size(dimensions); - } - Ok(win) } unsafe fn register_window_class( - window_icon: &Option, - taskbar_icon: &Option, + window_icon: &Option, + taskbar_icon: &Option, ) -> Vec { let class_name: Vec<_> = OsStr::new("Window Class") .encode_wide() @@ -938,11 +823,11 @@ unsafe fn register_window_class( let h_icon = taskbar_icon .as_ref() - .map(|icon| icon.handle) + .map(|icon| icon.inner.as_raw_handle()) .unwrap_or(ptr::null_mut()); let h_icon_small = window_icon .as_ref() - .map(|icon| icon.handle) + .map(|icon| icon.inner.as_raw_handle()) .unwrap_or(ptr::null_mut()); let class = winuser::WNDCLASSEXW { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index be98bcc930..6a6f0255ec 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -1,7 +1,9 @@ use crate::{ - dpi::LogicalSize, - platform_impl::platform::{event_loop, icon::WinIcon, util}, - window::{CursorIcon, Fullscreen, WindowAttributes}, + dpi::{PhysicalPosition, Size}, + event::ModifiersState, + icon::Icon, + platform_impl::platform::{event_loop, util}, + window::{CursorIcon, Fullscreen, Theme, WindowAttributes}, }; use parking_lot::MutexGuard; use std::{io, ptr}; @@ -14,39 +16,38 @@ use winapi::{ }; /// Contains information about states and the window that the callback is going to use. -#[derive(Clone)] pub struct WindowState { pub mouse: MouseProperties, /// Used by `WM_GETMINMAXINFO`. - pub min_size: Option, - pub max_size: Option, + pub min_size: Option, + pub max_size: Option, - pub window_icon: Option, - pub taskbar_icon: Option, + pub window_icon: Option, + pub taskbar_icon: Option, pub saved_window: Option, - pub dpi_factor: f64, + pub scale_factor: f64, + pub modifiers_state: ModifiersState, pub fullscreen: Option, - /// Used to supress duplicate redraw attempts when calling `request_redraw` multiple - /// times in `EventsCleared`. - pub queued_out_of_band_redraw: bool, + pub current_theme: Theme, + pub preferred_theme: Option, pub high_surrogate: Option, - window_flags: WindowFlags, + pub window_flags: WindowFlags, } #[derive(Clone)] pub struct SavedWindow { - pub client_rect: RECT, - pub dpi_factor: f64, + pub placement: winuser::WINDOWPLACEMENT, } #[derive(Clone)] pub struct MouseProperties { pub cursor: CursorIcon, - pub buttons_down: u32, + pub capture_count: u32, cursor_flags: CursorFlags, + pub last_position: Option>, } bitflags! { @@ -70,7 +71,8 @@ bitflags! { /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is /// included here to make masking easier. - const MARKER_FULLSCREEN = 1 << 9; + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 9; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 13; /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. /// In most cases, it's okay to let those parameters change the state. However, when we're @@ -79,12 +81,11 @@ bitflags! { /// window's state to match our stored state. This controls whether to accept those changes. const MARKER_RETAIN_STATE_ON_SIZE = 1 << 10; - const FULLSCREEN_AND_MASK = !( - WindowFlags::DECORATIONS.bits | - WindowFlags::RESIZABLE.bits | - WindowFlags::MAXIMIZED.bits - ); - const FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; + const MARKER_IN_SIZE_MOVE = 1 << 11; + + const MINIMIZED = 1 << 12; + + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; const INVISIBLE_AND_MASK = !WindowFlags::MAXIMIZED.bits; } @@ -93,28 +94,32 @@ bitflags! { impl WindowState { pub fn new( attributes: &WindowAttributes, - window_icon: Option, - taskbar_icon: Option, - dpi_factor: f64, + taskbar_icon: Option, + scale_factor: f64, + current_theme: Theme, + preferred_theme: Option, ) -> WindowState { WindowState { mouse: MouseProperties { cursor: CursorIcon::default(), - buttons_down: 0, + capture_count: 0, cursor_flags: CursorFlags::empty(), + last_position: None, }, min_size: attributes.min_inner_size, max_size: attributes.max_inner_size, - window_icon, + window_icon: attributes.window_icon.clone(), taskbar_icon, saved_window: None, - dpi_factor, + scale_factor, + modifiers_state: ModifiersState::default(), fullscreen: None, - queued_out_of_band_redraw: false, + current_theme, + preferred_theme, high_surrogate: None, window_flags: WindowFlags::empty(), } @@ -169,9 +174,8 @@ impl MouseProperties { impl WindowFlags { fn mask(mut self) -> WindowFlags { - if self.contains(WindowFlags::MARKER_FULLSCREEN) { - self &= WindowFlags::FULLSCREEN_AND_MASK; - self |= WindowFlags::FULLSCREEN_OR_MASK; + if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { + self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; } if !self.contains(WindowFlags::VISIBLE) { self &= WindowFlags::INVISIBLE_AND_MASK; @@ -212,6 +216,9 @@ impl WindowFlags { if self.contains(WindowFlags::CHILD) { style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. } + if self.contains(WindowFlags::MINIMIZED) { + style |= WS_MINIMIZE; + } if self.contains(WindowFlags::MAXIMIZED) { style |= WS_MAXIMIZE; } @@ -219,6 +226,12 @@ impl WindowFlags { style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU; style_ex |= WS_EX_ACCEPTFILES; + if self.intersects( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + ) { + style &= !WS_OVERLAPPEDWINDOW; + } + (style, style_ex) } @@ -260,7 +273,7 @@ impl WindowFlags { | winuser::SWP_NOSIZE | winuser::SWP_NOACTIVATE, ); - winuser::UpdateWindow(window); + winuser::InvalidateRgn(window, ptr::null_mut(), 0); } } @@ -276,14 +289,30 @@ impl WindowFlags { } } + // Minimize operations should execute after maximize for proper window animations + if diff.contains(WindowFlags::MINIMIZED) { + unsafe { + winuser::ShowWindow( + window, + match new.contains(WindowFlags::MINIMIZED) { + true => winuser::SW_MINIMIZE, + false => winuser::SW_RESTORE, + }, + ); + } + } + if diff != WindowFlags::empty() { let (style, style_ex) = new.to_window_styles(); unsafe { winuser::SendMessageW(window, *event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID, 1, 0); - winuser::SetWindowLongW(window, winuser::GWL_STYLE, style as _); - winuser::SetWindowLongW(window, winuser::GWL_EXSTYLE, style_ex as _); + // This condition is necessary to avoid having an unrestorable window + if !new.contains(WindowFlags::MINIMIZED) { + winuser::SetWindowLongW(window, winuser::GWL_STYLE, style as _); + winuser::SetWindowLongW(window, winuser::GWL_EXSTYLE, style_ex as _); + } let mut flags = winuser::SWP_NOZORDER | winuser::SWP_NOMOVE @@ -293,7 +322,9 @@ impl WindowFlags { // We generally don't want style changes here to affect window // focus, but for fullscreen windows they must be activated // (i.e. focused) so that they appear on top of the taskbar - if !new.contains(WindowFlags::MARKER_FULLSCREEN) { + if !new.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) + && !new.contains(WindowFlags::MARKER_BORDERLESS_FULLSCREEN) + { flags |= winuser::SWP_NOACTIVATE; } diff --git a/src/platform_impl/windows/xinput.rs b/src/platform_impl/windows/xinput.rs new file mode 100644 index 0000000000..f0b3c6be9f --- /dev/null +++ b/src/platform_impl/windows/xinput.rs @@ -0,0 +1,330 @@ +use std::sync::{Arc, Weak}; +use std::{io, mem}; + +use rusty_xinput::*; +use winapi::shared::minwindef::{DWORD, WORD}; +use winapi::um::xinput::*; + +use crate::{ + event::{ + device::{BatteryLevel, GamepadAxis, GamepadButton, GamepadEvent, RumbleError, Side}, + ElementState, + }, + platform_impl::platform::util, +}; + +lazy_static! { + static ref XINPUT_HANDLE: Option = XInputHandle::load_default().ok(); +} + +static BUTTONS: &[(WORD, u32, GamepadButton)] = &[ + (XINPUT_GAMEPAD_DPAD_UP, 12, GamepadButton::DPadUp), + (XINPUT_GAMEPAD_DPAD_DOWN, 13, GamepadButton::DPadDown), + (XINPUT_GAMEPAD_DPAD_LEFT, 14, GamepadButton::DPadLeft), + (XINPUT_GAMEPAD_DPAD_RIGHT, 15, GamepadButton::DPadRight), + (XINPUT_GAMEPAD_START, 9, GamepadButton::Start), + (XINPUT_GAMEPAD_BACK, 8, GamepadButton::Select), + (XINPUT_GAMEPAD_LEFT_THUMB, 10, GamepadButton::LeftStick), + (XINPUT_GAMEPAD_RIGHT_THUMB, 11, GamepadButton::RightStick), + (XINPUT_GAMEPAD_LEFT_SHOULDER, 4, GamepadButton::LeftShoulder), + ( + XINPUT_GAMEPAD_RIGHT_SHOULDER, + 5, + GamepadButton::RightShoulder, + ), + (XINPUT_GAMEPAD_A, 0, GamepadButton::South), + (XINPUT_GAMEPAD_B, 1, GamepadButton::East), + (XINPUT_GAMEPAD_X, 2, GamepadButton::West), + (XINPUT_GAMEPAD_Y, 3, GamepadButton::North), +]; + +pub fn id_from_name(name: &str) -> Option { + // A device name looks like \\?\HID#VID_046D&PID_C21D&IG_00#8&6daf3b6&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} + // The IG_00 substring indicates that this is an XInput gamepad, and that the ID is 00 + let pat = "IG_0"; + name.find(pat) + .and_then(|i| name[i + pat.len()..].chars().next()) + .and_then(|c| match c { + '0' => Some(0), + '1' => Some(1), + '2' => Some(2), + '3' => Some(3), + _ => None, + }) +} + +#[derive(Debug)] +pub struct XInputGamepad { + shared: Arc, + prev_state: Option, + state: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct XInputGamepadShared { + port: DWORD, +} + +impl XInputGamepad { + pub fn new(port: DWORD) -> Option { + XINPUT_HANDLE.as_ref().map(|_| XInputGamepad { + shared: Arc::new(XInputGamepadShared { port }), + prev_state: None, + state: None, + }) + } + + pub fn update_state(&mut self) -> Option<()> { + let state = XINPUT_HANDLE + .as_ref() + .and_then(|h| h.get_state(self.shared.port).ok()); + if state.is_some() { + self.prev_state = mem::replace(&mut self.state, state); + Some(()) + } else { + None + } + } + + fn check_trigger_digital( + events: &mut Vec, + value: bool, + prev_value: Option, + side: Side, + ) { + const LEFT_TRIGGER_ID: u32 = /*BUTTONS.len() as _*/ 16; + const RIGHT_TRIGGER_ID: u32 = LEFT_TRIGGER_ID + 1; + if Some(value) != prev_value { + let state = if value { + ElementState::Pressed + } else { + ElementState::Released + }; + let (button_id, button) = match side { + Side::Left => (LEFT_TRIGGER_ID, Some(GamepadButton::LeftTrigger)), + Side::Right => (RIGHT_TRIGGER_ID, Some(GamepadButton::RightTrigger)), + }; + events.push(GamepadEvent::Button { + button_id, + button, + state, + }); + } + } + + pub fn get_changed_buttons(&self, events: &mut Vec) { + let (buttons, left_trigger, right_trigger) = match self.state.as_ref() { + Some(state) => ( + state.raw.Gamepad.wButtons, + state.left_trigger_bool(), + state.right_trigger_bool(), + ), + None => return, + }; + let (prev_buttons, prev_left, prev_right) = self + .prev_state + .as_ref() + .map(|state| { + ( + state.raw.Gamepad.wButtons, + Some(state.left_trigger_bool()), + Some(state.right_trigger_bool()), + ) + }) + .unwrap_or_else(|| (0, None, None)); + /* + A = buttons + B = prev_buttons + C = changed + P = pressed + R = released + A B C C A P C B R + (0 0) 0 (0 0) 0 (0 0) 0 + (0 1) 1 (1 1) 1 (1 0) 0 + (1 0) 1 (1 0) 0 (1 1) 1 + (1 1) 0 (0 1) 0 (0 1) 0 + */ + let changed = buttons ^ prev_buttons; + let pressed = changed & buttons; + let released = changed & prev_buttons; + for &(flag, button_id, button) in BUTTONS { + let button = Some(button); + if util::has_flag(pressed, flag) { + events.push(GamepadEvent::Button { + button_id, + button, + state: ElementState::Pressed, + }); + } else if util::has_flag(released, flag) { + events.push(GamepadEvent::Button { + button_id, + button, + state: ElementState::Released, + }); + } + } + Self::check_trigger_digital(events, left_trigger, prev_left, Side::Left); + Self::check_trigger_digital(events, right_trigger, prev_right, Side::Right); + } + + fn check_trigger( + events: &mut Vec, + value: u8, + prev_value: Option, + side: Side, + ) { + const LEFT_TRIGGER_ID: u32 = 4; + const RIGHT_TRIGGER_ID: u32 = LEFT_TRIGGER_ID + 1; + if Some(value) != prev_value { + let (axis_id, axis) = match side { + Side::Left => (LEFT_TRIGGER_ID, Some(GamepadAxis::LeftTrigger)), + Side::Right => (RIGHT_TRIGGER_ID, Some(GamepadAxis::RightTrigger)), + }; + events.push(GamepadEvent::Axis { + axis_id, + axis, + value: value as f64 / u8::max_value() as f64, + stick: false, + }); + } + } + + fn check_stick( + events: &mut Vec, + value: (i16, i16), + prev_value: Option<(i16, i16)>, + stick: Side, + ) { + let (id, axis) = match stick { + Side::Left => ((0, 1), (GamepadAxis::LeftStickX, GamepadAxis::LeftStickY)), + Side::Right => ((2, 3), (GamepadAxis::RightStickX, GamepadAxis::RightStickY)), + }; + let prev_x = prev_value.map(|prev| prev.0); + let prev_y = prev_value.map(|prev| prev.1); + + let value_f64 = |value_int: i16| match value_int.signum() { + 0 => 0.0, + 1 => value_int as f64 / i16::max_value() as f64, + -1 => value_int as f64 / (i16::min_value() as f64).abs(), + _ => unreachable!(), + }; + + let value_f64 = (value_f64(value.0), value_f64(value.1)); + if prev_x != Some(value.0) { + events.push(GamepadEvent::Axis { + axis_id: id.0, + axis: Some(axis.0), + value: value_f64.0, + stick: true, + }); + } + if prev_y != Some(value.1) { + events.push(GamepadEvent::Axis { + axis_id: id.1, + axis: Some(axis.1), + value: value_f64.1, + stick: true, + }); + } + if prev_x != Some(value.0) || prev_y != Some(value.1) { + events.push(GamepadEvent::Stick { + x_id: id.0, + y_id: id.1, + x_value: value_f64.0, + y_value: value_f64.1, + side: stick, + }) + } + } + + pub fn get_changed_axes(&self, events: &mut Vec) { + let state = match self.state { + Some(ref state) => state, + None => return, + }; + let left_stick = state.left_stick_raw(); + let right_stick = state.right_stick_raw(); + let left_trigger = state.left_trigger(); + let right_trigger = state.right_trigger(); + + let prev_state = self.prev_state.as_ref(); + let prev_left_stick = prev_state.map(|state| state.left_stick_raw()); + let prev_right_stick = prev_state.map(|state| state.right_stick_raw()); + let prev_left_trigger = prev_state.map(|state| state.left_trigger()); + let prev_right_trigger = prev_state.map(|state| state.right_trigger()); + + Self::check_stick(events, left_stick, prev_left_stick, Side::Left); + Self::check_stick(events, right_stick, prev_right_stick, Side::Right); + Self::check_trigger(events, left_trigger, prev_left_trigger, Side::Left); + Self::check_trigger(events, right_trigger, prev_right_trigger, Side::Right); + } + + pub fn get_gamepad_events(&self) -> Vec { + let mut events = Vec::new(); + self.get_changed_axes(&mut events); + self.get_changed_buttons(&mut events); + events + } + + pub fn shared_data(&self) -> Weak { + Arc::downgrade(&self.shared) + } +} + +impl Drop for XInputGamepad { + fn drop(&mut self) { + // For some reason, if you don't attempt to retrieve the xinput gamepad state at least once + // after the gamepad was disconnected, all future attempts to read from a given port (even + // if a controller was plugged back into said port) will fail! I don't know why that happens, + // but this fixes it, so 🤷. + XINPUT_HANDLE + .as_ref() + .and_then(|h| h.get_state(self.shared.port).ok()); + } +} + +impl XInputGamepadShared { + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + let left_speed = (left_speed.max(0.0).min(1.0) * u16::max_value() as f64) as u16; + let right_speed = (right_speed.max(0.0).min(1.0) * u16::max_value() as f64) as u16; + + let result = XINPUT_HANDLE + .as_ref() + .unwrap() + .set_state(self.port, left_speed, right_speed); + result.map_err(|e| match e { + XInputUsageError::XInputNotLoaded | XInputUsageError::InvalidControllerID => panic!( + "unexpected xinput error {:?}; this is a bug and should be reported", + e + ), + XInputUsageError::DeviceNotConnected => RumbleError::DeviceNotConnected, + XInputUsageError::UnknownError(code) => { + RumbleError::OsError(io::Error::from_raw_os_error(code as i32)) + } + }) + } + + pub fn port(&self) -> u8 { + self.port as _ + } + + pub fn battery_level(&self) -> Option { + use rusty_xinput::BatteryLevel as XBatteryLevel; + + let battery_info = XINPUT_HANDLE + .as_ref() + .unwrap() + .get_gamepad_battery_information(self.port) + .ok()?; + match battery_info.battery_type { + BatteryType::ALKALINE | BatteryType::NIMH => match battery_info.battery_level { + XBatteryLevel::EMPTY => Some(BatteryLevel::Empty), + XBatteryLevel::LOW => Some(BatteryLevel::Low), + XBatteryLevel::MEDIUM => Some(BatteryLevel::Medium), + XBatteryLevel::FULL => Some(BatteryLevel::Full), + _ => None, + }, + _ => None, + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000000..18e8c1941b --- /dev/null +++ b/src/util.rs @@ -0,0 +1,29 @@ +use std::ops::BitAnd; + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub fn clamp(value: f64, min: f64, max: f64) -> f64 { + if value > max { + max + } else if value < min { + min + } else { + value + } +} + +pub fn normalize_asymmetric(value: f64, min: f64, max: f64) -> f64 { + let range = max - min; + let translated = value - min; + let scaled = translated / range; + clamp(scaled, 0.0, 1.0) +} + +pub fn normalize_symmetric(value: f64, min: f64, max: f64) -> f64 { + (2.0 * normalize_asymmetric(value, min, max)) - 1.0 +} diff --git a/src/window.rs b/src/window.rs index fedd3d0ac1..052f5746e0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,14 +2,14 @@ use std::fmt; use crate::{ - dpi::{LogicalPosition, LogicalSize}, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, monitor::{MonitorHandle, VideoMode}, platform_impl, }; -pub use crate::icon::*; +pub use crate::icon::{BadIcon, Icon}; /// Represents a window. /// @@ -26,12 +26,14 @@ pub use crate::icon::*; /// let window = Window::new(&event_loop).unwrap(); /// /// event_loop.run(move |event, _, control_flow| { +/// *control_flow = ControlFlow::Wait; +/// /// match event { /// Event::WindowEvent { /// event: WindowEvent::CloseRequested, /// .. /// } => *control_flow = ControlFlow::Exit, -/// _ => *control_flow = ControlFlow::Wait, +/// _ => (), /// } /// }); /// ``` @@ -78,7 +80,7 @@ impl WindowId { } /// Object that allows you to build windows. -#[derive(Clone)] +#[derive(Clone, Default)] pub struct WindowBuilder { /// The attributes to use to create the window. pub window: WindowAttributes, @@ -102,17 +104,17 @@ pub struct WindowAttributes { /// used. /// /// The default is `None`. - pub inner_size: Option, + pub inner_size: Option, /// The minimum dimensions a window can be, If this is `None`, the window will have no minimum dimensions (aside from reserved). /// /// The default is `None`. - pub min_inner_size: Option, + pub min_inner_size: Option, /// The maximum dimensions a window can be, If this is `None`, the maximum will have no maximum or will be set to the primary monitor's dimensions by the platform. /// /// The default is `None`. - pub max_inner_size: Option, + pub max_inner_size: Option, /// Whether the window is resizable or not. /// @@ -185,10 +187,7 @@ impl WindowBuilder { /// Initializes a new `WindowBuilder` with default values. #[inline] pub fn new() -> Self { - WindowBuilder { - window: Default::default(), - platform_specific: Default::default(), - } + Default::default() } /// Requests the window to be of specific dimensions. @@ -197,8 +196,8 @@ impl WindowBuilder { /// /// [`Window::set_inner_size`]: crate::window::Window::set_inner_size #[inline] - pub fn with_inner_size(mut self, size: LogicalSize) -> Self { - self.window.inner_size = Some(size); + pub fn with_inner_size>(mut self, size: S) -> Self { + self.window.inner_size = Some(size.into()); self } @@ -208,8 +207,8 @@ impl WindowBuilder { /// /// [`Window::set_min_inner_size`]: crate::window::Window::set_min_inner_size #[inline] - pub fn with_min_inner_size(mut self, min_size: LogicalSize) -> Self { - self.window.min_inner_size = Some(min_size); + pub fn with_min_inner_size>(mut self, min_size: S) -> Self { + self.window.min_inner_size = Some(min_size.into()); self } @@ -219,8 +218,8 @@ impl WindowBuilder { /// /// [`Window::set_max_inner_size`]: crate::window::Window::set_max_inner_size #[inline] - pub fn with_max_inner_size(mut self, max_size: LogicalSize) -> Self { - self.window.max_inner_size = Some(max_size); + pub fn with_max_inner_size>(mut self, max_size: S) -> Self { + self.window.max_inner_size = Some(max_size.into()); self } @@ -252,8 +251,8 @@ impl WindowBuilder { /// /// [`Window::set_fullscreen`]: crate::window::Window::set_fullscreen #[inline] - pub fn with_fullscreen(mut self, monitor: Option) -> Self { - self.window.fullscreen = monitor; + pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { + self.window.fullscreen = fullscreen; self } @@ -366,25 +365,25 @@ impl Window { WindowId(self.window.id()) } - /// Returns the DPI factor that can be used to map logical pixels to physical pixels, and vice versa. + /// 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. /// /// Note that this value can change depending on user action (for example if the window is - /// moved to another screen); as such, tracking `WindowEvent::HiDpiFactorChanged` events is + /// moved to another screen); as such, tracking `WindowEvent::ScaleFactorChanged` events is /// the most robust way to track the DPI you need to use to draw. /// /// ## Platform-specific /// - /// - **X11:** This respects Xft.dpi, and can be overridden using the `WINIT_HIDPI_FACTOR` environment variable. + /// - **X11:** This respects Xft.dpi, and can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. /// - **Android:** Always returns 1.0. /// - **iOS:** Can only be called on the main thread. Returns the underlying `UIView`'s /// [`contentScaleFactor`]. /// /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc #[inline] - pub fn hidpi_factor(&self) -> f64 { - self.window.hidpi_factor() + pub fn scale_factor(&self) -> f64 { + self.window.scale_factor() } /// Emits a `WindowEvent::RedrawRequested` event in the associated event loop after all OS @@ -393,15 +392,16 @@ impl Window { /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with /// OS-requested redraws (e.g. when a window gets resized). /// - /// This function can cause `RedrawRequested` events to be emitted after `Event::EventsCleared` + /// This function can cause `RedrawRequested` events to be emitted after `Event::MainEventsCleared` /// but before `Event::NewEvents` if called in the following circumstances: - /// * While processing `EventsCleared`. - /// * While processing a `RedrawRequested` event that was sent during `EventsCleared` or any + /// * While processing `MainEventsCleared`. + /// * While processing a `RedrawRequested` event that was sent during `MainEventsCleared` or any /// directly subsequent `RedrawRequested` event. /// /// ## Platform-specific /// /// - **iOS:** Can only be called on the main thread. + /// - **Android:** Unsupported. #[inline] pub fn request_redraw(&self) { self.window.request_redraw() @@ -419,10 +419,13 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the /// window's [safe area] in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns the + /// same value as `outer_position`._ + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] - pub fn inner_position(&self) -> Result { + pub fn inner_position(&self) -> Result, NotSupportedError> { self.window.inner_position() } @@ -440,8 +443,10 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the /// window in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. #[inline] - pub fn outer_position(&self) -> Result { + pub fn outer_position(&self) -> Result, NotSupportedError> { self.window.outer_position() } @@ -454,25 +459,26 @@ 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. + /// - **Android / Wayland:** Unsupported. #[inline] - pub fn set_outer_position(&self, position: LogicalPosition) { - self.window.set_outer_position(position) + pub fn set_outer_position>(&self, position: P) { + self.window.set_outer_position(position.into()) } - /// Returns the logical size of the window's client area. + /// Returns the physical size of the window's client area. /// /// The client area is the content of the window, excluding the title bar and borders. /// - /// Converting the returned `LogicalSize` to `PhysicalSize` produces the size your framebuffer should be. - /// /// ## Platform-specific /// - /// - **iOS:** Can only be called on the main thread. Returns the `LogicalSize` of the window's + /// - **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. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] - pub fn inner_size(&self) -> LogicalSize { + pub fn inner_size(&self) -> PhysicalSize { self.window.inner_size() } @@ -483,24 +489,26 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Unimplemented. Currently this panics, as it's not clear what `set_inner_size` - /// would mean for iOS. + /// - **iOS / Android:** Unsupported. + /// - **Web:** Sets the size of the canvas element. #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { - self.window.set_inner_size(size) + pub fn set_inner_size>(&self, size: S) { + self.window.set_inner_size(size.into()) } - /// Returns the logical size of the entire window. + /// Returns the physical size of the entire window. /// /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), /// use `inner_size` instead. /// /// ## Platform-specific /// - /// - **iOS:** Can only be called on the main thread. Returns the `LogicalSize` of the window in + /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window in /// screen space coordinates. + /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as + /// `inner_size`._ #[inline] - pub fn outer_size(&self) -> LogicalSize { + pub fn outer_size(&self) -> PhysicalSize { self.window.outer_size() } @@ -508,22 +516,20 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web:** Unsupported. #[inline] - pub fn set_min_inner_size(&self, dimensions: Option) { - self.window.set_min_inner_size(dimensions) + pub fn set_min_inner_size>(&self, min_size: Option) { + self.window.set_min_inner_size(min_size.map(|s| s.into())) } /// Sets a maximum dimension size for the window. /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Andraid / Web:** Unsupported. #[inline] - pub fn set_max_inner_size(&self, dimensions: Option) { - self.window.set_max_inner_size(dimensions) + pub fn set_max_inner_size>(&self, max_size: Option) { + self.window.set_max_inner_size(max_size.map(|s| s.into())) } } @@ -533,7 +539,7 @@ impl Window { /// /// ## Platform-specific /// - /// - Has no effect on iOS. + /// - **iOS / Android:** Unsupported. #[inline] pub fn set_title(&self, title: &str) { self.window.set_title(title) @@ -544,9 +550,8 @@ impl Window { /// If `false`, this will hide the window. If `true`, this will show the window. /// ## Platform-specific /// - /// - **Android:** Has no effect. + /// - **Android / Wayland / Web:** Unsupported. /// - **iOS:** Can only be called on the main thread. - /// - **Web:** Has no effect. #[inline] pub fn set_visible(&self, visible: bool) { self.window.set_visible(visible) @@ -565,24 +570,44 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn set_resizable(&self, resizable: bool) { self.window.set_resizable(resizable) } + /// Sets the window to minimized or back + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + /// - **Wayland:** Un-minimize is unsupported. + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.window.set_minimized(minimized); + } + /// Sets the window to maximized or back. /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn set_maximized(&self, maximized: bool) { self.window.set_maximized(maximized) } + /// Gets the window's current maximized state. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11:** Not implemented. + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_maximized(&self) -> bool { + self.window.is_maximized() + } + /// Sets the window to fullscreen or back. /// /// ## Platform-specific @@ -600,8 +625,9 @@ impl Window { /// /// The dock and the menu bar are always disabled in fullscreen mode. /// - **iOS:** Can only be called on the main thread. - /// - **Wayland:** Does not support exclusive fullscreen mode. + /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// - **Android:** Unsupported. #[inline] pub fn set_fullscreen(&self, fullscreen: Option) { self.window.set_fullscreen(fullscreen) @@ -612,6 +638,8 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Can only be called on the main thread. + /// - **Android:** Will always return `None`. + /// - **Wayland:** Can return `Borderless(None)` when there are no monitors. #[inline] pub fn fullscreen(&self) -> Option { self.window.fullscreen() @@ -620,9 +648,8 @@ impl Window { /// Turn window decorations on or off. /// /// ## Platform-specific - /// - **iOS:** Can only be called on the main thread. Controls whether the status bar is hidden - /// via [`setPrefersStatusBarHidden`]. - /// - **Web:** Has no effect. + /// + /// - **iOS / Android / Web:** Unsupported. /// /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc #[inline] @@ -634,8 +661,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web / Wayland:** Unsupported. #[inline] pub fn set_always_on_top(&self, always_on_top: bool) { self.window.set_always_on_top(always_on_top) @@ -646,7 +672,7 @@ impl Window { /// /// ## Platform-specific /// - /// This only has an effect on Windows and X11. + /// - **iOS / Android / Web / Wayland / macOS:** Unsupported. /// /// On Windows, this sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. @@ -662,11 +688,27 @@ impl Window { /// /// ## Platform-specific /// - /// **iOS:** Has no effect. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn set_ime_position>(&self, position: P) { + self.window.set_ime_position(position.into()) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. #[inline] - pub fn set_ime_position(&self, position: LogicalPosition) { - self.window.set_ime_position(position) + pub fn request_user_attention(&self, request_type: Option) { + self.window.request_user_attention(request_type) } } @@ -676,8 +718,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Has no effect. - /// - **Android:** Has no effect. + /// - **iOS / Android:** Unsupported. #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { self.window.set_cursor_icon(cursor); @@ -687,24 +728,21 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Always returns an `Err`. - /// - **Web:** Has no effect. + /// - **iOS / Android / Web / Wayland:** Always returns an [`ExternalError::NotSupported`]. #[inline] - pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { - self.window.set_cursor_position(position) + pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { + self.window.set_cursor_position(position.into()) } /// Grabs the cursor, preventing it from leaving the window. /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// /// ## Platform-specific /// - /// - **macOS:** This presently merely locks the cursor in a fixed location, which looks visually - /// awkward. - /// - **Wayland:** This presently merely locks the cursor in a fixed location, which looks visually - /// awkward. - /// - **Android:** Has no effect. - /// - **iOS:** Always returns an Err. - /// - **Web:** Has no effect. + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. #[inline] pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { self.window.set_cursor_grab(grab) @@ -721,8 +759,7 @@ 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:** Has no effect. - /// - **Android:** Has no effect. + /// - **iOS / Android:** Unsupported. #[inline] pub fn set_cursor_visible(&self, visible: bool) { self.window.set_cursor_visible(visible) @@ -731,19 +768,21 @@ impl Window { /// Monitor info functions. impl Window { - /// Returns the monitor on which the window currently resides + /// 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) -> MonitorHandle { + pub fn current_monitor(&self) -> Option { self.window.current_monitor() } /// Returns the list of all the monitors available on the system. /// - /// This is the same as `EventLoop::available_monitors`, and is provided for convenience. + /// This is the same as `EventLoopWindowTarget::available_monitors`, and is provided for convenience. /// /// ## Platform-specific /// @@ -758,20 +797,27 @@ impl Window { /// Returns the primary monitor of the system. /// - /// This is the same as `EventLoop::primary_monitor`, and is provided for convenience. + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// This is the same as `EventLoopWindowTarget::primary_monitor`, and is provided for convenience. /// /// ## Platform-specific /// /// **iOS:** Can only be called on the main thread. + /// **Wayland:** Always returns `None`. #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle { - inner: self.window.primary_monitor(), - } + pub fn primary_monitor(&self) -> Option { + self.window.primary_monitor() } } unsafe impl raw_window_handle::HasRawWindowHandle for Window { + /// Returns a `raw_window_handle::RawWindowHandle` for the Window + /// + /// ## Platform-specific + /// + /// - **Android:** Only available after receiving the Resumed event and before Suspended. *If you* + /// *try to get the handle outside of that period, this function will panic*! fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { self.window.raw_window_handle() } @@ -842,8 +888,38 @@ impl Default for CursorIcon { } } +/// Fullscreen modes. #[derive(Clone, Debug, PartialEq)] pub enum Fullscreen { Exclusive(VideoMode), - Borderless(MonitorHandle), + + /// Providing `None` to `Borderless` will fullscreen on the current monitor. + Borderless(Option), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Theme { + Light, + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between `Critical` and `Informational`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UserAttentionType { + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + Informational, +} + +impl Default for UserAttentionType { + fn default() -> Self { + UserAttentionType::Informational + } } diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 252b271a4b..7221009813 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -22,6 +22,8 @@ fn window_send() { fn ids_send() { // ensures that the various `..Id` types implement `Send` needs_send::(); - needs_send::(); + needs_send::(); + needs_send::(); + needs_send::(); needs_send::(); } diff --git a/tests/serde_objects.rs b/tests/serde_objects.rs index df51898861..ad729dcd1b 100644 --- a/tests/serde_objects.rs +++ b/tests/serde_objects.rs @@ -31,8 +31,9 @@ fn events_serde() { #[test] fn dpi_serde() { - needs_serde::(); - needs_serde::(); - needs_serde::(); - needs_serde::(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); }