Compare commits

...

43 Commits

Author SHA1 Message Date
Kirill Chibisov
e9809ef54b Winit version 0.30.13 2026-03-02 22:49:04 +09:00
Kirill Chibisov
efb5b37fff chore: fix ci 2026-03-02 22:49:04 +09:00
Kirill Chibisov
a9baf5ecda fix(android): Populate KeyEvent.text via Key::to_text()
The `text` field on `KeyEvent` was hardcoded to `None` on Android,
making it impossible for custom `NativeActivity` subclasses that
show the IME to receive functional text input using *for example* the
existing `winit-egui` crate which relies on this field being set.

Use `Key::to_text()` on press events to derive `text` from
`logical_key`, matching the convention used by the Windows and macOS
backends.

Supposedly that doesn't include all kinds of special virtual unicode
keys, but at least the basics work this way.
2026-03-02 22:49:04 +09:00
Pedro Macedo
6bb43fd130 wayland: implement resize increments 2026-03-02 22:49:04 +09:00
Kotomine Shiki
17a73f4dd4 win32: fix ime setcontext lparam
Fixes #3893.
2026-03-02 22:49:04 +09:00
Takaranoao
bccc568345 fix(macOS): clamp IME selected_range to prevent substringToIndex crash
macOS native Pinyin IME can send a selected_range that exceeds the
marked text string length (e.g. index 8 for a 6-character string).
This caused an NSRangeException in substringToIndex:, crashing the
application with SIGABRT.

Clamp both location and end to the string's UTF-16 length before
calling substringToIndex.
2026-03-02 22:49:04 +09:00
SuchAFuriousDeath
69b8a07ae0 winit-x11: fix debug mode overflow panic in set_timestamp
Fixes #4484
2026-03-02 22:49:04 +09:00
Silico_Biomancer
3eb731f8b5 winit-x11: replace xfixes with x11rb in set_hittest
The xfixes implementation is not that reliable and rather simple to
replace, so use x11rb to implement the same functionality.

Fixes #4120.
Co-authored-by: avitran0 <holyhades64@gmail.com>
2026-03-02 22:49:04 +09:00
itsamine27
7035dd554f winit-win32: Fix ABI mismatch in INIT_MAIN_THREAD_ID
Fixes #4435.
2026-03-02 22:49:04 +09:00
Dan Harris
ab4c6bfc82 macOS: fix a crash when dragging non-file content onto window
Winit only supports text, thus we should ignore the rest
instead of crashing.
2026-03-02 22:49:04 +09:00
Kirill Chibisov
f6893a4390 Winit version 0.30.12 2025-07-27 21:59:42 +09:00
Kirill Chibisov
c0a8bedee2 chore: fix typos from updated typos tool 2025-07-27 21:59:42 +09:00
Kirill Chibisov
b248ecba31 winit: silence wasm on nightly
The lint is needed for stable, but is no longer present on nightly, so
silence it for the time being.
2025-07-27 21:59:42 +09:00
Kirill Chibisov
b49d34ebf0 ci/deny: add rustix
Will take a while to move to 1.0 for everyone.
2025-07-27 21:59:42 +09:00
Robert Wallis
cc43ea13d9 macOS: fix runtime crash on macos26 "type code 'q', but found 'Q'"
Fixes #4299.
2025-07-27 21:59:42 +09:00
Kirill Chibisov
911fad0af0 Winit version 0.30.11 2025-05-21 17:50:00 +09:00
Kirill Chibisov
2191eacfc8 chore: appease clippy 2025-05-21 17:50:00 +09:00
Kirill Chibisov
f7ac8127e3 wayland: fix pump events's loop drop deadlock 2025-05-21 17:50:00 +09:00
Varphone Wong
bd2b5cda8d windows: Fix crash in for Windows versions < 17763
In Windows versions < 17763, `GetProcAddress("#132")` from `uxtheme.dll`
also returns a non-null pointer. However, the function does not match
the expected `extern "system" fn() -> bool` prototype, which causes a
crash when it is called.

This fix ensures compatibility by adding proper checks to prevent such
crashes on older Windows versions.
2025-05-21 17:50:00 +09:00
Kirill Chibisov
3930a6334f ci/deny: allow scripts in zerocopy 2025-05-21 17:50:00 +09:00
Bruce Mitchener
17b5737972 Fix typos from updated typos tool (#4213) 2025-05-21 17:50:00 +09:00
Kirill Chibisov
f49a2a1827 clippy: fix casing in windows backend 2025-05-21 17:50:00 +09:00
Kirill Chibisov
2385410366 Winit version 0.30.10 2025-04-30 20:46:44 +09:00
Kirill Chibisov
6db1343c0b wayland: bump wayland-rs to avoid yanked release 2025-04-30 20:46:44 +09:00
Mitoma Ryo
a26899a75d windows: fix incorrect cursor_range calculation in Ime::Preedit
The `text` is retrieved as UTF-8 while `attributes` are based on UTF-16,
thus the offset was getting out of sync on some unicode payloads
like surrogate pairs.

Fixes #3967.
2025-04-30 20:46:44 +09:00
Mads Marquart
80bddda641 macOS: Fix monitors connected via certain Thunderbolt hubs
Instead of panicking, raise a warning and return `None` or similar.

Co-Authored-By: RJ <rj@metabrew.com>
2025-04-30 20:46:44 +09:00
Mads Marquart
5f1e9f6cc1 macOS: Store UUID in MonitorHandle instead of CGDirectDisplayID
The monitor UUID is what actually represents the monitor,
CGDirectDisplayID is closer in correspondence to a specific framebuffer.
2025-04-30 20:46:44 +09:00
Mads Marquart
57baf72741 Allow the user to register the application delegate on iOS
iOS parts of #3758.

This allows the user to override the application delegate themselves,
which opens several doors for customization that were previously closed.
To do this, we use notifications instead of a top-level application
delegate.

One effect of not providing an application delegate on iOS is that we no
longer act as-if the application successfully open all URLs there.

This is a breaking change, although unlikely to matter in practice,
since the return value of `application:didFinishLaunchingWithOptions:`
is seldom used by the system (and this is likely the preferred behaviour
anyhow).
2025-04-30 20:46:44 +09:00
Mads Marquart
da7a09658a fix: Support fractional refresh rates in video modes on macOS (#4191)
We were rounding the refresh rate before converting it to millihertz.
2025-04-30 20:46:44 +09:00
Mads Marquart
53321dc6f5 Swizzle sendEvent: instead of subclassing NSApplication
This is done to avoid order-dependent behavior that you'd otherwise
encounter where `EventLoop::new` had to be called at the beginning of
`fn main` to ensure that Winit's application was the one being
registered as the main application by calling `sharedApplication`.

Fixes https://github.com/rust-windowing/winit/issues/3772.

This should also make it (more) possible to use multiple versions of
Winit in the same application (though that's still untested).

Finally, it should allow the user to override `NSApplication` themselves
if they need to do that for some reason.
2025-04-30 20:46:44 +09:00
Mads Marquart
6556cde246 macOS: Close windows automatically when exiting
This disallows carrying over open windows between calls of
`run_app_on_demand` (which wasn't intended to be supported anyhow).
2025-04-30 20:46:44 +09:00
jpy794
7672fd5657 wayland: support fractional scale for custom cursor 2025-04-30 20:46:44 +09:00
Putta Khunchalee
847511672a wayland: add WindowExtWayland::xdg_toplevel
Fixes #4068.
2025-04-30 20:46:44 +09:00
Kirill Chibisov
53bbe6c273 wayland: ensure external loop is notified with pump_events
Spawn a thread when pump_events is used, so the external thread will
get woken-up correctly. This only happens when timeout was given.

Fixes #4183.
2025-04-30 20:46:44 +09:00
robtfm
a224b3de06 windows: add locked cursor 2025-04-30 20:46:44 +09:00
Kirill Chibisov
114599c2da wayland/fix: crash due consequent calls to set_cursor_grab
Only mark that the grab was applied when it actually got applied.
Previously there was an issue with grab being marked as applied without
a pointer over the window, when in reality it wasn't.

Fixes #4073.
2025-04-30 20:46:44 +09:00
Kirill Chibisov
aaecc92b62 chore: fix clippy issues 2025-04-30 20:46:44 +09:00
Kirill Chibisov
c6cfa048b0 x11:wayland: fix pump_events blocking with Wait
Using `Duration::Zero` with `Wait` polling mode was still blocking until
the event was actually delivered. Thus when `pump_events` API is used,
ensure that it's not happening.

Fixes #4130.
2025-04-30 20:46:44 +09:00
aloucks
c591089ece macOS: Make set_simple_fullscreen honor set_borderless_game (#4164)
* Prevent panic when calling set_simple_fullscreen(false) on macos

Calling `set_simple_fullscreen(false)` to restore the window after
a previous call to `set_simple_fullscreen(true)` panics with
`view must be installed in a window` in the call to `set_style_mask`
with the old style.

Moving the `set_style_mask` call after the frame has been resized
fixes the issue.

* Hide the doc and menubar on macos when using set_borderless_game
with set_simple_fullscreen
2025-04-30 20:46:44 +09:00
aloucks
ec7677d692 Fix a pause in the event loop when clicking the title bar on windows (#4136)
* Fix a pause in the event loop when clicking the title bar on windows

When clicking the title bar on Windows, to drag the window, there is
a noticible pause in continuous redraw requests. This was fixed
in #839 and then regressed in #1852. The cursor blinks in both
cases and is unrelated. The regression made the blink happen after
the pause instead of immediately.

* Update the event loop pause note on the WM_NCLBUTTONDOWN handler

The application example was also updated to optionally animate the fill color
in order to demonstrate continuous redraw without pauses in the event
loop.
2025-04-30 20:46:44 +09:00
rctlmk
b9b2f1643e Windows: add IconExtWindows::from_resource_name (#4137) 2025-04-30 20:46:44 +09:00
Kirill Chibisov
1db15b6875 chore: fix clippy lints 2025-04-30 20:46:44 +09:00
Mads Marquart
37a4394a3e Remove apple/appkit/window.rs
Accidentally added in 485ae90aae.
2025-03-17 23:50:15 +01:00
59 changed files with 1159 additions and 702 deletions

View File

@@ -111,8 +111,13 @@ jobs:
cargo generate-lockfile
cargo update -p ahash --precise 0.8.7
cargo update -p bumpalo --precise 3.14.0
cargo update -p softbuffer --precise 0.4.0
cargo update -p objc2-encode --precise 4.0.3
cargo update -p orbclient --precise 0.3.47
cargo update -p image --precise 0.25.0
cargo update -p gethostname@1.1.0 --precise 1.0.2
cargo update -p unicode-ident --precise 1.0.10
cargo update -p syn --precise 2.0.114
- name: Install GCC Multilib
if: (matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686')

View File

@@ -1,6 +1,6 @@
[package]
name = "winit"
version = "0.30.9"
version = "0.30.13"
authors = [
"The winit contributors",
"Pierre Krieger <pierre.krieger1708@gmail.com>",
@@ -117,12 +117,12 @@ android-activity = "0.6.0"
ndk = { version = "0.9.0", default-features = false }
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
block2 = "0.5.1"
core-foundation = "0.9.3"
objc2 = "0.5.2"
objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.23.1"
block2 = "0.5.1"
[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation]
version = "0.2.2"
@@ -180,11 +180,13 @@ features = [
[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation]
version = "0.2.2"
features = [
"block2",
"dispatch",
"NSArray",
"NSEnumerator",
"NSGeometry",
"NSObjCRuntime",
"NSOperation",
"NSString",
"NSProcessInfo",
"NSThread",
@@ -266,14 +268,14 @@ sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-feature
"calloop",
], optional = true }
sctk-adwaita = { version = "0.10.1", default-features = false, optional = true }
wayland-backend = { version = "0.3.5", default-features = false, features = [
wayland-backend = { version = "0.3.10", default-features = false, features = [
"client_system",
], optional = true }
wayland-client = { version = "0.31.4", optional = true }
wayland-protocols = { version = "0.32.2", features = [
wayland-client = { version = "0.31.10", optional = true }
wayland-protocols = { version = "0.32.8", features = [
"staging",
], optional = true }
wayland-protocols-plasma = { version = "0.3.2", features = [
wayland-protocols-plasma = { version = "0.3.8", features = [
"client",
], optional = true }
x11-dl = { version = "2.19.1", optional = true }

View File

@@ -8,7 +8,7 @@
```toml
[dependencies]
winit = "0.30.9"
winit = "0.30.13"
```
## [Documentation](https://docs.rs/winit)
@@ -33,6 +33,10 @@ Winit is designed to be a low-level brick in a hierarchy of libraries. Consequen
show something on the window you need to use the platform-specific getters provided by winit, or
another library.
## CONTRIBUTING
For contributing guidelines see [CONTRIBUTING.md](./CONTRIBUTING.md).
## MSRV Policy
This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to

View File

@@ -40,8 +40,10 @@ multiple-versions = "deny"
skip = [
{ crate = "raw-window-handle", reason = "we depend on multiple behind features" },
{ crate = "bitflags@1", reason = "the ecosystem is in the process of migrating" },
{ crate = "rustix@0.38", reason = "the ecosystem is in the process of migrating" },
{ crate = "linux-raw-sys@0.4", reason = "the ecosystem is in the process of migrating" },
]
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
[bans.build]
include-archives = true
@@ -53,6 +55,10 @@ allow = [
]
crate = "android-activity"
[[bans.build.bypass]]
allow-globs = ["ci/*", "githooks/*", "cargo.sh"]
crate = "zerocopy"
[[bans.build.bypass]]
allow-globs = ["freetype2/*"]
crate = "freetype-sys"

View File

@@ -250,7 +250,7 @@
- On Web, fix some `WindowBuilder` methods doing nothing.
- On Web, fix some `Window` methods using incorrect HTML attributes instead of CSS properties.
- On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events.
- On Web, fix touch input not gaining or loosing focus.
- On Web, fix touch input not gaining or losing focus.
- On Web, fix touch location to be as accurate as mouse position.
- On Web, handle coalesced pointer events, which increases the resolution of pointer inputs.
- On Web, implement `Window::focus_window()`.

View File

@@ -1,3 +1,61 @@
## 0.30.13
### Added
- On Wayland, add `Window::set_resize_increments`.
### Fixed
- On macOS, fixed crash when dragging non-file content onto window.
- On X11, fix `set_hittest` not working on some window managers.
- On X11, fix debug mode overflow panic in `set_timestamp`.
- On macOS, fix crash in `set_marked_text` when native Pinyin IME sends out-of-bounds `selected_range`.
- On Windows, fix `WM_IME_SETCONTEXT` IME UI flag masking on `lParam`.
- On Android, populate `KeyEvent::text` and `KeyEvent::text_with_all_modifiers` via `Key::to_text()`.
## 0.30.12
### Fixed
- On macOS, fix crash on macOS 26 by using objc2's `relax-sign-encoding` feature.
## 0.30.11
### Fixed
- On Windows, fixed crash in should_apps_use_dark_mode() for Windows versions < 17763.
- On Wayland, fixed `pump_events` driven loop deadlocking when loop was not drained before exit.
## 0.30.10
### Added
- On Windows, add `IconExtWindows::from_resource_name`.
- On Windows, add `CursorGrabMode::Locked`.
- On Wayland, add `WindowExtWayland::xdg_toplevel`.
### Changed
- On macOS, no longer need control of the main `NSApplication` class (which means you can now override it yourself).
- On iOS, remove custom application delegates. You are now allowed to override the
application delegate yourself.
- On iOS, no longer act as-if the application successfully open all URLs. Override
`application:didFinishLaunchingWithOptions:` and provide the desired behaviour yourself.
### Fixed
- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw.
- On macOS, `WindowExtMacOS::set_simple_fullscreen` now honors `WindowExtMacOS::set_borderless_game`
- On X11 and Wayland, fixed pump_events with `Some(Duration::Zero)` blocking with `Wait` polling mode
- On Wayland, fixed a crash when consequently calling `set_cursor_grab` without pointer focus.
- On Wayland, ensure that external event loop is woken-up when using pump_events and integrating via `FD`.
- On Wayland, apply fractional scaling to custom cursors.
- On macOS, fixed `run_app_on_demand` returning without closing open windows.
- On macOS, fixed `VideoMode::refresh_rate_millihertz` for fractional refresh rates.
- On macOS, store monitor handle to avoid panics after going in/out of sleep.
- On macOS, allow certain invalid monitor handles and return `None` instead of panicking.
- On Windows, fixed `Ime::Preedit` cursor offset calculation.
## 0.30.9
### Changed

View File

@@ -621,7 +621,7 @@ pub struct KeyEvent {
///
/// # Example
///
/// In games, you often want to ignore repated key events - this can be
/// In games, you often want to ignore repeated key events - this can be
/// done by ignoring events where this property is set.
///
/// ```
@@ -1152,7 +1152,7 @@ mod tests {
#[test]
fn ensure_attrs_do_not_panic() {
foreach_event!(|event: event::Event<()>| {
let _ = format!("{:?}", event);
let _ = format!("{event:?}");
});
let _ = event::StartCause::Init.clone();

View File

@@ -1232,7 +1232,7 @@ pub enum NamedKey {
Dimmer,
/// Swap video sources. (`VK_DISPLAY_SWAP`)
DisplaySwap,
/// Select Digital Video Rrecorder. (`KEYCODE_DVR`)
/// Select Digital Video Recorder. (`KEYCODE_DVR`)
DVR,
/// Exit the current application. (`VK_EXIT`)
Exit,

View File

@@ -165,7 +165,6 @@
//! [`Window`]: window::Window
//! [`WindowId`]: window::WindowId
//! [`WindowAttributes`]: window::WindowAttributes
//! [window_new]: window::Window::new
//! [`create_window`]: event_loop::ActiveEventLoop::create_window
//! [`Window::id()`]: window::Window::id
//! [`WindowEvent`]: event::WindowEvent
@@ -183,8 +182,11 @@
#![cfg_attr(clippy, deny(warnings))]
// Doc feature labels can be tested locally by running RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly
// doc
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc, docsrs)))]
#![cfg_attr(docsrs, feature(doc_cfg), doc(auto_cfg(hide(doc, docsrs))))]
#![allow(clippy::missing_safety_doc)]
#![warn(clippy::uninlined_format_args)]
// TODO: wasm-binding needs to be updated for that to be resolved, for now just silence it.
#![cfg_attr(web_platform, allow(unknown_lints, renamed_and_removed_lints, wasm_c_abi))]
#[cfg(feature = "rwh_04")]
pub use rwh_04 as raw_window_handle_04;

View File

@@ -62,7 +62,7 @@
//! If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building
//! with `cargo apk`, then the minimal changes would be:
//! 1. Remove `ndk-glue` from your `Cargo.toml`
//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.9",
//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.13",
//! features = [ "android-native-activity" ] }`
//! 3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc
//! macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize

View File

@@ -3,11 +3,14 @@
//! Winit has an OS requirement of iOS 8 or higher, and is regularly tested on
//! iOS 9.3.
//!
//! ## Window initialization
//!
//! iOS's main `UIApplicationMain` does some init work that's required by all
//! UI-related code (see issue [#1705]). It is best to create your windows
//! inside `Event::Resumed`.
//! inside [`ApplicationHandler::resumed`].
//!
//! [#1705]: https://github.com/rust-windowing/winit/issues/1705
//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed
//!
//! ## Building app
//!
@@ -63,6 +66,16 @@
//! opengl will result in segfault.
//!
//! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed.
//!
//! ## Custom `UIApplicationDelegate`
//!
//! Winit usually handles everything related to the lifecycle events of the application. Sometimes,
//! though, you might want to access some of the more niche stuff that [the application
//! delegate][app-delegate] provides. This functionality is not exposed directly in Winit, since it
//! would increase the API surface by quite a lot. Instead, Winit guarantees that it will not
//! register an application delegate, so you can set up a custom one in a nib file instead.
//!
//! [app-delegate]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate?language=objc
use std::os::raw::c_void;

View File

@@ -3,16 +3,84 @@
//! Winit has an OS requirement of macOS 10.11 or higher (same as Rust
//! itself), and is regularly tested on macOS 10.14.
//!
//! ## Window initialization
//!
//! A lot of functionality expects the application to be ready before you
//! start doing anything; this includes creating windows, fetching monitors,
//! drawing, and so on, see issues [#2238], [#2051] and [#2087].
//!
//! If you encounter problems, you should try doing your initialization inside
//! `Event::Resumed`.
//! [`ApplicationHandler::resumed`].
//!
//! [#2238]: https://github.com/rust-windowing/winit/issues/2238
//! [#2051]: https://github.com/rust-windowing/winit/issues/2051
//! [#2087]: https://github.com/rust-windowing/winit/issues/2087
//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed
//!
//! ## Custom `NSApplicationDelegate`
//!
//! Winit usually handles everything related to the lifecycle events of the application. Sometimes,
//! though, you might want to do more niche stuff, such as [handle when the user re-activates the
//! application][reopen]. Such functionality is not exposed directly in Winit, since it would
//! increase the API surface by quite a lot.
//!
//! [reopen]: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen?language=objc
//!
//! Instead, Winit guarantees that it will not register an application delegate, so the solution is
//! to register your own application delegate, as outlined in the following example (see
//! `objc2-app-kit` for more detailed information).
#![cfg_attr(target_os = "macos", doc = "```")]
#![cfg_attr(not(target_os = "macos"), doc = "```ignore")]
//! use objc2::rc::Retained;
//! use objc2::runtime::ProtocolObject;
//! use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
//! use objc2_app_kit::{NSApplication, NSApplicationDelegate};
//! use objc2_foundation::{NSArray, NSURL, MainThreadMarker, NSObject, NSObjectProtocol};
//! use winit::event_loop::EventLoop;
//!
//! declare_class!(
//! struct AppDelegate;
//!
//! unsafe impl ClassType for AppDelegate {
//! type Super = NSObject;
//! type Mutability = mutability::MainThreadOnly;
//! const NAME: &'static str = "MyAppDelegate";
//! }
//!
//! impl DeclaredClass for AppDelegate {}
//!
//! unsafe impl NSObjectProtocol for AppDelegate {}
//!
//! unsafe impl NSApplicationDelegate for AppDelegate {
//! #[method(application:openURLs:)]
//! fn application_openURLs(&self, application: &NSApplication, urls: &NSArray<NSURL>) {
//! // Note: To specifically get `application:openURLs:` to work, you _might_
//! // have to bundle your application. This is not done in this example.
//! println!("open urls: {application:?}, {urls:?}");
//! }
//! }
//! );
//!
//! impl AppDelegate {
//! fn new(mtm: MainThreadMarker) -> Retained<Self> {
//! unsafe { msg_send_id![super(mtm.alloc().set_ivars(())), init] }
//! }
//! }
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let event_loop = EventLoop::new()?;
//!
//! let mtm = MainThreadMarker::new().unwrap();
//! let delegate = AppDelegate::new(mtm);
//! // Important: Call `sharedApplication` after `EventLoop::new`,
//! // doing it before is not yet supported.
//! let app = NSApplication::sharedApplication(mtm);
//! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
//!
//! // event_loop.run_app(&mut my_app);
//! Ok(())
//! }
//! ```
use std::os::raw::c_void;
@@ -95,7 +163,9 @@ pub trait WindowExtMacOS {
/// Getter for the [`WindowExtMacOS::set_option_as_alt`].
fn option_as_alt(&self) -> OptionAsAlt;
/// Disable the Menu Bar and Dock in Borderless Fullscreen mode. Useful for games.
/// Disable the Menu Bar and Dock in Simple or Borderless Fullscreen mode. Useful for games.
/// The effect is applied when [`WindowExtMacOS::set_simple_fullscreen`] or
/// [`Window::set_fullscreen`] is called.
fn set_borderless_game(&self, borderless_game: bool);
/// Getter for the [`WindowExtMacOS::set_borderless_game`].

View File

@@ -51,19 +51,19 @@ pub trait EventLoopExtPumpEvents {
/// buffered and handled outside of Winit include:
/// - `RedrawRequested` events, used to schedule rendering.
///
/// macOS for example uses a `drawRect` callback to drive rendering
/// within applications and expects rendering to be finished before
/// the `drawRect` callback returns.
/// macOS for example uses a `drawRect` callback to drive rendering
/// within applications and expects rendering to be finished before
/// the `drawRect` callback returns.
///
/// For portability it's strongly recommended that applications should
/// keep their rendering inside the closure provided to Winit.
/// For portability it's strongly recommended that applications should
/// keep their rendering inside the closure provided to Winit.
/// - Any lifecycle events, such as `Suspended` / `Resumed`.
///
/// The handling of these events needs to be synchronized with the
/// operating system and it would never be appropriate to buffer a
/// notification that your application has been suspended or resumed and
/// then handled that later since there would always be a chance that
/// other lifecycle events occur while the event is buffered.
/// The handling of these events needs to be synchronized with the
/// operating system and it would never be appropriate to buffer a
/// notification that your application has been suspended or resumed and
/// then handled that later since there would always be a chance that
/// other lifecycle events occur while the event is buffered.
///
/// ## Supported Platforms
///

View File

@@ -63,8 +63,8 @@ pub trait EventLoopExtRunOnDemand {
/// are delivered via callbacks based on an event loop that is internal to the browser itself.
/// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS.
#[cfg_attr(not(web_platform), doc = "[^1]: `spawn()` is only available on `wasm` platforms.")]
#[rustfmt::skip]
///
#[rustfmt::skip]
/// [`exit()`]: ActiveEventLoop::exit()
/// [`set_control_flow()`]: ActiveEventLoop::set_control_flow()
fn run_app_on_demand<A: ApplicationHandler<Self::UserEvent>>(

View File

@@ -13,6 +13,10 @@
//! * `wayland-csd-adwaita` (default).
//! * `wayland-csd-adwaita-crossfont`.
//! * `wayland-csd-adwaita-notitle`.
use std::ffi::c_void;
use std::ptr::NonNull;
use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder};
use crate::monitor::MonitorHandle;
use crate::window::{Window, WindowAttributes};
@@ -72,9 +76,25 @@ impl<T> EventLoopBuilderExtWayland for EventLoopBuilder<T> {
}
/// Additional methods on [`Window`] that are specific to Wayland.
pub trait WindowExtWayland {}
///
/// [`Window`]: crate::window::Window
pub trait WindowExtWayland {
/// Returns `xdg_toplevel` of the window or [`None`] if the window is X11 window.
fn xdg_toplevel(&self) -> Option<NonNull<c_void>>;
}
impl WindowExtWayland for Window {}
impl WindowExtWayland for Window {
#[inline]
fn xdg_toplevel(&self) -> Option<NonNull<c_void>> {
#[allow(clippy::single_match)]
match &self.window {
#[cfg(x11_platform)]
crate::platform_impl::Window::X(_) => None,
#[cfg(wayland_platform)]
crate::platform_impl::Window::Wayland(window) => window.xdg_toplevel(),
}
}
}
/// Additional methods on [`WindowAttributes`] that are specific to Wayland.
pub trait WindowAttributesExtWayland {

View File

@@ -660,6 +660,17 @@ impl DeviceIdExtWindows for DeviceId {
}
/// Additional methods on `Icon` that are specific to Windows.
///
/// Windows icons can be created from files, or from the [`embedded resources`](https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files).
///
/// The `ICON` resource definition statement use the following syntax:
/// ```rc
/// nameID ICON filename
/// ```
/// `nameID` is a unique name or a 16-bit unsigned integer value identifying the resource,
/// `filename` is the name of the file that contains the resource.
///
/// More information about the `ICON` resource can be found at [`Microsoft Learn`](https://learn.microsoft.com/en-us/windows/win32/menurc/icon-resource) portal.
pub trait IconExtWindows: Sized {
/// Create an icon from a file path.
///
@@ -671,7 +682,12 @@ pub trait IconExtWindows: Sized {
fn from_path<P: AsRef<Path>>(path: P, size: Option<PhysicalSize<u32>>)
-> Result<Self, BadIcon>;
/// Create an icon from a resource embedded in this executable or library.
/// Create an icon from a resource embedded in this executable or library by its ordinal id.
///
/// The valid `ordinal` values range from 1 to [`u16::MAX`] (inclusive). The value `0` is an
/// invalid ordinal id, but it can be used with [`from_resource_name`] as `"0"`.
///
/// [`from_resource_name`]: IconExtWindows::from_resource_name
///
/// Specify `size` to load a specific icon size from the file, or `None` to load the default
/// icon size from the file.
@@ -679,6 +695,55 @@ pub trait IconExtWindows: Sized {
/// In cases where the specified size does not exist in the file, Windows may perform scaling
/// to get an icon of the desired size.
fn from_resource(ordinal: u16, size: Option<PhysicalSize<u32>>) -> Result<Self, BadIcon>;
/// Create an icon from a resource embedded in this executable or library by its name.
///
/// Specify `size` to load a specific icon size from the file, or `None` to load the default
/// icon size from the file.
///
/// In cases where the specified size does not exist in the file, Windows may perform scaling
/// to get an icon of the desired size.
///
/// # Notes
///
/// Consider the following resource definition statements:
/// ```rc
/// app ICON "app.ico"
/// 1 ICON "a.ico"
/// 0027 ICON "custom.ico"
/// 0 ICON "alt.ico"
/// ```
///
/// Due to some internal implementation details of the resource embedding/loading process on
/// Windows platform, strings that can be interpreted as 16-bit unsigned integers (`"1"`,
/// `"002"`, etc.) cannot be used as valid resource names, and instead should be passed into
/// [`from_resource`]:
///
/// [`from_resource`]: IconExtWindows::from_resource
///
/// ```rust,no_run
/// use winit::platform::windows::IconExtWindows;
/// use winit::window::Icon;
///
/// assert!(Icon::from_resource_name("app", None).is_ok());
/// assert!(Icon::from_resource(1, None).is_ok());
/// assert!(Icon::from_resource(27, None).is_ok());
/// assert!(Icon::from_resource_name("27", None).is_err());
/// assert!(Icon::from_resource_name("0027", None).is_err());
/// ```
///
/// While `0` cannot be used as an ordinal id (see [`from_resource`]), it can be used as a
/// name:
///
/// [`from_resource`]: IconExtWindows::from_resource
///
/// ```rust,no_run
/// # use winit::platform::windows::IconExtWindows;
/// # use winit::window::Icon;
/// assert!(Icon::from_resource_name("0", None).is_ok());
/// assert!(Icon::from_resource(0, None).is_err());
/// ```
fn from_resource_name(name: &str, size: Option<PhysicalSize<u32>>) -> Result<Self, BadIcon>;
}
impl IconExtWindows for Icon {
@@ -694,4 +759,9 @@ impl IconExtWindows for Icon {
let win_icon = crate::platform_impl::WinIcon::from_resource(ordinal, size)?;
Ok(Icon { inner: win_icon })
}
fn from_resource_name(name: &str, size: Option<PhysicalSize<u32>>) -> Result<Self, BadIcon> {
let win_icon = crate::platform_impl::WinIcon::from_resource_name(name, size)?;
Ok(Icon { inner: win_icon })
}
}

View File

@@ -446,6 +446,13 @@ impl<T: 'static> EventLoop<T> {
&mut self.combining_accent,
);
let logical_key = keycodes::to_logical(key_char, keycode);
let text = if state == event::ElementState::Pressed {
logical_key.to_text().map(smol_str::SmolStr::new)
} else {
None
};
let event = event::Event::WindowEvent {
window_id: window::WindowId(WindowId),
event: event::WindowEvent::KeyboardInput {
@@ -453,10 +460,10 @@ impl<T: 'static> EventLoop<T> {
event: event::KeyEvent {
state,
physical_key: keycodes::to_physical_key(keycode),
logical_key: keycodes::to_logical(key_char, keycode),
logical_key,
location: keycodes::to_location(keycode),
repeat: key.repeat_count() > 0,
text: None,
text,
platform_specific: KeyEventExtra {},
},
is_synthetic: false,

View File

@@ -1,369 +0,0 @@
#![allow(clippy::unnecessary_cast)]
use dpi::{Position, Size};
use objc2::rc::{autoreleasepool, Retained};
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{NSResponder, NSWindow};
use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject};
use super::event_loop::ActiveEventLoop;
use super::window_delegate::WindowDelegate;
use crate::error::RequestError;
use crate::monitor::MonitorHandle as CoreMonitorHandle;
use crate::window::{
Cursor, Fullscreen, Icon, ImePurpose, Theme, UserAttentionType, Window as CoreWindow,
WindowAttributes, WindowButtons, WindowId, WindowLevel,
};
pub(crate) struct Window {
window: MainThreadBound<Retained<WinitWindow>>,
/// The window only keeps a weak reference to this, so we must keep it around here.
delegate: MainThreadBound<Retained<WindowDelegate>>,
}
impl Window {
pub(crate) fn new(
window_target: &ActiveEventLoop,
attributes: WindowAttributes,
) -> Result<Self, RequestError> {
let mtm = window_target.mtm;
let delegate =
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
Ok(Window {
window: MainThreadBound::new(delegate.window().retain(), mtm),
delegate: MainThreadBound::new(delegate, mtm),
})
}
pub(crate) fn maybe_wait_on_main<R: Send>(
&self,
f: impl FnOnce(&WindowDelegate) -> R + Send,
) -> R {
self.delegate.get_on_main(|delegate| f(delegate))
}
#[cfg(feature = "rwh_06")]
#[inline]
pub(crate) fn raw_window_handle_rwh_06(
&self,
) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
if let Some(mtm) = MainThreadMarker::new() {
Ok(self.delegate.get(mtm).raw_window_handle_rwh_06())
} else {
Err(rwh_06::HandleError::Unavailable)
}
}
#[cfg(feature = "rwh_06")]
#[inline]
pub(crate) fn raw_display_handle_rwh_06(
&self,
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new()))
}
}
impl Drop for Window {
fn drop(&mut self) {
// Restore the video mode.
if matches!(self.fullscreen(), Some(Fullscreen::Exclusive(_))) {
self.set_fullscreen(None);
}
self.window.get_on_main(|window| autoreleasepool(|_| window.close()))
}
}
#[cfg(feature = "rwh_06")]
impl rwh_06::HasDisplayHandle for Window {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = self.raw_display_handle_rwh_06()?;
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
}
}
#[cfg(feature = "rwh_06")]
impl rwh_06::HasWindowHandle for Window {
fn window_handle(&self) -> Result<rwh_06::WindowHandle<'_>, rwh_06::HandleError> {
let raw = self.raw_window_handle_rwh_06()?;
unsafe { Ok(rwh_06::WindowHandle::borrow_raw(raw)) }
}
}
impl CoreWindow for Window {
fn id(&self) -> crate::window::WindowId {
self.maybe_wait_on_main(|delegate| delegate.id())
}
fn scale_factor(&self) -> f64 {
self.maybe_wait_on_main(|delegate| delegate.scale_factor())
}
fn request_redraw(&self) {
self.maybe_wait_on_main(|delegate| delegate.request_redraw());
}
fn pre_present_notify(&self) {
self.maybe_wait_on_main(|delegate| delegate.pre_present_notify());
}
fn reset_dead_keys(&self) {
self.maybe_wait_on_main(|delegate| delegate.reset_dead_keys());
}
fn inner_position(&self) -> Result<dpi::PhysicalPosition<i32>, RequestError> {
Ok(self.maybe_wait_on_main(|delegate| delegate.inner_position()))
}
fn outer_position(&self) -> Result<dpi::PhysicalPosition<i32>, RequestError> {
Ok(self.maybe_wait_on_main(|delegate| delegate.outer_position()))
}
fn set_outer_position(&self, position: Position) {
self.maybe_wait_on_main(|delegate| delegate.set_outer_position(position));
}
fn surface_size(&self) -> dpi::PhysicalSize<u32> {
self.maybe_wait_on_main(|delegate| delegate.surface_size())
}
fn request_surface_size(&self, size: Size) -> Option<dpi::PhysicalSize<u32>> {
self.maybe_wait_on_main(|delegate| delegate.request_surface_size(size))
}
fn outer_size(&self) -> dpi::PhysicalSize<u32> {
self.maybe_wait_on_main(|delegate| delegate.outer_size())
}
fn set_min_surface_size(&self, min_size: Option<Size>) {
self.maybe_wait_on_main(|delegate| delegate.set_min_surface_size(min_size))
}
fn set_max_surface_size(&self, max_size: Option<Size>) {
self.maybe_wait_on_main(|delegate| delegate.set_max_surface_size(max_size));
}
fn surface_resize_increments(&self) -> Option<dpi::PhysicalSize<u32>> {
self.maybe_wait_on_main(|delegate| delegate.surface_resize_increments())
}
fn set_surface_resize_increments(&self, increments: Option<Size>) {
self.maybe_wait_on_main(|delegate| delegate.set_surface_resize_increments(increments));
}
fn set_title(&self, title: &str) {
self.maybe_wait_on_main(|delegate| delegate.set_title(title));
}
fn set_transparent(&self, transparent: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_transparent(transparent));
}
fn set_blur(&self, blur: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_blur(blur));
}
fn set_visible(&self, visible: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_visible(visible));
}
fn is_visible(&self) -> Option<bool> {
self.maybe_wait_on_main(|delegate| delegate.is_visible())
}
fn set_resizable(&self, resizable: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_resizable(resizable))
}
fn is_resizable(&self) -> bool {
self.maybe_wait_on_main(|delegate| delegate.is_resizable())
}
fn set_enabled_buttons(&self, buttons: WindowButtons) {
self.maybe_wait_on_main(|delegate| delegate.set_enabled_buttons(buttons))
}
fn enabled_buttons(&self) -> WindowButtons {
self.maybe_wait_on_main(|delegate| delegate.enabled_buttons())
}
fn set_minimized(&self, minimized: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_minimized(minimized));
}
fn is_minimized(&self) -> Option<bool> {
self.maybe_wait_on_main(|delegate| delegate.is_minimized())
}
fn set_maximized(&self, maximized: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_maximized(maximized));
}
fn is_maximized(&self) -> bool {
self.maybe_wait_on_main(|delegate| delegate.is_maximized())
}
fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
self.maybe_wait_on_main(|delegate| delegate.set_fullscreen(fullscreen.map(Into::into)))
}
fn fullscreen(&self) -> Option<Fullscreen> {
self.maybe_wait_on_main(|delegate| delegate.fullscreen().map(Into::into))
}
fn set_decorations(&self, decorations: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_decorations(decorations));
}
fn is_decorated(&self) -> bool {
self.maybe_wait_on_main(|delegate| delegate.is_decorated())
}
fn set_window_level(&self, level: WindowLevel) {
self.maybe_wait_on_main(|delegate| delegate.set_window_level(level));
}
fn set_window_icon(&self, window_icon: Option<Icon>) {
self.maybe_wait_on_main(|delegate| delegate.set_window_icon(window_icon));
}
fn set_ime_cursor_area(&self, position: Position, size: Size) {
self.maybe_wait_on_main(|delegate| delegate.set_ime_cursor_area(position, size));
}
fn set_ime_allowed(&self, allowed: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_ime_allowed(allowed));
}
fn set_ime_purpose(&self, purpose: ImePurpose) {
self.maybe_wait_on_main(|delegate| delegate.set_ime_purpose(purpose));
}
fn focus_window(&self) {
self.maybe_wait_on_main(|delegate| delegate.focus_window());
}
fn has_focus(&self) -> bool {
self.maybe_wait_on_main(|delegate| delegate.has_focus())
}
fn request_user_attention(&self, request_type: Option<UserAttentionType>) {
self.maybe_wait_on_main(|delegate| delegate.request_user_attention(request_type));
}
fn set_theme(&self, theme: Option<Theme>) {
self.maybe_wait_on_main(|delegate| delegate.set_theme(theme));
}
fn theme(&self) -> Option<Theme> {
self.maybe_wait_on_main(|delegate| delegate.theme())
}
fn set_content_protected(&self, protected: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_content_protected(protected));
}
fn title(&self) -> String {
self.maybe_wait_on_main(|delegate| delegate.title())
}
fn set_cursor(&self, cursor: Cursor) {
self.maybe_wait_on_main(|delegate| delegate.set_cursor(cursor));
}
fn set_cursor_position(&self, position: Position) -> Result<(), RequestError> {
self.maybe_wait_on_main(|delegate| delegate.set_cursor_position(position))
}
fn set_cursor_grab(&self, mode: crate::window::CursorGrabMode) -> Result<(), RequestError> {
self.maybe_wait_on_main(|delegate| delegate.set_cursor_grab(mode))
}
fn set_cursor_visible(&self, visible: bool) {
self.maybe_wait_on_main(|delegate| delegate.set_cursor_visible(visible))
}
fn drag_window(&self) -> Result<(), RequestError> {
self.maybe_wait_on_main(|delegate| delegate.drag_window())
}
fn drag_resize_window(
&self,
direction: crate::window::ResizeDirection,
) -> Result<(), RequestError> {
Ok(self.maybe_wait_on_main(|delegate| delegate.drag_resize_window(direction))?)
}
fn show_window_menu(&self, position: Position) {
self.maybe_wait_on_main(|delegate| delegate.show_window_menu(position))
}
fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> {
self.maybe_wait_on_main(|delegate| delegate.set_cursor_hittest(hittest));
Ok(())
}
fn current_monitor(&self) -> Option<CoreMonitorHandle> {
self.maybe_wait_on_main(|delegate| {
delegate.current_monitor().map(|inner| CoreMonitorHandle { inner })
})
}
fn available_monitors(&self) -> Box<dyn Iterator<Item = CoreMonitorHandle>> {
self.maybe_wait_on_main(|delegate| {
Box::new(
delegate.available_monitors().into_iter().map(|inner| CoreMonitorHandle { inner }),
)
})
}
fn primary_monitor(&self) -> Option<CoreMonitorHandle> {
self.maybe_wait_on_main(|delegate| {
delegate.primary_monitor().map(|inner| CoreMonitorHandle { inner })
})
}
#[cfg(feature = "rwh_06")]
fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
#[cfg(feature = "rwh_06")]
fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle {
self
}
}
declare_class!(
#[derive(Debug)]
pub struct WinitWindow;
unsafe impl ClassType for WinitWindow {
#[inherits(NSResponder, NSObject)]
type Super = NSWindow;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitWindow";
}
impl DeclaredClass for WinitWindow {}
unsafe impl WinitWindow {
#[method(canBecomeMainWindow)]
fn can_become_main_window(&self) -> bool {
trace_scope!("canBecomeMainWindow");
true
}
#[method(canBecomeKeyWindow)]
fn can_become_key_window(&self) -> bool {
trace_scope!("canBecomeKeyWindow");
true
}
}
);
impl WinitWindow {
pub(super) fn id(&self) -> WindowId {
WindowId::from_raw(self as *const Self as usize)
}
}

View File

@@ -1,60 +0,0 @@
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
use objc2_foundation::{MainThreadMarker, NSObject};
use objc2_ui_kit::UIApplication;
use super::app_state::{self, send_occluded_event_for_all_windows, EventWrapper};
use crate::event::Event;
declare_class!(
pub struct AppDelegate;
unsafe impl ClassType for AppDelegate {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitApplicationDelegate";
}
impl DeclaredClass for AppDelegate {}
// UIApplicationDelegate protocol
unsafe impl AppDelegate {
#[method(application:didFinishLaunchingWithOptions:)]
fn did_finish_launching(&self, _application: &UIApplication, _: *mut NSObject) -> bool {
app_state::did_finish_launching(MainThreadMarker::new().unwrap());
true
}
#[method(applicationDidBecomeActive:)]
fn did_become_active(&self, _application: &UIApplication) {
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed))
}
#[method(applicationWillResignActive:)]
fn will_resign_active(&self, _application: &UIApplication) {
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended))
}
#[method(applicationWillEnterForeground:)]
fn will_enter_foreground(&self, application: &UIApplication) {
send_occluded_event_for_all_windows(application, false);
}
#[method(applicationDidEnterBackground:)]
fn did_enter_background(&self, application: &UIApplication) {
send_occluded_event_for_all_windows(application, true);
}
#[method(applicationWillTerminate:)]
fn will_terminate(&self, application: &UIApplication) {
app_state::terminated(application);
}
#[method(applicationDidReceiveMemoryWarning:)]
fn did_receive_memory_warning(&self, _application: &UIApplication) {
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning))
}
}
);

View File

@@ -13,8 +13,14 @@ use core_foundation::runloop::{
};
use objc2::rc::Retained;
use objc2::{msg_send_id, ClassType};
use objc2_foundation::{MainThreadMarker, NSString};
use objc2_ui_kit::{UIApplication, UIApplicationMain, UIDevice, UIScreen, UIUserInterfaceIdiom};
use objc2_foundation::{MainThreadMarker, NSNotificationCenter, NSObject};
use objc2_ui_kit::{
UIApplication, UIApplicationDidBecomeActiveNotification,
UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification,
UIApplicationDidReceiveMemoryWarningNotification, UIApplicationMain,
UIApplicationWillEnterForegroundNotification, UIApplicationWillResignActiveNotification,
UIApplicationWillTerminateNotification, UIDevice, UIScreen, UIUserInterfaceIdiom,
};
use crate::error::EventLoopError;
use crate::event::Event;
@@ -25,8 +31,8 @@ use crate::platform::ios::Idiom;
use crate::platform_impl::ios::app_state::{EventLoopHandler, HandlePendingUserEvents};
use crate::window::{CustomCursor, CustomCursorSource, Theme};
use super::app_delegate::AppDelegate;
use super::app_state::AppState;
use super::app_state::{send_occluded_event_for_all_windows, AppState, EventWrapper};
use super::notification_center::create_observer;
use super::{app_state, monitor, MonitorHandle};
#[derive(Debug)]
@@ -132,6 +138,18 @@ pub struct EventLoop<T: 'static> {
sender: Sender<T>,
receiver: Receiver<T>,
window_target: RootActiveEventLoop,
// Since iOS 9.0, we no longer need to remove the observers before they are deallocated; the
// system instead cleans it up next time it would have posted a notification to it.
//
// Though we do still need to keep the observers around to prevent them from being deallocated.
_did_finish_launching_observer: Retained<NSObject>,
_did_become_active_observer: Retained<NSObject>,
_will_resign_active_observer: Retained<NSObject>,
_will_enter_foreground_observer: Retained<NSObject>,
_did_enter_background_observer: Retained<NSObject>,
_will_terminate_observer: Retained<NSObject>,
_did_receive_memory_warning_observer: Retained<NSObject>,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -158,11 +176,97 @@ impl<T: 'static> EventLoop<T> {
// this line sets up the main run loop before `UIApplicationMain`
setup_control_flow_observers();
let center = unsafe { NSNotificationCenter::defaultCenter() };
let _did_finish_launching_observer = create_observer(
&center,
// `application:didFinishLaunchingWithOptions:`
unsafe { UIApplicationDidFinishLaunchingNotification },
move |_| {
app_state::did_finish_launching(mtm);
},
);
let _did_become_active_observer = create_observer(
&center,
// `applicationDidBecomeActive:`
unsafe { UIApplicationDidBecomeActiveNotification },
move |_| {
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed));
},
);
let _will_resign_active_observer = create_observer(
&center,
// `applicationWillResignActive:`
unsafe { UIApplicationWillResignActiveNotification },
move |_| {
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended));
},
);
let _will_enter_foreground_observer = create_observer(
&center,
// `applicationWillEnterForeground:`
unsafe { UIApplicationWillEnterForegroundNotification },
move |notification| {
let app = unsafe { notification.object() }.expect(
"UIApplicationWillEnterForegroundNotification to have application object",
);
// SAFETY: The `object` in `UIApplicationWillEnterForegroundNotification` is
// documented to be `UIApplication`.
let app: Retained<UIApplication> = unsafe { Retained::cast(app) };
send_occluded_event_for_all_windows(&app, false);
},
);
let _did_enter_background_observer = create_observer(
&center,
// `applicationDidEnterBackground:`
unsafe { UIApplicationDidEnterBackgroundNotification },
move |notification| {
let app = unsafe { notification.object() }.expect(
"UIApplicationDidEnterBackgroundNotification to have application object",
);
// SAFETY: The `object` in `UIApplicationDidEnterBackgroundNotification` is
// documented to be `UIApplication`.
let app: Retained<UIApplication> = unsafe { Retained::cast(app) };
send_occluded_event_for_all_windows(&app, true);
},
);
let _will_terminate_observer = create_observer(
&center,
// `applicationWillTerminate:`
unsafe { UIApplicationWillTerminateNotification },
move |notification| {
let app = unsafe { notification.object() }
.expect("UIApplicationWillTerminateNotification to have application object");
// SAFETY: The `object` in `UIApplicationWillTerminateNotification` is
// (somewhat) documented to be `UIApplication`.
let app: Retained<UIApplication> = unsafe { Retained::cast(app) };
app_state::terminated(&app);
},
);
let _did_receive_memory_warning_observer = create_observer(
&center,
// `applicationDidReceiveMemoryWarning:`
unsafe { UIApplicationDidReceiveMemoryWarningNotification },
move |_| {
app_state::handle_nonuser_event(
mtm,
EventWrapper::StaticEvent(Event::MemoryWarning),
);
},
);
Ok(EventLoop {
mtm,
sender,
receiver,
window_target: RootActiveEventLoop { p: ActiveEventLoop { mtm }, _marker: PhantomData },
_did_finish_launching_observer,
_did_become_active_observer,
_will_resign_active_observer,
_will_enter_foreground_observer,
_did_enter_background_observer,
_will_terminate_observer,
_did_receive_memory_warning_observer,
})
}
@@ -192,9 +296,6 @@ impl<T: 'static> EventLoop<T> {
app_state::will_launch(self.mtm, handler);
// Ensure application delegate is initialized
let _ = AppDelegate::class();
extern "C" {
// These functions are in crt_externs.h.
fn _NSGetArgc() -> *mut c_int;
@@ -205,8 +306,10 @@ impl<T: 'static> EventLoop<T> {
UIApplicationMain(
*_NSGetArgc(),
NonNull::new(*_NSGetArgv()).unwrap(),
// We intentionally override neither the application nor the delegate, to allow the
// user to do so themselves!
None,
None,
Some(&NSString::from_str(AppDelegate::NAME)),
)
};
unreachable!()

View File

@@ -1,9 +1,9 @@
#![allow(clippy::let_unit_value)]
mod app_delegate;
mod app_state;
mod event_loop;
mod monitor;
mod notification_center;
mod view;
mod view_controller;
mod window;

View File

@@ -0,0 +1,27 @@
use std::ptr::NonNull;
use block2::RcBlock;
use objc2::rc::Retained;
use objc2_foundation::{NSNotification, NSNotificationCenter, NSNotificationName, NSObject};
/// Observe the given notification.
///
/// This is used in Winit as an alternative to declaring an application delegate, as we want to
/// give the user full control over those.
pub fn create_observer(
center: &NSNotificationCenter,
name: &NSNotificationName,
handler: impl Fn(&NSNotification) + 'static,
) -> Retained<NSObject> {
let block = RcBlock::new(move |notification: NonNull<NSNotification>| {
handler(unsafe { notification.as_ref() });
});
unsafe {
center.addObserverForName_object_queue_usingBlock(
Some(name),
None, // No sender filter
None, // No queue, run on posting thread (i.e. main thread)
&block,
)
}
}

View File

@@ -190,7 +190,7 @@ declare_class!(
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -recognizer.scale())
}
state => panic!("unexpected recognizer state: {:?}", state),
state => panic!("unexpected recognizer state: {state:?}"),
};
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
@@ -249,7 +249,7 @@ declare_class!(
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -recognizer.rotation())
}
state => panic!("unexpected recognizer state: {:?}", state),
state => panic!("unexpected recognizer state: {state:?}"),
};
// Make delta negative to match macos, convert to degrees
@@ -300,7 +300,7 @@ declare_class!(
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -last_pan.x, -last_pan.y)
}
state => panic!("unexpected recognizer state: {:?}", state),
state => panic!("unexpected recognizer state: {state:?}"),
};

View File

@@ -320,7 +320,7 @@ impl<'a, 'b> KeyEventResults<'a, 'b> {
// The current behaviour makes it so composing a character overrides attempts to input a
// control character with the `Ctrl` key. We can potentially add a configuration option
// if someone specifically wants the oppsite behaviour.
// if someone specifically wants the opposite behaviour.
pub fn text_with_all_modifiers(&mut self) -> Option<SmolStr> {
match self.composed_text() {
Ok(text) => text,

View File

@@ -696,6 +696,7 @@ unsafe extern "C" fn x_error_callback(
0
}
#[allow(clippy::large_enum_variant)]
pub enum EventLoop<T: 'static> {
#[cfg(wayland_platform)]
Wayland(Box<wayland::EventLoop<T>>),
@@ -768,9 +769,9 @@ impl<T: 'static> EventLoop<T> {
// Create the display based on the backend.
match backend {
#[cfg(wayland_platform)]
Backend::Wayland => EventLoop::new_wayland_any_thread().map_err(Into::into),
Backend::Wayland => EventLoop::new_wayland_any_thread(),
#[cfg(x11_platform)]
Backend::X => EventLoop::new_x11_any_thread().map_err(Into::into),
Backend::X => EventLoop::new_x11_any_thread(),
}
}
@@ -849,6 +850,7 @@ impl<T: 'static> EventLoopProxy<T> {
}
}
#[allow(clippy::large_enum_variant)]
pub enum ActiveEventLoop {
#[cfg(wayland_platform)]
Wayland(wayland::ActiveEventLoop),

View File

@@ -4,15 +4,21 @@ use std::cell::{Cell, RefCell};
use std::io::Result as IOResult;
use std::marker::PhantomData;
use std::mem;
use std::os::fd::OwnedFd;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use calloop::ping::Ping;
use rustix::event::{PollFd, PollFlags};
use rustix::pipe::{self, PipeFlags};
use sctk::reexports::calloop::Error as CalloopError;
use sctk::reexports::calloop_wayland_source::WaylandSource;
use sctk::reexports::client::{globals, Connection, QueueHandle};
use tracing::warn;
use crate::cursor::OnlyCursorImage;
use crate::dpi::LogicalSize;
@@ -68,6 +74,8 @@ pub struct EventLoop<T: 'static> {
// XXX drop after everything else, just to be safe.
/// Calloop's event loop.
event_loop: calloop::EventLoop<'static, WinitState>,
pump_event_notifier: Option<PumpEventNotifier>,
}
impl<T: 'static> EventLoop<T> {
@@ -168,6 +176,7 @@ impl<T: 'static> EventLoop<T> {
p: PlatformActiveEventLoop::Wayland(window_target),
_marker: PhantomData,
},
pump_event_notifier: None,
};
Ok(event_loop)
@@ -223,6 +232,27 @@ impl<T: 'static> EventLoop<T> {
PumpStatus::Exit(code)
} else {
// NOTE: spawn a wake-up thread, thus if we have code reading the wayland connection
// in parallel to winit, we ensure that the loop itself is marked as having events.
if timeout.is_some() && self.pump_event_notifier.is_none() {
let awakener = match &self.window_target.p {
PlatformActiveEventLoop::Wayland(window_target) => {
window_target.event_loop_awakener.clone()
},
#[cfg(x11_platform)]
PlatformActiveEventLoop::X(_) => unreachable!(),
};
self.pump_event_notifier =
Some(PumpEventNotifier::spawn(self.connection.clone(), awakener));
}
if let Some(pump_event_notifier) = self.pump_event_notifier.as_ref() {
// Notify that we don't have to wait, since we're out of winit.
*pump_event_notifier.control.0.lock().unwrap() = PumpEventNotifierAction::Monitor;
pump_event_notifier.control.1.notify_one();
}
PumpStatus::Continue
}
}
@@ -285,7 +315,10 @@ impl<T: 'static> EventLoop<T> {
// Reduce spurious wake-ups.
let dispatched_events = self.with_state(|state| state.dispatched_events);
if matches!(cause, StartCause::WaitCancelled { .. }) && !dispatched_events {
if matches!(cause, StartCause::WaitCancelled { .. })
&& !dispatched_events
&& timeout.is_none()
{
continue;
}
@@ -600,7 +633,7 @@ impl<T> AsRawFd for EventLoop<T> {
pub struct ActiveEventLoop {
/// The event loop wakeup source.
pub event_loop_awakener: calloop::ping::Ping,
pub event_loop_awakener: Ping,
/// The main queue used by the event loop.
pub queue_handle: QueueHandle<WinitState>,
@@ -684,3 +717,94 @@ impl ActiveEventLoop {
.into())
}
}
#[derive(Debug)]
struct PumpEventNotifier {
/// Whether we're in winit or not.
control: Arc<(Mutex<PumpEventNotifierAction>, Condvar)>,
/// Waker handle for the working thread.
worker_waker: Option<OwnedFd>,
/// Thread handle.
handle: Option<JoinHandle<()>>,
}
impl Drop for PumpEventNotifier {
fn drop(&mut self) {
// Wake-up the thread.
if let Some(worker_waker) = self.worker_waker.as_ref() {
let _ = rustix::io::write(worker_waker.as_fd(), &[0u8]);
}
*self.control.0.lock().unwrap() = PumpEventNotifierAction::Shutdown;
self.control.1.notify_one();
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
impl PumpEventNotifier {
fn spawn(connection: Connection, awakener: Ping) -> Self {
// Start from the waiting state.
let control = Arc::new((Mutex::new(PumpEventNotifierAction::Pause), Condvar::new()));
let control_thread = Arc::clone(&control);
let (read, write) = match pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK) {
Ok((read, write)) => (read, write),
Err(_) => return Self { control, handle: None, worker_waker: None },
};
let handle =
std::thread::Builder::new().name(String::from("pump_events mon")).spawn(move || {
let (lock, cvar) = &*control_thread;
'outer: loop {
let mut wait = lock.lock().unwrap();
while *wait == PumpEventNotifierAction::Pause {
wait = cvar.wait(wait).unwrap();
}
// Exit the loop when we're asked to. Given that we poll
// only once we can take the `prepare_read`, but in some cases
// it could be not possible, we may block on `join`.
if *wait == PumpEventNotifierAction::Shutdown {
break 'outer;
}
// Wake-up the main loop and put this one back to sleep.
*wait = PumpEventNotifierAction::Pause;
drop(wait);
while let Some(read_guard) = connection.prepare_read() {
let _ = connection.flush();
let poll_fd = PollFd::from_borrowed_fd(connection.as_fd(), PollFlags::IN);
let pipe_poll_fd = PollFd::from_borrowed_fd(read.as_fd(), PollFlags::IN);
// Read from the `fd` before going back to poll.
if Ok(1) == rustix::io::read(read.as_fd(), &mut [0u8; 1]) {
break 'outer;
}
let _ = rustix::event::poll(&mut [poll_fd, pipe_poll_fd], -1);
// Non-blocking read the connection.
let _ = read_guard.read_without_dispatch();
}
awakener.ping();
}
});
if let Some(err) = handle.as_ref().err() {
warn!("failed to spawn pump_events wake-up thread: {err}");
}
PumpEventNotifier { control, handle: handle.ok(), worker_waker: Some(write) }
}
}
#[derive(Debug, PartialEq, Eq)]
enum PumpEventNotifierAction {
/// Monitor the wayland queue.
Monitor,
/// Pause monitoring.
Pause,
/// Shutdown the thread.
Shutdown,
}

View File

@@ -99,7 +99,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
WlKeyboardEvent::Leave { surface, .. } => {
let window_id = wayland::make_wid(&surface);
// NOTE: we should drop the repeat regardless whethere it was for the present
// NOTE: we should drop the repeat regardless whether it was for the present
// window of for the window which just went gone.
keyboard_state.current_repeat = None;
if let Some(token) = keyboard_state.repeat_token.take() {

View File

@@ -96,8 +96,12 @@ impl SeatHandler for WinitState {
},
SeatCapability::Pointer if seat_state.pointer.is_none() => {
let surface = self.compositor_state.create_surface(queue_handle);
let viewport = self
.viewporter_state
.as_ref()
.map(|state| state.get_viewport(&surface, queue_handle));
let surface_id = surface.id();
let pointer_data = WinitPointerData::new(seat.clone());
let pointer_data = WinitPointerData::new(seat.clone(), viewport);
let themed_pointer = self
.seat_state
.get_pointer_with_theme_and_data(

View File

@@ -18,6 +18,7 @@ use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_ma
use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1};
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::csd_frame::FrameClick;
use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
use sctk::compositor::SurfaceData;
use sctk::globals::GlobalData;
@@ -193,7 +194,7 @@ impl PointerHandler for WinitState {
pointer_data.phase = phase;
// Mice events have both pixel and discrete delta's at the same time. So prefer
// the descrite values if they are present.
// the discrete values if they are present.
let delta = if has_discrete_scroll {
// NOTE: Wayland sign convention is the inverse of winit.
MouseScrollDelta::LineDelta(
@@ -225,13 +226,17 @@ pub struct WinitPointerData {
/// The data required by the sctk.
sctk_data: PointerData,
/// Viewport for fractional cursor.
viewport: Option<WpViewport>,
}
impl WinitPointerData {
pub fn new(seat: WlSeat) -> Self {
pub fn new(seat: WlSeat, viewport: Option<WpViewport>) -> Self {
Self {
inner: Mutex::new(WinitPointerDataInner::default()),
sctk_data: PointerData::new(seat),
viewport,
}
}
@@ -312,6 +317,18 @@ impl WinitPointerData {
locked_pointer.set_cursor_position_hint(surface_x, surface_y);
}
}
pub fn viewport(&self) -> Option<&WpViewport> {
self.viewport.as_ref()
}
}
impl Drop for WinitPointerData {
fn drop(&mut self) {
if let Some(viewport) = self.viewport.take() {
viewport.destroy();
}
}
}
impl PointerDataExt for WinitPointerData {

View File

@@ -1,5 +1,7 @@
//! The Wayland window.
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
@@ -164,6 +166,12 @@ impl Window {
Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor),
}
// Apply resize increments.
if let Some(increments) = attributes.resize_increments {
let increments = increments.to_logical(window_state.scale_factor());
window_state.set_resize_increments(Some(increments));
}
// Activate the window when the token is passed.
if let (Some(xdg_activation), Some(token)) =
(xdg_activation.as_ref(), attributes.platform_specific.activation_token)
@@ -223,6 +231,10 @@ impl Window {
window_events_sink,
})
}
pub(crate) fn xdg_toplevel(&self) -> Option<NonNull<c_void>> {
NonNull::new(self.window.xdg_toplevel().id().as_ptr().cast())
}
}
impl Window {
@@ -327,12 +339,19 @@ impl Window {
#[inline]
pub fn resize_increments(&self) -> Option<PhysicalSize<u32>> {
None
let window_state = self.window_state.lock().unwrap();
let scale_factor = window_state.scale_factor();
window_state
.resize_increments()
.map(|size| super::logical_to_physical_rounded(size, scale_factor))
}
#[inline]
pub fn set_resize_increments(&self, _increments: Option<Size>) {
warn!("`set_resize_increments` is not implemented for Wayland");
pub fn set_resize_increments(&self, increments: Option<Size>) {
let mut window_state = self.window_state.lock().unwrap();
let scale_factor = window_state.scale_factor();
let increments = increments.map(|size| size.to_logical(scale_factor));
window_state.set_resize_increments(increments);
}
#[inline]

View File

@@ -31,7 +31,7 @@ use sctk::subcompositor::SubcompositorState;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use crate::cursor::CustomCursor as RootCustomCursor;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size};
use crate::error::{ExternalError, NotSupportedError};
use crate::platform_impl::wayland::logical_to_physical_rounded;
use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor};
@@ -127,6 +127,7 @@ pub struct WindowState {
/// Min size.
min_inner_size: LogicalSize<u32>,
max_inner_size: Option<LogicalSize<u32>>,
resize_increments: Option<LogicalSize<u32>>,
/// The size of the window when no states were applied to it. The primary use for it
/// is to fallback to original window size, before it was maximized, if the compositor
@@ -202,6 +203,7 @@ impl WindowState {
last_configure: None,
max_inner_size: None,
min_inner_size: MIN_WINDOW_SIZE,
resize_increments: None,
pointer_constraints,
pointers: Default::default(),
queue_handle: queue_handle.clone(),
@@ -222,9 +224,9 @@ impl WindowState {
}
/// Apply closure on the given pointer.
fn apply_on_pointer<F: Fn(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
fn apply_on_pointer<F: FnMut(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
&self,
callback: F,
mut callback: F,
) {
self.pointers.iter().filter_map(Weak::upgrade).for_each(|pointer| {
let data = pointer.pointer().winit_data();
@@ -340,6 +342,42 @@ impl WindowState {
.unwrap_or(new_size.height);
}
// Apply size increments.
//
// We conditionally apply increments to avoid conflicts with the compositor's layout rules:
// 1. If the window is floating (constrain == true), we snap to increments to ensure the
// app's grid alignment.
// 2. If the user is interactively resizing (is_resizing), we snap the size to provide
// feedback.
//
// However, we MUST NOT snap if the compositor enforces a specific size (constrain == false,
// or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would
// shrink the window below the allocated area, creating visible gaps between valid
// windows or screen edges.
if (constrain || configure.is_resizing())
&& !configure.is_maximized()
&& !configure.is_fullscreen()
&& !configure.is_tiled()
{
if let Some(increments) = self.resize_increments {
// We use min size as a base size for the increments, similar to how X11 does it.
//
// This ensures that we can always reach the min size and the increments are
// calculated from it.
let (delta_width, delta_height) = (
new_size.width.saturating_sub(self.min_inner_size.width),
new_size.height.saturating_sub(self.min_inner_size.height),
);
let width =
self.min_inner_size.width + (delta_width / increments.width) * increments.width;
let height = self.min_inner_size.height
+ (delta_height / increments.height) * increments.height;
new_size = (width, height).into();
}
}
let new_state = configure.state;
let old_state = self.last_configure.as_ref().map(|configure| configure.state);
@@ -725,18 +763,39 @@ impl WindowState {
self.selected_cursor = SelectedCursor::Custom(cursor);
}
/// Set the resize increments of the window.
pub fn set_resize_increments(&mut self, increments: Option<LogicalSize<u32>>) {
self.resize_increments = increments;
// NOTE: We don't update the window size here, because it will be done on the next resize
// or configure event.
}
/// Get the resize increments of the window.
pub fn resize_increments(&self) -> Option<LogicalSize<u32>> {
self.resize_increments
}
fn apply_custom_cursor(&self, cursor: &CustomCursor) {
self.apply_on_pointer(|pointer, _| {
self.apply_on_pointer(|pointer, data| {
let surface = pointer.surface();
let scale = surface.data::<SurfaceData>().unwrap().surface_data().scale_factor();
let scale = if let Some(viewport) = data.viewport() {
let scale = self.scale_factor();
let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale);
viewport.set_destination(size.width, size.height);
scale
} else {
let scale = surface.data::<SurfaceData>().unwrap().surface_data().scale_factor();
surface.set_buffer_scale(scale);
scale as f64
};
surface.set_buffer_scale(scale);
surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0);
if surface.version() >= 4 {
surface.damage_buffer(0, 0, cursor.w, cursor.h);
} else {
surface.damage(0, 0, cursor.w / scale, cursor.h / scale);
let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale);
surface.damage(0, 0, size.width, size.height);
}
surface.commit();
@@ -746,12 +805,9 @@ impl WindowState {
.and_then(|data| data.pointer_data().latest_enter_serial())
.unwrap();
pointer.pointer().set_cursor(
serial,
Some(surface),
cursor.hotspot_x / scale,
cursor.hotspot_y / scale,
);
let hotspot =
PhysicalPosition::new(cursor.hotspot_x, cursor.hotspot_y).to_logical(scale);
pointer.pointer().set_cursor(serial, Some(surface), hotspot.x, hotspot.y);
});
}
@@ -827,34 +883,51 @@ impl WindowState {
None => return Err(ExternalError::NotSupported(NotSupportedError::new())),
};
// Replace the current mode.
let old_mode = std::mem::replace(&mut self.cursor_grab_mode.current_grab_mode, mode);
match old_mode {
CursorGrabMode::None => (),
let mut unset_old = false;
match self.cursor_grab_mode.current_grab_mode {
CursorGrabMode::None => unset_old = true,
CursorGrabMode::Confined => self.apply_on_pointer(|_, data| {
data.unconfine_pointer();
unset_old = true;
}),
CursorGrabMode::Locked => {
self.apply_on_pointer(|_, data| data.unlock_pointer());
self.apply_on_pointer(|_, data| {
data.unlock_pointer();
unset_old = true;
});
},
}
// In case we haven't unset the old mode, it means that we don't have a cursor above
// the window, thus just wait for it to re-appear.
if !unset_old {
return Ok(());
}
let mut set_mode = false;
let surface = self.window.wl_surface();
match mode {
CursorGrabMode::Locked => self.apply_on_pointer(|pointer, data| {
let pointer = pointer.pointer();
data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle)
data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle);
set_mode = true;
}),
CursorGrabMode::Confined => self.apply_on_pointer(|pointer, data| {
let pointer = pointer.pointer();
data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle)
data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle);
set_mode = true;
}),
CursorGrabMode::None => {
// Current lock/confine was already removed.
set_mode = true;
},
}
// Replace the current grab mode after we've ensure that it got updated.
if set_mode {
self.cursor_grab_mode.current_grab_mode = mode;
}
Ok(())
}

View File

@@ -172,7 +172,7 @@ fn push_display(buffer: &mut Vec<u8>, display: &impl std::fmt::Display) {
}
}
write!(Writer { buffer }, "{}", display).unwrap();
write!(Writer { buffer }, "{display}").unwrap();
}
#[cfg(test)]

View File

@@ -81,7 +81,7 @@ extern "C" fn preedit_draw_callback(
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
tracing::warn!(
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
"invalid chg range: buffer length={}, but chg_first={} chg_length={}",
client_data.text.len(),
call_data.chg_first,
call_data.chg_length
@@ -158,7 +158,7 @@ impl PreeditCallbacks {
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
let start_callback = create_xim_callback(client_data, unsafe {
mem::transmute::<usize, unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer)>(
preedit_start_callback as usize,
preedit_start_callback as *const () as usize,
)
});
let done_callback = create_xim_callback(client_data, preedit_done_callback);

View File

@@ -51,10 +51,9 @@ impl ImeInner {
}
pub unsafe fn close_im_if_necessary(&self) -> Result<bool, XError> {
if !self.is_destroyed && self.im.is_some() {
unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true)
} else {
Ok(false)
match self.im.as_ref() {
Some(im) if !self.is_destroyed => unsafe { close_im(&self.xconn, im.im).map(|_| true) },
_ => Ok(false),
}
}

View File

@@ -176,7 +176,7 @@ unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXi
)
.map_err(GetXimServersError::GetPropertyError)?
.into_iter()
.map(ffi::Atom::from)
.map(|atom| atom as _)
.collect::<Vec<_>>();
let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());

View File

@@ -497,6 +497,7 @@ impl<T: 'static> EventLoop<T> {
// If we don't have any pending `_receiver`
if !self.has_pending()
&& !matches!(&cause, StartCause::ResumeTimeReached { .. } | StartCause::Poll)
&& timeout.is_none()
{
return;
}
@@ -849,24 +850,24 @@ pub enum X11Error {
impl fmt::Display for X11Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
X11Error::Xlib(e) => write!(f, "Xlib error: {}", e),
X11Error::Connect(e) => write!(f, "X11 connection error: {}", e),
X11Error::Connection(e) => write!(f, "X11 connection error: {}", e),
X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e),
X11Error::GetProperty(e) => write!(f, "Failed to get X property {}", e),
X11Error::X11(e) => write!(f, "X11 error: {:?}", e),
X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s),
X11Error::Xlib(e) => write!(f, "Xlib error: {e}"),
X11Error::Connect(e) => write!(f, "X11 connection error: {e}"),
X11Error::Connection(e) => write!(f, "X11 connection error: {e}"),
X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {e}"),
X11Error::GetProperty(e) => write!(f, "Failed to get X property {e}"),
X11Error::X11(e) => write!(f, "X11 error: {e:?}"),
X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {s}"),
X11Error::InvalidActivationToken(s) => write!(
f,
"Invalid activation token: {}",
std::str::from_utf8(s).unwrap_or("<invalid utf8>")
),
X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {}", s),
X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {s}"),
X11Error::NoSuchVisual(visualid) => {
write!(f, "Could not find a matching X11 visual for ID `{:x}`", visualid)
write!(f, "Could not find a matching X11 visual for ID `{visualid:x}`")
},
X11Error::XsettingsParse(err) => {
write!(f, "Failed to parse xsettings: {:?}", err)
write!(f, "Failed to parse xsettings: {err:?}")
},
}
}

View File

@@ -51,7 +51,7 @@ where
}
impl XConnection {
// This is impoartant, so pay attention!
// This is important, so pay attention!
// Xlib has an output buffer, and tries to hide the async nature of X from you.
// This buffer contains the requests you make, and is flushed under various circumstances:
// 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed"

View File

@@ -79,7 +79,7 @@ impl XConnection {
.iter()
// XRROutputInfo contains an array of mode ids that correspond to
// modes in the array in XRRScreenResources
.filter(|x| output_modes.iter().any(|id| x.id == *id))
.filter(|x| output_modes.contains(&x.id))
.map(|mode| {
VideoModeHandle {
size: (mode.width.into(), mode.height.into()),

View File

@@ -8,9 +8,8 @@ use std::{cmp, env};
use tracing::{debug, info, warn};
use x11rb::connection::Connection;
use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification};
use x11rb::protocol::shape::SK;
use x11rb::protocol::xfixes::{ConnectionExt, RegionWrapper};
use x11rb::protocol::xproto::{self, ConnectionExt as _, Rectangle};
use x11rb::protocol::shape::{ConnectionExt as ShapeExt, SK, SO};
use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle};
use x11rb::protocol::{randr, xinput};
use crate::cursor::{Cursor, CustomCursor as RootCustomCursor};
@@ -976,8 +975,8 @@ impl UnownedWindow {
let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT];
match state {
Ok(atoms) => {
let horz_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == horz_atom);
let vert_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == vert_atom);
let horz_maximized = atoms.contains(&horz_atom);
let vert_maximized = atoms.contains(&vert_atom);
horz_maximized && vert_maximized
},
_ => false,
@@ -1622,6 +1621,15 @@ impl UnownedWindow {
#[inline]
pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> {
// Implement cursor hittest for X11 by either setting an empty or full window input shape.
// In X11, every window has two "shapes":
// * Bounding shape: defines the visible outline of the window.
// * Input shape: defines the region of the window that receives pointer/keyboard events.
// If the input shape is the full window rectangle, the window behaves normally.
// If the input shape is empty, the window is completely clickthrough.
// Here, we implement hit test by mapping `hittest = true` to "restore a full input shape"
// and `hittest = false` to "clear the input shape" (empty list of rectangles).
let mut rectangles: Vec<Rectangle> = Vec::new();
if hittest {
let size = self.inner_size();
@@ -1632,11 +1640,18 @@ impl UnownedWindow {
height: size.height as u16,
})
}
let region = RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles)
.map_err(|_e| ExternalError::Ignored)?;
self.xconn
.xcb_connection()
.xfixes_set_window_shape_region(self.xwindow, SK::INPUT, 0, 0, region.region())
.shape_rectangles(
SO::SET,
SK::INPUT,
ClipOrdering::UNSORTED,
self.xwindow,
0,
0,
&rectangles,
)
.map_err(|_e| ExternalError::Ignored)?;
self.shared_state_lock().cursor_hittest = Some(hittest);
Ok(())

View File

@@ -145,7 +145,7 @@ impl XConnection {
fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option<xproto::Atom> {
// Fetch the _XSETTINGS_S[screen number] atom.
let xsettings_screen = xcb
.intern_atom(false, format!("_XSETTINGS_S{}", default_screen).as_bytes())
.intern_atom(false, format!("_XSETTINGS_S{default_screen}").as_bytes())
.ok()?
.reply()
.ok()?
@@ -234,9 +234,7 @@ impl XConnection {
// Store the timestamp in the slot if it's greater than the last one.
let mut last_timestamp = self.timestamp.load(Ordering::Relaxed);
loop {
let wrapping_sub = |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32);
if wrapping_sub(timestamp, last_timestamp) <= 0 {
if (timestamp as i32).wrapping_sub(last_timestamp as i32) <= 0 {
break;
}

View File

@@ -1,49 +1,104 @@
#![allow(clippy::unnecessary_cast)]
#![allow(unknown_lints)] // New lint below
#![allow(static_mut_refs)] // Uses `MainThreadBound` in new version.
use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder};
use objc2_foundation::{MainThreadMarker, NSObject};
use std::cell::Cell;
use std::mem;
use objc2::runtime::{Imp, Sel};
use objc2::sel;
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType};
use objc2_foundation::MainThreadMarker;
use super::app_state::ApplicationDelegate;
use crate::event::{DeviceEvent, ElementState};
declare_class!(
pub(super) struct WinitApplication;
type SendEvent = extern "C" fn(&NSApplication, Sel, &NSEvent);
unsafe impl ClassType for WinitApplication {
#[inherits(NSResponder, NSObject)]
type Super = NSApplication;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitApplication";
}
// NOTE: Only used on the main thread. Ideally, we'd use `MainThreadBound`, but that isn't
// constructible from `const` with this `objc2` version.
static mut ORIGINAL: Cell<Option<SendEvent>> = Cell::new(None);
impl DeclaredClass for WinitApplication {}
extern "C" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) {
let mtm = MainThreadMarker::from(app);
unsafe impl WinitApplication {
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196)
// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553)
#[method(sendEvent:)]
fn send_event(&self, event: &NSEvent) {
// For posterity, there are some undocumented event types
// (https://github.com/servo/cocoa-rs/issues/155)
// but that doesn't really matter here.
let event_type = unsafe { event.r#type() };
let modifier_flags = unsafe { event.modifierFlags() };
if event_type == NSEventType::KeyUp
&& modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand)
{
if let Some(key_window) = self.keyWindow() {
key_window.sendEvent(event);
}
} else {
let delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
maybe_dispatch_device_event(&delegate, event);
unsafe { msg_send![super(self), sendEvent: event] }
}
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
// Overriding `sendEvent:` fixes that. (https://stackoverflow.com/a/15294196)
// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553)
//
// For posterity, there are some undocumented event types
// (https://github.com/servo/cocoa-rs/issues/155)
// but that doesn't really matter here.
let event_type = unsafe { event.r#type() };
let modifier_flags = unsafe { event.modifierFlags() };
if event_type == NSEventType::KeyUp
&& modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand)
{
if let Some(key_window) = app.keyWindow() {
key_window.sendEvent(event);
}
return;
}
);
// Events are generally scoped to the window level, so the best way
// to get device events is to listen for them on NSApplication.
let delegate = ApplicationDelegate::get(mtm);
maybe_dispatch_device_event(&delegate, event);
let _ = mtm;
let original = unsafe { ORIGINAL.get().expect("no existing sendEvent: handler set") };
original(app, sel, event)
}
/// Override the [`sendEvent:`][NSApplication::sendEvent] method on the given application class.
///
/// The previous implementation created a subclass of [`NSApplication`], however we would like to
/// give the user full control over their `NSApplication`, so we override the method here using
/// method swizzling instead.
///
/// This _should_ also allow two versions of Winit to exist in the same application.
///
/// See the following links for more info on method swizzling:
/// - <https://nshipster.com/method-swizzling/>
/// - <https://spin.atomicobject.com/method-swizzling-objective-c/>
/// - <https://web.archive.org/web/20130308110627/http://cocoadev.com/wiki/MethodSwizzling>
///
/// NOTE: This function assumes that the passed in application object is the one returned from
/// [`NSApplication::sharedApplication`], i.e. the one and only global shared application object.
/// For testing though, we allow it to be a different object.
pub(crate) fn override_send_event(global_app: &NSApplication) {
let mtm = MainThreadMarker::from(global_app);
let class = global_app.class();
let method =
class.instance_method(sel!(sendEvent:)).expect("NSApplication must have sendEvent: method");
// SAFETY: Converting our `sendEvent:` implementation to an IMP.
let overridden = unsafe { mem::transmute::<SendEvent, Imp>(send_event) };
// If we've already overridden the method, don't do anything.
// FIXME(madsmtm): Use `std::ptr::fn_addr_eq` (Rust 1.85) once available in MSRV.
#[allow(unknown_lints, unpredictable_function_pointer_comparisons)]
if overridden == method.implementation() {
return;
}
// SAFETY: Our implementation has:
// 1. The same signature as `sendEvent:`.
// 2. Does not impose extra safety requirements on callers.
let original = unsafe { method.set_implementation(overridden) };
// SAFETY: This is the actual signature of `sendEvent:`.
let original = unsafe { mem::transmute::<Imp, SendEvent>(original) };
// NOTE: If NSApplication was safe to use from multiple threads, then this would potentially be
// a (checked) race-condition, since one could call `sendEvent:` before the original had been
// stored here.
//
// It is only usable from the main thread, however, so we're good!
let _ = mtm;
unsafe { ORIGINAL.set(Some(original)) };
}
fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) {
let event_type = unsafe { event.r#type() };
@@ -85,3 +140,56 @@ fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent)
_ => (),
}
}
#[cfg(test)]
mod tests {
use objc2::rc::Retained;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use super::*;
#[test]
fn test_override() {
// FIXME(madsmtm): Ensure this always runs (maybe use cargo-nextest or `--test-threads=1`?)
let Some(mtm) = MainThreadMarker::new() else { return };
// Create a new application, without making it the shared application.
let app = unsafe { NSApplication::new(mtm) };
override_send_event(&app);
// Test calling twice works.
override_send_event(&app);
// FIXME(madsmtm): Can't test this yet, need some way to mock AppState.
// unsafe {
// let event = super::super::event::dummy_event().unwrap();
// app.sendEvent(&event)
// }
}
#[test]
fn test_custom_class() {
let Some(_mtm) = MainThreadMarker::new() else { return };
declare_class!(
struct TestApplication;
unsafe impl ClassType for TestApplication {
type Super = NSApplication;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "TestApplication";
}
impl DeclaredClass for TestApplication {}
unsafe impl TestApplication {
#[method(sendEvent:)]
fn send_event(&self, _event: &NSEvent) {
todo!()
}
}
);
let app: Retained<TestApplication> = unsafe { msg_send_id![TestApplication::class(), new] };
override_send_event(&app);
}
}

View File

@@ -11,7 +11,7 @@ use objc2_app_kit::{
use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol};
use super::event_handler::EventHandler;
use super::event_loop::{stop_app_immediately, ActiveEventLoop, PanicInfo};
use super::event_loop::{notify_windows_of_exit, stop_app_immediately, ActiveEventLoop, PanicInfo};
use super::observer::{EventLoopWaker, RunLoop};
use super::{menu, WindowId, DEVICE_ID};
use crate::event::{DeviceEvent, Event, StartCause, WindowEvent};
@@ -165,7 +165,9 @@ impl ApplicationDelegate {
fn will_terminate(&self, _notification: &NSNotification) {
trace_scope!("applicationWillTerminate:");
// TODO: Notify every window that it will be destroyed, like done in iOS?
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
notify_windows_of_exit(&app);
self.internal_exit();
}
@@ -392,6 +394,7 @@ impl ApplicationDelegate {
if self.exiting() {
let app = NSApplication::sharedApplication(mtm);
stop_app_immediately(&app);
notify_windows_of_exit(&app);
}
if self.ivars().stop_before_wait.get() {

View File

@@ -128,7 +128,7 @@ pub(crate) fn create_key_event(ns_event: &NSEvent, is_press: bool, is_repeat: bo
let logical_key = match text_with_all_modifiers.as_ref() {
// Only checking for ctrl and cmd here, not checking for alt because we DO want to
// include its effect in the key. For example if -on the Germay layout- one
// include its effect in the key. For example if -on the German layout- one
// presses alt+8, the logical key should be "{"
// Also not checking if this is a release event because then this issue would
// still affect the key release.

View File

@@ -16,11 +16,11 @@ use core_foundation::runloop::{
};
use objc2::rc::{autoreleasepool, Retained};
use objc2::runtime::ProtocolObject;
use objc2::{msg_send_id, sel, ClassType};
use objc2::sel;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
use super::app::WinitApplication;
use super::app::override_send_event;
use super::app_state::{ApplicationDelegate, HandlePendingUserEvents};
use super::event::dummy_event;
use super::monitor::{self, MonitorHandle};
@@ -220,15 +220,8 @@ impl<T> EventLoop<T> {
let mtm = MainThreadMarker::new()
.expect("on macOS, `EventLoop` must be created on the main thread!");
let app: Retained<NSApplication> =
unsafe { msg_send_id![WinitApplication::class(), sharedApplication] };
if !app.is_kind_of::<WinitApplication>() {
panic!(
"`winit` requires control over the principal class. You must create the event \
loop before other parts of your application initialize NSApplication"
);
}
// Initialize the application (if it has not already been).
let app = NSApplication::sharedApplication(mtm);
let activation_policy = match attributes.activation_policy {
None => None,
@@ -247,6 +240,9 @@ impl<T> EventLoop<T> {
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
});
// Override `sendEvent:` on the application to forward to our application state.
override_send_event(&app);
let panic_info: Rc<PanicInfo> = Default::default();
setup_control_flow_observers(mtm, Rc::downgrade(&panic_info));
@@ -421,6 +417,22 @@ pub(super) fn stop_app_immediately(app: &NSApplication) {
});
}
/// Tell all windows to close.
///
/// This will synchronously trigger `WindowEvent::Destroyed` within
/// `windowWillClose:`, giving the application one last chance to handle
/// those events. It doesn't matter if the user also ends up closing the
/// windows in `Window`'s `Drop` impl, once a window has been closed once, it
/// stays closed.
///
/// This ensures that no windows linger on after the event loop has exited,
/// see <https://github.com/rust-windowing/winit/issues/4135>.
pub(super) fn notify_windows_of_exit(app: &NSApplication) {
for window in app.windows() {
window.close();
}
}
/// Catches panics that happen inside `f` and when a panic
/// happens, stops the `sharedApplication`
#[inline]

View File

@@ -68,6 +68,8 @@ pub type CGDisplayModeRef = *mut c_void;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
pub fn CGDisplayGetDisplayIDFromUUID(uuid: CFUUIDRef) -> CGDirectDisplayID;
}
#[link(name = "CoreGraphics", kind = "framework")]

View File

@@ -14,6 +14,7 @@ use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_app_kit::NSScreen;
use objc2_foundation::{ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect};
use tracing::warn;
use super::ffi;
use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
@@ -98,18 +99,29 @@ impl VideoModeHandle {
}
}
/// `CGDirectDisplayID` is documented as:
/// > a framebuffer, a color correction (gamma) table, and possibly an attached monitor.
///
/// That is, it doesn't actually represent the monitor itself. Instead, we use the UUID of the
/// monitor, as retrieved from `CGDisplayCreateUUIDFromDisplayID` (this makes the monitor ID stable,
/// even across reboots and video mode changes).
///
/// NOTE: I'd be perfectly valid to store `[u8; 16]` in here instead, we only store `CFUUID` to
/// avoid having to re-create it when we want to fetch the display ID.
#[derive(Clone)]
pub struct MonitorHandle(CGDirectDisplayID);
pub struct MonitorHandle(CFUUID);
// SAFETY: CFUUID is immutable.
// FIXME(madsmtm): Upstream this into `objc2-core-foundation`.
unsafe impl Send for MonitorHandle {}
unsafe impl Sync for MonitorHandle {}
type MonitorUuid = [u8; 16];
impl MonitorHandle {
/// Internal comparisons of [`MonitorHandle`]s are done first requesting a UUID for the handle.
fn uuid(&self) -> MonitorUuid {
let cf_uuid = unsafe {
CFUUID::wrap_under_create_rule(ffi::CGDisplayCreateUUIDFromDisplayID(self.0))
};
let uuid = unsafe { CFUUIDGetUUIDBytes(cf_uuid.as_concrete_TypeRef()) };
let uuid = unsafe { CFUUIDGetUUIDBytes(self.0.as_concrete_TypeRef()) };
MonitorUuid::from([
uuid.byte0,
uuid.byte1,
@@ -129,11 +141,26 @@ impl MonitorHandle {
uuid.byte15,
])
}
fn display_id(&self) -> CGDirectDisplayID {
unsafe { ffi::CGDisplayGetDisplayIDFromUUID(self.0.as_concrete_TypeRef()) }
}
#[track_caller]
pub(crate) fn new(display_id: CGDirectDisplayID) -> Option<Self> {
// kCGNullDirectDisplay
if display_id == 0 {
// `CGDisplayCreateUUIDFromDisplayID` checks kCGNullDirectDisplay internally.
warn!("constructing monitor from invalid display ID 0; falling back to main monitor");
}
let ptr = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(display_id) };
if ptr.is_null() {
return None;
}
Some(Self(unsafe { CFUUID::wrap_under_create_rule(ptr) }))
}
}
// `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 {
self.uuid() == other.uuid()
@@ -164,7 +191,8 @@ pub fn available_monitors() -> VecDeque<MonitorHandle> {
if let Ok(displays) = CGDisplay::active_displays() {
let mut monitors = VecDeque::with_capacity(displays.len());
for display in displays {
monitors.push_back(MonitorHandle(display));
// Display ID just fetched from `CGGetActiveDisplayList`, should be fine to unwrap.
monitors.push_back(MonitorHandle::new(display).expect("invalid display ID"));
}
monitors
} else {
@@ -173,7 +201,8 @@ pub fn available_monitors() -> VecDeque<MonitorHandle> {
}
pub fn primary_monitor() -> MonitorHandle {
MonitorHandle(CGDisplay::main().id)
// Display ID just fetched from `CGMainDisplayID`, should be fine to unwrap.
MonitorHandle::new(CGDisplay::main().id).expect("invalid display ID")
}
impl fmt::Debug for MonitorHandle {
@@ -190,26 +219,20 @@ impl fmt::Debug for MonitorHandle {
}
impl MonitorHandle {
pub fn new(id: CGDirectDisplayID) -> Self {
MonitorHandle(id)
}
// TODO: Be smarter about this:
// <https://github.com/glfw/glfw/blob/57cbded0760a50b9039ee0cb3f3c14f60145567c/src/cocoa_monitor.m#L44-L126>
pub fn name(&self) -> Option<String> {
let MonitorHandle(display_id) = *self;
let screen_num = CGDisplay::new(display_id).model_number();
let screen_num = CGDisplay::new(self.display_id()).model_number();
Some(format!("Monitor #{screen_num}"))
}
#[inline]
pub fn native_identifier(&self) -> u32 {
self.0
self.display_id()
}
pub fn size(&self) -> PhysicalSize<u32> {
let MonitorHandle(display_id) = *self;
let display = CGDisplay::new(display_id);
let display = CGDisplay::new(self.display_id());
let height = display.pixels_high();
let width = display.pixels_wide();
PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor())
@@ -236,14 +259,15 @@ impl MonitorHandle {
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
unsafe {
let current_display_mode = NativeDisplayMode(CGDisplayCopyDisplayMode(self.0) as _);
let current_display_mode =
NativeDisplayMode(CGDisplayCopyDisplayMode(self.display_id()) as _);
let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0);
if refresh_rate > 0.0 {
return Some((refresh_rate * 1000.0).round() as u32);
}
let mut display_link = std::ptr::null_mut();
if ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link)
if ffi::CVDisplayLinkCreateWithCGDisplay(self.display_id(), &mut display_link)
!= ffi::kCVReturnSuccess
{
return None;
@@ -266,27 +290,34 @@ impl MonitorHandle {
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)
.map(move |i| {
let mode = CFArrayGetValueAtIndex(array, i) as *mut _;
ffi::CGDisplayModeRetain(mode);
mode
})
.collect();
CFRelease(array as *const _);
modes
let array = ffi::CGDisplayCopyAllDisplayModes(self.display_id(), std::ptr::null());
if array.is_null() {
// Occasionally, certain CalDigit Thunderbolt Hubs report a spurious monitor
// during sleep/wake/cycling monitors. It tends to have null
// or 1 video mode only. See <https://github.com/bevyengine/bevy/issues/17827>.
warn!(monitor = ?self, "failed to get a list of display modes");
Vec::new()
} else {
let array_count = CFArrayGetCount(array);
let modes: Vec<_> = (0..array_count)
.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_hertz = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64;
let cg_refresh_rate_hertz = ffi::CGDisplayModeGetRefreshRate(mode);
// CGDisplayModeGetRefreshRate returns 0.0 for any display that
// isn't a CRT
let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0 {
(cg_refresh_rate_hertz * 1000) as u32
let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0.0 {
(cg_refresh_rate_hertz * 1000.0).round() as u32
} else {
refresh_rate_millihertz
};
@@ -322,8 +353,14 @@ impl MonitorHandle {
let uuid = self.uuid();
NSScreen::screens(mtm).into_iter().find(|screen| {
let other_native_id = get_display_id(screen);
let other = MonitorHandle::new(other_native_id);
uuid == other.uuid()
if let Some(other) = MonitorHandle::new(other_native_id) {
uuid == other.uuid()
} else {
// Display ID was just fetched from live NSScreen, but can still result in `None`
// with certain Thunderbolt docked monitors.
warn!(other_native_id, "comparing against screen with invalid display ID");
false
}
})
}
}

View File

@@ -316,9 +316,15 @@ declare_class!(
// sending a `None` cursor range.
None
} else {
// Clamp to string length to avoid NSRangeException from out-of-bounds
// indices sent by macOS IME (e.g. native Pinyin, see
// https://github.com/alacritty/alacritty/issues/8791).
let len = string.length();
let location = selected_range.location.min(len);
let end = selected_range.end().min(len);
// Convert the selected range from UTF-16 indices to UTF-8 indices.
let sub_string_a = unsafe { string.substringToIndex(selected_range.location) };
let sub_string_b = unsafe { string.substringToIndex(selected_range.end()) };
let sub_string_a = unsafe { string.substringToIndex(location) };
let sub_string_b = unsafe { string.substringToIndex(end) };
let lowerbound_utf8 = sub_string_a.len();
let upperbound_utf8 = sub_string_b.len();
Some((lowerbound_utf8, upperbound_utf8))

View File

@@ -373,7 +373,10 @@ declare_class!(
use std::path::PathBuf;
let pb: Retained<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap();
let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) {
Some(filenames) => filenames,
None => return false.into(),
};
let filenames: Retained<NSArray<NSString>> = unsafe { Retained::cast(filenames) };
filenames.into_iter().for_each(|file| {
@@ -399,7 +402,10 @@ declare_class!(
use std::path::PathBuf;
let pb: Retained<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap();
let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) {
Some(filenames) => filenames,
None => return false.into(),
};
let filenames: Retained<NSArray<NSString>> = unsafe { Retained::cast(filenames) };
filenames.into_iter().for_each(|file| {
@@ -1592,7 +1598,14 @@ impl WindowDelegate {
// Allow directly accessing the current monitor internally without unwrapping.
pub(crate) fn current_monitor_inner(&self) -> Option<MonitorHandle> {
let display_id = get_display_id(&*self.window().screen()?);
Some(MonitorHandle::new(display_id))
if let Some(monitor) = MonitorHandle::new(display_id) {
Some(monitor)
} else {
// NOTE: Display ID was just fetched from live NSScreen, but can still result in `None`
// with certain Thunderbolt docked monitors.
warn!(display_id, "got screen with invalid display ID");
None
}
}
#[inline]
@@ -1746,9 +1759,13 @@ impl WindowExtMacOS for WindowDelegate {
self.ivars().is_simple_fullscreen.set(true);
// Simulate pre-Lion fullscreen by hiding the dock and menu bar
let presentation_options =
let presentation_options = if self.is_borderless_game() {
NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar
} else {
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar;
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar
};
app.setPresentationOptions(presentation_options);
// Hide the titlebar
@@ -1762,11 +1779,8 @@ impl WindowExtMacOS for WindowDelegate {
self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false);
self.toggle_style_mask(NSWindowStyleMask::Resizable, false);
self.window().setMovable(false);
true
} else {
let new_mask = self.saved_style();
self.set_style_mask(new_mask);
self.ivars().is_simple_fullscreen.set(false);
let save_presentation_opts = self.ivars().save_presentation_opts.get();
@@ -1778,9 +1792,10 @@ impl WindowExtMacOS for WindowDelegate {
self.window().setFrame_display(frame, true);
self.window().setMovable(true);
true
self.set_style_mask(new_mask);
}
true
}
#[inline]

View File

@@ -420,7 +420,7 @@ impl Window {
window::ResizeDirection::West => "L",
};
self.window_socket
.write(format!("D,{}", arg).as_bytes())
.write(format!("D,{arg}").as_bytes())
.map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?;
Ok(())
}

View File

@@ -132,7 +132,13 @@ fn should_apps_use_dark_mode() -> bool {
static SHOULD_APPS_USE_DARK_MODE: Lazy<Option<ShouldAppsUseDarkMode>> = Lazy::new(|| unsafe {
const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR;
let module = LoadLibraryA("uxtheme.dll\0".as_ptr());
// We won't try to do anything for windows versions < 17763
// (Windows 10 October 2018 update)
if !*DARK_MODE_SUPPORTED {
return None;
}
let module = LoadLibraryA("uxtheme.dll\0".as_ptr().cast());
if module == 0 {
return None;

View File

@@ -574,9 +574,11 @@ fn main_thread_id() -> u32 {
//
// See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute
#[link_section = ".CRT$XCU"]
static INIT_MAIN_THREAD_ID: unsafe fn() = {
unsafe fn initer() {
unsafe { MAIN_THREAD_ID = GetCurrentThreadId() };
static INIT_MAIN_THREAD_ID: unsafe extern "C" fn() = {
unsafe extern "C" fn initer() {
unsafe {
MAIN_THREAD_ID = GetCurrentThreadId();
}
}
initer
};
@@ -738,7 +740,7 @@ fn wait_for_messages_impl(
unsafe {
// Either:
// 1. User wants to wait indefinely if timeout is not set.
// 1. User wants to wait indefinitely if timeout is not set.
// 2. We failed to get and set high resolution timer and we need something instead of it.
let wait_duration_ms = timeout.map(dur2timeout).unwrap_or(INFINITE);
@@ -1216,6 +1218,31 @@ unsafe fn public_window_callback_inner(
WM_NCLBUTTONDOWN => {
if wparam == HTCAPTION as _ {
// Prevent the user event loop from pausing when left clicking the title bar.
//
// When the user interacts with the title bar, Windows enters the modal event
// loop. Currently, a left click causes a pause for about 500ms. Sending a dummy
// mouse-move event seems to cancel the modal loop early, preventing the pause.
// The application will never see this dummy event.
//
// The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE
// event is not using the same coordinate system of the WM_NCLBUTTONDOWN event.
// One uses client-area coordinates and the other is screen-coordinates. In any
// case, passing the lparam as-is with the dummy event does not seem the cancel
// the modal loop.
//
// However, passing in a value of 0 has been observed to always cancel the pause.
//
// Other notes:
//
// For some unknown reason, the cursor will blink when clicking the title bar.
// Cancelling the modal loop early causes the blink to happen *immediately*.
// Otherwise, the blank happens *after* the pause.
//
// When right-click the title bar, the system window menu is presented to the user,
// and the modal event loop begins. This dummy event does *not* prevent the freeze
// in the main event loop caused by that popup menu.
let lparam = 0;
unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) };
}
result = ProcResult::DefWindowProc(wparam);
@@ -1585,9 +1612,9 @@ unsafe fn public_window_callback_inner(
},
WM_IME_SETCONTEXT => {
// Hide composing text drawn by IME.
let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize);
result = ProcResult::DefWindowProc(wparam);
// IME UI visibility flags are in lparam.
let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize);
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
},
// this is necessary for us to maintain minimize/restore state
@@ -2614,7 +2641,7 @@ unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: RAWINPUT) {
}
enum PointerMoveKind {
/// Pointer enterd to the window.
/// Pointer entered to the window.
Enter,
/// Pointer leaved the window client area.
Leave,

View File

@@ -109,13 +109,28 @@ impl WinIcon {
pub fn from_resource(
resource_id: u16,
size: Option<PhysicalSize<u32>>,
) -> Result<Self, BadIcon> {
Self::from_resource_ptr(resource_id as PCWSTR, size)
}
pub fn from_resource_name(
resource_name: &str,
size: Option<PhysicalSize<u32>>,
) -> Result<Self, BadIcon> {
let wide_name = util::encode_wide(resource_name);
Self::from_resource_ptr(wide_name.as_ptr(), size)
}
fn from_resource_ptr(
resource: PCWSTR,
size: Option<PhysicalSize<u32>>,
) -> Result<Self, BadIcon> {
// width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size
let (width, height) = size.map(Into::into).unwrap_or((0, 0));
let handle = unsafe {
LoadImageW(
util::get_instance_handle(),
resource_id as PCWSTR,
resource,
IMAGE_ICON,
width,
height,

View File

@@ -35,8 +35,13 @@ impl ImeContext {
let mut first = None;
let mut last = None;
let mut boundary_before_char = 0;
let mut attr_idx = 0;
for chr in text.chars() {
let Some(attr) = attrs.get(attr_idx).copied() else {
break;
};
for (attr, chr) in attrs.into_iter().zip(text.chars()) {
let char_is_targeted =
attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED;
@@ -47,6 +52,7 @@ impl ImeContext {
}
boundary_before_char += chr.len_utf8();
attr_idx += chr.len_utf16();
}
if first.is_some() && last.is_none() {

View File

@@ -213,8 +213,7 @@ impl KeyEventBuilder {
.unwrap_or(false);
if more_char_coming {
// No need to produce an event just yet, because there are still more
// characters that need to appended to this keyobard
// event
// characters that need to be appended to this keyboard event
MatchResult::TokenToRemove(pending_token)
} else {
let mut event_info = self.event_info.lock().unwrap();
@@ -335,8 +334,8 @@ impl KeyEventBuilder {
// 1. If caps-lock is *not* held down but *is* active, then we have to synthesize all
// printable keys, respecting the caps-lock state.
// 2. If caps-lock is held down, we could choose to synthesize its keypress after every
// other key, in which case all other keys *must* be sythesized as if the caps-lock state
// was be the opposite of what it currently is.
// other key, in which case all other keys *must* be synthesized as if the caps-lock
// state was be the opposite of what it currently is.
// --
// For the sake of simplicity we are choosing to always synthesize
// caps-lock first, and always use the current caps-lock state
@@ -466,7 +465,7 @@ enum PartialText {
enum PartialLogicalKey {
/// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If
/// the text consists of multiple grapheme clusters (user-precieved characters) that means that
/// the text consists of multiple grapheme clusters (user-perceived characters) that means that
/// dead key could not be combined with the second input, and in that case we should fall back
/// to using what would have without a dead-key input.
TextOr(Key),

View File

@@ -225,16 +225,16 @@ pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option<PhysicalKey> {
if scancode == 0xe11d || scancode == 0xe02a {
// At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing
// Ctrl+NumLock.
// This equvalence means that if the user presses Pause, the keyboard will emit two
// This equivalence means that if the user presses Pause, the keyboard will emit two
// subsequent keypresses:
// 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100)
// 2, 0x0045 - Which on its own can be interpreted as Pause
//
// There's another combination which isn't quite an equivalence:
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, pressing
// PrtSc (print screen) produces the following sequence:
// 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000)
// 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on
// 2, 0xE037 - Which is a numpad multiply (0x37) with an extension flag (0xE000). This on
// its own it can be interpreted as PrtSc
//
// For this reason, if we encounter the first keypress, we simply ignore it, trusting

View File

@@ -7,7 +7,7 @@ use std::{io, mem, ptr};
use crate::utils::Lazy;
use windows_sys::core::{HRESULT, PCWSTR};
use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT};
use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, POINT, RECT};
use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR};
use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
use windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER;
@@ -17,9 +17,9 @@ use windows_sys::Win32::UI::HiDpi::{
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO};
use windows_sys::Win32::UI::WindowsAndMessaging::{
ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement, GetWindowRect,
IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM,
IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT,
ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement,
GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP,
IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT,
SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_MAXIMIZE,
WINDOWPLACEMENT,
};
@@ -99,6 +99,13 @@ pub fn set_cursor_hidden(hidden: bool) {
}
}
pub fn get_cursor_position() -> Result<POINT, io::Error> {
unsafe {
let mut point: POINT = mem::zeroed();
win_to_err(GetCursorPos(&mut point)).map(|_| point)
}
}
pub fn get_cursor_clip() -> Result<RECT, io::Error> {
unsafe {
let mut rect: RECT = mem::zeroed();
@@ -221,31 +228,31 @@ pub type GetDpiForMonitor = unsafe extern "system" fn(
pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL;
pub type AdjustWindowRectExForDpi = unsafe extern "system" fn(
rect: *mut RECT,
dwStyle: u32,
bMenu: BOOL,
dwExStyle: u32,
dw_style: u32,
b_menu: BOOL,
dw_ex_style: u32,
dpi: u32,
) -> BOOL;
pub type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
entriesCount: *mut u32,
pointerCount: *mut u32,
pointerInfo: *mut POINTER_INFO,
pointer_id: u32,
entries_count: *mut u32,
pointer_count: *mut u32,
pointer_info: *mut POINTER_INFO,
) -> BOOL;
pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL;
pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointer_id: u32) -> BOOL;
pub type GetPointerDeviceRects = unsafe extern "system" fn(
device: HANDLE,
pointerDeviceRect: *mut RECT,
displayRect: *mut RECT,
pointer_device_rect: *mut RECT,
display_rect: *mut RECT,
) -> BOOL;
pub type GetPointerTouchInfo =
unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL;
unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL;
pub type GetPointerPenInfo =
unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL;
unsafe extern "system" fn(point_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL;
pub(crate) static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));

View File

@@ -428,14 +428,6 @@ impl Window {
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let confine = match mode {
CursorGrabMode::None => false,
CursorGrabMode::Confined => true,
CursorGrabMode::Locked => {
return Err(ExternalError::NotSupported(NotSupportedError::new()))
},
};
let window = self.window;
let window_state = Arc::clone(&self.window_state);
let (tx, rx) = channel();
@@ -446,7 +438,10 @@ impl Window {
.lock()
.unwrap()
.mouse
.set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine))
.set_cursor_flags(window, |f| {
f.set(CursorFlags::GRABBED, mode != CursorGrabMode::None);
f.set(CursorFlags::LOCKED, mode == CursorGrabMode::Locked);
})
.map_err(|e| ExternalError::Os(os_error!(e)));
let _ = tx.send(result);
});

View File

@@ -77,6 +77,7 @@ bitflags! {
const GRABBED = 1 << 0;
const HIDDEN = 1 << 1;
const IN_WINDOW = 1 << 2;
const LOCKED = 1 << 3;
}
}
bitflags! {
@@ -485,7 +486,22 @@ impl CursorFlags {
if util::is_focused(window) {
let cursor_clip = match self.contains(CursorFlags::GRABBED) {
true => {
if self.contains(CursorFlags::HIDDEN) {
if self.contains(CursorFlags::LOCKED) {
if let Ok(pos) = util::get_cursor_position() {
Some(RECT {
left: pos.x,
right: pos.x + 1,
top: pos.y,
bottom: pos.y + 1,
})
} else {
// If lock is applied while the cursor is not available, lock it to the
// middle of the window.
let cx = (client_rect.left + client_rect.right) / 2;
let cy = (client_rect.top + client_rect.bottom) / 2;
Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 })
}
} else if self.contains(CursorFlags::HIDDEN) {
// Confine the cursor to the center of the window if the cursor is hidden.
// This avoids problems with the cursor activating
// the taskbar if the window borders or overlaps that.

View File

@@ -884,7 +884,7 @@ impl Window {
///
/// ## Platform-specific
///
/// - **iOS / Android / Web / Wayland / Orbital:** Always returns [`None`].
/// - **iOS / Android / Web / Orbital:** Always returns [`None`].
#[inline]
pub fn resize_increments(&self) -> Option<PhysicalSize<u32>> {
let _span = tracing::debug_span!("winit::Window::resize_increments",).entered();
@@ -900,7 +900,6 @@ impl Window {
///
/// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole
/// numbers.
/// - **Wayland:** Not implemented.
/// - **iOS / Android / Web / Orbital:** Unsupported.
#[inline]
pub fn set_resize_increments<S: Into<Size>>(&self, increments: Option<S>) {
@@ -1707,8 +1706,7 @@ pub enum CursorGrabMode {
///
/// ## Platform-specific
///
/// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for
/// now.
/// - **X11:** Not implemented. Always returns [`ExternalError::NotSupported`] for now.
/// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`].
Locked,
}
@@ -1826,10 +1824,11 @@ pub enum WindowLevel {
/// ## Platform-specific
///
/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum ImePurpose {
/// No special hints for the IME (default).
#[default]
Normal,
/// The IME is used for password input.
Password,
@@ -1839,12 +1838,6 @@ pub enum ImePurpose {
Terminal,
}
impl Default for ImePurpose {
fn default() -> Self {
Self::Normal
}
}
/// An opaque token used to activate the [`Window`].
///
/// [`Window`]: crate::window::Window
@@ -1864,14 +1857,14 @@ impl ActivationToken {
/// won't get focused automatically), but won't yield any errors.
///
/// To obtain a valid token, use
#[cfg_attr(any(x11_platform, wayland_platform, docsrs), doc = " [`request_activation_token`].")]
#[cfg_attr(
any(x11_platform, wayland_platform, docsrs),
doc = " [`request_activation_token`](crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token)."
)]
#[cfg_attr(
not(any(x11_platform, wayland_platform, docsrs)),
doc = " `request_activation_token`."
)]
///
#[rustfmt::skip]
/// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token
pub fn from_raw(token: String) -> Self {
Self { token }
}