Compare commits

..

134 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
Kirill Chibisov
1ae4f5cdea Winit version 0.30.9 2025-02-06 13:13:19 +03:00
Mads Marquart
501d9b4a44 ios: fix timers
Fixes #4074.
Fixes #3816.
2025-02-06 13:13:19 +03:00
Kirill Chibisov
487137b867 x11: fix modifiers replay
The serial was not unique, thus leading to issues and replay being
triggered for normal input. Track modifiers based on they keycodes
instead, since it's more unique.

Links: https://github.com/alacritty/alacritty/issues/8461
2025-02-06 13:13:19 +03:00
Kirill Chibisov
b77ea7d218 x11: fix crash with uim
Let's just not forward events to the IME once the user requested that
it should be disabled, though, still try to change its state explicitly.

Fixes #4082.
2025-02-06 13:13:19 +03:00
Mads Marquart
3154c60ef4 Document that we require cargo +nightly fmt (#4105) 2025-02-06 13:13:19 +03:00
Tom Churchman
abfe90bddb wayland: clear IME preedit only when necessary
When all we'll be doing is setting a new preedit, the preedit doesn't
have to be explicitly cleared first. This change is perhaps debatable.

The direct reason for this is to make it easier to work around
quirks/bugs: in Masonry we've found IBus appears to resend
the IME preedit in response to `Window::set_ime_cursor_area`
(`zwp_text_input_v3::set_cursor_rectangle`). Because currently the
preedit is first cleared, a new IME cursor area is sent, which again
causes IBus to resend the preedit. This can loop for a while.

The Wayland protocol is mechanically quite prescriptive,
it says for zwp_text_input_v3:event:done.

> 1. Replace existing preedit string with the cursor.
> 2. Delete requested surrounding text.
> 3. Insert commit string with the cursor at its end.
> 4. Calculate surrounding text to send.
> 5. Insert new preedit text in cursor position.
> 6. Place cursor inside preedit text.

Winit currently doesn't do surrounding text, so 2. and 4. can be
ignored. In Winit's IME model, without a commit, sending just the
`Ime::Preedit` event without explicitly clearing is arguably still
equivalent to doing 1., 5., and 6.
2025-02-06 13:13:19 +03:00
Pascal Hertleif
090498a4a6 Use wrapper type for CFUUID (#4032)
This no longer exposes `CGDisplayCreateUUIDFromDisplayID` and instead
uses `CFUUID` to avoid a leak.

Monitor comparisons should also be more stable now.
2025-02-06 13:13:19 +03:00
Kirill Chibisov
58402b58cf Winit version 0.30.8 2025-01-04 08:45:12 +03:00
Kirill Chibisov
d7710f7264 api: add ActivationToken::{from,into}_raw
This is needed when passing and getting token from the IPC to activate
the window.
2025-01-04 08:45:12 +03:00
Kirill Chibisov
61314cd50a x11: fix cursor grab mode tracking on error
Fixes #4064.
2025-01-04 08:45:12 +03:00
Kirill Chibisov
6b5cc165dd x11: add workaround for disabling IME on gnome
GNOME doesn't list that there's a _NONE_ style at all, but it still
works if you use it.
2025-01-04 08:45:12 +03:00
Matt Campbell
43c323ccc0 windows: fix the event loop not waking on accessibility requests
Fixes #4055.
2025-01-04 08:45:12 +03:00
Kirill Chibisov
9cbce055d3 Winit version 0.30.7 2024-12-22 23:00:41 +03:00
Kirill Chibisov
727583ffbf x11: fix KeyboardInput delivered twice with IME
The filtered events were still processed even though they shouldn't once
we know that they're filtered.

Fixes #4048.
2024-12-22 23:00:41 +03:00
Kirill Chibisov
82f0949aea Winit version 0.30.6 2024-12-21 09:05:36 +03:00
Kirill Chibisov
5d2151e893 chore: fix typos 2024-12-21 09:05:36 +03:00
daxpedda
6f22822025 Update minimum version of wasm-bindgen (#3860) 2024-12-21 09:05:36 +03:00
Mads Marquart
cf829a44e7 ci: fix cargo deny 2024-12-21 09:05:36 +03:00
dependabot[bot]
f2fb7dee3d build(deps): bump EmbarkStudios/cargo-deny-action
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: daxpedda <daxpedda@gmail.com>
2024-12-21 09:05:36 +03:00
Skip R.
836e717021 macOS: Fix checking for undocumented cursors (#4033)
`AnyClass::responds_to` delegates to `class_respondsToSelector`, a
function provided by the Objective-C runtime. However, at some point,
this began to return `false` for selectors referring to undocumented
cursors, despite the cursors remaining accessible via said selectors.
That this check fails prevents the cursors from being used.

We can instead send `respondsToSelector:` to the `NSCursor` class
itself. As an instance method, this is nominally impossible; however,
Apple grants an exemption[1] that permits class objects to perform
instance methods defined in the root class.

Checking for the undocumented cursors in this way gets them working
again, at least on macOS Sequoia 15.1.1.

[1]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocObjectsClasses.html#//apple_ref/doc/uid/TP30001163-CH11-TPXREF120
2024-12-21 09:05:36 +03:00
Mads Marquart
1e44c354fe Fix MonitorHandle PartialEq and Hash on iOS (#4013) 2024-12-21 09:05:36 +03:00
Mads Marquart
1fb118031c macOS: Fix crash when pressing Caps Lock (#4024)
Events emitted by `flagsChanged:` cannot access
`charactersIgnoringModifiers`. We were previously doing this because we
were trying to re-use the `create_key_event` function, but that is unsuited
for this purpose, so I have separated the `flagsChanged:` logic out from it.
2024-12-21 09:05:36 +03:00
Mads Marquart
d6ab3dcb86 chore: fix clippy lints 2024-12-21 09:05:36 +03:00
Rodrigo Rivas Costa
d828523b39 x11: move up XInput2 event registration
It should be done before mapping the window, or we could
lose the firsst XInput2 events, such as the first FocusIn.

Fixes #2841.
2024-12-21 09:05:36 +03:00
Nico Burns
fccfd359db macOS: set activation policy by default if app is not bundled (#3961) 2024-12-21 09:05:36 +03:00
lucasmerlin
5c1cc652c4 Basic iOS IME support (#3823)
This implements basic iOS IME support (typing, backspace, support for emojis
etc but no autocomplete or copy / paste menu).

Co-authored-by: Mads Marquart <mads@marquart.dk>
2024-12-21 09:05:36 +03:00
Piotr Podusowski
1d18a6ca57 android: use show_soft_input to summon the keyboard
Route it via the `Window::set_ime_allowed` like on iOS.
2024-12-21 09:05:36 +03:00
Mads Marquart
bab2d82fd3 docs: add fn main to root examples
This is not strictly required, but makes the examples a bit easier to
read understand (especially since the `EventLoop` really _should_ be
created inside `fn main`, and not in some random function potentially
running on a random thread).
2024-12-21 09:05:36 +03:00
Matěj Laitl
bbab3d8e35 x11: fix WindowAttributesExtX11::with_x11_screen()
Based on https://github.com/rust-windowing/winit/pull/3973, which should
be merged first.

There's an API to programmatically specify X11 screen id (override what
is determined from the `DISPLAY` env variable), but it doesn't work.

Seeting up X Server with 2 screens and calling `DISPLAY=:0
X11_SCREEN_ID=1 cargo run --example window` should be equivalent to
calling `DISPLAY=:0.1 cargo run --example window`

The latter works (and places the window on the correct screen), but the
former yields

`failed to create initial window: Os(OsError { line: 620, file:
"src/platform_impl/linux/x11/window.rs", error: X11Error(X11Error {
error_kind: Match, error_code: 8, sequence: 219, bad_value: 1319,
minor_opcode: 0, major_opcode: 1, extension_name: None, request_name:
Some("CreateWindow") }) })`

_Here `1319` is the root window id for screen 0, which doesn't match the
screen 1 that we request._

The problem is that we need to factor in the screen id when determining
the parent (root) window when not explicitly set. This patch does that.

---

Also: Extend the window example with X11_{SCREEN,VISUAL}_ID env variables
2024-12-21 09:05:36 +03:00
Matěj Laitl
b02a70e6bb x11: iterate only visuals from the matching screen 2024-12-21 09:05:36 +03:00
AngelicosPhosphoros
9ceb8d4144 Improve waiting for messages on Windows
Previous version used [`SetTimer`] with `GetMessageW` for waiting.
The downside of UI timers like ones created by `SetTimer`,
is that they may be late by up to 15-16 ms.

To fix this behaviour, I added use of high resolution timers created by
[`CreateWaitableTimerExW`] with the flag
`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`.
In my previous experience, waiting on such timers have precision of
roundly 0.5 ms which is the best available on Windows at the moment. I
use [`MsgWaitForMultipleObjectsEx`] to wait simultaneously for both
timer and newly arriving events.

Unfortunately, high resolution timers are available only since Windows
10 1803. However:

1. Win 10 is already getting to the end of support, like all previous
   versions, so it is OK to rely on APIs introduced in it;
2. I use `dwMilliseconds` parameter of `MsgWaitForMultipleObjectsEx` as
   a fallback. It should perform not worse compared to waiting for
   events from `SetTimer`.

I also refactored code to remove event dispatching from function
responsible for waiting for events. This provides more clear separations
of concern and avoids unnecessary duplication of dispatching logic.

After [review] from @rib, I also moved the waiting itself from
`wait_for_messages` method to separate function, so it is clearly seen
that `wait_for_messages` do 3 things: notify app that we about to wait,
wait, notify that we have new events.

I have tested behaviour using a egui app with Vulkan rendering with
`VK_PRESENT_MODE_IMMEDIATE_KHR`, and older version consistently have
twice less FPS than requested (e.g. 30 FPS when limit is 60 and 60 FPS
when limit is 120) while newer version works more correctly (almost
always 60 FPS when limit is 60, and only 5-10 frames missing when FPS is
set to 120 or more).

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

[`CreateWaitableTimerExW`]: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createwaitabletimerexw
[`MsgWaitForMultipleObjectsEx`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjectsex
[`SetTimer`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
[review]: https://github.com/rust-windowing/winit/pull/3950#discussion_r1800184479
2024-12-21 09:05:36 +03:00
Shane Celis
485ae90aae macOS: fix panic during drag_window
Return error from it instead of unwrapping.
2024-12-21 09:05:36 +03:00
Kirill Chibisov
107bf91a2d x11: don't forward key events to IME when it's disabled
Fixes #3815.
2024-12-21 09:05:36 +03:00
Mads Marquart
1bc405e526 macOS: Fix move event sometimes being triggered on resize (#3914) 2024-12-21 09:05:36 +03:00
Kirill Chibisov
685090021b chore: fix nightly CI on linux/ios
Co-authored-by: Mads Marquart <mads@marquart.dk>
2024-12-21 09:05:36 +03:00
purajit
cfe7eb8954 Prevent winit from overriding LSUIElement in package manifests (#3920) 2024-12-21 09:05:36 +03:00
Tarek Abdel Sater
838b2459a7 macOS: add option to explicitly hide menu/dock in Borderless (#3882) 2024-12-21 09:05:36 +03:00
John Nunley
f4a5ad9c22 x11: use more information in X11 "not supported" errors
This makes it so, when X11 fails to initialize due to not loading a
library, it provides more verbose information on what exactly happened.

Fixes #3883.
Signed-off-by: John Nunley <dev@notgull.net>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2024-12-21 09:05:36 +03:00
John Nunley
00ecd1533e m: Ignore mutex poisoning in X11_BACKEND
A panic doesn't really put any of the fields in XConnection into an invalid
state, so there is no real reason to panic when poisoning is detected.
So just ignore the poison.

Closes #3870

Signed-off-by: John Nunley <dev@notgull.net>
2024-12-21 09:05:36 +03:00
Kirill Chibisov
114512bb32 Winit version 0.30.5 2024-08-08 21:26:00 +03:00
Kirill Chibisov
8c07319ced chore: drop libera link
It has low amount of users and not active, so better direct people
straight to matrix at this point.
2024-08-08 21:26:00 +03:00
daxpedda
fa3ec006e2 Add ActiveEventLoop::system_theme()
This also fixes macOS returning `None` in `Window::theme()` if no theme
override is set, instead it now returns the system theme.

MacOS and Wayland were the only ones working correctly according to the
documentation, which was an oversight. The documentation was "fixed"
now.

Fixes #3837.
2024-08-08 21:26:00 +03:00
Pascal Hertleif
f3d1f922f9 macOS: skip releasing unavailable monitors
Prevent assertion error when trying to close a fullscreen window that
was on a display that got disconnected.
2024-08-08 21:26:00 +03:00
daxpedda
77e8fe6094 Windows: use Box::leak() instead of Box::into_raw()
This was detected by a new change in Nightly Rust that applied
`#[must_use]` to the return value of `Box::into_raw()`.
2024-08-08 21:26:00 +03:00
Kirill Chibisov
5fce4a09de android: add {Active,}EventLoopExtAndroid::android_app
This type comes from the user and stored for the entire lifetime, so
no need to hide it from them after they've passed it to winit.

Fixes #3818.
2024-08-08 21:26:00 +03:00
daxpedda
5735786f42 Fix CI for Rust v1.80 (#3822)
`clippy::doc_lazy_continuation` was added, which needed some fixing from our side.
2024-08-08 21:26:00 +03:00
daxpedda
9ca8c09208 Web: don't block pen input (#3813) 2024-08-08 21:26:00 +03:00
daxpedda
4b1aa51094 macOS: fix building with feature = "rwh_04" 2024-08-08 21:26:00 +03:00
daxpedda
1dec9b4b33 Web: implement Error for CustomCursorError (#3793) 2024-08-08 21:26:00 +03:00
Kirill Chibisov
04d8a284a0 Winit version 0.30.4 2024-07-16 21:04:25 +03:00
daxpedda
949cb0f203 Web: fix MouseMotion coordinate space (#3770) 2024-07-16 21:04:25 +03:00
daxpedda
bf68ac0b14 Web: fix WindowEvent::Resized not using rAF (#3790) 2024-07-16 21:04:25 +03:00
Kirill Chibisov
ba188376d1 wayland: bump dependencies
Update SCTK as a follow-up to 1170554dbd (ignore events to dead
objects) fixing it for wl_output.
2024-07-16 21:04:25 +03:00
daxpedda
6509f8a18b Make DeviceId/WindowId::dummy() safe (#3784) 2024-07-16 21:04:25 +03:00
daxpedda
71dea4637d Fix CI (#3775) 2024-07-16 21:04:25 +03:00
Kirill Chibisov
ec24c3efd3 wayland: ignore events for dead objects
Nothing wrong will happen if we ignore events when compositor is at
wrong, at least crashing because compositor is just _wrong_ probably is
not a great option.

Links: https://github.com/alacritty/alacritty/issues/8065
2024-07-16 21:04:25 +03:00
Mads Marquart
feca480b4c Avoid path when importing modules (#3755)
Rust tooling generally works better this way. This includes
rust-analyzer, but more noticeably the output from `tracing` typically
prints the module path, which did not correspond to the actual file
system before.

Concretely, tracing output from the macOS backend changes from printing:
`winit::platform_impl::platform::util`
To printing:
`winit::platform_impl::macos::util`
2024-07-16 21:04:25 +03:00
John Nunley
655fdc896f ci: Use taiki-e/checkout-action
taiki-e/checkout-action has a few advantages over actions/checkout,
such as:

- It is written in Bash rather than Node.js
- It does not have frequent breaking changes, reducing churn

Signed-off-by: John Nunley <dev@notgull.net>
2024-07-16 21:04:25 +03:00
msiglreith
faa641e57f Add notgull as Windows maintainer 2024-07-16 21:04:25 +03:00
msiglreith
7c81364e1c Update codeowner list 2024-07-16 21:04:25 +03:00
Bruce Mitchener
6abfef1220 Use default-features, not default_features (#3746)
The latter syntax is deprecated and will be removed in Rust
2024 edition. This also generates a warning with current
versions of Rust.
2024-07-16 21:04:25 +03:00
Kirill Chibisov
d2d4d20108 Winit version 0.30.3 2024-06-21 20:16:14 +03:00
daxpedda
d8f4d8f1b7 Web: implement WaitUntilStrategy (#3739) 2024-06-21 20:16:14 +03:00
daxpedda
a974640a66 Web: set control flow strategies on EventLoop (#3740) 2024-06-21 20:16:14 +03:00
Mads Marquart
3d7d766182 macOS: set the theme on the NSWindow, instead of application-wide
This new implementation uses:
- The NSAppearanceCustomization protocol for retrieving the appearance
  of the window, instead of using the application-wide
  `-[NSApplication effectiveAppearance]`.
- Key-Value observing for observing the `effectiveAppearance` to compute
  the `ThemeChanged` event, instead of using the undocumented
  `AppleInterfaceThemeChangedNotification` notification.

This also fixes `WindowBuilder::with_theme` not having any effect, and
the conversion between `Theme` and `NSAppearance` is made a bit more
robust.
2024-06-21 20:16:14 +03:00
Kirill Chibisov
c73d8cff20 x11: fix build on arm
The c_char type, which was used under the hood is different depending
on arch, thus use it directly instead of i8.

Fixes #3735.
2024-06-21 20:16:14 +03:00
Kirill Chibisov
79aa95b212 Winit version 0.30.2 2024-06-15 20:00:04 +03:00
Kirill Chibisov
ecd14688dc Revert: Web: don't wait for polling when sending events
This is a breaking change, thus revert it for patch series.
2024-06-15 20:00:04 +03:00
Kirill Chibisov
b512ed1e63 macOS: fix opacity handling
Not using `NSColor::clearColor()` results in Quartz thinking that the
window is not transparent at all, which results in artifacts.

However, not setting the `windowBackgroundColor` in
`Window::set_transparent` results in border not properly rendered.

Fixes: 94664ff687 (Don't set the background color)
2024-06-15 20:00:04 +03:00
Kirill Chibisov
96388f4f6b chore: address 1.79 clippy lints 2024-06-15 20:00:04 +03:00
daxpedda
1745b01502 Web: fix crash InnerSizeWriter::request_inner_size() (#3727) 2024-06-15 20:00:04 +03:00
daxpedda
54e974c090 Web: don't overwrite cursor with CursorIcon::Default (#3729) 2024-06-15 20:00:04 +03:00
daxpedda
b14d5c0c99 Web: queue EventLoopProxy::send_event() to microtask 2024-06-15 20:00:04 +03:00
John Nunley
437747b966 Winit version 0.30.1
Signed-off-by: John Nunley <dev@notgull.net>
2024-06-10 18:40:33 +03:00
Mads Marquart
21e266f3b7 macOS/iOS: Various refactorings in application state (#3720)
I'm preparing to get rid of our application delegate in favour of registering
notification observers, to do so I'm renaming `app_delegate.rs` to
`app_state.rs`, and moving the functionality out of the Objective-C method
into a normal method.

Additionally, `AppState` previously implemented `Default`, but really, this
was a hack done because someone (probably myself) was too lazy to write out
the full initialization in `AppDelegate::new`.
2024-06-10 18:40:33 +03:00
Mads Marquart
3a0928af45 macOS: Improve event queuing (#3708)
* Use AppKit's internal queuing mechanisms

This allows events to be queued in a more consistent order, they're now
interleaved with events that we handle immediately (like redraw events),
instead of being handled afterwards.

* Only queue events if necessary

This makes the call stack / backtraces easier to understand whenever
possible, and generally improves upon the order in which events are
delivered.
2024-06-10 18:40:33 +03:00
Philippe Renon
ad92b4f89d doc: clarify Window::pre_present_notify availability
Fixes #3703.
2024-06-10 18:40:33 +03:00
ShikiSuen
bf4445bb62 Handle _selected_range sent to NSTextInputClient.setMarkedText(). (#3619)
Co-authored-by: Mads Marquart <mads@marquart.dk>
2024-06-10 18:40:33 +03:00
Mads Marquart
391a22217d Implement ApplicationHandler for &mut A and Box<A> (#3709) 2024-06-10 18:40:33 +03:00
Mads Marquart
fb4a674ee5 Update objc2 to v0.2.2 (#3702)
- Use new `bitflags!` support.
- Use `objc2-ui-kit`.
- Change usage of `Id` to `Retained`.
2024-06-10 18:40:33 +03:00
Diggory Hardy
43f296b2b3 event_loop: add is_x11 and is_wayland on EventLoop 2024-06-10 18:40:33 +03:00
Golden Water
042667c5eb Resize when size changes on scale change on macOS
This fixes an issue where the window glitched due to resize
when the user doesn't actually change the window, but macOS
function to update window size was still called.
2024-06-10 18:40:33 +03:00
Kirill Chibisov
3206d105fe chore: explicitly use cfg_aliases 0.2.1
This correctly handles recent nightly lint that requires to explicitly
define the CFG guards.
2024-06-10 18:40:33 +03:00
Kevin Müller
1afec3ca0d bugfix: Replace pointer dereference with read_unaligned
On Raspberry Pi, using the Rust crate eframe caused the program to crash on
mouse movement. The Backtrace lead to this specific line of code, and the exact
error was a "misaligned pointer dereference: address must be a multiple of 0x8
but is xxxx"

The edit has been tested with the Raspberry Pi, which works now.
2024-06-10 18:40:33 +03:00
Ryan Burleson
c4a8e9321d fix doc typo in application.rs (#3676) 2024-06-10 18:40:33 +03:00
linkmauve
dee7a405fc Reexport older versions of raw-window-handle
When the user decides to use an older version of raw-window-handle,
through the rwh_04 or rwh_05 features, it makes sense to reexport the
crate so they don’t have to depend on it manually and can instead use
winit::raw_window_handle.
2024-06-10 18:40:33 +03:00
Mads Marquart
a298b4d00e Reduce usage of direct msg_send! 2024-06-10 18:40:33 +03:00
Mads Marquart
aebd5edc9e macOS: Move util::EMPTY_RANGE to usage spot (#3685) 2024-06-10 18:40:33 +03:00
Mads Marquart
c801b69d3e Retain ApplicationDelegate in NSWindowDelegate and NSView
The delegate is only weakly referenced by NSApplication, so getting it
from there may fail if the event loop has been dropped.

Fixes #3668.
2024-06-10 18:40:33 +03:00
Mads Marquart
ebd6454f8f Use rustc-check-cfg (#3682) 2024-06-10 18:40:33 +03:00
daxpedda
4f2f0bc08f Web: fix Clippy v1.78 FPs (#3678) 2024-06-10 18:40:33 +03:00
131 changed files with 4108 additions and 2713 deletions

8
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,6 @@
# Android
/src/platform/android.rs @msiglreith @MarijnS95
/src/platform_impl/android @msiglreith @MarijnS95
/src/platform/android.rs @MarijnS95
/src/platform_impl/android @MarijnS95
# iOS
/src/platform/ios.rs @madsmtm
@@ -26,8 +26,8 @@
/src/platform_impl/web @daxpedda
# Windows
/src/platform/windows.rs @msiglreith
/src/platform_impl/windows @msiglreith
/src/platform/windows.rs @notgull
/src/platform_impl/windows @notgull
# Orbital (Redox OS)
/src/platform/orbital.rs @jackpot51

View File

@@ -10,7 +10,7 @@ jobs:
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: taiki-e/checkout-action@v1
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: taiki-e/checkout-action@v1
- uses: taiki-e/install-action@v2
with:
tool: typos-cli
@@ -88,7 +88,7 @@ jobs:
CMD: ${{ matrix.platform.cmd }}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/checkout-action@v1
- name: Restore cache of cargo folder
# We use `restore` and later `save`, so that we can create the key after
@@ -107,7 +107,17 @@ jobs:
- name: Generate lockfile
# Also updates the crates.io index
run: cargo generate-lockfile && cargo update -p ahash --precise 0.8.7 && cargo update -p bumpalo --precise 3.14.0
run: |
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')
@@ -220,9 +230,24 @@ jobs:
- { name: 'Windows', target: x86_64-pc-windows-gnu }
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: taiki-e/checkout-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
log-level: error
arguments: --all-features --target ${{ matrix.platform.target }}
swc:
name: Minimize JavaScript
runs-on: ubuntu-latest
steps:
- uses: taiki-e/checkout-action@v1
- name: Install SWC
run: sudo npm i -g @swc/cli
- name: Run SWC
run: |
swc src/platform_impl/web/web_sys/worker.js -o src/platform_impl/web/web_sys/worker.min.js
- name: Check for diff
run: |
[[ -z $(git status -s) ]]

3
.gitignore vendored
View File

@@ -3,8 +3,5 @@ target/
rls/
.vscode/
*~
*.wasm
*.ts
*.js
#*#
.DS_Store

12
.swcrc Normal file
View File

@@ -0,0 +1,12 @@
{
"minify": true,
"jsc": {
"target": "es2022",
"minify": {
"compress": {
"unused": true
},
"mangle": true
}
}
}

View File

@@ -19,6 +19,11 @@ All patches have to be sent on Github as [pull requests][prs]. To simplify your
life during review it's recommended to check the "give contributors write access
to the branch" checkbox.
We use unstable Rustfmt options across the project, so please run
`cargo +nightly fmt` before submitting your work. If you are unable to do so,
the maintainers can do it for you before merging, just state so in your pull
request description.
#### Handling review
During the review process certain events could require an action from your side,

View File

@@ -1,6 +1,6 @@
[package]
name = "winit"
version = "0.30.0"
version = "0.30.13"
authors = [
"The winit contributors",
"Pierre Krieger <pierre.krieger1708@gmail.com>",
@@ -14,7 +14,17 @@ rust-version.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
exclude = ["/.cargo"]
include = [
"/build.rs",
"/docs",
"/examples",
"/FEATURES.md",
"/LICENSE",
"/src",
"!/src/platform_impl/web/script",
"/src/platform_impl/web/script/**/*.min.js",
"/tests",
]
[package.metadata.docs.rs]
features = [
@@ -71,7 +81,7 @@ rwh_05 = ["dep:rwh_05", "ndk/rwh_05"]
rwh_06 = ["dep:rwh_06", "ndk/rwh_06"]
[build-dependencies]
cfg_aliases = "0.2.0"
cfg_aliases = "0.2.1"
[dependencies]
bitflags = "2"
@@ -86,11 +96,11 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = [
], optional = true }
serde = { workspace = true, optional = true }
smol_str = "0.2.0"
tracing = { version = "0.1.40", default_features = false }
tracing = { version = "0.1.40", default-features = false }
[dev-dependencies]
image = { version = "0.25.0", default-features = false, features = ["png"] }
tracing = { version = "0.1.40", default_features = false, features = ["log"] }
tracing = { version = "0.1.40", default-features = false, features = ["log"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
winit = { path = ".", features = ["rwh_05"] }
@@ -107,36 +117,43 @@ 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.1"
objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.23.1"
[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation]
version = "0.2.0"
version = "0.2.2"
features = [
"block2",
"dispatch",
"NSArray",
"NSAttributedString",
"NSData",
"NSDictionary",
"NSDistributedNotificationCenter",
"NSEnumerator",
"NSKeyValueObserving",
"NSNotification",
"NSObjCRuntime",
"NSString",
"NSPathUtilities",
"NSProcessInfo",
"NSRunLoop",
"NSString",
"NSThread",
"NSValue",
]
[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit]
version = "0.2.0"
version = "0.2.2"
features = [
"NSAppearance",
"NSApplication",
"NSBitmapImageRep",
"NSButton",
"NSColor",
"NSControl",
"NSCursor",
"NSDragging",
@@ -161,19 +178,46 @@ features = [
]
[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation]
version = "0.2.0"
version = "0.2.2"
features = [
"block2",
"dispatch",
"NSArray",
"NSEnumerator",
"NSGeometry",
"NSObjCRuntime",
"NSOperation",
"NSString",
"NSProcessInfo",
"NSThread",
"NSSet",
]
[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit]
version = "0.2.2"
features = [
"UIApplication",
"UIDevice",
"UIEvent",
"UIGeometry",
"UIGestureRecognizer",
"UITextInput",
"UITextInputTraits",
"UIOrientation",
"UIPanGestureRecognizer",
"UIPinchGestureRecognizer",
"UIResponder",
"UIRotationGestureRecognizer",
"UIScreen",
"UIScreenMode",
"UITapGestureRecognizer",
"UITouch",
"UITraitCollection",
"UIView",
"UIViewController",
"UIWindow",
]
[target.'cfg(target_os = "windows")'.dependencies]
unicode-segmentation = "1.7.1"
@@ -190,6 +234,7 @@ features = [
"Win32_System_Com",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_Security",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
@@ -209,7 +254,7 @@ features = [
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies]
ahash = { version = "0.8.7", features = ["no-rng"], optional = true }
bytemuck = { version = "1.13.1", default-features = false, optional = true }
calloop = "0.12.3"
calloop = "0.13.0"
libc = "0.2.64"
memmap2 = { version = "0.9.0", optional = true }
percent-encoding = { version = "2.0", optional = true }
@@ -219,18 +264,18 @@ rustix = { version = "0.38.4", default-features = false, features = [
"thread",
"process",
] }
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = [
sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-features = false, features = [
"calloop",
], optional = true }
sctk-adwaita = { version = "0.9.0", default_features = false, optional = true }
wayland-backend = { version = "0.3.0", default_features = false, features = [
sctk-adwaita = { version = "0.10.1", default-features = false, optional = true }
wayland-backend = { version = "0.3.10", default-features = false, features = [
"client_system",
], optional = true }
wayland-client = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.31.0", features = [
wayland-client = { version = "0.31.10", optional = true }
wayland-protocols = { version = "0.32.8", features = [
"staging",
], optional = true }
wayland-protocols-plasma = { version = "0.2.0", features = [
wayland-protocols-plasma = { version = "0.3.8", features = [
"client",
], optional = true }
x11-dl = { version = "2.19.1", optional = true }
@@ -248,57 +293,63 @@ xkbcommon-dl = "0.4.2"
orbclient = { version = "0.3.47", default-features = false }
redox_syscall = "0.4.1"
[target.'cfg(target_family = "wasm")'.dependencies.web_sys]
package = "web-sys"
version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'Blob',
'console',
'CssStyleDeclaration',
'Document',
'DomException',
'DomRect',
'DomRectReadOnly',
'Element',
'Event',
'EventTarget',
'FocusEvent',
'HtmlCanvasElement',
'HtmlElement',
'HtmlImageElement',
'ImageBitmap',
'ImageBitmapOptions',
'ImageBitmapRenderingContext',
'ImageData',
'IntersectionObserver',
'IntersectionObserverEntry',
'KeyboardEvent',
'MediaQueryList',
'MessageChannel',
'MessagePort',
'Node',
'PageTransitionEvent',
'PointerEvent',
'PremultiplyAlpha',
'ResizeObserver',
'ResizeObserverBoxOptions',
'ResizeObserverEntry',
'ResizeObserverOptions',
'ResizeObserverSize',
'VisibilityState',
'Window',
'WheelEvent',
'Url',
]
[target.'cfg(target_family = "wasm")'.dependencies]
js-sys = "0.3.64"
js-sys = "0.3.70"
pin-project = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2.93"
wasm-bindgen-futures = "0.4.43"
web-time = "1"
web_sys = { package = "web-sys", version = "0.3.70", features = [
"AbortController",
"AbortSignal",
"Blob",
"BlobPropertyBag",
"console",
"CssStyleDeclaration",
"Document",
"DomException",
"DomRect",
"DomRectReadOnly",
"Element",
"Event",
"EventTarget",
"FocusEvent",
"HtmlCanvasElement",
"HtmlElement",
"HtmlImageElement",
"ImageBitmap",
"ImageBitmapOptions",
"ImageBitmapRenderingContext",
"ImageData",
"IntersectionObserver",
"IntersectionObserverEntry",
"KeyboardEvent",
"MediaQueryList",
"MessageChannel",
"MessagePort",
"Navigator",
"Node",
"OrientationLockType",
"OrientationType",
"PageTransitionEvent",
"Permissions",
"PermissionState",
"PermissionStatus",
"PointerEvent",
"PremultiplyAlpha",
"ResizeObserver",
"ResizeObserverBoxOptions",
"ResizeObserverEntry",
"ResizeObserverOptions",
"ResizeObserverSize",
"Screen",
"ScreenOrientation",
"Url",
"VisibilityState",
"WheelEvent",
"Window",
"Worker",
] }
[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies]
atomic-waker = "1"

View File

@@ -8,7 +8,7 @@
```toml
[dependencies]
winit = "0.30.0"
winit = "0.30.13"
```
## [Documentation](https://docs.rs/winit)
@@ -19,7 +19,7 @@ For features _outside_ the scope of winit, see [Are we GUI Yet?](https://arewegu
## Contact Us
Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room. If you don't get an answer there, try [![Libera.Chat](https://img.shields.io/badge/libera.chat-%23winit-red.svg)](https://web.libera.chat/#winit).
Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room.
The maintainers have a meeting every friday at UTC 15. The meeting notes can be found [here](https://hackmd.io/@winit-meetings).
@@ -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

@@ -1,10 +1,10 @@
use cfg_aliases::cfg_aliases;
fn main() {
// The script doesn't depend on our code
// The script doesn't depend on our code.
println!("cargo:rerun-if-changed=build.rs");
// Setup cfg aliases
// Setup cfg aliases.
cfg_aliases! {
// Systems.
android_platform: { target_os = "android" },
@@ -21,4 +21,7 @@ fn main() {
wayland_platform: { all(feature = "wayland", free_unix, not(redox)) },
orbital_platform: { redox },
}
// Winit defined cfgs.
println!("cargo:rustc-check-cfg=cfg(unreleased_changelogs)");
}

View File

@@ -1,15 +1,20 @@
# https://embarkstudios.github.io/cargo-deny/
# https://embarkstudios.github.io/cargo-deny
# cargo install cargo-deny
# cargo update && cargo deny --all-features --log-level error --target aarch64-apple-ios check
# cargo update && cargo deny --target aarch64-apple-ios check
# Note: running just `cargo deny check` without a `--target` will result in
# false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324
[graph]
all-features = true
exclude-dev = true
targets = [
{ triple = "aarch64-apple-ios" },
{ triple = "aarch64-linux-android" },
{ triple = "i686-pc-windows-gnu" },
{ triple = "i686-pc-windows-msvc" },
{ triple = "i686-unknown-linux-gnu" },
{ triple = "wasm32-unknown-unknown" },
{ triple = "wasm32-unknown-unknown", features = [
"atomics",
] },
{ triple = "x86_64-apple-darwin" },
{ triple = "x86_64-apple-ios" },
{ triple = "x86_64-pc-windows-gnu" },
@@ -18,46 +23,58 @@ targets = [
{ triple = "x86_64-unknown-redox" },
]
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
ignore = []
[licenses]
allow = [
"Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"ISC", # https://tldrlegal.com/license/isc-license
"MIT", # https://tldrlegal.com/license/mit-license
"Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html
]
confidence-threshold = 1.0
private = { ignore = true }
[bans]
multiple-versions = "deny"
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
deny = []
skip = [
{ name = "raw-window-handle" }, # we intentionally have multiple versions of this
{ name = "bitflags" }, # the ecosystem is in the process of migrating.
{ name = "libloading" }, # x11rb uses a different version until the next update
{ 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" },
]
skip-tree = []
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
[bans.build]
include-archives = true
interpreted = "deny"
[licenses]
private = { ignore = true }
unlicensed = "deny"
allow-osi-fsf-free = "neither"
confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text
copyleft = "deny"
[[bans.build.bypass]]
allow = [
"Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
"Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
"ISC", # https://tldrlegal.com/license/-isc-license
"LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321
"MIT-0", # https://choosealicense.com/licenses/mit-0/
"MIT", # https://tldrlegal.com/license/mit-license
"MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux.
"OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html
"OpenSSL", # https://www.openssl.org/source/license.html - used on Linux
"Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
"Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
{ path = "generate-bindings.sh", checksum = "268ec23248218d779e33853cdc60e2985e70214ff004716cd734270de1f6b561" },
]
crate = "android-activity"
[[bans.build.bypass]]
allow-globs = ["ci/*", "githooks/*", "cargo.sh"]
crate = "zerocopy"
[[bans.build.bypass]]
allow-globs = ["freetype2/*"]
crate = "freetype-sys"
[[bans.build.bypass]]
allow-globs = ["lib/*.a"]
crate = "windows_i686_gnu"
[[bans.build.bypass]]
allow-globs = ["lib/*.lib"]
crate = "windows_i686_msvc"
[[bans.build.bypass]]
allow-globs = ["lib/*.a"]
crate = "windows_x86_64_gnu"
[[bans.build.bypass]]
allow-globs = ["lib/*.lib"]
crate = "windows_x86_64_msvc"

View File

@@ -31,6 +31,8 @@ use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMac
use winit::platform::startup_notify::{
self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify,
};
#[cfg(x11_platform)]
use winit::platform::x11::WindowAttributesExtX11;
#[path = "util/tracing.rs"]
mod tracing;
@@ -140,6 +142,28 @@ impl Application {
window_attributes = window_attributes.with_activation_token(token);
}
#[cfg(x11_platform)]
match std::env::var("X11_VISUAL_ID") {
Ok(visual_id_str) => {
info!("Using X11 visual id {visual_id_str}");
let visual_id = visual_id_str.parse()?;
window_attributes = window_attributes.with_x11_visual(visual_id);
},
Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"),
}
#[cfg(x11_platform)]
match std::env::var("X11_SCREEN_ID") {
Ok(screen_id_str) => {
info!("Placing the window on X11 screen {screen_id_str}");
let screen_id = screen_id_str.parse()?;
window_attributes = window_attributes.with_x11_screen(screen_id);
},
Err(_) => info!(
"Set the X11_SCREEN_ID env variable to place the window on non-default screen"
),
}
#[cfg(macos_platform)]
if let Some(tab_id) = _tab_id {
window_attributes = window_attributes.with_tabbing_identifier(&tab_id);
@@ -212,6 +236,12 @@ impl Application {
Action::PrintHelp => self.print_help(),
#[cfg(macos_platform)]
Action::CycleOptionAsAlt => window.cycle_option_as_alt(),
Action::SetTheme(theme) => {
window.window.set_theme(theme);
// Get the resulting current theme to draw with
let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark);
window.set_draw_theme(actual_theme);
},
#[cfg(macos_platform)]
Action::CreateNewTab => {
let tab_id = window.window.tabbing_identifier();
@@ -334,7 +364,7 @@ impl ApplicationHandler<UserEvent> for Application {
},
WindowEvent::ThemeChanged(theme) => {
info!("Theme changed to {theme:?}");
window.set_theme(theme);
window.set_draw_theme(theme);
},
WindowEvent::RedrawRequested => {
if let Err(err) = window.draw() {
@@ -733,8 +763,8 @@ impl WindowState {
self.window.request_redraw();
}
/// Change the theme.
fn set_theme(&mut self, theme: Theme) {
/// Change the theme that things are drawn in.
fn set_draw_theme(&mut self, theme: Theme) {
self.theme = theme;
self.window.request_redraw();
}
@@ -884,6 +914,7 @@ enum Action {
ShowWindowMenu,
#[cfg(macos_platform)]
CycleOptionAsAlt,
SetTheme(Option<Theme>),
#[cfg(macos_platform)]
CreateNewTab,
RequestResize,
@@ -915,6 +946,9 @@ impl Action {
Action::ShowWindowMenu => "Show window menu",
#[cfg(macos_platform)]
Action::CycleOptionAsAlt => "Cycle option as alt mode",
Action::SetTheme(None) => "Change to the system theme",
Action::SetTheme(Some(Theme::Light)) => "Change to a light theme",
Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme",
#[cfg(macos_platform)]
Action::CreateNewTab => "Create new tab",
Action::RequestResize => "Request a resize",
@@ -1059,6 +1093,10 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
Action::AnimationCustomCursor,
),
Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility),
// K.
Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)),
Binding::new("K", ModifiersState::SUPER, Action::SetTheme(Some(Theme::Light))),
Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))),
#[cfg(macos_platform)]
Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab),
#[cfg(macos_platform)]

View File

@@ -190,7 +190,7 @@ pub trait ApplicationHandler<T: 'static = ()> {
/// Emitted when the event loop is being shut down.
///
/// This is irreversible - if this method is called, it is guaranteed that the event loop
/// will exist right after.
/// will exit right after.
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
let _ = event_loop;
}
@@ -223,3 +223,117 @@ pub trait ApplicationHandler<T: 'static = ()> {
let _ = event_loop;
}
}
impl<A: ?Sized + ApplicationHandler<T>, T: 'static> ApplicationHandler<T> for &mut A {
#[inline]
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
(**self).new_events(event_loop, cause);
}
#[inline]
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
(**self).resumed(event_loop);
}
#[inline]
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) {
(**self).user_event(event_loop, event);
}
#[inline]
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
(**self).window_event(event_loop, window_id, event);
}
#[inline]
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: DeviceId,
event: DeviceEvent,
) {
(**self).device_event(event_loop, device_id, event);
}
#[inline]
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
(**self).about_to_wait(event_loop);
}
#[inline]
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
(**self).suspended(event_loop);
}
#[inline]
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
(**self).exiting(event_loop);
}
#[inline]
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
(**self).memory_warning(event_loop);
}
}
impl<A: ?Sized + ApplicationHandler<T>, T: 'static> ApplicationHandler<T> for Box<A> {
#[inline]
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
(**self).new_events(event_loop, cause);
}
#[inline]
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
(**self).resumed(event_loop);
}
#[inline]
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) {
(**self).user_event(event_loop, event);
}
#[inline]
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
(**self).window_event(event_loop, window_id, event);
}
#[inline]
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: DeviceId,
event: DeviceEvent,
) {
(**self).device_event(event_loop, device_id, event);
}
#[inline]
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
(**self).about_to_wait(event_loop);
}
#[inline]
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
(**self).suspended(event_loop);
}
#[inline]
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
(**self).exiting(event_loop);
}
#[inline]
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
(**self).memory_warning(event_loop);
}
}

View File

@@ -137,7 +137,7 @@
- On X11, non-resizable windows now have maximize explicitly disabled.
- On Windows, support paths longer than MAX_PATH (260 characters) in `WindowEvent::DroppedFile`
and `WindowEvent::HoveredFile`.
and `WindowEvent::HoveredFile`.
- On Mac, implement `DeviceEvent::Button`.
- Change `Event::Suspended(true / false)` to `Event::Suspended` and `Event::Resumed`.
- On X11, fix sanity check which checks that a monitor's reported width and height (in millimeters) are non-zero when calculating the DPI factor.

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,192 @@
## 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
- On Wayland, no longer send an explicit clearing `Ime::Preedit` just prior to a new `Ime::Preedit`.
### Fixed
- On X11, fix crash with uim.
- On X11, fix modifiers for keys that were sent by the same X11 request.
- On iOS, fix high CPU usage even when using `ControlFlow::Wait`.
## 0.30.8
### Added
- `ActivationToken::from_raw` and `ActivationToken::into_raw`.
- On X11, add a workaround for disabling IME on GNOME.
### Fixed
- On Windows, fixed the event loop not waking on accessibility requests.
- On X11, fixed cursor grab mode state tracking on error.
## 0.30.7
### Fixed
- On X11, fixed KeyboardInput delivered twice when IME enabled.
## 0.30.6
### Added
- On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game`
to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games.
- On X11, the `window` example now understands the `X11_VISUAL_ID` and `X11_SCREEN_ID` env
variables to test the respective modifiers of window creation.
- On Android, the soft keyboard can now be shown using `Window::set_ime_allowed`.
- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`.
### Fixed
- On macOS, fix `WindowEvent::Moved` sometimes being triggered unnecessarily on resize.
- On macOS, package manifest definitions of `LSUIElement` will no longer be overridden with the
default activation policy, unless explicitly provided during initialization.
- On macOS, fix crash when calling `drag_window()` without a left click present.
- On X11, key events forward to IME anyway, even when it's disabled.
- On Windows, make `ControlFlow::WaitUntil` work more precisely using `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`.
- On X11, creating windows on screen that is not the first one (e.g. `DISPLAY=:0.1`) works again.
- On X11, creating windows while passing `with_x11_screen(non_default_screen)` works again.
- On X11, fix XInput handling that prevented a new window from getting the focus in some cases.
- On macOS, fix crash when pressing Caps Lock in certain configurations.
- On iOS, fixed `MonitorHandle`'s `PartialEq` and `Hash` implementations.
- On macOS, fixed undocumented cursors (e.g. zoom, resize, help) always appearing to be invalid and falling back to the default cursor.
## 0.30.5
### Added
- Add `ActiveEventLoop::system_theme()`, returning the current system theme.
- On Web, implement `Error` for `platform::web::CustomCursorError`.
- On Android, add `{Active,}EventLoopExtAndroid::android_app()` to access the app used to create the loop.
### Fixed
- On MacOS, fix building with `feature = "rwh_04"`.
- On Web, pen events are now routed through to `WindowEvent::Cursor*`.
- On macOS, fix panic when releasing not available monitor.
- On MacOS, return the system theme in `Window::theme()` if no theme override is set.
## 0.30.4
### Changed
- `DeviceId::dummy()` and `WindowId::dummy()` are no longer marked `unsafe`.
### Fixed
- On Wayland, avoid crashing when compositor is misbehaving.
- On Web, fix `WindowEvent::Resized` not using `requestAnimationFrame` when sending
`WindowEvent::RedrawRequested` and also potentially causing `WindowEvent::RedrawRequested`
to not be de-duplicated.
- Account for different browser engine implementations of pointer movement coordinate space.
## 0.30.3
### Added
- On Web, add `EventLoopExtWebSys::(set_)poll_strategy()` to allow setting
control flow strategies before starting the event loop.
- On Web, add `WaitUntilStrategy`, which allows to set different strategies for
`ControlFlow::WaitUntil`. By default the Prioritized Task Scheduling API is
used, with a fallback to `setTimeout()` with a trick to circumvent throttling
to 4ms. But an option to use a Web worker to schedule the timer is available
as well, which commonly prevents any throttling when the window is not focused.
### Changed
- On macOS, set the window theme on the `NSWindow` instead of application-wide.
### Fixed
- On X11, build on arm platforms.
- On macOS, fixed `WindowBuilder::with_theme` not having any effect on the window.
## 0.30.2
### Fixed
- On Web, fix `EventLoopProxy::send_event()` triggering event loop immediately
when not called from inside the event loop. Now queues a microtask instead.
- On Web, stop overwriting default cursor with `CursorIcon::Default`.
- On Web, prevent crash when using `InnerSizeWriter::request_inner_size()`.
- On macOS, fix not working opacity for entire window.
## 0.30.1
### Added
- Reexport `raw-window-handle` versions 0.4 and 0.5 as `raw_window_handle_04` and `raw_window_handle_05`.
- Implement `ApplicationHandler` for `&mut` references and heap allocations to something that implements `ApplicationHandler`.
### Fixed
- On macOS, fix panic on exit when dropping windows outside the event loop.
- On macOS, fix window dragging glitches when dragging across a monitor boundary with different scale factor.
- On macOS, fix the range in `Ime::Preedit`.
- On macOS, use the system's internal mechanisms for queuing events.
- On macOS, handle events directly instead of queuing when possible.
## 0.30.0
### Added

View File

@@ -3,20 +3,20 @@
- Added event `WindowEvent::HiDPIFactorChanged`.
- Added method `MonitorId::get_hidpi_factor`.
- Deprecated `get_inner_size_pixels` and `get_inner_size_points` methods of `Window` in favor of
`get_inner_size`.
`get_inner_size`.
- **Breaking:** `EventsLoop` is `!Send` and `!Sync` because of platform-dependant constraints,
but `Window`, `WindowId`, `DeviceId` and `MonitorId` guaranteed to be `Send`.
- `MonitorId::get_position` now returns `(i32, i32)` instead of `(u32, u32)`.
- Rewrite of the wayland backend to use wayland-client-0.11
- Support for dead keys on wayland for keyboard utf8 input
- Monitor enumeration on Windows is now implemented using `EnumDisplayMonitors` instead of
`EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`.
`EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`.
- On Windows added `MonitorIdExt::hmonitor` method
- Impl `Clone` for `EventsLoopProxy`
- `EventsLoop::get_primary_monitor()` on X11 will fallback to any available monitor if no primary is found
- Support for touch event on wayland
- `WindowEvent`s `MouseMoved`, `MouseEntered`, and `MouseLeft` have been renamed to
`CursorMoved`, `CursorEntered`, and `CursorLeft`.
`CursorMoved`, `CursorEntered`, and `CursorLeft`.
- New `DeviceEvent`s added, `MouseMotion` and `MouseWheel`.
- Send `CursorMoved` event after `CursorEntered` and `Focused` events.
- Add support for `ModifiersState`, `MouseMove`, `MouseInput`, `MouseMotion` for emscripten backend.

View File

@@ -389,6 +389,8 @@ pub enum WindowEvent {
/// Applications might wish to react to this to change the theme of the content of the window
/// when the system changes the window theme.
///
/// This only reports a change if the window theme was not overridden by [`Window::set_theme`].
///
/// ## Platform-specific
///
/// - **iOS / Android / X11 / Wayland / Orbital:** Unsupported.
@@ -447,16 +449,13 @@ pub struct DeviceId(pub(crate) platform_impl::DeviceId);
impl DeviceId {
/// Returns a dummy id, useful for unit testing.
///
/// # Safety
/// # Notes
///
/// The only guarantee made about the return value of this function is that
/// it will always be equal to itself and to future values returned by this function.
/// No other guarantees are made. This may be equal to a real `DeviceId`.
///
/// **Passing this into a winit function will result in undefined behavior.**
pub const unsafe fn dummy() -> Self {
#[allow(unused_unsafe)]
DeviceId(unsafe { platform_impl::DeviceId::dummy() })
pub const fn dummy() -> Self {
DeviceId(platform_impl::DeviceId::dummy())
}
}
@@ -533,11 +532,11 @@ pub struct KeyEvent {
/// ## Caveats
///
/// - Certain niche hardware will shuffle around physical key positions, e.g. a keyboard that
/// implements DVORAK in hardware (or firmware)
/// implements DVORAK in hardware (or firmware)
/// - Your application will likely have to handle keyboards which are missing keys that your
/// own keyboard has.
/// own keyboard has.
/// - Certain `KeyCode`s will move between a couple of different positions depending on what
/// layout the keyboard was manufactured to support.
/// layout the keyboard was manufactured to support.
///
/// **Because of these caveats, it is important that you provide users with a way to configure
/// most (if not all) keybinds in your application.**
@@ -559,8 +558,7 @@ pub struct KeyEvent {
///
/// This has two use cases:
/// - Allows querying whether the current input is a Dead key.
/// - Allows handling key-bindings on platforms which don't
/// support [`key_without_modifiers`].
/// - Allows handling key-bindings on platforms which don't support [`key_without_modifiers`].
///
/// If you use this field (or [`key_without_modifiers`] for that matter) for keyboard
/// shortcuts, **it is important that you provide users with a way to configure your
@@ -568,8 +566,8 @@ pub struct KeyEvent {
/// incompatible keyboard layout.**
///
/// ## Platform-specific
/// - **Web:** Dead keys might be reported as the real key instead
/// of `Dead` depending on the browser/OS.
/// - **Web:** Dead keys might be reported as the real key instead of `Dead` depending on the
/// browser/OS.
///
/// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers
pub logical_key: keyboard::Key,
@@ -623,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.
///
/// ```
@@ -851,8 +849,8 @@ pub struct Touch {
///
/// - Only available on **iOS** 9.0+, **Windows** 8+, **Web**, and **Android**.
/// - **Android**: This will never be [None]. If the device doesn't support pressure
/// sensitivity, force will either be 0.0 or 1.0. Also see the
/// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE).
/// sensitivity, force will either be 0.0 or 1.0. Also see the
/// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE).
pub force: Option<Force>,
/// Unique identifier of a finger.
pub id: u64,
@@ -1019,7 +1017,7 @@ mod tests {
($closure:expr) => {{
#[allow(unused_mut)]
let mut x = $closure;
let did = unsafe { event::DeviceId::dummy() };
let did = event::DeviceId::dummy();
#[allow(deprecated)]
{
@@ -1029,7 +1027,7 @@ mod tests {
use crate::window::WindowId;
// Mainline events.
let wid = unsafe { WindowId::dummy() };
let wid = WindowId::dummy();
x(UserEvent(()));
x(NewEvents(event::StartCause::Init));
x(AboutToWait);
@@ -1154,11 +1152,11 @@ 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();
let did = unsafe { crate::event::DeviceId::dummy() }.clone();
let did = crate::event::DeviceId::dummy().clone();
HashSet::new().insert(did);
let mut set = [did, did, did];
set.sort_unstable();

View File

@@ -23,7 +23,7 @@ use crate::error::{EventLoopError, OsError};
use crate::event::Event;
use crate::monitor::MonitorHandle;
use crate::platform_impl;
use crate::window::{CustomCursor, CustomCursorSource, Window, WindowAttributes};
use crate::window::{CustomCursor, CustomCursorSource, Theme, Window, WindowAttributes};
/// Provides a way to retrieve events from the system and from the windows that were registered to
/// the events loop.
@@ -103,11 +103,14 @@ impl<T> EventLoopBuilder<T> {
///
/// [`platform`]: crate::platform
#[cfg_attr(
android,
android_platform,
doc = "[`.with_android_app(app)`]: \
crate::platform::android::EventLoopBuilderExtAndroid::with_android_app"
)]
#[cfg_attr(not(android), doc = "[`.with_android_app(app)`]: #only-available-on-android")]
#[cfg_attr(
not(android_platform),
doc = "[`.with_android_app(app)`]: #only-available-on-android"
)]
#[inline]
pub fn build(&mut self) -> Result<EventLoop<T>, EventLoopError> {
let _span = tracing::debug_span!("winit::EventLoopBuilder::build").entered();
@@ -434,6 +437,17 @@ impl ActiveEventLoop {
self.p.listen_device_events(allowed);
}
/// Returns the current system theme.
///
/// Returns `None` if it cannot be determined on the current platform.
///
/// ## Platform-specific
///
/// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported.
pub fn system_theme(&self) -> Option<Theme> {
self.p.system_theme()
}
/// Sets the [`ControlFlow`].
pub fn set_control_flow(&self, control_flow: ControlFlow) {
self.p.set_control_flow(control_flow)
@@ -487,7 +501,7 @@ unsafe impl rwh_05::HasRawDisplayHandle for ActiveEventLoop {
/// A proxy for the underlying display handle.
///
/// The purpose of this type is to provide a cheaply clonable handle to the underlying
/// The purpose of this type is to provide a cheaply cloneable handle to the underlying
/// display handle. This is often used by graphics APIs to connect to the underlying APIs.
/// It is difficult to keep a handle to the [`EventLoop`] type or the [`ActiveEventLoop`]
/// type. In contrast, this type involves no lifetimes and can be persisted for as long as

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

@@ -7,7 +7,12 @@
//!
//! ```no_run
//! use winit::event_loop::EventLoop;
//! let event_loop = EventLoop::new().unwrap();
//!
//! # // Intentionally use `fn main` for clarity
//! fn main() {
//! let event_loop = EventLoop::new().unwrap();
//! // ...
//! }
//! ```
//!
//! Then you create a [`Window`] with [`create_window`].
@@ -84,19 +89,22 @@
//! }
//! }
//!
//! let event_loop = EventLoop::new().unwrap();
//! # // Intentionally use `fn main` for clarity
//! fn main() {
//! let event_loop = EventLoop::new().unwrap();
//!
//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't
//! // dispatched any events. This is ideal for games and similar applications.
//! event_loop.set_control_flow(ControlFlow::Poll);
//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't
//! // dispatched any events. This is ideal for games and similar applications.
//! event_loop.set_control_flow(ControlFlow::Poll);
//!
//! // ControlFlow::Wait pauses the event loop if no events are available to process.
//! // This is ideal for non-game applications that only update in response to user
//! // input, and uses significantly less power/CPU time than ControlFlow::Poll.
//! event_loop.set_control_flow(ControlFlow::Wait);
//! // ControlFlow::Wait pauses the event loop if no events are available to process.
//! // This is ideal for non-game applications that only update in response to user
//! // input, and uses significantly less power/CPU time than ControlFlow::Poll.
//! event_loop.set_control_flow(ControlFlow::Wait);
//!
//! let mut app = App::default();
//! event_loop.run_app(&mut app);
//! let mut app = App::default();
//! event_loop.run_app(&mut app);
//! }
//! ```
//!
//! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be
@@ -157,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
@@ -175,9 +182,16 @@
#![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;
#[cfg(feature = "rwh_05")]
pub use rwh_05 as raw_window_handle_05;
#[cfg(feature = "rwh_06")]
pub use rwh_06 as raw_window_handle;

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.0",
//! 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
@@ -76,12 +76,22 @@ use crate::window::{Window, WindowAttributes};
use self::activity::{AndroidApp, ConfigurationRef, Rect};
/// Additional methods on [`EventLoop`] that are specific to Android.
pub trait EventLoopExtAndroid {}
pub trait EventLoopExtAndroid {
/// Get the [`AndroidApp`] which was used to create this event loop.
fn android_app(&self) -> &AndroidApp;
}
impl<T> EventLoopExtAndroid for EventLoop<T> {}
impl<T> EventLoopExtAndroid for EventLoop<T> {
fn android_app(&self) -> &AndroidApp {
&self.event_loop.android_app
}
}
/// Additional methods on [`ActiveEventLoop`] that are specific to Android.
pub trait ActiveEventLoopExtAndroid {}
pub trait ActiveEventLoopExtAndroid {
/// Get the [`AndroidApp`] which was used to create this event loop.
fn android_app(&self) -> &AndroidApp;
}
/// Additional methods on [`Window`] that are specific to Android.
pub trait WindowExtAndroid {
@@ -100,7 +110,11 @@ impl WindowExtAndroid for Window {
}
}
impl ActiveEventLoopExtAndroid for ActiveEventLoop {}
impl ActiveEventLoopExtAndroid for ActiveEventLoop {
fn android_app(&self) -> &AndroidApp {
&self.p.app
}
}
/// Additional methods on [`WindowAttributes`] that are specific to Android.
pub trait WindowAttributesExtAndroid {}
@@ -108,9 +122,9 @@ pub trait WindowAttributesExtAndroid {}
impl WindowAttributesExtAndroid for WindowAttributes {}
pub trait EventLoopBuilderExtAndroid {
/// Associates the `AndroidApp` that was passed to `android_main()` with the event loop
/// Associates the [`AndroidApp`] that was passed to `android_main()` with the event loop
///
/// This must be called on Android since the `AndroidApp` is not global state.
/// This must be called on Android since the [`AndroidApp`] is not global state.
fn with_android_app(&mut self, app: AndroidApp) -> &mut Self;
/// Calling this will mark the volume keys to be manually handled by the application
@@ -147,7 +161,7 @@ impl<T> EventLoopBuilderExtAndroid for EventLoopBuilder<T> {
/// depending on the `android_activity` crate, and instead consume the API that
/// is re-exported by Winit.
///
/// For compatibility applications should then import the `AndroidApp` type for
/// For compatibility applications should then import the [`AndroidApp`] type for
/// their `android_main(app: AndroidApp)` function like:
/// ```rust
/// #[cfg(target_os = "android")]

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;
@@ -357,7 +370,7 @@ impl MonitorHandleExtIOS for MonitorHandle {
fn ui_screen(&self) -> *mut c_void {
// SAFETY: The marker is only used to get the pointer of the screen
let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() };
objc2::rc::Id::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void
objc2::rc::Retained::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void
}
#[inline]

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;
@@ -94,6 +162,14 @@ pub trait WindowExtMacOS {
/// Getter for the [`WindowExtMacOS::set_option_as_alt`].
fn option_as_alt(&self) -> OptionAsAlt;
/// 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`].
fn is_borderless_game(&self) -> bool;
}
impl WindowExtMacOS for Window {
@@ -166,6 +242,16 @@ impl WindowExtMacOS for Window {
fn option_as_alt(&self) -> OptionAsAlt {
self.window.maybe_wait_on_main(|w| w.option_as_alt())
}
#[inline]
fn set_borderless_game(&self, borderless_game: bool) {
self.window.maybe_wait_on_main(|w| w.set_borderless_game(borderless_game))
}
#[inline]
fn is_borderless_game(&self) -> bool {
self.window.maybe_wait_on_main(|w| w.is_borderless_game())
}
}
/// Corresponds to `NSApplicationActivationPolicy`.
@@ -216,6 +302,8 @@ pub trait WindowAttributesExtMacOS {
///
/// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set.
fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self;
/// See [`WindowExtMacOS::set_borderless_game`] for details on what this means if set.
fn with_borderless_game(self, borderless_game: bool) -> Self;
}
impl WindowAttributesExtMacOS for WindowAttributes {
@@ -284,12 +372,21 @@ impl WindowAttributesExtMacOS for WindowAttributes {
self.platform_specific.option_as_alt = option_as_alt;
self
}
#[inline]
fn with_borderless_game(mut self, borderless_game: bool) -> Self {
self.platform_specific.borderless_game = borderless_game;
self
}
}
pub trait EventLoopBuilderExtMacOS {
/// Sets the activation policy for the application.
/// Sets the activation policy for the application. If used, this will override
/// any relevant settings provided in the package manifest.
/// For instance, `with_activation_policy(ActivationPolicy::Regular)` will prevent
/// the application from running as an "agent", even if LSUIElement is set to true.
///
/// It is set to [`ActivationPolicy::Regular`] by default.
/// If unused, the Winit will honor the package manifest.
///
/// # Example
///
@@ -341,7 +438,7 @@ pub trait EventLoopBuilderExtMacOS {
impl<T> EventLoopBuilderExtMacOS for EventLoopBuilder<T> {
#[inline]
fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self {
self.platform_specific.activation_policy = activation_policy;
self.platform_specific.activation_policy = Some(activation_policy);
self
}
@@ -375,7 +472,7 @@ impl MonitorHandleExtMacOS for MonitorHandle {
fn ns_screen(&self) -> Option<*mut c_void> {
// SAFETY: We only use the marker to get a pointer
let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() };
self.inner.ns_screen(mtm).map(|s| objc2::rc::Id::as_ptr(&s) as _)
self.inner.ns_screen(mtm).map(|s| objc2::rc::Retained::as_ptr(&s) as _)
}
}

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
///
@@ -74,13 +74,13 @@ pub trait EventLoopExtPumpEvents {
///
/// ## Unsupported Platforms
///
/// - **Web:** This API is fundamentally incompatible with the event-based way in which
/// Web browsers work because it's not possible to have a long-running external
/// loop that would block the browser and there is nothing that can be
/// polled to ask for new new events. Events are delivered via callbacks based
/// on an event loop that is internal to the browser itself.
/// - **Web:** This API is fundamentally incompatible with the event-based way in which Web
/// browsers work because it's not possible to have a long-running external loop that would
/// block the browser and there is nothing that can be polled to ask for new new events.
/// Events are delivered via callbacks based on an event loop that is internal to the browser
/// itself.
/// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so
/// there's no way to support the same approach to polling as on MacOS.
/// there's no way to support the same approach to polling as on MacOS.
///
/// ## Platform-specific
///

View File

@@ -42,7 +42,9 @@ pub trait EventLoopExtRunOnDemand {
/// # Caveats
/// - This extension isn't available on all platforms, since it's not always possible to return
/// to the caller (specifically this is impossible on iOS and Web - though with the Web
/// backend it is possible to use `EventLoopExtWebSys::spawn()`[^1] more than once instead).
/// backend it is possible to use `EventLoopExtWebSys::spawn()`
#[cfg_attr(not(web_platform), doc = "[^1]")]
/// more than once instead).
/// - No [`Window`] state can be carried between separate runs of the event loop.
///
/// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you
@@ -61,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

@@ -64,7 +64,7 @@ impl EventLoopExtStartupNotify for ActiveEventLoop {
crate::platform_impl::ActiveEventLoop::X(_) => env::var(X11_VAR),
}
.ok()
.map(ActivationToken::_new)
.map(ActivationToken::from_raw)
}
}
@@ -94,6 +94,6 @@ pub fn reset_activation_token_env() {
///
/// This could be used before running daemon processes.
pub fn set_activation_token_env(token: ActivationToken) {
env::set_var(X11_VAR, &token._token);
env::set_var(WAYLAND_VAR, token._token);
env::set_var(X11_VAR, &token.token);
env::set_var(WAYLAND_VAR, token.token);
}

View File

@@ -13,7 +13,11 @@
//! * `wayland-csd-adwaita` (default).
//! * `wayland-csd-adwaita-crossfont`.
//! * `wayland-csd-adwaita-notitle`.
use crate::event_loop::{ActiveEventLoop, EventLoopBuilder};
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};
@@ -32,6 +36,19 @@ impl ActiveEventLoopExtWayland for ActiveEventLoop {
}
}
/// Additional methods on [`EventLoop`] that are specific to Wayland.
pub trait EventLoopExtWayland {
/// True if the [`EventLoop`] uses Wayland.
fn is_wayland(&self) -> bool;
}
impl<T: 'static> EventLoopExtWayland for EventLoop<T> {
#[inline]
fn is_wayland(&self) -> bool {
self.event_loop.is_wayland()
}
}
/// Additional methods on [`EventLoopBuilder`] that are specific to Wayland.
pub trait EventLoopBuilderExtWayland {
/// Force using Wayland.
@@ -59,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

@@ -190,6 +190,34 @@ pub trait EventLoopExtWebSys {
fn spawn<F>(self, event_handler: F)
where
F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
/// Sets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn set_poll_strategy(&self, strategy: PollStrategy);
/// Gets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;
/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;
}
impl<T> EventLoopExtWebSys for EventLoop<T> {
@@ -207,6 +235,22 @@ impl<T> EventLoopExtWebSys for EventLoop<T> {
{
self.event_loop.spawn(event_handler)
}
fn set_poll_strategy(&self, strategy: PollStrategy) {
self.event_loop.set_poll_strategy(strategy);
}
fn poll_strategy(&self) -> PollStrategy {
self.event_loop.poll_strategy()
}
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.event_loop.set_wait_until_strategy(strategy);
}
fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.event_loop.wait_until_strategy()
}
}
pub trait ActiveEventLoopExtWebSys {
@@ -224,6 +268,20 @@ pub trait ActiveEventLoopExtWebSys {
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;
/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;
/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
/// cursor has completely finished loading.
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
@@ -244,6 +302,16 @@ impl ActiveEventLoopExtWebSys for ActiveEventLoop {
fn poll_strategy(&self) -> PollStrategy {
self.p.poll_strategy()
}
#[inline]
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.p.set_wait_until_strategy(strategy);
}
#[inline]
fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.p.wait_until_strategy()
}
}
/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
@@ -272,6 +340,29 @@ pub enum PollStrategy {
Scheduler,
}
/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WaitUntilStrategy {
/// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
/// this will fallback to [`setTimeout()`].
///
/// This strategy is commonly not affected by browser throttling unless the window is not
/// focused.
///
/// This is the default strategy.
///
/// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
/// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
#[default]
Scheduler,
/// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker].
///
/// This strategy is commonly not affected by browser throttling regardless of window focus.
///
/// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
Worker,
}
pub trait CustomCursorExtWebSys {
/// Returns if this cursor is an animation.
fn is_animation(&self) -> bool;
@@ -331,7 +422,7 @@ impl fmt::Display for BadAnimation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "No cursors supplied"),
Self::Animation => write!(f, "A supplied cursor is an animtion"),
Self::Animation => write!(f, "A supplied cursor is an animation"),
}
}
}
@@ -370,3 +461,5 @@ impl Display for CustomCursorError {
}
}
}
impl Error for CustomCursorError {}

View File

@@ -476,10 +476,10 @@ pub trait WindowAttributesExtWindows {
/// the menus look. If you use this, it is recommended that you combine it with
/// `with_theme(Some(Theme::Light))` to avoid a jarring effect.
#[cfg_attr(
platform_windows,
windows_platform,
doc = "[`CreateMenu`]: windows_sys::Win32::UI::WindowsAndMessaging::CreateMenu"
)]
#[cfg_attr(not(platform_windows), doc = "[`CreateMenu`]: #only-available-on-windows")]
#[cfg_attr(not(windows_platform), doc = "[`CreateMenu`]: #only-available-on-windows")]
fn with_menu(self, menu: HMENU) -> Self;
/// This sets `ICON_BIG`. A good ceiling here is 256x256.
@@ -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

@@ -2,7 +2,7 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::event_loop::{ActiveEventLoop, EventLoopBuilder};
use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder};
use crate::monitor::MonitorHandle;
use crate::window::{Window, WindowAttributes};
@@ -81,9 +81,7 @@ pub type XWindow = u32;
#[inline]
pub fn register_xlib_error_hook(hook: XlibErrorHook) {
// Append new hook.
unsafe {
crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook);
}
crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook);
}
/// Additional methods on [`ActiveEventLoop`] that are specific to X11.
@@ -99,6 +97,19 @@ impl ActiveEventLoopExtX11 for ActiveEventLoop {
}
}
/// Additional methods on [`EventLoop`] that are specific to X11.
pub trait EventLoopExtX11 {
/// True if the [`EventLoop`] uses X11.
fn is_x11(&self) -> bool;
}
impl<T: 'static> EventLoopExtX11 for EventLoop<T> {
#[inline]
fn is_x11(&self) -> bool {
!self.event_loop.is_wayland()
}
}
/// Additional methods on [`EventLoopBuilder`] that are specific to X11.
pub trait EventLoopBuilderExtX11 {
/// Force using X11.

View File

@@ -1,5 +1,3 @@
#![cfg(android_platform)]
use std::cell::Cell;
use std::collections::VecDeque;
use std::hash::Hash;
@@ -134,7 +132,7 @@ impl RedrawRequester {
pub struct KeyEventExtra {}
pub struct EventLoop<T: 'static> {
android_app: AndroidApp,
pub(crate) android_app: AndroidApp,
window_target: event_loop::ActiveEventLoop,
redraw_flag: SharedFlag,
user_events_sender: mpsc::Sender<T>,
@@ -448,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 {
@@ -455,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,
@@ -648,7 +653,7 @@ impl<T> EventLoopProxy<T> {
}
pub struct ActiveEventLoop {
app: AndroidApp,
pub(crate) app: AndroidApp,
control_flow: Cell<ControlFlow>,
exit: Cell<bool>,
redraw_requester: RedrawRequester,
@@ -679,6 +684,11 @@ impl ActiveEventLoop {
rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty())
}
#[inline]
pub fn system_theme(&self) -> Option<Theme> {
None
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(
@@ -905,7 +915,13 @@ impl Window {
pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {}
pub fn set_ime_allowed(&self, _allowed: bool) {}
pub fn set_ime_allowed(&self, allowed: bool) {
if allowed {
self.app.show_soft_input(true);
} else {
self.app.hide_soft_input(true);
}
}
pub fn set_ime_purpose(&self, _purpose: ImePurpose) {}

View File

@@ -1,101 +0,0 @@
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol};
use super::app_state::{self, EventWrapper};
use super::uikit::{UIApplication, UIWindow};
use super::window::WinitUIWindow;
use crate::event::{Event, WindowEvent};
use crate::window::WindowId as RootWindowId;
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) {
self.send_occluded_event_for_all_windows(application, false);
}
#[method(applicationDidEnterBackground:)]
fn did_enter_background(&self, application: &UIApplication) {
self.send_occluded_event_for_all_windows(application, true);
}
#[method(applicationWillTerminate:)]
fn will_terminate(&self, application: &UIApplication) {
let mut events = Vec::new();
for window in application.windows().iter() {
if window.is_kind_of::<WinitUIWindow>() {
// SAFETY: We just checked that the window is a `winit` window
let window = unsafe {
let ptr: *const UIWindow = window;
let ptr: *const WinitUIWindow = ptr.cast();
&*ptr
};
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::Destroyed,
}));
}
}
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(mtm, events);
app_state::terminated(mtm);
}
#[method(applicationDidReceiveMemoryWarning:)]
fn did_receive_memory_warning(&self, _application: &UIApplication) {
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning))
}
}
);
impl AppDelegate {
fn send_occluded_event_for_all_windows(&self, application: &UIApplication, occluded: bool) {
let mut events = Vec::new();
for window in application.windows().iter() {
if window.is_kind_of::<WinitUIWindow>() {
// SAFETY: We just checked that the window is a `winit` window
let window = unsafe {
let ptr: *const UIWindow = window;
let ptr: *const WinitUIWindow = ptr.cast();
&*ptr
};
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::Occluded(occluded),
}));
}
}
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(mtm, events);
}
}

View File

@@ -13,14 +13,15 @@ use core_foundation::runloop::{
kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate,
CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate,
};
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{msg_send, sel};
use objc2_foundation::{
CGRect, CGSize, MainThreadMarker, NSInteger, NSOperatingSystemVersion, NSProcessInfo,
CGRect, CGSize, MainThreadMarker, NSInteger, NSObjectProtocol, NSOperatingSystemVersion,
NSProcessInfo,
};
use objc2_ui_kit::{UIApplication, UICoordinateSpace, UIView, UIWindow};
use super::uikit::UIView;
use super::window::WinitUIWindow;
use crate::dpi::PhysicalSize;
use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent};
@@ -71,7 +72,7 @@ pub(crate) enum EventWrapper {
#[derive(Debug)]
pub struct ScaleFactorChanged {
pub(super) window: Id<WinitUIWindow>,
pub(super) window: Retained<WinitUIWindow>,
pub(super) suggested_size: PhysicalSize<u32>,
pub(super) scale_factor: f64,
}
@@ -98,25 +99,25 @@ impl Event<HandlePendingUserEvents> {
#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"]
enum AppStateImpl {
NotLaunched {
queued_windows: Vec<Id<WinitUIWindow>>,
queued_windows: Vec<Retained<WinitUIWindow>>,
queued_events: Vec<EventWrapper>,
queued_gpu_redraws: HashSet<Id<WinitUIWindow>>,
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
},
Launching {
queued_windows: Vec<Id<WinitUIWindow>>,
queued_windows: Vec<Retained<WinitUIWindow>>,
queued_events: Vec<EventWrapper>,
queued_handler: EventLoopHandler,
queued_gpu_redraws: HashSet<Id<WinitUIWindow>>,
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
},
ProcessingEvents {
handler: EventLoopHandler,
queued_gpu_redraws: HashSet<Id<WinitUIWindow>>,
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
active_control_flow: ControlFlow,
},
// special state to deal with reentrancy and prevent mutable aliasing.
InUserCallback {
queued_events: Vec<EventWrapper>,
queued_gpu_redraws: HashSet<Id<WinitUIWindow>>,
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
},
ProcessingRedraws {
handler: EventLoopHandler,
@@ -146,6 +147,8 @@ impl AppState {
// must be mut because plain `static` requires `Sync`
static mut APP_STATE: RefCell<Option<AppState>> = RefCell::new(None);
#[allow(unknown_lints)] // New lint below
#[allow(static_mut_refs)] // TODO: Use `MainThreadBound` instead.
let mut guard = unsafe { APP_STATE.borrow_mut() };
if guard.is_none() {
#[inline(never)]
@@ -227,7 +230,9 @@ impl AppState {
});
}
fn did_finish_launching_transition(&mut self) -> (Vec<Id<WinitUIWindow>>, Vec<EventWrapper>) {
fn did_finish_launching_transition(
&mut self,
) -> (Vec<Retained<WinitUIWindow>>, Vec<EventWrapper>) {
let (windows, events, handler, queued_gpu_redraws) = match self.take_state() {
AppStateImpl::Launching {
queued_windows,
@@ -343,7 +348,7 @@ impl AppState {
UserCallbackTransitionResult::Success { handler, active_control_flow, processing_redraws }
}
fn main_events_cleared_transition(&mut self) -> HashSet<Id<WinitUIWindow>> {
fn main_events_cleared_transition(&mut self) -> HashSet<Retained<WinitUIWindow>> {
let (handler, queued_gpu_redraws, active_control_flow) = match self.take_state() {
AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => {
(handler, queued_gpu_redraws, active_control_flow)
@@ -370,6 +375,7 @@ impl AppState {
(ControlFlow::Wait, ControlFlow::Wait) => {
let start = Instant::now();
self.set_state(AppStateImpl::Waiting { waiting_handler, start });
self.waker.stop()
},
(ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant))
if old_instant == new_instant =>
@@ -411,7 +417,7 @@ impl AppState {
}
}
pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Id<WinitUIWindow>) {
pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Retained<WinitUIWindow>) {
let mut this = AppState::get_mut(mtm);
match this.state_mut() {
&mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => {
@@ -431,7 +437,7 @@ pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Id<WinitUIWindow>)
window.makeKeyAndVisible();
}
pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Id<WinitUIWindow>) {
pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Retained<WinitUIWindow>) {
let mut this = AppState::get_mut(mtm);
match this.state_mut() {
&mut AppStateImpl::NotLaunched { ref mut queued_gpu_redraws, .. }
@@ -659,6 +665,28 @@ fn handle_user_events(mtm: MainThreadMarker) {
}
}
pub(crate) fn send_occluded_event_for_all_windows(application: &UIApplication, occluded: bool) {
let mtm = MainThreadMarker::from(application);
let mut events = Vec::new();
#[allow(deprecated)]
for window in application.windows().iter() {
if window.is_kind_of::<WinitUIWindow>() {
// SAFETY: We just checked that the window is a `winit` window
let window = unsafe {
let ptr: *const UIWindow = window;
let ptr: *const WinitUIWindow = ptr.cast();
&*ptr
};
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::Occluded(occluded),
}));
}
}
handle_nonuser_events(mtm, events);
}
pub fn handle_main_events_cleared(mtm: MainThreadMarker) {
let mut this = AppState::get_mut(mtm);
if !this.has_launched() || this.has_terminated() {
@@ -693,7 +721,27 @@ pub fn handle_events_cleared(mtm: MainThreadMarker) {
AppState::get_mut(mtm).events_cleared_transition();
}
pub fn terminated(mtm: MainThreadMarker) {
pub(crate) fn terminated(application: &UIApplication) {
let mtm = MainThreadMarker::from(application);
let mut events = Vec::new();
#[allow(deprecated)]
for window in application.windows().iter() {
if window.is_kind_of::<WinitUIWindow>() {
// SAFETY: We just checked that the window is a `winit` window
let window = unsafe {
let ptr: *const UIWindow = window;
let ptr: *const WinitUIWindow = ptr.cast();
&*ptr
};
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::Destroyed,
}));
}
}
handle_nonuser_events(mtm, events);
let mut this = AppState::get_mut(mtm);
let mut handler = this.terminated_transition();
drop(this);
@@ -721,7 +769,7 @@ fn handle_hidpi_proxy(handler: &mut EventLoopHandler, event: ScaleFactorChanged)
view.setFrame(new_frame);
}
fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Id<UIView>, CGRect) {
fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Retained<UIView>, CGRect) {
let view_controller = window.rootViewController().unwrap();
let view = view_controller.view().unwrap();
let bounds = window.bounds();
@@ -753,7 +801,7 @@ impl EventLoopWaker {
// future, but that gets changed to fire immediately in did_finish_launching
let timer = CFRunLoopTimerCreate(
ptr::null_mut(),
std::f64::MAX,
f64::MAX,
0.000_000_1,
0,
0,
@@ -767,11 +815,11 @@ impl EventLoopWaker {
}
fn stop(&mut self) {
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) }
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) }
}
fn start(&mut self) {
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) }
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) }
}
fn start_at(&mut self, instant: Instant) {
@@ -857,23 +905,17 @@ fn meets_requirements(
}
fn get_version() -> NSOperatingSystemVersion {
unsafe {
let process_info = NSProcessInfo::processInfo();
let atleast_ios_8: bool = msg_send![
&process_info,
respondsToSelector: sel!(operatingSystemVersion)
];
// winit requires atleast iOS 8 because no one has put the time into supporting earlier os
// versions. Older iOS versions are increasingly difficult to test. For example,
// Xcode 11 does not support debugging on devices with an iOS version of less than
// 8. Another example, in order to use an iOS simulator older than iOS 8, you must
// download an older version of Xcode (<9), and at least Xcode 7 has been tested to
// not even run on macOS 10.15 - Xcode 8 might?
//
// The minimum required iOS version is likely to grow in the future.
assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater");
process_info.operatingSystemVersion()
}
let process_info = NSProcessInfo::processInfo();
let atleast_ios_8 = process_info.respondsToSelector(sel!(operatingSystemVersion));
// Winit requires atleast iOS 8 because no one has put the time into supporting earlier os
// versions. Older iOS versions are increasingly difficult to test. For example, Xcode 11 does
// not support debugging on devices with an iOS version of less than 8. Another example, in
// order to use an iOS simulator older than iOS 8, you must download an older version of Xcode
// (<9), and at least Xcode 7 has been tested to not even run on macOS 10.15 - Xcode 8 might?
//
// The minimum required iOS version is likely to grow in the future.
assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater");
process_info.operatingSystemVersion()
}
pub fn os_capabilities() -> OSCapabilities {

View File

@@ -1,7 +1,7 @@
use std::collections::VecDeque;
use std::ffi::c_void;
use std::ffi::{c_char, c_int, c_void};
use std::marker::PhantomData;
use std::ptr;
use std::ptr::{self, NonNull};
use std::sync::mpsc::{self, Receiver, Sender};
use core_foundation::base::{CFIndex, CFRelease};
@@ -11,8 +11,16 @@ use core_foundation::runloop::{
CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate,
CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
};
use objc2::ClassType;
use objc2_foundation::{MainThreadMarker, NSString};
use objc2::rc::Retained;
use objc2::{msg_send_id, ClassType};
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;
@@ -20,12 +28,11 @@ use crate::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopClosed,
};
use crate::platform::ios::Idiom;
use crate::platform_impl::platform::app_state::{EventLoopHandler, HandlePendingUserEvents};
use crate::window::{CustomCursor, CustomCursorSource};
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::uikit::{UIApplication, UIApplicationMain, UIDevice, UIScreen, UIUserInterfaceIdiom};
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)]
@@ -44,7 +51,8 @@ impl ActiveEventLoop {
}
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
Some(MonitorHandle::new(UIScreen::main(self.mtm)))
#[allow(deprecated)]
Some(MonitorHandle::new(UIScreen::mainScreen(self.mtm)))
}
#[inline]
@@ -56,6 +64,11 @@ impl ActiveEventLoop {
rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty())
}
#[inline]
pub fn system_theme(&self) -> Option<Theme> {
None
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(
@@ -125,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)]
@@ -151,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,
})
}
@@ -163,7 +274,8 @@ impl<T: 'static> EventLoop<T> {
where
F: FnMut(Event<T>, &RootActiveEventLoop),
{
let application = UIApplication::shared(self.mtm);
let application: Option<Retained<UIApplication>> =
unsafe { msg_send_id![UIApplication::class(), sharedApplication] };
assert!(
application.is_none(),
"\
@@ -184,11 +296,21 @@ 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;
fn _NSGetArgv() -> *mut *mut *mut c_char;
}
unsafe {
UIApplicationMain(0, ptr::null(), None, Some(&NSString::from_str(AppDelegate::NAME)))
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,
)
};
unreachable!()
}
@@ -205,7 +327,7 @@ impl<T: 'static> EventLoop<T> {
// EventLoopExtIOS
impl<T: 'static> EventLoop<T> {
pub fn idiom(&self) -> Idiom {
match UIDevice::current(self.mtm).userInterfaceIdiom() {
match UIDevice::currentDevice(self.mtm).userInterfaceIdiom() {
UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified,
UIUserInterfaceIdiom::Phone => Idiom::Phone,
UIUserInterfaceIdiom::Pad => Idiom::Pad,
@@ -260,8 +382,7 @@ impl<T> EventLoopProxy<T> {
cancel: None,
perform: event_loop_proxy_handler,
};
let source =
CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::max_value() - 1, &mut context);
let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context);
CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes);
CFRunLoopWakeUp(rl);
@@ -344,7 +465,7 @@ fn setup_control_flow_observers() {
ptr::null_mut(),
kCFRunLoopAfterWaiting,
1, // repeat = true
CFIndex::min_value(),
CFIndex::MIN,
control_flow_begin_handler,
ptr::null_mut(),
);
@@ -364,7 +485,7 @@ fn setup_control_flow_observers() {
ptr::null_mut(),
kCFRunLoopExit | kCFRunLoopBeforeWaiting,
1, // repeat = true
CFIndex::max_value(),
CFIndex::MAX,
control_flow_end_handler,
ptr::null_mut(),
);

View File

@@ -1,11 +1,9 @@
#![cfg(ios_platform)]
#![allow(clippy::let_unit_value)]
mod app_delegate;
mod app_state;
mod event_loop;
mod monitor;
mod uikit;
mod notification_center;
mod view;
mod view_controller;
mod window;
@@ -34,7 +32,7 @@ pub(crate) use crate::platform_impl::Fullscreen;
pub struct DeviceId;
impl DeviceId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
DeviceId
}
}

View File

@@ -4,22 +4,22 @@ use std::collections::{BTreeSet, VecDeque};
use std::{fmt, hash, ptr};
use objc2::mutability::IsRetainable;
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::Message;
use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger};
use objc2_ui_kit::{UIScreen, UIScreenMode};
use super::uikit::{UIScreen, UIScreenMode};
use crate::dpi::{PhysicalPosition, PhysicalSize};
use crate::monitor::VideoModeHandle as RootVideoModeHandle;
use crate::platform_impl::platform::app_state;
// Workaround for `MainThreadBound` implementing almost no traits
#[derive(Debug)]
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Id<T>>);
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Retained<T>>);
impl<T: IsRetainable + Message> Clone for MainThreadBoundDelegateImpls<T> {
fn clone(&self) -> Self {
Self(run_on_main(|mtm| MainThreadBound::new(Id::clone(self.0.get(mtm)), mtm)))
Self(run_on_main(|mtm| MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm)))
}
}
@@ -27,7 +27,7 @@ impl<T: IsRetainable + Message> hash::Hash for MainThreadBoundDelegateImpls<T> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Id::as_ptr(self.0.get(mtm)).hash(state);
Retained::as_ptr(self.0.get(mtm)).hash(state);
}
}
@@ -35,7 +35,7 @@ impl<T: IsRetainable + Message> PartialEq for MainThreadBoundDelegateImpls<T> {
fn eq(&self, other: &Self) -> bool {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Id::as_ptr(self.0.get(mtm)) == Id::as_ptr(other.0.get(mtm))
Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm))
}
}
@@ -52,8 +52,8 @@ pub struct VideoModeHandle {
impl VideoModeHandle {
fn new(
uiscreen: Id<UIScreen>,
screen_mode: Id<UIScreenMode>,
uiscreen: Retained<UIScreen>,
screen_mode: Retained<UIScreenMode>,
mtm: MainThreadMarker,
) -> VideoModeHandle {
let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen);
@@ -83,13 +83,13 @@ impl VideoModeHandle {
self.monitor.clone()
}
pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Id<UIScreenMode> {
pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained<UIScreenMode> {
self.screen_mode.0.get(mtm)
}
}
pub struct MonitorHandle {
ui_screen: MainThreadBound<Id<UIScreen>>,
ui_screen: MainThreadBound<Retained<UIScreen>>,
}
impl Clone for MonitorHandle {
@@ -102,13 +102,20 @@ impl Clone for MonitorHandle {
impl hash::Hash for MonitorHandle {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
(self as *const Self).hash(state);
// SAFETY: Only getting the pointer.
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Retained::as_ptr(self.ui_screen.get(mtm)).hash(state);
}
}
impl PartialEq for MonitorHandle {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self, other)
// SAFETY: Only getting the pointer.
let mtm = unsafe { MainThreadMarker::new_unchecked() };
ptr::eq(
Retained::as_ptr(self.ui_screen.get(mtm)),
Retained::as_ptr(other.ui_screen.get(mtm)),
)
}
}
@@ -122,8 +129,10 @@ impl PartialOrd for MonitorHandle {
impl Ord for MonitorHandle {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// SAFETY: Only getting the pointer.
// TODO: Make a better ordering
(self as *const Self).cmp(&(other as *const Self))
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Retained::as_ptr(self.ui_screen.get(mtm)).cmp(&Retained::as_ptr(other.ui_screen.get(mtm)))
}
}
@@ -140,20 +149,22 @@ impl fmt::Debug for MonitorHandle {
}
impl MonitorHandle {
pub(crate) fn new(ui_screen: Id<UIScreen>) -> Self {
// Holding `Id<UIScreen>` implies we're on the main thread.
pub(crate) fn new(ui_screen: Retained<UIScreen>) -> Self {
// Holding `Retained<UIScreen>` implies we're on the main thread.
let mtm = MainThreadMarker::new().unwrap();
Self { ui_screen: MainThreadBound::new(ui_screen, mtm) }
}
pub fn name(&self) -> Option<String> {
run_on_main(|mtm| {
let main = UIScreen::main(mtm);
#[allow(deprecated)]
let main = UIScreen::mainScreen(mtm);
if *self.ui_screen(mtm) == main {
Some("Primary".to_string())
} else if *self.ui_screen(mtm) == main.mirroredScreen() {
} else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() {
Some("Mirrored".to_string())
} else {
#[allow(deprecated)]
UIScreen::screens(mtm)
.iter()
.position(|rhs| rhs == &**self.ui_screen(mtm))
@@ -197,7 +208,7 @@ impl MonitorHandle {
})
}
pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Id<UIScreen> {
pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained<UIScreen> {
self.ui_screen.get(mtm)
}
@@ -237,5 +248,30 @@ fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 {
}
pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque<MonitorHandle> {
#[allow(deprecated)]
UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect()
}
#[cfg(test)]
mod tests {
use objc2_foundation::NSSet;
use super::*;
// Test that UIScreen pointer comparisons are correct.
#[test]
#[allow(deprecated)]
fn screen_comparisons() {
// Test code, doesn't matter that it's not thread safe
let mtm = unsafe { MainThreadMarker::new_unchecked() };
assert!(ptr::eq(&*UIScreen::mainScreen(mtm), &*UIScreen::mainScreen(mtm)));
let main = UIScreen::mainScreen(mtm);
assert!(UIScreen::screens(mtm).iter().any(|screen| ptr::eq(screen, &*main)));
assert!(unsafe {
NSSet::setWithArray(&UIScreen::screens(mtm)).containsObject(&UIScreen::mainScreen(mtm))
});
}
}

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

@@ -1,31 +0,0 @@
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::{CGRect, MainThreadMarker, NSArray, NSObject};
use super::{UIResponder, UIWindow};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIApplication;
unsafe impl ClassType for UIApplication {
#[inherits(NSObject)]
type Super = UIResponder;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIApplication {
pub fn shared(_mtm: MainThreadMarker) -> Option<Id<Self>> {
unsafe { msg_send_id![Self::class(), sharedApplication] }
}
pub fn windows(&self) -> Id<NSArray<UIWindow>> {
unsafe { msg_send_id![self, windows] }
}
#[method(statusBarFrame)]
pub fn statusBarFrame(&self) -> CGRect;
}
);

View File

@@ -1,12 +0,0 @@
use objc2::{extern_class, mutability, ClassType};
use objc2_foundation::NSObject;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UICoordinateSpace;
unsafe impl ClassType for UICoordinateSpace {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);

View File

@@ -1,41 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::{MainThreadMarker, NSInteger, NSObject};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIDevice;
unsafe impl ClassType for UIDevice {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIDevice {
pub fn current(_mtm: MainThreadMarker) -> Id<Self> {
unsafe { msg_send_id![Self::class(), currentDevice] }
}
#[method(userInterfaceIdiom)]
pub fn userInterfaceIdiom(&self) -> UIUserInterfaceIdiom;
}
);
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UIUserInterfaceIdiom(NSInteger);
unsafe impl Encode for UIUserInterfaceIdiom {
const ENCODING: Encoding = NSInteger::ENCODING;
}
impl UIUserInterfaceIdiom {
pub const Unspecified: UIUserInterfaceIdiom = UIUserInterfaceIdiom(-1);
pub const Phone: UIUserInterfaceIdiom = UIUserInterfaceIdiom(0);
pub const Pad: UIUserInterfaceIdiom = UIUserInterfaceIdiom(1);
pub const TV: UIUserInterfaceIdiom = UIUserInterfaceIdiom(2);
pub const CarPlay: UIUserInterfaceIdiom = UIUserInterfaceIdiom(3);
}

View File

@@ -1,12 +0,0 @@
use objc2::{extern_class, mutability, ClassType};
use objc2_foundation::NSObject;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIEvent;
unsafe impl ClassType for UIEvent {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);

View File

@@ -1,14 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2_foundation::NSUInteger;
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UIRectEdge(pub NSUInteger);
impl UIRectEdge {
pub const NONE: Self = Self(0);
}
unsafe impl Encode for UIRectEdge {
const ENCODING: Encoding = NSUInteger::ENCODING;
}

View File

@@ -1,176 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use objc2::{extern_class, extern_methods, extern_protocol, mutability, ClassType, ProtocolType};
use objc2_foundation::{CGFloat, CGPoint, NSInteger, NSObject, NSObjectProtocol, NSUInteger};
use super::UIView;
extern_class!(
/// [`UIGestureRecognizer`](https://developer.apple.com/documentation/uikit/uigesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIGestureRecognizer;
unsafe impl ClassType for UIGestureRecognizer {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIGestureRecognizer {
#[method(state)]
pub fn state(&self) -> UIGestureRecognizerState;
/// [`delegate`](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624207-delegate?language=objc)
/// @property(nullable, nonatomic, weak) id<UIGestureRecognizerDelegate> delegate;
#[method(setDelegate:)]
pub fn setDelegate(&self, delegate: &ProtocolObject<dyn UIGestureRecognizerDelegate>);
#[method_id(delegate)]
pub fn delegate(&self) -> Id<ProtocolObject<dyn UIGestureRecognizerDelegate>>;
}
);
unsafe impl Encode for UIGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
// [`UIGestureRecognizerState`](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state)
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UIGestureRecognizerState(NSInteger);
unsafe impl Encode for UIGestureRecognizerState {
const ENCODING: Encoding = NSInteger::ENCODING;
}
#[allow(dead_code)]
impl UIGestureRecognizerState {
pub const Possible: Self = Self(0);
pub const Began: Self = Self(1);
pub const Changed: Self = Self(2);
pub const Ended: Self = Self(3);
pub const Cancelled: Self = Self(4);
pub const Failed: Self = Self(5);
}
// [`UIPinchGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipinchgesturerecognizer)
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIPinchGestureRecognizer;
unsafe impl ClassType for UIPinchGestureRecognizer {
type Super = UIGestureRecognizer;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIPinchGestureRecognizer {
#[method(scale)]
pub fn scale(&self) -> CGFloat;
#[method(velocity)]
pub fn velocity(&self) -> CGFloat;
}
);
unsafe impl Encode for UIPinchGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
extern_class!(
/// [`UIRotationGestureRecognizer`](https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIRotationGestureRecognizer;
unsafe impl ClassType for UIRotationGestureRecognizer {
type Super = UIGestureRecognizer;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIRotationGestureRecognizer {
#[method(rotation)]
pub fn rotation(&self) -> CGFloat;
#[method(velocity)]
pub fn velocity(&self) -> CGFloat;
}
);
unsafe impl Encode for UIRotationGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
extern_class!(
/// [`UITapGestureRecognizer`](https://developer.apple.com/documentation/uikit/uitapgesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UITapGestureRecognizer;
unsafe impl ClassType for UITapGestureRecognizer {
type Super = UIGestureRecognizer;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UITapGestureRecognizer {
#[method(setNumberOfTapsRequired:)]
pub fn setNumberOfTapsRequired(&self, number_of_taps_required: NSUInteger);
#[method(setNumberOfTouchesRequired:)]
pub fn setNumberOfTouchesRequired(&self, number_of_touches_required: NSUInteger);
}
);
unsafe impl Encode for UITapGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
extern_class!(
/// [`UIPanGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIPanGestureRecognizer;
unsafe impl ClassType for UIPanGestureRecognizer {
type Super = UIGestureRecognizer;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIPanGestureRecognizer {
#[method(translationInView:)]
pub fn translationInView(&self, view: &UIView) -> CGPoint;
#[method(setTranslation:inView:)]
pub fn setTranslationInView(&self, translation: CGPoint, view: &UIView);
#[method(velocityInView:)]
pub fn velocityInView(&self, view: &UIView) -> CGPoint;
#[method(setMinimumNumberOfTouches:)]
pub fn setMinimumNumberOfTouches(&self, minimum_number_of_touches: NSUInteger);
#[method(minimumNumberOfTouches)]
pub fn minimumNumberOfTouches(&self) -> NSUInteger;
#[method(setMaximumNumberOfTouches:)]
pub fn setMaximumNumberOfTouches(&self, maximum_number_of_touches: NSUInteger);
#[method(maximumNumberOfTouches)]
pub fn maximumNumberOfTouches(&self) -> NSUInteger;
}
);
extern_protocol!(
/// (@protocol UIGestureRecognizerDelegate)[https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate?language=objc]
pub(crate) unsafe trait UIGestureRecognizerDelegate: NSObjectProtocol {}
unsafe impl ProtocolType for dyn UIGestureRecognizerDelegate {
const NAME: &'static str = "UIGestureRecognizerDelegate";
}
);

View File

@@ -1,53 +0,0 @@
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
use std::os::raw::{c_char, c_int};
use objc2_foundation::NSString;
mod application;
mod coordinate_space;
mod device;
mod event;
mod geometry;
mod gesture_recognizer;
mod responder;
mod screen;
mod screen_mode;
mod status_bar_style;
mod touch;
mod trait_collection;
mod view;
mod view_controller;
mod window;
pub(crate) use self::application::UIApplication;
pub(crate) use self::coordinate_space::UICoordinateSpace;
pub(crate) use self::device::{UIDevice, UIUserInterfaceIdiom};
pub(crate) use self::event::UIEvent;
pub(crate) use self::geometry::UIRectEdge;
pub(crate) use self::gesture_recognizer::{
UIGestureRecognizer, UIGestureRecognizerDelegate, UIGestureRecognizerState,
UIPanGestureRecognizer, UIPinchGestureRecognizer, UIRotationGestureRecognizer,
UITapGestureRecognizer,
};
pub(crate) use self::responder::UIResponder;
pub(crate) use self::screen::{UIScreen, UIScreenOverscanCompensation};
pub(crate) use self::screen_mode::UIScreenMode;
pub(crate) use self::status_bar_style::UIStatusBarStyle;
pub(crate) use self::touch::{UITouch, UITouchPhase, UITouchType};
pub(crate) use self::trait_collection::{UIForceTouchCapability, UITraitCollection};
#[allow(unused_imports)]
pub(crate) use self::view::{UIEdgeInsets, UIView};
pub(crate) use self::view_controller::{UIInterfaceOrientationMask, UIViewController};
pub(crate) use self::window::UIWindow;
#[link(name = "UIKit", kind = "framework")]
extern "C" {
pub fn UIApplicationMain(
argc: c_int,
argv: *const c_char,
principalClassName: Option<&NSString>,
delegateClassName: Option<&NSString>,
) -> c_int;
}

View File

@@ -1,12 +0,0 @@
use objc2::{extern_class, mutability, ClassType};
use objc2_foundation::NSObject;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIResponder;
unsafe impl ClassType for UIResponder {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);

View File

@@ -1,80 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::{CGFloat, CGRect, MainThreadMarker, NSArray, NSInteger, NSObject};
use super::{UICoordinateSpace, UIScreenMode};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIScreen;
unsafe impl ClassType for UIScreen {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIScreen {
pub fn main(_mtm: MainThreadMarker) -> Id<Self> {
unsafe { msg_send_id![Self::class(), mainScreen] }
}
pub fn screens(_mtm: MainThreadMarker) -> Id<NSArray<Self>> {
unsafe { msg_send_id![Self::class(), screens] }
}
#[method(bounds)]
pub fn bounds(&self) -> CGRect;
#[method(scale)]
pub fn scale(&self) -> CGFloat;
#[method(nativeBounds)]
pub fn nativeBounds(&self) -> CGRect;
#[method(nativeScale)]
pub fn nativeScale(&self) -> CGFloat;
#[method(maximumFramesPerSecond)]
pub fn maximumFramesPerSecond(&self) -> NSInteger;
pub fn mirroredScreen(&self) -> Id<Self> {
unsafe { msg_send_id![Self::class(), mirroredScreen] }
}
pub fn preferredMode(&self) -> Option<Id<UIScreenMode>> {
unsafe { msg_send_id![self, preferredMode] }
}
#[method(setCurrentMode:)]
pub fn setCurrentMode(&self, mode: Option<&UIScreenMode>);
pub fn availableModes(&self) -> Id<NSArray<UIScreenMode>> {
unsafe { msg_send_id![self, availableModes] }
}
#[method(setOverscanCompensation:)]
pub fn setOverscanCompensation(&self, overscanCompensation: UIScreenOverscanCompensation);
pub fn coordinateSpace(&self) -> Id<UICoordinateSpace> {
unsafe { msg_send_id![self, coordinateSpace] }
}
}
);
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UIScreenOverscanCompensation(NSInteger);
unsafe impl Encode for UIScreenOverscanCompensation {
const ENCODING: Encoding = NSInteger::ENCODING;
}
#[allow(dead_code)]
impl UIScreenOverscanCompensation {
pub const Scale: Self = Self(0);
pub const InsetBounds: Self = Self(1);
pub const None: Self = Self(2);
}

View File

@@ -1,19 +0,0 @@
use objc2::{extern_class, extern_methods, mutability, ClassType};
use objc2_foundation::{CGSize, NSObject};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIScreenMode;
unsafe impl ClassType for UIScreenMode {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIScreenMode {
#[method(size)]
pub fn size(&self) -> CGSize;
}
);

View File

@@ -1,16 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2_foundation::NSInteger;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[allow(dead_code)]
#[repr(isize)]
pub enum UIStatusBarStyle {
#[default]
Default = 0,
LightContent = 1,
DarkContent = 3,
}
unsafe impl Encode for UIStatusBarStyle {
const ENCODING: Encoding = NSInteger::ENCODING;
}

View File

@@ -1,65 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::{extern_class, extern_methods, mutability, ClassType};
use objc2_foundation::{CGFloat, CGPoint, NSInteger, NSObject};
use super::UIView;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UITouch;
unsafe impl ClassType for UITouch {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UITouch {
#[method(locationInView:)]
pub fn locationInView(&self, view: Option<&UIView>) -> CGPoint;
#[method(type)]
pub fn type_(&self) -> UITouchType;
#[method(force)]
pub fn force(&self) -> CGFloat;
#[method(maximumPossibleForce)]
pub fn maximumPossibleForce(&self) -> CGFloat;
#[method(altitudeAngle)]
pub fn altitudeAngle(&self) -> CGFloat;
#[method(phase)]
pub fn phase(&self) -> UITouchPhase;
}
);
#[derive(Debug, PartialEq, Eq)]
#[allow(dead_code)]
#[repr(isize)]
pub enum UITouchType {
Direct = 0,
Indirect,
Pencil,
}
unsafe impl Encode for UITouchType {
const ENCODING: Encoding = NSInteger::ENCODING;
}
#[derive(Debug)]
#[allow(dead_code)]
#[repr(isize)]
pub enum UITouchPhase {
Began = 0,
Moved,
Stationary,
Ended,
Cancelled,
}
unsafe impl Encode for UITouchPhase {
const ENCODING: Encoding = NSInteger::ENCODING;
}

View File

@@ -1,33 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::{extern_class, extern_methods, mutability, ClassType};
use objc2_foundation::{NSInteger, NSObject};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UITraitCollection;
unsafe impl ClassType for UITraitCollection {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UITraitCollection {
#[method(forceTouchCapability)]
pub fn forceTouchCapability(&self) -> UIForceTouchCapability;
}
);
#[derive(Debug, PartialEq, Eq)]
#[allow(dead_code)]
#[repr(isize)]
pub enum UIForceTouchCapability {
Unknown = 0,
Unavailable,
Available,
}
unsafe impl Encode for UIForceTouchCapability {
const ENCODING: Encoding = NSInteger::ENCODING;
}

View File

@@ -1,93 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::{CGFloat, CGRect, NSObject};
use super::{UICoordinateSpace, UIGestureRecognizer, UIResponder, UIViewController};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIView;
unsafe impl ClassType for UIView {
#[inherits(NSObject)]
type Super = UIResponder;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIView {
#[method(bounds)]
pub fn bounds(&self) -> CGRect;
#[method(setBounds:)]
pub fn setBounds(&self, value: CGRect);
#[method(frame)]
pub fn frame(&self) -> CGRect;
#[method(setFrame:)]
pub fn setFrame(&self, value: CGRect);
#[method(contentScaleFactor)]
pub fn contentScaleFactor(&self) -> CGFloat;
#[method(setContentScaleFactor:)]
pub fn setContentScaleFactor(&self, val: CGFloat);
#[method(setMultipleTouchEnabled:)]
pub fn setMultipleTouchEnabled(&self, val: bool);
pub fn rootViewController(&self) -> Option<Id<UIViewController>> {
unsafe { msg_send_id![self, rootViewController] }
}
#[method(setRootViewController:)]
pub fn setRootViewController(&self, rootViewController: Option<&UIViewController>);
#[method(convertRect:toCoordinateSpace:)]
pub fn convertRect_toCoordinateSpace(
&self,
rect: CGRect,
coordinateSpace: &UICoordinateSpace,
) -> CGRect;
#[method(convertRect:fromCoordinateSpace:)]
pub fn convertRect_fromCoordinateSpace(
&self,
rect: CGRect,
coordinateSpace: &UICoordinateSpace,
) -> CGRect;
#[method(safeAreaInsets)]
pub fn safeAreaInsets(&self) -> UIEdgeInsets;
#[method(setNeedsDisplay)]
pub fn setNeedsDisplay(&self);
#[method(addGestureRecognizer:)]
pub fn addGestureRecognizer(&self, gestureRecognizer: &UIGestureRecognizer);
#[method(removeGestureRecognizer:)]
pub fn removeGestureRecognizer(&self, gestureRecognizer: &UIGestureRecognizer);
}
);
#[repr(C)]
#[derive(Debug, Clone)]
pub struct UIEdgeInsets {
pub top: CGFloat,
pub left: CGFloat,
pub bottom: CGFloat,
pub right: CGFloat,
}
unsafe impl Encode for UIEdgeInsets {
const ENCODING: Encoding = Encoding::Struct("UIEdgeInsets", &[
CGFloat::ENCODING,
CGFloat::ENCODING,
CGFloat::ENCODING,
CGFloat::ENCODING,
]);
}

View File

@@ -1,57 +0,0 @@
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::{NSObject, NSUInteger};
use super::{UIResponder, UIView};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIViewController;
unsafe impl ClassType for UIViewController {
#[inherits(NSObject)]
type Super = UIResponder;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIViewController {
#[method(attemptRotationToDeviceOrientation)]
pub fn attemptRotationToDeviceOrientation();
#[method(setNeedsStatusBarAppearanceUpdate)]
pub fn setNeedsStatusBarAppearanceUpdate(&self);
#[method(setNeedsUpdateOfHomeIndicatorAutoHidden)]
pub fn setNeedsUpdateOfHomeIndicatorAutoHidden(&self);
#[method(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]
pub fn setNeedsUpdateOfScreenEdgesDeferringSystemGestures(&self);
pub fn view(&self) -> Option<Id<UIView>> {
unsafe { msg_send_id![self, view] }
}
#[method(setView:)]
pub fn setView(&self, view: Option<&UIView>);
}
);
bitflags::bitflags! {
#[derive(Clone, Copy)]
pub struct UIInterfaceOrientationMask: NSUInteger {
const Portrait = 1 << 1;
const PortraitUpsideDown = 1 << 2;
const LandscapeRight = 1 << 3;
const LandscapeLeft = 1 << 4;
const Landscape = Self::LandscapeLeft.bits() | Self::LandscapeRight.bits();
const AllButUpsideDown = Self::Landscape.bits() | Self::Portrait.bits();
const All = Self::AllButUpsideDown.bits() | Self::PortraitUpsideDown.bits();
}
}
unsafe impl Encode for UIInterfaceOrientationMask {
const ENCODING: Encoding = NSUInteger::ENCODING;
}

View File

@@ -1,36 +0,0 @@
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2_foundation::NSObject;
use super::{UIResponder, UIScreen, UIView};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIWindow;
unsafe impl ClassType for UIWindow {
#[inherits(UIResponder, NSObject)]
type Super = UIView;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIWindow {
pub fn screen(&self) -> Id<UIScreen> {
unsafe { msg_send_id![self, screen] }
}
#[method(setScreen:)]
pub fn setScreen(&self, screen: &UIScreen);
#[method(setHidden:)]
pub fn setHidden(&self, flag: bool);
#[method(makeKeyAndVisible)]
pub fn makeKeyAndVisible(&self);
#[method(isKeyWindow)]
pub fn isKeyWindow(&self) -> bool;
}
);

View File

@@ -1,31 +1,31 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::{Cell, RefCell};
use objc2::rc::Id;
use objc2::runtime::{AnyClass, NSObjectProtocol, ProtocolObject};
use objc2::{
declare_class, extern_methods, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
use objc2::rc::Retained;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass};
use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString};
use objc2_ui_kit::{
UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer,
UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer,
UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer,
UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView,
};
use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet};
use super::app_state::{self, EventWrapper};
use super::uikit::{
UIEvent, UIForceTouchCapability, UIGestureRecognizer, UIGestureRecognizerDelegate,
UIGestureRecognizerState, UIPanGestureRecognizer, UIPinchGestureRecognizer, UIResponder,
UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase, UITouchType,
UITraitCollection, UIView,
};
use super::window::WinitUIWindow;
use crate::dpi::PhysicalPosition;
use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent};
use crate::event::{ElementState, Event, Force, KeyEvent, Touch, TouchPhase, WindowEvent};
use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey};
use crate::platform_impl::platform::DEVICE_ID;
use crate::platform_impl::KeyEventExtra;
use crate::window::{WindowAttributes, WindowId as RootWindowId};
pub struct WinitViewState {
pinch_gesture_recognizer: RefCell<Option<Id<UIPinchGestureRecognizer>>>,
doubletap_gesture_recognizer: RefCell<Option<Id<UITapGestureRecognizer>>>,
rotation_gesture_recognizer: RefCell<Option<Id<UIRotationGestureRecognizer>>>,
pan_gesture_recognizer: RefCell<Option<Id<UIPanGestureRecognizer>>>,
pinch_gesture_recognizer: RefCell<Option<Retained<UIPinchGestureRecognizer>>>,
doubletap_gesture_recognizer: RefCell<Option<Retained<UITapGestureRecognizer>>>,
rotation_gesture_recognizer: RefCell<Option<Retained<UIRotationGestureRecognizer>>>,
pan_gesture_recognizer: RefCell<Option<Retained<UIPanGestureRecognizer>>>,
// for iOS delta references the start of the Gesture
rotation_last_delta: Cell<CGFloat>,
@@ -39,7 +39,7 @@ declare_class!(
unsafe impl ClassType for WinitView {
#[inherits(UIResponder, NSObject)]
type Super = UIView;
type Mutability = mutability::InteriorMutable;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitUIView";
}
@@ -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
@@ -270,7 +270,7 @@ declare_class!(
fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) {
let window = self.window().unwrap();
let translation = recognizer.translationInView(self);
let translation = recognizer.translationInView(Some(self));
let (phase, dx, dy) = match recognizer.state() {
UIGestureRecognizerState::Began => {
@@ -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:?}"),
};
@@ -316,6 +316,11 @@ declare_class!(
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, gesture_event);
}
#[method(canBecomeFirstResponder)]
fn can_become_first_responder(&self) -> bool {
true
}
}
unsafe impl NSObjectProtocol for WinitView {}
@@ -326,32 +331,35 @@ declare_class!(
true
}
}
);
extern_methods!(
#[allow(non_snake_case)]
unsafe impl WinitView {
fn window(&self) -> Option<Id<WinitUIWindow>> {
unsafe { msg_send_id![self, window] }
unsafe impl UITextInputTraits for WinitView {
}
unsafe impl UIKeyInput for WinitView {
#[method(hasText)]
fn has_text(&self) -> bool {
true
}
unsafe fn traitCollection(&self) -> Id<UITraitCollection> {
msg_send_id![self, traitCollection]
#[method(insertText:)]
fn insert_text(&self, text: &NSString) {
self.handle_insert_text(text)
}
// TODO: Allow the user to customize this
#[method(layerClass)]
pub(crate) fn layerClass() -> &'static AnyClass;
#[method(deleteBackward)]
fn delete_backward(&self) {
self.handle_delete_backward()
}
}
);
impl WinitView {
pub(crate) fn new(
_mtm: MainThreadMarker,
mtm: MainThreadMarker,
window_attributes: &WindowAttributes,
frame: CGRect,
) -> Id<Self> {
let this = Self::alloc().set_ivars(WinitViewState {
) -> Retained<Self> {
let this = mtm.alloc().set_ivars(WinitViewState {
pinch_gesture_recognizer: RefCell::new(None),
doubletap_gesture_recognizer: RefCell::new(None),
rotation_gesture_recognizer: RefCell::new(None),
@@ -361,7 +369,7 @@ impl WinitView {
pinch_last_delta: Cell::new(0.0),
pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }),
});
let this: Id<Self> = unsafe { msg_send_id![super(this), initWithFrame: frame] };
let this: Retained<Self> = unsafe { msg_send_id![super(this), initWithFrame: frame] };
this.setMultipleTouchEnabled(true);
@@ -372,13 +380,23 @@ impl WinitView {
this
}
fn window(&self) -> Option<Retained<WinitUIWindow>> {
// SAFETY: `WinitView`s are always installed in a `WinitUIWindow`
(**self).window().map(|window| unsafe { Retained::cast(window) })
}
pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) {
let mtm = MainThreadMarker::from(self);
if should_recognize {
if self.ivars().pinch_gesture_recognizer.borrow().is_none() {
let pinch: Id<UIPinchGestureRecognizer> = unsafe {
msg_send_id![UIPinchGestureRecognizer::alloc(), initWithTarget: self, action: sel!(pinchGesture:)]
let pinch = unsafe {
UIPinchGestureRecognizer::initWithTarget_action(
mtm.alloc(),
Some(self),
Some(sel!(pinchGesture:)),
)
};
pinch.setDelegate(ProtocolObject::from_ref(self));
pinch.setDelegate(Some(ProtocolObject::from_ref(self)));
self.addGestureRecognizer(&pinch);
self.ivars().pinch_gesture_recognizer.replace(Some(pinch));
}
@@ -393,12 +411,17 @@ impl WinitView {
minimum_number_of_touches: u8,
maximum_number_of_touches: u8,
) {
let mtm = MainThreadMarker::from(self);
if should_recognize {
if self.ivars().pan_gesture_recognizer.borrow().is_none() {
let pan: Id<UIPanGestureRecognizer> = unsafe {
msg_send_id![UIPanGestureRecognizer::alloc(), initWithTarget: self, action: sel!(panGesture:)]
let pan = unsafe {
UIPanGestureRecognizer::initWithTarget_action(
mtm.alloc(),
Some(self),
Some(sel!(panGesture:)),
)
};
pan.setDelegate(ProtocolObject::from_ref(self));
pan.setDelegate(Some(ProtocolObject::from_ref(self)));
pan.setMinimumNumberOfTouches(minimum_number_of_touches as _);
pan.setMaximumNumberOfTouches(maximum_number_of_touches as _);
self.addGestureRecognizer(&pan);
@@ -410,12 +433,17 @@ impl WinitView {
}
pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) {
let mtm = MainThreadMarker::from(self);
if should_recognize {
if self.ivars().doubletap_gesture_recognizer.borrow().is_none() {
let tap: Id<UITapGestureRecognizer> = unsafe {
msg_send_id![UITapGestureRecognizer::alloc(), initWithTarget: self, action: sel!(doubleTapGesture:)]
let tap = unsafe {
UITapGestureRecognizer::initWithTarget_action(
mtm.alloc(),
Some(self),
Some(sel!(doubleTapGesture:)),
)
};
tap.setDelegate(ProtocolObject::from_ref(self));
tap.setDelegate(Some(ProtocolObject::from_ref(self)));
tap.setNumberOfTapsRequired(2);
tap.setNumberOfTouchesRequired(1);
self.addGestureRecognizer(&tap);
@@ -427,12 +455,17 @@ impl WinitView {
}
pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) {
let mtm = MainThreadMarker::from(self);
if should_recognize {
if self.ivars().rotation_gesture_recognizer.borrow().is_none() {
let rotation: Id<UIRotationGestureRecognizer> = unsafe {
msg_send_id![UIRotationGestureRecognizer::alloc(), initWithTarget: self, action: sel!(rotationGesture:)]
let rotation = unsafe {
UIRotationGestureRecognizer::initWithTarget_action(
mtm.alloc(),
Some(self),
Some(sel!(rotationGesture:)),
)
};
rotation.setDelegate(ProtocolObject::from_ref(self));
rotation.setDelegate(Some(ProtocolObject::from_ref(self)));
self.addGestureRecognizer(&rotation);
self.ivars().rotation_gesture_recognizer.replace(Some(rotation));
}
@@ -447,9 +480,9 @@ impl WinitView {
let os_supports_force = app_state::os_capabilities().force_touch;
for touch in touches {
let logical_location = touch.locationInView(None);
let touch_type = touch.type_();
let touch_type = touch.r#type();
let force = if os_supports_force {
let trait_collection = unsafe { self.traitCollection() };
let trait_collection = self.traitCollection();
let touch_capability = trait_collection.forceTouchCapability();
// Both the OS _and_ the device need to be checked for force touch support.
if touch_capability == UIForceTouchCapability::Available
@@ -482,7 +515,7 @@ impl WinitView {
// 2 is UITouchPhase::Stationary and is not expected here
UITouchPhase::Ended => TouchPhase::Ended,
UITouchPhase::Cancelled => TouchPhase::Cancelled,
_ => panic!("unexpected touch phase: {:?}", phase as i32),
_ => panic!("unexpected touch phase: {phase:?}"),
};
let physical_location = {
@@ -506,4 +539,69 @@ impl WinitView {
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(mtm, touch_events);
}
fn handle_insert_text(&self, text: &NSString) {
let window = self.window().unwrap();
let window_id = RootWindowId(window.id());
let mtm = MainThreadMarker::new().unwrap();
// send individual events for each character
app_state::handle_nonuser_events(
mtm,
text.to_string().chars().flat_map(|c| {
let text = smol_str::SmolStr::from_iter([c]);
// Emit both press and release events
[ElementState::Pressed, ElementState::Released].map(|state| {
EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
event: KeyEvent {
text: if state == ElementState::Pressed {
Some(text.clone())
} else {
None
},
state,
location: KeyLocation::Standard,
repeat: false,
logical_key: Key::Character(text.clone()),
physical_key: PhysicalKey::Unidentified(
NativeKeyCode::Unidentified,
),
platform_specific: KeyEventExtra {},
},
is_synthetic: false,
device_id: DEVICE_ID,
},
})
})
}),
);
}
fn handle_delete_backward(&self) {
let window = self.window().unwrap();
let window_id = RootWindowId(window.id());
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(
mtm,
[ElementState::Pressed, ElementState::Released].map(|state| {
EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: KeyEvent {
state,
logical_key: Key::Named(NamedKey::Backspace),
physical_key: PhysicalKey::Code(KeyCode::Backspace),
platform_specific: KeyEventExtra {},
repeat: false,
location: KeyLocation::Standard,
text: None,
},
is_synthetic: false,
},
})
}),
);
}
}

View File

@@ -1,14 +1,14 @@
use std::cell::Cell;
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_foundation::{MainThreadMarker, NSObject};
use super::app_state::{self};
use super::uikit::{
use objc2_ui_kit::{
UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle,
UIUserInterfaceIdiom, UIView, UIViewController,
};
use super::app_state::{self};
use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations};
use crate::window::WindowAttributes;
@@ -26,7 +26,7 @@ declare_class!(
unsafe impl ClassType for WinitViewController {
#[inherits(UIResponder, NSObject)]
type Super = UIViewController;
type Mutability = mutability::InteriorMutable;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitUIViewController";
}
@@ -114,7 +114,7 @@ impl WinitViewController {
mtm: MainThreadMarker,
valid_orientations: ValidOrientations,
) {
let mask = match (valid_orientations, UIDevice::current(mtm).userInterfaceIdiom()) {
let mask = match (valid_orientations, UIDevice::currentDevice(mtm).userInterfaceIdiom()) {
(ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => {
UIInterfaceOrientationMask::AllButUpsideDown
},
@@ -129,23 +129,24 @@ impl WinitViewController {
},
};
self.ivars().supported_orientations.set(mask);
UIViewController::attemptRotationToDeviceOrientation();
#[allow(deprecated)]
UIViewController::attemptRotationToDeviceOrientation(mtm);
}
pub(crate) fn new(
mtm: MainThreadMarker,
window_attributes: &WindowAttributes,
view: &UIView,
) -> Id<Self> {
) -> Retained<Self> {
// These are set properly below, we just to set them to something in the meantime.
let this = Self::alloc().set_ivars(ViewControllerState {
let this = mtm.alloc().set_ivars(ViewControllerState {
prefers_status_bar_hidden: Cell::new(false),
preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default),
prefers_home_indicator_auto_hidden: Cell::new(false),
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::NONE),
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()),
});
let this: Id<Self> = unsafe { msg_send_id![super(this), init] };
let this: Retained<Self> = unsafe { msg_send_id![super(this), init] };
this.set_prefers_status_bar_hidden(
window_attributes.platform_specific.prefers_status_bar_hidden,

View File

@@ -2,16 +2,19 @@
use std::collections::VecDeque;
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObject};
use objc2::{class, declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker};
use objc2_foundation::{
CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol,
};
use objc2_ui_kit::{
UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation,
UIViewController, UIWindow,
};
use tracing::{debug, warn};
use super::app_state::EventWrapper;
use super::uikit::{
UIApplication, UIResponder, UIScreen, UIScreenOverscanCompensation, UIViewController, UIWindow,
};
use super::view::WinitView;
use super::view_controller::WinitViewController;
use crate::cursor::Cursor;
@@ -35,7 +38,7 @@ declare_class!(
unsafe impl ClassType for WinitUIWindow {
#[inherits(UIResponder, NSObject)]
type Super = UIWindow;
type Mutability = mutability::InteriorMutable;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitUIWindow";
}
@@ -76,8 +79,8 @@ impl WinitUIWindow {
window_attributes: &WindowAttributes,
frame: CGRect,
view_controller: &UIViewController,
) -> Id<Self> {
let this: Id<Self> = unsafe { msg_send_id![Self::alloc(), initWithFrame: frame] };
) -> Retained<Self> {
let this: Retained<Self> = unsafe { msg_send_id![mtm.alloc(), initWithFrame: frame] };
this.setRootViewController(Some(view_controller));
@@ -104,9 +107,9 @@ impl WinitUIWindow {
}
pub struct Inner {
window: Id<WinitUIWindow>,
view_controller: Id<WinitViewController>,
view: Id<WinitView>,
window: Retained<WinitUIWindow>,
view_controller: Retained<WinitViewController>,
view: Retained<WinitView>,
gl_or_metal_backed: bool,
}
@@ -364,12 +367,24 @@ impl Inner {
warn!("`Window::set_ime_cursor_area` is ignored on iOS")
}
pub fn set_ime_allowed(&self, _allowed: bool) {
warn!("`Window::set_ime_allowed` is ignored on iOS")
/// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`,
/// requesting focus for the [WinitView]. Since [WinitView] implements
/// [objc2_ui_kit::UIKeyInput], the keyboard will be shown.
/// <https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder>
pub fn set_ime_allowed(&self, allowed: bool) {
if allowed {
unsafe {
self.view.becomeFirstResponder();
}
} else {
unsafe {
self.view.resignFirstResponder();
}
}
}
pub fn set_ime_purpose(&self, _purpose: ImePurpose) {
warn!("`Window::set_ime_allowed` is ignored on iOS")
warn!("`Window::set_ime_purpose` is ignored on iOS")
}
pub fn focus_window(&self) {
@@ -394,7 +409,8 @@ impl Inner {
}
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
Some(MonitorHandle::new(UIScreen::main(MainThreadMarker::new().unwrap())))
#[allow(deprecated)]
Some(MonitorHandle::new(UIScreen::mainScreen(MainThreadMarker::new().unwrap())))
}
pub fn id(&self) -> WindowId {
@@ -404,18 +420,18 @@ impl Inner {
#[cfg(feature = "rwh_04")]
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
let mut window_handle = rwh_04::UiKitHandle::empty();
window_handle.ui_window = Id::as_ptr(&self.window) as _;
window_handle.ui_view = Id::as_ptr(&self.view) as _;
window_handle.ui_view_controller = Id::as_ptr(&self.view_controller) as _;
window_handle.ui_window = Retained::as_ptr(&self.window) as _;
window_handle.ui_view = Retained::as_ptr(&self.view) as _;
window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _;
rwh_04::RawWindowHandle::UiKit(window_handle)
}
#[cfg(feature = "rwh_05")]
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
let mut window_handle = rwh_05::UiKitWindowHandle::empty();
window_handle.ui_window = Id::as_ptr(&self.window) as _;
window_handle.ui_view = Id::as_ptr(&self.view) as _;
window_handle.ui_view_controller = Id::as_ptr(&self.view_controller) as _;
window_handle.ui_window = Retained::as_ptr(&self.window) as _;
window_handle.ui_view = Retained::as_ptr(&self.view) as _;
window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _;
rwh_05::RawWindowHandle::UiKit(window_handle)
}
@@ -427,11 +443,11 @@ impl Inner {
#[cfg(feature = "rwh_06")]
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
let mut window_handle = rwh_06::UiKitWindowHandle::new({
let ui_view = Id::as_ptr(&self.view) as _;
std::ptr::NonNull::new(ui_view).expect("Id<T> should never be null")
let ui_view = Retained::as_ptr(&self.view) as _;
std::ptr::NonNull::new(ui_view).expect("Retained<T> should never be null")
});
window_handle.ui_view_controller =
std::ptr::NonNull::new(Id::as_ptr(&self.view_controller) as _);
std::ptr::NonNull::new(Retained::as_ptr(&self.view_controller) as _);
rwh_06::RawWindowHandle::UiKit(window_handle)
}
@@ -481,7 +497,8 @@ impl Window {
// TODO: transparency, visible
let main_screen = UIScreen::main(mtm);
#[allow(deprecated)]
let main_screen = UIScreen::mainScreen(mtm);
let fullscreen = window_attributes.fullscreen.clone().map(Into::into);
let screen = match fullscreen {
Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(mtm),
@@ -505,12 +522,8 @@ impl Window {
let view = WinitView::new(mtm, &window_attributes, frame);
let gl_or_metal_backed = unsafe {
let layer_class = WinitView::layerClass();
let is_metal = msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)];
let is_gl = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)];
is_metal || is_gl
};
let gl_or_metal_backed =
view.isKindOfClass(class!(CAMetalLayer)) || view.isKindOfClass(class!(CAEAGLLayer));
let view_controller = WinitViewController::new(mtm, &window_attributes, &view);
let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller);
@@ -674,10 +687,8 @@ impl Inner {
} else {
let screen_frame = self.rect_to_screen_space(bounds);
let status_bar_frame = {
let app = UIApplication::shared(MainThreadMarker::new().unwrap()).expect(
"`Window::get_inner_position` cannot be called before `EventLoop::run_app` on \
iOS",
);
let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap());
#[allow(deprecated)]
app.statusBarFrame()
};
let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height {
@@ -702,7 +713,7 @@ pub struct WindowId {
}
impl WindowId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
WindowId { window: std::ptr::null_mut() }
}
}

View File

@@ -184,7 +184,7 @@ pub struct KeyContext<'a> {
scratch_buffer: &'a mut Vec<u8>,
}
impl<'a> KeyContext<'a> {
impl KeyContext<'_> {
pub fn process_key_event(
&mut self,
keycode: u32,
@@ -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

@@ -117,6 +117,8 @@ pub(crate) static X11_BACKEND: Lazy<Mutex<Result<Arc<XConnection>, XNotSupported
pub enum OsError {
Misc(&'static str),
#[cfg(x11_platform)]
XNotSupported(XNotSupported),
#[cfg(x11_platform)]
XError(Arc<X11Error>),
#[cfg(wayland_platform)]
WaylandError(Arc<wayland::WaylandError>),
@@ -127,6 +129,8 @@ impl fmt::Display for OsError {
match *self {
OsError::Misc(e) => _f.pad(e),
#[cfg(x11_platform)]
OsError::XNotSupported(ref e) => fmt::Display::fmt(e, _f),
#[cfg(x11_platform)]
OsError::XError(ref e) => fmt::Display::fmt(e, _f),
#[cfg(wayland_platform)]
OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f),
@@ -157,7 +161,7 @@ impl From<u64> for WindowId {
}
impl WindowId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
Self(0)
}
}
@@ -171,11 +175,11 @@ pub enum DeviceId {
}
impl DeviceId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
#[cfg(wayland_platform)]
return DeviceId::Wayland(unsafe { wayland::DeviceId::dummy() });
return DeviceId::Wayland(wayland::DeviceId::dummy());
#[cfg(all(not(wayland_platform), x11_platform))]
return DeviceId::X(unsafe { x11::DeviceId::dummy() });
return DeviceId::X(x11::DeviceId::dummy());
}
}
@@ -643,18 +647,18 @@ pub(crate) enum PlatformCustomCursor {
/// Hooks for X11 errors.
#[cfg(x11_platform)]
pub(crate) static mut XLIB_ERROR_HOOKS: Mutex<Vec<XlibErrorHook>> = Mutex::new(Vec::new());
pub(crate) static XLIB_ERROR_HOOKS: Mutex<Vec<XlibErrorHook>> = Mutex::new(Vec::new());
#[cfg(x11_platform)]
unsafe extern "C" fn x_error_callback(
display: *mut x11::ffi::Display,
event: *mut x11::ffi::XErrorEvent,
) -> c_int {
let xconn_lock = X11_BACKEND.lock().unwrap();
let xconn_lock = X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner());
if let Ok(ref xconn) = *xconn_lock {
// Call all the hooks.
let mut error_handled = false;
for hook in unsafe { XLIB_ERROR_HOOKS.lock() }.unwrap().iter() {
for hook in XLIB_ERROR_HOOKS.lock().unwrap().iter() {
error_handled |= hook(display as *mut _, event as *mut _);
}
@@ -692,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>>),
@@ -764,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(),
}
}
@@ -777,14 +782,26 @@ impl<T: 'static> EventLoop<T> {
#[cfg(x11_platform)]
fn new_x11_any_thread() -> Result<EventLoop<T>, EventLoopError> {
let xconn = match X11_BACKEND.lock().unwrap().as_ref() {
let xconn = match X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()).as_ref() {
Ok(xconn) => xconn.clone(),
Err(_) => return Err(EventLoopError::NotSupported(NotSupportedError::new())),
Err(err) => {
return Err(EventLoopError::Os(os_error!(OsError::XNotSupported(err.clone()))))
},
};
Ok(EventLoop::X(x11::EventLoop::new(xconn)))
}
#[inline]
pub fn is_wayland(&self) -> bool {
match *self {
#[cfg(wayland_platform)]
EventLoop::Wayland(_) => true,
#[cfg(x11_platform)]
_ => false,
}
}
pub fn create_proxy(&self) -> EventLoopProxy<T> {
x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy)
}
@@ -833,6 +850,7 @@ impl<T: 'static> EventLoopProxy<T> {
}
}
#[allow(clippy::large_enum_variant)]
pub enum ActiveEventLoop {
#[cfg(wayland_platform)]
Wayland(wayland::ActiveEventLoop),
@@ -887,6 +905,11 @@ impl ActiveEventLoop {
x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_05())
}
#[inline]
pub fn system_theme(&self) -> Option<Theme> {
None
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(

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

@@ -1,5 +1,3 @@
#![cfg(wayland_platform)]
//! Winit's Wayland backend.
use std::fmt::Display;
@@ -68,7 +66,7 @@ impl From<WaylandError> for OsError {
pub struct DeviceId;
impl DeviceId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
DeviceId
}
}

View File

@@ -18,7 +18,6 @@ use crate::keyboard::ModifiersState;
use crate::platform_impl::common::xkb::Context;
use crate::platform_impl::wayland::event_loop::sink::EventSink;
use crate::platform_impl::wayland::seat::WinitSeatState;
use crate::platform_impl::wayland::state::WinitState;
use crate::platform_impl::wayland::{self, DeviceId, WindowId};
@@ -33,7 +32,17 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
) {
let seat_state = match state.seats.get_mut(&data.seat.id()) {
Some(seat_state) => seat_state,
None => return,
None => {
warn!("Received keyboard event {event:?} without seat");
return;
},
};
let keyboard_state = match seat_state.keyboard_state.as_mut() {
Some(keyboard_state) => keyboard_state,
None => {
warn!("Received keyboard event {event:?} without keyboard");
return;
},
};
match event {
@@ -43,7 +52,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
warn!("non-xkb compatible keymap")
},
WlKeymapFormat::XkbV1 => {
let context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context;
let context = &mut keyboard_state.xkb_context;
context.set_keymap_from_fd(fd, size as usize);
},
_ => unreachable!(),
@@ -67,7 +76,6 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
};
// Drop the repeat, if there were any.
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
keyboard_state.current_repeat = None;
if let Some(token) = keyboard_state.repeat_token.take() {
keyboard_state.loop_handle.remove(token);
@@ -91,9 +99,8 @@ 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.
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
keyboard_state.current_repeat = None;
if let Some(token) = keyboard_state.repeat_token.take() {
keyboard_state.loop_handle.remove(token);
@@ -128,7 +135,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
let key = key + 8;
key_input(
seat_state,
keyboard_state,
&mut state.events_sink,
data,
key,
@@ -136,7 +143,6 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
false,
);
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
let delay = match keyboard_state.repeat_info {
RepeatInfo::Repeat { delay, .. } => delay,
RepeatInfo::Disable => return,
@@ -163,18 +169,25 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
state.dispatched_events = true;
let data = wl_keyboard.data::<KeyboardData>().unwrap();
let seat_state = state.seats.get_mut(&data.seat.id()).unwrap();
let seat_state = match state.seats.get_mut(&data.seat.id()) {
Some(seat_state) => seat_state,
None => return TimeoutAction::Drop,
};
// NOTE: The removed on event source is batched, but key change to
// `None` is instant.
let repeat_keycode =
match seat_state.keyboard_state.as_ref().unwrap().current_repeat {
Some(repeat_keycode) => repeat_keycode,
None => return TimeoutAction::Drop,
};
let keyboard_state = match seat_state.keyboard_state.as_mut() {
Some(keyboard_state) => keyboard_state,
None => return TimeoutAction::Drop,
};
// NOTE: The removed on event source is batched, but key change to `None`
// is instant.
let repeat_keycode = match keyboard_state.current_repeat {
Some(repeat_keycode) => repeat_keycode,
None => return TimeoutAction::Drop,
};
key_input(
seat_state,
keyboard_state,
&mut state.events_sink,
data,
repeat_keycode,
@@ -183,7 +196,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
);
// NOTE: the gap could change dynamically while repeat is going.
match seat_state.keyboard_state.as_ref().unwrap().repeat_info {
match keyboard_state.repeat_info {
RepeatInfo::Repeat { gap, .. } => TimeoutAction::ToDuration(gap),
RepeatInfo::Disable => TimeoutAction::Drop,
}
@@ -194,7 +207,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
let key = key + 8;
key_input(
seat_state,
keyboard_state,
&mut state.events_sink,
data,
key,
@@ -202,7 +215,6 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
false,
);
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
if keyboard_state.repeat_info != RepeatInfo::Disable
&& keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key)
&& Some(key) == keyboard_state.current_repeat
@@ -216,7 +228,7 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
WlKeyboardEvent::Modifiers {
mods_depressed, mods_latched, mods_locked, group, ..
} => {
let xkb_context = &mut seat_state.keyboard_state.as_mut().unwrap().xkb_context;
let xkb_context = &mut keyboard_state.xkb_context;
let xkb_state = match xkb_context.state_mut() {
Some(state) => state,
None => return,
@@ -240,7 +252,6 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> for WinitState {
);
},
WlKeyboardEvent::RepeatInfo { rate, delay } => {
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
keyboard_state.repeat_info = if rate == 0 {
// Stop the repeat once we get a disable event.
keyboard_state.current_repeat = None;
@@ -348,7 +359,7 @@ impl KeyboardData {
}
fn key_input(
seat_state: &mut WinitSeatState,
keyboard_state: &mut KeyboardState,
event_sink: &mut EventSink,
data: &KeyboardData,
keycode: u32,
@@ -360,8 +371,6 @@ fn key_input(
None => return,
};
let keyboard_state = seat_state.keyboard_state.as_mut().unwrap();
let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId));
if let Some(mut key_context) = keyboard_state.xkb_context.key_context() {
let event = key_context.process_key_event(keycode, state, repeat);

View File

@@ -3,6 +3,7 @@
use std::sync::Arc;
use ahash::AHashMap;
use tracing::warn;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
@@ -76,7 +77,13 @@ impl SeatHandler for WinitState {
seat: WlSeat,
capability: SeatCapability,
) {
let seat_state = self.seats.get_mut(&seat.id()).unwrap();
let seat_state = match self.seats.get_mut(&seat.id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_seat::new_capability for unknown seat");
return;
},
};
match capability {
SeatCapability::Touch if seat_state.touch.is_none() => {
@@ -89,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(
@@ -139,7 +150,13 @@ impl SeatHandler for WinitState {
seat: WlSeat,
capability: SeatCapability,
) {
let seat_state = self.seats.get_mut(&seat.id()).unwrap();
let seat_state = match self.seats.get_mut(&seat.id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_seat::remove_capability for unknown seat");
return;
},
};
if let Some(text_input) = seat_state.text_input.take() {
text_input.destroy();

View File

@@ -4,6 +4,8 @@ use std::ops::Deref;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tracing::warn;
use sctk::reexports::client::delegate_dispatch;
use sctk::reexports::client::protocol::wl_pointer::WlPointer;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
@@ -16,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;
@@ -41,7 +44,21 @@ impl PointerHandler for WinitState {
events: &[PointerEvent],
) {
let seat = pointer.winit_data().seat();
let seat_state = self.seats.get(&seat.id()).unwrap();
let seat_state = match self.seats.get(&seat.id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received pointer event without seat");
return;
},
};
let themed_pointer = match seat_state.pointer.as_ref() {
Some(pointer) => pointer,
None => {
warn!("Received pointer event without pointer");
return;
},
};
let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId));
@@ -78,9 +95,7 @@ impl PointerHandler for WinitState {
event.position.0,
event.position.1,
) {
if let Some(pointer) = seat_state.pointer.as_ref() {
let _ = pointer.set_cursor(connection, icon);
}
let _ = themed_pointer.set_cursor(connection, icon);
}
},
PointerEventKind::Leave { .. } if parent_surface != surface => {
@@ -113,9 +128,7 @@ impl PointerHandler for WinitState {
self.events_sink
.push_window_event(WindowEvent::CursorEntered { device_id }, window_id);
if let Some(pointer) = seat_state.pointer.as_ref().map(Arc::downgrade) {
window.pointer_entered(pointer);
}
window.pointer_entered(Arc::downgrade(themed_pointer));
// Set the currently focused surface.
pointer.winit_data().inner.lock().unwrap().surface = Some(window_id);
@@ -126,9 +139,7 @@ impl PointerHandler for WinitState {
);
},
PointerEventKind::Leave { .. } => {
if let Some(pointer) = seat_state.pointer.as_ref().map(Arc::downgrade) {
window.pointer_left(pointer);
}
window.pointer_left(Arc::downgrade(themed_pointer));
// Remove the active surface.
pointer.winit_data().inner.lock().unwrap().surface = None;
@@ -183,15 +194,15 @@ 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 {
// XXX Wayland sign convention is the inverse of winit.
// NOTE: Wayland sign convention is the inverse of winit.
MouseScrollDelta::LineDelta(
(-horizontal.discrete) as f32,
(-vertical.discrete) as f32,
)
} else {
// XXX Wayland sign convention is the inverse of winit.
// NOTE: Wayland sign convention is the inverse of winit.
MouseScrollDelta::PixelDelta(
LogicalPosition::new(-horizontal.absolute, -vertical.absolute)
.to_physical(scale_factor),
@@ -215,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,
}
}
@@ -302,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

@@ -121,11 +121,15 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
None => return,
};
// Clear preedit at the start of `Done`.
state.events_sink.push_window_event(
WindowEvent::Ime(Ime::Preedit(String::new(), None)),
window_id,
);
// Clear preedit, unless all we'll be doing next is sending a new preedit.
if text_input_data.pending_commit.is_some()
|| text_input_data.pending_preedit.is_none()
{
state.events_sink.push_window_event(
WindowEvent::Ime(Ime::Preedit(String::new(), None)),
window_id,
);
}
// Send `Commit`.
if let Some(text) = text_input_data.pending_commit.take() {

View File

@@ -1,5 +1,7 @@
//! Touch handling.
use tracing::warn;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::protocol::wl_touch::WlTouch;
@@ -31,11 +33,16 @@ impl TouchHandler for WinitState {
None => return,
};
let location = LogicalPosition::<f64>::from(position);
let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap();
let seat_state = match self.seats.get_mut(&touch.seat().id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_touch::down without seat");
return;
},
};
// Update the state of the point.
let location = LogicalPosition::<f64>::from(position);
seat_state.touch_map.insert(id, TouchPoint { surface, location });
self.events_sink.push_window_event(
@@ -61,7 +68,13 @@ impl TouchHandler for WinitState {
_: u32,
id: i32,
) {
let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap();
let seat_state = match self.seats.get_mut(&touch.seat().id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_touch::up without seat");
return;
},
};
// Remove the touch point.
let touch_point = match seat_state.touch_map.remove(&id) {
@@ -98,7 +111,13 @@ impl TouchHandler for WinitState {
id: i32,
position: (f64, f64),
) {
let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap();
let seat_state = match self.seats.get_mut(&touch.seat().id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_touch::motion without seat");
return;
},
};
// Remove the touch point.
let touch_point = match seat_state.touch_map.get_mut(&id) {
@@ -129,7 +148,13 @@ impl TouchHandler for WinitState {
}
fn cancel(&mut self, _: &Connection, _: &QueueHandle<Self>, touch: &WlTouch) {
let seat_state = self.seats.get_mut(&touch.seat().id()).unwrap();
let seat_state = match self.seats.get_mut(&touch.seat().id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received wl_touch::cancel without seat");
return;
},
};
for (id, touch_point) in seat_state.touch_map.drain() {
let window_id = wayland::make_wid(&touch_point.surface);

View File

@@ -339,12 +339,30 @@ impl CompositorHandler for WinitState {
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wayland_client::protocol::wl_surface::WlSurface,
_: &WlSurface,
_: wayland_client::protocol::wl_output::Transform,
) {
// TODO(kchibisov) we need to expose it somehow in winit.
}
fn surface_enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlSurface,
_: &WlOutput,
) {
}
fn surface_leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlSurface,
_: &WlOutput,
) {
}
fn scale_factor_changed(
&mut self,
_: &Connection,

View File

@@ -80,7 +80,7 @@ impl Dispatch<XdgActivationTokenV1, XdgActivationTokenData, WinitState> for XdgA
state.events_sink.push_window_event(
crate::event::WindowEvent::ActivationTokenDone {
serial: *serial,
token: ActivationToken::_new(token),
token: ActivationToken::from_raw(token),
},
*window_id,
);

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,11 +166,17 @@ 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)
{
xdg_activation.activate(token._token, &surface);
xdg_activation.activate(token.token, &surface);
}
// XXX Do initial commit.
@@ -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

@@ -165,14 +165,14 @@ fn push_display(buffer: &mut Vec<u8>, display: &impl std::fmt::Display) {
buffer: &'a mut Vec<u8>,
}
impl<'a> std::fmt::Write for Writer<'a> {
impl std::fmt::Write for Writer<'_> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.buffer.extend_from_slice(s.as_bytes());
Ok(())
}
}
write!(Writer { buffer }, "{}", display).unwrap();
write!(Writer { buffer }, "{display}").unwrap();
}
#[cfg(test)]

View File

@@ -66,7 +66,10 @@ pub struct EventProcessor {
pub active_window: Option<xproto::Window>,
/// Latest modifiers we've sent for the user to trigger change in event.
pub modifiers: Cell<ModifiersState>,
pub xfiltered_modifiers: VecDeque<c_ulong>,
// Track modifiers based on keycodes. NOTE: that serials generally don't work for tracking
// since they are not unique and could be duplicated in case of sequence of key events is
// delivered at near the same time.
pub xfiltered_modifiers: VecDeque<u8>,
pub xmodmap: util::ModifierKeymap,
pub is_composing: bool,
}
@@ -129,6 +132,7 @@ impl EventProcessor {
/// Specifically, this involves all of the KeyPress events in compose/pre-edit sequences,
/// along with an extra copy of the KeyRelease events. This also prevents backspace and
/// arrow keys from being detected twice.
#[must_use]
fn filter_event(&mut self, xev: &mut XEvent) -> bool {
let wt = Self::window_target(&self.target);
unsafe {
@@ -145,20 +149,38 @@ impl EventProcessor {
{
let event_type = xev.get_type();
if self.filter_event(xev) {
if event_type == xlib::KeyPress || event_type == xlib::KeyRelease {
// If we have IME disabled, don't try to `filter_event`, since only IME can consume them
// and forward back. This is not desired for e.g. games since some IMEs may delay the input
// and game can toggle IME back when e.g. typing into some field where latency won't really
// matter.
let filtered = if event_type == xlib::KeyPress || event_type == xlib::KeyRelease {
let wt = Self::window_target(&self.target);
let ime = wt.ime.as_ref();
let window = self.active_window.map(|window| window as XWindow);
let forward_to_ime = ime
.and_then(|ime| window.map(|window| ime.borrow().is_ime_allowed(window)))
.unwrap_or(false);
let filtered = forward_to_ime && self.filter_event(xev);
if filtered {
let xev: &XKeyEvent = xev.as_ref();
if self.xmodmap.is_modifier(xev.keycode as u8) {
// Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen
// when the modifiers are consumed entirely or serials are altered.
//
// Both cases shouldn't happen in well behaving clients.
// when the modifiers are consumed entirely.
if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN {
self.xfiltered_modifiers.pop_back();
}
self.xfiltered_modifiers.push_front(xev.serial);
self.xfiltered_modifiers.push_front(xev.keycode as u8);
}
}
filtered
} else {
self.filter_event(xev)
};
// Don't process event if it was filtered.
if filtered {
return;
}
@@ -434,7 +456,7 @@ impl EventProcessor {
let flags = xev.data.get_long(1);
let version = flags >> 24;
self.dnd.version = Some(version);
let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1;
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
if !has_more_types {
let type_list = vec![
xev.data.get_long(2) as xproto::Atom,
@@ -929,7 +951,7 @@ impl EventProcessor {
// itself are out of sync due to XkbState being delivered before XKeyEvent, since it's
// being replayed by the XIM, thus we should replay ourselves.
let replay = if let Some(position) =
self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.serial)
self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.keycode as u8)
{
// We don't have to replay modifiers pressed before the current event if some events
// were not forwarded to us, since their state is irrelevant.
@@ -1424,7 +1446,7 @@ impl EventProcessor {
if !xinput2::XIMaskIsSet(mask, i) {
continue;
}
let x = unsafe { *value };
let x = unsafe { value.read_unaligned() };
// We assume that every XInput2 device with analog axes is a pointing device emitting
// relative coordinates.

View File

@@ -123,19 +123,15 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
let is_allowed =
old_context.as_ref().map(|old_context| old_context.is_allowed()).unwrap_or_default();
// We can't use the style from the old context here, since it may change on reload, so
// pick style from the new XIM based on the old state.
let style = if is_allowed { new_im.preedit_style } else { new_im.none_style };
let new_context = {
let result = unsafe {
ImeContext::new(
xconn,
new_im.im,
style,
&new_im,
*window,
spot,
(*inner).event_sender.clone(),
is_allowed,
)
};
if result.is_err() {

View File

@@ -5,10 +5,9 @@ use std::{mem, ptr};
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
use super::{ffi, util, XConnection, XError};
use crate::platform_impl::platform::x11::ime::input_method::{InputMethod, Style, XIMStyle};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
/// IME creation error.
#[derive(Debug)]
@@ -82,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 +157,9 @@ struct PreeditCallbacks {
impl PreeditCallbacks {
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
let start_callback = create_xim_callback(client_data, unsafe {
mem::transmute(preedit_start_callback as usize)
mem::transmute::<usize, unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer)>(
preedit_start_callback as *const () as usize,
)
});
let done_callback = create_xim_callback(client_data, preedit_done_callback);
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
@@ -182,7 +183,7 @@ struct ImeContextClientData {
pub struct ImeContext {
pub(crate) ic: ffi::XIC,
pub(crate) ic_spot: ffi::XPoint,
pub(crate) style: Style,
pub(crate) allowed: bool,
// Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free
// from there we keep the pointer to automatically deallocate it.
_client_data: Box<ImeContextClientData>,
@@ -191,11 +192,11 @@ pub struct ImeContext {
impl ImeContext {
pub(crate) unsafe fn new(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: Style,
im: &InputMethod,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
event_sender: ImeEventSender,
allowed: bool,
) -> Result<Self, ImeContextCreationError> {
let client_data = Box::into_raw(Box::new(ImeContextClientData {
window,
@@ -204,20 +205,24 @@ impl ImeContext {
cursor_pos: 0,
}));
let style = if allowed { im.preedit_style } else { im.none_style };
let ic = match style as _ {
Style::Preedit(style) => unsafe {
ImeContext::create_preedit_ic(
xconn,
im,
im.im,
style,
window,
client_data as ffi::XPointer,
)
},
Style::Nothing(style) => unsafe {
ImeContext::create_nothing_ic(xconn, im, style, window)
ImeContext::create_nothing_ic(xconn, im.im, style, window)
},
Style::None(style) => unsafe {
ImeContext::create_none_ic(xconn, im.im, style, window)
},
Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) },
}
.ok_or(ImeContextCreationError::Null)?;
@@ -226,7 +231,7 @@ impl ImeContext {
let mut context = ImeContext {
ic,
ic_spot: ffi::XPoint { x: 0, y: 0 },
style,
allowed,
_client_data: unsafe { Box::from_raw(client_data) },
};
@@ -333,7 +338,7 @@ impl ImeContext {
}
pub fn is_allowed(&self) -> bool {
!matches!(self.style, Style::None(_))
self.allowed
}
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks

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

@@ -10,15 +10,13 @@ use std::sync::Arc;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use tracing::debug;
use super::{ffi, util, XConnection, XError};
use self::callbacks::*;
use self::context::ImeContext;
pub use self::context::ImeContextCreationError;
use self::inner::{close_im, ImeInner};
use self::input_method::{PotentialInputMethods, Style};
use self::input_method::PotentialInputMethods;
use super::{ffi, util, XConnection, XError};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -114,39 +112,26 @@ impl Ime {
pub fn create_context(
&mut self,
window: ffi::Window,
with_preedit: bool,
with_ime: bool,
) -> Result<bool, ImeContextCreationError> {
let context = if self.is_destroyed() {
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
None
} else {
let im = self.inner.im.as_ref().unwrap();
let style = if with_preedit { im.preedit_style } else { im.none_style };
let context = unsafe {
ImeContext::new(
&self.inner.xconn,
im.im,
style,
im,
window,
None,
self.inner.event_sender.clone(),
with_ime,
)?
};
// Check the state on the context, since it could fail to enable or disable preedit.
let event = if matches!(style, Style::None(_)) {
if with_preedit {
debug!("failed to create IME context with preedit support.")
}
ImeEvent::Disabled
} else {
if !with_preedit {
debug!("failed to create IME context without preedit support.")
}
ImeEvent::Enabled
};
let event = if context.is_allowed() { ImeEvent::Enabled } else { ImeEvent::Disabled };
self.inner.event_sender.send((window, event)).expect("Failed to send enabled event");
Some(context)
@@ -226,6 +211,16 @@ impl Ime {
// Create new context supporting IME input.
let _ = self.create_context(window, allowed);
}
pub fn is_ime_allowed(&self, window: ffi::Window) -> bool {
if self.is_destroyed() {
false
} else if let Some(Some(context)) = self.inner.contexts.get(&window) {
context.is_allowed()
} else {
false
}
}
}
impl Drop for Ime {

View File

@@ -1,5 +1,3 @@
#![cfg(x11_platform)]
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet, VecDeque};
use std::ffi::CStr;
@@ -499,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;
}
@@ -533,7 +532,7 @@ impl<T: 'static> EventLoop<T> {
window_id: crate::window::WindowId(window_id),
event: WindowEvent::ActivationTokenDone {
serial,
token: crate::window::ActivationToken::_new(token),
token: crate::window::ActivationToken::from_raw(token),
},
};
callback(event, &self.event_processor.target)
@@ -753,14 +752,14 @@ impl<'a> DeviceInfo<'a> {
}
}
impl<'a> Drop for DeviceInfo<'a> {
impl Drop for DeviceInfo<'_> {
fn drop(&mut self) {
assert!(!self.info.is_null());
unsafe { (self.xconn.xinput2.XIFreeDeviceInfo)(self.info as *mut _) };
}
}
impl<'a> Deref for DeviceInfo<'a> {
impl Deref for DeviceInfo<'_> {
type Target = [ffi::XIDeviceInfo];
fn deref(&self) -> &Self::Target {
@@ -773,7 +772,7 @@ pub struct DeviceId(xinput::DeviceId);
impl DeviceId {
#[allow(unused)]
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
DeviceId(0)
}
}
@@ -851,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:?}")
},
}
}
@@ -959,7 +958,7 @@ trait CookieResultExt {
fn expect_then_ignore_error(self, msg: &str);
}
impl<'a, E: fmt::Debug> CookieResultExt for Result<VoidCookie<'a>, E> {
impl<E: fmt::Debug> CookieResultExt for Result<VoidCookie<'_>, E> {
fn expect_then_ignore_error(self, msg: &str) {
self.expect(msg).ignore_error()
}

View File

@@ -301,7 +301,7 @@ impl XConnection {
let info = self
.xcb_connection()
.extension_information(randr::X11_EXTENSION_NAME)?
.ok_or_else(|| X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?;
.ok_or(X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?;
// Select input data.
let event_mask =

View File

@@ -19,7 +19,7 @@ impl<'a, T> XSmartPointer<'a, T> {
}
}
impl<'a, T> Deref for XSmartPointer<'a, T> {
impl<T> Deref for XSmartPointer<'_, T> {
type Target = T;
fn deref(&self) -> &T {
@@ -27,13 +27,13 @@ impl<'a, T> Deref for XSmartPointer<'a, T> {
}
}
impl<'a, T> DerefMut for XSmartPointer<'a, T> {
impl<T> DerefMut for XSmartPointer<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.ptr }
}
}
impl<'a, T> Drop for XSmartPointer<'a, T> {
impl<T> Drop for XSmartPointer<'_, T> {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XFree)(self.ptr as *mut _);

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};
@@ -144,12 +143,26 @@ impl UnownedWindow {
) -> Result<UnownedWindow, RootOsError> {
let xconn = &event_loop.xconn;
let atoms = xconn.atoms();
let screen_id = match window_attrs.platform_specific.x11.screen_id {
Some(id) => id,
None => xconn.default_screen_index() as c_int,
};
let screen = {
let screen_id_usize = usize::try_from(screen_id)
.map_err(|_| os_error!(OsError::Misc("screen id must be non-negative")))?;
xconn.xcb_connection().setup().roots.get(screen_id_usize).ok_or(os_error!(
OsError::Misc("requested screen id not present in server's response")
))?
};
#[cfg(feature = "rwh_06")]
let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) {
Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window,
Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(),
Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"),
None => event_loop.root,
None => screen.root,
};
#[cfg(not(feature = "rwh_06"))]
let root = event_loop.root;
@@ -207,18 +220,10 @@ impl UnownedWindow {
dimensions
};
let screen_id = match window_attrs.platform_specific.x11.screen_id {
Some(id) => id,
None => xconn.default_screen_index() as c_int,
};
// An iterator over all of the visuals combined with their depths.
let mut all_visuals = xconn
.xcb_connection()
.setup()
.roots
// An iterator over the visuals matching screen id combined with their depths.
let mut all_visuals = screen
.allowed_depths
.iter()
.flat_map(|root| &root.allowed_depths)
.flat_map(|depth| depth.visuals.iter().map(move |visual| (visual, depth.depth)));
// creating
@@ -484,6 +489,20 @@ impl UnownedWindow {
);
leap!(result).ignore_error();
// Select XInput2 events
let mask = xinput::XIEventMask::MOTION
| xinput::XIEventMask::BUTTON_PRESS
| xinput::XIEventMask::BUTTON_RELEASE
| xinput::XIEventMask::ENTER
| xinput::XIEventMask::LEAVE
| xinput::XIEventMask::FOCUS_IN
| xinput::XIEventMask::FOCUS_OUT
| xinput::XIEventMask::TOUCH_BEGIN
| xinput::XIEventMask::TOUCH_UPDATE
| xinput::XIEventMask::TOUCH_END;
leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask))
.ignore_error();
// Set visibility (map window)
if window_attrs.visible {
leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error();
@@ -507,20 +526,6 @@ impl UnownedWindow {
}
}
// Select XInput2 events
let mask = xinput::XIEventMask::MOTION
| xinput::XIEventMask::BUTTON_PRESS
| xinput::XIEventMask::BUTTON_RELEASE
| xinput::XIEventMask::ENTER
| xinput::XIEventMask::LEAVE
| xinput::XIEventMask::FOCUS_IN
| xinput::XIEventMask::FOCUS_OUT
| xinput::XIEventMask::TOUCH_BEGIN
| xinput::XIEventMask::TOUCH_UPDATE
| xinput::XIEventMask::TOUCH_END;
leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask))
.ignore_error();
// Try to create input context for the window.
if let Some(ime) = event_loop.ime.as_ref() {
let result = ime.borrow_mut().create_context(window.xwindow as ffi::Window, false);
@@ -553,7 +558,7 @@ impl UnownedWindow {
// Remove the startup notification if we have one.
if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() {
leap!(xconn.remove_activation_token(xwindow, &startup._token));
leap!(xconn.remove_activation_token(xwindow, &startup.token));
}
// We never want to give the user a broken window, since by then, it's too late to handle.
@@ -970,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,
@@ -1486,6 +1491,11 @@ impl UnownedWindow {
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
// We don't support the locked cursor yet, so ignore it early on.
if mode == CursorGrabMode::Locked {
return Err(ExternalError::NotSupported(NotSupportedError::new()));
}
let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap();
if mode == *grabbed_lock {
return Ok(());
@@ -1497,40 +1507,40 @@ impl UnownedWindow {
.xcb_connection()
.ungrab_pointer(x11rb::CURRENT_TIME)
.expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`");
*grabbed_lock = CursorGrabMode::None;
let result = match mode {
CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| {
ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into())))
}),
CursorGrabMode::Confined => {
let result = {
self.xconn
.xcb_connection()
.grab_pointer(
true as _,
self.xwindow,
xproto::EventMask::BUTTON_PRESS
| xproto::EventMask::BUTTON_RELEASE
| xproto::EventMask::ENTER_WINDOW
| xproto::EventMask::LEAVE_WINDOW
| xproto::EventMask::POINTER_MOTION
| xproto::EventMask::POINTER_MOTION_HINT
| xproto::EventMask::BUTTON1_MOTION
| xproto::EventMask::BUTTON2_MOTION
| xproto::EventMask::BUTTON3_MOTION
| xproto::EventMask::BUTTON4_MOTION
| xproto::EventMask::BUTTON5_MOTION
| xproto::EventMask::KEYMAP_STATE,
xproto::GrabMode::ASYNC,
xproto::GrabMode::ASYNC,
self.xwindow,
0u32,
x11rb::CURRENT_TIME,
)
.expect("Failed to call `grab_pointer`")
.reply()
.expect("Failed to receive reply from `grab_pointer`")
};
let result = self
.xconn
.xcb_connection()
.grab_pointer(
true as _,
self.xwindow,
xproto::EventMask::BUTTON_PRESS
| xproto::EventMask::BUTTON_RELEASE
| xproto::EventMask::ENTER_WINDOW
| xproto::EventMask::LEAVE_WINDOW
| xproto::EventMask::POINTER_MOTION
| xproto::EventMask::POINTER_MOTION_HINT
| xproto::EventMask::BUTTON1_MOTION
| xproto::EventMask::BUTTON2_MOTION
| xproto::EventMask::BUTTON3_MOTION
| xproto::EventMask::BUTTON4_MOTION
| xproto::EventMask::BUTTON5_MOTION
| xproto::EventMask::KEYMAP_STATE,
xproto::GrabMode::ASYNC,
xproto::GrabMode::ASYNC,
self.xwindow,
0u32,
x11rb::CURRENT_TIME,
)
.expect("Failed to call `grab_pointer`")
.reply()
.expect("Failed to receive reply from `grab_pointer`");
match result.status {
xproto::GrabStatus::SUCCESS => Ok(()),
@@ -1550,9 +1560,7 @@ impl UnownedWindow {
}
.map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err))))
},
CursorGrabMode::Locked => {
return Err(ExternalError::NotSupported(NotSupportedError::new()));
},
CursorGrabMode::Locked => return Ok(()),
};
if result.is_ok() {
@@ -1613,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();
@@ -1623,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,50 +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 super::app_delegate::ApplicationDelegate;
use super::event::flags_contains;
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
&& flags_contains(modifier_flags, 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() };
@@ -58,25 +112,27 @@ fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent)
let delta_y = unsafe { event.deltaY() } as f64;
if delta_x != 0.0 {
delegate.queue_device_event(DeviceEvent::Motion { axis: 0, value: delta_x });
delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 0, value: delta_x });
}
if delta_y != 0.0 {
delegate.queue_device_event(DeviceEvent::Motion { axis: 1, value: delta_y })
delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 1, value: delta_y })
}
if delta_x != 0.0 || delta_y != 0.0 {
delegate.queue_device_event(DeviceEvent::MouseMotion { delta: (delta_x, delta_y) });
delegate.maybe_queue_device_event(DeviceEvent::MouseMotion {
delta: (delta_x, delta_y),
});
}
},
NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => {
delegate.queue_device_event(DeviceEvent::Button {
delegate.maybe_queue_device_event(DeviceEvent::Button {
button: unsafe { event.buttonNumber() } as u32,
state: ElementState::Pressed,
});
},
NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => {
delegate.queue_device_event(DeviceEvent::Button {
delegate.maybe_queue_device_event(DeviceEvent::Button {
button: unsafe { event.buttonNumber() } as u32,
state: ElementState::Released,
});
@@ -84,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

@@ -1,40 +1,29 @@
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::mem;
use std::rc::Weak;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::rc::Retained;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate};
use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol, NSSize};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSRunningApplication,
};
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::window::WinitWindow;
use super::{menu, WindowId, DEVICE_ID};
use crate::dpi::PhysicalSize;
use crate::event::{DeviceEvent, Event, InnerSizeWriter, StartCause, WindowEvent};
use crate::event::{DeviceEvent, Event, StartCause, WindowEvent};
use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow};
use crate::window::WindowId as RootWindowId;
#[derive(Debug)]
struct Policy(NSApplicationActivationPolicy);
impl Default for Policy {
fn default() -> Self {
Self(NSApplicationActivationPolicy::Regular)
}
}
#[derive(Debug, Default)]
pub(super) struct State {
activation_policy: Policy,
pub(super) struct AppState {
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
run_loop: RunLoop,
event_handler: EventHandler,
stop_on_launch: Cell<bool>,
stop_before_wait: Cell<bool>,
@@ -50,8 +39,9 @@ pub(super) struct State {
waker: RefCell<EventLoopWaker>,
start_time: Cell<Option<Instant>>,
wait_timeout: Cell<Option<Instant>>,
pending_events: RefCell<VecDeque<QueuedEvent>>,
pending_redraw: RefCell<Vec<WindowId>>,
// NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and
// as such should be careful to not add fields that, in turn, strongly reference those.
}
declare_class!(
@@ -65,62 +55,20 @@ declare_class!(
}
impl DeclaredClass for ApplicationDelegate {
type Ivars = State;
type Ivars = AppState;
}
unsafe impl NSObjectProtocol for ApplicationDelegate {}
unsafe impl NSApplicationDelegate for ApplicationDelegate {
// Note: This will, globally, only be run once, no matter how many
// `EventLoop`s the user creates.
#[method(applicationDidFinishLaunching:)]
fn did_finish_launching(&self, _sender: Option<&AnyObject>) {
trace_scope!("applicationDidFinishLaunching:");
self.ivars().is_launched.set(true);
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
// We need to delay setting the activation policy and activating the app
// until `applicationDidFinishLaunching` has been called. Otherwise the
// menu bar is initially unresponsive on macOS 10.15.
app.setActivationPolicy(self.ivars().activation_policy.0);
window_activation_hack(&app);
#[allow(deprecated)]
app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps);
if self.ivars().default_menu {
// The menubar initialization should be before the `NewEvents` event, to allow
// overriding of the default menu even if it's created
menu::initialize(&app);
}
self.ivars().waker.borrow_mut().start();
self.set_is_running(true);
self.dispatch_init_events();
// If the application is being launched via `EventLoop::pump_app_events()` then we'll
// want to stop the app once it is launched (and return to the external loop)
//
// In this case we still want to consider Winit's `EventLoop` to be "running",
// so we call `start_running()` above.
if self.ivars().stop_on_launch.get() {
// Note: the original idea had been to only stop the underlying `RunLoop`
// for the app but that didn't work as expected (`-[NSApplication run]`
// effectively ignored the attempt to stop the RunLoop and re-started it).
//
// So we return from `pump_events` by stopping the application.
let app = NSApplication::sharedApplication(mtm);
stop_app_immediately(&app);
}
fn app_did_finish_launching(&self, notification: &NSNotification) {
self.did_finish_launching(notification)
}
#[method(applicationWillTerminate:)]
fn will_terminate(&self, _sender: Option<&AnyObject>) {
trace_scope!("applicationWillTerminate:");
// TODO: Notify every window that it will be destroyed, like done in iOS?
self.internal_exit();
fn app_will_terminate(&self, notification: &NSNotification) {
self.will_terminate(notification)
}
}
);
@@ -128,26 +76,108 @@ declare_class!(
impl ApplicationDelegate {
pub(super) fn new(
mtm: MainThreadMarker,
activation_policy: NSApplicationActivationPolicy,
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
) -> Id<Self> {
let this = mtm.alloc().set_ivars(State {
activation_policy: Policy(activation_policy),
) -> Retained<Self> {
let this = mtm.alloc().set_ivars(AppState {
activation_policy,
default_menu,
activate_ignoring_other_apps,
..Default::default()
run_loop: RunLoop::main(mtm),
event_handler: EventHandler::new(),
stop_on_launch: Cell::new(false),
stop_before_wait: Cell::new(false),
stop_after_wait: Cell::new(false),
stop_on_redraw: Cell::new(false),
is_launched: Cell::new(false),
is_running: Cell::new(false),
exit: Cell::new(false),
control_flow: Cell::new(ControlFlow::default()),
waker: RefCell::new(EventLoopWaker::new()),
start_time: Cell::new(None),
wait_timeout: Cell::new(None),
pending_redraw: RefCell::new(vec![]),
});
unsafe { msg_send_id![super(this), init] }
}
pub fn get(mtm: MainThreadMarker) -> Id<Self> {
// NOTE: This will, globally, only be run once, no matter how many
// `EventLoop`s the user creates.
fn did_finish_launching(&self, _notification: &NSNotification) {
trace_scope!("applicationDidFinishLaunching:");
self.ivars().is_launched.set(true);
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
// We need to delay setting the activation policy and activating the app
// until `applicationDidFinishLaunching` has been called. Otherwise the
// menu bar is initially unresponsive on macOS 10.15.
// If no activation policy is explicitly provided, do not set it at all
// to allow the package manifest to define behavior via LSUIElement.
if let Some(activation_policy) = self.ivars().activation_policy {
app.setActivationPolicy(activation_policy);
} else {
// If no activation policy is explicitly provided, and the application
// is bundled, do not set the activation policy at all, to allow the
// package manifest to define the behavior via LSUIElement.
//
// See:
// - https://github.com/rust-windowing/winit/issues/261
// - https://github.com/rust-windowing/winit/issues/3958
let is_bundled =
unsafe { NSRunningApplication::currentApplication().bundleIdentifier().is_some() };
if !is_bundled {
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
}
}
window_activation_hack(&app);
#[allow(deprecated)]
app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps);
if self.ivars().default_menu {
// The menubar initialization should be before the `NewEvents` event, to allow
// overriding of the default menu even if it's created
menu::initialize(&app);
}
self.ivars().waker.borrow_mut().start();
self.set_is_running(true);
self.dispatch_init_events();
// If the application is being launched via `EventLoop::pump_app_events()` then we'll
// want to stop the app once it is launched (and return to the external loop)
//
// In this case we still want to consider Winit's `EventLoop` to be "running",
// so we call `start_running()` above.
if self.ivars().stop_on_launch.get() {
// NOTE: the original idea had been to only stop the underlying `RunLoop`
// for the app but that didn't work as expected (`-[NSApplication run]`
// effectively ignored the attempt to stop the RunLoop and re-started it).
//
// So we return from `pump_events` by stopping the application.
let app = NSApplication::sharedApplication(mtm);
stop_app_immediately(&app);
}
}
fn will_terminate(&self, _notification: &NSNotification) {
trace_scope!("applicationWillTerminate:");
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
notify_windows_of_exit(&app);
self.internal_exit();
}
pub fn get(mtm: MainThreadMarker) -> Retained<Self> {
let app = NSApplication::sharedApplication(mtm);
let delegate =
unsafe { app.delegate() }.expect("a delegate was not configured on the application");
if delegate.is_kind_of::<Self>() {
// SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate`
unsafe { Id::cast(delegate) }
unsafe { Retained::cast(delegate) }
} else {
panic!("tried to get a delegate that was not the one Winit has registered")
}
@@ -188,7 +218,7 @@ impl ApplicationDelegate {
/// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits.
///
/// Note: that if the `NSApplication` has been launched then that state is preserved,
/// NOTE: that if the `NSApplication` has been launched then that state is preserved,
/// and we won't need to re-launch the app if subsequent EventLoops are run.
pub fn internal_exit(&self) {
self.handle_event(Event::LoopExiting);
@@ -232,28 +262,16 @@ impl ApplicationDelegate {
self.ivars().control_flow.get()
}
pub fn queue_window_event(&self, window_id: WindowId, event: WindowEvent) {
self.ivars()
.pending_events
.borrow_mut()
.push_back(QueuedEvent::WindowEvent(window_id, event));
pub fn maybe_queue_window_event(&self, window_id: WindowId, event: WindowEvent) {
self.maybe_queue_event(Event::WindowEvent { window_id: RootWindowId(window_id), event });
}
pub fn queue_device_event(&self, event: DeviceEvent) {
self.ivars().pending_events.borrow_mut().push_back(QueuedEvent::DeviceEvent(event));
pub fn handle_window_event(&self, window_id: WindowId, event: WindowEvent) {
self.handle_event(Event::WindowEvent { window_id: RootWindowId(window_id), event });
}
pub fn queue_static_scale_factor_changed_event(
&self,
window: Id<WinitWindow>,
suggested_size: PhysicalSize<u32>,
scale_factor: f64,
) {
self.ivars().pending_events.borrow_mut().push_back(QueuedEvent::ScaleFactorChanged {
window,
suggested_size,
scale_factor,
});
pub fn maybe_queue_device_event(&self, event: DeviceEvent) {
self.maybe_queue_event(Event::DeviceEvent { device_id: DEVICE_ID, event });
}
pub fn handle_redraw(&self, window_id: WindowId) {
@@ -281,9 +299,27 @@ impl ApplicationDelegate {
if !pending_redraw.contains(&window_id) {
pending_redraw.push(window_id);
}
unsafe { RunLoop::get() }.wakeup();
self.ivars().run_loop.wakeup();
}
#[track_caller]
fn maybe_queue_event(&self, event: Event<HandlePendingUserEvents>) {
// Most programmer actions in AppKit (e.g. change window fullscreen, set focused, etc.)
// result in an event being queued, and applied at a later point.
//
// However, it is not documented which actions do this, and which ones are done immediately,
// so to make sure that we don't encounter re-entrancy issues, we first check if we're
// currently handling another event, and if we are, we queue the event instead.
if !self.ivars().event_handler.in_use() {
self.handle_event(event);
} else {
tracing::debug!(?event, "had to queue event since another is currently being handled");
let this = self.retain();
self.ivars().run_loop.queue_closure(move || this.handle_event(event));
}
}
#[track_caller]
fn handle_event(&self, event: Event<HandlePendingUserEvents>) {
self.ivars().event_handler.handle_event(event, &ActiveEventLoop::new_root(self.retain()))
}
@@ -345,47 +381,6 @@ impl ApplicationDelegate {
self.handle_event(Event::UserEvent(HandlePendingUserEvents));
let events = mem::take(&mut *self.ivars().pending_events.borrow_mut());
for event in events {
match event {
QueuedEvent::WindowEvent(window_id, event) => {
self.handle_event(Event::WindowEvent {
window_id: RootWindowId(window_id),
event,
});
},
QueuedEvent::DeviceEvent(event) => {
self.handle_event(Event::DeviceEvent { device_id: DEVICE_ID, event });
},
QueuedEvent::ScaleFactorChanged { window, suggested_size, scale_factor } => {
let new_inner_size = Arc::new(Mutex::new(suggested_size));
let scale_factor_changed_event = Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::ScaleFactorChanged {
scale_factor,
inner_size_writer: InnerSizeWriter::new(Arc::downgrade(
&new_inner_size,
)),
},
};
self.handle_event(scale_factor_changed_event);
let physical_size = *new_inner_size.lock().unwrap();
drop(new_inner_size);
let logical_size = physical_size.to_logical(scale_factor);
let size = NSSize::new(logical_size.width, logical_size.height);
window.setContentSize(size);
let resized_event = Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::Resized(physical_size),
};
self.handle_event(resized_event);
},
}
}
let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut());
for window_id in redraw {
self.handle_event(Event::WindowEvent {
@@ -399,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() {
@@ -416,17 +412,6 @@ impl ApplicationDelegate {
}
}
#[derive(Debug)]
pub(crate) enum QueuedEvent {
WindowEvent(WindowId, WindowEvent),
DeviceEvent(DeviceEvent),
ScaleFactorChanged {
window: Id<WinitWindow>,
suggested_size: PhysicalSize<u32>,
scale_factor: f64,
},
}
#[derive(Debug)]
pub(crate) struct HandlePendingUserEvents;

View File

@@ -2,9 +2,9 @@ use std::ffi::c_uchar;
use std::slice;
use std::sync::OnceLock;
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::runtime::Sel;
use objc2::{msg_send_id, sel, ClassType};
use objc2::{msg_send, msg_send_id, sel, ClassType};
use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
use objc2_foundation::{
ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize,
@@ -15,7 +15,7 @@ use crate::cursor::{CursorImage, OnlyCursorImageSource};
use crate::window::CursorIcon;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CustomCursor(pub(crate) Id<NSCursor>);
pub struct CustomCursor(pub(crate) Retained<NSCursor>);
// SAFETY: NSCursor is immutable and thread-safe
// TODO(madsmtm): Put this logic in objc2-app-kit itself
@@ -28,7 +28,7 @@ impl CustomCursor {
}
}
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Id<NSCursor> {
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Retained<NSCursor> {
let width = cursor.width;
let height = cursor.height;
@@ -60,14 +60,14 @@ pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Id<NSCursor> {
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
}
pub(crate) fn default_cursor() -> Id<NSCursor> {
pub(crate) fn default_cursor() -> Retained<NSCursor> {
NSCursor::arrowCursor()
}
unsafe fn try_cursor_from_selector(sel: Sel) -> Option<Id<NSCursor>> {
unsafe fn try_cursor_from_selector(sel: Sel) -> Option<Retained<NSCursor>> {
let cls = NSCursor::class();
if cls.responds_to(sel) {
let cursor: Id<NSCursor> = unsafe { msg_send_id![cls, performSelector: sel] };
if msg_send![cls, respondsToSelector: sel] {
let cursor: Retained<NSCursor> = unsafe { msg_send_id![cls, performSelector: sel] };
Some(cursor)
} else {
tracing::warn!("cursor `{sel}` appears to be invalid");
@@ -82,7 +82,7 @@ macro_rules! def_undocumented_cursor {
)*} => {$(
$(#[$($m)*])*
#[allow(non_snake_case)]
fn $name() -> Id<NSCursor> {
fn $name() -> Retained<NSCursor> {
unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) }
}
)*};
@@ -112,7 +112,7 @@ def_undocumented_cursor!(
// Note that loading `busybutclickable` with this code won't animate
// the frames; instead you'll just get them all in a column.
unsafe fn load_webkit_cursor(name: &NSString) -> Id<NSCursor> {
unsafe fn load_webkit_cursor(name: &NSString) -> Retained<NSCursor> {
// Snatch a cursor from WebKit; They fit the style of the native
// cursors, and will seem completely standard to macOS users.
//
@@ -128,7 +128,7 @@ unsafe fn load_webkit_cursor(name: &NSString) -> Id<NSCursor> {
// TODO: Handle PLists better
let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist"));
let info: Id<NSDictionary<NSObject, NSObject>> = unsafe {
let info: Retained<NSDictionary<NSObject, NSObject>> = unsafe {
msg_send_id![
<NSDictionary<NSObject, NSObject>>::class(),
dictionaryWithContentsOfFile: &*info_path,
@@ -155,15 +155,15 @@ unsafe fn load_webkit_cursor(name: &NSString) -> Id<NSCursor> {
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
}
fn webkit_move() -> Id<NSCursor> {
fn webkit_move() -> Retained<NSCursor> {
unsafe { load_webkit_cursor(ns_string!("move")) }
}
fn webkit_cell() -> Id<NSCursor> {
fn webkit_cell() -> Retained<NSCursor> {
unsafe { load_webkit_cursor(ns_string!("cell")) }
}
pub(crate) fn invisible_cursor() -> Id<NSCursor> {
pub(crate) fn invisible_cursor() -> Retained<NSCursor> {
// 16x16 GIF data for invisible cursor
// You can reproduce this via ImageMagick.
// $ convert -size 16x16 xc:none cursor.gif
@@ -174,7 +174,7 @@ pub(crate) fn invisible_cursor() -> Id<NSCursor> {
0xa3, 0x9c, 0xb4, 0xda, 0x8b, 0xb3, 0x3e, 0x05, 0x00, 0x3b,
];
fn new_invisible() -> Id<NSCursor> {
fn new_invisible() -> Retained<NSCursor> {
// TODO: Consider using `dataWithBytesNoCopy:`
let data = NSData::with_bytes(CURSOR_BYTES);
let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap();
@@ -187,7 +187,7 @@ pub(crate) fn invisible_cursor() -> Id<NSCursor> {
CURSOR.get_or_init(|| CustomCursor(new_invisible())).0.clone()
}
pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Id<NSCursor> {
pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained<NSCursor> {
match icon {
CursorIcon::Default => default_cursor(),
CursorIcon::Pointer => NSCursor::pointingHandCursor(),

View File

@@ -2,7 +2,7 @@ use std::ffi::c_void;
use core_foundation::base::CFRelease;
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType};
use objc2_foundation::{run_on_main, NSPoint};
use smol_str::SmolStr;
@@ -92,17 +92,12 @@ fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key {
/// Create `KeyEvent` for the given `NSEvent`.
///
/// This function shouldn't be called when the IME input is in process.
pub(crate) fn create_key_event(
ns_event: &NSEvent,
is_press: bool,
is_repeat: bool,
key_override: Option<PhysicalKey>,
) -> KeyEvent {
pub(crate) fn create_key_event(ns_event: &NSEvent, is_press: bool, is_repeat: bool) -> KeyEvent {
use ElementState::{Pressed, Released};
let state = if is_press { Pressed } else { Released };
let scancode = unsafe { ns_event.keyCode() };
let mut physical_key = key_override.unwrap_or_else(|| scancode_to_physicalkey(scancode as u32));
let mut physical_key = scancode_to_physicalkey(scancode as u32);
// NOTE: The logical key should heed both SHIFT and ALT if possible.
// For instance:
@@ -111,20 +106,15 @@ pub(crate) fn create_key_event(
// * Pressing CTRL SHIFT A: logical key should also be "A"
// This is not easy to tease out of `NSEvent`, but we do our best.
let text_with_all_modifiers: Option<SmolStr> = if key_override.is_some() {
let characters = unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default();
let text_with_all_modifiers = if characters.is_empty() {
None
} else {
let characters =
unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default();
if characters.is_empty() {
None
} else {
if matches!(physical_key, PhysicalKey::Unidentified(_)) {
// The key may be one of the funky function keys
physical_key = extra_function_key_to_code(scancode, &characters);
}
Some(SmolStr::new(characters))
if matches!(physical_key, PhysicalKey::Unidentified(_)) {
// The key may be one of the funky function keys
physical_key = extra_function_key_to_code(scancode, &characters);
}
Some(SmolStr::new(characters))
};
let key_from_code = code_to_key(physical_key, scancode);
@@ -133,12 +123,12 @@ pub(crate) fn create_key_event(
let key_without_modifiers = get_modifierless_char(scancode);
let modifiers = unsafe { ns_event.modifierFlags() };
let has_ctrl = flags_contains(modifiers, NSEventModifierFlags::NSEventModifierFlagControl);
let has_cmd = flags_contains(modifiers, NSEventModifierFlags::NSEventModifierFlagCommand);
let has_ctrl = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagControl);
let has_cmd = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagCommand);
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.
@@ -305,16 +295,12 @@ const NX_DEVICELALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x000000
const NX_DEVICERALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000040);
const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00002000);
pub(super) fn flags_contains(flags: NSEventModifierFlags, value: NSEventModifierFlags) -> bool {
flags.0 & value.0 == value.0
}
pub(super) fn lalt_pressed(event: &NSEvent) -> bool {
flags_contains(unsafe { event.modifierFlags() }, NX_DEVICELALTKEYMASK)
unsafe { event.modifierFlags() }.contains(NX_DEVICELALTKEYMASK)
}
pub(super) fn ralt_pressed(event: &NSEvent) -> bool {
flags_contains(unsafe { event.modifierFlags() }, NX_DEVICERALTKEYMASK)
unsafe { event.modifierFlags() }.contains(NX_DEVICERALTKEYMASK)
}
pub(super) fn event_mods(event: &NSEvent) -> Modifiers {
@@ -322,38 +308,33 @@ pub(super) fn event_mods(event: &NSEvent) -> Modifiers {
let mut state = ModifiersState::empty();
let mut pressed_mods = ModifiersKeys::empty();
state.set(
ModifiersState::SHIFT,
flags_contains(flags, NSEventModifierFlags::NSEventModifierFlagShift),
);
pressed_mods.set(ModifiersKeys::LSHIFT, flags_contains(flags, NX_DEVICELSHIFTKEYMASK));
pressed_mods.set(ModifiersKeys::RSHIFT, flags_contains(flags, NX_DEVICERSHIFTKEYMASK));
state
.set(ModifiersState::SHIFT, flags.contains(NSEventModifierFlags::NSEventModifierFlagShift));
pressed_mods.set(ModifiersKeys::LSHIFT, flags.contains(NX_DEVICELSHIFTKEYMASK));
pressed_mods.set(ModifiersKeys::RSHIFT, flags.contains(NX_DEVICERSHIFTKEYMASK));
state.set(
ModifiersState::CONTROL,
flags_contains(flags, NSEventModifierFlags::NSEventModifierFlagControl),
flags.contains(NSEventModifierFlags::NSEventModifierFlagControl),
);
pressed_mods.set(ModifiersKeys::LCONTROL, flags_contains(flags, NX_DEVICELCTLKEYMASK));
pressed_mods.set(ModifiersKeys::RCONTROL, flags_contains(flags, NX_DEVICERCTLKEYMASK));
pressed_mods.set(ModifiersKeys::LCONTROL, flags.contains(NX_DEVICELCTLKEYMASK));
pressed_mods.set(ModifiersKeys::RCONTROL, flags.contains(NX_DEVICERCTLKEYMASK));
state.set(
ModifiersState::ALT,
flags_contains(flags, NSEventModifierFlags::NSEventModifierFlagOption),
);
pressed_mods.set(ModifiersKeys::LALT, flags_contains(flags, NX_DEVICELALTKEYMASK));
pressed_mods.set(ModifiersKeys::RALT, flags_contains(flags, NX_DEVICERALTKEYMASK));
state.set(ModifiersState::ALT, flags.contains(NSEventModifierFlags::NSEventModifierFlagOption));
pressed_mods.set(ModifiersKeys::LALT, flags.contains(NX_DEVICELALTKEYMASK));
pressed_mods.set(ModifiersKeys::RALT, flags.contains(NX_DEVICERALTKEYMASK));
state.set(
ModifiersState::SUPER,
flags_contains(flags, NSEventModifierFlags::NSEventModifierFlagCommand),
flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand),
);
pressed_mods.set(ModifiersKeys::LSUPER, flags_contains(flags, NX_DEVICELCMDKEYMASK));
pressed_mods.set(ModifiersKeys::RSUPER, flags_contains(flags, NX_DEVICERCMDKEYMASK));
pressed_mods.set(ModifiersKeys::LSUPER, flags.contains(NX_DEVICELCMDKEYMASK));
pressed_mods.set(ModifiersKeys::RSUPER, flags.contains(NX_DEVICERCMDKEYMASK));
Modifiers { state, pressed_mods }
}
pub(super) fn dummy_event() -> Option<Id<NSEvent>> {
pub(super) fn dummy_event() -> Option<Retained<NSEvent>> {
unsafe {
NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
NSEventType::ApplicationDefined,

View File

@@ -1,7 +1,7 @@
use std::cell::RefCell;
use std::{fmt, mem};
use super::app_delegate::HandlePendingUserEvents;
use super::app_state::HandlePendingUserEvents;
use crate::event::Event;
use crate::event_loop::ActiveEventLoop as RootActiveEventLoop;
@@ -16,7 +16,7 @@ impl fmt::Debug for EventHandlerData {
}
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub(crate) struct EventHandler {
/// This can be in the following states:
/// - Not registered by the event loop (None).
@@ -26,6 +26,10 @@ pub(crate) struct EventHandler {
}
impl EventHandler {
pub(crate) const fn new() -> Self {
Self { inner: RefCell::new(None) }
}
/// Set the event loop handler for the duration of the given closure.
///
/// This is similar to using the `scoped-tls` or `scoped-tls-hkt` crates

View File

@@ -14,14 +14,14 @@ use core_foundation::runloop::{
kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext,
CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
};
use objc2::rc::{autoreleasepool, Id};
use objc2::rc::{autoreleasepool, Retained};
use objc2::runtime::ProtocolObject;
use objc2::{msg_send_id, ClassType};
use objc2::sel;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
use super::app::WinitApplication;
use super::app_delegate::{ApplicationDelegate, HandlePendingUserEvents};
use super::app::override_send_event;
use super::app_state::{ApplicationDelegate, HandlePendingUserEvents};
use super::event::dummy_event;
use super::monitor::{self, MonitorHandle};
use super::observer::setup_control_flow_observers;
@@ -33,7 +33,7 @@ use crate::event_loop::{
use crate::platform::macos::ActivationPolicy;
use crate::platform::pump_events::PumpStatus;
use crate::platform_impl::platform::cursor::CustomCursor;
use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource};
use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, Theme};
#[derive(Default)]
pub struct PanicInfo {
@@ -67,17 +67,21 @@ impl PanicInfo {
#[derive(Debug)]
pub struct ActiveEventLoop {
delegate: Id<ApplicationDelegate>,
delegate: Retained<ApplicationDelegate>,
pub(super) mtm: MainThreadMarker,
}
impl ActiveEventLoop {
pub(super) fn new_root(delegate: Id<ApplicationDelegate>) -> RootWindowTarget {
pub(super) fn new_root(delegate: Retained<ApplicationDelegate>) -> RootWindowTarget {
let mtm = MainThreadMarker::from(&*delegate);
let p = Self { delegate, mtm };
RootWindowTarget { p, _marker: PhantomData }
}
pub(super) fn app_delegate(&self) -> &ApplicationDelegate {
&self.delegate
}
pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor {
RootCustomCursor { inner: CustomCursor::new(source.inner) }
}
@@ -102,6 +106,17 @@ impl ActiveEventLoop {
rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty())
}
#[inline]
pub fn system_theme(&self) -> Option<Theme> {
let app = NSApplication::sharedApplication(self.mtm);
if app.respondsToSelector(sel!(effectiveAppearance)) {
Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance()))
} else {
Some(Theme::Light)
}
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(
@@ -133,9 +148,7 @@ impl ActiveEventLoop {
pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle {
OwnedDisplayHandle
}
}
impl ActiveEventLoop {
pub(crate) fn hide_application(&self) {
NSApplication::sharedApplication(self.mtm).hide(None)
}
@@ -172,12 +185,12 @@ pub struct EventLoop<T: 'static> {
///
/// We intentionally don't store `WinitApplication` since we want to have
/// the possibility of swapping that out at some point.
app: Id<NSApplication>,
app: Retained<NSApplication>,
/// The application delegate that we've registered.
///
/// The delegate is only weakly referenced by NSApplication, so we must
/// keep it around here as well.
delegate: Id<ApplicationDelegate>,
delegate: Retained<ApplicationDelegate>,
// Event sender and receiver, used for EventLoopProxy.
sender: mpsc::Sender<T>,
@@ -189,18 +202,14 @@ pub struct EventLoop<T: 'static> {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) struct PlatformSpecificEventLoopAttributes {
pub(crate) activation_policy: ActivationPolicy,
pub(crate) activation_policy: Option<ActivationPolicy>,
pub(crate) default_menu: bool,
pub(crate) activate_ignoring_other_apps: bool,
}
impl Default for PlatformSpecificEventLoopAttributes {
fn default() -> Self {
Self {
activation_policy: Default::default(), // Regular
default_menu: true,
activate_ignoring_other_apps: true,
}
Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true }
}
}
@@ -211,20 +220,14 @@ impl<T> EventLoop<T> {
let mtm = MainThreadMarker::new()
.expect("on macOS, `EventLoop` must be created on the main thread!");
let app: Id<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 {
ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular,
ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory,
ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited,
None => None,
Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular),
Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory),
Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited),
};
let delegate = ApplicationDelegate::new(
mtm,
@@ -237,8 +240,11 @@ 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(Rc::downgrade(&panic_info));
setup_control_flow_observers(mtm, Rc::downgrade(&panic_info));
let (sender, receiver) = mpsc::channel();
Ok(EventLoop {
@@ -411,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]
@@ -480,8 +502,7 @@ impl<T> EventLoopProxy<T> {
cancel: None,
perform: event_loop_proxy_handler,
};
let source =
CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::max_value() - 1, &mut context);
let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context);
CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes);
CFRunLoopWakeUp(rl);

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

@@ -1,4 +1,4 @@
use objc2::rc::Id;
use objc2::rc::Retained;
use objc2::runtime::Sel;
use objc2::sel;
use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem};
@@ -48,10 +48,10 @@ pub fn initialize(app: &NSApplication) {
Some(sel!(hideOtherApplications:)),
Some(KeyEquivalent {
key: ns_string!("h"),
masks: Some(NSEventModifierFlags(
NSEventModifierFlags::NSEventModifierFlagOption.0
| NSEventModifierFlags::NSEventModifierFlagCommand.0,
)),
masks: Some(
NSEventModifierFlags::NSEventModifierFlagOption
| NSEventModifierFlags::NSEventModifierFlagCommand,
),
}),
);
@@ -91,7 +91,7 @@ fn menu_item(
title: &NSString,
selector: Option<Sel>,
key_equivalent: Option<KeyEquivalent<'_>>,
) -> Id<NSMenuItem> {
) -> Retained<NSMenuItem> {
let (key, masks) = match key_equivalent {
Some(ke) => (ke.key, ke.masks),
None => (ns_string!(""), None),

View File

@@ -2,7 +2,7 @@
mod util;
mod app;
mod app_delegate;
mod app_state;
mod cursor;
mod event;
mod event_handler;
@@ -37,7 +37,7 @@ pub(crate) use crate::platform_impl::Fullscreen;
pub struct DeviceId;
impl DeviceId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
DeviceId
}
}

View File

@@ -6,13 +6,15 @@ use std::fmt;
use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex};
use core_foundation::base::{CFRelease, TCFType};
use core_foundation::string::CFString;
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUID};
use core_graphics::display::{
CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode,
};
use objc2::rc::Id;
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};
@@ -97,18 +99,71 @@ 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 uuid = unsafe { CFUUIDGetUUIDBytes(self.0.as_concrete_TypeRef()) };
MonitorUuid::from([
uuid.byte0,
uuid.byte1,
uuid.byte2,
uuid.byte3,
uuid.byte4,
uuid.byte5,
uuid.byte6,
uuid.byte7,
uuid.byte8,
uuid.byte9,
uuid.byte10,
uuid.byte11,
uuid.byte12,
uuid.byte13,
uuid.byte14,
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 {
unsafe {
ffi::CGDisplayCreateUUIDFromDisplayID(self.0)
== ffi::CGDisplayCreateUUIDFromDisplayID(other.0)
}
self.uuid() == other.uuid()
}
}
@@ -122,18 +177,13 @@ impl PartialOrd for MonitorHandle {
impl Ord for MonitorHandle {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
unsafe {
ffi::CGDisplayCreateUUIDFromDisplayID(self.0)
.cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0))
}
self.uuid().cmp(&other.uuid())
}
}
impl std::hash::Hash for MonitorHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
unsafe {
ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state);
}
self.uuid().hash(state);
}
}
@@ -141,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 {
@@ -150,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 {
@@ -167,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())
@@ -213,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;
@@ -243,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
};
@@ -295,14 +349,18 @@ impl MonitorHandle {
}
}
pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Id<NSScreen>> {
let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) };
pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Retained<NSScreen>> {
let uuid = self.uuid();
NSScreen::screens(mtm).into_iter().find(|screen| {
let other_native_id = get_display_id(screen);
let other_uuid = unsafe {
ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID)
};
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

@@ -1,21 +1,28 @@
//! Utilities for working with `CFRunLoop`.
//!
//! See Apple's documentation on Run Loops for details:
//! <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html>
use std::cell::Cell;
use std::ffi::c_void;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::ptr;
use std::rc::Weak;
use std::time::Instant;
use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease};
use block2::Block;
use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease, CFTypeRef};
use core_foundation::date::CFAbsoluteTimeGetCurrent;
use core_foundation::runloop::{
kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopExit,
CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain,
kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode,
kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain,
CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopObserverCreate,
CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate,
CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CFRunLoopWakeUp,
};
use objc2_foundation::MainThreadMarker;
use tracing::error;
use super::app_delegate::ApplicationDelegate;
use super::app_state::ApplicationDelegate;
use super::event_loop::{stop_app_on_panic, PanicInfo};
use super::ffi;
@@ -84,10 +91,20 @@ extern "C" fn control_flow_end_handler(
}
}
#[derive(Debug)]
pub struct RunLoop(CFRunLoopRef);
impl Default for RunLoop {
fn default() -> Self {
Self(ptr::null_mut())
}
}
impl RunLoop {
pub unsafe fn get() -> Self {
pub fn main(mtm: MainThreadMarker) -> Self {
// SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so
// scheduling (and scheduling a non-`Send` block) to that thread is allowed.
let _ = mtm;
RunLoop(unsafe { CFRunLoopGetMain() })
}
@@ -114,9 +131,79 @@ impl RunLoop {
};
unsafe { CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes) };
}
/// Submit a closure to run on the main thread as the next step in the run loop, before other
/// event sources are processed.
///
/// This is used for running event handlers, as those are not allowed to run re-entrantly.
///
/// # Implementation
///
/// This queuing could be implemented in the following several ways with subtle differences in
/// timing. This list is sorted in rough order in which they are run:
///
/// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`.
///
/// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the
/// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both
/// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop).
///
/// a. `atStart = true`.
///
/// b. `atStart = false`.
///
/// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not
/// respect the ordering that runloop events have.
///
/// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we
/// want the event to be queued in a way that preserves the order the events originally arrived
/// in.
///
/// As an example, let's assume that we receive two events from the user, a mouse click which we
/// handled by queuing it, and a window resize which we handled immediately. If we allowed
/// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of
/// the queue, and the events would appear out of order to the user of Winit. So we must instead
/// put the event at the very front of the queue, to be handled as soon as possible after
/// handling whatever event it's currently handling.
pub fn queue_closure(&self, closure: impl FnOnce() + 'static) {
extern "C" {
fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: &Block<dyn Fn()>);
}
// Convert `FnOnce()` to `Block<dyn Fn()>`.
let closure = Cell::new(Some(closure));
let block = block2::RcBlock::new(move || {
if let Some(closure) = closure.take() {
closure()
} else {
error!("tried to execute queued closure on main thread twice");
}
});
// There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa:
// - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`.
// - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window.
// - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop.
// - `NSConnectionReplyMode`: TODO.
//
// We only want to run event handlers in the default mode, as we support running a blocking
// modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and
// resizing such panel window enters the event tracking run loop mode, so we can't directly
// trigger events inside that mode either.
//
// Any events that are queued while running a modal or when live-resizing will instead wait,
// and be delivered to the application afterwards.
//
// [#1779]: https://github.com/rust-windowing/winit/issues/1779
let mode = unsafe { kCFRunLoopDefaultMode as CFTypeRef };
// SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`.
unsafe { CFRunLoopPerformBlock(self.0, mode, &block) }
}
}
pub fn setup_control_flow_observers(panic_info: Weak<PanicInfo>) {
pub fn setup_control_flow_observers(mtm: MainThreadMarker, panic_info: Weak<PanicInfo>) {
let run_loop = RunLoop::main(mtm);
unsafe {
let mut context = CFRunLoopObserverContext {
info: Weak::into_raw(panic_info) as *mut _,
@@ -125,16 +212,15 @@ pub fn setup_control_flow_observers(panic_info: Weak<PanicInfo>) {
release: None,
copyDescription: None,
};
let run_loop = RunLoop::get();
run_loop.add_observer(
kCFRunLoopAfterWaiting,
CFIndex::min_value(),
CFIndex::MIN,
control_flow_begin_handler,
&mut context as *mut _,
);
run_loop.add_observer(
kCFRunLoopExit | kCFRunLoopBeforeWaiting,
CFIndex::max_value(),
CFIndex::MAX,
control_flow_end_handler,
&mut context as *mut _,
);
@@ -165,8 +251,8 @@ impl Drop for EventLoopWaker {
}
}
impl Default for EventLoopWaker {
fn default() -> EventLoopWaker {
impl EventLoopWaker {
pub(crate) fn new() -> Self {
extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {}
unsafe {
// Create a timer with a 0.1µs interval (1ns does not work) to mimic polling.
@@ -174,7 +260,7 @@ impl Default for EventLoopWaker {
// future, but that gets changed to fire immediately in did_finish_launching
let timer = CFRunLoopTimerCreate(
ptr::null_mut(),
std::f64::MAX,
f64::MAX,
0.000_000_1,
0,
0,
@@ -182,23 +268,21 @@ impl Default for EventLoopWaker {
ptr::null_mut(),
);
CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes);
EventLoopWaker { timer, start_instant: Instant::now(), next_fire_date: None }
Self { timer, start_instant: Instant::now(), next_fire_date: None }
}
}
}
impl EventLoopWaker {
pub fn stop(&mut self) {
if self.next_fire_date.is_some() {
self.next_fire_date = None;
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) }
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) }
}
}
pub fn start(&mut self) {
if self.next_fire_date != Some(self.start_instant) {
self.next_fire_date = Some(self.start_instant);
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) }
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) }
}
}

View File

@@ -1,8 +1,5 @@
use objc2_foundation::{NSNotFound, NSRange, NSUInteger};
use tracing::trace;
pub static EMPTY_RANGE: NSRange = NSRange { location: NSNotFound as NSUInteger, length: 0 };
macro_rules! trace_scope {
($s:literal) => {
let _crate = $crate::platform_impl::platform::util::TraceGuard::new(module_path!(), $s);

View File

@@ -3,32 +3,30 @@ use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::ptr;
use objc2::rc::{Id, WeakId};
use objc2::rc::{Retained, WeakId};
use objc2::runtime::{AnyObject, Sel};
use objc2::{
class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
};
use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient,
NSTrackingRectTag, NSView,
NSTrackingRectTag, NSView, NSViewFrameDidChangeNotification,
};
use objc2_foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying,
NSMutableAttributedString, NSObject, NSObjectProtocol, NSPoint, NSRange, NSRect, NSSize,
NSString, NSUInteger,
NSMutableAttributedString, NSNotFound, NSNotificationCenter, NSObject, NSObjectProtocol,
NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
};
use super::app_delegate::ApplicationDelegate;
use super::app_state::ApplicationDelegate;
use super::cursor::{default_cursor, invisible_cursor};
use super::event::{
code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed,
scancode_to_physicalkey,
scancode_to_physicalkey, KeyEventExtra,
};
use super::window::WinitWindow;
use super::{util, DEVICE_ID};
use super::DEVICE_ID;
use crate::dpi::{LogicalPosition, LogicalSize};
use crate::event::{
DeviceEvent, ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
WindowEvent,
};
use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey};
@@ -37,7 +35,7 @@ use crate::platform::macos::OptionAsAlt;
#[derive(Debug)]
struct CursorState {
visible: bool,
cursor: Id<NSCursor>,
cursor: Retained<NSCursor>,
}
impl Default for CursorState {
@@ -110,8 +108,11 @@ fn get_left_modifier_code(key: &Key) -> KeyCode {
}
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ViewState {
/// Strong reference to the global application state.
app_delegate: Retained<ApplicationDelegate>,
cursor_state: RefCell<CursorState>,
ime_position: Cell<NSPoint>,
ime_size: Cell<NSSize>,
@@ -130,7 +131,7 @@ pub struct ViewState {
/// to the application, even during IME
forward_key_to_app: Cell<bool>,
marked_text: RefCell<Id<NSMutableAttributedString>>,
marked_text: RefCell<Retained<NSMutableAttributedString>>,
accepts_first_mouse: bool,
// Weak reference because the window keeps a strong reference to the view
@@ -199,19 +200,15 @@ declare_class!(
}
#[method(drawRect:)]
fn draw_rect(&self, rect: NSRect) {
fn draw_rect(&self, _rect: NSRect) {
trace_scope!("drawRect:");
// It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`.
if let Some(window) = self.ivars()._ns_window.load() {
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.handle_redraw(window.id());
self.ivars().app_delegate.handle_redraw(window.id());
}
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![super(self), drawRect: rect];
}
// This is a direct subclass of NSView, no need to call superclass' drawRect:
}
#[method(acceptsFirstResponder)]
@@ -224,7 +221,7 @@ declare_class!(
// IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem
// TODO: Add an API extension for using `NSTouchBar`
#[method_id(touchBar)]
fn touch_bar(&self) -> Option<Id<NSObject>> {
fn touch_bar(&self) -> Option<Retained<NSObject>> {
trace_scope!("touchBar");
None
}
@@ -257,33 +254,36 @@ declare_class!(
if length > 0 {
NSRange::new(0, length)
} else {
util::EMPTY_RANGE
// Documented to return `{NSNotFound, 0}` if there is no marked range.
NSRange::new(NSNotFound as NSUInteger, 0)
}
}
#[method(selectedRange)]
fn selected_range(&self) -> NSRange {
trace_scope!("selectedRange");
util::EMPTY_RANGE
// Documented to return `{NSNotFound, 0}` if there is no selection.
NSRange::new(NSNotFound as NSUInteger, 0)
}
#[method(setMarkedText:selectedRange:replacementRange:)]
fn set_marked_text(
&self,
string: &NSObject,
_selected_range: NSRange,
selected_range: NSRange,
_replacement_range: NSRange,
) {
// TODO: Use _replacement_range, requires changing the event to report surrounding text.
trace_scope!("setMarkedText:selectedRange:replacementRange:");
// SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`.
let (marked_text, preedit_string) = if string.is_kind_of::<NSAttributedString>() {
let (marked_text, string) = if string.is_kind_of::<NSAttributedString>() {
let string: *const NSObject = string;
let string: *const NSAttributedString = string.cast();
let string = unsafe { &*string };
(
NSMutableAttributedString::from_attributed_nsstring(string),
string.string().to_string(),
string.string(),
)
} else {
let string: *const NSObject = string;
@@ -291,7 +291,7 @@ declare_class!(
let string = unsafe { &*string };
(
NSMutableAttributedString::from_nsstring(string),
string.to_string(),
string.copy(),
)
};
@@ -311,16 +311,27 @@ declare_class!(
self.ivars().ime_state.set(ImeState::Ground);
}
// Empty string basically means that there's no preedit, so indicate that by sending
// `None` cursor range.
let cursor_range = if preedit_string.is_empty() {
let cursor_range = if string.is_empty() {
// An empty string basically means that there's no preedit, so indicate that by
// sending a `None` cursor range.
None
} else {
Some((preedit_string.len(), preedit_string.len()))
// 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(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))
};
// Send WindowEvent for updating marked text
self.queue_event(WindowEvent::Ime(Ime::Preedit(preedit_string, cursor_range)));
self.queue_event(WindowEvent::Ime(Ime::Preedit(string.to_string(), cursor_range)));
}
#[method(unmarkText)]
@@ -341,7 +352,7 @@ declare_class!(
}
#[method_id(validAttributesForMarkedText)]
fn valid_attributes_for_marked_text(&self) -> Id<NSArray<NSAttributedStringKey>> {
fn valid_attributes_for_marked_text(&self) -> Retained<NSArray<NSAttributedStringKey>> {
trace_scope!("validAttributesForMarkedText");
NSArray::new()
}
@@ -351,7 +362,7 @@ declare_class!(
&self,
_range: NSRange,
_actual_range: *mut NSRange,
) -> Option<Id<NSAttributedString>> {
) -> Option<Retained<NSAttributedString>> {
trace_scope!("attributedSubstringForProposedRange:actualRange:");
None
}
@@ -380,6 +391,7 @@ declare_class!(
#[method(insertText:replacementRange:)]
fn insert_text(&self, string: &NSObject, _replacement_range: NSRange) {
// TODO: Use _replacement_range, requires changing the event to report surrounding text.
trace_scope!("insertText:replacementRange:");
// SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`.
@@ -393,7 +405,7 @@ declare_class!(
unsafe { &*string }.to_string()
};
let is_control = string.chars().next().map_or(false, |c| c.is_control());
let is_control = string.chars().next().is_some_and(|c| c.is_control());
// Commit only if we have marked text.
if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control {
@@ -476,7 +488,7 @@ declare_class!(
};
if !had_ime_input || self.ivars().forward_key_to_app.get() {
let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }, None);
let key_event = create_key_event(&event, true, unsafe { event.isARepeat() });
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: key_event,
@@ -499,7 +511,7 @@ declare_class!(
) {
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: create_key_event(&event, false, false, None),
event: create_key_event(&event, false, false),
is_synthetic: false,
});
}
@@ -546,7 +558,7 @@ declare_class!(
.expect("could not find current event");
self.update_modifiers(&event, false);
let event = create_key_event(&event, true, unsafe { event.isARepeat() }, None);
let event = create_key_event(&event, true, unsafe { event.isARepeat() });
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
@@ -680,7 +692,7 @@ declare_class!(
self.update_modifiers(event, false);
self.queue_device_event(DeviceEvent::MouseWheel { delta });
self.ivars().app_delegate.maybe_queue_device_event(DeviceEvent::MouseWheel { delta });
self.queue_event(WindowEvent::MouseWheel {
device_id: DEVICE_ID,
delta,
@@ -773,34 +785,40 @@ declare_class!(
impl WinitView {
pub(super) fn new(
app_delegate: &ApplicationDelegate,
window: &WinitWindow,
accepts_first_mouse: bool,
option_as_alt: OptionAsAlt,
) -> Id<Self> {
) -> Retained<Self> {
let mtm = MainThreadMarker::from(window);
let this = mtm.alloc().set_ivars(ViewState {
app_delegate: app_delegate.retain(),
cursor_state: Default::default(),
ime_position: Default::default(),
ime_size: Default::default(),
modifiers: Default::default(),
phys_modifiers: Default::default(),
tracking_rect: Default::default(),
ime_state: Default::default(),
input_source: Default::default(),
ime_allowed: Default::default(),
forward_key_to_app: Default::default(),
marked_text: Default::default(),
accepts_first_mouse,
_ns_window: WeakId::new(&window.retain()),
option_as_alt: Cell::new(option_as_alt),
..Default::default()
});
let this: Id<Self> = unsafe { msg_send_id![super(this), init] };
let this: Retained<Self> = unsafe { msg_send_id![super(this), init] };
this.setPostsFrameChangedNotifications(true);
let notification_center: &AnyObject =
unsafe { msg_send![class!(NSNotificationCenter), defaultCenter] };
// About frame change
let frame_did_change_notification_name =
NSString::from_str("NSViewFrameDidChangeNotification");
#[allow(clippy::let_unit_value)]
let notification_center = unsafe { NSNotificationCenter::defaultCenter() };
unsafe {
let _: () = msg_send![
notification_center,
addObserver: &*this,
selector: sel!(frameDidChange:),
name: &*frame_did_change_notification_name,
object: &*this,
];
notification_center.addObserver_selector_name_object(
&this,
sel!(frameDidChange:),
Some(NSViewFrameDidChangeNotification),
Some(&this),
)
}
*this.ivars().input_source.borrow_mut() = this.current_input_source();
@@ -808,7 +826,7 @@ impl WinitView {
this
}
fn window(&self) -> Id<WinitWindow> {
fn window(&self) -> Retained<WinitWindow> {
// TODO: Simply use `window` property on `NSView`.
// That only returns a window _after_ the view has been attached though!
// (which is incompatible with `frameDidChange:`)
@@ -818,13 +836,7 @@ impl WinitView {
}
fn queue_event(&self, event: WindowEvent) {
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.queue_window_event(self.window().id(), event);
}
fn queue_device_event(&self, event: DeviceEvent) {
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.queue_device_event(event);
self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event);
}
fn scale_factor(&self) -> f64 {
@@ -843,11 +855,11 @@ impl WinitView {
.unwrap_or_default()
}
pub(super) fn cursor_icon(&self) -> Id<NSCursor> {
pub(super) fn cursor_icon(&self) -> Retained<NSCursor> {
self.ivars().cursor_state.borrow().cursor.clone()
}
pub(super) fn set_cursor_icon(&self, icon: Id<NSCursor>) {
pub(super) fn set_cursor_icon(&self, icon: Retained<NSCursor>) {
let mut cursor_state = self.ivars().cursor_state.borrow_mut();
cursor_state.cursor = icon;
}
@@ -927,22 +939,36 @@ impl WinitView {
let scancode = unsafe { ns_event.keyCode() };
let physical_key = scancode_to_physicalkey(scancode as u32);
// We'll correct the `is_press` later.
let mut event = create_key_event(ns_event, false, false, Some(physical_key));
let key = code_to_key(physical_key, scancode);
let logical_key = code_to_key(physical_key, scancode);
// Ignore processing of unknown modifiers because we can't determine whether
// it was pressed or release reliably.
let Some(event_modifier) = key_to_modifier(&key) else {
//
// Furthermore, sometimes normal keys are reported inside flagsChanged:, such as
// when holding Caps Lock while pressing another key, see:
// https://github.com/alacritty/alacritty/issues/8268
let Some(event_modifier) = key_to_modifier(&logical_key) else {
break 'send_event;
};
event.physical_key = physical_key;
event.logical_key = key.clone();
event.location = code_to_location(physical_key);
let mut event = KeyEvent {
location: code_to_location(physical_key),
logical_key: logical_key.clone(),
physical_key,
repeat: false,
// We'll correct this later.
state: Pressed,
text: None,
platform_specific: KeyEventExtra {
text_with_all_modifiers: None,
key_without_modifiers: logical_key.clone(),
},
};
let location_mask = ModLocationMask::from_location(event.location);
let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut();
let phys_mod = phys_mod_state.entry(key).or_insert(ModLocationMask::empty());
let phys_mod =
phys_mod_state.entry(logical_key).or_insert(ModLocationMask::empty());
let is_active = current_modifiers.state().contains(event_modifier);
let mut events = VecDeque::with_capacity(2);
@@ -1080,7 +1106,7 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
// NOTE: to get option as alt working we need to rewrite events
// we're getting from the operating system, which makes it
// impossible to provide such events as extra in `KeyEvent`.
fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id<NSEvent> {
fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained<NSEvent> {
let ev_mods = event_mods(event).state;
let ignore_alt_characters = match option_as_alt {
OptionAsAlt::OnlyLeft if lalt_pressed(event) => true,

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unnecessary_cast)]
use objc2::rc::{autoreleasepool, Id};
use objc2::rc::{autoreleasepool, Retained};
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{NSResponder, NSWindow};
use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject};
@@ -11,9 +11,9 @@ use crate::error::OsError as RootOsError;
use crate::window::WindowAttributes;
pub(crate) struct Window {
window: MainThreadBound<Id<WinitWindow>>,
window: MainThreadBound<Retained<WinitWindow>>,
/// The window only keeps a weak reference to this, so we must keep it around here.
delegate: MainThreadBound<Id<WindowDelegate>>,
delegate: MainThreadBound<Retained<WindowDelegate>>,
}
impl Drop for Window {
@@ -28,7 +28,9 @@ impl Window {
attributes: WindowAttributes,
) -> Result<Self, RootOsError> {
let mtm = window_target.mtm;
let delegate = autoreleasepool(|_| WindowDelegate::new(attributes, mtm))?;
let delegate = autoreleasepool(|_| {
WindowDelegate::new(window_target.app_delegate(), attributes, mtm)
})?;
Ok(Window {
window: MainThreadBound::new(delegate.window().retain(), mtm),
delegate: MainThreadBound::new(delegate, mtm),
@@ -72,7 +74,7 @@ impl Window {
pub struct WindowId(pub usize);
impl WindowId {
pub const unsafe fn dummy() -> Self {
pub const fn dummy() -> Self {
Self(0)
}
}

View File

@@ -1,37 +1,41 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::ffi::c_void;
use std::ptr;
use std::sync::{Arc, Mutex};
use core_graphics::display::{CGDisplay, CGPoint};
use monitor::VideoModeHandle;
use objc2::rc::{autoreleasepool, Id};
use objc2::rc::{autoreleasepool, Retained};
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2::{
class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
};
use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass};
use objc2_app_kit::{
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication,
NSApplicationPresentationOptions, NSBackingStoreType, NSDraggingDestination,
NSFilenamesPboardType, NSPasteboard, NSRequestUserAttentionType, NSScreen, NSView,
NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel,
NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
NSWindowTabbingMode, NSWindowTitleVisibility,
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization,
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard,
NSRequestUserAttentionType, NSScreen, NSView, NSWindowButton, NSWindowDelegate,
NSWindowFullScreenButton, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility,
};
use objc2_foundation::{
CGFloat, MainThreadMarker, NSArray, NSCopying, NSObject, NSObjectProtocol, NSPoint, NSRect,
NSSize, NSString,
ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSKeyValueChangeKey,
NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSObject,
NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint,
NSRect, NSSize, NSString,
};
use tracing::{trace, warn};
use super::app_delegate::ApplicationDelegate;
use super::app_state::ApplicationDelegate;
use super::cursor::cursor_from_icon;
use super::monitor::{self, flip_window_screen_coordinates, get_display_id};
use super::observer::RunLoop;
use super::view::WinitView;
use super::window::WinitWindow;
use super::{ffi, Fullscreen, MonitorHandle, OsError, WindowId};
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError};
use crate::event::WindowEvent;
use crate::event::{InnerSizeWriter, WindowEvent};
use crate::platform::macos::{OptionAsAlt, WindowExtMacOS};
use crate::window::{
Cursor, CursorGrabMode, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -51,6 +55,7 @@ pub struct PlatformSpecificWindowAttributes {
pub accepts_first_mouse: bool,
pub tabbing_identifier: Option<String>,
pub option_as_alt: OptionAsAlt,
pub borderless_game: bool,
}
impl Default for PlatformSpecificWindowAttributes {
@@ -68,20 +73,22 @@ impl Default for PlatformSpecificWindowAttributes {
accepts_first_mouse: true,
tabbing_identifier: None,
option_as_alt: Default::default(),
borderless_game: false,
}
}
}
#[derive(Debug)]
pub(crate) struct State {
window: Id<WinitWindow>,
/// Strong reference to the global application state.
app_delegate: Retained<ApplicationDelegate>,
current_theme: Cell<Option<Theme>>,
window: Retained<WinitWindow>,
// During `windowDidResize`, we use this to only send Moved if the position changed.
//
// This is expressed in native screen coordinates.
previous_position: Cell<Option<NSPoint>>,
// This is expressed in desktop coordinates, and flipped to match Winit's coordinate system.
previous_position: Cell<NSPoint>,
// Used to prevent redundant events.
previous_scale_factor: Cell<f64>,
@@ -115,6 +122,7 @@ pub(crate) struct State {
standard_frame: Cell<Option<NSRect>>,
is_simple_fullscreen: Cell<bool>,
saved_style: Cell<Option<NSWindowStyleMask>>,
is_borderless_game: Cell<bool>,
}
declare_class!(
@@ -183,7 +191,17 @@ declare_class!(
#[method(windowDidChangeBackingProperties:)]
fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeBackingProperties:");
self.queue_static_scale_factor_changed_event();
let scale_factor = self.scale_factor();
if scale_factor == self.ivars().previous_scale_factor.get() {
return;
};
self.ivars().previous_scale_factor.set(scale_factor);
let mtm = MainThreadMarker::from(self);
let this = self.retain();
RunLoop::main(mtm).queue_closure(move || {
this.handle_scale_factor_changed(scale_factor);
});
}
#[method(windowDidBecomeKey:)]
@@ -230,7 +248,7 @@ declare_class!(
None => {
let current_monitor = self.current_monitor_inner();
*fullscreen = Some(Fullscreen::Borderless(current_monitor));
}
},
}
self.ivars().in_fullscreen_transition.set(true);
}
@@ -261,11 +279,9 @@ declare_class!(
let mut options = proposed_options;
let fullscreen = self.ivars().fullscreen.borrow();
if let Some(Fullscreen::Exclusive(_)) = &*fullscreen {
options = NSApplicationPresentationOptions(
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen.0
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock.0
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar.0,
);
options = NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar;
}
options
@@ -316,14 +332,12 @@ declare_class!(
self.ivars().in_fullscreen_transition.set(false);
self.ivars().target_fullscreen.replace(None);
if self.ivars().initial_fullscreen.get() {
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![
self.window(),
performSelector: sel!(toggleFullScreen:),
withObject: ptr::null::<AnyObject>(),
afterDelay: 0.5,
];
self.window().performSelector_withObject_afterDelay(
sel!(toggleFullScreen:),
None,
0.5,
)
};
} else {
self.restore_state_from_fullscreen();
@@ -334,8 +348,7 @@ declare_class!(
#[method(windowDidChangeOcclusionState:)]
fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeOcclusionState:");
let visible = self.window().occlusionState().0 & NSWindowOcclusionState::Visible.0
== NSWindowOcclusionState::Visible.0;
let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible);
self.queue_event(WindowEvent::Occluded(!visible));
}
@@ -359,11 +372,12 @@ declare_class!(
use std::path::PathBuf;
let pb: Id<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb
.propertyListForType(unsafe { NSFilenamesPboardType })
.unwrap();
let filenames: Id<NSArray<NSString>> = unsafe { Id::cast(filenames) };
let pb: Retained<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
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| {
let path = PathBuf::from(file.to_string());
@@ -387,11 +401,12 @@ declare_class!(
use std::path::PathBuf;
let pb: Id<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb
.propertyListForType(unsafe { NSFilenamesPboardType })
.unwrap();
let filenames: Id<NSArray<NSString>> = unsafe { Id::cast(filenames) };
let pb: Retained<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
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| {
let path = PathBuf::from(file.to_string());
@@ -415,34 +430,71 @@ declare_class!(
}
}
// Key-Value Observing
unsafe impl WindowDelegate {
// Observe theme change
#[method(effectiveAppearanceDidChange:)]
fn effective_appearance_did_change(this: *mut Self, sender: Option<&AnyObject>) {
trace_scope!("effectiveAppearanceDidChange:");
unsafe {
msg_send![
this,
performSelectorOnMainThread: sel!(effectiveAppearanceDidChangedOnMainThread:),
withObject: sender,
waitUntilDone: false,
]
}
}
#[method(observeValueForKeyPath:ofObject:change:context:)]
fn observe_value(
&self,
key_path: Option<&NSString>,
_object: Option<&AnyObject>,
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
trace_scope!("observeValueForKeyPath:ofObject:change:context:");
// NOTE: We don't _really_ need to check the key path, as there should only be one, but
// in the future we might want to observe other key paths.
if key_path == Some(ns_string!("effectiveAppearance")) {
let change = change.expect("requested a change dictionary in `addObserver`, but none was provided");
let old = change.get(unsafe { NSKeyValueChangeOldKey }).expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`");
let new = change.get(unsafe { NSKeyValueChangeNewKey }).expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`");
#[method(effectiveAppearanceDidChangedOnMainThread:)]
fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) {
let mtm = MainThreadMarker::from(self);
let theme = get_ns_theme(mtm);
let old_theme = self.ivars().current_theme.replace(Some(theme));
if old_theme != Some(theme) {
self.queue_event(WindowEvent::ThemeChanged(theme));
// SAFETY: The value of `effectiveAppearance` is `NSAppearance`
let old: *const AnyObject = old;
let old: *const NSAppearance = old.cast();
let old: &NSAppearance = unsafe { &*old };
let new: *const AnyObject = new;
let new: *const NSAppearance = new.cast();
let new: &NSAppearance = unsafe { &*new };
trace!(old = %unsafe { old.name() }, new = %unsafe { new.name() }, "effectiveAppearance changed");
// Ignore the change if the window's theme is customized by the user (since in that
// case the `effectiveAppearance` is only emitted upon said customization, and then
// it's triggered directly by a user action, and we don't want to emit the event).
if unsafe { self.window().appearance() }.is_some() {
return;
}
let old = appearance_to_theme(old);
let new = appearance_to_theme(new);
// Check that the theme changed in Winit's terms (the theme might have changed on
// other parameters, such as level of contrast, but the event should not be emitted
// in those cases).
if old == new {
return;
}
self.queue_event(WindowEvent::ThemeChanged(new));
} else {
panic!("unknown observed keypath {key_path:?}");
}
}
}
);
fn new_window(attrs: &WindowAttributes, mtm: MainThreadMarker) -> Option<Id<WinitWindow>> {
impl Drop for WindowDelegate {
fn drop(&mut self) {
unsafe {
self.window().removeObserver_forKeyPath(self, ns_string!("effectiveAppearance"));
}
}
}
fn new_window(
app_delegate: &ApplicationDelegate,
attrs: &WindowAttributes,
mtm: MainThreadMarker,
) -> Option<Retained<WinitWindow>> {
autoreleasepool(|_| {
let screen = match attrs.fullscreen.clone().map(Into::into) {
Some(Fullscreen::Borderless(Some(monitor)))
@@ -487,38 +539,38 @@ fn new_window(attrs: &WindowAttributes, mtm: MainThreadMarker) -> Option<Id<Wini
// if decorations is set to false, ignore pl_attrs
//
// if the titlebar is hidden, ignore other pl_attrs
NSWindowStyleMask::Borderless.0
| NSWindowStyleMask::Resizable.0
| NSWindowStyleMask::Miniaturizable.0
NSWindowStyleMask::Borderless
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Miniaturizable
} else {
// default case, resizable window with titlebar and titlebar buttons
NSWindowStyleMask::Closable.0
| NSWindowStyleMask::Miniaturizable.0
| NSWindowStyleMask::Resizable.0
| NSWindowStyleMask::Titled.0
NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Titled
};
if !attrs.resizable {
masks &= !NSWindowStyleMask::Resizable.0;
masks &= !NSWindowStyleMask::Resizable;
}
if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) {
masks &= !NSWindowStyleMask::Miniaturizable.0;
masks &= !NSWindowStyleMask::Miniaturizable;
}
if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) {
masks &= !NSWindowStyleMask::Closable.0;
masks &= !NSWindowStyleMask::Closable;
}
if attrs.platform_specific.fullsize_content_view {
masks |= NSWindowStyleMask::FullSizeContentView.0;
masks |= NSWindowStyleMask::FullSizeContentView;
}
let window: Option<Id<WinitWindow>> = unsafe {
let window: Option<Retained<WinitWindow>> = unsafe {
msg_send_id![
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame,
styleMask: NSWindowStyleMask(masks),
styleMask: masks,
backing: NSBackingStoreType::NSBackingStoreBuffered,
defer: false,
]
@@ -579,6 +631,7 @@ fn new_window(attrs: &WindowAttributes, mtm: MainThreadMarker) -> Option<Id<Wini
}
let view = WinitView::new(
app_delegate,
&window,
attrs.platform_specific.accepts_first_mouse,
attrs.platform_specific.option_as_alt,
@@ -605,6 +658,8 @@ fn new_window(attrs: &WindowAttributes, mtm: MainThreadMarker) -> Option<Id<Wini
if attrs.transparent {
window.setOpaque(false);
// See `set_transparent` for details on why we do this.
window.setBackgroundColor(unsafe { Some(&NSColor::clearColor()) });
}
// register for drag and drop operations.
@@ -618,8 +673,12 @@ fn new_window(attrs: &WindowAttributes, mtm: MainThreadMarker) -> Option<Id<Wini
}
impl WindowDelegate {
pub fn new(attrs: WindowAttributes, mtm: MainThreadMarker) -> Result<Id<Self>, RootOsError> {
let window = new_window(&attrs, mtm)
pub(super) fn new(
app_delegate: &ApplicationDelegate,
attrs: WindowAttributes,
mtm: MainThreadMarker,
) -> Result<Retained<Self>, RootOsError> {
let window = new_window(app_delegate, &attrs, mtm)
.ok_or_else(|| os_error!(OsError::CreationError("couldn't create `NSWindow`")))?;
#[cfg(feature = "rwh_06")]
@@ -627,8 +686,8 @@ impl WindowDelegate {
Some(rwh_06::RawWindowHandle::AppKit(handle)) => {
// SAFETY: Caller ensures the pointer is valid or NULL
// Unwrap is fine, since the pointer comes from `NonNull`.
let parent_view: Id<NSView> =
unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap();
let parent_view: Retained<NSView> =
unsafe { Retained::retain(handle.ns_view.as_ptr().cast()) }.unwrap();
let parent = parent_view.window().ok_or_else(|| {
os_error!(OsError::CreationError("parent view should be installed in a window"))
})?;
@@ -654,15 +713,14 @@ impl WindowDelegate {
let scale_factor = window.backingScaleFactor() as _;
let current_theme = match attrs.preferred_theme {
Some(theme) => Some(theme),
None => Some(get_ns_theme(mtm)),
};
if let Some(appearance) = theme_to_appearance(attrs.preferred_theme) {
unsafe { window.setAppearance(Some(&appearance)) };
}
let delegate = mtm.alloc().set_ivars(State {
app_delegate: app_delegate.retain(),
window: window.retain(),
current_theme: Cell::new(current_theme),
previous_position: Cell::new(None),
previous_position: Cell::new(flip_window_screen_coordinates(window.frame())),
previous_scale_factor: Cell::new(scale_factor),
resize_increments: Cell::new(resize_increments),
decorations: Cell::new(attrs.decorations),
@@ -676,26 +734,29 @@ impl WindowDelegate {
standard_frame: Cell::new(None),
is_simple_fullscreen: Cell::new(false),
saved_style: Cell::new(None),
is_borderless_game: Cell::new(attrs.platform_specific.borderless_game),
});
let delegate: Id<WindowDelegate> = unsafe { msg_send_id![super(delegate), init] };
let delegate: Retained<WindowDelegate> = unsafe { msg_send_id![super(delegate), init] };
if scale_factor != 1.0 {
delegate.queue_static_scale_factor_changed_event();
let delegate = delegate.clone();
RunLoop::main(mtm).queue_closure(move || {
delegate.handle_scale_factor_changed(scale_factor);
});
}
window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
// Enable theme change event
let notification_center: Id<AnyObject> =
unsafe { msg_send_id![class!(NSDistributedNotificationCenter), defaultCenter] };
let notification_name = NSString::from_str("AppleInterfaceThemeChangedNotification");
let _: () = unsafe {
msg_send![
&notification_center,
addObserver: &*delegate,
selector: sel!(effectiveAppearanceDidChange:),
name: &*notification_name,
object: ptr::null::<AnyObject>(),
]
// Listen for theme change event.
//
// SAFETY: The observer is un-registered in the `Drop` of the delegate.
unsafe {
window.addObserver_forKeyPath_options_context(
&delegate,
ns_string!("effectiveAppearance"),
NSKeyValueObservingOptions::NSKeyValueObservingOptionNew
| NSKeyValueObservingOptions::NSKeyValueObservingOptionOld,
ptr::null_mut(),
)
};
if attrs.blur {
@@ -740,9 +801,9 @@ impl WindowDelegate {
}
#[track_caller]
pub(super) fn view(&self) -> Id<WinitView> {
pub(super) fn view(&self) -> Retained<WinitView> {
// SAFETY: The view inside WinitWindow is always `WinitView`
unsafe { Id::cast(self.window().contentView().unwrap()) }
unsafe { Retained::cast(self.window().contentView().unwrap()) }
}
#[track_caller]
@@ -756,36 +817,40 @@ impl WindowDelegate {
}
pub(crate) fn queue_event(&self, event: WindowEvent) {
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.queue_window_event(self.window().id(), event);
self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event);
}
fn queue_static_scale_factor_changed_event(&self) {
let scale_factor = self.scale_factor();
if scale_factor == self.ivars().previous_scale_factor.get() {
return;
};
fn handle_scale_factor_changed(&self, scale_factor: CGFloat) {
let app_delegate = &self.ivars().app_delegate;
let window = self.window();
self.ivars().previous_scale_factor.set(scale_factor);
let content_size = self.window().contentRectForFrameRect(self.window().frame()).size;
let content_size = window.contentRectForFrameRect(window.frame()).size;
let content_size = LogicalSize::new(content_size.width, content_size.height);
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.queue_static_scale_factor_changed_event(
self.window().retain(),
content_size.to_physical(scale_factor),
let suggested_size = content_size.to_physical(scale_factor);
let new_inner_size = Arc::new(Mutex::new(suggested_size));
app_delegate.handle_window_event(window.id(), WindowEvent::ScaleFactorChanged {
scale_factor,
);
inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)),
});
let physical_size = *new_inner_size.lock().unwrap();
drop(new_inner_size);
if physical_size != suggested_size {
let logical_size = physical_size.to_logical(scale_factor);
let size = NSSize::new(logical_size.width, logical_size.height);
window.setContentSize(size);
}
app_delegate.handle_window_event(window.id(), WindowEvent::Resized(physical_size));
}
fn emit_move_event(&self) {
let frame = self.window().frame();
if self.ivars().previous_position.get() == Some(frame.origin) {
let position = flip_window_screen_coordinates(self.window().frame());
if self.ivars().previous_position.get() == position {
return;
}
self.ivars().previous_position.set(Some(frame.origin));
self.ivars().previous_position.set(position);
let position = flip_window_screen_coordinates(frame);
let position =
LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor());
self.queue_event(WindowEvent::Moved(position));
@@ -803,7 +868,23 @@ impl WindowDelegate {
}
pub fn set_transparent(&self, transparent: bool) {
self.window().setOpaque(!transparent)
// This is just a hint for Quartz, it doesn't actually speculate with window alpha.
// Providing a wrong value here could result in visual artifacts, when the window is
// transparent.
self.window().setOpaque(!transparent);
// AppKit draws the window with a background color by default, which is usually really
// nice, but gets in the way when we want to allow the contents of the window to be
// transparent, as in that case, the transparent contents will just be drawn on top of
// the background color. As such, to allow the window to be transparent, we must also set
// the background color to one with an empty alpha channel.
let color = if transparent {
unsafe { NSColor::clearColor() }
} else {
unsafe { NSColor::windowBackgroundColor() }
};
self.window().setBackgroundColor(Some(&color));
}
pub fn set_blur(&self, blur: bool) {
@@ -833,8 +914,7 @@ impl WindowDelegate {
}
pub fn request_redraw(&self) {
let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
app_delegate.queue_redraw(self.window().id());
self.ivars().app_delegate.queue_redraw(self.window().id());
}
#[inline]
@@ -903,8 +983,8 @@ impl WindowDelegate {
pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize {
width: std::f32::MAX as f64,
height: std::f32::MAX as f64,
width: f32::MAX as f64,
height: f32::MAX as f64,
}));
let scale_factor = self.scale_factor();
let max_size = dimensions.to_logical::<CGFloat>(scale_factor);
@@ -958,13 +1038,13 @@ impl WindowDelegate {
self.ivars().resizable.set(resizable);
let fullscreen = self.ivars().fullscreen.borrow().is_some();
if !fullscreen {
let mut mask = self.window().styleMask().0;
let mut mask = self.window().styleMask();
if resizable {
mask |= NSWindowStyleMask::Resizable.0;
mask |= NSWindowStyleMask::Resizable;
} else {
mask &= !NSWindowStyleMask::Resizable.0;
mask &= !NSWindowStyleMask::Resizable;
}
self.set_style_mask(NSWindowStyleMask(mask));
self.set_style_mask(mask);
}
// Otherwise, we don't change the mask until we exit fullscreen.
}
@@ -976,23 +1056,23 @@ impl WindowDelegate {
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let mut mask = self.window().styleMask().0;
let mut mask = self.window().styleMask();
if buttons.contains(WindowButtons::CLOSE) {
mask |= NSWindowStyleMask::Closable.0;
mask |= NSWindowStyleMask::Closable;
} else {
mask &= !NSWindowStyleMask::Closable.0;
mask &= !NSWindowStyleMask::Closable;
}
if buttons.contains(WindowButtons::MINIMIZE) {
mask |= NSWindowStyleMask::Miniaturizable.0;
mask |= NSWindowStyleMask::Miniaturizable;
} else {
mask &= !NSWindowStyleMask::Miniaturizable.0;
mask &= !NSWindowStyleMask::Miniaturizable;
}
// This must happen before the button's "enabled" status has been set,
// hence we do it synchronously.
self.set_style_mask(NSWindowStyleMask(mask));
self.set_style_mask(mask);
// We edit the button directly instead of using `NSResizableWindowMask`,
// since that mask also affect the resizability of the window (which is
@@ -1089,7 +1169,8 @@ impl WindowDelegate {
#[inline]
pub fn drag_window(&self) -> Result<(), ExternalError> {
let mtm = MainThreadMarker::from(self);
let event = NSApplication::sharedApplication(mtm).currentEvent().unwrap();
let event =
NSApplication::sharedApplication(mtm).currentEvent().ok_or(ExternalError::Ignored)?;
self.window().performWindowDragWithEvent(&event);
Ok(())
}
@@ -1113,9 +1194,8 @@ impl WindowDelegate {
// we make it resizable temporarily.
let curr_mask = self.window().styleMask();
let required =
NSWindowStyleMask(NSWindowStyleMask::Titled.0 | NSWindowStyleMask::Resizable.0);
let needs_temp_mask = !mask_contains(curr_mask, required);
let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable;
let needs_temp_mask = !curr_mask.contains(required);
if needs_temp_mask {
self.set_style_mask(required);
}
@@ -1132,12 +1212,12 @@ impl WindowDelegate {
fn saved_style(&self) -> NSWindowStyleMask {
let base_mask =
self.ivars().saved_style.take().unwrap_or_else(|| self.window().styleMask()).0;
NSWindowStyleMask(if self.ivars().resizable.get() {
base_mask | NSWindowStyleMask::Resizable.0
self.ivars().saved_style.take().unwrap_or_else(|| self.window().styleMask());
if self.ivars().resizable.get() {
base_mask | NSWindowStyleMask::Resizable
} else {
base_mask & !NSWindowStyleMask::Resizable.0
})
base_mask & !NSWindowStyleMask::Resizable
}
}
/// This is called when the window is exiting fullscreen, whether by the
@@ -1192,7 +1272,7 @@ impl WindowDelegate {
return;
}
if mask_contains(self.window().styleMask(), NSWindowStyleMask::Resizable) {
if self.window().styleMask().contains(NSWindowStyleMask::Resizable) {
// Just use the native zoom if resizable
self.window().zoom(None);
} else {
@@ -1339,17 +1419,27 @@ impl WindowDelegate {
}
match (old_fullscreen, fullscreen) {
(None, Some(_)) => {
(None, Some(fullscreen)) => {
// `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
// set a normal style temporarily. The previous state will be
// restored in `WindowDelegate::window_did_exit_fullscreen`.
let curr_mask = self.window().styleMask();
let required =
NSWindowStyleMask(NSWindowStyleMask::Titled.0 | NSWindowStyleMask::Resizable.0);
if !mask_contains(curr_mask, required) {
let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable;
if !curr_mask.contains(required) {
self.set_style_mask(required);
self.ivars().saved_style.set(Some(curr_mask));
}
// In borderless games, we want to disable the dock and menu bar
// by setting the presentation options. We do this here rather than in
// `window:willUseFullScreenPresentationOptions` because for some reason
// the menu bar remains interactable despite being hidden.
if self.is_borderless_game() && matches!(fullscreen, Fullscreen::Borderless(_)) {
let presentation_options = NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar;
app.setPresentationOptions(presentation_options);
}
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Borderless(_)), None) => {
@@ -1357,13 +1447,7 @@ impl WindowDelegate {
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Exclusive(ref video_mode)), None) => {
unsafe {
ffi::CGRestorePermanentDisplayConfiguration();
assert_eq!(
ffi::CGDisplayRelease(video_mode.monitor().native_identifier()),
ffi::kCGErrorSuccess
);
};
restore_and_release_display(&video_mode.monitor());
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => {
@@ -1377,11 +1461,10 @@ impl WindowDelegate {
// delegate in `window:willUseFullScreenPresentationOptions:`.
self.ivars().save_presentation_opts.set(Some(app.presentationOptions()));
let presentation_options = NSApplicationPresentationOptions(
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen.0
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock.0
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar.0,
);
let presentation_options =
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar;
app.setPresentationOptions(presentation_options);
let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1;
@@ -1389,19 +1472,13 @@ impl WindowDelegate {
},
(Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => {
let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or(
NSApplicationPresentationOptions(NSApplicationPresentationOptions::NSApplicationPresentationFullScreen.0
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock.0
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar.0),
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar
);
app.setPresentationOptions(presentation_options);
unsafe {
ffi::CGRestorePermanentDisplayConfiguration();
assert_eq!(
ffi::CGDisplayRelease(video_mode.monitor().native_identifier()),
ffi::kCGErrorSuccess
);
};
restore_and_release_display(&video_mode.monitor());
// Restore the normal window level following the Borderless fullscreen
// `CGShieldingWindowLevel() + 1` hack.
@@ -1430,19 +1507,19 @@ impl WindowDelegate {
let new_mask = {
let mut new_mask = if decorations {
NSWindowStyleMask::Closable.0
| NSWindowStyleMask::Miniaturizable.0
| NSWindowStyleMask::Resizable.0
| NSWindowStyleMask::Titled.0
NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Titled
} else {
NSWindowStyleMask::Borderless.0 | NSWindowStyleMask::Resizable.0
NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable
};
if !resizable {
new_mask &= !NSWindowStyleMask::Resizable.0;
new_mask &= !NSWindowStyleMask::Resizable;
}
new_mask
};
self.set_style_mask(NSWindowStyleMask(new_mask));
self.set_style_mask(new_mask);
}
#[inline]
@@ -1521,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]
@@ -1545,7 +1629,7 @@ impl WindowDelegate {
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
let mut window_handle = rwh_04::AppKitHandle::empty();
window_handle.ns_window = self.window() as *const WinitWindow as *mut _;
window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _;
window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _;
rwh_04::RawWindowHandle::AppKit(window_handle)
}
@@ -1554,7 +1638,7 @@ impl WindowDelegate {
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
let mut window_handle = rwh_05::AppKitWindowHandle::empty();
window_handle.ns_window = self.window() as *const WinitWindow as *mut _;
window_handle.ns_view = Id::as_ptr(&self.view()) as *mut _;
window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _;
rwh_05::RawWindowHandle::AppKit(window_handle)
}
@@ -1568,8 +1652,8 @@ impl WindowDelegate {
#[inline]
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
let window_handle = rwh_06::AppKitWindowHandle::new({
let ptr = Id::as_ptr(&self.view()) as *mut _;
std::ptr::NonNull::new(ptr).expect("Id<T> should never be null")
let ptr = Retained::as_ptr(&self.view()) as *mut _;
std::ptr::NonNull::new(ptr).expect("Retained<T> should never be null")
});
rwh_06::RawWindowHandle::AppKit(window_handle)
}
@@ -1577,26 +1661,34 @@ impl WindowDelegate {
fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) {
let current_style_mask = self.window().styleMask();
if on {
self.set_style_mask(NSWindowStyleMask(current_style_mask.0 | mask.0));
self.set_style_mask(current_style_mask | mask);
} else {
self.set_style_mask(NSWindowStyleMask(current_style_mask.0 & (!mask.0)));
self.set_style_mask(current_style_mask & !mask);
}
}
#[inline]
pub fn theme(&self) -> Option<Theme> {
self.ivars().current_theme.get()
}
#[inline]
pub fn has_focus(&self) -> bool {
self.window().isKeyWindow()
}
pub fn theme(&self) -> Option<Theme> {
unsafe { self.window().appearance() }
.map(|appearance| appearance_to_theme(&appearance))
.or_else(|| {
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
if app.respondsToSelector(sel!(effectiveAppearance)) {
Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance()))
} else {
Some(Theme::Light)
}
})
}
pub fn set_theme(&self, theme: Option<Theme>) {
let mtm = MainThreadMarker::from(self);
set_ns_theme(theme, mtm);
self.ivars().current_theme.set(theme.or_else(|| Some(get_ns_theme(mtm))));
unsafe { self.window().setAppearance(theme_to_appearance(theme).as_deref()) };
}
#[inline]
@@ -1617,6 +1709,21 @@ impl WindowDelegate {
}
}
fn restore_and_release_display(monitor: &MonitorHandle) {
let available_monitors = monitor::available_monitors();
if available_monitors.contains(monitor) {
unsafe {
ffi::CGRestorePermanentDisplayConfiguration();
assert_eq!(ffi::CGDisplayRelease(monitor.native_identifier()), ffi::kCGErrorSuccess);
};
} else {
warn!(
monitor = monitor.name(),
"Tried to restore exclusive fullscreen on a monitor that is no longer available"
);
}
}
impl WindowExtMacOS for WindowDelegate {
#[inline]
fn simple_fullscreen(&self) -> bool {
@@ -1652,10 +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 = NSApplicationPresentationOptions(
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock.0
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar.0,
);
let presentation_options = if self.is_borderless_game() {
NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar
} else {
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar
};
app.setPresentationOptions(presentation_options);
// Hide the titlebar
@@ -1669,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();
@@ -1685,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]
@@ -1751,45 +1859,52 @@ impl WindowExtMacOS for WindowDelegate {
fn option_as_alt(&self) -> OptionAsAlt {
self.view().option_as_alt()
}
}
fn mask_contains(mask: NSWindowStyleMask, value: NSWindowStyleMask) -> bool {
mask.0 & value.0 == value.0
fn set_borderless_game(&self, borderless_game: bool) {
self.ivars().is_borderless_game.set(borderless_game);
}
fn is_borderless_game(&self) -> bool {
self.ivars().is_borderless_game.get()
}
}
const DEFAULT_STANDARD_FRAME: NSRect =
NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0));
pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme {
let app = NSApplication::sharedApplication(mtm);
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
if !has_theme {
return Theme::Light;
}
let appearance = app.effectiveAppearance();
let name = appearance
.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[
NSString::from_str("NSAppearanceNameAqua"),
NSString::from_str("NSAppearanceNameDarkAqua"),
]))
.unwrap();
match &*name.to_string() {
"NSAppearanceNameDarkAqua" => Theme::Dark,
_ => Theme::Light,
fn dark_appearance_name() -> &'static NSString {
// Don't use the static `NSAppearanceNameDarkAqua` to allow linking on macOS < 10.14
ns_string!("NSAppearanceNameDarkAqua")
}
pub fn appearance_to_theme(appearance: &NSAppearance) -> Theme {
let best_match = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[
unsafe { NSAppearanceNameAqua.copy() },
dark_appearance_name().copy(),
]));
if let Some(best_match) = best_match {
if *best_match == *dark_appearance_name() {
Theme::Dark
} else {
Theme::Light
}
} else {
warn!(?appearance, "failed to determine the theme of the appearance");
// Default to light in this case
Theme::Light
}
}
fn set_ns_theme(theme: Option<Theme>, mtm: MainThreadMarker) {
let app = NSApplication::sharedApplication(mtm);
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
if has_theme {
let appearance = theme.map(|t| {
let name = match t {
Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"),
Theme::Light => NSString::from_str("NSAppearanceNameAqua"),
};
NSAppearance::appearanceNamed(&name).unwrap()
});
app.setAppearance(appearance.as_ref().map(|a| a.as_ref()));
fn theme_to_appearance(theme: Option<Theme>) -> Option<Retained<NSAppearance>> {
let appearance = match theme? {
Theme::Light => unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua) },
Theme::Dark => NSAppearance::appearanceNamed(dark_appearance_name()),
};
if let Some(appearance) = appearance {
Some(appearance)
} else {
warn!(?theme, "could not find appearance for theme");
// Assume system appearance in this case
None
}
}

View File

@@ -1,27 +1,35 @@
use crate::monitor::{MonitorHandle as RootMonitorHandle, VideoModeHandle as RootVideoModeHandle};
use crate::window::Fullscreen as RootFullscreen;
#[cfg(windows_platform)]
#[path = "windows/mod.rs"]
mod platform;
#[cfg(any(x11_platform, wayland_platform))]
#[path = "linux/mod.rs"]
mod platform;
#[cfg(macos_platform)]
#[path = "macos/mod.rs"]
mod platform;
#[cfg(android_platform)]
#[path = "android/mod.rs"]
mod platform;
mod android;
#[cfg(ios_platform)]
#[path = "ios/mod.rs"]
mod platform;
#[cfg(web_platform)]
#[path = "web/mod.rs"]
mod platform;
mod ios;
#[cfg(any(x11_platform, wayland_platform))]
mod linux;
#[cfg(macos_platform)]
mod macos;
#[cfg(orbital_platform)]
#[path = "orbital/mod.rs"]
mod platform;
mod orbital;
#[cfg(web_platform)]
mod web;
#[cfg(windows_platform)]
mod windows;
#[cfg(android_platform)]
use android as platform;
#[cfg(ios_platform)]
use ios as platform;
#[cfg(any(x11_platform, wayland_platform))]
use linux as platform;
#[cfg(macos_platform)]
use macos as platform;
#[cfg(orbital_platform)]
use orbital as platform;
#[cfg(web_platform)]
use web as platform;
#[cfg(windows_platform)]
use windows as platform;
pub use self::platform::*;

View File

@@ -20,7 +20,7 @@ use crate::keyboard::{
PhysicalKey,
};
use crate::window::{
CustomCursor as RootCustomCursor, CustomCursorSource, WindowId as RootWindowId,
CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId,
};
use super::{
@@ -775,6 +775,11 @@ impl ActiveEventLoop {
rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty())
}
#[inline]
pub fn system_theme(&self) -> Option<Theme> {
None
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(

View File

@@ -103,7 +103,7 @@ pub struct WindowId {
impl WindowId {
pub const fn dummy() -> Self {
WindowId { fd: u64::max_value() }
WindowId { fd: u64::MAX }
}
}
@@ -154,7 +154,7 @@ impl<'a> WindowProperties<'a> {
}
}
impl<'a> fmt::Display for WindowProperties<'a> {
impl fmt::Display for WindowProperties<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,

Some files were not shown because too many files have changed in this diff Show More