diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d6f55915..ecbd5d4b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ - [ ] Tested on all platforms changed +- [ ] Compilation warnings were addressed - [ ] `cargo fmt` has been run on this branch - [ ] Added an entry to `CHANGELOG.md` if knowledge of this change could be valuable to users - [ ] Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior diff --git a/.travis.yml b/.travis.yml index 7e6bc833..aaa56d9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ matrix: os: osx rust: stable - # iOS + # iOS x86_64 - env: TARGET=x86_64-apple-ios os: osx rust: nightly @@ -42,18 +42,35 @@ matrix: 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 + install: - rustup self update - rustup target add $TARGET; true - - rustup component add rustfmt + - rustup toolchain install stable + - rustup component add rustfmt --toolchain stable script: - - cargo fmt --all -- --check + - cargo +stable fmt --all -- --check - cargo build --target $TARGET --verbose - cargo build --target $TARGET --features serde --verbose - # Running iOS apps on OSX requires the simulator so we skip that for now - - if [ "$TARGET" != "x86_64-apple-ios" ]; then cargo test --target $TARGET --verbose; fi - - if [ "$TARGET" != "x86_64-apple-ios" ]; then cargo test --target $TARGET --features serde --verbose; fi + # Running iOS apps on macOS requires the Simulator so we skip that for now + - if [[ $TARGET != *-apple-ios ]]; then cargo test --target $TARGET --verbose; fi + - if [[ $TARGET != *-apple-ios ]]; then cargo test --target $TARGET --features serde --verbose; fi after_success: - | diff --git a/CHANGELOG.md b/CHANGELOG.md index 65555cf0..5be8107c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Unreleased +- On macOS, implement `run_return`. +- On iOS, fix inverted parameter in `set_prefers_home_indicator_hidden`. +- On X11, performance is improved when rapidly calling `Window::set_cursor_icon`. +- On iOS, fix improper `msg_send` usage that was UB and/or would break if `!` is stabilized. +- On Windows, unset `maximized` when manually changing the window's position or size. +- On Windows, add touch pressure information for touch events. +- On macOS, differentiate between `CursorIcon::Grab` and `CursorIcon::Grabbing`. +- On Wayland, fix event processing sometimes stalling when using OpenGL with vsync. +- Officially remove the Emscripten backend. +- On Windows, fix handling of surrogate pairs when dispatching `ReceivedCharacter`. +- On macOS 10.15, fix freeze upon exiting exclusive fullscreen mode. +- On X11, allow setting mulitple `XWindowType`s. +- On iOS, fix null window on initial `HiDpiFactorChanged` event. +- On Windows, fix fullscreen window shrinking upon getting restored to a normal window. +- On macOS, fix events not being emitted during modal loops, such as when windows are being resized + by the user. +- On Windows, fix hovering the mouse over the active window creating an endless stream of CursorMoved events. +- 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 + +# 0.20.0 Alpha 3 (2019-08-14) + +- On macOS, drop the run closure on exit. +- On Windows, location of `WindowEvent::Touch` are window client coordinates instead of screen coordinates. +- On X11, fix delayed events after window redraw. +- On macOS, add `WindowBuilderExt::with_disallow_hidpi` to have the option to turn off best resolution openGL surface. +- On Windows, screen saver won't start if the window is in fullscreen mode. +- Change all occurrences of the `new_user_event` method to `with_user_event`. +- On macOS, the dock and the menu bar are now hidden in fullscreen mode. +- `Window::set_fullscreen` now takes `Option` where `Fullscreen` + consists of `Fullscreen::Exclusive(VideoMode)` and + `Fullscreen::Borderless(MonitorHandle)` variants. + - Adds support for exclusive fullscreen mode. +- On iOS, add support for hiding the home indicator. +- On iOS, add support for deferring system gestures. +- On iOS, fix a crash that occurred while acquiring a monitor's name. +- On iOS, fix armv7-apple-ios compile target. +- Removed the `T: Clone` requirement from the `Clone` impl of `EventLoopProxy`. +- On iOS, disable overscan compensation for external displays (removes black + bars surrounding the image). +- On Linux, the functions `is_wayland`, `is_x11`, `xlib_xconnection` and `wayland_display` have been moved to a new `EventLoopWindowTargetExtUnix` trait. +- On iOS, add `set_prefers_status_bar_hidden` extension function instead of + hijacking `set_decorations` for this purpose. +- On macOS and iOS, corrected the auto trait impls of `EventLoopProxy`. +- On iOS, add touch pressure information for touch events. +- Implement `raw_window_handle::HasRawWindowHandle` for `Window` type on all supported platforms. +- On macOS, fix the signature of `-[NSView drawRect:]`. +- On iOS, fix the behavior of `ControlFlow::Poll`. It wasn't polling if that was the only mode ever used by the application. +- On iOS, fix DPI sent out by views on creation was `0.0` - now it gives a reasonable number. +- On iOS, RedrawRequested now works for gl/metal backed views. +- On iOS, RedrawRequested is generally ordered after EventsCleared. + # 0.20.0 Alpha 2 (2019-07-09) - On X11, non-resizable windows now have maximize explicitly disabled. diff --git a/Cargo.toml b/Cargo.toml index 814d6777..b3446b4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winit" -version = "0.20.0-alpha2" +version = "0.20.0-alpha3" authors = ["The winit contributors", "Pierre Krieger "] description = "Cross-platform window creation library." edition = "2018" @@ -25,6 +25,7 @@ libc = "0.2" log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } derivative = "1.0.2" +raw-window-handle = "0.1" [dev-dependencies] image = "0.21" @@ -48,7 +49,7 @@ version = "0.1.3" default_features = false features = ["display_link"] -[target.'cfg(target_os = "windows")'.dependencies] +[target.'cfg(any(target_os = "ios", target_os = "windows"))'.dependencies] bitflags = "1" [target.'cfg(target_os = "windows")'.dependencies.winapi] @@ -80,10 +81,10 @@ wayland-client = { version = "0.23.0", features = [ "dlopen", "egl", "cursor", " calloop = "0.4.2" smithay-client-toolkit = "0.6" x11-dl = "2.18.3" -percent-encoding = "1.0" +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.8" +version = "0.9" [target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] package = "web-sys" @@ -116,3 +117,4 @@ optional = true package = "stdweb" version = "0.4.18" optional = true + diff --git a/FEATURES.md b/FEATURES.md index d71b00e4..9610b7ba 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -12,7 +12,6 @@ be used to create both games and applications. It supports the main graphical pl - iOS - Android - Web - - via Emscripten - via WASM Most platforms expose capabilities that cannot be meaningfully transposed onto others. Winit does not @@ -84,6 +83,9 @@ If your PR makes notable changes to Winit's features, please update this section - **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. +- **Exclusive fullscreen**: Winit allows changing the video mode of the monitor + for fullscreen windows, and if applicable, captures the monitor for exclusive + use by this application. - **HiDPI support**: Winit assists developers in appropriately scaling HiDPI content. - **Popup / modal windows**: Windows can be created relative to the client area of other windows, and parent windows can be disabled in favor of popup windows. This feature also guarantees that popup windows @@ -100,6 +102,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Cursor grab**: Locking the cursor so it cannot exit the client area of a window. - **Cursor icon**: Changing the cursor icon, or hiding the cursor. - **Touch events**: Single-touch events. +- **Touch pressure**: Touch events contain information about the amount of force being applied. - **Multitouch**: Multi-touch events, including cancellation of a gesture. - **Keyboard events**: Properly processing keyboard events using the user-specified keymap and translating keypresses into UTF-8 characters, handling dead keys and IMEs. @@ -129,6 +132,21 @@ If your PR makes notable changes to Winit's features, please update this section * GTK Theme Variant * Base window size +### iOS +* `winit` has a minimum OS requirement of iOS 8 +* Get the `UIWindow` object pointer +* Get the `UIViewController` object pointer +* Get the `UIView` object pointer +* Get the `UIScreen` object pointer +* Setting the `UIView` hidpi factor +* Valid orientations +* Home indicator visibility +* Status bar visibility +* Deferrring system gestures +* Support for custom `UIView` derived class +* Getting the device idiom +* Getting the preferred video mode + ## Usability * `serde`: Enables serialization/deserialization of certain types with Serde. (Maintainer: @Osspial) @@ -143,55 +161,57 @@ Legend: - ❓: Unknown status ### Windowing -|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |Emscripten| +|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |WASM | |-------------------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | |Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |❓ | |Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ | -|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |**N/A** | -|Window decorations |✔️ |✔️ |✔️ |▢[#306] |**N/A**|**N/A**|**N/A** | -|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | +|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |❓ | +|Window decorations |✔️ |✔️ |✔️ |▢[#306] |**N/A**|**N/A**|❓ | +|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | |Window resizing |✔️ |▢[#219]|✔️ |▢[#306] |**N/A**|**N/A**|❓ | -|Window resize increments |❌ |❌ |❌ |❌ |❌ |❌ |❌ | -|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** | -|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ | -|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ | -|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ | -|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |❌ | +|Window resize increments |❌ |❌ |❌ |❌ |❌ |❌ |❓ | +|Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | +|Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | +|Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | +|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | +|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | +|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |❓ | +|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |❓ | +|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |❓ | ### System information -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| -|---------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | -|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |❌ | +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | +|---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | +|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | +|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |❓ | ### Input handling -|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| +|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ | -|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A** | -|Cursor grab |✔️ |▢[#165] |▢[#242] |❌[#306] |**N/A**|**N/A**|✔️ | -|Cursor icon |✔️ |✔️ |✔️ |❌[#306] |**N/A**|**N/A**|❌ | -|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ | -|Multitouch |❓ |❌ |✔️ |✔️ |❓ |❌ |❌ | -|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |✔️ | +|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|❓ | +|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|❓ | +|Cursor grab |✔️ |▢[#165] |▢[#242] |❌[#306] |**N/A**|**N/A**|❓ | +|Cursor icon |✔️ |✔️ |✔️ |❌[#306] |**N/A**|**N/A**|❓ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❓ | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❓ | +|Multitouch |✔️ |❌ |✔️ |✔️ |❓ |✔️ |❓ | +|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |❓ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ | -|Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❌ | -|Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❌ | -|Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❌ | +|Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | +|Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ | +|Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ | ### Pending API Reworks Changes in the API that have been agreed upon but aren't implemented across all platforms. -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ | -|Event Loop 2.0 ([#459]) |✔️ |❌ |❌ |✔️ |❌ |❌ |❌ | -|Keyboard Input ([#812]) |❌ |❌ |❌ |❌ |❌ |❌ |❌ | +|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |❓ | +|Event Loop 2.0 ([#459]) |✔️ |✔️ |❌ |✔️ |❌ |✔️ |❓ | +|Keyboard Input ([#812]) |❌ |❌ |❌ |❌ |❌ |❌ |❓ | ### Completed API Reworks -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | [#165]: https://github.com/rust-windowing/winit/issues/165 diff --git a/README.md b/README.md index 6abcecdf..8d685f26 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ```toml [dependencies] -winit = "0.20.0-alpha2" +winit = "0.20.0-alpha3" ``` ## [Documentation](https://docs.rs/winit) @@ -66,7 +66,7 @@ Winit provides the following features, which can be enabled in your `Cargo.toml` ### Platform-specific usage -#### Emscripten and WebAssembly +#### WebAssembly Building a binary will yield a `.js` file. In order to use it in an HTML file, you need to: diff --git a/examples/custom_events.rs b/examples/custom_events.rs new file mode 100644 index 00000000..c85d4ecd --- /dev/null +++ b/examples/custom_events.rs @@ -0,0 +1,41 @@ +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[derive(Debug, Clone, Copy)] +enum CustomEvent { + Timer, +} + +fn main() { + let event_loop = EventLoop::::with_user_event(); + + let _window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + // `EventLoopProxy` allows you to dispatch custom events to the main Winit event + // loop from any thread. + let event_loop_proxy = event_loop.create_proxy(); + + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + event_loop_proxy.send_event(CustomEvent::Timer).ok(); + } + }); + + 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, + }); +} diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index 27df2276..d4b83bb0 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,56 +1,35 @@ -use std::io::{self, Write}; -use winit::{ - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - monitor::MonitorHandle, - window::WindowBuilder, -}; +use std::io::{stdin, stdout, Write}; +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() { let event_loop = EventLoop::new(); - #[cfg(target_os = "macos")] - let mut macos_use_simple_fullscreen = false; + print!("Please choose the fullscreen mode: (1) exclusive, (2) borderless: "); + stdout().flush().unwrap(); - let monitor = { - // On macOS there are two fullscreen modes "native" and "simple" - #[cfg(target_os = "macos")] - { - print!("Please choose the fullscreen mode: (1) native, (2) simple: "); - io::stdout().flush().unwrap(); + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); - let mut num = String::new(); - io::stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().ok().expect("Please enter a number"); - match num { - 2 => macos_use_simple_fullscreen = true, - _ => {} - } + 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)), + _ => panic!("Please enter a valid number"), + }); - // Prompt for monitor when using native fullscreen - if !macos_use_simple_fullscreen { - Some(prompt_for_monitor(&event_loop)) - } else { - None - } - } - - #[cfg(not(target_os = "macos"))] - Some(prompt_for_monitor(&event_loop)) - }; - - let mut is_fullscreen = monitor.is_some(); let mut is_maximized = false; let mut decorations = true; let window = WindowBuilder::new() .with_title("Hello world!") - .with_fullscreen(monitor) + .with_fullscreen(fullscreen.clone()) .build(&event_loop) .unwrap(); event_loop.run(move |event, _, control_flow| { - println!("{:?}", event); *control_flow = ControlFlow::Wait; match event { @@ -67,35 +46,14 @@ fn main() { } => match (virtual_code, state) { (VirtualKeyCode::Escape, _) => *control_flow = ControlFlow::Exit, (VirtualKeyCode::F, ElementState::Pressed) => { - #[cfg(target_os = "macos")] - { - if macos_use_simple_fullscreen { - use winit::platform::macos::WindowExtMacOS; - if WindowExtMacOS::set_simple_fullscreen(&window, !is_fullscreen) { - is_fullscreen = !is_fullscreen; - } - return; - } - } - - is_fullscreen = !is_fullscreen; - if !is_fullscreen { + if window.fullscreen().is_some() { window.set_fullscreen(None); } else { - window.set_fullscreen(Some(window.current_monitor())); + window.set_fullscreen(fullscreen.clone()); } } (VirtualKeyCode::S, ElementState::Pressed) => { println!("window.fullscreen {:?}", window.fullscreen()); - - #[cfg(target_os = "macos")] - { - use winit::platform::macos::WindowExtMacOS; - println!( - "window.simple_fullscreen {:?}", - WindowExtMacOS::simple_fullscreen(&window) - ); - } } (VirtualKeyCode::M, ElementState::Pressed) => { is_maximized = !is_maximized; @@ -121,10 +79,10 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { } print!("Please write the number of the monitor to use: "); - io::stdout().flush().unwrap(); + stdout().flush().unwrap(); let mut num = String::new(); - io::stdin().read_line(&mut num).unwrap(); + stdin().read_line(&mut num).unwrap(); let num = num.trim().parse().ok().expect("Please enter a number"); let monitor = event_loop .available_monitors() @@ -135,3 +93,24 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { monitor } + +fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { + for (i, video_mode) in monitor.video_modes().enumerate() { + println!("Video mode #{}: {}", i, video_mode); + } + + print!("Please write the number of the video mode to use: "); + stdout().flush().unwrap(); + + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); + let video_mode = monitor + .video_modes() + .nth(num) + .expect("Please enter a valid ID"); + + println!("Using {}", video_mode); + + video_mode +} diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index e6f3cac4..65afeae8 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -2,7 +2,15 @@ fn main() { extern crate env_logger; +<<<<<<< HEAD use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; +======= +use winit::{ + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{CursorIcon, Fullscreen, WindowBuilder}, +}; +>>>>>>> master use winit::{ event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, @@ -21,11 +29,34 @@ fn main() { .with_inner_size(WINDOW_SIZE.into()) .build(&event_loop) .unwrap(); + + let mut video_modes: Vec<_> = window.current_monitor().video_modes().collect(); + let mut video_mode_id = 0usize; + let (tx, rx) = mpsc::channel(); window_senders.insert(window.id(), tx); thread::spawn(move || { while let Ok(event) = rx.recv() { match event { + WindowEvent::Moved { .. } => { + // We need to update our chosen video mode if the window + // 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_mode_id = video_mode_id.min(video_modes.len()); + let video_mode = video_modes.iter().nth(video_mode_id); + + // Different monitors may support different video modes, + // and the index we chose previously may now point to a + // completely different video mode, so notify the user + if video_mode != previous_video_mode.as_ref() { + println!( + "Window moved to another monitor, picked video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + } WindowEvent::KeyboardInput { input: KeyboardInput { @@ -46,9 +77,26 @@ fn main() { false => CursorIcon::Default, }), D => window.set_decorations(!state), - F => window.set_fullscreen(match state { - true => Some(window.current_monitor()), - false => None, + // Cycle through video modes + Right | Left => { + video_mode_id = match key { + Left => video_mode_id.saturating_sub(1), + Right => (video_modes.len() - 1).min(video_mode_id + 1), + _ => unreachable!(), + }; + println!( + "Picking video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + F => window.set_fullscreen(match (state, modifiers.alt) { + (true, false) => { + Some(Fullscreen::Borderless(window.current_monitor())) + } + (true, true) => Some(Fullscreen::Exclusive( + video_modes.iter().nth(video_mode_id).unwrap().clone(), + )), + (false, _) => None, }), G => window.set_cursor_grab(state).unwrap(), H => window.set_cursor_visible(!state), @@ -58,6 +106,7 @@ fn main() { println!("-> inner_position : {:?}", window.inner_position()); println!("-> outer_size : {:?}", window.outer_size()); println!("-> inner_size : {:?}", window.inner_size()); + println!("-> fullscreen : {:?}", window.fullscreen()); } L => window.set_min_inner_size(match state { true => Some(WINDOW_SIZE.into()), @@ -110,6 +159,7 @@ fn main() { | WindowEvent::KeyboardInput { input: KeyboardInput { + state: ElementState::Released, virtual_keycode: Some(VirtualKeyCode::Escape), .. }, diff --git a/examples/proxy.rs b/examples/proxy.rs deleted file mode 100644 index 6c5bbbc6..00000000 --- a/examples/proxy.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -fn main() { - use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }; - - let event_loop: EventLoop = EventLoop::new_user_event(); - - let _window = WindowBuilder::new() - .with_title("A fantastic window!") - .build(&event_loop) - .unwrap(); - - let proxy = event_loop.create_proxy(); - - std::thread::spawn(move || { - let mut counter = 0; - // Wake up the `event_loop` once every second. - loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - proxy.send_event(counter).unwrap(); - counter += 1; - } - }); - - event_loop.run(move |event, _, control_flow| { - println!("{:?}", event); - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => *control_flow = ControlFlow::Wait, - } - }); -} - -#[cfg(target_arch = "wasm32")] -fn main() { - panic!("Example not supported on Wasm"); -} diff --git a/examples/video_modes.rs b/examples/video_modes.rs index f8c6aa08..f923fa92 100644 --- a/examples/video_modes.rs +++ b/examples/video_modes.rs @@ -7,6 +7,6 @@ fn main() { println!("Listing available video modes:"); for mode in monitor.video_modes() { - println!("{:?}", mode); + println!("{}", mode); } } diff --git a/examples/window_run_return.rs b/examples/window_run_return.rs index 3598e10c..82c49d61 100644 --- a/examples/window_run_return.rs +++ b/examples/window_run_return.rs @@ -1,4 +1,17 @@ +<<<<<<< HEAD #[cfg(not(target_arch = "wasm32"))] +======= +// Limit this example to only compatible platforms. +#[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +>>>>>>> master fn main() { use winit::{ event::{Event, WindowEvent}, @@ -6,6 +19,10 @@ fn main() { platform::desktop::EventLoopExtDesktop, window::WindowBuilder, }; +<<<<<<< HEAD +======= + +>>>>>>> master let mut event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -40,7 +57,13 @@ fn main() { println!("Okay we're done now for real."); } +<<<<<<< HEAD #[cfg(target_arch = "wasm32")] fn main() { panic!("Example not supported on Wasm"); +======= +#[cfg(any(target_os = "ios", target_os = "android"))] +fn main() { + println!("This platform doesn't support run_return."); +>>>>>>> master } diff --git a/src/event.rs b/src/event.rs index 7e40fcf9..d5548908 100644 --- a/src/event.rs +++ b/src/event.rs @@ -134,6 +134,10 @@ pub enum WindowEvent { input: KeyboardInput, }, + /// Keyboard modifiers have changed + #[doc(hidden)] + ModifiersChanged { modifiers: ModifiersState }, + /// The cursor has moved on the window. CursorMoved { device_id: DeviceId, @@ -304,30 +308,94 @@ pub enum TouchPhase { Cancelled, } -/// Represents touch event +/// Represents a touch event /// -/// Every time user touches screen new Start event with some finger id is generated. -/// When the finger is removed from the screen End event with same id is generated. +/// Every time the user touches the screen, a new `Start` event with an unique +/// identifier for the finger is generated. When the finger is lifted, an `End` +/// event is generated with the same finger id. /// -/// For every id there will be at least 2 events with phases Start and End (or Cancelled). -/// There may be 0 or more Move events. +/// After a `Start` event has been emitted, there may be zero or more `Move` +/// events when the finger is moved or the touch pressure changes. /// +/// The finger id may be reused by the system after an `End` event. The user +/// should assume that a new `Start` event received with the same id has nothing +/// to do with the old finger and is a new finger. /// -/// Depending on platform implementation id may or may not be reused by system after End event. -/// -/// Gesture regonizer using this event should assume that Start event received with same id -/// as previously received End event is a new finger and has nothing to do with an old one. -/// -/// Touch may be cancelled if for example window lost focus. +/// A `Cancelled` event is emitted when the system has canceled tracking this +/// touch, such as when the window loses focus, or on iOS if the user moves the +/// device against their face. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Touch { pub device_id: DeviceId, pub phase: TouchPhase, pub location: LogicalPosition, - /// unique identifier of a finger. + /// Describes how hard the screen was pressed. May be `None` if the platform + /// does not support pressure sensitivity. + /// + /// ## Platform-specific + /// + /// - Only available on **iOS** 9.0+ and **Windows** 8+. + pub force: Option, + /// Unique identifier of a finger. pub id: u64, } +/// Describes the force of a touch event +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Force { + /// On iOS, the force is calibrated so that the same number corresponds to + /// roughly the same amount of pressure on the screen regardless of the + /// device. + Calibrated { + /// The force of the touch, where a value of 1.0 represents the force of + /// an average touch (predetermined by the system, not user-specific). + /// + /// The force reported by Apple Pencil is measured along the axis of the + /// pencil. If you want a force perpendicular to the device, you need to + /// calculate this value using the `altitude_angle` value. + force: f64, + /// The maximum possible force for a touch. + /// + /// The value of this field is sufficiently high to provide a wide + /// dynamic range for values of the `force` field. + max_possible_force: f64, + /// The altitude (in radians) of the stylus. + /// + /// A value of 0 radians indicates that the stylus is parallel to the + /// surface. The value of this property is Pi/2 when the stylus is + /// perpendicular to the surface. + altitude_angle: Option, + }, + /// If the platform reports the force as normalized, we have no way of + /// knowing how much pressure 1.0 corresponds to – we know it's the maximum + /// amount of force, but as to how much force, you might either have to + /// press really really hard, or not hard at all, depending on the device. + Normalized(f64), +} + +impl Force { + /// Returns the force normalized to the range between 0.0 and 1.0 inclusive. + /// Instead of normalizing the force, you should prefer to handle + /// `Force::Calibrated` so that the amount of force the user has to apply is + /// consistent across devices. + pub fn normalized(&self) -> f64 { + match self { + Force::Calibrated { + force, + max_possible_force, + altitude_angle, + } => { + let force = match altitude_angle { + Some(altitude_angle) => force / altitude_angle.sin(), + None => *force, + }; + force / max_possible_force + } + Force::Normalized(force) => *force, + } + } +} + /// Hardware-dependent keyboard scan code. pub type ScanCode = u32; diff --git a/src/event_loop.rs b/src/event_loop.rs index 5230c6cf..72c5f094 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -37,9 +37,10 @@ pub struct EventLoop { /// Target that associates windows with an `EventLoop`. /// -/// This type exists to allow you to create new windows while Winit executes your callback. -/// `EventLoop` will coerce into this type, so functions that take this as a parameter can also -/// take `&EventLoop`. +/// This type exists to allow you to create new windows while Winit executes +/// your callback. `EventLoop` will coerce into this type (`impl Deref for +/// EventLoop`), so functions that take this as a parameter can also take +/// `&EventLoop`. pub struct EventLoopWindowTarget { pub(crate) p: platform_impl::EventLoopWindowTarget, pub(crate) _marker: ::std::marker::PhantomData<*mut ()>, // Not Send nor Sync @@ -99,7 +100,7 @@ impl EventLoop<()> { /// /// - **iOS:** Can only be called on the main thread. pub fn new() -> EventLoop<()> { - EventLoop::<()>::new_user_event() + EventLoop::<()>::with_user_event() } } @@ -114,7 +115,7 @@ impl EventLoop { /// ## Platform-specific /// /// - **iOS:** Can only be called on the main thread. - pub fn new_user_event() -> EventLoop { + pub fn with_user_event() -> EventLoop { EventLoop { event_loop: platform_impl::EventLoop::new(), _marker: ::std::marker::PhantomData, @@ -172,11 +173,18 @@ impl Deref for EventLoop { } /// Used to send custom events to `EventLoop`. -#[derive(Clone)] pub struct EventLoopProxy { event_loop_proxy: platform_impl::EventLoopProxy, } +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + event_loop_proxy: self.event_loop_proxy.clone(), + } + } +} + impl EventLoopProxy { /// Send an event to the `EventLoop` from which this proxy was created. This emits a /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this diff --git a/src/lib.rs b/src/lib.rs index 68a4040c..6eed4f04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,10 +121,9 @@ extern crate log; #[macro_use] extern crate serde; #[macro_use] -#[cfg(target_os = "windows")] extern crate derivative; #[macro_use] -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "ios", target_os = "windows"))] extern crate bitflags; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] diff --git a/src/monitor.rs b/src/monitor.rs index f27ef9d7..8e085a58 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -52,17 +52,41 @@ impl Iterator for AvailableMonitorsIter { /// - [`MonitorHandle::video_modes`][monitor_get]. /// /// [monitor_get]: ../monitor/struct.MonitorHandle.html#method.video_modes -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Derivative)] +#[derivative(Clone, Debug = "transparent", PartialEq, Eq, Hash)] pub struct VideoMode { - pub(crate) size: (u32, u32), - pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) video_mode: platform_impl::VideoMode, +} + +impl PartialOrd for VideoMode { + fn partial_cmp(&self, other: &VideoMode) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoMode { + fn cmp(&self, other: &VideoMode) -> std::cmp::Ordering { + // TODO: we can impl `Ord` for `PhysicalSize` once we switch from `f32` + // to `u32` there + let size: (u32, u32) = self.size().into(); + let other_size: (u32, u32) = other.size().into(); + self.monitor().cmp(&other.monitor()).then( + size.cmp(&other_size) + .then( + self.refresh_rate() + .cmp(&other.refresh_rate()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } } impl VideoMode { /// Returns the resolution of this video mode. + #[inline] pub fn size(&self) -> PhysicalSize { - self.size.into() + self.video_mode.size() } /// Returns the bit depth of this video mode, as in how many bits you have @@ -73,15 +97,37 @@ impl VideoMode { /// /// - **Wayland:** Always returns 32. /// - **iOS:** Always returns 32. + #[inline] pub fn bit_depth(&self) -> u16 { - self.bit_depth + self.video_mode.bit_depth() } /// Returns the refresh rate of this video mode. **Note**: the returned /// refresh rate is an integer approximation, and you shouldn't rely on this /// value to be exact. + #[inline] pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + self.video_mode.refresh_rate() + } + + /// Returns the monitor that this video mode is valid for. Each monitor has + /// a separate set of valid video modes. + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.video_mode.monitor() + } +} + +impl std::fmt::Display for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} Hz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate(), + self.bit_depth() + ) } } @@ -90,7 +136,7 @@ impl VideoMode { /// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. /// /// [`Window`]: ../window/struct.Window.html -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct MonitorHandle { pub(crate) inner: platform_impl::MonitorHandle, } diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 8c1034a4..9218204f 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -4,13 +4,13 @@ use std::os::raw::c_void; use crate::{ event_loop::EventLoop, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::{Window, WindowBuilder}, }; -/// Additional methods on `EventLoop` that are specific to iOS. +/// Additional methods on [`EventLoop`] that are specific to iOS. pub trait EventLoopExtIOS { - /// Returns the idiom (phone/tablet/tv/etc) for the current device. + /// Returns the [`Idiom`] (phone/tablet/tv/etc) for the current device. fn idiom(&self) -> Idiom; } @@ -20,32 +20,80 @@ impl EventLoopExtIOS for EventLoop { } } -/// Additional methods on `Window` that are specific to iOS. +/// Additional methods on [`Window`] that are specific to iOS. pub trait WindowExtIOS { - /// Returns a pointer to the `UIWindow` that is used by this window. + /// Returns a pointer to the [`UIWindow`] that is used by this window. /// - /// The pointer will become invalid when the `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc fn ui_window(&self) -> *mut c_void; - /// Returns a pointer to the `UIViewController` that is used by this window. + /// Returns a pointer to the [`UIViewController`] that is used by this window. /// - /// The pointer will become invalid when the `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIViewController`]: https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc fn ui_view_controller(&self) -> *mut c_void; - /// Returns a pointer to the `UIView` that is used by this window. + /// Returns a pointer to the [`UIView`] that is used by this window. /// - /// The pointer will become invalid when the `Window` is destroyed. + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIView`]: https://developer.apple.com/documentation/uikit/uiview?language=objc fn ui_view(&self) -> *mut c_void; - /// Sets the HiDpi factor used by this window. + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `hidpi_factor`. /// - /// This translates to `-[UIWindow setContentScaleFactor:hidpi_factor]`. + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::hidpi_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); - /// Sets the valid orientations for screens showing this `Window`. + /// Sets the valid orientations for the [`Window`]. /// - /// On iPhones and iPods upside down portrait is never enabled. + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This changes the value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc), + /// and then calls + /// [`-[UIViewController attemptRotationToDeviceOrientation]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621400-attemptrotationtodeviceorientati?language=objc). fn set_valid_orientations(&self, valid_orientations: ValidOrientations); + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_prefers_home_indicator_hidden(&self, hidden: bool); + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This changes the value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge); + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsStatusBarAppearanceUpdate]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc). + fn set_prefers_status_bar_hidden(&self, hidden: bool); } impl WindowExtIOS for Window { @@ -73,23 +121,79 @@ impl WindowExtIOS for Window { fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { self.window.set_valid_orientations(valid_orientations) } + + #[inline] + fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.window.set_prefers_home_indicator_hidden(hidden) + } + + #[inline] + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.window + .set_preferred_screen_edges_deferring_system_gestures(edges) + } + + #[inline] + fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.window.set_prefers_status_bar_hidden(hidden) + } } -/// Additional methods on `WindowBuilder` that are specific to iOS. +/// Additional methods on [`WindowBuilder`] that are specific to iOS. pub trait WindowBuilderExtIOS { - /// Sets the root view class used by the `Window`, otherwise a barebones `UIView` is provided. + /// Sets the root view class used by the [`Window`], otherwise a barebones [`UIView`] is provided. /// - /// The class will be initialized by calling `[root_view initWithFrame:CGRect]` + /// An instance of the class will be initialized by calling [`-[UIView initWithFrame:]`](https://developer.apple.com/documentation/uikit/uiview/1622488-initwithframe?language=objc). + /// + /// [`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 `hidpi_factor`. /// /// The default value is device dependent, and it's recommended GLES or Metal applications set - /// this to `MonitorHandle::hidpi_factor()`. + /// this to [`MonitorHandle::hidpi_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; - /// Sets the valid orientations for the `Window`. + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This sets the initial value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc). fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> WindowBuilder; + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_prefers_home_indicator_hidden(self, hidden: bool) -> WindowBuilder; + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_preferred_screen_edges_deferring_system_gestures( + self, + edges: ScreenEdge, + ) -> WindowBuilder; + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc). + fn with_prefers_status_bar_hidden(self, hidden: bool) -> WindowBuilder; } impl WindowBuilderExtIOS for WindowBuilder { @@ -110,12 +214,41 @@ impl WindowBuilderExtIOS for WindowBuilder { self.platform_specific.valid_orientations = valid_orientations; self } + + #[inline] + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> WindowBuilder { + self.platform_specific.prefers_home_indicator_hidden = hidden; + self + } + + #[inline] + fn with_preferred_screen_edges_deferring_system_gestures( + mut self, + edges: ScreenEdge, + ) -> WindowBuilder { + self.platform_specific + .preferred_screen_edges_deferring_system_gestures = edges; + self + } + + #[inline] + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> WindowBuilder { + self.platform_specific.prefers_status_bar_hidden = hidden; + self + } } -/// Additional methods on `MonitorHandle` that are specific to iOS. +/// Additional methods on [`MonitorHandle`] that are specific to iOS. pub trait MonitorHandleExtIOS { - /// Returns a pointer to the `UIScreen` that is used by this monitor. + /// Returns a pointer to the [`UIScreen`] that is used by this monitor. + /// + /// [`UIScreen`]: https://developer.apple.com/documentation/uikit/uiscreen?language=objc fn ui_screen(&self) -> *mut c_void; + + /// Returns the preferred [`VideoMode`] for this monitor. + /// + /// This translates to a call to [`-[UIScreen preferredMode]`](https://developer.apple.com/documentation/uikit/uiscreen/1617823-preferredmode?language=objc). + fn preferred_video_mode(&self) -> VideoMode; } impl MonitorHandleExtIOS for MonitorHandle { @@ -123,9 +256,14 @@ impl MonitorHandleExtIOS for MonitorHandle { fn ui_screen(&self) -> *mut c_void { self.inner.ui_screen() as _ } + + #[inline] + fn preferred_video_mode(&self) -> VideoMode { + self.inner.preferred_video_mode() + } } -/// Valid orientations for a particular `Window`. +/// Valid orientations for a particular [`Window`]. #[derive(Clone, Copy, Debug)] pub enum ValidOrientations { /// Excludes `PortraitUpsideDown` on iphone @@ -161,3 +299,19 @@ pub enum Idiom { TV, CarPlay, } + +bitflags! { + /// The [edges] of a screen. + /// + /// [edges]: https://developer.apple.com/documentation/uikit/uirectedge?language=objc + #[derive(Default)] + pub struct ScreenEdge: u8 { + const NONE = 0; + const TOP = 1 << 0; + const LEFT = 1 << 1; + const BOTTOM = 1 << 2; + const RIGHT = 1 << 3; + const ALL = ScreenEdge::TOP.bits | ScreenEdge::LEFT.bits + | ScreenEdge::BOTTOM.bits | ScreenEdge::RIGHT.bits; + } +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index f08c774b..8101f0f7 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -129,6 +129,7 @@ pub trait WindowBuilderExtMacOS { 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_disallow_hidpi(self, disallow_hidpi: bool) -> WindowBuilder; } impl WindowBuilderExtMacOS for WindowBuilder { @@ -182,6 +183,12 @@ impl WindowBuilderExtMacOS for WindowBuilder { self.platform_specific.resize_increments = Some(increments.into()); self } + + #[inline] + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> WindowBuilder { + self.platform_specific.disallow_hidpi = disallow_hidpi; + self + } } /// Additional methods on `MonitorHandle` that are specific to MacOS. diff --git a/src/platform/unix.rs b/src/platform/unix.rs index 494ed398..a72b1d33 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -6,14 +6,15 @@ use smithay_client_toolkit::window::{ButtonState, Theme}; use crate::{ dpi::LogicalSize, - event_loop::EventLoop, + event_loop::{EventLoop, EventLoopWindowTarget}, monitor::MonitorHandle, window::{Window, WindowBuilder}, }; use crate::platform_impl::{ x11::{ffi::XVisualInfo, XConnection}, - EventLoop as LinuxEventLoop, Window as LinuxWindow, + EventLoop as LinuxEventLoop, EventLoopWindowTarget as LinuxEventLoopWindowTarget, + Window as LinuxWindow, }; // TODO: stupid hack so that glutin can do its work @@ -90,6 +91,57 @@ impl Theme for WaylandThemeObject { } } +/// Additional methods on `EventLoopWindowTarget` that are specific to Unix. +pub trait EventLoopWindowTargetExtUnix { + /// True if the `EventLoopWindowTarget` uses Wayland. + fn is_wayland(&self) -> bool; + /// + /// True if the `EventLoopWindowTarget` uses X11. + fn is_x11(&self) -> bool; + + #[doc(hidden)] + fn xlib_xconnection(&self) -> Option>; + + /// Returns a pointer to the `wl_display` object of wayland that is used by this + /// `EventLoopWindowTarget`. + /// + /// Returns `None` if the `EventLoop` doesn't use wayland (if it uses xlib for example). + /// + /// The pointer will become invalid when the winit `EventLoop` is destroyed. + fn wayland_display(&self) -> Option<*mut raw::c_void>; +} + +impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { + #[inline] + fn is_wayland(&self) -> bool { + self.p.is_wayland() + } + + #[inline] + fn is_x11(&self) -> bool { + !self.p.is_wayland() + } + + #[inline] + #[doc(hidden)] + fn xlib_xconnection(&self) -> Option> { + match self.p { + LinuxEventLoopWindowTarget::X(ref e) => Some(e.x_connection().clone()), + _ => None, + } + } + + #[inline] + fn wayland_display(&self) -> Option<*mut raw::c_void> { + match self.p { + LinuxEventLoopWindowTarget::Wayland(ref p) => { + Some(p.display().get_display_ptr() as *mut _) + } + _ => None, + } + } +} + /// Additional methods on `EventLoop` that are specific to Unix. pub trait EventLoopExtUnix { /// Builds a new `EventLoops` that is forced to use X11. @@ -101,22 +153,6 @@ pub trait EventLoopExtUnix { fn new_wayland() -> Self where Self: Sized; - - /// True if the `EventLoop` uses Wayland. - fn is_wayland(&self) -> bool; - - /// True if the `EventLoop` uses X11. - fn is_x11(&self) -> bool; - - #[doc(hidden)] - fn xlib_xconnection(&self) -> Option>; - - /// Returns a pointer to the `wl_display` object of wayland that is used by this `EventLoop`. - /// - /// Returns `None` if the `EventLoop` doesn't use wayland (if it uses xlib for example). - /// - /// The pointer will become invalid when the glutin `EventLoop` is destroyed. - fn wayland_display(&self) -> Option<*mut raw::c_void>; } impl EventLoopExtUnix for EventLoop { @@ -138,33 +174,6 @@ impl EventLoopExtUnix for EventLoop { _marker: ::std::marker::PhantomData, } } - - #[inline] - fn is_wayland(&self) -> bool { - self.event_loop.is_wayland() - } - - #[inline] - fn is_x11(&self) -> bool { - !self.event_loop.is_wayland() - } - - #[inline] - #[doc(hidden)] - fn xlib_xconnection(&self) -> Option> { - match self.event_loop { - LinuxEventLoop::X(ref e) => Some(e.x_connection().clone()), - _ => None, - } - } - - #[inline] - fn wayland_display(&self) -> Option<*mut raw::c_void> { - match self.event_loop { - LinuxEventLoop::Wayland(ref e) => Some(e.display().get_display_ptr() as *mut _), - _ => None, - } - } } /// Additional methods on `Window` that are specific to Unix. @@ -258,17 +267,17 @@ impl WindowExtUnix for Window { } #[inline] - fn xcb_connection(&self) -> Option<*mut raw::c_void> { - match self.window { - LinuxWindow::X(ref w) => Some(w.xcb_connection()), - _ => None, + fn set_urgent(&self, is_urgent: bool) { + if let LinuxWindow::X(ref w) = self.window { + w.set_urgent(is_urgent); } } #[inline] - fn set_urgent(&self, is_urgent: bool) { - if let LinuxWindow::X(ref w) = self.window { - w.set_urgent(is_urgent); + fn xcb_connection(&self) -> Option<*mut raw::c_void> { + match self.window { + LinuxWindow::X(ref w) => Some(w.xcb_connection()), + _ => None, } } @@ -304,82 +313,82 @@ impl WindowExtUnix for Window { /// Additional methods on `WindowBuilder` that are specific to Unix. pub trait WindowBuilderExtUnix { - fn with_x11_visual(self, visual_infos: *const T) -> WindowBuilder; - fn with_x11_screen(self, screen_id: i32) -> WindowBuilder; + fn with_x11_visual(self, visual_infos: *const T) -> Self; + 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. - fn with_class(self, class: String, instance: String) -> WindowBuilder; + fn with_class(self, class: String, instance: String) -> Self; /// Build window with override-redirect flag; defaults to false. Only relevant on X11. - fn with_override_redirect(self, override_redirect: bool) -> WindowBuilder; - /// Build window with `_NET_WM_WINDOW_TYPE` hint; defaults to `Normal`. Only relevant on X11. - fn with_x11_window_type(self, x11_window_type: XWindowType) -> WindowBuilder; + fn with_override_redirect(self, override_redirect: bool) -> Self; + /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. Only relevant on 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. - fn with_gtk_theme_variant(self, variant: String) -> WindowBuilder; + 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) -> WindowBuilder; + fn with_resize_increments(self, increments: LogicalSize) -> Self; /// Build window with base size hint. Only implemented on X11. - fn with_base_size(self, base_size: LogicalSize) -> WindowBuilder; + fn with_base_size(self, base_size: LogicalSize) -> 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) - fn with_app_id(self, app_id: String) -> WindowBuilder; + fn with_app_id(self, app_id: String) -> Self; } impl WindowBuilderExtUnix for WindowBuilder { #[inline] - fn with_x11_visual(mut self, visual_infos: *const T) -> WindowBuilder { + 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 } #[inline] - fn with_x11_screen(mut self, screen_id: i32) -> WindowBuilder { + fn with_x11_screen(mut self, screen_id: i32) -> Self { self.platform_specific.screen_id = Some(screen_id); self } #[inline] - fn with_class(mut self, instance: String, class: String) -> WindowBuilder { + fn with_class(mut self, instance: String, class: String) -> Self { self.platform_specific.class = Some((instance, class)); self } #[inline] - fn with_override_redirect(mut self, override_redirect: bool) -> WindowBuilder { + fn with_override_redirect(mut self, override_redirect: bool) -> Self { self.platform_specific.override_redirect = override_redirect; self } #[inline] - fn with_x11_window_type(mut self, x11_window_type: XWindowType) -> WindowBuilder { - self.platform_specific.x11_window_type = x11_window_type; + fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { + self.platform_specific.x11_window_types = x11_window_types; self } #[inline] - fn with_resize_increments(mut self, increments: LogicalSize) -> WindowBuilder { - self.platform_specific.resize_increments = Some(increments.into()); - self - } - - #[inline] - fn with_base_size(mut self, base_size: LogicalSize) -> WindowBuilder { - self.platform_specific.base_size = Some(base_size.into()); - self - } - - #[inline] - fn with_gtk_theme_variant(mut self, variant: String) -> WindowBuilder { + fn with_gtk_theme_variant(mut self, variant: String) -> Self { self.platform_specific.gtk_theme_variant = Some(variant); self } #[inline] - fn with_app_id(mut self, app_id: String) -> WindowBuilder { + fn with_resize_increments(mut self, increments: LogicalSize) -> Self { + self.platform_specific.resize_increments = Some(increments.into()); + self + } + + #[inline] + fn with_base_size(mut self, base_size: LogicalSize) -> Self { + self.platform_specific.base_size = Some(base_size.into()); + self + } + + #[inline] + fn with_app_id(mut self, app_id: String) -> Self { self.platform_specific.app_id = Some(app_id); self } diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 42430d01..f88251ea 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -75,6 +75,7 @@ impl EventLoop { android_glue::MotionAction::Cancel => TouchPhase::Cancelled, }, location, + force: None, // TODO id: motion.pointer_id as u64, device_id: DEVICE_ID, }), diff --git a/src/platform_impl/emscripten/ffi.rs b/src/platform_impl/emscripten/ffi.rs deleted file mode 100644 index 6eb9d743..00000000 --- a/src/platform_impl/emscripten/ffi.rs +++ /dev/null @@ -1,363 +0,0 @@ -#![allow(dead_code, non_camel_case_types, non_snake_case)] - -#[cfg(test)] -use std::mem; -use std::os::raw::{c_char, c_double, c_int, c_long, c_ulong, c_ushort, c_void}; - -pub type EM_BOOL = c_int; -pub type EM_UTF8 = c_char; -pub type EMSCRIPTEN_RESULT = c_int; - -pub const EM_TRUE: EM_BOOL = 1; -pub const EM_FALSE: EM_BOOL = 0; - -// values for EMSCRIPTEN_RESULT -pub const EMSCRIPTEN_RESULT_SUCCESS: c_int = 0; -pub const EMSCRIPTEN_RESULT_DEFERRED: c_int = 1; -pub const EMSCRIPTEN_RESULT_NOT_SUPPORTED: c_int = -1; -pub const EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED: c_int = -2; -pub const EMSCRIPTEN_RESULT_INVALID_TARGET: c_int = -3; -pub const EMSCRIPTEN_RESULT_UNKNOWN_TARGET: c_int = -4; -pub const EMSCRIPTEN_RESULT_INVALID_PARAM: c_int = -5; -pub const EMSCRIPTEN_RESULT_FAILED: c_int = -6; -pub const EMSCRIPTEN_RESULT_NO_DATA: c_int = -7; - -// values for EMSCRIPTEN EVENT -pub const EMSCRIPTEN_EVENT_KEYPRESS: c_int = 1; -pub const EMSCRIPTEN_EVENT_KEYDOWN: c_int = 2; -pub const EMSCRIPTEN_EVENT_KEYUP: c_int = 3; -pub const EMSCRIPTEN_EVENT_CLICK: c_int = 4; -pub const EMSCRIPTEN_EVENT_MOUSEDOWN: c_int = 5; -pub const EMSCRIPTEN_EVENT_MOUSEUP: c_int = 6; -pub const EMSCRIPTEN_EVENT_DBLCLICK: c_int = 7; -pub const EMSCRIPTEN_EVENT_MOUSEMOVE: c_int = 8; -pub const EMSCRIPTEN_EVENT_WHEEL: c_int = 9; -pub const EMSCRIPTEN_EVENT_RESIZE: c_int = 10; -pub const EMSCRIPTEN_EVENT_SCROLL: c_int = 11; -pub const EMSCRIPTEN_EVENT_BLUR: c_int = 12; -pub const EMSCRIPTEN_EVENT_FOCUS: c_int = 13; -pub const EMSCRIPTEN_EVENT_FOCUSIN: c_int = 14; -pub const EMSCRIPTEN_EVENT_FOCUSOUT: c_int = 15; -pub const EMSCRIPTEN_EVENT_DEVICEORIENTATION: c_int = 16; -pub const EMSCRIPTEN_EVENT_DEVICEMOTION: c_int = 17; -pub const EMSCRIPTEN_EVENT_ORIENTATIONCHANGE: c_int = 18; -pub const EMSCRIPTEN_EVENT_FULLSCREENCHANGE: c_int = 19; -pub const EMSCRIPTEN_EVENT_POINTERLOCKCHANGE: c_int = 20; -pub const EMSCRIPTEN_EVENT_VISIBILITYCHANGE: c_int = 21; -pub const EMSCRIPTEN_EVENT_TOUCHSTART: c_int = 22; -pub const EMSCRIPTEN_EVENT_TOUCHEND: c_int = 23; -pub const EMSCRIPTEN_EVENT_TOUCHMOVE: c_int = 24; -pub const EMSCRIPTEN_EVENT_TOUCHCANCEL: c_int = 25; -pub const EMSCRIPTEN_EVENT_GAMEPADCONNECTED: c_int = 26; -pub const EMSCRIPTEN_EVENT_GAMEPADDISCONNECTED: c_int = 27; -pub const EMSCRIPTEN_EVENT_BEFOREUNLOAD: c_int = 28; -pub const EMSCRIPTEN_EVENT_BATTERYCHARGINGCHANGE: c_int = 29; -pub const EMSCRIPTEN_EVENT_BATTERYLEVELCHANGE: c_int = 30; -pub const EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST: c_int = 31; -pub const EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED: c_int = 32; -pub const EMSCRIPTEN_EVENT_MOUSEENTER: c_int = 33; -pub const EMSCRIPTEN_EVENT_MOUSELEAVE: c_int = 34; -pub const EMSCRIPTEN_EVENT_MOUSEOVER: c_int = 35; -pub const EMSCRIPTEN_EVENT_MOUSEOUT: c_int = 36; -pub const EMSCRIPTEN_EVENT_CANVASRESIZED: c_int = 37; -pub const EMSCRIPTEN_EVENT_POINTERLOCKERROR: c_int = 38; - -pub const EM_HTML5_SHORT_STRING_LEN_BYTES: usize = 32; - -pub const DOM_KEY_LOCATION_STANDARD: c_ulong = 0x00; -pub const DOM_KEY_LOCATION_LEFT: c_ulong = 0x01; -pub const DOM_KEY_LOCATION_RIGHT: c_ulong = 0x02; -pub const DOM_KEY_LOCATION_NUMPAD: c_ulong = 0x03; - -pub type em_callback_func = Option; - -pub type em_key_callback_func = Option< - unsafe extern "C" fn( - eventType: c_int, - keyEvent: *const EmscriptenKeyboardEvent, - userData: *mut c_void, - ) -> EM_BOOL, ->; - -pub type em_mouse_callback_func = Option< - unsafe extern "C" fn( - eventType: c_int, - mouseEvent: *const EmscriptenMouseEvent, - userData: *mut c_void, - ) -> EM_BOOL, ->; - -pub type em_pointerlockchange_callback_func = Option< - unsafe extern "C" fn( - eventType: c_int, - pointerlockChangeEvent: *const EmscriptenPointerlockChangeEvent, - userData: *mut c_void, - ) -> EM_BOOL, ->; - -pub type em_fullscreenchange_callback_func = Option< - unsafe extern "C" fn( - eventType: c_int, - fullscreenChangeEvent: *const EmscriptenFullscreenChangeEvent, - userData: *mut c_void, - ) -> EM_BOOL, ->; - -pub type em_touch_callback_func = Option< - unsafe extern "C" fn( - eventType: c_int, - touchEvent: *const EmscriptenTouchEvent, - userData: *mut c_void, - ) -> EM_BOOL, ->; - -#[repr(C)] -pub struct EmscriptenFullscreenChangeEvent { - pub isFullscreen: c_int, - pub fullscreenEnabled: c_int, - pub nodeName: [c_char; 128usize], - pub id: [c_char; 128usize], - pub elementWidth: c_int, - pub elementHeight: c_int, - pub screenWidth: c_int, - pub screenHeight: c_int, -} -#[test] -fn bindgen_test_layout_EmscriptenFullscreenChangeEvent() { - assert_eq!(mem::size_of::(), 280usize); - assert_eq!(mem::align_of::(), 4usize); -} - -#[repr(C)] -#[derive(Debug, Copy)] -pub struct EmscriptenKeyboardEvent { - pub key: [c_char; 32usize], - pub code: [c_char; 32usize], - pub location: c_ulong, - pub ctrlKey: c_int, - pub shiftKey: c_int, - pub altKey: c_int, - pub metaKey: c_int, - pub repeat: c_int, - pub locale: [c_char; 32usize], - pub charValue: [c_char; 32usize], - pub charCode: c_ulong, - pub keyCode: c_ulong, - pub which: c_ulong, -} -#[test] -fn bindgen_test_layout_EmscriptenKeyboardEvent() { - assert_eq!(mem::size_of::(), 184usize); - assert_eq!(mem::align_of::(), 8usize); -} -impl Clone for EmscriptenKeyboardEvent { - fn clone(&self) -> Self { - *self - } -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EmscriptenMouseEvent { - pub timestamp: f64, - pub screenX: c_long, - pub screenY: c_long, - pub clientX: c_long, - pub clientY: c_long, - pub ctrlKey: c_int, - pub shiftKey: c_int, - pub altKey: c_int, - pub metaKey: c_int, - pub button: c_ushort, - pub buttons: c_ushort, - pub movementX: c_long, - pub movementY: c_long, - pub targetX: c_long, - pub targetY: c_long, - pub canvasX: c_long, - pub canvasY: c_long, - pub padding: c_long, -} -#[test] -fn bindgen_test_layout_EmscriptenMouseEvent() { - assert_eq!(mem::size_of::(), 120usize); - assert_eq!(mem::align_of::(), 8usize); -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EmscriptenTouchPoint { - pub identifier: c_long, - pub screenX: c_long, - pub screenY: c_long, - pub clientX: c_long, - pub clientY: c_long, - pub pageX: c_long, - pub pageY: c_long, - pub isChanged: c_int, - pub onTarget: c_int, - pub targetX: c_long, - pub targetY: c_long, - pub canvasX: c_long, - pub canvasY: c_long, -} -#[test] -fn bindgen_test_layout_EmscriptenTouchPoint() { - assert_eq!(mem::size_of::(), 96usize); - assert_eq!(mem::align_of::(), 8usize); -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EmscriptenTouchEvent { - pub numTouches: c_int, - pub ctrlKey: c_int, - pub shiftKey: c_int, - pub altKey: c_int, - pub metaKey: c_int, - pub touches: [EmscriptenTouchPoint; 32usize], -} -#[test] -fn bindgen_test_layout_EmscriptenTouchEvent() { - assert_eq!(mem::size_of::(), 3096usize); - assert_eq!(mem::align_of::(), 8usize); -} - -#[repr(C)] -pub struct EmscriptenPointerlockChangeEvent { - pub isActive: c_int, - pub nodeName: [c_char; 128usize], - pub id: [c_char; 128usize], -} -#[test] -fn bindgen_test_layout_EmscriptenPointerlockChangeEvent() { - assert_eq!(mem::size_of::(), 260usize); - assert_eq!(mem::align_of::(), 4usize); -} - -extern "C" { - pub fn emscripten_set_canvas_size(width: c_int, height: c_int) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_get_canvas_size( - width: *mut c_int, - height: *mut c_int, - is_fullscreen: *mut c_int, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_element_css_size( - target: *const c_char, - width: c_double, - height: c_double, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_get_element_css_size( - target: *const c_char, - width: *mut c_double, - height: *mut c_double, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_request_pointerlock( - target: *const c_char, - deferUntilInEventHandler: EM_BOOL, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_exit_pointerlock() -> EMSCRIPTEN_RESULT; - - pub fn emscripten_request_fullscreen( - target: *const c_char, - deferUntilInEventHandler: EM_BOOL, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_exit_fullscreen() -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_keydown_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: EM_BOOL, - callback: em_key_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_keyup_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: EM_BOOL, - callback: em_key_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_mousemove_callback( - target: *const c_char, - user_data: *mut c_void, - use_capture: EM_BOOL, - callback: em_mouse_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_mousedown_callback( - target: *const c_char, - user_data: *mut c_void, - use_capture: EM_BOOL, - callback: em_mouse_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_mouseup_callback( - target: *const c_char, - user_data: *mut c_void, - use_capture: EM_BOOL, - callback: em_mouse_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_hide_mouse(); - - pub fn emscripten_get_device_pixel_ratio() -> f64; - - pub fn emscripten_set_pointerlockchange_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: EM_BOOL, - callback: em_pointerlockchange_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_fullscreenchange_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: EM_BOOL, - callback: em_fullscreenchange_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_asm_const(code: *const c_char); - - pub fn emscripten_set_main_loop( - func: em_callback_func, - fps: c_int, - simulate_infinite_loop: EM_BOOL, - ); - - pub fn emscripten_cancel_main_loop(); - - pub fn emscripten_set_touchstart_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: c_int, - callback: em_touch_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_touchend_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: c_int, - callback: em_touch_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_touchmove_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: c_int, - callback: em_touch_callback_func, - ) -> EMSCRIPTEN_RESULT; - - pub fn emscripten_set_touchcancel_callback( - target: *const c_char, - userData: *mut c_void, - useCapture: c_int, - callback: em_touch_callback_func, - ) -> EMSCRIPTEN_RESULT; -} diff --git a/src/platform_impl/emscripten/mod.rs b/src/platform_impl/emscripten/mod.rs deleted file mode 100644 index 9148dab0..00000000 --- a/src/platform_impl/emscripten/mod.rs +++ /dev/null @@ -1,1245 +0,0 @@ -#![cfg(target_os = "emscripten")] - -mod ffi; - -use std::{ - cell::RefCell, - collections::VecDeque, - mem, - os::raw::{c_char, c_double, c_int, c_ulong, c_void}, - ptr, str, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, - }, -}; - -use crate::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - error::{ExternalError, NotSupportedError}, - window::MonitorHandle as RootMonitorHandle, -}; - -const DOCUMENT_NAME: &'static str = "#document\0"; - -fn hidpi_factor() -> f64 { - unsafe { ffi::emscripten_get_device_pixel_ratio() as f64 } -} - -#[derive(Clone, Default)] -pub struct PlatformSpecificWindowBuilderAttributes; - -unsafe impl Send for PlatformSpecificWindowBuilderAttributes {} -unsafe impl Sync for PlatformSpecificWindowBuilderAttributes {} - -pub type OsError = std::io::Error; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId; - -impl DeviceId { - pub unsafe fn dummy() -> Self { - DeviceId - } -} - -#[derive(Clone, Default)] -pub struct PlatformSpecificHeadlessBuilderAttributes; - -#[derive(Debug, Clone)] -pub struct MonitorHandle; - -impl MonitorHandle { - #[inline] - pub fn name(&self) -> Option { - Some("Canvas".to_owned()) - } - - #[inline] - pub fn outer_position(&self) -> PhysicalPosition { - unimplemented!() - } - - #[inline] - pub fn size(&self) -> PhysicalSize { - (0, 0).into() - } - - #[inline] - pub fn hidpi_factor(&self) -> f64 { - hidpi_factor() - } -} - -// Used to assign a callback to emscripten main loop -thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(ptr::null_mut())); - -// Used to assign a callback to emscripten main loop -pub fn set_main_loop_callback(callback: F) -where - F: FnMut(), -{ - MAIN_LOOP_CALLBACK.with(|log| { - *log.borrow_mut() = &callback as *const _ as *mut c_void; - }); - - unsafe { - ffi::emscripten_set_main_loop(Some(wrapper::), 0, 1); - } - - unsafe extern "C" fn wrapper() - where - F: FnMut(), - { - MAIN_LOOP_CALLBACK.with(|z| { - let closure = *z.borrow_mut() as *mut F; - (*closure)(); - }); - } -} - -#[derive(Clone)] -pub struct EventLoopProxy; - -impl EventLoopProxy { - pub fn wakeup(&self) -> Result<(), ::EventLoopClosed> { - unimplemented!() - } -} - -pub struct EventLoop { - window: Mutex>>, - interrupted: AtomicBool, -} - -impl EventLoop { - pub fn new() -> EventLoop { - EventLoop { - window: Mutex::new(None), - interrupted: AtomicBool::new(false), - } - } - - #[inline] - pub fn interrupt(&self) { - self.interrupted.store(true, Ordering::Relaxed); - } - - #[inline] - pub fn create_proxy(&self) -> EventLoopProxy { - unimplemented!() - } - - #[inline] - pub fn available_monitors(&self) -> VecDeque { - let mut list = VecDeque::with_capacity(1); - list.push_back(MonitorHandle); - list - } - - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle - } - - pub fn poll_events(&self, mut callback: F) - where - F: FnMut(::Event), - { - let ref mut window = *self.window.lock().unwrap(); - if let &mut Some(ref mut window) = window { - while let Some(event) = window.events.lock().unwrap().pop_front() { - callback(event) - } - } - } - - pub fn run_forever(&self, mut callback: F) - where - F: FnMut(::Event) -> ::ControlFlow, - { - self.interrupted.store(false, Ordering::Relaxed); - - // TODO: handle control flow - - set_main_loop_callback(|| { - self.poll_events(|e| { - callback(e); - }); - ::std::thread::sleep(::std::time::Duration::from_millis(5)); - if self.interrupted.load(Ordering::Relaxed) { - unsafe { - ffi::emscripten_cancel_main_loop(); - } - } - }); - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId(usize); - -impl WindowId { - pub unsafe fn dummy() -> Self { - WindowId(0) - } -} - -pub struct Window2 { - cursor_grabbed: Mutex, - cursor_visible: Mutex, - is_fullscreen: bool, - events: Box>>, -} - -pub struct Window { - window: Arc, -} - -fn show_mouse() { - // Hide mouse hasn't show mouse equivalent. - // There is a pull request on emscripten that hasn't been merged #4616 - // that contains: - // - // var styleSheet = document.styleSheets[0]; - // var rules = styleSheet.cssRules; - // for (var i = 0; i < rules.length; i++) { - // if (rules[i].cssText.substr(0, 6) == 'canvas') { - // styleSheet.deleteRule(i); - // i--; - // } - // } - // styleSheet.insertRule('canvas.emscripten { border: none; cursor: auto; }', 0); - unsafe { - ffi::emscripten_asm_const(b"var styleSheet = document.styleSheets[0]; var rules = styleSheet.cssRules; for (var i = 0; i < rules.length; i++) { if (rules[i].cssText.substr(0, 6) == 'canvas') { styleSheet.deleteRule(i); i--; } } styleSheet.insertRule('canvas.emscripten { border: none; cursor: auto; }', 0);\0".as_ptr() as *const c_char); - } -} - -extern "C" fn mouse_callback( - event_type: c_int, - event: *const ffi::EmscriptenMouseEvent, - event_queue: *mut c_void, -) -> ffi::EM_BOOL { - unsafe { - let queue: &Mutex> = mem::transmute(event_queue); - - let modifiers = ::ModifiersState { - shift: (*event).shiftKey == ffi::EM_TRUE, - ctrl: (*event).ctrlKey == ffi::EM_TRUE, - alt: (*event).altKey == ffi::EM_TRUE, - logo: (*event).metaKey == ffi::EM_TRUE, - }; - - match event_type { - ffi::EMSCRIPTEN_EVENT_MOUSEMOVE => { - let dpi_factor = hidpi_factor(); - let position = LogicalPosition::from_physical( - ((*event).canvasX as f64, (*event).canvasY as f64), - dpi_factor, - ); - queue.lock().unwrap().push_back(::Event::WindowEvent { - window_id: ::WindowId(WindowId(0)), - event: ::WindowEvent::CursorMoved { - device_id: ::DeviceId(DeviceId), - position, - modifiers, - }, - }); - queue.lock().unwrap().push_back(::Event::DeviceEvent { - device_id: ::DeviceId(DeviceId), - event: ::DeviceEvent::MouseMotion { - delta: ((*event).movementX as f64, (*event).movementY as f64), - }, - }); - } - mouse_input @ ffi::EMSCRIPTEN_EVENT_MOUSEDOWN - | mouse_input @ ffi::EMSCRIPTEN_EVENT_MOUSEUP => { - let button = match (*event).button { - 0 => ::MouseButton::Left, - 1 => ::MouseButton::Middle, - 2 => ::MouseButton::Right, - other => ::MouseButton::Other(other as u8), - }; - let state = match mouse_input { - ffi::EMSCRIPTEN_EVENT_MOUSEDOWN => ::ElementState::Pressed, - ffi::EMSCRIPTEN_EVENT_MOUSEUP => ::ElementState::Released, - _ => unreachable!(), - }; - queue.lock().unwrap().push_back(::Event::WindowEvent { - window_id: ::WindowId(WindowId(0)), - event: ::WindowEvent::MouseInput { - device_id: ::DeviceId(DeviceId), - state, - button, - modifiers, - }, - }) - } - _ => {} - } - } - ffi::EM_FALSE -} - -extern "C" fn keyboard_callback( - event_type: c_int, - event: *const ffi::EmscriptenKeyboardEvent, - event_queue: *mut c_void, -) -> ffi::EM_BOOL { - unsafe { - let queue: &Mutex> = mem::transmute(event_queue); - - let modifiers = ::ModifiersState { - shift: (*event).shiftKey == ffi::EM_TRUE, - ctrl: (*event).ctrlKey == ffi::EM_TRUE, - alt: (*event).altKey == ffi::EM_TRUE, - logo: (*event).metaKey == ffi::EM_TRUE, - }; - - match event_type { - ffi::EMSCRIPTEN_EVENT_KEYDOWN => { - queue.lock().unwrap().push_back(::Event::WindowEvent { - window_id: ::WindowId(WindowId(0)), - event: ::WindowEvent::KeyboardInput { - device_id: ::DeviceId(DeviceId), - input: ::KeyboardInput { - scancode: key_translate((*event).key) as u32, - state: ::ElementState::Pressed, - virtual_keycode: key_translate_virt((*event).key, (*event).location), - modifiers, - }, - }, - }); - } - ffi::EMSCRIPTEN_EVENT_KEYUP => { - queue.lock().unwrap().push_back(::Event::WindowEvent { - window_id: ::WindowId(WindowId(0)), - event: ::WindowEvent::KeyboardInput { - device_id: ::DeviceId(DeviceId), - input: ::KeyboardInput { - scancode: key_translate((*event).key) as u32, - state: ::ElementState::Released, - virtual_keycode: key_translate_virt((*event).key, (*event).location), - modifiers, - }, - }, - }); - } - _ => {} - } - } - ffi::EM_FALSE -} - -extern "C" fn touch_callback( - event_type: c_int, - event: *const ffi::EmscriptenTouchEvent, - event_queue: *mut c_void, -) -> ffi::EM_BOOL { - unsafe { - let queue: &Mutex> = mem::transmute(event_queue); - - let phase = match event_type { - ffi::EMSCRIPTEN_EVENT_TOUCHSTART => ::TouchPhase::Started, - ffi::EMSCRIPTEN_EVENT_TOUCHEND => ::TouchPhase::Ended, - ffi::EMSCRIPTEN_EVENT_TOUCHMOVE => ::TouchPhase::Moved, - ffi::EMSCRIPTEN_EVENT_TOUCHCANCEL => ::TouchPhase::Cancelled, - _ => return ffi::EM_FALSE, - }; - - for touch in 0..(*event).numTouches as usize { - let touch = (*event).touches[touch]; - if touch.isChanged == ffi::EM_TRUE { - let dpi_factor = hidpi_factor(); - let location = LogicalPosition::from_physical( - (touch.canvasX as f64, touch.canvasY as f64), - dpi_factor, - ); - queue.lock().unwrap().push_back(::Event::WindowEvent { - window_id: ::WindowId(WindowId(0)), - event: ::WindowEvent::Touch(::Touch { - device_id: ::DeviceId(DeviceId), - phase, - id: touch.identifier as u64, - location, - }), - }); - } - } - } - ffi::EM_FALSE -} - -// In case of fullscreen window this method will request fullscreen on change -#[allow(non_snake_case)] -unsafe extern "C" fn fullscreen_callback( - _eventType: c_int, - _fullscreenChangeEvent: *const ffi::EmscriptenFullscreenChangeEvent, - _userData: *mut c_void, -) -> ffi::EM_BOOL { - ffi::emscripten_request_fullscreen(ptr::null(), ffi::EM_TRUE); - ffi::EM_FALSE -} - -// In case of pointer grabbed this method will request pointer lock on change -#[allow(non_snake_case)] -unsafe extern "C" fn pointerlockchange_callback( - _eventType: c_int, - _pointerlockChangeEvent: *const ffi::EmscriptenPointerlockChangeEvent, - _userData: *mut c_void, -) -> ffi::EM_BOOL { - ffi::emscripten_request_pointerlock(ptr::null(), ffi::EM_TRUE); - ffi::EM_FALSE -} - -fn em_try(res: ffi::EMSCRIPTEN_RESULT) -> Result<(), String> { - match res { - ffi::EMSCRIPTEN_RESULT_SUCCESS | ffi::EMSCRIPTEN_RESULT_DEFERRED => Ok(()), - r @ _ => Err(error_to_str(r).to_string()), - } -} - -impl Window { - pub fn new( - event_loop: &EventLoop, - attribs: ::WindowAttributes, - _pl_attribs: PlatformSpecificWindowBuilderAttributes, - ) -> Result { - if event_loop.window.lock().unwrap().is_some() { - return Err(::CreationError::OsError( - "Cannot create another window".to_owned(), - )); - } - - let w = Window2 { - cursor_grabbed: Mutex::new(false), - cursor_visible: Mutex::new(true), - events: Default::default(), - is_fullscreen: attribs.fullscreen.is_some(), - }; - - let window = Window { - window: Arc::new(w), - }; - - // TODO: set up more event callbacks - unsafe { - em_try(ffi::emscripten_set_mousemove_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(mouse_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_mousedown_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(mouse_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_mouseup_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(mouse_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_keydown_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(keyboard_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_keyup_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(keyboard_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_touchstart_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(touch_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_touchend_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(touch_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_touchmove_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(touch_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - em_try(ffi::emscripten_set_touchcancel_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - mem::transmute(&*window.window.events), - ffi::EM_FALSE, - Some(touch_callback), - )) - .map_err(|e| ::CreationError::OsError(format!("emscripten error: {}", e)))?; - } - - if attribs.fullscreen.is_some() { - unsafe { - em_try(ffi::emscripten_request_fullscreen( - ptr::null(), - ffi::EM_TRUE, - )) - .map_err(|e| ::CreationError::OsError(e))?; - em_try(ffi::emscripten_set_fullscreenchange_callback( - ptr::null(), - 0 as *mut c_void, - ffi::EM_FALSE, - Some(fullscreen_callback), - )) - .map_err(|e| ::CreationError::OsError(e))?; - } - } else if let Some(size) = attribs.inner_size { - window.set_inner_size(size); - } - - *event_loop.window.lock().unwrap() = Some(window.window.clone()); - Ok(window) - } - - #[inline] - pub fn id(&self) -> WindowId { - WindowId(0) - } - - #[inline] - pub fn set_title(&self, _title: &str) {} - - #[inline] - pub fn outer_position(&self) -> Option { - Some((0, 0).into()) - } - - #[inline] - pub fn inner_position(&self) -> Option { - Some((0, 0).into()) - } - - #[inline] - pub fn set_outer_position(&self, _: LogicalPosition) {} - - #[inline] - pub fn inner_size(&self) -> Option { - unsafe { - let mut width = 0; - let mut height = 0; - let mut fullscreen = 0; - - if ffi::emscripten_get_canvas_size(&mut width, &mut height, &mut fullscreen) - != ffi::EMSCRIPTEN_RESULT_SUCCESS - { - None - } else { - let dpi_factor = self.hidpi_factor(); - let logical = LogicalSize::from_physical((width as u32, height as u32), dpi_factor); - Some(logical) - } - } - } - - #[inline] - pub fn outer_size(&self) -> Option { - self.inner_size() - } - - #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { - unsafe { - let dpi_factor = self.hidpi_factor(); - let physical = PhysicalSize::from_logical(size, dpi_factor); - let (width, height): (u32, u32) = physical.into(); - ffi::emscripten_set_element_css_size( - ptr::null(), - width as c_double, - height as c_double, - ); - } - } - - #[inline] - pub fn set_min_inner_size(&self, _dimensions: Option) { - // N/A - } - - #[inline] - pub fn set_max_inner_size(&self, _dimensions: Option) { - // N/A - } - - #[inline] - pub fn set_resizable(&self, _resizable: bool) { - // N/A - } - - #[inline] - pub fn show(&self) { - // N/A - } - - #[inline] - pub fn hide(&self) { - // N/A - } - - #[inline] - pub fn set_cursor_icon(&self, _cursor: ::CursorIcon) { - // N/A - } - - #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - let mut grabbed_lock = self.window.cursor_grabbed.lock().unwrap(); - if grab == *grabbed_lock { - return Ok(()); - } - unsafe { - if grab { - em_try(ffi::emscripten_set_pointerlockchange_callback( - ptr::null(), - 0 as *mut c_void, - ffi::EM_FALSE, - Some(pointerlockchange_callback), - ))?; - em_try(ffi::emscripten_request_pointerlock( - ptr::null(), - ffi::EM_TRUE, - ))?; - } else { - em_try(ffi::emscripten_set_pointerlockchange_callback( - ptr::null(), - 0 as *mut c_void, - ffi::EM_FALSE, - None, - ))?; - em_try(ffi::emscripten_exit_pointerlock())?; - } - } - *grabbed_lock = grab; - Ok(()) - } - - #[inline] - pub fn set_cursor_visible(&self, visible: bool) { - let mut visible_lock = self.window.cursor_visible.lock().unwrap(); - if visible == *visible_lock { - return; - } - if visible { - show_mouse(); - } else { - unsafe { ffi::emscripten_hide_mouse() }; - } - *visible_lock = visible; - } - - #[inline] - pub fn hidpi_factor(&self) -> f64 { - hidpi_factor() - } - - #[inline] - pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), ExternalError> { - Err("Setting cursor position is not possible on Emscripten.".to_owned()) - } - - #[inline] - pub fn set_maximized(&self, _maximized: bool) { - // iOS has single screen maximized apps so nothing to do - } - - #[inline] - pub fn fullscreen(&self) -> Option<::MonitorHandle> { - None - } - - #[inline] - pub fn set_fullscreen(&self, _monitor: Option<::MonitorHandle>) { - // iOS has single screen maximized apps so nothing to do - } - - #[inline] - pub fn set_decorations(&self, _decorations: bool) { - // N/A - } - - #[inline] - pub fn set_always_on_top(&self, _always_on_top: bool) { - // N/A - } - - #[inline] - pub fn set_window_icon(&self, _icon: Option<::Icon>) { - // N/A - } - - #[inline] - pub fn set_ime_position(&self, _logical_spot: LogicalPosition) { - // N/A - } - - #[inline] - pub fn current_monitor(&self) -> RootMonitorHandle { - RootMonitorHandle { - inner: MonitorHandle, - } - } - - #[inline] - pub fn available_monitors(&self) -> VecDeque { - let mut list = VecDeque::with_capacity(1); - list.push_back(MonitorHandle); - list - } - - #[inline] - pub fn primary_monitor(&self) -> MonitorHandle { - MonitorHandle - } -} - -impl Drop for Window { - fn drop(&mut self) { - // Delete window from event_loop - // TODO: ? - /*if let Some(ev) = self.event_loop.upgrade() { - let _ = ev.window.lock().unwrap().take().unwrap(); - }*/ - - unsafe { - // Return back to normal cursor state - self.hide_cursor(false); - self.set_cursor_grab(false); - - // Exit fullscreen if on - if self.window.is_fullscreen { - ffi::emscripten_set_fullscreenchange_callback( - ptr::null(), - 0 as *mut c_void, - ffi::EM_FALSE, - None, - ); - ffi::emscripten_exit_fullscreen(); - } - - // Delete callbacks - ffi::emscripten_set_keydown_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - 0 as *mut c_void, - ffi::EM_FALSE, - None, - ); - ffi::emscripten_set_keyup_callback( - DOCUMENT_NAME.as_ptr() as *const c_char, - 0 as *mut c_void, - ffi::EM_FALSE, - None, - ); - } - } -} - -fn error_to_str(code: ffi::EMSCRIPTEN_RESULT) -> &'static str { - match code { - ffi::EMSCRIPTEN_RESULT_SUCCESS | ffi::EMSCRIPTEN_RESULT_DEFERRED => { - "Internal error in the library (success detected as failure)" - } - - ffi::EMSCRIPTEN_RESULT_NOT_SUPPORTED => "Not supported", - ffi::EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED => "Failed not deferred", - ffi::EMSCRIPTEN_RESULT_INVALID_TARGET => "Invalid target", - ffi::EMSCRIPTEN_RESULT_UNKNOWN_TARGET => "Unknown target", - ffi::EMSCRIPTEN_RESULT_INVALID_PARAM => "Invalid parameter", - ffi::EMSCRIPTEN_RESULT_FAILED => "Failed", - ffi::EMSCRIPTEN_RESULT_NO_DATA => "No data", - - _ => "Undocumented error", - } -} - -fn key_translate(input: [ffi::EM_UTF8; ffi::EM_HTML5_SHORT_STRING_LEN_BYTES]) -> u8 { - let slice = &input[0..input.iter().take_while(|x| **x != 0).count()]; - let maybe_key = unsafe { str::from_utf8(mem::transmute::<_, &[u8]>(slice)) }; - let key = match maybe_key { - Ok(key) => key, - Err(_) => { - return 0; - } - }; - if key.chars().count() == 1 { - key.as_bytes()[0] - } else { - 0 - } -} - -fn key_translate_virt( - input: [ffi::EM_UTF8; ffi::EM_HTML5_SHORT_STRING_LEN_BYTES], - location: c_ulong, -) -> Option<::VirtualKeyCode> { - let slice = &input[0..input.iter().take_while(|x| **x != 0).count()]; - let maybe_key = unsafe { str::from_utf8(mem::transmute::<_, &[u8]>(slice)) }; - let key = match maybe_key { - Ok(key) => key, - Err(_) => { - return None; - } - }; - use VirtualKeyCode::*; - match key { - "Alt" => match location { - ffi::DOM_KEY_LOCATION_LEFT => Some(LAlt), - ffi::DOM_KEY_LOCATION_RIGHT => Some(RAlt), - _ => None, - }, - "AltGraph" => None, - "CapsLock" => None, - "Control" => match location { - ffi::DOM_KEY_LOCATION_LEFT => Some(LControl), - ffi::DOM_KEY_LOCATION_RIGHT => Some(RControl), - _ => None, - }, - "Fn" => None, - "FnLock" => None, - "Hyper" => None, - "Meta" => None, - "NumLock" => Some(Numlock), - "ScrollLock" => Some(Scroll), - "Shift" => match location { - ffi::DOM_KEY_LOCATION_LEFT => Some(LShift), - ffi::DOM_KEY_LOCATION_RIGHT => Some(RShift), - _ => None, - }, - "Super" => None, - "Symbol" => None, - "SymbolLock" => None, - - "Enter" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(NumpadEnter), - _ => Some(Return), - }, - "Tab" => Some(Tab), - " " => Some(Space), - - "ArrowDown" => Some(Down), - "ArrowLeft" => Some(Left), - "ArrowRight" => Some(Right), - "ArrowUp" => Some(Up), - "End" => None, - "Home" => None, - "PageDown" => None, - "PageUp" => None, - - "Backspace" => Some(Back), - "Clear" => None, - "Copy" => None, - "CrSel" => None, - "Cut" => None, - "Delete" => None, - "EraseEof" => None, - "ExSel" => None, - "Insert" => Some(Insert), - "Paste" => None, - "Redo" => None, - "Undo" => None, - - "Accept" => None, - "Again" => None, - "Attn" => None, - "Cancel" => None, - "ContextMenu" => None, - "Escape" => Some(Escape), - "Execute" => None, - "Find" => None, - "Finish" => None, - "Help" => None, - "Pause" => Some(Pause), - "Play" => None, - "Props" => None, - "Select" => None, - "ZoomIn" => None, - "ZoomOut" => None, - - "BrightnessDown" => None, - "BrightnessUp" => None, - "Eject" => None, - "LogOff" => None, - "Power" => Some(Power), - "PowerOff" => None, - "PrintScreen" => Some(Snapshot), - "Hibernate" => None, - "Standby" => Some(Sleep), - "WakeUp" => Some(Wake), - - "AllCandidates" => None, - "Alphanumeric" => None, - "CodeInput" => None, - "Compose" => Some(Compose), - "Convert" => Some(Convert), - "Dead" => None, - "FinalMode" => None, - "GroupFirst" => None, - "GroupLast" => None, - "GroupNext" => None, - "GroupPrevious" => None, - "ModeChange" => None, - "NextCandidate" => None, - "NonConvert" => None, - "PreviousCandidate" => None, - "Process" => None, - "SingleCandidate" => None, - - "HangulMode" => None, - "HanjaMode" => None, - "JunjaMode" => None, - - "Eisu" => None, - "Hankaku" => None, - "Hiragana" => None, - "HiraganaKatakana" => None, - "KanaMode" => Some(Kana), - "KanjiMode" => Some(Kanji), - "Romaji" => None, - "Zenkaku" => None, - "ZenkakuHanaku" => None, - - "F1" => Some(F1), - "F2" => Some(F2), - "F3" => Some(F3), - "F4" => Some(F4), - "F5" => Some(F5), - "F6" => Some(F6), - "F7" => Some(F7), - "F8" => Some(F8), - "F9" => Some(F9), - "F10" => Some(F10), - "F11" => Some(F11), - "F12" => Some(F12), - "F13" => Some(F13), - "F14" => Some(F14), - "F15" => Some(F15), - "F16" => Some(F16), - "F17" => Some(F17), - "F18" => Some(F18), - "F19" => Some(F19), - "F20" => Some(F20), - "F21" => Some(F21), - "F22" => Some(F22), - "F23" => Some(F23), - "F24" => Some(F24), - "Soft1" => None, - "Soft2" => None, - "Soft3" => None, - "Soft4" => None, - - "AppSwitch" => None, - "Call" => None, - "Camera" => None, - "CameraFocus" => None, - "EndCall" => None, - "GoBack" => None, - "GoHome" => None, - "HeadsetHook" => None, - "LastNumberRedial" => None, - "Notification" => None, - "MannerMode" => None, - "VoiceDial" => None, - - "ChannelDown" => None, - "ChannelUp" => None, - "MediaFastForward" => None, - "MediaPause" => None, - "MediaPlay" => None, - "MediaPlayPause" => Some(PlayPause), - "MediaRecord" => None, - "MediaRewind" => None, - "MediaStop" => Some(MediaStop), - "MediaTrackNext" => Some(NextTrack), - "MediaTrackPrevious" => Some(PrevTrack), - - "AudioBalanceLeft" => None, - "AudioBalanceRight" => None, - "AudioBassDown" => None, - "AudioBassBoostDown" => None, - "AudioBassBoostToggle" => None, - "AudioBassBoostUp" => None, - "AudioBassUp" => None, - "AudioFaderFront" => None, - "AudioFaderRear" => None, - "AudioSurroundModeNext" => None, - "AudioTrebleDown" => None, - "AudioTrebleUp" => None, - "AudioVolumeDown" => Some(VolumeDown), - "AudioVolumeMute" => Some(Mute), - "AudioVolumeUp" => Some(VolumeUp), - "MicrophoneToggle" => None, - "MicrophoneVolumeDown" => None, - "MicrophoneVolumeMute" => None, - "MicrophoneVolumeUp" => None, - - "TV" => None, - "TV3DMode" => None, - "TVAntennaCable" => None, - "TVAudioDescription" => None, - "TVAudioDescriptionMixDown" => None, - "TVAudioDescriptionMixUp" => None, - "TVContentsMenu" => None, - "TVDataService" => None, - "TVInput" => None, - "TVInputComponent1" => None, - "TVInputComponent2" => None, - "TVInputComposite1" => None, - "TVInputComposite2" => None, - "TVInputHDM1" => None, - "TVInputHDM2" => None, - "TVInputHDM3" => None, - "TVInputHDM4" => None, - "TVInputVGA1" => None, - "TVMediaContext" => None, - "TVNetwork" => None, - "TVNumberEntry" => None, - "TVPower" => None, - "TVRadioService" => None, - "TVSatellite" => None, - "TVSatelliteBS" => None, - "TVSatelliteCS" => None, - "TVSatelliteToggle" => None, - "TVTerrestrialAnalog" => None, - "TVTerrestrialDigital" => None, - "TVTimer" => None, - - "AVRInput" => None, - "AVRPower" => None, - "ColorF0Red" => None, - "ColorF1Green" => None, - "ColorF2Yellow" => None, - "ColorF3Blue" => None, - "ColorF4Grey" => None, - "ColorF5Brown" => None, - "ClosedCaptionToggle" => None, - "Dimmer" => None, - "DisplaySwap" => None, - "DVR" => None, - "Exit" => None, - "FavoriteClear0" => None, - "FavoriteClear1" => None, - "FavoriteClear2" => None, - "FavoriteClear3" => None, - "FavoriteRecall0" => None, - "FavoriteRecall1" => None, - "FavoriteRecall2" => None, - "FavoriteRecall3" => None, - "FavoriteStore0" => None, - "FavoriteStore1" => None, - "FavoriteStore2" => None, - "FavoriteStore3" => None, - "FavoriteStore4" => None, - "Guide" => None, - "GuideNextDay" => None, - "GuidePreviousDay" => None, - "Info" => None, - "InstantReplay" => None, - "Link" => None, - "ListProgram" => None, - "LiveContent" => None, - "Lock" => None, - "MediaApps" => None, - "MediaAudioTrack" => None, - "MediaLast" => None, - "MediaSkipBackward" => None, - "MediaSkipForward" => None, - "MediaStepBackward" => None, - "MediaStepForward" => None, - "MediaTopMenu" => None, - "NavigateIn" => None, - "NavigateNext" => None, - "NavigateOut" => None, - "NavigatePrevious" => None, - "NextFavoriteChannel" => None, - "NextUserProfile" => None, - "OnDemand" => None, - "Pairing" => None, - "PinPDown" => None, - "PinPMove" => None, - "PinPToggle" => None, - "PinPUp" => None, - "PlaySpeedDown" => None, - "PlaySpeedReset" => None, - "PlaySpeedUp" => None, - "RandomToggle" => None, - "RcLowBattery" => None, - "RecordSpeedNext" => None, - "RfBypass" => None, - "ScanChannelsToggle" => None, - "ScreenModeNext" => None, - "Settings" => None, - "SplitScreenToggle" => None, - "STBInput" => None, - "STBPower" => None, - "Subtitle" => None, - "Teletext" => None, - "VideoModeNext" => None, - "Wink" => None, - "ZoomToggle" => None, - - "SpeechCorrectionList" => None, - "SpeechInputToggle" => None, - - "Close" => None, - "New" => None, - "Open" => None, - "Print" => None, - "Save" => None, - "SpellCheck" => None, - "MailForward" => None, - "MailReply" => None, - "MailSend" => None, - - "LaunchCalculator" => Some(Calculator), - "LaunchCalendar" => None, - "LaunchContacts" => None, - "LaunchMail" => Some(Mail), - "LaunchMediaPlayer" => None, - "LaunchMusicPlayer" => None, - "LaunchMyComputer" => Some(MyComputer), - "LaunchPhone" => None, - "LaunchScreenSaver" => None, - "LaunchSpreadsheet" => None, - "LaunchWebCam" => None, - "LaunchWordProcessor" => None, - "LaunchApplication1" => None, - "LaunchApplication2" => None, - "LaunchApplication3" => None, - "LaunchApplication4" => None, - "LaunchApplication5" => None, - "LaunchApplication6" => None, - "LaunchApplication7" => None, - "LaunchApplication8" => None, - "LaunchApplication9" => None, - "LaunchApplication10" => None, - "LaunchApplication11" => None, - "LaunchApplication12" => None, - "LaunchApplication13" => None, - "LaunchApplication14" => None, - "LaunchApplication15" => None, - "LaunchApplication16" => None, - - "BrowserBack" => Some(WebBack), - "BrowserFavorites" => Some(WebFavorites), - "BrowserForward" => Some(WebForward), - "BrowserHome" => Some(WebHome), - "BrowserRefresh" => Some(WebRefresh), - "BrowserSearch" => Some(WebSearch), - "BrowserStop" => Some(WebStop), - - "Decimal" => Some(Decimal), - "Key11" => None, - "Key12" => None, - "Multiply" | "*" => Some(Multiply), - "Add" | "+" => Some(Add), - // "Clear" => None, - "Divide" => Some(Divide), - "Subtract" | "-" => Some(Subtract), - "Separator" => None, - "0" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad0), - _ => Some(Key0), - }, - "1" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad1), - _ => Some(Key1), - }, - "2" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad2), - _ => Some(Key2), - }, - "3" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad3), - _ => Some(Key3), - }, - "4" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad4), - _ => Some(Key4), - }, - "5" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad5), - _ => Some(Key5), - }, - "6" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad6), - _ => Some(Key6), - }, - "7" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad7), - _ => Some(Key7), - }, - "8" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad8), - _ => Some(Key8), - }, - "9" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(Numpad9), - _ => Some(Key9), - }, - - "A" | "a" => Some(A), - "B" | "b" => Some(B), - "C" | "c" => Some(C), - "D" | "d" => Some(D), - "E" | "e" => Some(E), - "F" | "f" => Some(F), - "G" | "g" => Some(G), - "H" | "h" => Some(H), - "I" | "i" => Some(I), - "J" | "j" => Some(J), - "K" | "k" => Some(K), - "L" | "l" => Some(L), - "M" | "m" => Some(M), - "N" | "n" => Some(N), - "O" | "o" => Some(O), - "P" | "p" => Some(P), - "Q" | "q" => Some(Q), - "R" | "r" => Some(R), - "S" | "s" => Some(S), - "T" | "t" => Some(T), - "U" | "u" => Some(U), - "V" | "v" => Some(V), - "W" | "w" => Some(W), - "X" | "x" => Some(X), - "Y" | "y" => Some(Y), - "Z" | "z" => Some(Z), - - "'" => Some(Apostrophe), - "\\" => Some(Backslash), - ":" => Some(Colon), - "," => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(NumpadComma), - _ => Some(Comma), - }, - "=" => match location { - ffi::DOM_KEY_LOCATION_NUMPAD => Some(NumpadEquals), - _ => Some(Equals), - }, - "{" => Some(LBracket), - "." => Some(Period), - "}" => Some(RBracket), - ";" => Some(Semicolon), - "/" => Some(Slash), - - _ => None, - } -} diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 5b982b14..5b5b726b 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -1,50 +1,96 @@ +#![deny(unused_results)] + use std::{ cell::{RefCell, RefMut}, - mem::{self, ManuallyDrop}, + collections::HashSet, + mem, os::raw::c_void, ptr, time::Instant, }; -use crate::{ - event::{Event, StartCause}, - event_loop::ControlFlow, -}; +use objc::runtime::{BOOL, YES}; -use crate::platform_impl::platform::{ - event_loop::{EventHandler, Never}, - ffi::{ - id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, - CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, - CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSUInteger, +use crate::{ + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, + platform_impl::platform::{ + event_loop::{EventHandler, Never}, + ffi::{ + id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, + CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSInteger, NSOperatingSystemVersion, + NSUInteger, + }, }, + window::WindowId as RootWindowId, }; macro_rules! bug { - ($msg:expr) => { - panic!("winit iOS bug, file an issue: {}", $msg) + ($($msg:tt)*) => { + panic!("winit iOS bug, file an issue: {}", format!($($msg)*)) }; } +macro_rules! bug_assert { + ($test:expr, $($msg:tt)*) => { + assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +enum UserCallbackTransitionResult<'a> { + Success { + event_handler: Box, + active_control_flow: ControlFlow, + processing_redraws: bool, + }, + ReentrancyPrevented { + queued_events: &'a mut Vec>, + }, +} + +impl Event { + fn is_redraw(&self) -> bool { + if let Event::WindowEvent { + window_id: _, + event: WindowEvent::RedrawRequested, + } = self + { + true + } else { + false + } + } +} + // this is the state machine for the app lifecycle #[derive(Debug)] +#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] enum AppStateImpl { NotLaunched { queued_windows: Vec, queued_events: Vec>, + queued_gpu_redraws: HashSet, }, Launching { queued_windows: Vec, queued_events: Vec>, queued_event_handler: Box, + queued_gpu_redraws: HashSet, }, ProcessingEvents { event_handler: Box, + queued_gpu_redraws: HashSet, active_control_flow: ControlFlow, }, // special state to deal with reentrancy and prevent mutable aliasing. InUserCallback { queued_events: Vec>, + queued_gpu_redraws: HashSet, + }, + ProcessingRedraws { + event_handler: Box, + active_control_flow: ControlFlow, }, Waiting { waiting_event_handler: Box, @@ -56,9 +102,16 @@ enum AppStateImpl { Terminated, } -impl Drop for AppStateImpl { +struct AppState { + // This should never be `None`, except for briefly during a state transition. + app_state: Option, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl Drop for AppState { fn drop(&mut self) { - match self { + match self.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. @@ -66,22 +119,18 @@ impl Drop for AppStateImpl { | &mut AppStateImpl::Launching { ref mut queued_windows, .. - } => unsafe { + } => { for &mut window in queued_windows { - let () = msg_send![window, release]; + unsafe { + let () = msg_send![window, release]; + } } - }, + } _ => {} } } } -pub struct AppState { - app_state: AppStateImpl, - control_flow: ControlFlow, - waker: EventLoopWaker, -} - impl AppState { // requires main thread unsafe fn get_mut() -> RefMut<'static, AppState> { @@ -95,7 +144,6 @@ impl AppState { "bug in winit: `AppState::get_mut()` can only be called on the main thread" ); } - let mut guard = APP_STATE.borrow_mut(); if guard.is_none() { #[inline(never)] @@ -103,10 +151,11 @@ impl AppState { unsafe fn init_guard(guard: &mut RefMut<'static, Option>) { let waker = EventLoopWaker::new(CFRunLoopGetMain()); **guard = Some(AppState { - app_state: AppStateImpl::NotLaunched { + app_state: Some(AppStateImpl::NotLaunched { queued_windows: Vec::new(), queued_events: Vec::new(), - }, + queued_gpu_redraws: HashSet::new(), + }), control_flow: ControlFlow::default(), waker, }); @@ -116,233 +165,153 @@ impl AppState { RefMut::map(guard, |state| state.as_mut().unwrap()) } - // requires main thread and window is a UIWindow - // retains window - pub unsafe fn set_key_window(window: id) { - let mut this = AppState::get_mut(); - match &mut this.app_state { - &mut AppStateImpl::NotLaunched { - ref mut queued_windows, - .. - } => { - queued_windows.push(window); - msg_send![window, retain]; - return; - } - &mut AppStateImpl::ProcessingEvents { .. } => {} - &mut AppStateImpl::InUserCallback { .. } => {} - &mut AppStateImpl::Terminated => panic!( - "Attempt to create a `Window` \ - after the app has terminated" - ), - app_state => unreachable!("unexpected state: {:#?}", app_state), /* all other cases should be impossible */ + fn state(&self) -> &AppStateImpl { + match &self.app_state { + Some(ref state) => state, + None => bug!("`AppState` previously failed a state transition"), } - drop(this); - msg_send![window, makeKeyAndVisible] } - // requires main thread - pub unsafe fn will_launch(queued_event_handler: Box) { - let mut this = AppState::get_mut(); - let (queued_windows, queued_events) = match &mut this.app_state { - &mut AppStateImpl::NotLaunched { - ref mut queued_windows, - ref mut queued_events, - } => { - let windows = ptr::read(queued_windows); - let events = ptr::read(queued_events); - (windows, events) - } - _ => panic!( - "winit iOS expected the app to be in a `NotLaunched` \ - state, but was not - please file an issue" - ), + fn state_mut(&mut self) -> &mut AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn take_state(&mut self) -> AppStateImpl { + match self.app_state.take() { + Some(state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn set_state(&mut self, new_state: AppStateImpl) { + bug_assert!( + self.app_state.is_none(), + "attempted to set an `AppState` without calling `take_state` first {:?}", + self.app_state + ); + self.app_state = Some(new_state) + } + + fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => mem::replace(state, new_state), + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn has_launched(&self) -> bool { + match self.state() { + &AppStateImpl::NotLaunched { .. } | &AppStateImpl::Launching { .. } => false, + _ => true, + } + } + + fn will_launch_transition(&mut self, queued_event_handler: Box) { + let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::NotLaunched { + queued_windows, + queued_events, + queued_gpu_redraws, + } => (queued_windows, queued_events, queued_gpu_redraws), + s => bug!("unexpected state {:?}", s), }; - ptr::write( - &mut this.app_state, + self.set_state(AppStateImpl::Launching { + queued_windows, + queued_events, + queued_event_handler, + queued_gpu_redraws, + }); + } + + 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, queued_events, queued_event_handler, - }, - ); + queued_gpu_redraws, + } => ( + queued_windows, + queued_events, + queued_event_handler, + queued_gpu_redraws, + ), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::Poll, + queued_gpu_redraws, + }); + (windows, events) } - // requires main thread - pub unsafe fn did_finish_launching() { - let mut this = AppState::get_mut(); - let windows = match &mut this.app_state { - &mut AppStateImpl::Launching { - ref mut queued_windows, - .. - } => mem::replace(queued_windows, Vec::new()), - _ => panic!( - "winit iOS expected the app to be in a `Launching` \ - state, but was not - please file an issue" - ), - }; - // have to drop RefMut because the window setup code below can trigger new events - drop(this); - - for window in windows { - let count: NSUInteger = msg_send![window, retainCount]; - // make sure the window is still referenced - if count > 1 { - // Do a little screen dance here to account for windows being created before - // `UIApplicationMain` is called. This fixes visual issues such as being - // offcenter and sized incorrectly. Additionally, to fix orientation issues, we - // gotta reset the `rootViewController`. - // - // relevant iOS log: - // ``` - // [ApplicationLifecycle] Windows were created before application initialzation - // completed. This may result in incorrect visual appearance. - // ``` - let screen: id = msg_send![window, screen]; - let () = msg_send![screen, retain]; - let () = msg_send![window, setScreen:0 as id]; - let () = msg_send![window, setScreen: screen]; - let () = msg_send![screen, release]; - let controller: id = msg_send![window, rootViewController]; - let () = msg_send![window, setRootViewController:ptr::null::<()>()]; - let () = msg_send![window, setRootViewController: controller]; - let () = msg_send![window, makeKeyAndVisible]; - } - let () = msg_send![window, release]; + fn wakeup_transition(&mut self) -> Option> { + // before `AppState::did_finish_launching` is called, pretend there is no running + // event loop. + if !self.has_launched() { + return None; } - let mut this = AppState::get_mut(); - let (windows, events, event_handler) = match &mut this.app_state { - &mut AppStateImpl::Launching { - ref mut queued_windows, - ref mut queued_events, - ref mut queued_event_handler, - } => { - let windows = ptr::read(queued_windows); - let events = ptr::read(queued_events); - let event_handler = ptr::read(queued_event_handler); - (windows, events, event_handler) - } - _ => panic!( - "winit iOS expected the app to be in a `Launching` \ - state, but was not - please file an issue" + let (event_handler, event) = match (self.control_flow, self.take_state()) { + ( + ControlFlow::Poll, + AppStateImpl::PollFinished { + waiting_event_handler, + }, + ) => (waiting_event_handler, Event::NewEvents(StartCause::Poll)), + ( + ControlFlow::Wait, + AppStateImpl::Waiting { + waiting_event_handler, + start, + }, + ) => ( + waiting_event_handler, + Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + }), ), - }; - ptr::write( - &mut this.app_state, - AppStateImpl::ProcessingEvents { - event_handler, - active_control_flow: ControlFlow::Poll, - }, - ); - drop(this); - - let events = std::iter::once(Event::NewEvents(StartCause::Init)).chain(events); - AppState::handle_nonuser_events(events); - - // the above window dance hack, could possibly trigger new windows to be created. - // we can just set those windows up normally, as they were created after didFinishLaunching - for window in windows { - let count: NSUInteger = msg_send![window, retainCount]; - // make sure the window is still referenced - if count > 1 { - let () = msg_send![window, makeKeyAndVisible]; - } - let () = msg_send![window, release]; - } - } - - // requires main thread - // AppState::did_finish_launching handles the special transition `Init` - pub unsafe fn handle_wakeup_transition() { - let mut this = AppState::get_mut(); - let event = - match this.control_flow { - ControlFlow::Poll => { - let event_handler = match &mut this.app_state { - &mut AppStateImpl::NotLaunched { .. } - | &mut AppStateImpl::Launching { .. } => return, - &mut AppStateImpl::PollFinished { - ref mut waiting_event_handler, - } => ptr::read(waiting_event_handler), - _ => bug!("`EventHandler` unexpectedly started polling"), - }; - ptr::write( - &mut this.app_state, - AppStateImpl::ProcessingEvents { - event_handler, - active_control_flow: ControlFlow::Poll, - }, - ); - Event::NewEvents(StartCause::Poll) - } - ControlFlow::Wait => { - let (event_handler, start) = match &mut this.app_state { - &mut AppStateImpl::NotLaunched { .. } - | &mut AppStateImpl::Launching { .. } => return, - &mut AppStateImpl::Waiting { - ref mut waiting_event_handler, - ref mut start, - } => (ptr::read(waiting_event_handler), *start), - _ => bug!("`EventHandler` unexpectedly woke up"), - }; - ptr::write( - &mut this.app_state, - AppStateImpl::ProcessingEvents { - event_handler, - active_control_flow: ControlFlow::Wait, - }, - ); + ( + ControlFlow::WaitUntil(requested_resume), + AppStateImpl::Waiting { + waiting_event_handler, + start, + }, + ) => { + let event = if Instant::now() >= requested_resume { + Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + }) + } else { Event::NewEvents(StartCause::WaitCancelled { start, - requested_resume: None, + requested_resume: Some(requested_resume), }) - } - ControlFlow::WaitUntil(requested_resume) => { - let (event_handler, start) = match &mut this.app_state { - &mut AppStateImpl::NotLaunched { .. } - | &mut AppStateImpl::Launching { .. } => return, - &mut AppStateImpl::Waiting { - ref mut waiting_event_handler, - ref mut start, - } => (ptr::read(waiting_event_handler), *start), - _ => bug!("`EventHandler` unexpectedly woke up"), - }; - ptr::write( - &mut this.app_state, - AppStateImpl::ProcessingEvents { - event_handler, - active_control_flow: ControlFlow::WaitUntil(requested_resume), - }, - ); - if Instant::now() >= requested_resume { - Event::NewEvents(StartCause::ResumeTimeReached { - start, - requested_resume, - }) - } else { - Event::NewEvents(StartCause::WaitCancelled { - start, - requested_resume: Some(requested_resume), - }) - } - } - ControlFlow::Exit => bug!("unexpected controlflow `Exit`"), - }; - drop(this); - AppState::handle_nonuser_event(event) + }; + (waiting_event_handler, event) + } + (ControlFlow::Exit, _) => bug!("unexpected `ControlFlow` `Exit`"), + s => bug!("`EventHandler` unexpectedly woke up {:?}", s), + }; + + self.set_state(AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws: Default::default(), + active_control_flow: self.control_flow, + }); + Some(event) } - // requires main thread - pub unsafe fn handle_nonuser_event(event: Event) { - AppState::handle_nonuser_events(std::iter::once(event)) - } - - // requires main thread - pub unsafe fn handle_nonuser_events>>(events: I) { - let mut this = AppState::get_mut(); - let mut control_flow = this.control_flow; - let (mut event_handler, active_control_flow) = match &mut this.app_state { + fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> { + // If we're not able to process an event due to recursion or `Init` not having been sent out + // yet, then queue the events up. + match self.state_mut() { &mut AppStateImpl::Launching { ref mut queued_events, .. @@ -355,211 +324,488 @@ impl AppState { ref mut queued_events, .. } => { - queued_events.extend(events); - return; + // A lifetime cast: early returns are not currently handled well with NLL, but + // polonius handles them well. This transmute is a safe workaround. + return unsafe { + mem::transmute::< + UserCallbackTransitionResult<'_>, + UserCallbackTransitionResult<'_>, + >(UserCallbackTransitionResult::ReentrancyPrevented { + queued_events, + }) + }; } - &mut AppStateImpl::ProcessingEvents { - ref mut event_handler, - ref mut active_control_flow, - } => (ptr::read(event_handler), *active_control_flow), - &mut AppStateImpl::PollFinished { .. } - | &mut AppStateImpl::Waiting { .. } - | &mut AppStateImpl::Terminated => bug!("unexpected attempted to process an event"), - }; - ptr::write( - &mut this.app_state, - AppStateImpl::InUserCallback { - queued_events: Vec::new(), - }, - ); - drop(this); - for event in events { - event_handler.handle_nonuser_event(event, &mut control_flow) + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {} + + s @ &mut AppStateImpl::PollFinished { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::Terminated => { + bug!("unexpected attempted to process an event {:?}", s) + } } - loop { - let mut this = AppState::get_mut(); - let queued_events = match &mut this.app_state { - &mut AppStateImpl::InUserCallback { - ref mut queued_events, - } => mem::replace(queued_events, Vec::new()), - _ => bug!("unexpected `AppStateImpl`"), - }; - if queued_events.is_empty() { - this.app_state = AppStateImpl::ProcessingEvents { + + let (event_handler, queued_gpu_redraws, active_control_flow, processing_redraws) = + match self.take_state() { + AppStateImpl::Launching { .. } + | AppStateImpl::NotLaunched { .. } + | AppStateImpl::InUserCallback { .. } => unreachable!(), + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } => ( + event_handler, + queued_gpu_redraws, + active_control_flow, + false, + ), + AppStateImpl::ProcessingRedraws { event_handler, active_control_flow, - }; - this.control_flow = control_flow; - break; - } - drop(this); - for event in queued_events { - event_handler.handle_nonuser_event(event, &mut control_flow) - } + } => (event_handler, Default::default(), active_control_flow, true), + AppStateImpl::PollFinished { .. } + | AppStateImpl::Waiting { .. } + | AppStateImpl::Terminated => unreachable!(), + }; + self.set_state(AppStateImpl::InUserCallback { + queued_events: Vec::new(), + queued_gpu_redraws, + }); + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, } } - // requires main thread - pub unsafe fn handle_user_events() { - let mut this = AppState::get_mut(); - let mut control_flow = this.control_flow; - let (mut event_handler, active_control_flow) = match &mut this.app_state { - &mut AppStateImpl::NotLaunched { .. } | &mut AppStateImpl::Launching { .. } => return, - &mut AppStateImpl::ProcessingEvents { - ref mut event_handler, - ref mut active_control_flow, - } => (ptr::read(event_handler), *active_control_flow), - &mut AppStateImpl::InUserCallback { .. } - | &mut AppStateImpl::PollFinished { .. } - | &mut AppStateImpl::Waiting { .. } - | &mut AppStateImpl::Terminated => bug!("unexpected attempted to process an event"), + fn main_events_cleared_transition(&mut self) -> HashSet { + let (event_handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } => (event_handler, queued_gpu_redraws, active_control_flow), + s => bug!("unexpected state {:?}", s), }; - ptr::write( - &mut this.app_state, - AppStateImpl::InUserCallback { - queued_events: Vec::new(), - }, - ); - drop(this); - - event_handler.handle_user_events(&mut control_flow); - loop { - let mut this = AppState::get_mut(); - let queued_events = match &mut this.app_state { - &mut AppStateImpl::InUserCallback { - ref mut queued_events, - } => mem::replace(queued_events, Vec::new()), - _ => bug!("unexpected `AppStateImpl`"), - }; - if queued_events.is_empty() { - this.app_state = AppStateImpl::ProcessingEvents { - event_handler, - active_control_flow, - }; - this.control_flow = control_flow; - break; - } - drop(this); - for event in queued_events { - event_handler.handle_nonuser_event(event, &mut control_flow) - } - event_handler.handle_user_events(&mut control_flow); - } + self.set_state(AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + }); + queued_gpu_redraws } - // requires main thread - pub unsafe fn handle_events_cleared() { - let mut this = AppState::get_mut(); - match &mut this.app_state { - &mut AppStateImpl::NotLaunched { .. } | &mut AppStateImpl::Launching { .. } => return, - &mut AppStateImpl::ProcessingEvents { .. } => {} - _ => unreachable!(), - }; - drop(this); - - AppState::handle_user_events(); - AppState::handle_nonuser_event(Event::EventsCleared); - - let mut this = AppState::get_mut(); - let (event_handler, old) = match &mut this.app_state { - &mut AppStateImpl::ProcessingEvents { - ref mut event_handler, - ref mut active_control_flow, - } => ( - ManuallyDrop::new(ptr::read(event_handler)), - *active_control_flow, - ), - _ => unreachable!(), + fn events_cleared_transition(&mut self) { + if !self.has_launched() { + return; + } + let (waiting_event_handler, old) = match self.take_state() { + AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + } => (event_handler, active_control_flow), + s => bug!("unexpected state {:?}", s), }; - let new = this.control_flow; + let new = self.control_flow; match (old, new) { - (ControlFlow::Poll, ControlFlow::Poll) => ptr::write( - &mut this.app_state, - AppStateImpl::PollFinished { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - }, - ), + (ControlFlow::Poll, ControlFlow::Poll) => self.set_state(AppStateImpl::PollFinished { + waiting_event_handler, + }), (ControlFlow::Wait, ControlFlow::Wait) => { let start = Instant::now(); - ptr::write( - &mut this.app_state, - AppStateImpl::Waiting { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - start, - }, - ) + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); } (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) if old_instant == new_instant => { let start = Instant::now(); - ptr::write( - &mut this.app_state, - AppStateImpl::Waiting { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - start, - }, - ) + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); } (_, ControlFlow::Wait) => { let start = Instant::now(); - ptr::write( - &mut this.app_state, - AppStateImpl::Waiting { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - start, - }, - ); - this.waker.stop() + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + self.waker.stop() } (_, ControlFlow::WaitUntil(new_instant)) => { let start = Instant::now(); - ptr::write( - &mut this.app_state, - AppStateImpl::Waiting { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - start, - }, - ); - this.waker.start_at(new_instant) + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + self.waker.start_at(new_instant) } (_, ControlFlow::Poll) => { - ptr::write( - &mut this.app_state, - AppStateImpl::PollFinished { - waiting_event_handler: ManuallyDrop::into_inner(event_handler), - }, - ); - this.waker.start() + self.set_state(AppStateImpl::PollFinished { + waiting_event_handler, + }); + self.waker.start() } (_, ControlFlow::Exit) => { // https://developer.apple.com/library/archive/qa/qa1561/_index.html // it is not possible to quit an iOS app gracefully and programatically warn!("`ControlFlow::Exit` ignored on iOS"); - this.control_flow = old + self.control_flow = old } } } - pub fn terminated() { - let mut this = unsafe { AppState::get_mut() }; - let mut old = mem::replace(&mut this.app_state, AppStateImpl::Terminated); - let mut control_flow = this.control_flow; - if let AppStateImpl::ProcessingEvents { - ref mut event_handler, - .. - } = old - { - drop(this); - event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) - } else { - bug!("`LoopDestroyed` happened while not processing events") + fn terminated_transition(&mut self) -> Box { + match self.replace_state(AppStateImpl::Terminated) { + AppStateImpl::ProcessingRedraws { event_handler, .. } => event_handler, + s => bug!( + "`LoopDestroyed` happened while not processing events {:?}", + s + ), } } } +// requires main thread and window is a UIWindow +// retains window +pub unsafe fn set_key_window(window: id) { + bug_assert!( + { + let is_window: BOOL = msg_send![window, isKindOfClass: class!(UIWindow)]; + is_window == YES + }, + "set_key_window called with an incorrect type" + ); + let mut this = AppState::get_mut(); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_windows, + .. + } => return queued_windows.push(msg_send![window, retain]), + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::InUserCallback { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {} + s @ &mut AppStateImpl::Launching { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } + drop(this); + msg_send![window, makeKeyAndVisible] +} + +// requires main thread and window is a UIWindow +// retains window +pub unsafe fn queue_gl_or_metal_redraw(window: id) { + bug_assert!( + { + let is_window: BOOL = msg_send![window, isKindOfClass: class!(UIWindow)]; + is_window == YES + }, + "set_key_window called with an incorrect type" + ); + let mut this = AppState::get_mut(); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::Launching { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::ProcessingEvents { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_gpu_redraws, + .. + } => drop(queued_gpu_redraws.insert(window)), + s @ &mut AppStateImpl::ProcessingRedraws { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } + drop(this); +} + +// requires main thread +pub unsafe fn will_launch(queued_event_handler: Box) { + AppState::get_mut().will_launch_transition(queued_event_handler) +} + +// requires main thread +pub unsafe fn did_finish_launching() { + let mut this = AppState::get_mut(); + let windows = match this.state_mut() { + AppStateImpl::Launching { queued_windows, .. } => mem::replace(queued_windows, Vec::new()), + s => bug!("unexpected state {:?}", s), + }; + + // start waking up the event loop now! + bug_assert!( + this.control_flow == ControlFlow::Poll, + "unexpectedly not setup to `Poll` on launch!" + ); + this.waker.start(); + + // have to drop RefMut because the window setup code below can trigger new events + drop(this); + + for window in windows { + let count: NSUInteger = msg_send![window, retainCount]; + // make sure the window is still referenced + if count > 1 { + // Do a little screen dance here to account for windows being created before + // `UIApplicationMain` is called. This fixes visual issues such as being + // offcenter and sized incorrectly. Additionally, to fix orientation issues, we + // gotta reset the `rootViewController`. + // + // relevant iOS log: + // ``` + // [ApplicationLifecycle] Windows were created before application initialzation + // completed. This may result in incorrect visual appearance. + // ``` + let screen: id = msg_send![window, screen]; + let _: id = msg_send![screen, retain]; + let () = msg_send![window, setScreen:0 as id]; + let () = msg_send![window, setScreen: screen]; + let () = msg_send![screen, release]; + let controller: id = msg_send![window, rootViewController]; + let () = msg_send![window, setRootViewController:ptr::null::<()>()]; + let () = msg_send![window, setRootViewController: controller]; + let () = msg_send![window, makeKeyAndVisible]; + } + let () = msg_send![window, release]; + } + + let (windows, events) = AppState::get_mut().did_finish_launching_transition(); + + let events = std::iter::once(Event::NewEvents(StartCause::Init)).chain(events); + handle_nonuser_events(events); + + // the above window dance hack, could possibly trigger new windows to be created. + // we can just set those windows up normally, as they were created after didFinishLaunching + for window in windows { + let count: NSUInteger = msg_send![window, retainCount]; + // make sure the window is still referenced + if count > 1 { + let () = msg_send![window, makeKeyAndVisible]; + } + let () = msg_send![window, release]; + } +} + +// requires main thread +// AppState::did_finish_launching handles the special transition `Init` +pub unsafe fn handle_wakeup_transition() { + let mut this = AppState::get_mut(); + let wakeup_event = match this.wakeup_transition() { + None => return, + Some(wakeup_event) => wakeup_event, + }; + drop(this); + + handle_nonuser_event(wakeup_event) +} + +// requires main thread +pub unsafe fn handle_nonuser_event(event: Event) { + handle_nonuser_events(std::iter::once(event)) +} + +// requires main thread +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() { + UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { + queued_events.extend(events); + return; + } + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, + } => (event_handler, active_control_flow, processing_redraws), + }; + 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 + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + + loop { + let mut this = AppState::get_mut(); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::replace(queued_events, Vec::new()), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(if processing_redraws { + bug_assert!( + queued_gpu_redraws.is_empty(), + "redraw queued while processing redraws" + ); + AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + } + } else { + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } + }); + this.control_flow = control_flow; + break; + } + 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 + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + } +} + +// requires main thread +unsafe fn handle_user_events() { + let mut this = AppState::get_mut(); + let mut control_flow = this.control_flow; + let (mut event_handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { .. } => { + bug!("unexpected attempted to process an event") + } + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, + } => (event_handler, active_control_flow, processing_redraws), + }; + if processing_redraws { + bug!("user events attempted to be sent out while `ProcessingRedraws`"); + } + drop(this); + + event_handler.handle_user_events(&mut control_flow); + + loop { + let mut this = AppState::get_mut(); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::replace(queued_events, Vec::new()), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + }); + this.control_flow = control_flow; + break; + } + drop(this); + + for event in queued_events { + event_handler.handle_nonuser_event(event, &mut control_flow) + } + event_handler.handle_user_events(&mut control_flow); + } +} + +// requires main thread +pub unsafe fn handle_main_events_cleared() { + let mut this = AppState::get_mut(); + if !this.has_launched() { + return; + } + match this.state_mut() { + &mut AppStateImpl::ProcessingEvents { .. } => {} + _ => bug!("`ProcessingRedraws` happened unexpectedly"), + }; + drop(this); + + // User events are always sent out at the end of the "MainEventLoop" + handle_user_events(); + handle_nonuser_event(Event::EventsCleared); + + let mut this = AppState::get_mut(); + let redraw_events = this + .main_events_cleared_transition() + .into_iter() + .map(|window| Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::RedrawRequested, + }); + drop(this); + + handle_nonuser_events(redraw_events); +} + +// requires main thread +pub unsafe fn handle_events_cleared() { + AppState::get_mut().events_cleared_transition(); +} + +// requires main thread +pub unsafe fn terminated() { + let mut this = AppState::get_mut(); + let mut event_handler = this.terminated_transition(); + let mut control_flow = this.control_flow; + drop(this); + + event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) +} + struct EventLoopWaker { timer: CFRunLoopTimerRef, } @@ -577,9 +823,9 @@ impl EventLoopWaker { fn new(rl: CFRunLoopRef) -> EventLoopWaker { extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} unsafe { - // create a timer with a 1microsec interval (1ns does not work) to mimic polling. - // it is initially setup with a first fire time really far into the - // future, but that gets changed to fire immediatley in did_finish_launching + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching let timer = CFRunLoopTimerCreate( ptr::null_mut(), std::f64::MAX, @@ -618,3 +864,95 @@ impl EventLoopWaker { } } } + +macro_rules! os_capabilities { + ( + $( + $(#[$attr:meta])* + $error_name:ident: $objc_call:literal, + $name:ident: $major:literal-$minor:literal + ),* + $(,)* + ) => { + #[derive(Clone, Debug)] + pub struct OSCapabilities { + $( + pub $name: bool, + )* + + os_version: NSOperatingSystemVersion, + } + + impl From for OSCapabilities { + fn from(os_version: NSOperatingSystemVersion) -> OSCapabilities { + $(let $name = os_version.meets_requirements($major, $minor);)* + OSCapabilities { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + log::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.major, self.os_version.minor, self.os_version.patch, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +impl NSOperatingSystemVersion { + fn meets_requirements(&self, required_major: NSInteger, required_minor: NSInteger) -> bool { + (self.major, self.minor) >= (required_major, required_minor) + } +} + +pub fn os_capabilities() -> OSCapabilities { + lazy_static! { + static ref OS_CAPABILITIES: OSCapabilities = { + let version: NSOperatingSystemVersion = unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let atleast_ios_8: BOOL = msg_send![ + process_info, + respondsToSelector: sel!(operatingSystemVersion) + ]; + // winit requires atleast iOS 8 because no one has put the time into supporting earlier os versions. + // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support + // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS + // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 + // has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!( + atleast_ios_8 == YES, + "`winit` requires iOS version 8 or greater" + ); + msg_send![process_info, operatingSystemVersion] + }; + version.into() + }; + } + OS_CAPABILITIES.clone() +} diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index 469d11b1..b26b1eb6 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -16,15 +16,14 @@ use crate::{ }; use crate::platform_impl::platform::{ - app_state::AppState, + app_state, ffi::{ id, kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode, kCFRunLoopEntry, kCFRunLoopExit, nil, CFIndex, CFRelease, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, - CFRunLoopSourceSignal, CFRunLoopWakeUp, NSOperatingSystemVersion, NSString, - UIApplicationMain, UIUserInterfaceIdiom, + CFRunLoopSourceSignal, CFRunLoopWakeUp, NSString, UIApplicationMain, UIUserInterfaceIdiom, }, monitor, view, MonitorHandle, }; @@ -32,13 +31,6 @@ use crate::platform_impl::platform::{ pub struct EventLoopWindowTarget { receiver: Receiver, sender_to_clone: Sender, - capabilities: Capabilities, -} - -impl EventLoopWindowTarget { - pub fn capabilities(&self) -> &Capabilities { - &self.capabilities - } } pub struct EventLoop { @@ -64,18 +56,11 @@ impl EventLoop { // this line sets up the main run loop before `UIApplicationMain` setup_control_flow_observers(); - let version: NSOperatingSystemVersion = unsafe { - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - msg_send![process_info, operatingSystemVersion] - }; - let capabilities = version.into(); - EventLoop { window_target: RootEventLoopWindowTarget { p: EventLoopWindowTarget { receiver, sender_to_clone, - capabilities, }, _marker: PhantomData, }, @@ -95,7 +80,7 @@ impl EventLoop { `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ Note: `EventLoop::run` calls `UIApplicationMain` on iOS" ); - AppState::will_launch(Box::new(EventLoopHandler { + app_state::will_launch(Box::new(EventLoopHandler { f: event_handler, event_loop: self.window_target, })); @@ -142,8 +127,7 @@ pub struct EventLoopProxy { source: CFRunLoopSourceRef, } -unsafe impl Send for EventLoopProxy {} -unsafe impl Sync for EventLoopProxy {} +unsafe impl Send for EventLoopProxy {} impl Clone for EventLoopProxy { fn clone(&self) -> EventLoopProxy { @@ -163,7 +147,7 @@ impl Drop for EventLoopProxy { impl EventLoopProxy { fn new(sender: Sender) -> EventLoopProxy { unsafe { - // just wakeup the eventloop + // just wake up the eventloop extern "C" fn event_loop_proxy_handler(_: *mut c_void) {} // adding a Source to the main CFRunLoop lets us wake it up and @@ -171,7 +155,7 @@ impl EventLoopProxy { let rl = CFRunLoopGetMain(); // we want all the members of context to be zero/null, except one let mut context: CFRunLoopSourceContext = mem::zeroed(); - context.perform = event_loop_proxy_handler; + context.perform = Some(event_loop_proxy_handler); let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::max_value() - 1, &mut context); CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); @@ -204,15 +188,40 @@ fn setup_control_flow_observers() { unsafe { #[allow(non_upper_case_globals)] match activity { - kCFRunLoopAfterWaiting => AppState::handle_wakeup_transition(), + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(), kCFRunLoopEntry => unimplemented!(), // not expected to ever happen _ => unreachable!(), } } } + // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in + // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end + // priority to be 0, in order to send EventsCleared 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`. + // + // 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. + // + // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. + extern "C" fn control_flow_main_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(), + kCFRunLoopExit => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + } + } + // end is queued with the lowest priority to ensure it is processed after other observers - // without that, LoopDestroyed will get sent after EventsCleared extern "C" fn control_flow_end_handler( _: CFRunLoopObserverRef, activity: CFRunLoopActivity, @@ -221,7 +230,7 @@ fn setup_control_flow_observers() { unsafe { #[allow(non_upper_case_globals)] match activity { - kCFRunLoopBeforeWaiting => AppState::handle_events_cleared(), + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(), kCFRunLoopExit => unimplemented!(), // not expected to ever happen _ => unreachable!(), } @@ -229,6 +238,7 @@ fn setup_control_flow_observers() { } let main_loop = CFRunLoopGetMain(); + let begin_observer = CFRunLoopObserverCreate( ptr::null_mut(), kCFRunLoopEntry | kCFRunLoopAfterWaiting, @@ -238,6 +248,17 @@ fn setup_control_flow_observers() { ptr::null_mut(), ); CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode); + + let main_end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + 0, // see comment on `control_flow_main_end_handler` + control_flow_main_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, main_end_observer, kCFRunLoopDefaultMode); + let end_observer = CFRunLoopObserverCreate( ptr::null_mut(), kCFRunLoopExit | kCFRunLoopBeforeWaiting, @@ -297,20 +318,3 @@ pub unsafe fn get_idiom() -> Idiom { let raw_idiom: UIUserInterfaceIdiom = msg_send![device, userInterfaceIdiom]; raw_idiom.into() } - -pub struct Capabilities { - pub supports_safe_area: bool, -} - -impl From for Capabilities { - fn from(os_version: NSOperatingSystemVersion) -> Capabilities { - assert!( - os_version.major >= 8, - "`winit` current requires iOS version 8 or greater" - ); - - let supports_safe_area = os_version.major >= 11; - - Capabilities { supports_safe_area } - } -} diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 9b0c26be..4ecd47f7 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -1,10 +1,10 @@ #![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] -use std::{ffi::CString, ops::BitOr, os::raw::*}; +use std::{convert::TryInto, ffi::CString, ops::BitOr, os::raw::*}; use objc::{runtime::Object, Encode, Encoding}; -use crate::platform::ios::{Idiom, ValidOrientations}; +use crate::platform::ios::{Idiom, ScreenEdge, ValidOrientations}; pub type id = *mut Object; pub const nil: id = 0 as id; @@ -70,6 +70,24 @@ pub enum UITouchPhase { Cancelled, } +#[derive(Debug, PartialEq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UIForceTouchCapability { + Unknown = 0, + Unavailable, + Available, +} + +#[derive(Debug, PartialEq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchType { + Direct = 0, + Indirect, + Pencil, +} + #[repr(C)] #[derive(Debug, Clone)] pub struct UIEdgeInsets { @@ -173,6 +191,51 @@ impl UIInterfaceOrientationMask { } } +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIRectEdge(NSUInteger); + +unsafe impl Encode for UIRectEdge { + fn encode() -> Encoding { + NSUInteger::encode() + } +} + +impl From for UIRectEdge { + fn from(screen_edge: ScreenEdge) -> UIRectEdge { + assert_eq!( + screen_edge.bits() & !ScreenEdge::ALL.bits(), + 0, + "invalid `ScreenEdge`" + ); + UIRectEdge(screen_edge.bits().into()) + } +} + +impl Into for UIRectEdge { + fn into(self) -> ScreenEdge { + let bits: u8 = self.0.try_into().expect("invalid `UIRectEdge`"); + ScreenEdge::from_bits(bits).expect("invalid `ScreenEdge`") + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIScreenOverscanCompensation(NSInteger); + +unsafe impl Encode for UIScreenOverscanCompensation { + fn encode() -> Encoding { + NSInteger::encode() + } +} + +#[allow(dead_code)] +impl UIScreenOverscanCompensation { + pub const Scale: UIScreenOverscanCompensation = UIScreenOverscanCompensation(0); + pub const InsetBounds: UIScreenOverscanCompensation = UIScreenOverscanCompensation(1); + pub const None: UIScreenOverscanCompensation = UIScreenOverscanCompensation(2); +} + #[link(name = "UIKit", kind = "framework")] #[link(name = "CoreFoundation", kind = "framework")] extern "C" { @@ -268,14 +331,14 @@ pub enum CFRunLoopTimerContext {} pub struct CFRunLoopSourceContext { pub version: CFIndex, pub info: *mut c_void, - pub retain: extern "C" fn(*const c_void) -> *const c_void, - pub release: extern "C" fn(*const c_void), - pub copyDescription: extern "C" fn(*const c_void) -> CFStringRef, - pub equal: extern "C" fn(*const c_void, *const c_void) -> Boolean, - pub hash: extern "C" fn(*const c_void) -> CFHashCode, - pub schedule: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), - pub cancel: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), - pub perform: extern "C" fn(*mut c_void), + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, + pub equal: Option Boolean>, + pub hash: Option CFHashCode>, + pub schedule: Option, + pub cancel: Option, + pub perform: Option, } pub trait NSString: Sized { diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index dfb659f9..3141dea4 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -79,7 +79,7 @@ use std::fmt; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index c75b7ef6..14279f66 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -1,18 +1,98 @@ use std::{ - collections::{HashSet, VecDeque}, + collections::{BTreeSet, VecDeque}, fmt, ops::{Deref, DerefMut}, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::{ + app_state, + ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}, + }, }; -use crate::platform_impl::platform::ffi::{ - id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger, -}; +#[derive(Debug, PartialEq, Eq, Hash)] +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) monitor: MonitorHandle, +} +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] }, + 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"); + let os_capabilities = app_state::os_capabilities(); + let refresh_rate: NSInteger = if os_capabilities.maximum_frames_per_second { + msg_send![uiscreen, maximumFramesPerSecond] + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + }; + let size: CGSize = msg_send![screen_mode, size]; + 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], + monitor: MonitorHandle::retained_new(uiscreen), + } + } + + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Inner { uiscreen: id, } @@ -25,6 +105,7 @@ impl Drop for Inner { } } +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MonitorHandle { inner: Inner, } @@ -106,9 +187,10 @@ impl MonitorHandle { impl Inner { pub fn name(&self) -> Option { unsafe { - if self.uiscreen == main_uiscreen().uiscreen { + let main = main_uiscreen(); + if self.uiscreen == main.uiscreen { Some("Primary".to_string()) - } else if self.uiscreen == mirrored_uiscreen().uiscreen { + } else if self.uiscreen == mirrored_uiscreen(&main).uiscreen { Some("Mirrored".to_string()) } else { uiscreens() @@ -140,22 +222,18 @@ impl Inner { } } - pub fn video_modes(&self) -> impl Iterator { - let refresh_rate: NSInteger = unsafe { msg_send![self.uiscreen, maximumFramesPerSecond] }; + pub fn video_modes(&self) -> impl Iterator { + let mut modes = BTreeSet::new(); + unsafe { + let available_modes: id = msg_send![self.uiscreen, availableModes]; + let available_mode_count: NSUInteger = msg_send![available_modes, count]; - let available_modes: id = unsafe { msg_send![self.uiscreen, availableModes] }; - let available_mode_count: NSUInteger = unsafe { msg_send![available_modes, count] }; - - let mut modes = HashSet::with_capacity(available_mode_count); - - for i in 0..available_mode_count { - let mode: id = unsafe { msg_send![available_modes, objectAtIndex: i] }; - let size: CGSize = unsafe { msg_send![mode, size] }; - modes.insert(VideoMode { - size: (size.width as u32, size.height as u32), - bit_depth: 32, - refresh_rate: refresh_rate as u16, - }); + for i in 0..available_mode_count { + let mode: id = msg_send![available_modes, objectAtIndex: i]; + modes.insert(RootVideoMode { + video_mode: VideoMode::retained_new(self.uiscreen, mode), + }); + } } modes.into_iter() @@ -167,6 +245,15 @@ impl Inner { pub fn ui_screen(&self) -> id { self.uiscreen } + + pub fn preferred_video_mode(&self) -> RootVideoMode { + unsafe { + let mode: id = msg_send![self.uiscreen, preferredMode]; + RootVideoMode { + video_mode: VideoMode::retained_new(self.uiscreen, mode), + } + } + } } // requires being run on main thread @@ -176,8 +263,8 @@ pub unsafe fn main_uiscreen() -> MonitorHandle { } // requires being run on main thread -unsafe fn mirrored_uiscreen() -> MonitorHandle { - let uiscreen: id = msg_send![class!(UIScreen), mirroredScreen]; +unsafe fn mirrored_uiscreen(monitor: &MonitorHandle) -> MonitorHandle { + let uiscreen: id = msg_send![monitor.uiscreen, mirroredScreen]; MonitorHandle::retained_new(uiscreen) } diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index b174b75a..5868059b 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -6,18 +6,78 @@ use objc::{ }; use crate::{ - event::{DeviceId as RootDeviceId, Event, Touch, TouchPhase, WindowEvent}, + event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, platform::ios::MonitorHandleExtIOS, - window::{WindowAttributes, WindowId as RootWindowId}, + platform_impl::platform::{ + app_state::{self, OSCapabilities}, + event_loop, + ffi::{ + id, nil, CGFloat, CGPoint, CGRect, UIForceTouchCapability, UIInterfaceOrientationMask, + UIRectEdge, UITouchPhase, UITouchType, + }, + window::PlatformSpecificWindowBuilderAttributes, + DeviceId, + }, + window::{Fullscreen, WindowAttributes, WindowId as RootWindowId}, }; -use crate::platform_impl::platform::{ - app_state::AppState, - event_loop, - ffi::{id, nil, CGFloat, CGPoint, CGRect, UIInterfaceOrientationMask, UITouchPhase}, - window::PlatformSpecificWindowBuilderAttributes, - DeviceId, -}; +macro_rules! add_property { + ( + $decl:ident, + $name:ident: $t:ty, + $setter_name:ident: |$object:ident| $after_set:expr, + $getter_name:ident, + ) => { + add_property!( + $decl, + $name: $t, + $setter_name: true, |_, _|{}; |$object| $after_set, + $getter_name, + ) + }; + ( + $decl:ident, + $name:ident: $t:ty, + $setter_name:ident: $capability:expr, $err:expr; |$object:ident| $after_set:expr, + $getter_name:ident, + ) => { + { + const VAR_NAME: &'static str = concat!("_", stringify!($name)); + $decl.add_ivar::<$t>(VAR_NAME); + let setter = if $capability { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + unsafe { + $object.set_ivar::<$t>(VAR_NAME, value); + } + $after_set + } + $setter_name + } else { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + unsafe { + $object.set_ivar::<$t>(VAR_NAME, value); + } + $err(&app_state::os_capabilities(), "ignoring") + } + $setter_name + }; + #[allow(non_snake_case)] + extern "C" fn $getter_name($object: &Object, _: Sel) -> $t { + unsafe { *$object.get_ivar::<$t>(VAR_NAME) } + } + $decl.add_method( + sel!($setter_name:), + setter as extern "C" fn(&mut Object, Sel, $t), + ); + $decl.add_method( + sel!($getter_name), + $getter_name as extern "C" fn(&Object, Sel) -> $t, + ); + } + }; +} // requires main thread unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { @@ -41,7 +101,8 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { extern "C" fn draw_rect(object: &Object, _: Sel, rect: CGRect) { unsafe { let window: id = msg_send![object, window]; - AppState::handle_nonuser_event(Event::WindowEvent { + assert!(!window.is_null()); + app_state::handle_nonuser_event(Event::WindowEvent { window_id: RootWindowId(window.into()), event: WindowEvent::RedrawRequested, }); @@ -52,22 +113,143 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { extern "C" fn layout_subviews(object: &Object, _: Sel) { unsafe { + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![super(object, superclass), layoutSubviews]; + let window: id = msg_send![object, window]; + assert!(!window.is_null()); let 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]; let size = crate::dpi::LogicalSize { - width: screen_frame.size.width, - height: screen_frame.size.height, + width: screen_frame.size.width as _, + height: screen_frame.size.height as _, }; - AppState::handle_nonuser_event(Event::WindowEvent { + app_state::handle_nonuser_event(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, + ) { + unsafe { let superclass: &'static Class = msg_send![object, superclass]; - let () = msg_send![super(object, superclass), layoutSubviews]; + let () = msg_send![ + super(object, superclass), + setContentScaleFactor: untrusted_hidpi_factor + ]; + + let window: id = msg_send![object, window]; + // `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow + // makeKeyAndVisible]` at window creation time (either manually or internally by + // UIKit when the `UIView` is first created), in which case we send no events here + if window.is_null() { + return; + } + // `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]; + assert!( + !hidpi_factor.is_nan() + && hidpi_factor.is_finite() + && hidpi_factor.is_sign_positive() + && hidpi_factor > 0.0, + "invalid hidpi_factor set on UIView", + ); + let bounds: CGRect = msg_send![object, 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]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as _, + 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), + })), + ); + } + } + + extern "C" fn handle_touches(object: &Object, _: Sel, touches: id, _: id) { + unsafe { + let window: id = msg_send![object, window]; + assert!(!window.is_null()); + let uiscreen: id = msg_send![window, screen]; + let touches_enum: id = msg_send![touches, objectEnumerator]; + let mut touch_events = Vec::new(); + let os_supports_force = app_state::os_capabilities().force_touch; + loop { + let touch: id = msg_send![touches_enum, nextObject]; + if touch == nil { + break; + } + let 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]; + let touch_capability: UIForceTouchCapability = + msg_send![trait_collection, forceTouchCapability]; + // Both the OS _and_ the device need to be checked for force touch support. + if touch_capability == UIForceTouchCapability::Available { + let force: CGFloat = msg_send![touch, force]; + let max_possible_force: CGFloat = + msg_send![touch, maximumPossibleForce]; + let altitude_angle: Option = if touch_type == UITouchType::Pencil { + let angle: CGFloat = msg_send![touch, altitudeAngle]; + Some(angle as _) + } else { + None + }; + Some(Force::Calibrated { + force: force as _, + max_possible_force: max_possible_force as _, + altitude_angle, + }) + } else { + None + } + } else { + None + }; + let touch_id = touch as u64; + let phase: UITouchPhase = msg_send![touch, phase]; + let phase = match phase { + UITouchPhase::Began => TouchPhase::Started, + UITouchPhase::Moved => TouchPhase::Moved, + // 2 is UITouchPhase::Stationary and is not expected here + UITouchPhase::Ended => TouchPhase::Ended, + UITouchPhase::Cancelled => TouchPhase::Cancelled, + _ => panic!("unexpected touch phase: {:?}", phase as i32), + }; + + touch_events.push(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(), + force, + phase, + }), + }); + } + app_state::handle_nonuser_events(touch_events); } } @@ -82,184 +264,9 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { sel!(layoutSubviews), layout_subviews as extern "C" fn(&Object, Sel), ); - decl.register() - }) -} - -// requires main thread -unsafe fn get_view_controller_class() -> &'static Class { - static mut CLASS: Option<&'static Class> = None; - if CLASS.is_none() { - let uiviewcontroller_class = class!(UIViewController); - - extern "C" fn set_prefers_status_bar_hidden(object: &mut Object, _: Sel, hidden: BOOL) { - unsafe { - object.set_ivar::("_prefers_status_bar_hidden", hidden); - let () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; - } - } - - extern "C" fn prefers_status_bar_hidden(object: &Object, _: Sel) -> BOOL { - unsafe { *object.get_ivar::("_prefers_status_bar_hidden") } - } - - extern "C" fn set_supported_orientations( - object: &mut Object, - _: Sel, - orientations: UIInterfaceOrientationMask, - ) { - unsafe { - object.set_ivar::( - "_supported_orientations", - orientations, - ); - let () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; - } - } - - extern "C" fn supported_orientations( - object: &Object, - _: Sel, - ) -> UIInterfaceOrientationMask { - unsafe { *object.get_ivar::("_supported_orientations") } - } - - extern "C" fn should_autorotate(_: &Object, _: Sel) -> BOOL { - YES - } - - let mut decl = ClassDecl::new("WinitUIViewController", uiviewcontroller_class) - .expect("Failed to declare class `WinitUIViewController`"); - decl.add_ivar::("_prefers_status_bar_hidden"); - decl.add_ivar::("_supported_orientations"); decl.add_method( - sel!(setPrefersStatusBarHidden:), - set_prefers_status_bar_hidden as extern "C" fn(&mut Object, Sel, BOOL), - ); - decl.add_method( - sel!(prefersStatusBarHidden), - prefers_status_bar_hidden as extern "C" fn(&Object, Sel) -> BOOL, - ); - decl.add_method( - sel!(setSupportedInterfaceOrientations:), - set_supported_orientations - as extern "C" fn(&mut Object, Sel, UIInterfaceOrientationMask), - ); - decl.add_method( - sel!(supportedInterfaceOrientations), - supported_orientations as extern "C" fn(&Object, Sel) -> UIInterfaceOrientationMask, - ); - decl.add_method( - sel!(shouldAutorotate), - should_autorotate as extern "C" fn(&Object, Sel) -> BOOL, - ); - CLASS = Some(decl.register()); - } - CLASS.unwrap() -} - -// requires main thread -unsafe fn get_window_class() -> &'static Class { - static mut CLASS: Option<&'static Class> = None; - if CLASS.is_none() { - let uiwindow_class = class!(UIWindow); - - extern "C" fn become_key_window(object: &Object, _: Sel) { - unsafe { - AppState::handle_nonuser_event(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 { - AppState::handle_nonuser_event(Event::WindowEvent { - window_id: RootWindowId(object.into()), - event: WindowEvent::Focused(false), - }); - let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; - } - } - - extern "C" fn handle_touches(object: &Object, _: Sel, touches: id, _: id) { - unsafe { - let uiscreen = msg_send![object, screen]; - let touches_enum: id = msg_send![touches, objectEnumerator]; - let mut touch_events = Vec::new(); - loop { - let touch: id = msg_send![touches_enum, nextObject]; - if touch == nil { - break; - } - let location: CGPoint = msg_send![touch, locationInView: nil]; - let touch_id = touch as u64; - let phase: UITouchPhase = msg_send![touch, phase]; - let phase = match phase { - UITouchPhase::Began => TouchPhase::Started, - UITouchPhase::Moved => TouchPhase::Moved, - // 2 is UITouchPhase::Stationary and is not expected here - UITouchPhase::Ended => TouchPhase::Ended, - UITouchPhase::Cancelled => TouchPhase::Cancelled, - _ => panic!("unexpected touch phase: {:?}", phase as i32), - }; - - touch_events.push(Event::WindowEvent { - window_id: RootWindowId(object.into()), - event: WindowEvent::Touch(Touch { - device_id: RootDeviceId(DeviceId { uiscreen }), - id: touch_id, - location: (location.x as f64, location.y as f64).into(), - phase, - }), - }); - } - AppState::handle_nonuser_events(touch_events); - } - } - - extern "C" fn set_content_scale_factor(object: &mut Object, _: Sel, hidpi_factor: CGFloat) { - unsafe { - let () = msg_send![ - super(object, class!(UIWindow)), - setContentScaleFactor: hidpi_factor - ]; - let view_controller: id = msg_send![object, rootViewController]; - let view: id = msg_send![view_controller, view]; - let () = msg_send![view, setContentScaleFactor: hidpi_factor]; - let bounds: CGRect = msg_send![object, bounds]; - let screen: id = msg_send![object, screen]; - let screen_space: id = msg_send![screen, coordinateSpace]; - let screen_frame: CGRect = - msg_send![object, convertRect:bounds toCoordinateSpace:screen_space]; - let size = crate::dpi::LogicalSize { - width: screen_frame.size.width, - height: screen_frame.size.height, - }; - AppState::handle_nonuser_events( - std::iter::once(Event::WindowEvent { - window_id: RootWindowId(object.into()), - event: WindowEvent::HiDpiFactorChanged(hidpi_factor as _), - }) - .chain(std::iter::once(Event::WindowEvent { - window_id: RootWindowId(object.into()), - event: WindowEvent::Resized(size), - })), - ); - } - } - - let mut decl = ClassDecl::new("WinitUIWindow", uiwindow_class) - .expect("Failed to declare class `WinitUIWindow`"); - decl.add_method( - sel!(becomeKeyWindow), - become_key_window as extern "C" fn(&Object, Sel), - ); - decl.add_method( - sel!(resignKeyWindow), - resign_key_window as extern "C" fn(&Object, Sel), + sel!(setContentScaleFactor:), + set_content_scale_factor as extern "C" fn(&mut Object, Sel, CGFloat), ); decl.add_method( @@ -279,9 +286,114 @@ unsafe fn get_window_class() -> &'static Class { handle_touches as extern "C" fn(this: &Object, _: Sel, _: id, _: id), ); + decl.register() + }) +} + +// requires main thread +unsafe fn get_view_controller_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let os_capabilities = app_state::os_capabilities(); + + let uiviewcontroller_class = class!(UIViewController); + + extern "C" fn should_autorotate(_: &Object, _: Sel) -> BOOL { + YES + } + + let mut decl = ClassDecl::new("WinitUIViewController", uiviewcontroller_class) + .expect("Failed to declare class `WinitUIViewController`"); decl.add_method( - sel!(setContentScaleFactor:), - set_content_scale_factor as extern "C" fn(&mut Object, Sel, CGFloat), + sel!(shouldAutorotate), + should_autorotate as extern "C" fn(&Object, Sel) -> BOOL, + ); + add_property! { + decl, + prefers_status_bar_hidden: BOOL, + setPrefersStatusBarHidden: |object| { + unsafe { + let () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; + } + }, + prefersStatusBarHidden, + } + add_property! { + decl, + prefers_home_indicator_auto_hidden: BOOL, + setPrefersHomeIndicatorAutoHidden: + os_capabilities.home_indicator_hidden, + OSCapabilities::home_indicator_hidden_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; + } + }, + prefersHomeIndicatorAutoHidden, + } + add_property! { + decl, + supported_orientations: UIInterfaceOrientationMask, + setSupportedInterfaceOrientations: |object| { + unsafe { + let () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; + } + }, + supportedInterfaceOrientations, + } + add_property! { + decl, + preferred_screen_edges_deferring_system_gestures: UIRectEdge, + setPreferredScreenEdgesDeferringSystemGestures: + os_capabilities.defer_system_gestures, + OSCapabilities::defer_system_gestures_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; + } + }, + preferredScreenEdgesDeferringSystemGestures, + } + CLASS = Some(decl.register()); + } + CLASS.unwrap() +} + +// requires main thread +unsafe fn get_window_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let uiwindow_class = class!(UIWindow); + + extern "C" fn become_key_window(object: &Object, _: Sel) { + unsafe { + app_state::handle_nonuser_event(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 { + window_id: RootWindowId(object.into()), + event: WindowEvent::Focused(false), + }); + let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; + } + } + + let mut decl = ClassDecl::new("WinitUIWindow", uiwindow_class) + .expect("Failed to declare class `WinitUIWindow`"); + decl.add_method( + sel!(becomeKeyWindow), + become_key_window as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(resignKeyWindow), + resign_key_window as extern "C" fn(&Object, Sel), ); CLASS = Some(decl.register()); @@ -302,13 +414,16 @@ 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]; + } view } // requires main thread pub unsafe fn create_view_controller( - window_attributes: &WindowAttributes, + _window_attributes: &WindowAttributes, platform_attributes: &PlatformSpecificWindowBuilderAttributes, view: id, ) -> id { @@ -324,16 +439,24 @@ pub unsafe fn create_view_controller( !view_controller.is_null(), "Failed to initialize `UIViewController` instance" ); - let status_bar_hidden = if window_attributes.decorations { - NO - } else { + let status_bar_hidden = if platform_attributes.prefers_status_bar_hidden { YES + } else { + NO }; let idiom = event_loop::get_idiom(); let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom( platform_attributes.valid_orientations, idiom, ); + let prefers_home_indicator_hidden = if platform_attributes.prefers_home_indicator_hidden { + YES + } else { + NO + }; + let edges: UIRectEdge = platform_attributes + .preferred_screen_edges_deferring_system_gestures + .into(); let () = msg_send![ view_controller, setPrefersStatusBarHidden: status_bar_hidden @@ -342,6 +465,14 @@ pub unsafe fn create_view_controller( view_controller, setSupportedInterfaceOrientations: supported_orientations ]; + let () = msg_send![ + view_controller, + setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden + ]; + let () = msg_send![ + view_controller, + setPreferredScreenEdgesDeferringSystemGestures: edges + ]; let () = msg_send![view_controller, setView: view]; view_controller } @@ -349,7 +480,7 @@ pub unsafe fn create_view_controller( // requires main thread pub unsafe fn create_window( window_attributes: &WindowAttributes, - platform_attributes: &PlatformSpecificWindowBuilderAttributes, + _platform_attributes: &PlatformSpecificWindowBuilderAttributes, frame: CGRect, view_controller: id, ) -> id { @@ -363,11 +494,16 @@ pub unsafe fn create_window( "Failed to initialize `UIWindow` instance" ); let () = msg_send![window, setRootViewController: view_controller]; - if let Some(hidpi_factor) = platform_attributes.hidpi_factor { - let () = msg_send![window, setContentScaleFactor: hidpi_factor as CGFloat]; - } - if let &Some(ref monitor) = &window_attributes.fullscreen { - let () = msg_send![window, setScreen:monitor.ui_screen()]; + 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]; + msg_send![window, setScreen:video_mode.monitor().ui_screen()] + } + Some(Fullscreen::Borderless(ref monitor)) => { + msg_send![window, setScreen:monitor.ui_screen()] + } + None => (), } window @@ -376,17 +512,17 @@ pub unsafe fn create_window( pub fn create_delegate_class() { extern "C" fn did_finish_launching(_: &mut Object, _: Sel, _: id, _: id) -> BOOL { unsafe { - AppState::did_finish_launching(); + app_state::did_finish_launching(); } YES } extern "C" fn did_become_active(_: &Object, _: Sel, _: id) { - unsafe { AppState::handle_nonuser_event(Event::Resumed) } + unsafe { app_state::handle_nonuser_event(Event::Resumed) } } extern "C" fn will_resign_active(_: &Object, _: Sel, _: id) { - unsafe { AppState::handle_nonuser_event(Event::Suspended) } + unsafe { app_state::handle_nonuser_event(Event::Suspended) } } extern "C" fn will_enter_foreground(_: &Object, _: Sel, _: id) {} @@ -411,8 +547,8 @@ pub fn create_delegate_class() { }); } } - AppState::handle_nonuser_events(events); - AppState::terminated(); + app_state::handle_nonuser_events(events); + app_state::terminated(); } } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index ea4f022f..eea0c304 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -1,30 +1,34 @@ +use raw_window_handle::{ios::IOSHandle, RawWindowHandle}; use std::{ collections::VecDeque, ops::{Deref, DerefMut}, }; -use objc::runtime::{Class, Object, NO, YES}; +use objc::runtime::{Class, Object, BOOL, NO, YES}; use crate::{ dpi::{self, LogicalPosition, LogicalSize}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::{Event, WindowEvent}, icon::Icon, monitor::MonitorHandle as RootMonitorHandle, - platform::ios::{MonitorHandleExtIOS, ValidOrientations}, + platform::ios::{MonitorHandleExtIOS, ScreenEdge, ValidOrientations}, platform_impl::platform::{ - app_state::AppState, - event_loop, - ffi::{id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask}, + app_state, event_loop, + ffi::{ + id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, + UIRectEdge, UIScreenOverscanCompensation, + }, monitor, view, EventLoopWindowTarget, MonitorHandle, }, - window::{CursorIcon, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId}, }; pub struct Inner { pub window: id, pub view_controller: id, pub view: id, - supports_safe_area: bool, + gl_or_metal_backed: bool, } impl Drop for Inner { @@ -55,7 +59,19 @@ impl Inner { pub fn request_redraw(&self) { unsafe { - let () = msg_send![self.view, setNeedsDisplay]; + if self.gl_or_metal_backed { + // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or CAMetalLayer. + // Ordinarily the OS sets up a bunch of UIKit state before calling drawRect: on a UIView, but when using + // raw or gl/metal for drawing this work is completely avoided. + // + // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been confirmed via + // testing. + // + // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc + app_state::queue_gl_or_metal_redraw(self.window); + } else { + let () = msg_send![self.view, setNeedsDisplay]; + } } } @@ -63,8 +79,8 @@ impl Inner { unsafe { let safe_area = self.safe_area_screen_space(); Ok(LogicalPosition { - x: safe_area.origin.x, - y: safe_area.origin.y, + x: safe_area.origin.x as _, + y: safe_area.origin.y as _, }) } } @@ -73,8 +89,8 @@ impl Inner { unsafe { let screen_frame = self.screen_frame(); Ok(LogicalPosition { - x: screen_frame.origin.x, - y: screen_frame.origin.y, + x: screen_frame.origin.x as _, + y: screen_frame.origin.y as _, }) } } @@ -98,8 +114,8 @@ impl Inner { unsafe { let safe_area = self.safe_area_screen_space(); LogicalSize { - width: safe_area.size.width, - height: safe_area.size.height, + width: safe_area.size.width as _, + height: safe_area.size.height as _, } } } @@ -108,8 +124,8 @@ impl Inner { unsafe { let screen_frame = self.screen_frame(); LogicalSize { - width: screen_frame.size.width, - height: screen_frame.size.height, + width: screen_frame.size.width as _, + height: screen_frame.size.height as _, } } } @@ -157,26 +173,41 @@ impl Inner { warn!("`Window::set_maximized` is ignored on iOS") } - pub fn set_fullscreen(&self, monitor: Option) { + pub fn set_fullscreen(&self, monitor: Option) { unsafe { - match monitor { - Some(monitor) => { - let uiscreen = monitor.ui_screen() as id; - let current: id = msg_send![self.window, screen]; - let bounds: CGRect = msg_send![uiscreen, bounds]; - - // this is pretty slow on iOS, so avoid doing it if we can - if uiscreen != current { - let () = msg_send![self.window, setScreen: uiscreen]; - } - let () = msg_send![self.window, setFrame: bounds]; + let uiscreen = match monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.video_mode.monitor.ui_screen() as id; + let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode]; + uiscreen } - None => warn!("`Window::set_fullscreen(None)` ignored on iOS"), + Some(Fullscreen::Borderless(monitor)) => monitor.ui_screen() as id, + None => { + warn!("`Window::set_fullscreen(None)` ignored on iOS"); + return; + } + }; + + // this is pretty slow on iOS, so avoid doing it if we can + let current: id = msg_send![self.window, screen]; + if uiscreen != current { + let () = msg_send![self.window, setScreen: uiscreen]; } + + let bounds: CGRect = msg_send![uiscreen, bounds]; + let () = msg_send![self.window, setFrame: bounds]; + + // For external displays, we must disable overscan compensation or + // the displayed image will have giant black bars surrounding it on + // each side + let () = msg_send![ + uiscreen, + setOverscanCompensation: UIScreenOverscanCompensation::None + ]; } } - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { unsafe { let monitor = self.current_monitor(); let uiscreen = monitor.inner.ui_screen(); @@ -189,21 +220,15 @@ impl Inner { && screen_space_bounds.size.width == screen_bounds.size.width && screen_space_bounds.size.height == screen_bounds.size.height { - Some(monitor) + Some(Fullscreen::Borderless(monitor)) } else { None } } } - pub fn set_decorations(&self, decorations: bool) { - unsafe { - let status_bar_hidden = if decorations { NO } else { YES }; - let () = msg_send![ - self.view_controller, - setPrefersStatusBarHidden: status_bar_hidden - ]; - } + pub fn set_decorations(&self, _decorations: bool) { + warn!("`Window::set_decorations` is ignored on iOS") } pub fn set_always_on_top(&self, _always_on_top: bool) { @@ -238,6 +263,16 @@ impl Inner { pub fn id(&self) -> WindowId { self.window.into() } + + pub fn raw_window_handle(&self) -> RawWindowHandle { + let handle = IOSHandle { + ui_window: self.window as _, + ui_view: self.view as _, + ui_view_controller: self.view_controller as _, + ..IOSHandle::empty() + }; + RawWindowHandle::IOS(handle) + } } pub struct Window { @@ -277,7 +312,7 @@ impl DerefMut for Window { impl Window { pub fn new( - event_loop: &EventLoopWindowTarget, + _event_loop: &EventLoopWindowTarget, window_attributes: WindowAttributes, platform_attributes: PlatformSpecificWindowBuilderAttributes, ) -> Result { @@ -293,25 +328,38 @@ impl Window { // TODO: transparency, visible unsafe { - let screen = window_attributes - .fullscreen - .as_ref() - .map(|screen| screen.ui_screen() as _) - .unwrap_or_else(|| monitor::main_uiscreen().ui_screen()); + let screen = match window_attributes.fullscreen { + 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(), + }; + 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, - height: dim.height, + width: dim.width as _, + height: dim.height as _, }, }, None => screen_bounds, }; let view = view::create_view(&window_attributes, &platform_attributes, frame.clone()); + + let gl_or_metal_backed = { + let view_class: id = msg_send![view, class]; + let layer_class: id = msg_send![view_class, layerClass]; + let is_metal: BOOL = + msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)]; + let is_gl: BOOL = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)]; + is_metal == YES || is_gl == YES + }; + let view_controller = view::create_view_controller(&window_attributes, &platform_attributes, view); let window = view::create_window( @@ -321,17 +369,41 @@ impl Window { view_controller, ); - let supports_safe_area = event_loop.capabilities().supports_safe_area; - let result = Window { inner: Inner { window, view_controller, view, - supports_safe_area, + gl_or_metal_backed, }, }; - AppState::set_key_window(window); + app_state::set_key_window(window); + + // Like the Windows and macOS backends, we send a `HiDpiFactorChanged` 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 bounds: CGRect = msg_send![view, bounds]; + let screen: id = msg_send![window, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![view, convertRect:bounds toCoordinateSpace:screen_space]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as _, + 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), + })), + ); + } + Ok(result) } } @@ -370,6 +442,36 @@ impl Inner { msg_send![ self.view_controller, setSupportedInterfaceOrientations: supported_orientations + ] + } + } + + pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + unsafe { + let prefers_home_indicator_hidden = if hidden { YES } else { NO }; + let () = msg_send![ + self.view_controller, + setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden + ]; + } + } + + pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + let edges: UIRectEdge = edges.into(); + unsafe { + let () = msg_send![ + self.view_controller, + setPreferredScreenEdgesDeferringSystemGestures: edges + ]; + } + } + + pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { + unsafe { + let status_bar_hidden = if hidden { YES } else { NO }; + let () = msg_send![ + self.view_controller, + setPrefersStatusBarHidden: status_bar_hidden ]; } } @@ -406,7 +508,7 @@ impl Inner { // requires main thread unsafe fn safe_area_screen_space(&self) -> CGRect { let bounds: CGRect = msg_send![self.window, bounds]; - if self.supports_safe_area { + if app_state::os_capabilities().safe_area { let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets]; let safe_bounds = CGRect { origin: CGPoint { @@ -494,6 +596,9 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub root_view_class: &'static Class, pub hidpi_factor: Option, pub valid_orientations: ValidOrientations, + pub prefers_home_indicator_hidden: bool, + pub prefers_status_bar_hidden: bool, + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, } impl Default for PlatformSpecificWindowBuilderAttributes { @@ -502,6 +607,9 @@ impl Default for PlatformSpecificWindowBuilderAttributes { root_view_class: class!(UIView), hidpi_factor: None, valid_orientations: Default::default(), + prefers_home_indicator_hidden: false, + prefers_status_bar_hidden: false, + preferred_screen_edges_deferring_system_gestures: Default::default(), } } } diff --git a/src/platform_impl/linux/dlopen.rs b/src/platform_impl/linux/dlopen.rs deleted file mode 100644 index 8be592e4..00000000 --- a/src/platform_impl/linux/dlopen.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -#![allow(dead_code)] - -use std::os::raw::{c_char, c_int, c_void}; - -pub const RTLD_LAZY: c_int = 0x001; -pub const RTLD_NOW: c_int = 0x002; - -#[link(name = "dl")] -extern "C" { - pub fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; - pub fn dlerror() -> *mut c_char; - pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; - pub fn dlclose(handle: *mut c_void) -> c_int; -} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 73d9f643..60430a2c 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -1,23 +1,25 @@ #![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, os::raw::*, sync::Arc}; +use std::{collections::VecDeque, env, ffi::CStr, fmt, mem::MaybeUninit, os::raw::*, sync::Arc}; use parking_lot::Mutex; +use raw_window_handle::RawWindowHandle; use smithay_client_toolkit::reexports::client::ConnectError; pub use self::x11::XNotSupported; -use self::x11::{ffi::XVisualInfo, XConnection, XError}; +use self::x11::{ + ffi::XVisualInfo, get_xtarget, util::WindowType as XWindowType, XConnection, XError, +}; use crate::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::Event, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, icon::Icon, - monitor::{MonitorHandle as RootMonitorHandle, VideoMode}, - window::{CursorIcon, WindowAttributes}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; -mod dlopen; pub mod wayland; pub mod x11; @@ -30,7 +32,7 @@ pub mod x11; /// If this variable is set with any other value, winit will panic. const BACKEND_PREFERENCE_ENV_VAR: &str = "WINIT_UNIX_BACKEND"; -#[derive(Clone, Default)] +#[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub visual_infos: Option, pub screen_id: Option, @@ -38,11 +40,27 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub base_size: Option<(u32, u32)>, pub class: Option<(String, String)>, pub override_redirect: bool, - pub x11_window_type: x11::util::WindowType, + pub x11_window_types: Vec, pub gtk_theme_variant: Option, pub app_id: Option, } +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> Self { + Self { + visual_infos: None, + screen_id: None, + resize_increments: None, + base_size: None, + class: None, + override_redirect: false, + x11_window_types: vec![XWindowType::Normal], + gtk_theme_variant: None, + app_id: None, + } + } +} + lazy_static! { pub static ref X11_BACKEND: Mutex, XNotSupported>> = { Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new)) }; @@ -92,7 +110,7 @@ impl DeviceId { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum MonitorHandle { X(x11::MonitorHandle), Wayland(wayland::MonitorHandle), @@ -140,7 +158,7 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> Box> { + pub fn video_modes(&self) -> Box> { match self { MonitorHandle::X(m) => Box::new(m.video_modes()), MonitorHandle::Wayland(m) => Box::new(m.video_modes()), @@ -148,6 +166,46 @@ impl MonitorHandle { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VideoMode { + X(x11::VideoMode), + 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(), + } + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + match self { + &VideoMode::X(ref m) => m.bit_depth(), + &VideoMode::Wayland(ref 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(), + } + } + + #[inline] + pub fn monitor(&self) -> RootMonitorHandle { + match self { + &VideoMode::X(ref m) => m.monitor(), + &VideoMode::Wayland(ref m) => m.monitor(), + } + } +} + impl Window { #[inline] pub fn new( @@ -310,17 +368,15 @@ impl Window { } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { match self { &Window::X(ref w) => w.fullscreen(), - &Window::Wayland(ref w) => w.fullscreen().map(|monitor_id| RootMonitorHandle { - inner: MonitorHandle::Wayland(monitor_id), - }), + &Window::Wayland(ref w) => w.fullscreen(), } } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { + 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), @@ -402,6 +458,13 @@ impl Window { &Window::Wayland(ref window) => MonitorHandle::Wayland(window.primary_monitor()), } } + + pub fn raw_window_handle(&self) -> RawWindowHandle { + match self { + &Window::X(ref window) => RawWindowHandle::X11(window.raw_window_handle()), + &Window::Wayland(ref window) => RawWindowHandle::Wayland(window.raw_window_handle()), + } + } } unsafe extern "C" fn x_error_callback( @@ -410,14 +473,16 @@ unsafe extern "C" fn x_error_callback( ) -> c_int { let xconn_lock = X11_BACKEND.lock(); if let Ok(ref xconn) = *xconn_lock { - let mut buf: [c_char; 1024] = mem::uninitialized(); + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buf: [MaybeUninit; 1024] = MaybeUninit::uninit().assume_init(); (xconn.xlib.XGetErrorText)( display, (*event).error_code as c_int, - buf.as_mut_ptr(), + buf.as_mut_ptr() as *mut c_char, buf.len() as c_int, ); - let description = CStr::from_ptr(buf.as_ptr()).to_string_lossy(); + let description = CStr::from_ptr(buf.as_ptr() as *const c_char).to_string_lossy(); let error = XError { description: description.into_owned(), @@ -439,12 +504,20 @@ pub enum EventLoop { X(x11::EventLoop), } -#[derive(Clone)] pub enum EventLoopProxy { X(x11::EventLoopProxy), 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()), + } + } +} + impl EventLoop { pub fn new() -> EventLoop { if let Ok(env_var) = env::var(BACKEND_PREFERENCE_ENV_VAR) { @@ -502,7 +575,7 @@ impl EventLoop { .into_iter() .map(MonitorHandle::Wayland) .collect(), - EventLoop::X(ref evlp) => evlp + EventLoop::X(ref evlp) => get_xtarget(&evlp.target) .x_connection() .available_monitors() .into_iter() @@ -515,7 +588,9 @@ impl EventLoop { pub fn primary_monitor(&self) -> MonitorHandle { match *self { EventLoop::Wayland(ref evlp) => MonitorHandle::Wayland(evlp.primary_monitor()), - EventLoop::X(ref evlp) => MonitorHandle::X(evlp.x_connection().primary_monitor()), + EventLoop::X(ref evlp) => { + MonitorHandle::X(get_xtarget(&evlp.target).x_connection().primary_monitor()) + } } } @@ -546,14 +621,6 @@ impl EventLoop { } } - #[inline] - pub fn is_wayland(&self) -> bool { - match *self { - EventLoop::Wayland(_) => true, - EventLoop::X(_) => false, - } - } - pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget { match *self { EventLoop::Wayland(ref evl) => evl.window_target(), @@ -576,6 +643,16 @@ pub enum EventLoopWindowTarget { X(x11::EventLoopWindowTarget), } +impl EventLoopWindowTarget { + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + EventLoopWindowTarget::Wayland(_) => true, + EventLoopWindowTarget::X(_) => false, + } + } +} + fn sticky_exit_callback( evt: Event, target: &RootELW, diff --git a/src/platform_impl/linux/wayland/event_loop.rs b/src/platform_impl/linux/wayland/event_loop.rs index 7c3b21a0..272fc832 100644 --- a/src/platform_impl/linux/wayland/event_loop.rs +++ b/src/platform_impl/linux/wayland/event_loop.rs @@ -16,8 +16,11 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize}, event::ModifiersState, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, - monitor::VideoMode, - platform_impl::platform::sticky_exit_callback, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::{ + sticky_exit_callback, MonitorHandle as PlatformMonitorHandle, + VideoMode as PlatformVideoMode, + }, }; use super::{window::WindowStore, DeviceId, WindowId}; @@ -87,7 +90,6 @@ pub struct EventLoop { // 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. -#[derive(Clone)] pub struct EventLoopProxy { user_sender: ::calloop::channel::Sender, } @@ -108,6 +110,14 @@ pub struct EventLoopWindowTarget { _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) @@ -138,7 +148,7 @@ impl EventLoop { let mut seat_manager = SeatManager { sink: sink.clone(), - relative_pointer_manager_proxy: None, + relative_pointer_manager_proxy: Rc::new(RefCell::new(None)), store: store.clone(), seats: seats.clone(), kbd_sender, @@ -154,13 +164,16 @@ impl EventLoop { version, } => { if interface == "zwp_relative_pointer_manager_v1" { - seat_manager.relative_pointer_manager_proxy = Some( - registry - .bind(version, id, move |pointer_manager| { - pointer_manager.implement_closure(|_, _| (), ()) - }) - .unwrap(), - ) + 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 == "wl_seat" { seat_manager.add_seat(id, version, registry) @@ -304,6 +317,25 @@ impl EventLoop { // 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 => { @@ -318,7 +350,12 @@ impl EventLoop { ); } ControlFlow::Wait => { - self.inner_loop.dispatch(None, &mut ()).unwrap(); + 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(), @@ -331,7 +368,7 @@ impl EventLoop { ControlFlow::WaitUntil(deadline) => { let start = Instant::now(); // compute the blocking duration - let duration = if deadline > start { + let duration = if deadline > start && !instant_wakeup { deadline - start } else { ::std::time::Duration::from_millis(0) @@ -380,15 +417,17 @@ impl EventLoop { available_monitors(&self.outputs) } - pub fn display(&self) -> &Display { - &*self.display - } - pub fn window_target(&self) -> &RootELW { &self.window_target } } +impl EventLoopWindowTarget { + pub fn display(&self) -> &Display { + &*self.display + } +} + /* * Private EventLoop Internals */ @@ -457,7 +496,7 @@ struct SeatManager { store: Arc>, seats: Arc>>, kbd_sender: ::calloop::channel::Sender<(crate::event::WindowEvent, super::WindowId)>, - relative_pointer_manager_proxy: Option, + relative_pointer_manager_proxy: Rc>>, } impl SeatManager { @@ -469,7 +508,7 @@ impl SeatManager { store: self.store.clone(), pointer: None, relative_pointer: None, - relative_pointer_manager_proxy: self.relative_pointer_manager_proxy.as_ref().cloned(), + relative_pointer_manager_proxy: self.relative_pointer_manager_proxy.clone(), keyboard: None, touch: None, kbd_sender: self.kbd_sender.clone(), @@ -501,7 +540,7 @@ struct SeatData { kbd_sender: ::calloop::channel::Sender<(crate::event::WindowEvent, super::WindowId)>, pointer: Option, relative_pointer: Option, - relative_pointer_manager_proxy: Option, + relative_pointer_manager_proxy: Rc>>, keyboard: Option, touch: Option, modifiers_tracker: Arc>, @@ -521,17 +560,19 @@ impl SeatData { self.modifiers_tracker.clone(), )); - self.relative_pointer = - self.relative_pointer_manager_proxy - .as_ref() - .and_then(|manager| { - super::pointer::implement_relative_pointer( - self.sink.clone(), - self.pointer.as_ref().unwrap(), - manager, - ) - .ok() - }) + 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) { @@ -603,17 +644,67 @@ impl Drop for SeatData { * 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 Clone for MonitorHandle { - fn clone(&self) -> MonitorHandle { - MonitorHandle { - proxy: self.proxy.clone(), - mgr: self.mgr.clone(), - } +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); } } @@ -680,15 +771,20 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> impl Iterator { + 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(|x| 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, + .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(), + }), }) } } diff --git a/src/platform_impl/linux/wayland/keyboard.rs b/src/platform_impl/linux/wayland/keyboard.rs index 727062ad..4a236691 100644 --- a/src/platform_impl/linux/wayland/keyboard.rs +++ b/src/platform_impl/linux/wayland/keyboard.rs @@ -83,7 +83,17 @@ pub fn init_keyboard( KbEvent::RepeatInfo { .. } => { /* Handled by smithay client toolkit */ } KbEvent::Modifiers { modifiers: event_modifiers, - } => *modifiers_tracker.lock().unwrap() = event_modifiers.into(), + } => { + let modifiers = event_modifiers.into(); + + *modifiers_tracker.lock().unwrap() = modifiers; + + if let Some(wid) = *target.lock().unwrap() { + my_sink + .send((WindowEvent::ModifiersChanged { modifiers }, wid)) + .unwrap(); + } + } } }, move |repeat_event: KeyRepeatEvent, _| { diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index aa96e276..09cd66d1 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -3,7 +3,8 @@ pub use self::{ event_loop::{ - EventLoop, EventLoopProxy, EventLoopWindowTarget, MonitorHandle, WindowEventsSink, + EventLoop, EventLoopProxy, EventLoopWindowTarget, MonitorHandle, VideoMode, + WindowEventsSink, }, window::Window, }; diff --git a/src/platform_impl/linux/wayland/touch.rs b/src/platform_impl/linux/wayland/touch.rs index b72be7b8..33644a86 100644 --- a/src/platform_impl/linux/wayland/touch.rs +++ b/src/platform_impl/linux/wayland/touch.rs @@ -39,6 +39,7 @@ pub(crate) fn implement_touch( ), phase: TouchPhase::Started, location: (x, y).into(), + force: None, // TODO id: id as u64, }), wid, @@ -61,6 +62,7 @@ pub(crate) fn implement_touch( ), phase: TouchPhase::Ended, location: pt.location.into(), + force: None, // TODO id: id as u64, }), pt.wid, @@ -78,6 +80,7 @@ pub(crate) fn implement_touch( ), phase: TouchPhase::Moved, location: (x, y).into(), + force: None, // TODO id: id as u64, }), pt.wid, @@ -94,6 +97,7 @@ pub(crate) fn implement_touch( ), phase: TouchPhase::Cancelled, location: pt.location.into(), + force: None, // TODO id: pt.id as u64, }), pt.wid, diff --git a/src/platform_impl/linux/wayland/window.rs b/src/platform_impl/linux/wayland/window.rs index 773af760..ddd4b4b9 100644 --- a/src/platform_impl/linux/wayland/window.rs +++ b/src/platform_impl/linux/wayland/window.rs @@ -1,3 +1,4 @@ +use raw_window_handle::unix::WaylandHandle; use std::{ collections::VecDeque, sync::{Arc, Mutex, Weak}, @@ -8,10 +9,11 @@ use crate::{ 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, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; use smithay_client_toolkit::{ @@ -25,7 +27,6 @@ use smithay_client_toolkit::{ }; use super::{make_wid, EventLoopWindowTarget, MonitorHandle, WindowId}; -use crate::platform_impl::platform::wayland::event_loop::{available_monitors, primary_monitor}; pub struct Window { surface: wl_surface::WlSurface, @@ -108,13 +109,19 @@ impl Window { } // Check for fullscreen requirements - if let Some(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - }) = attributes.fullscreen - { - frame.set_fullscreen(Some(&monitor_id.proxy)); - } else if attributes.maximized { - frame.set_maximized(); + 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); @@ -252,25 +259,31 @@ impl Window { } } - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { if *(self.fullscreen.lock().unwrap()) { - Some(self.current_monitor()) + Some(Fullscreen::Borderless(RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(self.current_monitor()), + })) } else { None } } - pub fn set_fullscreen(&self, monitor: Option) { - if let Some(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - }) = monitor - { - self.frame - .lock() - .unwrap() - .set_fullscreen(Some(&monitor_id.proxy)); - } else { - self.frame.lock().unwrap().unset_fullscreen(); + 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(), } } @@ -321,6 +334,14 @@ impl Window { 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 { diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 572889b4..3ca469df 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -8,6 +8,8 @@ use super::{ XExtension, }; +use util::modifiers::{ModifierKeyState, ModifierKeymap}; + use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{DeviceEvent, Event, KeyboardInput, ModifiersState, WindowEvent}, @@ -21,6 +23,9 @@ pub(super) struct EventProcessor { pub(super) devices: RefCell>, pub(super) xi2ext: XExtension, pub(super) target: Rc>, + pub(super) mod_keymap: ModifierKeymap, + pub(super) device_mod_state: ModifierKeyState, + pub(super) window_mod_state: ModifierKeyState, } impl EventProcessor { @@ -112,12 +117,22 @@ impl EventProcessor { let event_type = xev.get_type(); match event_type { ffi::MappingNotify => { - unsafe { - (wt.xconn.xlib.XRefreshKeyboardMapping)(xev.as_mut()); + let mapping: &ffi::XMappingEvent = xev.as_ref(); + + if mapping.request == ffi::MappingModifier + || mapping.request == ffi::MappingKeyboard + { + unsafe { + (wt.xconn.xlib.XRefreshKeyboardMapping)(xev.as_mut()); + } + wt.xconn + .check_errors() + .expect("Failed to call XRefreshKeyboardMapping"); + + self.mod_keymap.reset_from_x_connection(&wt.xconn); + self.device_mod_state.update(&self.mod_keymap); + self.window_mod_state.update(&self.mod_keymap); } - wt.xconn - .check_errors() - .expect("Failed to call XRefreshKeyboardMapping"); } ffi::ClientMessage => { @@ -374,7 +389,11 @@ impl EventProcessor { 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; - shared_state_lock.last_monitor = Some(monitor.clone()); + + // Avoid caching an invalid dummy monitor handle + if monitor.id != 0 { + shared_state_lock.last_monitor = Some(monitor.clone()); + } new_hidpi_factor }; if last_hidpi_factor != new_hidpi_factor { @@ -514,13 +533,6 @@ impl EventProcessor { // 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 modifiers = ModifiersState { - alt: xkev.state & ffi::Mod1Mask != 0, - shift: xkev.state & ffi::ShiftMask != 0, - ctrl: xkev.state & ffi::ControlMask != 0, - logo: xkev.state & ffi::Mod4Mask != 0, - }; - let keysym = unsafe { let mut keysym = 0; (wt.xconn.xlib.XLookupString)( @@ -535,6 +547,8 @@ impl EventProcessor { }; let virtual_keycode = events::keysym_to_element(keysym as c_uint); + let modifiers = self.window_mod_state.modifiers(); + callback(Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { @@ -547,6 +561,27 @@ impl EventProcessor { }, }, }); + + if let Some(modifier) = + self.mod_keymap.get_modifier(xkev.keycode as ffi::KeyCode) + { + self.window_mod_state.key_event( + state, + xkev.keycode as ffi::KeyCode, + modifier, + ); + + let new_modifiers = self.window_mod_state.modifiers(); + + if modifiers != new_modifiers { + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged { + modifiers: new_modifiers, + }, + }); + } + } } if state == Pressed { @@ -859,6 +894,21 @@ impl EventProcessor { event: Focused(true), }); + // When focus is gained, send any existing modifiers + // to the window in a ModifiersChanged event. This is + // done to compensate for modifier keys that may be + // changed while a window is out of focus. + if !self.device_mod_state.is_empty() { + self.window_mod_state = self.device_mod_state.clone(); + + let modifiers = self.window_mod_state.modifiers(); + + 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 @@ -890,6 +940,22 @@ impl EventProcessor { .borrow_mut() .unfocus(xev.event) .expect("Failed to unfocus input context"); + + // When focus is lost, send a ModifiersChanged event + // containing no modifiers set. This is done to compensate + // for modifier keys that may be changed while a window + // is out of focus. + if !self.window_mod_state.is_empty() { + self.window_mod_state.clear(); + + callback(Event::WindowEvent { + window_id: mkwid(xev.event), + event: WindowEvent::ModifiersChanged { + modifiers: ModifiersState::default(), + }, + }); + } + callback(Event::WindowEvent { window_id: mkwid(xev.event), event: Focused(false), @@ -918,6 +984,7 @@ impl EventProcessor { device_id: mkdid(xev.deviceid), phase, location, + force: None, // TODO id: xev.detail as u64, }), }) @@ -1021,18 +1088,25 @@ impl EventProcessor { let virtual_keycode = events::keysym_to_element(keysym as c_uint); + if let Some(modifier) = + self.mod_keymap.get_modifier(keycode as ffi::KeyCode) + { + self.device_mod_state.key_event( + state, + keycode as ffi::KeyCode, + modifier, + ); + } + + let modifiers = self.device_mod_state.modifiers(); + callback(Event::DeviceEvent { device_id: mkdid(device_id), event: DeviceEvent::Key(KeyboardInput { scancode, virtual_keycode, state, - // So, in an ideal world we can use libxkbcommon to get modifiers. - // However, libxkbcommon-x11 isn't as commonly installed as one - // would hope. We can still use the Xkb extension to get - // comprehensive keyboard state updates, but interpreting that - // info manually is going to be involved. - modifiers: ModifiersState::default(), + modifiers, }), }); } diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index ba61e719..5b283e68 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -11,7 +11,7 @@ mod window; mod xdisplay; pub use self::{ - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::UnownedWindow, xdisplay::{XConnection, XError, XNotSupported}, }; @@ -20,7 +20,7 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, ffi::CStr, - mem, + mem::{self, MaybeUninit}, ops::Deref, os::raw::*, rc::Rc, @@ -34,6 +34,7 @@ use self::{ dnd::{Dnd, DndState}, event_processor::EventProcessor, ime::{Ime, ImeCreationError, ImeReceiver, ImeSender}, + util::modifiers::ModifierKeymap, }; use crate::{ error::OsError as RootOsError, @@ -60,16 +61,24 @@ pub struct 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>>>, - target: Rc>, + pub(crate) target: Rc>, } -#[derive(Clone)] pub struct EventLoopProxy { user_sender: ::calloop::channel::Sender, } +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_sender: self.user_sender.clone(), + } + } +} + impl EventLoop { pub fn new(xconn: Arc) -> EventLoop { let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; @@ -100,22 +109,21 @@ impl EventLoop { .expect("Failed to query XRandR extension"); let xi2ext = unsafe { - let mut result = XExtension { - opcode: mem::uninitialized(), - first_event_id: mem::uninitialized(), - first_error_id: mem::uninitialized(), - }; + let mut ext = XExtension::default(); + let res = (xconn.xlib.XQueryExtension)( xconn.display, b"XInputExtension\0".as_ptr() as *const c_char, - &mut result.opcode as *mut c_int, - &mut result.first_event_id as *mut c_int, - &mut result.first_error_id as *mut c_int, + &mut ext.opcode, + &mut ext.first_event_id, + &mut ext.first_error_id, ); + if res == ffi::False { panic!("X server missing XInput extension"); } - result + + ext }; unsafe { @@ -136,6 +144,9 @@ impl EventLoop { xconn.update_cached_wm_info(root); + let mut mod_keymap = ModifierKeymap::new(); + mod_keymap.reset_from_x_connection(&xconn); + let target = Rc::new(RootELW { p: super::EventLoopWindowTarget::X(EventLoopWindowTarget { ime, @@ -172,13 +183,16 @@ impl EventLoop { // Handle X11 events let pending_events: Rc>> = Default::default(); - let mut processor = EventProcessor { + let processor = EventProcessor { target: target.clone(), dnd, devices: Default::default(), randr_event_offset, ime_receiver, xi2ext, + mod_keymap, + device_mod_state: Default::default(), + window_mod_state: Default::default(), }; // Register for device hotplug events @@ -190,6 +204,9 @@ impl EventLoop { 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); @@ -198,16 +215,11 @@ impl EventLoop { .handle() .insert_source(x11_events, { let pending_events = pending_events.clone(); - let mut callback = move |event| { - pending_events.borrow_mut().push_back(event); - }; move |evt, &mut ()| { if evt.readiness.is_readable() { - // process all pending events - let mut xev = unsafe { mem::uninitialized() }; - while unsafe { processor.poll_one_event(&mut xev) } { - processor.process_event(&mut xev, &mut callback); - } + let mut processor = processor.borrow_mut(); + let mut pending_events = pending_events.borrow_mut(); + drain_events(&mut processor, &mut pending_events); } } }) @@ -220,18 +232,13 @@ impl EventLoop { _user_source, user_sender, pending_user_events, + event_processor, target, }; result } - /// Returns the `XConnection` of this events loop. - #[inline] - pub fn x_connection(&self) -> &Arc { - &get_xtarget(&self.target).xconn - } - pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { user_sender: self.user_sender.clone(), @@ -249,6 +256,12 @@ impl EventLoop { 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, + ); + loop { // Empty the event buffer { @@ -272,8 +285,10 @@ impl EventLoop { } // Empty the redraw requests { - let mut guard = wt.pending_redraws.lock().unwrap(); - for wid in guard.drain() { + // Release the lock to prevent deadlock + let windows: Vec<_> = wt.pending_redraws.lock().unwrap().drain().collect(); + + for wid in windows { sticky_exit_callback( Event::WindowEvent { window_id: crate::window::WindowId(super::WindowId::X(wid)), @@ -354,6 +369,10 @@ impl EventLoop { } } } + + // If the user callback had any interaction with the X server, + // it may have received and buffered some user input events. + self.drain_events(); } callback( @@ -370,13 +389,43 @@ impl EventLoop { 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 get_xtarget(rt: &RootELW) -> &EventLoopWindowTarget { - if let super::EventLoopWindowTarget::X(ref target) = rt.p { - target - } else { - unreachable!(); +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); + } +} + +pub(crate) fn get_xtarget(target: &RootELW) -> &EventLoopWindowTarget { + match target.p { + super::EventLoopWindowTarget::X(ref target) => target, + _ => unreachable!(), + } +} + +impl EventLoopWindowTarget { + /// Returns the `XConnection` of this events loop. + #[inline] + pub fn x_connection(&self) -> &Arc { + &self.xconn } } @@ -395,19 +444,19 @@ struct DeviceInfo<'a> { impl<'a> DeviceInfo<'a> { fn get(xconn: &'a XConnection, device: c_int) -> Option { unsafe { - let mut count = mem::uninitialized(); + let mut count = 0; let info = (xconn.xinput2.XIQueryDevice)(xconn.display, device, &mut count); - xconn.check_errors().ok().and_then(|_| { - if info.is_null() || count == 0 { - None - } else { - Some(DeviceInfo { - xconn, - info, - count: count as usize, - }) - } - }) + xconn.check_errors().ok()?; + + if info.is_null() || count == 0 { + None + } else { + Some(DeviceInfo { + xconn, + info, + count: count as usize, + }) + } } } } @@ -512,7 +561,7 @@ impl<'a> Drop for GenericEventCookie<'a> { } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone)] struct XExtension { opcode: c_int, first_event_id: c_int, diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 72026257..bb75e806 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -4,47 +4,66 @@ use parking_lot::Mutex; use super::{ ffi::{ - RRCrtcChangeNotifyMask, RROutputPropertyNotifyMask, RRScreenChangeNotifyMask, True, Window, - XRRScreenResources, + RRCrtc, RRCrtcChangeNotifyMask, RRMode, RROutputPropertyNotifyMask, + RRScreenChangeNotifyMask, True, Window, XRRCrtcInfo, XRRScreenResources, }, util, XConnection, XError, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::{MonitorHandle as PlatformMonitorHandle, VideoMode as PlatformVideoMode}, }; -// Used to test XRandR < 1.5 code path. This should always be committed as false. -const FORCE_RANDR_COMPAT: bool = false; -// Also used for testing. This should always be committed as false. +// Used for testing. This should always be committed as false. const DISABLE_MONITOR_LIST_CACHING: bool = false; lazy_static! { - static ref XRANDR_VERSION: Mutex> = Mutex::default(); static ref MONITORS: Mutex>> = Mutex::default(); } -fn version_is_at_least(major: c_int, minor: c_int) -> bool { - if let Some((avail_major, avail_minor)) = *XRANDR_VERSION.lock() { - if avail_major == major { - avail_minor >= minor - } else { - avail_major > major - } - } else { - unreachable!(); - } -} - pub fn invalidate_cached_monitor_list() -> Option> { // We update this lazily. (*MONITORS.lock()).take() } +#[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) native_mode: RRMode, + pub(crate) monitor: Option, +} + +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::X(self.monitor.clone().unwrap()), + } + } +} + #[derive(Debug, Clone)] pub struct MonitorHandle { /// The actual id - id: u32, + pub(crate) id: RRCrtc, /// The name of the monitor pub(crate) name: String, /// The size of the monitor @@ -61,16 +80,43 @@ pub struct MonitorHandle { video_modes: Vec, } +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +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.id.cmp(&other.id) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + impl MonitorHandle { - fn from_repr( + fn new( xconn: &XConnection, resources: *mut XRRScreenResources, - id: u32, - repr: util::MonitorRepr, + id: RRCrtc, + crtc: *mut XRRCrtcInfo, primary: bool, ) -> Option { - let (name, hidpi_factor, video_modes) = unsafe { xconn.get_output_info(resources, &repr)? }; - let (dimensions, position) = unsafe { (repr.size(), repr.position()) }; + let (name, hidpi_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, @@ -84,6 +130,19 @@ impl MonitorHandle { }) } + fn dummy() -> Self { + MonitorHandle { + id: 0, + name: "".into(), + hidpi_factor: 1.0, + dimensions: (1, 1), + position: (0, 0), + primary: true, + rect: util::AaRect::new((0, 0), (1, 1)), + video_modes: Vec::new(), + } + } + pub fn name(&self) -> Option { Some(self.name.clone()) } @@ -107,17 +166,27 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> impl Iterator { - self.video_modes.clone().into_iter() + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.video_modes.clone().into_iter().map(move |mut x| { + x.monitor = Some(monitor.clone()); + RootVideoMode { + video_mode: PlatformVideoMode::X(x), + } + }) } } impl XConnection { pub fn get_monitor_for_window(&self, window_rect: Option) -> MonitorHandle { let monitors = self.available_monitors(); - let default = monitors - .get(0) - .expect("[winit] Failed to find any monitors using XRandR."); + + if monitors.is_empty() { + // Return a dummy monitor to avoid panicking + return MonitorHandle::dummy(); + } + + let default = monitors.get(0).unwrap(); let window_rect = match window_rect { Some(rect) => rect, @@ -139,8 +208,12 @@ impl XConnection { fn query_monitor_list(&self) -> Vec { unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + let root = (self.xlib.XDefaultRootWindow)(self.display); - let resources = if version_is_at_least(1, 3) { + let resources = if (major == 1 && minor >= 3) || major > 1 { (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) } else { // WARNING: this function is supposedly very slow, on the order of hundreds of ms. @@ -155,48 +228,19 @@ impl XConnection { let mut available; let mut has_primary = false; - if self.xrandr_1_5.is_some() && version_is_at_least(1, 5) && !FORCE_RANDR_COMPAT { - // We're in XRandR >= 1.5, enumerate monitors. This supports things like MST and - // videowalls. - let xrandr_1_5 = self.xrandr_1_5.as_ref().unwrap(); - let mut monitor_count = 0; - let monitors = - (xrandr_1_5.XRRGetMonitors)(self.display, root, 1, &mut monitor_count); - assert!(monitor_count >= 0); - available = Vec::with_capacity(monitor_count as usize); - for monitor_index in 0..monitor_count { - let monitor = monitors.offset(monitor_index as isize); - let is_primary = (*monitor).primary != 0; + let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); + available = Vec::with_capacity((*resources).ncrtc as usize); + for crtc_index in 0..(*resources).ncrtc { + let crtc_id = *((*resources).crtcs.offset(crtc_index as isize)); + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let is_active = (*crtc).width > 0 && (*crtc).height > 0 && (*crtc).noutput > 0; + if is_active { + let is_primary = *(*crtc).outputs.offset(0) == primary; has_primary |= is_primary; - MonitorHandle::from_repr( - self, - resources, - monitor_index as u32, - monitor.into(), - is_primary, - ) - .map(|monitor_id| available.push(monitor_id)); - } - (xrandr_1_5.XRRFreeMonitors)(monitors); - } else { - // We're in XRandR < 1.5, enumerate CRTCs. Everything will work except MST and - // videowall setups will also show monitors that aren't in the logical groups the user - // cares about. - let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); - available = Vec::with_capacity((*resources).ncrtc as usize); - for crtc_index in 0..(*resources).ncrtc { - let crtc_id = *((*resources).crtcs.offset(crtc_index as isize)); - let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); - let is_active = (*crtc).width > 0 && (*crtc).height > 0 && (*crtc).noutput > 0; - if is_active { - let crtc = util::MonitorRepr::from(crtc); - let is_primary = crtc.get_output() == primary; - has_primary |= is_primary; - MonitorHandle::from_repr(self, resources, crtc_id as u32, crtc, is_primary) - .map(|monitor_id| available.push(monitor_id)); - } - (self.xrandr.XRRFreeCrtcInfo)(crtc); + MonitorHandle::new(self, resources, crtc_id, crtc, is_primary) + .map(|monitor_id| available.push(monitor_id)); } + (self.xrandr.XRRFreeCrtcInfo)(crtc); } // If no monitors were detected as being primary, we just pick one ourselves! @@ -232,23 +276,19 @@ impl XConnection { self.available_monitors() .into_iter() .find(|monitor| monitor.primary) - .expect("[winit] Failed to find any monitors using XRandR.") + .unwrap_or_else(MonitorHandle::dummy) } pub fn select_xrandr_input(&self, root: Window) -> Result { - { - let mut version_lock = XRANDR_VERSION.lock(); - if version_lock.is_none() { - let mut major = 0; - let mut minor = 0; - let has_extension = - unsafe { (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor) }; - if has_extension != True { - panic!("[winit] XRandR extension not available."); - } - *version_lock = Some((major, minor)); - } - } + let has_xrandr = unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor) + }; + assert!( + has_xrandr == True, + "[winit] XRandR extension not available." + ); let mut event_offset = 0; let mut error_offset = 0; diff --git a/src/platform_impl/linux/x11/util/client_msg.rs b/src/platform_impl/linux/x11/util/client_msg.rs index 4c72665a..3fafcdf9 100644 --- a/src/platform_impl/linux/x11/util/client_msg.rs +++ b/src/platform_impl/linux/x11/util/client_msg.rs @@ -30,13 +30,17 @@ impl XConnection { event_mask: Option, data: ClientMsgPayload, ) -> Flusher<'_> { - let mut event: ffi::XClientMessageEvent = unsafe { mem::uninitialized() }; - event.type_ = ffi::ClientMessage; - event.display = self.display; - event.window = window; - event.message_type = message_type; - event.format = c_long::FORMAT as c_int; - event.data = unsafe { mem::transmute(data) }; + let event = ffi::XClientMessageEvent { + type_: ffi::ClientMessage, + display: self.display, + window, + message_type, + format: c_long::FORMAT as c_int, + data: unsafe { mem::transmute(data) }, + // These fields are ignored by `XSendEvent` + serial: 0, + send_event: 0, + }; self.send_event(target_window, event_mask, event) } @@ -54,12 +58,17 @@ impl XConnection { 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 = unsafe { mem::uninitialized() }; - event.type_ = ffi::ClientMessage; - event.display = self.display; - event.window = window; - event.message_type = message_type; - event.format = format as c_int; + 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); diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs new file mode 100644 index 00000000..684af49d --- /dev/null +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -0,0 +1,129 @@ +use crate::window::CursorIcon; + +use super::*; + +impl XConnection { + pub fn set_cursor_icon(&self, window: ffi::Window, cursor: Option) { + let cursor = *self + .cursor_cache + .lock() + .entry(cursor) + .or_insert_with(|| self.get_cursor(cursor)); + + self.update_cursor(window, cursor); + } + + fn create_empty_cursor(&self) -> ffi::Cursor { + let data = 0; + let pixmap = unsafe { + let screen = (self.xlib.XDefaultScreen)(self.display); + let window = (self.xlib.XRootWindow)(self.display, screen); + (self.xlib.XCreateBitmapFromData)(self.display, window, &data, 1, 1) + }; + + if pixmap == 0 { + panic!("failed to allocate pixmap for cursor"); + } + + unsafe { + // We don't care about this color, since it only fills bytes + // in the pixmap which are not 0 in the mask. + let mut dummy_color = MaybeUninit::uninit(); + let cursor = (self.xlib.XCreatePixmapCursor)( + self.display, + pixmap, + pixmap, + dummy_color.as_mut_ptr(), + dummy_color.as_mut_ptr(), + 0, + 0, + ); + (self.xlib.XFreePixmap)(self.display, pixmap); + + cursor + } + } + + fn load_cursor(&self, name: &[u8]) -> ffi::Cursor { + unsafe { + (self.xcursor.XcursorLibraryLoadCursor)(self.display, name.as_ptr() as *const c_char) + } + } + + fn load_first_existing_cursor(&self, names: &[&[u8]]) -> ffi::Cursor { + for name in names.iter() { + let xcursor = self.load_cursor(name); + if xcursor != 0 { + return xcursor; + } + } + 0 + } + + fn get_cursor(&self, cursor: Option) -> ffi::Cursor { + let cursor = match cursor { + Some(cursor) => cursor, + None => return self.create_empty_cursor(), + }; + + let load = |name: &[u8]| self.load_cursor(name); + + let loadn = |names: &[&[u8]]| self.load_first_existing_cursor(names); + + // Try multiple names in some cases where the name + // differs on the desktop environments or themes. + // + // Try the better looking (or more suiting) names first. + match cursor { + CursorIcon::Alias => load(b"link\0"), + CursorIcon::Arrow => load(b"arrow\0"), + CursorIcon::Cell => load(b"plus\0"), + CursorIcon::Copy => load(b"copy\0"), + CursorIcon::Crosshair => load(b"crosshair\0"), + CursorIcon::Default => load(b"left_ptr\0"), + CursorIcon::Hand => loadn(&[b"hand2\0", b"hand1\0"]), + CursorIcon::Help => load(b"question_arrow\0"), + CursorIcon::Move => load(b"move\0"), + CursorIcon::Grab => loadn(&[b"openhand\0", b"grab\0"]), + CursorIcon::Grabbing => loadn(&[b"closedhand\0", b"grabbing\0"]), + CursorIcon::Progress => load(b"left_ptr_watch\0"), + CursorIcon::AllScroll => load(b"all-scroll\0"), + CursorIcon::ContextMenu => load(b"context-menu\0"), + + CursorIcon::NoDrop => loadn(&[b"no-drop\0", b"circle\0"]), + CursorIcon::NotAllowed => load(b"crossed_circle\0"), + + // Resize cursors + CursorIcon::EResize => load(b"right_side\0"), + CursorIcon::NResize => load(b"top_side\0"), + CursorIcon::NeResize => load(b"top_right_corner\0"), + CursorIcon::NwResize => load(b"top_left_corner\0"), + CursorIcon::SResize => load(b"bottom_side\0"), + CursorIcon::SeResize => load(b"bottom_right_corner\0"), + CursorIcon::SwResize => load(b"bottom_left_corner\0"), + CursorIcon::WResize => load(b"left_side\0"), + CursorIcon::EwResize => load(b"h_double_arrow\0"), + CursorIcon::NsResize => load(b"v_double_arrow\0"), + CursorIcon::NwseResize => loadn(&[b"bd_double_arrow\0", b"size_bdiag\0"]), + CursorIcon::NeswResize => loadn(&[b"fd_double_arrow\0", b"size_fdiag\0"]), + CursorIcon::ColResize => loadn(&[b"split_h\0", b"h_double_arrow\0"]), + CursorIcon::RowResize => loadn(&[b"split_v\0", b"v_double_arrow\0"]), + + CursorIcon::Text => loadn(&[b"text\0", b"xterm\0"]), + CursorIcon::VerticalText => load(b"vertical-text\0"), + + CursorIcon::Wait => load(b"watch\0"), + + CursorIcon::ZoomIn => load(b"zoom-in\0"), + CursorIcon::ZoomOut => load(b"zoom-out\0"), + } + } + + fn update_cursor(&self, window: ffi::Window, cursor: ffi::Cursor) { + unsafe { + (self.xlib.XDefineCursor)(self.display, window, cursor); + + self.flush_requests().expect("Failed to set the cursor"); + } + } +} diff --git a/src/platform_impl/linux/x11/util/geometry.rs b/src/platform_impl/linux/x11/util/geometry.rs index e66606af..6b59d13a 100644 --- a/src/platform_impl/linux/x11/util/geometry.rs +++ b/src/platform_impl/linux/x11/util/geometry.rs @@ -41,14 +41,14 @@ impl AaRect { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct TranslatedCoords { pub x_rel_root: c_int, pub y_rel_root: c_int, pub child: ffi::Window, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Geometry { pub root: ffi::Window, // If you want positions relative to the root window, use translate_coords. @@ -183,7 +183,8 @@ impl XConnection { window: ffi::Window, root: ffi::Window, ) -> Result { - let mut translated_coords: TranslatedCoords = unsafe { mem::uninitialized() }; + let mut coords = TranslatedCoords::default(); + unsafe { (self.xlib.XTranslateCoordinates)( self.display, @@ -191,18 +192,20 @@ impl XConnection { root, 0, 0, - &mut translated_coords.x_rel_root, - &mut translated_coords.y_rel_root, - &mut translated_coords.child, + &mut coords.x_rel_root, + &mut coords.y_rel_root, + &mut coords.child, ); } - //println!("XTranslateCoordinates coords:{:?}", translated_coords); - self.check_errors().map(|_| translated_coords) + + self.check_errors()?; + Ok(coords) } // This is adequate for inner_size pub fn get_geometry(&self, window: ffi::Window) -> Result { - let mut geometry: Geometry = unsafe { mem::uninitialized() }; + let mut geometry = Geometry::default(); + let _status = unsafe { (self.xlib.XGetGeometry)( self.display, @@ -216,8 +219,9 @@ impl XConnection { &mut geometry.depth, ) }; - //println!("XGetGeometry geo:{:?}", geometry); - self.check_errors().map(|_| geometry) + + self.check_errors()?; + Ok(geometry) } fn get_frame_extents(&self, window: ffi::Window) -> Option { @@ -264,10 +268,10 @@ impl XConnection { fn get_parent_window(&self, window: ffi::Window) -> Result { let parent = unsafe { - let mut root: ffi::Window = mem::uninitialized(); - let mut parent: ffi::Window = mem::uninitialized(); + let mut root = 0; + let mut parent = 0; let mut children: *mut ffi::Window = ptr::null_mut(); - let mut nchildren: c_uint = mem::uninitialized(); + let mut nchildren = 0; // What's filled into `parent` if `window` is the root window? let _status = (self.xlib.XQueryTree)( diff --git a/src/platform_impl/linux/x11/util/hint.rs b/src/platform_impl/linux/x11/util/hint.rs index a71d75e0..94c7e33a 100644 --- a/src/platform_impl/linux/x11/util/hint.rs +++ b/src/platform_impl/linux/x11/util/hint.rs @@ -72,21 +72,21 @@ impl Default for WindowType { impl WindowType { pub(crate) fn as_atom(&self, xconn: &Arc) -> ffi::Atom { use self::WindowType::*; - let atom_name: &[u8] = match self { - &Desktop => b"_NET_WM_WINDOW_TYPE_DESKTOP\0", - &Dock => b"_NET_WM_WINDOW_TYPE_DOCK\0", - &Toolbar => b"_NET_WM_WINDOW_TYPE_TOOLBAR\0", - &Menu => b"_NET_WM_WINDOW_TYPE_MENU\0", - &Utility => b"_NET_WM_WINDOW_TYPE_UTILITY\0", - &Splash => b"_NET_WM_WINDOW_TYPE_SPLASH\0", - &Dialog => b"_NET_WM_WINDOW_TYPE_DIALOG\0", - &DropdownMenu => b"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU\0", - &PopupMenu => b"_NET_WM_WINDOW_TYPE_POPUP_MENU\0", - &Tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP\0", - &Notification => b"_NET_WM_WINDOW_TYPE_NOTIFICATION\0", - &Combo => b"_NET_WM_WINDOW_TYPE_COMBO\0", - &Dnd => b"_NET_WM_WINDOW_TYPE_DND\0", - &Normal => b"_NET_WM_WINDOW_TYPE_NORMAL\0", + let atom_name: &[u8] = match *self { + Desktop => b"_NET_WM_WINDOW_TYPE_DESKTOP\0", + Dock => b"_NET_WM_WINDOW_TYPE_DOCK\0", + Toolbar => b"_NET_WM_WINDOW_TYPE_TOOLBAR\0", + Menu => b"_NET_WM_WINDOW_TYPE_MENU\0", + Utility => b"_NET_WM_WINDOW_TYPE_UTILITY\0", + Splash => b"_NET_WM_WINDOW_TYPE_SPLASH\0", + Dialog => b"_NET_WM_WINDOW_TYPE_DIALOG\0", + DropdownMenu => b"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU\0", + PopupMenu => b"_NET_WM_WINDOW_TYPE_POPUP_MENU\0", + Tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP\0", + Notification => b"_NET_WM_WINDOW_TYPE_NOTIFICATION\0", + Combo => b"_NET_WM_WINDOW_TYPE_COMBO\0", + Dnd => b"_NET_WM_WINDOW_TYPE_DND\0", + Normal => b"_NET_WM_WINDOW_TYPE_NORMAL\0", }; unsafe { xconn.get_atom_unchecked(atom_name) } } @@ -317,13 +317,13 @@ impl XConnection { pub fn get_normal_hints(&self, window: ffi::Window) -> Result, XError> { let size_hints = self.alloc_size_hints(); - let mut supplied_by_user: c_long = unsafe { mem::uninitialized() }; + let mut supplied_by_user = MaybeUninit::uninit(); unsafe { (self.xlib.XGetWMNormalHints)( self.display, window, size_hints.ptr, - &mut supplied_by_user, + supplied_by_user.as_mut_ptr(), ); } self.check_errors().map(|_| NormalHints { size_hints }) diff --git a/src/platform_impl/linux/x11/util/input.rs b/src/platform_impl/linux/x11/util/input.rs index 8f2e9833..1f02090d 100644 --- a/src/platform_impl/linux/x11/util/input.rs +++ b/src/platform_impl/linux/x11/util/input.rs @@ -1,4 +1,4 @@ -use std::str; +use std::{slice, str}; use super::*; use crate::event::ModifiersState; @@ -23,18 +23,19 @@ impl From for ModifiersState { } } +// NOTE: Some of these fields are not used, but may be of use in the future. pub struct PointerState<'a> { xconn: &'a XConnection, - root: ffi::Window, - child: ffi::Window, + pub root: ffi::Window, + pub child: ffi::Window, pub root_x: c_double, pub root_y: c_double, - win_x: c_double, - win_y: c_double, + pub win_x: c_double, + pub win_y: c_double, buttons: ffi::XIButtonState, modifiers: ffi::XIModifierState, - group: ffi::XIGroupState, - relative_to_window: bool, + pub group: ffi::XIGroupState, + pub relative_to_window: bool, } impl<'a> PointerState<'a> { @@ -93,29 +94,46 @@ impl XConnection { device_id: c_int, ) -> Result, XError> { unsafe { - let mut pointer_state: PointerState<'_> = mem::uninitialized(); - pointer_state.xconn = self; - pointer_state.relative_to_window = (self.xinput2.XIQueryPointer)( + let mut root = 0; + let mut child = 0; + let mut root_x = 0.0; + let mut root_y = 0.0; + let mut win_x = 0.0; + let mut win_y = 0.0; + let mut buttons = Default::default(); + let mut modifiers = Default::default(); + let mut group = Default::default(); + + let relative_to_window = (self.xinput2.XIQueryPointer)( self.display, device_id, window, - &mut pointer_state.root, - &mut pointer_state.child, - &mut pointer_state.root_x, - &mut pointer_state.root_y, - &mut pointer_state.win_x, - &mut pointer_state.win_y, - &mut pointer_state.buttons, - &mut pointer_state.modifiers, - &mut pointer_state.group, + &mut root, + &mut child, + &mut root_x, + &mut root_y, + &mut win_x, + &mut win_y, + &mut buttons, + &mut modifiers, + &mut group, ) == ffi::True; - if let Err(err) = self.check_errors() { - // Running the destrutor would be bad news for us... - mem::forget(pointer_state); - Err(err) - } else { - Ok(pointer_state) - } + + self.check_errors()?; + + Ok(PointerState { + xconn: self, + root, + child, + root_x, + root_y, + win_x, + win_y, + buttons, + modifiers, + group, + relative_to_window, + }) } } @@ -123,7 +141,8 @@ impl XConnection { &self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent, - buffer: &mut [u8], + buffer: *mut u8, + size: usize, ) -> (ffi::KeySym, ffi::Status, c_int) { let mut keysym: ffi::KeySym = 0; let mut status: ffi::Status = 0; @@ -131,8 +150,8 @@ impl XConnection { (self.xlib.Xutf8LookupString)( ic, key_event, - buffer.as_mut_ptr() as *mut c_char, - buffer.len() as c_int, + buffer as *mut c_char, + size as c_int, &mut keysym, &mut status, ) @@ -141,21 +160,28 @@ impl XConnection { } pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String { - let mut buffer: [u8; TEXT_BUFFER_SIZE] = unsafe { mem::uninitialized() }; - let (_, status, count) = self.lookup_utf8_inner(ic, key_event, &mut buffer); - // The buffer overflowed, so we'll make a new one on the heap. - if status == ffi::XBufferOverflow { - let mut buffer = Vec::with_capacity(count as usize); - unsafe { buffer.set_len(count as usize) }; - let (_, _, new_count) = self.lookup_utf8_inner(ic, key_event, &mut buffer); + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buffer: [MaybeUninit; TEXT_BUFFER_SIZE] = + unsafe { MaybeUninit::uninit().assume_init() }; + // If the buffer overflows, we'll make a new one on the heap. + let mut vec; + + let (_, status, count) = + self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len()); + + let bytes = if status == ffi::XBufferOverflow { + vec = Vec::with_capacity(count as usize); + let (_, _, new_count) = + self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity()); debug_assert_eq!(count, new_count); - str::from_utf8(&buffer[..count as usize]) - .unwrap_or("") - .to_string() + + unsafe { vec.set_len(count as usize) }; + &vec[..count as usize] } else { - str::from_utf8(&buffer[..count as usize]) - .unwrap_or("") - .to_string() - } + unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) } + }; + + str::from_utf8(bytes).unwrap_or("").to_string() } } diff --git a/src/platform_impl/linux/x11/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index 920205be..1410da28 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -3,12 +3,14 @@ mod atom; mod client_msg; +mod cursor; mod format; mod geometry; mod hint; mod icon; mod input; mod memory; +pub mod modifiers; mod randr; mod window_property; mod wm; @@ -18,7 +20,12 @@ pub use self::{ randr::*, window_property::*, wm::*, }; -use std::{mem, ops::BitAnd, os::raw::*, ptr}; +use std::{ + mem::{self, MaybeUninit}, + ops::BitAnd, + os::raw::*, + ptr, +}; use super::{ffi, XConnection, XError}; diff --git a/src/platform_impl/linux/x11/util/modifiers.rs b/src/platform_impl/linux/x11/util/modifiers.rs new file mode 100644 index 00000000..9b8da4b7 --- /dev/null +++ b/src/platform_impl/linux/x11/util/modifiers.rs @@ -0,0 +1,149 @@ +use std::{collections::HashMap, slice}; + +use super::*; + +use crate::event::{ElementState, ModifiersState}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const SHIFT_OFFSET: usize = 0; +const CONTROL_OFFSET: usize = 2; +const ALT_OFFSET: usize = 3; +const LOGO_OFFSET: usize = 6; +const NUM_MODS: usize = 8; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Modifier { + Alt, + Ctrl, + Shift, + Logo, +} + +#[derive(Debug, Default)] +pub struct ModifierKeymap { + // Maps keycodes to modifiers + keys: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub struct ModifierKeyState { + // Contains currently pressed modifier keys and their corresponding modifiers + keys: HashMap, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn get_modifier(&self, keycode: ffi::KeyCode) -> Option { + self.keys.get(&keycode).cloned() + } + + pub fn reset_from_x_connection(&mut self, xconn: &XConnection) { + unsafe { + let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + + if keymap.is_null() { + panic!("failed to allocate XModifierKeymap"); + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + pub fn reset_from_x_keymap(&mut self, keymap: &ffi::XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + + self.keys.clear(); + + self.read_x_keys(keys, SHIFT_OFFSET, keys_per_mod, Modifier::Shift); + self.read_x_keys(keys, CONTROL_OFFSET, keys_per_mod, Modifier::Ctrl); + self.read_x_keys(keys, ALT_OFFSET, keys_per_mod, Modifier::Alt); + self.read_x_keys(keys, LOGO_OFFSET, keys_per_mod, Modifier::Logo); + } + + fn read_x_keys( + &mut self, + keys: &[ffi::KeyCode], + offset: usize, + keys_per_mod: usize, + modifier: Modifier, + ) { + let start = offset * keys_per_mod; + let end = start + keys_per_mod; + + for &keycode in &keys[start..end] { + if keycode != 0 { + self.keys.insert(keycode, modifier); + } + } + } +} + +impl ModifierKeyState { + pub fn clear(&mut self) { + self.keys.clear(); + } + + pub fn is_empty(&self) -> bool { + self.keys.is_empty() + } + + pub fn update(&mut self, mods: &ModifierKeymap) { + self.keys.retain(|k, v| { + if let Some(m) = mods.get_modifier(*k) { + *v = m; + true + } else { + false + } + }); + } + + pub fn modifiers(&self) -> ModifiersState { + let mut state = ModifiersState::default(); + + for &m in self.keys.values() { + set_modifier(&mut state, m); + } + + state + } + + pub fn key_event(&mut self, state: ElementState, keycode: ffi::KeyCode, modifier: Modifier) { + match state { + ElementState::Pressed => self.key_press(keycode, modifier), + ElementState::Released => self.key_release(keycode), + } + } + + pub fn key_press(&mut self, keycode: ffi::KeyCode, modifier: Modifier) { + self.keys.insert(keycode, modifier); + } + + pub fn key_release(&mut self, keycode: ffi::KeyCode) { + self.keys.remove(&keycode); + } +} + +fn set_modifier(state: &mut ModifiersState, modifier: Modifier) { + match modifier { + Modifier::Alt => state.alt = true, + Modifier::Ctrl => state.ctrl = true, + Modifier::Shift => state.shift = true, + Modifier::Logo => state.logo = true, + } +} diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 1e9a41c6..1fbef41d 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,7 +1,10 @@ use std::{env, slice, str::FromStr}; -use super::*; -use crate::{dpi::validate_hidpi_factor, monitor::VideoMode}; +use super::{ + ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, + *, +}; +use crate::{dpi::validate_hidpi_factor, platform_impl::platform::x11::VideoMode}; pub fn calc_dpi_factor( (width_px, height_px): (u32, u32), @@ -34,47 +37,6 @@ pub fn calc_dpi_factor( dpi_factor } -pub enum MonitorRepr { - Monitor(*mut ffi::XRRMonitorInfo), - Crtc(*mut ffi::XRRCrtcInfo), -} - -impl MonitorRepr { - pub unsafe fn get_output(&self) -> ffi::RROutput { - match *self { - // Same member names, but different locations within the struct... - MonitorRepr::Monitor(monitor) => *((*monitor).outputs.offset(0)), - MonitorRepr::Crtc(crtc) => *((*crtc).outputs.offset(0)), - } - } - - pub unsafe fn size(&self) -> (u32, u32) { - match *self { - MonitorRepr::Monitor(monitor) => ((*monitor).width as u32, (*monitor).height as u32), - MonitorRepr::Crtc(crtc) => ((*crtc).width as u32, (*crtc).height as u32), - } - } - - pub unsafe fn position(&self) -> (i32, i32) { - match *self { - MonitorRepr::Monitor(monitor) => ((*monitor).x as i32, (*monitor).y as i32), - MonitorRepr::Crtc(crtc) => ((*crtc).x as i32, (*crtc).y as i32), - } - } -} - -impl From<*mut ffi::XRRMonitorInfo> for MonitorRepr { - fn from(monitor: *mut ffi::XRRMonitorInfo) -> Self { - MonitorRepr::Monitor(monitor) - } -} - -impl From<*mut ffi::XRRCrtcInfo> for MonitorRepr { - fn from(crtc: *mut ffi::XRRCrtcInfo) -> Self { - MonitorRepr::Crtc(crtc) - } -} - impl XConnection { // Retrieve DPI from Xft.dpi property pub unsafe fn get_xft_dpi(&self) -> Option { @@ -96,11 +58,11 @@ impl XConnection { } pub unsafe fn get_output_info( &self, - resources: *mut ffi::XRRScreenResources, - repr: &MonitorRepr, + resources: *mut XRRScreenResources, + crtc: *mut XRRCrtcInfo, ) -> Option<(String, f64, Vec)> { let output_info = - (self.xrandr.XRRGetOutputInfo)(self.display, resources, repr.get_output()); + (self.xrandr.XRRGetOutputInfo)(self.display, resources, *(*crtc).outputs.offset(0)); if output_info.is_null() { // When calling `XRRGetOutputInfo` on a virtual monitor (versus a physical display) // it's possible for it to return null. @@ -132,6 +94,10 @@ impl XConnection { size: (x.width, x.height), refresh_rate: (refresh_rate as f32 / 1000.0).round() as u16, bit_depth: bit_depth as u16, + native_mode: x.id, + // This is populated in `MonitorHandle::video_modes` as the + // video mode is returned to the user + monitor: None, } }); @@ -144,7 +110,7 @@ impl XConnection { dpi / 96. } else { calc_dpi_factor( - repr.size(), + ((*crtc).width as u32, (*crtc).height as u32), ( (*output_info).mm_width as u64, (*output_info).mm_height as u64, @@ -155,4 +121,61 @@ impl XConnection { (self.xrandr.XRRFreeOutputInfo)(output_info); Some((name, hidpi_factor, modes.collect())) } + pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Result<(), ()> { + unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + + let root = (self.xlib.XDefaultRootWindow)(self.display); + let resources = if (major == 1 && minor >= 3) || major > 1 { + (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) + } else { + (self.xrandr.XRRGetScreenResources)(self.display, root) + }; + + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let status = (self.xrandr.XRRSetCrtcConfig)( + self.display, + resources, + crtc_id, + CurrentTime, + (*crtc).x, + (*crtc).y, + mode_id, + (*crtc).rotation, + (*crtc).outputs.offset(0), + 1, + ); + + (self.xrandr.XRRFreeCrtcInfo)(crtc); + (self.xrandr.XRRFreeScreenResources)(resources); + + if status == Success as i32 { + Ok(()) + } else { + Err(()) + } + } + } + pub fn get_crtc_mode(&self, crtc_id: RRCrtc) -> RRMode { + unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + + let root = (self.xlib.XDefaultRootWindow)(self.display); + let resources = if (major == 1 && minor >= 3) || major > 1 { + (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) + } else { + (self.xrandr.XRRGetScreenResources)(self.display, root) + }; + + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let mode = (*crtc).mode; + (self.xrandr.XRRFreeCrtcInfo)(crtc); + (self.xrandr.XRRFreeScreenResources)(resources); + mode + } + } } diff --git a/src/platform_impl/linux/x11/util/window_property.rs b/src/platform_impl/linux/x11/util/window_property.rs index d719ea70..0b9e7a71 100644 --- a/src/platform_impl/linux/x11/util/window_property.rs +++ b/src/platform_impl/linux/x11/util/window_property.rs @@ -43,13 +43,14 @@ impl XConnection { let mut offset = 0; let mut done = false; + let mut actual_type = 0; + let mut actual_format = 0; + let mut quantity_returned = 0; + let mut bytes_after = 0; + let mut buf: *mut c_uchar = ptr::null_mut(); + while !done { unsafe { - let mut actual_type: ffi::Atom = mem::uninitialized(); - let mut actual_format: c_int = mem::uninitialized(); - let mut quantity_returned: c_ulong = mem::uninitialized(); - let mut bytes_after: c_ulong = mem::uninitialized(); - let mut buf: *mut c_uchar = ptr::null_mut(); (self.xlib.XGetWindowProperty)( self.display, window, diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 2211c29b..cf7759ec 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1,4 +1,15 @@ -use std::{cmp, collections::HashSet, env, ffi::CString, mem, os::raw::*, path::Path, sync::Arc}; +use raw_window_handle::unix::X11Handle; +use std::{ + cmp, + collections::HashSet, + env, + ffi::CString, + mem::{self, replace, MaybeUninit}, + os::raw::*, + path::Path, + ptr, slice, + sync::Arc, +}; use libc; use parking_lot::Mutex; @@ -6,12 +17,13 @@ use parking_lot::Mutex; use crate::{ dpi::{LogicalPosition, LogicalSize}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, - monitor::MonitorHandle as RootMonitorHandle, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::{ x11::{ime::ImeContextCreationError, MonitorHandle as X11MonitorHandle}, MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, + VideoMode as PlatformVideoMode, }, - window::{CursorIcon, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, }; use super::{ffi, util, EventLoopWindowTarget, ImeSender, WindowId, XConnection, XError}; @@ -36,18 +48,22 @@ pub struct SharedState { pub guessed_dpi: Option, pub last_monitor: Option, pub dpi_adjusted: Option<(f64, f64)>, - pub fullscreen: Option, - // Used to restore position after exiting fullscreen. + pub fullscreen: Option, + // Used to restore position after exiting fullscreen pub restore_position: Option<(i32, i32)>, + // Used to restore video mode after exiting fullscreen + pub desktop_video_mode: Option<(ffi::RRCrtc, ffi::RRMode)>, pub frame_extents: Option, pub min_inner_size: Option, pub max_inner_size: Option, + pub is_visible: bool, } impl SharedState { - fn new(dpi_factor: f64) -> Mutex { + fn new(dpi_factor: f64, is_visible: bool) -> Mutex { let mut shared_state = SharedState::default(); shared_state.guessed_dpi = Some(dpi_factor); + shared_state.is_visible = is_visible; Mutex::new(shared_state) } } @@ -103,7 +119,7 @@ impl UnownedWindow { .unwrap_or(1.0) }) } else { - return Err(os_error!(OsError::XMisc("No monitors were detected."))); + 1.0 }; info!("Guessed window DPI factor: {}", dpi_factor); @@ -216,7 +232,7 @@ impl UnownedWindow { cursor_grabbed: Mutex::new(false), cursor_visible: Mutex::new(true), ime_sender: Mutex::new(event_loop.ime_sender.clone()), - shared_state: SharedState::new(dpi_factor), + shared_state: SharedState::new(dpi_factor, window_attrs.visible), pending_redraws: event_loop.pending_redraws.clone(), }; @@ -282,9 +298,7 @@ impl UnownedWindow { window.set_pid().map(|flusher| flusher.queue()); - if pl_attribs.x11_window_type != Default::default() { - window.set_window_type(pl_attribs.x11_window_type).queue(); - } + window.set_window_types(pl_attribs.x11_window_types).queue(); if let Some(variant) = pl_attribs.gtk_theme_variant { window.set_gtk_theme_variant(variant).queue(); @@ -341,6 +355,8 @@ impl UnownedWindow { unsafe { (xconn.xlib.XMapRaised)(xconn.display, window.xwindow); } //.queue(); + + window.wait_for_visibility_notify(); } // Attempt to make keyboard input repeat detectable @@ -398,6 +414,7 @@ impl UnownedWindow { if window_attrs.fullscreen.is_some() { window .set_fullscreen_inner(window_attrs.fullscreen.clone()) + .unwrap() .queue(); } if window_attrs.always_on_top { @@ -405,27 +422,6 @@ impl UnownedWindow { .set_always_on_top_inner(window_attrs.always_on_top) .queue(); } - - if window_attrs.visible { - unsafe { - // XSetInputFocus generates an error if the window is not visible, so we wait - // until we receive VisibilityNotify. - let mut event = mem::uninitialized(); - (xconn.xlib.XIfEvent)( - // This will flush the request buffer IF it blocks. - xconn.display, - &mut event as *mut ffi::XEvent, - Some(visibility_predicate), - window.xwindow as _, - ); - (xconn.xlib.XSetInputFocus)( - xconn.display, - window.xwindow, - ffi::RevertToParent, - ffi::CurrentTime, - ); - } - } } // We never want to give the user a broken window, since by then, it's too late to handle. @@ -449,19 +445,22 @@ impl UnownedWindow { let pid_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_PID\0") }; let client_machine_atom = unsafe { self.xconn.get_atom_unchecked(b"WM_CLIENT_MACHINE\0") }; unsafe { - let (hostname, hostname_length) = { - // 64 would suffice for Linux, but 256 will be enough everywhere (as per SUSv2). For instance, this is - // the limit defined by OpenBSD. - const MAXHOSTNAMELEN: usize = 256; - let mut hostname: [c_char; MAXHOSTNAMELEN] = mem::uninitialized(); - let status = libc::gethostname(hostname.as_mut_ptr(), hostname.len()); - if status != 0 { - return None; - } - hostname[MAXHOSTNAMELEN - 1] = '\0' as c_char; // a little extra safety - let hostname_length = libc::strlen(hostname.as_ptr()); - (hostname, hostname_length as usize) - }; + // 64 would suffice for Linux, but 256 will be enough everywhere (as per SUSv2). For instance, this is + // the limit defined by OpenBSD. + const MAXHOSTNAMELEN: usize = 256; + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buffer: [MaybeUninit; MAXHOSTNAMELEN] = + MaybeUninit::uninit().assume_init(); + let status = libc::gethostname(buffer.as_mut_ptr() as *mut c_char, buffer.len()); + if status != 0 { + return None; + } + ptr::write(buffer[MAXHOSTNAMELEN - 1].as_mut_ptr() as *mut u8, b'\0'); // a little extra safety + let hostname_length = libc::strlen(buffer.as_ptr() as *const c_char); + + let hostname = slice::from_raw_parts(buffer.as_ptr() as *const c_char, hostname_length); + self.xconn .change_property( self.xwindow, @@ -482,15 +481,19 @@ impl UnownedWindow { } } - fn set_window_type(&self, window_type: util::WindowType) -> util::Flusher<'_> { + fn set_window_types(&self, window_types: Vec) -> util::Flusher<'_> { let hint_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_WINDOW_TYPE\0") }; - let window_type_atom = window_type.as_atom(&self.xconn); + let atoms: Vec<_> = window_types + .iter() + .map(|t| t.as_atom(&self.xconn)) + .collect(); + self.xconn.change_property( self.xwindow, hint_atom, ffi::XA_ATOM, util::PropMode::Replace, - &[window_type_atom], + &atoms, ) } @@ -548,44 +551,151 @@ impl UnownedWindow { fn set_fullscreen_hint(&self, fullscreen: bool) -> util::Flusher<'_> { let fullscreen_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE_FULLSCREEN\0") }; - self.set_netwm(fullscreen.into(), (fullscreen_atom as c_long, 0, 0, 0)) + let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom as c_long, 0, 0, 0)); + + if fullscreen { + // Ensure that the fullscreen window receives input focus to prevent + // locking up the user's display. + unsafe { + (self.xconn.xlib.XSetInputFocus)( + self.xconn.display, + self.xwindow, + ffi::RevertToParent, + ffi::CurrentTime, + ); + } + } + + flusher } - fn set_fullscreen_inner(&self, monitor: Option) -> util::Flusher<'_> { - match monitor { + fn set_fullscreen_inner(&self, fullscreen: Option) -> Option> { + let mut shared_state_lock = self.shared_state.lock(); + + if !shared_state_lock.is_visible { + // Setting fullscreen on a window that is not visible will generate an error. + return None; + } + + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if old_fullscreen == fullscreen { + return None; + } + shared_state_lock.fullscreen = fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Store the desktop video mode before entering exclusive + // fullscreen, so we can restore it upon exit, as XRandR does not + // provide a mechanism to set this per app-session or restore this + // to the desktop video mode as macOS and Windows do + ( + &None, + &Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: PlatformVideoMode::X(ref video_mode), + })), + ) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: PlatformVideoMode::X(ref video_mode), + })), + ) => { + let monitor = video_mode.monitor.as_ref().unwrap(); + shared_state_lock.desktop_video_mode = + Some((monitor.id, self.xconn.get_crtc_mode(monitor.id))); + } + // Restore desktop video mode upon exiting exclusive fullscreen + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let (monitor_id, mode_id) = shared_state_lock.desktop_video_mode.take().unwrap(); + self.xconn + .set_crtc_config(monitor_id, mode_id) + .expect("failed to restore desktop video mode"); + } + _ => (), + } + + drop(shared_state_lock); + + match fullscreen { None => { let flusher = self.set_fullscreen_hint(false); - if let Some(position) = self.shared_state.lock().restore_position.take() { + let mut shared_state_lock = self.shared_state.lock(); + if let Some(position) = shared_state_lock.restore_position.take() { self.set_position_inner(position.0, position.1).queue(); } - flusher + Some(flusher) } - Some(RootMonitorHandle { - inner: PlatformMonitorHandle::X(monitor), - }) => { + Some(fullscreen) => { + 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), + _ => unreachable!(), + }; + + // Don't set fullscreen on an invalid dummy monitor handle + if monitor.id == 0 { + return None; + } + + if let Some(video_mode) = video_mode { + // FIXME: this is actually not correct if we're setting the + // video mode to a resolution higher than the current + // desktop resolution, because XRandR does not automatically + // reposition the monitors to the right and below this + // monitor. + // + // What ends up happening is we will get the fullscreen + // window showing up on those monitors as well, because + // their virtual position now overlaps with the monitor that + // we just made larger.. + // + // It'd be quite a bit of work to handle this correctly (and + // nobody else seems to bother doing this correctly either), + // so we're just leaving this broken. Fixing this would + // involve storing all CRTCs upon entering fullscreen, + // restoring them upon exit, and after entering fullscreen, + // repositioning displays to the right and below this + // display. I think there would still be edge cases that are + // difficult or impossible to handle correctly, e.g. what if + // a new monitor was plugged in while in fullscreen? + // + // I think we might just want to disallow setting the video + // mode higher than the current desktop video mode (I'm sure + // this will make someone unhappy, but it's very unusual for + // games to want to do this anyway). + self.xconn + .set_crtc_config(monitor.id, video_mode.native_mode) + .expect("failed to set video mode"); + } + let window_position = self.outer_position_physical(); self.shared_state.lock().restore_position = Some(window_position); let monitor_origin: (i32, i32) = monitor.position().into(); self.set_position_inner(monitor_origin.0, monitor_origin.1) .queue(); - self.set_fullscreen_hint(true) + Some(self.set_fullscreen_hint(true)) } - _ => unreachable!(), } } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { self.shared_state.lock().fullscreen.clone() } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - self.shared_state.lock().fullscreen = monitor.clone(); - self.set_fullscreen_inner(monitor) - .flush() - .expect("Failed to change window fullscreen state"); - self.invalidate_cached_frame_extents(); + pub fn set_fullscreen(&self, fullscreen: Option) { + if let Some(flusher) = self.set_fullscreen_inner(fullscreen) { + flusher + .sync() + .expect("Failed to change window fullscreen state"); + self.invalidate_cached_frame_extents(); + } } fn get_rect(&self) -> util::AaRect { @@ -599,11 +709,11 @@ impl UnownedWindow { pub fn current_monitor(&self) -> X11MonitorHandle { let monitor = self.shared_state.lock().last_monitor.as_ref().cloned(); monitor.unwrap_or_else(|| { - let monitor = self - .xconn - .get_monitor_for_window(Some(self.get_rect())) - .to_owned(); - self.shared_state.lock().last_monitor = Some(monitor.clone()); + let monitor = self.xconn.get_monitor_for_window(Some(self.get_rect())); + // Avoid caching an invalid dummy monitor handle + if monitor.id != 0 { + self.shared_state.lock().last_monitor = Some(monitor.clone()); + } monitor }) } @@ -738,12 +848,22 @@ impl UnownedWindow { #[inline] pub fn set_visible(&self, visible: bool) { + let is_visible = self.shared_state.lock().is_visible; + + if visible == is_visible { + return; + } + match visible { true => unsafe { (self.xconn.xlib.XMapRaised)(self.xconn.display, self.xwindow); self.xconn .flush_requests() .expect("Failed to call XMapRaised"); + + // Some X requests may generate an error if the window is not + // visible, so we must wait until the window becomes visible. + self.wait_for_visibility_notify(); }, false => unsafe { (self.xconn.xlib.XUnmapWindow)(self.xconn.display, self.xwindow); @@ -752,6 +872,21 @@ impl UnownedWindow { .expect("Failed to call XUnmapWindow"); }, } + + self.shared_state.lock().is_visible = visible; + } + + fn wait_for_visibility_notify(&self) { + unsafe { + let mut event = MaybeUninit::uninit(); + + (self.xconn.xlib.XIfEvent)( + self.xconn.display, + event.as_mut_ptr(), + Some(visibility_predicate), + self.xwindow as _, + ); + } } fn update_cached_frame_extents(&self) { @@ -1025,131 +1160,14 @@ impl UnownedWindow { unsafe { (self.xconn.xlib_xcb.XGetXCBConnection)(self.xconn.display) as *mut _ } } - fn load_cursor(&self, name: &[u8]) -> ffi::Cursor { - unsafe { - (self.xconn.xcursor.XcursorLibraryLoadCursor)( - self.xconn.display, - name.as_ptr() as *const c_char, - ) - } - } - - fn load_first_existing_cursor(&self, names: &[&[u8]]) -> ffi::Cursor { - for name in names.iter() { - let xcursor = self.load_cursor(name); - if xcursor != 0 { - return xcursor; - } - } - 0 - } - - fn get_cursor(&self, cursor: CursorIcon) -> ffi::Cursor { - let load = |name: &[u8]| self.load_cursor(name); - - let loadn = |names: &[&[u8]]| self.load_first_existing_cursor(names); - - // Try multiple names in some cases where the name - // differs on the desktop environments or themes. - // - // Try the better looking (or more suiting) names first. - match cursor { - CursorIcon::Alias => load(b"link\0"), - CursorIcon::Arrow => load(b"arrow\0"), - CursorIcon::Cell => load(b"plus\0"), - CursorIcon::Copy => load(b"copy\0"), - CursorIcon::Crosshair => load(b"crosshair\0"), - CursorIcon::Default => load(b"left_ptr\0"), - CursorIcon::Hand => loadn(&[b"hand2\0", b"hand1\0"]), - CursorIcon::Help => load(b"question_arrow\0"), - CursorIcon::Move => load(b"move\0"), - CursorIcon::Grab => loadn(&[b"openhand\0", b"grab\0"]), - CursorIcon::Grabbing => loadn(&[b"closedhand\0", b"grabbing\0"]), - CursorIcon::Progress => load(b"left_ptr_watch\0"), - CursorIcon::AllScroll => load(b"all-scroll\0"), - CursorIcon::ContextMenu => load(b"context-menu\0"), - - CursorIcon::NoDrop => loadn(&[b"no-drop\0", b"circle\0"]), - CursorIcon::NotAllowed => load(b"crossed_circle\0"), - - // Resize cursors - CursorIcon::EResize => load(b"right_side\0"), - CursorIcon::NResize => load(b"top_side\0"), - CursorIcon::NeResize => load(b"top_right_corner\0"), - CursorIcon::NwResize => load(b"top_left_corner\0"), - CursorIcon::SResize => load(b"bottom_side\0"), - CursorIcon::SeResize => load(b"bottom_right_corner\0"), - CursorIcon::SwResize => load(b"bottom_left_corner\0"), - CursorIcon::WResize => load(b"left_side\0"), - CursorIcon::EwResize => load(b"h_double_arrow\0"), - CursorIcon::NsResize => load(b"v_double_arrow\0"), - CursorIcon::NwseResize => loadn(&[b"bd_double_arrow\0", b"size_bdiag\0"]), - CursorIcon::NeswResize => loadn(&[b"fd_double_arrow\0", b"size_fdiag\0"]), - CursorIcon::ColResize => loadn(&[b"split_h\0", b"h_double_arrow\0"]), - CursorIcon::RowResize => loadn(&[b"split_v\0", b"v_double_arrow\0"]), - - CursorIcon::Text => loadn(&[b"text\0", b"xterm\0"]), - CursorIcon::VerticalText => load(b"vertical-text\0"), - - CursorIcon::Wait => load(b"watch\0"), - - CursorIcon::ZoomIn => load(b"zoom-in\0"), - CursorIcon::ZoomOut => load(b"zoom-out\0"), - } - } - - fn update_cursor(&self, cursor: ffi::Cursor) { - unsafe { - (self.xconn.xlib.XDefineCursor)(self.xconn.display, self.xwindow, cursor); - if cursor != 0 { - (self.xconn.xlib.XFreeCursor)(self.xconn.display, cursor); - } - self.xconn - .flush_requests() - .expect("Failed to set or free the cursor"); - } - } - #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - *self.cursor.lock() = cursor; - if *self.cursor_visible.lock() { - self.update_cursor(self.get_cursor(cursor)); + let old_cursor = replace(&mut *self.cursor.lock(), cursor); + if cursor != old_cursor && *self.cursor_visible.lock() { + self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); } } - // TODO: This could maybe be cached. I don't think it's worth - // the complexity, since cursor changes are not so common, - // and this is just allocating a 1x1 pixmap... - fn create_empty_cursor(&self) -> Option { - let data = 0; - let pixmap = unsafe { - (self.xconn.xlib.XCreateBitmapFromData)(self.xconn.display, self.xwindow, &data, 1, 1) - }; - if pixmap == 0 { - // Failed to allocate - return None; - } - - let cursor = unsafe { - // We don't care about this color, since it only fills bytes - // in the pixmap which are not 0 in the mask. - let dummy_color: ffi::XColor = mem::uninitialized(); - let cursor = (self.xconn.xlib.XCreatePixmapCursor)( - self.xconn.display, - pixmap, - pixmap, - &dummy_color as *const _ as *mut _, - &dummy_color as *const _ as *mut _, - 0, - 0, - ); - (self.xconn.xlib.XFreePixmap)(self.xconn.display, pixmap); - cursor - }; - Some(cursor) - } - #[inline] pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { let mut grabbed_lock = self.cursor_grabbed.lock(); @@ -1219,14 +1237,13 @@ impl UnownedWindow { return; } let cursor = if visible { - self.get_cursor(*self.cursor.lock()) + Some(*self.cursor.lock()) } else { - self.create_empty_cursor() - .expect("Failed to create empty cursor") + None }; *visible_lock = visible; drop(visible_lock); - self.update_cursor(cursor); + self.xconn.set_cursor_icon(self.xwindow, cursor); } #[inline] @@ -1277,4 +1294,13 @@ impl UnownedWindow { .unwrap() .insert(WindowId(self.xwindow)); } + + #[inline] + pub fn raw_window_handle(&self) -> X11Handle { + X11Handle { + window: self.xwindow, + display: self.xconn.display as _, + ..X11Handle::empty() + } + } } diff --git a/src/platform_impl/linux/x11/xdisplay.rs b/src/platform_impl/linux/x11/xdisplay.rs index 79ace84f..176323ec 100644 --- a/src/platform_impl/linux/x11/xdisplay.rs +++ b/src/platform_impl/linux/x11/xdisplay.rs @@ -1,8 +1,10 @@ -use std::{error::Error, fmt, os::raw::c_int, ptr}; +use std::{collections::HashMap, error::Error, fmt, os::raw::c_int, ptr}; use libc; use parking_lot::Mutex; +use crate::window::CursorIcon; + use super::ffi; /// A connection to an X server. @@ -19,6 +21,7 @@ pub struct XConnection { pub display: *mut ffi::Display, pub x11_fd: c_int, pub latest_error: Mutex>, + pub cursor_cache: Mutex, ffi::Cursor>>, } unsafe impl Send for XConnection {} @@ -64,6 +67,7 @@ impl XConnection { display, x11_fd: fd, latest_error: Mutex::new(None), + cursor_cache: Default::default(), }) } diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index cccbb098..d063fe84 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -3,6 +3,7 @@ use std::{ fmt::{self, Debug}, hint::unreachable_unchecked, mem, + rc::Rc, sync::{ atomic::{AtomicBool, Ordering}, Mutex, MutexGuard, @@ -37,13 +38,13 @@ pub trait EventHandler: Debug { fn handle_user_events(&mut self, control_flow: &mut ControlFlow); } -struct EventLoopHandler { - callback: F, +struct EventLoopHandler { + callback: Box, &RootWindowTarget, &mut ControlFlow)>, will_exit: bool, - window_target: RootWindowTarget, + window_target: Rc>, } -impl Debug for EventLoopHandler { +impl Debug for EventLoopHandler { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("EventLoopHandler") @@ -52,11 +53,7 @@ impl Debug for EventLoopHandler { } } -impl EventHandler for EventLoopHandler -where - F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), - T: 'static, -{ +impl EventHandler for EventLoopHandler { fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow) { (self.callback)(event.userify(), &self.window_target, control_flow); self.will_exit |= *control_flow == ControlFlow::Exit; @@ -87,7 +84,6 @@ struct Handler { start_time: Mutex>, callback: Mutex>>, pending_events: Mutex>>, - deferred_events: Mutex>>, pending_redraw: Mutex>, waker: Mutex, } @@ -100,10 +96,6 @@ impl Handler { self.pending_events.lock().unwrap() } - fn deferred<'a>(&'a self) -> MutexGuard<'a, VecDeque>> { - self.deferred_events.lock().unwrap() - } - fn redraw<'a>(&'a self) -> MutexGuard<'a, Vec> { self.pending_redraw.lock().unwrap() } @@ -148,10 +140,6 @@ impl Handler { mem::replace(&mut *self.events(), Default::default()) } - fn take_deferred(&self) -> VecDeque> { - mem::replace(&mut *self.deferred(), Default::default()) - } - fn should_redraw(&self) -> Vec { mem::replace(&mut *self.redraw(), Default::default()) } @@ -180,13 +168,20 @@ impl Handler { pub enum AppState {} impl AppState { - pub fn set_callback(callback: F, window_target: RootWindowTarget) + // This function extends lifetime of `callback` to 'static as its side effect + pub unsafe fn set_callback(callback: F, window_target: Rc>) where - F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), - T: 'static, + F: FnMut(Event, &RootWindowTarget, &mut ControlFlow), { *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { - callback, + // This transmute is always safe, in case it was reached through `run`, since our + // lifetime will be already 'static. In other cases caller should ensure that all data + // they passed to callback will actually outlive it, some apps just can't move + // everything to event loop, so this is something that they should care about. + callback: mem::transmute::< + Box, &RootWindowTarget, &mut ControlFlow)>, + Box, &RootWindowTarget, &mut ControlFlow)>, + >(Box::new(callback)), will_exit: false, window_target, })); @@ -196,6 +191,7 @@ impl AppState { HANDLER.set_in_callback(true); HANDLER.handle_nonuser_event(Event::LoopDestroyed); HANDLER.set_in_callback(false); + HANDLER.callback.lock().unwrap().take(); } pub fn launched() { @@ -259,20 +255,6 @@ impl AppState { HANDLER.events().append(&mut events); } - pub fn send_event_immediately(event: Event) { - if !unsafe { msg_send![class!(NSThread), isMainThread] } { - panic!("Event sent from different thread: {:#?}", event); - } - HANDLER.deferred().push_back(event); - if !HANDLER.get_in_callback() { - HANDLER.set_in_callback(true); - for event in HANDLER.take_deferred() { - HANDLER.handle_nonuser_event(event); - } - HANDLER.set_in_callback(false); - } - } - pub fn cleared() { if !HANDLER.is_ready() { return; @@ -298,7 +280,7 @@ impl AppState { } HANDLER.update_start_time(); match HANDLER.get_old_and_new_control_flow() { - (ControlFlow::Exit, _) | (_, ControlFlow::Exit) => unreachable!(), + (ControlFlow::Exit, _) | (_, ControlFlow::Exit) => (), (old, new) if old == new => (), (_, ControlFlow::Wait) => HANDLER.waker().stop(), (_, ControlFlow::WaitUntil(instant)) => HANDLER.waker().start_at(instant), diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 2ad17e1d..f3df1b52 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -1,5 +1,6 @@ use std::{ - collections::VecDeque, marker::PhantomData, mem, os::raw::c_void, process, ptr, sync::mpsc, + collections::VecDeque, marker::PhantomData, mem, os::raw::c_void, process, ptr, rc::Rc, + sync::mpsc, }; use cocoa::{ @@ -34,7 +35,7 @@ impl Default for EventLoopWindowTarget { } pub struct EventLoop { - window_target: RootWindowTarget, + window_target: Rc>, _delegate: IdRef, } @@ -59,10 +60,10 @@ impl EventLoop { }; setup_control_flow_observers(); EventLoop { - window_target: RootWindowTarget { + window_target: Rc::new(RootWindowTarget { p: Default::default(), _marker: PhantomData, - }, + }), _delegate: delegate, } } @@ -81,46 +82,50 @@ impl EventLoop { &self.window_target } - pub fn run(self, callback: F) -> ! + pub fn run(mut self, callback: F) -> ! where F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), + { + self.run_return(callback); + process::exit(0); + } + + pub fn run_return(&mut self, callback: F) + where + F: FnMut(Event, &RootWindowTarget, &mut ControlFlow), { unsafe { let _pool = NSAutoreleasePool::new(nil); let app = NSApp(); assert_ne!(app, nil); - AppState::set_callback(callback, self.window_target); + AppState::set_callback(callback, Rc::clone(&self.window_target)); let _: () = msg_send![app, run]; AppState::exit(); - process::exit(0) } } - pub fn run_return(&mut self, _callback: F) - where - F: FnMut(Event, &RootWindowTarget, &mut ControlFlow), - { - unimplemented!(); - } - pub fn create_proxy(&self) -> Proxy { Proxy::new(self.window_target.p.sender.clone()) } } -#[derive(Clone)] pub struct Proxy { sender: mpsc::Sender, source: CFRunLoopSourceRef, } -unsafe impl Send for Proxy {} -unsafe impl Sync for Proxy {} +unsafe impl Send for Proxy {} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Proxy::new(self.sender.clone()) + } +} impl Proxy { fn new(sender: mpsc::Sender) -> Self { unsafe { - // just wakeup the eventloop + // just wake up the eventloop extern "C" fn event_loop_proxy_handler(_: *mut c_void) {} // adding a Source to the main CFRunLoop lets us wake it up and diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 7ddcdf6f..aec0fc97 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -6,6 +6,13 @@ use cocoa::{ base::id, foundation::{NSInteger, NSUInteger}, }; +use core_foundation::{ + array::CFArrayRef, dictionary::CFDictionaryRef, string::CFStringRef, uuid::CFUUIDRef, +}; +use core_graphics::{ + base::CGError, + display::{CGDirectDisplayID, CGDisplayConfigRef}, +}; use objc; pub const NSNotFound: NSInteger = NSInteger::max_value(); @@ -108,3 +115,95 @@ pub enum NSWindowLevel { NSPopUpMenuWindowLevel = kCGPopUpMenuWindowLevelKey as _, NSScreenSaverWindowLevel = kCGScreenSaverWindowLevelKey as _, } + +pub type CGDisplayFadeInterval = f32; +pub type CGDisplayReservationInterval = f32; +pub type CGDisplayBlendFraction = f32; + +pub const kCGDisplayBlendNormal: f32 = 0.0; +pub const kCGDisplayBlendSolidColor: f32 = 1.0; + +pub type CGDisplayFadeReservationToken = u32; +pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0; + +pub type Boolean = u8; +pub const FALSE: Boolean = 0; +pub const TRUE: Boolean = 1; + +pub const kCGErrorSuccess: i32 = 0; +pub const kCGErrorFailure: i32 = 1000; +pub const kCGErrorIllegalArgument: i32 = 1001; +pub const kCGErrorInvalidConnection: i32 = 1002; +pub const kCGErrorInvalidContext: i32 = 1003; +pub const kCGErrorCannotComplete: i32 = 1004; +pub const kCGErrorNotImplemented: i32 = 1006; +pub const kCGErrorRangeCheck: i32 = 1007; +pub const kCGErrorTypeCheck: i32 = 1008; +pub const kCGErrorInvalidOperation: i32 = 1010; +pub const kCGErrorNoneAvailable: i32 = 1011; + +pub const IO1BitIndexedPixels: &str = "P"; +pub const IO2BitIndexedPixels: &str = "PP"; +pub const IO4BitIndexedPixels: &str = "PPPP"; +pub const IO8BitIndexedPixels: &str = "PPPPPPPP"; +pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB"; +pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB"; + +pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB"; +pub const kIO64BitDirectPixels: &str = "-16R16G16B16"; + +pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16"; +pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32"; + +pub const IOYUV422Pixels: &str = "Y4U2V2"; +pub const IO8BitOverlayPixels: &str = "O8"; + +pub type CGWindowLevel = i32; +pub type CGDisplayModeRef = *mut libc::c_void; + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGRestorePermanentDisplayConfiguration(); + pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError; + pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError; + pub fn CGConfigureDisplayFadeEffect( + config: CGDisplayConfigRef, + fadeOutSeconds: CGDisplayFadeInterval, + fadeInSeconds: CGDisplayFadeInterval, + fadeRed: f32, + fadeGreen: f32, + fadeBlue: f32, + ) -> CGError; + pub fn CGAcquireDisplayFadeReservation( + seconds: CGDisplayReservationInterval, + token: *mut CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGDisplayFade( + token: CGDisplayFadeReservationToken, + duration: CGDisplayFadeInterval, + startBlend: CGDisplayBlendFraction, + endBlend: CGDisplayBlendFraction, + redBlend: f32, + greenBlend: f32, + blueBlend: f32, + synchronous: Boolean, + ) -> CGError; + pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError; + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + pub fn CGShieldingWindowLevel() -> CGWindowLevel; + pub fn CGDisplaySetDisplayMode( + display: CGDirectDisplayID, + mode: CGDisplayModeRef, + options: CFDictionaryRef, + ) -> CGError; + pub fn CGDisplayCopyAllDisplayModes( + display: CGDirectDisplayID, + options: CFDictionaryRef, + ) -> CFArrayRef; + pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64; + pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; + pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); + pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 02b63515..c26385da 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -17,7 +17,7 @@ use std::{fmt, ops::Deref, sync::Arc}; pub use self::{ event_loop::{EventLoop, EventLoopWindowTarget, Proxy as EventLoopProxy}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::{Id as WindowId, PlatformSpecificWindowBuilderAttributes, UnownedWindow}, }; use crate::{ diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index cf74c644..c9408890 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -1,25 +1,119 @@ use std::{collections::VecDeque, fmt}; +use super::ffi; +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}, }; -use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayMode}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + base::{CFRelease, TCFType}, + string::CFString, +}; +use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; use core_video_sys::{ kCVReturnSuccess, kCVTimeIsIndefinite, CVDisplayLinkCreateWithCGDisplay, CVDisplayLinkGetNominalOutputVideoRefreshPeriod, CVDisplayLinkRelease, }; -use crate::{ - dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, - platform_impl::platform::util::IdRef, -}; +#[derive(Derivative)] +#[derivative(Debug, Clone, PartialEq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + #[derivative(Debug = "ignore", PartialEq = "ignore", Hash = "ignore")] + pub(crate) native_mode: NativeDisplayMode, +} -#[derive(Clone, PartialEq)] +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + ffi::CGDisplayModeRelease(self.0); + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + ffi::CGDisplayModeRetain(self.0); + } + NativeDisplayMode(self.0) + } +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Clone)] pub struct MonitorHandle(CGDirectDisplayID); +// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that +// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an +// unique identifier that persists even across system reboots +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + == ffi::CGDisplayCreateUUIDFromDisplayID(other.0) + } + } +} + +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 { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + .cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0)) + } + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state); + } + } +} + pub fn available_monitors() -> VecDeque { if let Ok(displays) = CGDisplay::active_displays() { let mut monitors = VecDeque::with_capacity(displays.len()); @@ -101,7 +195,7 @@ impl MonitorHandle { unsafe { NSScreen::backingScaleFactor(screen) as f64 } } - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { let cv_refresh_rate = unsafe { let mut display_link = std::ptr::null_mut(); assert_eq!( @@ -117,11 +211,27 @@ impl MonitorHandle { time.timeScale as i64 / time.timeValue }; - CGDisplayMode::all_display_modes(self.0, std::ptr::null()) - .expect("failed to obtain list of display modes") - .into_iter() - .map(move |mode| { - let cg_refresh_rate = mode.refresh_rate().round() as i64; + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null()); + assert!(!array.is_null(), "failed to get list of display modes"); + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .into_iter() + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + }; + + modes.into_iter().map(move |mode| { + let cg_refresh_rate = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; // CGDisplayModeGetRefreshRate returns 0.0 for any display that // isn't a CRT @@ -131,34 +241,55 @@ impl MonitorHandle { cv_refresh_rate }; - VideoMode { - size: (mode.width() as u32, mode.height() as u32), + let pixel_encoding = + CFString::wrap_under_create_rule(ffi::CGDisplayModeCopyPixelEncoding(mode)) + .to_string(); + let bit_depth = if pixel_encoding.eq_ignore_ascii_case(ffi::IO32BitDirectPixels) { + 32 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) { + 16 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) { + 30 + } else { + unimplemented!() + }; + + let video_mode = VideoMode { + size: ( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), refresh_rate: refresh_rate as u16, - bit_depth: mode.bit_depth() as u16, - } + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + }; + + RootVideoMode { video_mode } }) + } } pub(crate) fn ns_screen(&self) -> Option { unsafe { - let native_id = self.native_identifier(); + 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 mut matching_screen: Option = None; for i in 0..count { let screen = msg_send![screens, objectAtIndex: i as NSUInteger]; let device_description = NSScreen::deviceDescription(screen); let value: id = msg_send![device_description, objectForKey:*key]; if value != nil { - let screen_number: NSUInteger = msg_send![value, unsignedIntegerValue]; - if screen_number as u32 == native_id { - matching_screen = Some(screen); - break; + let other_native_id: NSUInteger = msg_send![value, unsignedIntegerValue]; + let other_uuid = + ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID); + if uuid == other_uuid { + return Some(screen); } } } - matching_screen + None } } } diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index ddd20a8f..10e78e0e 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -1,10 +1,9 @@ use std::{self, os::raw::*, ptr, time::Instant}; -use crate::platform_impl::platform::app_state::AppState; +use crate::platform_impl::platform::{app_state::AppState, ffi}; #[link(name = "CoreFoundation", kind = "framework")] extern "C" { - pub static kCFRunLoopDefaultMode: CFRunLoopMode; pub static kCFRunLoopCommonModes: CFRunLoopMode; pub fn CFRunLoopGetMain() -> CFRunLoopRef; @@ -13,7 +12,7 @@ extern "C" { pub fn CFRunLoopObserverCreate( allocator: CFAllocatorRef, activities: CFOptionFlags, - repeats: Boolean, + repeats: ffi::Boolean, order: CFIndex, callout: CFRunLoopObserverCallBack, context: *mut CFRunLoopObserverContext, @@ -51,11 +50,6 @@ extern "C" { pub fn CFRelease(cftype: *const c_void); } -pub type Boolean = u8; -#[allow(dead_code)] -const FALSE: Boolean = 0; -const TRUE: Boolean = 1; - pub enum CFAllocator {} pub type CFAllocatorRef = *mut CFAllocator; pub enum CFRunLoop {} @@ -102,7 +96,7 @@ pub struct CFRunLoopSourceContext { pub retain: extern "C" fn(*const c_void) -> *const c_void, pub release: extern "C" fn(*const c_void), pub copyDescription: extern "C" fn(*const c_void) -> CFStringRef, - pub equal: extern "C" fn(*const c_void, *const c_void) -> Boolean, + pub equal: extern "C" fn(*const c_void, *const c_void) -> ffi::Boolean, pub hash: extern "C" fn(*const c_void) -> CFHashCode, pub schedule: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), pub cancel: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), @@ -162,12 +156,12 @@ impl RunLoop { let observer = CFRunLoopObserverCreate( ptr::null_mut(), flags, - TRUE, // Indicates we want this to run repeatedly - priority, // The lower the value, the sooner this will run + ffi::TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run handler, ptr::null_mut(), ); - CFRunLoopAddObserver(self.0, observer, kCFRunLoopDefaultMode); + CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes); } } @@ -204,9 +198,9 @@ impl Default for EventLoopWaker { fn default() -> EventLoopWaker { extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} unsafe { - // create a timer with a 1µs interval (1ns does not work) to mimic polling. - // it is initially setup with a first fire time really far into the - // future, but that gets changed to fire immediatley in did_finish_launching + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching let timer = CFRunLoopTimerCreate( ptr::null_mut(), std::f64::MAX, diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index bb8655fc..ad1a891b 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -206,7 +206,10 @@ extern "C" fn toggle_full_screen_callback(context: *mut c_void) { } } } - + // 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); @@ -228,6 +231,21 @@ pub unsafe fn toggle_full_screen_async( ); } +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, diff --git a/src/platform_impl/macos/util/cursor.rs b/src/platform_impl/macos/util/cursor.rs index e3e3bcf0..7f0b57fe 100644 --- a/src/platform_impl/macos/util/cursor.rs +++ b/src/platform_impl/macos/util/cursor.rs @@ -15,10 +15,12 @@ pub enum Cursor { impl From for Cursor { fn from(cursor: CursorIcon) -> Self { + // See native cursors at https://developer.apple.com/documentation/appkit/nscursor?language=objc. match cursor { CursorIcon::Arrow | CursorIcon::Default => Cursor::Native("arrowCursor"), CursorIcon::Hand => Cursor::Native("pointingHandCursor"), - CursorIcon::Grabbing | CursorIcon::Grab => Cursor::Native("closedHandCursor"), + CursorIcon::Grab => Cursor::Native("openHandCursor"), + CursorIcon::Grabbing => Cursor::Native("closedHandCursor"), CursorIcon::Text => Cursor::Native("IBeamCursor"), CursorIcon::VerticalText => Cursor::Native("IBeamCursorForVerticalLayout"), CursorIcon::Copy => Cursor::Native("dragCopyCursor"), diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 5a0fff9a..31a427c3 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -104,7 +104,7 @@ lazy_static! { ); decl.add_method( sel!(drawRect:), - draw_rect as extern "C" fn(&Object, Sel, id), + draw_rect as extern "C" fn(&Object, Sel, NSRect), ); decl.add_method( sel!(acceptsFirstResponder), @@ -280,7 +280,7 @@ extern "C" fn view_did_move_to_window(this: &Object, _sel: Sel) { trace!("Completed `viewDidMoveToWindow`"); } -extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: id) { +extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 4f6bf255..4c9e3f9b 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1,3 +1,4 @@ +use raw_window_handle::{macos::MacOSHandle, RawWindowHandle}; use std::{ collections::VecDeque, f64, @@ -8,6 +9,23 @@ use std::{ }, }; +use crate::{ + dpi::{LogicalPosition, LogicalSize}, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS}, + platform_impl::platform::{ + app_state::AppState, + ffi, + monitor::{self, MonitorHandle, VideoMode}, + util::{self, IdRef}, + view::{self, new_view}, + window_delegate::new_delegate, + OsError, + }, + window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId}, +}; use cocoa::{ appkit::{ self, CGFloat, NSApp, NSApplication, NSApplicationActivationPolicy, @@ -17,30 +35,12 @@ use cocoa::{ base::{id, nil}, foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString}, }; -use core_graphics::display::CGDisplay; +use core_graphics::display::{CGDisplay, CGDisplayMode}; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel, BOOL, NO, YES}, }; -use crate::{ - dpi::{LogicalPosition, LogicalSize}, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - icon::Icon, - monitor::MonitorHandle as RootMonitorHandle, - platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS}, - platform_impl::platform::{ - app_state::AppState, - ffi, - monitor::{self, MonitorHandle}, - util::{self, IdRef}, - view::{self, new_view}, - window_delegate::new_delegate, - OsError, - }, - window::{CursorIcon, WindowAttributes, WindowId as RootWindowId}, -}; - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Id(pub usize); @@ -66,6 +66,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub titlebar_buttons_hidden: bool, pub fullsize_content_view: bool, pub resize_increments: Option, + pub disallow_hidpi: bool, } fn create_app(activation_policy: ActivationPolicy) -> Option { @@ -86,10 +87,15 @@ fn create_app(activation_policy: ActivationPolicy) -> Option { } } -unsafe fn create_view(ns_window: id) -> Option<(IdRef, Weak>)> { +unsafe fn create_view( + ns_window: id, + pl_attribs: &PlatformSpecificWindowBuilderAttributes, +) -> Option<(IdRef, Weak>)> { let (ns_view, cursor) = new_view(ns_window); ns_view.non_nil().map(|ns_view| { - ns_view.setWantsBestResolutionOpenGLSurface_(YES); + if !pl_attribs.disallow_hidpi { + ns_view.setWantsBestResolutionOpenGLSurface_(YES); + } // On Mojave, views automatically become layer-backed shortly after being added to // a window. Changing the layer-backedness of a view breaks the association between @@ -113,11 +119,14 @@ fn create_window( unsafe { let pool = NSAutoreleasePool::new(nil); let screen = match attrs.fullscreen { - Some(ref monitor_id) => { - let monitor_screen = monitor_id.inner.ns_screen(); + Some(Fullscreen::Borderless(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))) } - _ => None, + None => None, }; let frame = match screen { Some(screen) => appkit::NSScreen::frame(screen), @@ -233,12 +242,15 @@ lazy_static! { #[derive(Default)] pub struct SharedState { pub resizable: bool, - pub fullscreen: Option, + pub fullscreen: Option, pub maximized: bool, pub standard_frame: Option, is_simple_fullscreen: bool, pub saved_style: Option, + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it save_presentation_opts: Option, + pub saved_desktop_display_mode: Option<(CGDisplay, CGDisplayMode)>, } impl SharedState { @@ -301,10 +313,11 @@ impl UnownedWindow { os_error!(OsError::CreationError("Couldn't create `NSWindow`")) })?; - let (ns_view, cursor) = unsafe { create_view(*ns_window) }.ok_or_else(|| { - unsafe { pool.drain() }; - os_error!(OsError::CreationError("Couldn't create `NSView`")) - })?; + let (ns_view, cursor) = + unsafe { create_view(*ns_window, &pl_attribs) }.ok_or_else(|| { + unsafe { pool.drain() }; + os_error!(OsError::CreationError("Couldn't create `NSView`")) + })?; let input_context = unsafe { util::create_input_context(*ns_view) }; @@ -355,16 +368,7 @@ impl UnownedWindow { let delegate = new_delegate(&window, fullscreen.is_some()); // Set fullscreen mode after we setup everything - if let Some(monitor) = fullscreen { - if monitor.inner != window.current_monitor().inner { - // To do this with native fullscreen, we probably need to - // warp the window... while we could use - // `enterFullScreenMode`, they're idiomatically different - // fullscreen modes, so we'd have to support both anyway. - unimplemented!(); - } - window.set_fullscreen(Some(monitor)); - } + window.set_fullscreen(fullscreen); // Setting the window as key has to happen *after* we set the fullscreen // state, since otherwise we'll briefly see the window at normal size @@ -594,19 +598,22 @@ impl UnownedWindow { } } + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` pub(crate) fn restore_state_from_fullscreen(&self) { - let maximized = { - trace!("Locked shared state in `restore_state_from_fullscreen`"); - let mut shared_state_lock = self.shared_state.lock().unwrap(); + trace!("Locked shared state in `restore_state_from_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); - shared_state_lock.fullscreen = None; + shared_state_lock.fullscreen = None; - let mask = self.saved_style(&mut *shared_state_lock); + let maximized = shared_state_lock.maximized; + let mask = self.saved_style(&mut *shared_state_lock); - self.set_style_mask_async(mask); - shared_state_lock.maximized - }; + drop(shared_state_lock); trace!("Unocked shared state in `restore_state_from_fullscreen`"); + + self.set_style_mask_async(mask); self.set_maximized(maximized); } @@ -627,44 +634,168 @@ impl UnownedWindow { } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { let shared_state_lock = self.shared_state.lock().unwrap(); shared_state_lock.fullscreen.clone() } #[inline] - /// TODO: Right now set_fullscreen do not work on switching monitors - /// in fullscreen mode - pub fn set_fullscreen(&self, monitor: Option) { + pub fn set_fullscreen(&self, fullscreen: Option) { + trace!("Locked shared state in `set_fullscreen`"); let shared_state_lock = self.shared_state.lock().unwrap(); if shared_state_lock.is_simple_fullscreen { + trace!("Unlocked shared state in `set_fullscreen`"); return; } - - let not_fullscreen = { - trace!("Locked shared state in `set_fullscreen`"); - let current = &shared_state_lock.fullscreen; - match (current, monitor) { - (&Some(ref a), Some(ref b)) if a.inner != b.inner => { - // Our best bet is probably to move to the origin of the - // target monitor. - unimplemented!() - } - (&None, None) | (&Some(_), Some(_)) => return, - _ => (), - } + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if fullscreen == old_fullscreen { trace!("Unlocked shared state in `set_fullscreen`"); - current.is_none() - }; + return; + } + trace!("Unlocked shared state in `set_fullscreen`"); + drop(shared_state_lock); - unsafe { - util::toggle_full_screen_async( - *self.ns_window, - *self.ns_view, - not_fullscreen, - Arc::downgrade(&self.shared_state), - ) - }; + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // 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::Exclusive(RootVideoMode { + video_mode: VideoMode { ref monitor, .. }, + }) => monitor, + } + .ns_screen() + .unwrap(); + + unsafe { + let old_screen = NSWindow::screen(*self.ns_window); + if old_screen != new_screen { + let mut screen_frame: NSRect = msg_send![new_screen, frame]; + // The coordinate system here has its origin at bottom-left + // and Y goes up + screen_frame.origin.y += screen_frame.size.height; + util::set_frame_top_left_point_async(*self.ns_window, screen_frame.origin); + } + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().inner.native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) + == ffi::kCGErrorSuccess + { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + trace!("Locked shared state in `set_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.fullscreen = fullscreen.clone(); + trace!("Unlocked shared state in `set_fullscreen`"); + + match (&old_fullscreen, &fullscreen) { + (&None, &Some(_)) => unsafe { + util::toggle_full_screen_async( + *self.ns_window, + *self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Borderless(_)), &None) => unsafe { + // State is restored by `window_did_exit_fullscreen` + util::toggle_full_screen_async( + *self.ns_window, + *self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), &None) => unsafe { + util::restore_display_mode_async(video_mode.monitor().inner.native_identifier()); + // Rest of the state is restored by `window_did_exit_fullscreen` + util::toggle_full_screen_async( + *self.ns_window, + *self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => unsafe { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + msg_send![*self.ns_window, setLevel: ffi::CGShieldingWindowLevel() + 1]; + }, + ( + &Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), + &Some(Fullscreen::Borderless(_)), + ) => unsafe { + util::restore_display_mode_async(video_mode.monitor().inner.native_identifier()); + }, + _ => (), + } } #[inline] @@ -764,6 +895,16 @@ impl UnownedWindow { pub fn primary_monitor(&self) -> MonitorHandle { monitor::primary_monitor() } + + #[inline] + pub fn raw_window_handle(&self) -> RawWindowHandle { + let handle = MacOSHandle { + ns_window: *self.ns_window as *mut _, + ns_view: *self.ns_view as *mut _, + ..MacOSHandle::empty() + }; + RawWindowHandle::MacOS(handle) + } } impl WindowExtMacOS for UnownedWindow { diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index c9680d89..041edf61 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -5,9 +5,9 @@ use std::{ }; use cocoa::{ - appkit::{self, NSView, NSWindow}, + appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow}, base::{id, nil}, - foundation::NSAutoreleasePool, + foundation::{NSAutoreleasePool, NSUInteger}, }; use objc::{ declare::ClassDecl, @@ -22,7 +22,7 @@ use crate::{ util::{self, IdRef}, window::{get_window_id, UnownedWindow}, }, - window::WindowId, + window::{Fullscreen, WindowId}, }; pub struct WindowDelegateState { @@ -84,11 +84,7 @@ impl WindowDelegateState { 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 event = Event::WindowEvent { - window_id: WindowId(get_window_id(*self.ns_window)), - event: WindowEvent::Resized(size), - }; - AppState::send_event_immediately(event); + self.emit_event(WindowEvent::Resized(size)); } fn emit_move_event(&mut self) { @@ -182,6 +178,11 @@ lazy_static! { dragging_exited as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(window:willUseFullScreenPresentationOptions:), + window_will_use_fullscreen_presentation_options + as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + ); decl.add_method( sel!(windowDidEnterFullScreen:), window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id), @@ -408,6 +409,26 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Completed `windowWillEnterFullscreen:`"); } +extern "C" fn window_will_use_fullscreen_presentation_options( + _this: &Object, + _: Sel, + _: id, + _proposed_options: NSUInteger, +) -> NSUInteger { + // Generally, games will want to disable the menu bar and the dock. Ideally, + // this would be configurable by the user. Unfortunately because of our + // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is + // placed on top of the menu bar in exclusive fullscreen mode. This looks + // broken so we always disable the menu bar in exclusive fullscreen. We may + // still want to make this configurable for borderless fullscreen. Right now + // we don't, for consistency. If we do, it should be documented that the + // user-provided options are ignored in exclusive fullscreen. + (NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar) + .bits() +} + /// Invoked when entered fullscreen extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidEnterFullscreen:`"); @@ -415,8 +436,21 @@ extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { state.with_window(|window| { let monitor = window.current_monitor(); trace!("Locked shared state in `window_did_enter_fullscreen`"); - window.shared_state.lock().unwrap().fullscreen = Some(monitor); - trace!("Unlocked shared state in `window_will_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + match shared_state.fullscreen { + // Exclusive mode sets the state in `set_fullscreen` as the user + // can't enter exclusive mode by other means (like the + // fullscreen button on the window decorations) + Some(Fullscreen::Exclusive(_)) => (), + // `window_did_enter_fullscreen` was triggered and we're already + // in fullscreen, so we must've reached here by `set_fullscreen` + // as it updates the state + Some(Fullscreen::Borderless(_)) => (), + // Otherwise, we must've reached fullscreen by the user clicking + // on the green fullscreen button. Update state! + None => shared_state.fullscreen = Some(Fullscreen::Borderless(monitor)), + } + trace!("Unlocked shared state in `window_did_enter_fullscreen`"); }); state.initial_fullscreen = false; }); diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 1c474f94..152065d8 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -21,9 +21,6 @@ mod platform; #[cfg(target_os = "ios")] #[path = "ios/mod.rs"] mod platform; -#[cfg(target_os = "emscripten")] -#[path = "emscripten/mod.rs"] -mod platform; #[cfg(target_arch = "wasm32")] #[path = "web/mod.rs"] mod platform; @@ -38,7 +35,6 @@ mod platform; not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "openbsd"), - not(target_os = "emscripten"), not(target_arch = "wasm32"), ))] compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/src/platform_impl/windows/dpi.rs b/src/platform_impl/windows/dpi.rs index 12186182..1a5fa13c 100644 --- a/src/platform_impl/windows/dpi.rs +++ b/src/platform_impl/windows/dpi.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_unsafe)] -use std::{mem, os::raw::c_void, sync::Once}; +use std::sync::Once; use winapi::{ shared::{ @@ -9,13 +9,12 @@ use winapi::{ winerror::S_OK, }, um::{ - libloaderapi::{GetProcAddress, LoadLibraryA}, shellscalingapi::{ MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, PROCESS_PER_MONITOR_DPI_AWARE, }, wingdi::{GetDeviceCaps, LOGPIXELSX}, - winnt::{HRESULT, LPCSTR}, + winnt::HRESULT, winuser::{self, MONITOR_DEFAULTTONEAREST}, }, }; @@ -35,33 +34,6 @@ type GetDpiForMonitor = unsafe extern "system" fn( ) -> HRESULT; type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; -// Helper function to dynamically load function pointer. -// `library` and `function` must be zero-terminated. -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) => { - get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')) - .map(|f| unsafe { mem::transmute::<*const _, $func>(f) }) - }; -} - lazy_static! { static ref GET_DPI_FOR_WINDOW: Option = get_function!("user32.dll", GetDpiForWindow); diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index b53d3add..94f7255e 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -1,3 +1,4 @@ +#![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. @@ -38,14 +39,14 @@ use winapi::{ }, um::{ commctrl, libloaderapi, ole2, processthreadsapi, winbase, - winnt::{LONG, LPCSTR, SHORT}, + winnt::{HANDLE, LONG, LPCSTR, SHORT}, winuser, }, }; use crate::{ dpi::{LogicalPosition, LogicalSize, PhysicalSize}, - event::{DeviceEvent, Event, KeyboardInput, StartCause, Touch, TouchPhase, WindowEvent}, + event::{DeviceEvent, Event, Force, KeyboardInput, StartCause, Touch, TouchPhase, WindowEvent}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, platform_impl::platform::{ dpi::{ @@ -62,6 +63,39 @@ use crate::{ window::WindowId as RootWindowId, }; +type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointerId: UINT, + entriesCount: *mut UINT, + pointerCount: *mut UINT, + pointerInfo: *mut winuser::POINTER_INFO, +) -> BOOL; + +type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: UINT) -> BOOL; +type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointerDeviceRect: *mut RECT, + displayRect: *mut RECT, +) -> BOOL; + +type GetPointerTouchInfo = + unsafe extern "system" fn(pointerId: UINT, touchInfo: *mut winuser::POINTER_TOUCH_INFO) -> BOOL; + +type GetPointerPenInfo = + unsafe extern "system" fn(pointId: UINT, penInfo: *mut winuser::POINTER_PEN_INFO) -> BOOL; + +lazy_static! { + static ref GET_POINTER_FRAME_INFO_HISTORY: Option = + get_function!("user32.dll", GetPointerFrameInfoHistory); + static ref SKIP_POINTER_FRAME_MESSAGES: Option = + get_function!("user32.dll", SkipPointerFrameMessages); + static ref GET_POINTER_DEVICE_RECTS: Option = + get_function!("user32.dll", GetPointerDeviceRects); + static ref GET_POINTER_TOUCH_INFO: Option = + get_function!("user32.dll", GetPointerTouchInfo); + static ref GET_POINTER_PEN_INFO: Option = + get_function!("user32.dll", GetPointerPenInfo); +} + pub(crate) struct SubclassInput { pub window_state: Arc>, pub event_loop_runner: EventLoopRunnerShared, @@ -654,13 +688,21 @@ impl EventLoopThreadExecutor { type ThreadExecFn = Box>; -#[derive(Clone)] pub struct EventLoopProxy { target_window: HWND, event_send: Sender, } unsafe impl Send for EventLoopProxy {} +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + target_window: self.target_window, + event_send: self.event_send.clone(), + } + } +} + impl EventLoopProxy { pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { unsafe { @@ -820,6 +862,13 @@ pub(crate) fn subclass_window(window: HWND, subclass_input: SubclassInput) assert_eq!(subclass_result, 1); } +fn normalize_pointer_pressure(pressure: u32) -> Option { + match pressure { + 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), + _ => None, + } +} + /// 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. // @@ -982,11 +1031,34 @@ unsafe extern "system" fn public_window_callback( winuser::WM_CHAR => { use crate::event::WindowEvent::ReceivedCharacter; - let chr: char = mem::transmute(wparam as u32); - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: ReceivedCharacter(chr), - }); + use std::char; + let is_high_surrogate = 0xD800 <= wparam && wparam <= 0xDBFF; + let is_low_surrogate = 0xDC00 <= wparam && wparam <= 0xDFFF; + + if is_high_surrogate { + subclass_input.window_state.lock().high_surrogate = Some(wparam as u16); + } else if is_low_surrogate { + let high_surrogate = subclass_input.window_state.lock().high_surrogate.take(); + + if let Some(high_surrogate) = high_surrogate { + let pair = [high_surrogate, wparam as u16]; + if let Some(Ok(chr)) = char::decode_utf16(pair.iter().copied()).next() { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ReceivedCharacter(chr), + }); + } + } + } else { + subclass_input.window_state.lock().high_surrogate = None; + + if let Some(chr) = char::from_u32(wparam as u32) { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ReceivedCharacter(chr), + }); + } + } 0 } @@ -996,6 +1068,17 @@ unsafe extern "system" fn public_window_callback( // other unwanted default hotkeys as well. winuser::WM_SYSCHAR => 0, + winuser::WM_SYSCOMMAND => { + if wparam == winuser::SC_SCREENSAVE { + let window_state = subclass_input.window_state.lock(); + if window_state.fullscreen.is_some() { + return 0; + } + } + + winuser::DefWindowProcW(window, msg, wparam, lparam) + } + winuser::WM_MOUSEMOVE => { use crate::event::WindowEvent::{CursorEntered, CursorMoved}; let mouse_was_outside_window = { @@ -1431,8 +1514,17 @@ unsafe extern "system" fn public_window_callback( { let dpi_factor = hwnd_scale_factor(window); for input in &inputs { - let x = (input.x as f64) / 100f64; - let y = (input.y as f64) / 100f64; + let mut location = POINT { + x: input.x / 100, + y: input.y / 100, + }; + + if winuser::ScreenToClient(window, &mut location as *mut _) == 0 { + continue; + } + + 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); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1447,6 +1539,7 @@ unsafe extern "system" fn public_window_callback( continue; }, location, + force: None, // WM_TOUCH doesn't support pressure information id: input.dwID as u64, device_id: DEVICE_ID, }), @@ -1457,6 +1550,147 @@ unsafe extern "system" fn public_window_callback( 0 } + winuser::WM_POINTERDOWN | winuser::WM_POINTERUPDATE | winuser::WM_POINTERUP => { + if let ( + Some(GetPointerFrameInfoHistory), + Some(SkipPointerFrameMessages), + Some(GetPointerDeviceRects), + ) = ( + *GET_POINTER_FRAME_INFO_HISTORY, + *SKIP_POINTER_FRAME_MESSAGES, + *GET_POINTER_DEVICE_RECTS, + ) { + let pointer_id = LOWORD(wparam as DWORD) as UINT; + let mut entries_count = 0 as UINT; + let mut pointers_count = 0 as UINT; + if GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count as *mut _, + &mut pointers_count as *mut _, + std::ptr::null_mut(), + ) == 0 + { + return 0; + } + + let pointer_info_count = (entries_count * pointers_count) as usize; + let mut pointer_infos = Vec::with_capacity(pointer_info_count); + pointer_infos.set_len(pointer_info_count); + if GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count as *mut _, + &mut pointers_count as *mut _, + pointer_infos.as_mut_ptr(), + ) == 0 + { + 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 + for pointer_info in pointer_infos.iter().rev() { + let mut device_rect = mem::MaybeUninit::uninit(); + let mut display_rect = mem::MaybeUninit::uninit(); + + if (GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + )) == 0 + { + continue; + } + + let device_rect = device_rect.assume_init(); + let display_rect = display_rect.assume_init(); + + // For the most precise himetric to pixel conversion we calculate the ratio between the resolution + // of the display device (pixel) and the touch device (himetric). + let himetric_to_pixel_ratio_x = (display_rect.right - display_rect.left) as f64 + / (device_rect.right - device_rect.left) as f64; + let himetric_to_pixel_ratio_y = (display_rect.bottom - display_rect.top) as f64 + / (device_rect.bottom - device_rect.top) as f64; + + // ptHimetricLocation's origin is 0,0 even on multi-monitor setups. + // On multi-monitor setups we need to translate the himetric location to the rect of the + // display device it's attached to. + let x = display_rect.left as f64 + + pointer_info.ptHimetricLocation.x as f64 * himetric_to_pixel_ratio_x; + let y = display_rect.top as f64 + + pointer_info.ptHimetricLocation.y as f64 * himetric_to_pixel_ratio_y; + + let mut location = POINT { + x: x.floor() as i32, + y: y.floor() as i32, + }; + + if winuser::ScreenToClient(window, &mut location as *mut _) == 0 { + continue; + } + + let force = match pointer_info.pointerType { + winuser::PT_TOUCH => { + let mut touch_info = mem::MaybeUninit::uninit(); + GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + match GetPointerTouchInfo( + pointer_info.pointerId, + touch_info.as_mut_ptr(), + ) { + 0 => None, + _ => normalize_pointer_pressure( + touch_info.assume_init().pressure, + ), + } + }) + } + winuser::PT_PEN => { + let mut pen_info = mem::MaybeUninit::uninit(); + GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { + match GetPointerPenInfo( + pointer_info.pointerId, + pen_info.as_mut_ptr(), + ) { + 0 => None, + _ => { + normalize_pointer_pressure(pen_info.assume_init().pressure) + } + } + }) + } + _ => None, + }; + + 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); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if pointer_info.pointerFlags & winuser::POINTER_FLAG_DOWN != 0 { + TouchPhase::Started + } else if pointer_info.pointerFlags & winuser::POINTER_FLAG_UP != 0 { + TouchPhase::Ended + } else if pointer_info.pointerFlags & winuser::POINTER_FLAG_UPDATE != 0 + { + TouchPhase::Moved + } else { + continue; + }, + location, + force, + id: pointer_info.pointerId as u64, + device_id: DEVICE_ID, + }), + }); + } + + SkipPointerFrameMessages(pointer_id); + } + 0 + } + winuser::WM_SETFOCUS => { use crate::event::WindowEvent::Focused; subclass_input.send_event(Event::WindowEvent { diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 58d86faf..34e9327d 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -4,7 +4,7 @@ use winapi::{self, shared::windef::HWND}; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::Window, }; @@ -67,6 +67,8 @@ impl WindowId { } } +#[macro_use] +mod util; mod dpi; mod drop_handler; mod event; @@ -74,6 +76,5 @@ mod event_loop; mod icon; mod monitor; mod raw_input; -mod util; mod window; mod window_state; diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index addb0175..cc2e1646 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -3,54 +3,64 @@ use winapi::{ minwindef::{BOOL, DWORD, LPARAM, TRUE, WORD}, windef::{HDC, HMONITOR, HWND, LPRECT, POINT}, }, - um::{wingdi, winnt::LONG, winuser}, + um::{wingdi, winuser}, }; use std::{ - collections::{HashSet, VecDeque}, + collections::{BTreeSet, VecDeque}, io, mem, ptr, }; use super::{util, EventLoop}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::platform::{ dpi::{dpi_to_scale_factor, get_monitor_dpi}, window::Window, }, }; -/// Win32 implementation of the main `MonitorHandle` object. #[derive(Derivative)] -#[derivative(Debug, Clone)] -pub struct MonitorHandle { - /// Monitor handle. - hmonitor: HMonitor, - #[derivative(Debug = "ignore")] - monitor_info: winuser::MONITORINFOEXW, - /// The system name of the monitor. - monitor_name: String, - /// True if this is the primary monitor. - primary: bool, - /// The position of the monitor in pixels on the desktop. - /// - /// A window that is positioned at these coordinates will overlap the monitor. - position: (i32, i32), - /// The current resolution in pixels on the monitor. - dimensions: (u32, u32), - /// DPI scale factor. - hidpi_factor: f64, +#[derivative(Debug, Clone, Eq, PartialEq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + #[derivative(Debug = "ignore", PartialEq = "ignore", Hash = "ignore")] + pub(crate) native_video_mode: wingdi::DEVMODEW, } +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(HMONITOR); + // Send is not implemented for HMONITOR, we have to wrap it and implement it manually. // For more info see: // https://github.com/retep998/winapi-rs/issues/360 // https://github.com/retep998/winapi-rs/issues/396 -#[derive(Debug, Clone)] -struct HMonitor(HMONITOR); -unsafe impl Send for HMonitor {} +unsafe impl Send for MonitorHandle {} unsafe extern "system" fn monitor_enum_proc( hmonitor: HMONITOR, @@ -59,7 +69,7 @@ unsafe extern "system" fn monitor_enum_proc( data: LPARAM, ) -> BOOL { let monitors = data as *mut VecDeque; - (*monitors).push_back(MonitorHandle::from_hmonitor(hmonitor)); + (*monitors).push_back(MonitorHandle::new(hmonitor)); TRUE // continue enumeration } @@ -79,12 +89,12 @@ pub fn available_monitors() -> VecDeque { pub fn primary_monitor() -> MonitorHandle { const ORIGIN: POINT = POINT { x: 0, y: 0 }; let hmonitor = unsafe { winuser::MonitorFromPoint(ORIGIN, winuser::MONITOR_DEFAULTTOPRIMARY) }; - MonitorHandle::from_hmonitor(hmonitor) + MonitorHandle::new(hmonitor) } pub fn current_monitor(hwnd: HWND) -> MonitorHandle { let hmonitor = unsafe { winuser::MonitorFromWindow(hwnd, winuser::MONITOR_DEFAULTTONEAREST) }; - MonitorHandle::from_hmonitor(hmonitor) + MonitorHandle::new(hmonitor) } impl EventLoop { @@ -125,73 +135,69 @@ pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result Self { - let monitor_info = get_monitor_info(hmonitor).expect("`GetMonitorInfoW` failed"); - let place = monitor_info.rcMonitor; - let dimensions = ( - (place.right - place.left) as u32, - (place.bottom - place.top) as u32, - ); - MonitorHandle { - hmonitor: HMonitor(hmonitor), - monitor_name: util::wchar_ptr_to_string(monitor_info.szDevice.as_ptr()), - primary: util::has_flag(monitor_info.dwFlags, winuser::MONITORINFOF_PRIMARY), - position: (place.left as i32, place.top as i32), - dimensions, - hidpi_factor: dpi_to_scale_factor(get_monitor_dpi(hmonitor).unwrap_or(96)), - monitor_info, - } + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor) } pub(crate) fn contains_point(&self, point: &POINT) -> bool { - let left = self.position.0 as LONG; - let right = left + self.dimensions.0 as LONG; - let top = self.position.1 as LONG; - let bottom = top + self.dimensions.1 as LONG; - point.x >= left && point.x <= right && point.y >= top && point.y <= bottom + 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 { - Some(self.monitor_name.clone()) + let monitor_info = get_monitor_info(self.0).unwrap(); + Some(util::wchar_ptr_to_string(monitor_info.szDevice.as_ptr())) } #[inline] pub fn native_identifier(&self) -> String { - self.monitor_name.clone() + self.name().unwrap() } #[inline] pub fn hmonitor(&self) -> HMONITOR { - self.hmonitor.0 + self.0 } #[inline] pub fn size(&self) -> PhysicalSize { - self.dimensions.into() + 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, + } } #[inline] pub fn position(&self) -> PhysicalPosition { - self.position.into() + let monitor_info = get_monitor_info(self.0).unwrap(); + PhysicalPosition { + x: monitor_info.rcMonitor.left as f64, + y: monitor_info.rcMonitor.top as f64, + } } #[inline] pub fn hidpi_factor(&self) -> f64 { - self.hidpi_factor + dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) } #[inline] - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { // EnumDisplaySettingsExW can return duplicate values (or some of the // fields are probably changing, but we aren't looking at those fields - // anyway), so we're using a HashSet deduplicate - let mut modes = HashSet::new(); + // anyway), so we're using a BTreeSet deduplicate + let mut modes = BTreeSet::new(); let mut i = 0; loop { unsafe { - let device_name = self.monitor_info.szDevice.as_ptr(); + let monitor_info = get_monitor_info(self.0).unwrap(); + let device_name = monitor_info.szDevice.as_ptr(); let mut mode: wingdi::DEVMODEW = mem::zeroed(); mode.dmSize = mem::size_of_val(&mode) as WORD; if winuser::EnumDisplaySettingsExW(device_name, i, &mut mode, 0) == 0 { @@ -205,10 +211,14 @@ impl MonitorHandle { | wingdi::DM_DISPLAYFREQUENCY; assert!(mode.dmFields & REQUIRED_FIELDS == REQUIRED_FIELDS); - modes.insert(VideoMode { - size: (mode.dmPelsWidth, mode.dmPelsHeight), - bit_depth: mode.dmBitsPerPel as u16, - refresh_rate: mode.dmDisplayFrequency as u16, + modes.insert(RootVideoMode { + video_mode: VideoMode { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate: mode.dmDisplayFrequency as u16, + monitor: self.clone(), + native_video_mode: mode, + }, }); } } diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 28513d30..96347849 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,6 +1,7 @@ use std::{ io, mem, ops::BitAnd, + os::raw::c_void, ptr, slice, sync::atomic::{AtomicBool, Ordering}, }; @@ -12,9 +13,44 @@ use winapi::{ minwindef::{BOOL, DWORD}, windef::{HWND, POINT, RECT}, }, - um::{winbase::lstrlenW, winuser}, + um::{ + libloaderapi::{GetProcAddress, LoadLibraryA}, + winbase::lstrlenW, + winnt::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, @@ -105,6 +141,16 @@ pub fn set_cursor_hidden(hidden: bool) { } } +pub fn get_cursor_clip() -> Result { + unsafe { + let mut rect: RECT = mem::zeroed(); + win_to_err(|| winuser::GetClipCursor(&mut rect)).map(|_| rect) + } +} + +/// Sets the cursor's clip rect. +/// +/// Note that calling this will automatically dispatch a `WM_MOUSEMOVE` event. pub fn set_cursor_clip(rect: Option) -> Result<(), io::Error> { unsafe { let rect_ptr = rect @@ -115,6 +161,19 @@ pub fn set_cursor_clip(rect: Option) -> Result<(), io::Error> { } } +pub fn get_desktop_rect() -> RECT { + unsafe { + let left = winuser::GetSystemMetrics(winuser::SM_XVIRTUALSCREEN); + let top = winuser::GetSystemMetrics(winuser::SM_YVIRTUALSCREEN); + RECT { + left, + top, + right: left + winuser::GetSystemMetrics(winuser::SM_CXVIRTUALSCREEN), + bottom: top + winuser::GetSystemMetrics(winuser::SM_CYVIRTUALSCREEN), + } + } +} + pub fn is_focused(window: HWND) -> bool { window == unsafe { winuser::GetActiveWindow() } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 577ecfd7..cb32c2ac 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1,6 +1,7 @@ #![cfg(target_os = "windows")] use parking_lot::Mutex; +use raw_window_handle::{windows::WindowsHandle, RawWindowHandle}; use std::{ cell::Cell, ffi::OsStr, @@ -46,7 +47,7 @@ use crate::{ window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorIcon, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, }; /// The Win32 implementation of the main `Window` object. @@ -211,6 +212,15 @@ impl Window { 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); } @@ -286,6 +296,15 @@ impl Window { 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(); + + 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_inner_size_physical(width, height); } @@ -327,7 +346,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::RESIZABLE, resizable) }); }); @@ -339,6 +358,15 @@ impl Window { self.window.0 } + #[inline] + pub fn raw_window_handle(&self) -> RawWindowHandle { + let handle = WindowsHandle { + hwnd: self.window.0 as *mut _, + ..WindowsHandle::empty() + }; + RawWindowHandle::Windows(handle) + } + #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { self.window_state.lock().mouse.cursor = cursor; @@ -421,80 +449,178 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::MAXIMIZED, maximized) }); }); } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { let window_state = self.window_state.lock(); window_state.fullscreen.clone() } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - unsafe { - let window = self.window.clone(); - let window_state = Arc::clone(&self.window_state); + pub fn set_fullscreen(&self, fullscreen: Option) { + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); - match &monitor { - &Some(RootMonitorHandle { ref inner }) => { - let (x, y): (i32, i32) = inner.position().into(); - let (width, height): (u32, u32) = inner.size().into(); + let mut window_state_lock = window_state.lock(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + if window_state_lock.fullscreen == fullscreen { + return; + } + window_state_lock.fullscreen = fullscreen.clone(); + drop(window_state_lock); - let mut monitor = monitor.clone(); - self.thread_executor.execute_in_thread(move || { - let mut window_state_lock = window_state.lock(); + self.thread_executor.execute_in_thread(move || { + let mut window_state_lock = window_state.lock(); - let client_rect = - util::get_client_rect(window.0).expect("get client rect failed!"); - window_state_lock.saved_window = Some(SavedWindow { - client_rect, - dpi_factor: window_state_lock.dpi_factor, - }); - - window_state_lock.fullscreen = monitor.take(); - WindowState::refresh_window_state( - window_state_lock, - window.0, - Some(RECT { - left: x, - top: y, - right: x + width as c_int, - bottom: y + height as c_int, - }), - ); - - mark_fullscreen(window.0, true); + // 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, }); } - &None => { - self.thread_executor.execute_in_thread(move || { - let mut window_state_lock = window_state.lock(); - window_state_lock.fullscreen = None; + _ => (), + } - if let Some(SavedWindow { - client_rect, - dpi_factor, - }) = window_state_lock.saved_window - { - window_state_lock.dpi_factor = dpi_factor; - window_state_lock.saved_window = None; + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (&None, &Some(Fullscreen::Exclusive(ref video_mode))) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(ref video_mode)), + ) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Exclusive(ref video_mode))) => + { + let monitor = video_mode.monitor(); - WindowState::refresh_window_state( - window_state_lock, + let mut display_name = OsStr::new(&monitor.inner.native_identifier()) + .encode_wide() + .collect::>(); + // `encode_wide` does not add a null-terminator but + // `ChangeDisplaySettingsExW` requires a null-terminated + // string, so add it + display_name.push(0); + + let mut native_video_mode = video_mode.video_mode.native_video_mode.clone(); + + let res = unsafe { + winuser::ChangeDisplaySettingsExW( + display_name.as_ptr(), + &mut native_video_mode, + std::ptr::null_mut(), + winuser::CDS_FULLSCREEN, + std::ptr::null_mut(), + ) + }; + + debug_assert!(res != winuser::DISP_CHANGE_BADFLAGS); + debug_assert!(res != winuser::DISP_CHANGE_BADMODE); + debug_assert!(res != winuser::DISP_CHANGE_BADPARAM); + debug_assert!(res != winuser::DISP_CHANGE_FAILED); + assert_eq!(res, winuser::DISP_CHANGE_SUCCESSFUL); + } + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let res = unsafe { + winuser::ChangeDisplaySettingsExW( + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + winuser::CDS_FULLSCREEN, + std::ptr::null_mut(), + ) + }; + + debug_assert!(res != winuser::DISP_CHANGE_BADFLAGS); + debug_assert!(res != winuser::DISP_CHANGE_BADMODE); + debug_assert!(res != winuser::DISP_CHANGE_BADPARAM); + debug_assert!(res != winuser::DISP_CHANGE_FAILED); + assert_eq!(res, winuser::DISP_CHANGE_SUCCESSFUL); + } + _ => (), + } + + unsafe { + // There are some scenarios where calling `ChangeDisplaySettingsExW` takes long + // enough to execute that the DWM thinks our program has frozen and takes over + // our program's window. When that happens, the `SetWindowPos` call below gets + // eaten and the window doesn't get set to the proper fullscreen position. + // + // Calling `PeekMessageW` here notifies Windows that our process is still running + // fine, taking control back from the DWM and ensuring that the `SetWindowPos` call + // below goes through. + let mut msg = mem::zeroed(); + winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0); + } + + // Update window style + WindowState::set_window_flags(window_state_lock, window.0, |f| { + f.set(WindowFlags::MARKER_FULLSCREEN, fullscreen.is_some()) + }); + + // 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(), + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + winuser::SetWindowPos( + window.0, + ptr::null_mut(), + position.0, + position.1, + size.0 as i32, + size.1 as i32, + winuser::SWP_ASYNCWINDOWPOS | winuser::SWP_NOZORDER, + ); + winuser::UpdateWindow(window.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; + drop(window_state_lock); + let client_rect = util::adjust_window_rect(window.0, client_rect).unwrap(); + + unsafe { + winuser::SetWindowPos( window.0, - Some(client_rect), + 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::UpdateWindow(window.0); } - - mark_fullscreen(window.0, false); - }); + } } } - } + + unsafe { + taskbar_mark_fullscreen(window.0, fullscreen.is_some()); + } + }); } #[inline] @@ -503,8 +629,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - let client_rect = util::get_client_rect(window.0).expect("get client rect failed!"); - WindowState::set_window_flags(window_state.lock(), window.0, Some(client_rect), |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::DECORATIONS, decorations) }); }); @@ -516,7 +641,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::ALWAYS_ON_TOP, always_on_top) }); }); @@ -769,9 +894,7 @@ unsafe fn init( let window_state = { let window_state = WindowState::new(&attributes, window_icon, taskbar_icon, dpi_factor); let window_state = Arc::new(Mutex::new(window_state)); - WindowState::set_window_flags(window_state.lock(), real_window.0, None, |f| { - *f = window_flags - }); + WindowState::set_window_flags(window_state.lock(), real_window.0, |f| *f = window_flags); window_state }; @@ -865,7 +988,7 @@ pub fn com_initialized() { // is activated. If the window is not fullscreen, the Shell falls back to // heuristics to determine how the window should be treated, which means // that it could still consider the window as fullscreen. :( -unsafe fn mark_fullscreen(handle: HWND, fullscreen: bool) { +unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { com_initialized(); TASKBAR_LIST.with(|task_bar_list_ptr| { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 9d494508..105d71e6 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -1,8 +1,7 @@ use crate::{ dpi::LogicalSize, - monitor::MonitorHandle, platform_impl::platform::{event_loop, icon::WinIcon, util}, - window::{CursorIcon, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; use parking_lot::MutexGuard; use std::{io, ptr}; @@ -29,10 +28,11 @@ pub struct WindowState { pub saved_window: Option, pub dpi_factor: f64, - pub fullscreen: Option, + 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 high_surrogate: Option, window_flags: WindowFlags, } @@ -84,6 +84,7 @@ bitflags! { WindowFlags::RESIZABLE.bits | WindowFlags::MAXIMIZED.bits ); + const FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; const INVISIBLE_AND_MASK = !WindowFlags::MAXIMIZED.bits; } @@ -114,6 +115,7 @@ impl WindowState { fullscreen: None, queued_out_of_band_redraw: false, + high_surrogate: None, window_flags: WindowFlags::empty(), } } @@ -122,32 +124,16 @@ impl WindowState { self.window_flags } - pub fn set_window_flags( - mut this: MutexGuard<'_, Self>, - window: HWND, - set_client_rect: Option, - f: F, - ) where + pub fn set_window_flags(mut this: MutexGuard<'_, Self>, window: HWND, f: F) + where F: FnOnce(&mut WindowFlags), { let old_flags = this.window_flags; f(&mut this.window_flags); - - let is_fullscreen = this.fullscreen.is_some(); - this.window_flags - .set(WindowFlags::MARKER_FULLSCREEN, is_fullscreen); let new_flags = this.window_flags; drop(this); - old_flags.apply_diff(window, new_flags, set_client_rect); - } - - pub fn refresh_window_state( - this: MutexGuard<'_, Self>, - window: HWND, - set_client_rect: Option, - ) { - Self::set_window_flags(this, window, set_client_rect, |_| ()); + old_flags.apply_diff(window, new_flags); } pub fn set_window_flags_in_place(&mut self, f: F) @@ -185,6 +171,7 @@ 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::VISIBLE) { self &= WindowFlags::INVISIBLE_AND_MASK; @@ -236,7 +223,7 @@ impl WindowFlags { } /// Adjust the window client rectangle to the return value, if present. - fn apply_diff(mut self, window: HWND, mut new: WindowFlags, set_client_rect: Option) { + fn apply_diff(mut self, window: HWND, mut new: WindowFlags) { self = self.mask(); new = new.mask(); @@ -295,45 +282,20 @@ impl WindowFlags { winuser::SetWindowLongW(window, winuser::GWL_STYLE, style as _); winuser::SetWindowLongW(window, winuser::GWL_EXSTYLE, style_ex as _); - match set_client_rect - .and_then(|r| util::adjust_window_rect_with_styles(window, style, style_ex, r)) - { - Some(client_rect) => { - let (x, y, w, h) = ( - client_rect.left, - client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, - ); - winuser::SetWindowPos( - window, - ptr::null_mut(), - x, - y, - w, - h, - winuser::SWP_NOZORDER - | winuser::SWP_FRAMECHANGED - | winuser::SWP_NOACTIVATE, - ); - } - None => { - // Refresh the window frame. - winuser::SetWindowPos( - window, - ptr::null_mut(), - 0, - 0, - 0, - 0, - winuser::SWP_NOZORDER - | winuser::SWP_NOMOVE - | winuser::SWP_NOSIZE - | winuser::SWP_FRAMECHANGED - | winuser::SWP_NOACTIVATE, - ); - } + let mut flags = winuser::SWP_NOZORDER + | winuser::SWP_NOMOVE + | winuser::SWP_NOSIZE + | winuser::SWP_FRAMECHANGED; + + // 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) { + flags |= winuser::SWP_NOACTIVATE; } + + // Refresh the window frame + winuser::SetWindowPos(window, ptr::null_mut(), 0, 0, 0, 0, flags); winuser::SendMessageW(window, *event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID, 0, 0); } } @@ -345,10 +307,25 @@ impl CursorFlags { let client_rect = util::get_client_rect(window)?; if util::is_focused(window) { - if self.contains(CursorFlags::GRABBED) { - util::set_cursor_clip(Some(client_rect))?; - } else { - util::set_cursor_clip(None)?; + let cursor_clip = match self.contains(CursorFlags::GRABBED) { + true => Some(client_rect), + false => None, + }; + + let rect_to_tuple = |rect: RECT| (rect.left, rect.top, rect.right, rect.bottom); + let active_cursor_clip = rect_to_tuple(util::get_cursor_clip()?); + let desktop_rect = rect_to_tuple(util::get_desktop_rect()); + + let active_cursor_clip = match desktop_rect == active_cursor_clip { + true => None, + false => Some(active_cursor_clip), + }; + + // We do this check because calling `set_cursor_clip` incessantly will flood the event + // loop with `WM_MOUSEMOVE` events, and `refresh_os_cursor` is called by `set_cursor_flags` + // which at times gets called once every iteration of the eventloop. + if active_cursor_clip != cursor_clip.map(rect_to_tuple) { + util::set_cursor_clip(cursor_clip)?; } } diff --git a/src/window.rs b/src/window.rs index dfde6713..1eaeec47 100644 --- a/src/window.rs +++ b/src/window.rs @@ -5,7 +5,7 @@ use crate::{ dpi::{LogicalPosition, LogicalSize}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, - monitor::{AvailableMonitorsIter, MonitorHandle}, + monitor::{AvailableMonitorsIter, MonitorHandle, VideoMode}, platform_impl, }; @@ -45,6 +45,18 @@ impl fmt::Debug for Window { } } +impl Drop for Window { + fn drop(&mut self) { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = self.fullscreen() { + self.set_fullscreen(None); + } + } +} + /// Identifier of a window. Unique for each window. /// /// Can be obtained with `window.id()`. @@ -110,7 +122,7 @@ pub struct WindowAttributes { /// Whether the window should be set as fullscreen upon creation. /// /// The default is `None`. - pub fullscreen: Option, + pub fullscreen: Option, /// The title of the window in the title bar. /// @@ -168,10 +180,11 @@ impl Default for WindowAttributes { } } } + impl WindowBuilder { /// Initializes a new `WindowBuilder` with default values. #[inline] - pub fn new() -> WindowBuilder { + pub fn new() -> Self { WindowBuilder { window: Default::default(), platform_specific: Default::default(), @@ -180,21 +193,21 @@ impl WindowBuilder { /// Requests the window to be of specific dimensions. #[inline] - pub fn with_inner_size(mut self, size: LogicalSize) -> WindowBuilder { + pub fn with_inner_size(mut self, size: LogicalSize) -> Self { self.window.inner_size = Some(size); self } /// Sets a minimum dimension size for the window #[inline] - pub fn with_min_inner_size(mut self, min_size: LogicalSize) -> WindowBuilder { + pub fn with_min_inner_size(mut self, min_size: LogicalSize) -> Self { self.window.min_inner_size = Some(min_size); self } /// Sets a maximum dimension size for the window #[inline] - pub fn with_max_inner_size(mut self, max_size: LogicalSize) -> WindowBuilder { + pub fn with_max_inner_size(mut self, max_size: LogicalSize) -> Self { self.window.max_inner_size = Some(max_size); self } @@ -210,57 +223,61 @@ impl WindowBuilder { /// /// Due to a bug in XFCE, this has no effect on Xfwm. #[inline] - pub fn with_resizable(mut self, resizable: bool) -> WindowBuilder { + pub fn with_resizable(mut self, resizable: bool) -> Self { self.window.resizable = resizable; self } /// Requests a specific title for the window. #[inline] - pub fn with_title>(mut self, title: T) -> WindowBuilder { + pub fn with_title>(mut self, title: T) -> Self { self.window.title = title.into(); self } - /// Sets the window fullscreen state. None means a normal window, Some(MonitorHandle) + /// Sets the window fullscreen state. None means a normal window, Some(Fullscreen) /// means a fullscreen window on that specific monitor + /// + /// ## Platform-specific + /// + /// - **Windows:** Screen saver is disabled in fullscreen mode. #[inline] - pub fn with_fullscreen(mut self, monitor: Option) -> WindowBuilder { + pub fn with_fullscreen(mut self, monitor: Option) -> Self { self.window.fullscreen = monitor; self } /// Requests maximized mode. #[inline] - pub fn with_maximized(mut self, maximized: bool) -> WindowBuilder { + pub fn with_maximized(mut self, maximized: bool) -> Self { self.window.maximized = maximized; self } /// Sets whether the window will be initially hidden or visible. #[inline] - pub fn with_visible(mut self, visible: bool) -> WindowBuilder { + pub fn with_visible(mut self, visible: bool) -> Self { self.window.visible = visible; self } /// Sets whether the background of the window should be transparent. #[inline] - pub fn with_transparent(mut self, transparent: bool) -> WindowBuilder { + pub fn with_transparent(mut self, transparent: bool) -> Self { self.window.transparent = transparent; self } /// Sets whether the window should have a border, a title bar, etc. #[inline] - pub fn with_decorations(mut self, decorations: bool) -> WindowBuilder { + pub fn with_decorations(mut self, decorations: bool) -> Self { self.window.decorations = decorations; self } /// Sets whether or not the window will always be on top of other windows. #[inline] - pub fn with_always_on_top(mut self, always_on_top: bool) -> WindowBuilder { + pub fn with_always_on_top(mut self, always_on_top: bool) -> Self { self.window.always_on_top = always_on_top; self } @@ -278,7 +295,7 @@ impl WindowBuilder { /// X11 has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// said, it's usually in the same ballpark as on Windows. #[inline] - pub fn with_window_icon(mut self, window_icon: Option) -> WindowBuilder { + pub fn with_window_icon(mut self, window_icon: Option) -> Self { self.window.window_icon = window_icon; self } @@ -291,7 +308,6 @@ impl WindowBuilder { self, window_target: &EventLoopWindowTarget, ) -> Result { - // building platform_impl::Window::new(&window_target.p, self.window, self.platform_specific) .map(|window| Window { window }) } @@ -398,9 +414,8 @@ impl Window { /// Modifies the position of the window. /// - /// See `outer_position` for more information about the coordinates. - /// - /// This is a no-op if the window has already been closed. + /// See `outer_position` for more information about the coordinates. This automatically un-maximizes the + /// window if it's maximized. /// /// ## Platform-specific /// @@ -430,7 +445,8 @@ impl Window { /// Modifies the inner size of the window. /// - /// See `inner_size` for more information about the values. + /// See `inner_size` for more information about the values. This automatically un-maximizes the + /// window if it's maximized. /// /// ## Platform-specific /// @@ -533,10 +549,27 @@ impl Window { /// /// ## Platform-specific /// + /// - **macOS:** `Fullscreen::Exclusive` provides true exclusive mode with a + /// video mode change. *Caveat!* macOS doesn't provide task switching (or + /// spaces!) while in exclusive fullscreen mode. This mode should be used + /// when a video mode change is desired, but for a better user experience, + /// borderless fullscreen might be preferred. + /// + /// `Fullscreen::Borderless` provides a borderless fullscreen window on a + /// separate space. This is the idiomatic way for fullscreen games to work + /// on macOS. See [`WindowExtMacOs::set_simple_fullscreen`][simple] if + /// separate spaces are not preferred. + /// + /// 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. + /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// + /// [simple]: + /// ../platform/macos/trait.WindowExtMacOS.html#tymethod.set_simple_fullscreen #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - self.window.set_fullscreen(monitor) + pub fn set_fullscreen(&self, fullscreen: Option) { + self.window.set_fullscreen(fullscreen) } /// Gets the window's current fullscreen state. @@ -545,7 +578,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { self.window.fullscreen() } @@ -553,10 +586,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS:** Can only be called on the main thread. Controls whether the status bar is hidden - /// via [`setPrefersStatusBarHidden`]. - /// - /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc + /// - **iOS:** Has no effect. #[inline] pub fn set_decorations(&self, decorations: bool) { self.window.set_decorations(decorations) @@ -692,6 +722,12 @@ impl Window { } } +unsafe impl raw_window_handle::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + self.window.raw_window_handle() + } +} + /// Describes the appearance of the mouse cursor. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -725,7 +761,9 @@ pub enum CursorIcon { Alias, Copy, NoDrop, + /// Indicates something can be grabbed. Grab, + /// Indicates something is grabbed. Grabbing, AllScroll, ZoomIn, @@ -754,3 +792,9 @@ impl Default for CursorIcon { CursorIcon::Default } } + +#[derive(Clone, Debug, PartialEq)] +pub enum Fullscreen { + Exclusive(VideoMode), + Borderless(MonitorHandle), +}