Compare commits

..

64 Commits

Author SHA1 Message Date
Vitaly Kravchenko
27e17e3f29 Fix AppKit live resize redraw timing 2026-06-24 17:03:24 +02:00
Simon Hausmann
56fde0791b Fix CI
Pin wayland-protocols to the previous version that still compiles with
our MSRV.
2026-06-24 16:58:57 +02:00
William-Selna
4ef144b519 macOS: make Ime::Preedit cursor range surrogate-safe in setMarkedText
`setMarkedText:selectedRange:replacementRange:` converted the IME's UTF-16
`selectedRange` into UTF-8 byte offsets by taking `substringToIndex:` prefixes
and measuring them with `NSString::len()`
(`lengthOfBytesUsingEncoding:NSUTF8StringEncoding`). When an index falls inside
a surrogate pair, the prefix ends in a lone high surrogate, which UTF-8 cannot
represent, so `lengthOfBytesUsingEncoding:` returns 0 for the entire prefix and
the offset silently collapses to 0.
2026-06-20 01:34:43 +09:00
Mads Marquart
7fe8c206ae chore: avoid unsafe in examples
By using `OwnedDisplayHandle`, which uses reference-counting, instead of
`DisplayHandle<'static>` (where the user has to guarantee that the
Application doesn't outlive the event loop).
2026-06-15 03:36:22 +09:00
Sergey Kikevich
fb78ecbb03 windows: fix freeze on keyboard layout switch
Any winit app on Windows freezes when the keyboard layout is switched by
tools such as Punto Switcher. A minidump of the frozen process shows the
main thread blocked in MsgWaitForMultipleObjectsEx (winit's event-loop
wait), re-entered during message dispatch through the switcher's injected
global hook (pshook64.dll, via CallNextHookEx) — a re-entrant Win32
message-loop wait, not a mutex deadlock (no LAYOUT_CACHE frames present).

Handle WM_INPUTLANGCHANGE to refresh the cached keyboard layout, then
defer to DefWindowProc so the message still propagates to first-level
child windows as the Win32 docs require. The cache refresh is the minimal
change that stops the freeze (verified on Windows 11 by isolation
variants); update_modifiers and swallowing the message are not needed.
2026-06-15 03:29:35 +09:00
Martin Marmsoler
850d5f59a7 winit-wayland: add pointer gesture hold 2026-06-13 02:36:23 +09:00
Martin Marmsoler
81b2729765 winit-x11: fix clippy lint 2026-06-05 02:25:58 +09:00
Kirill Chibisov
c4afadbfab winit-wayland: use ext-background-effect if available
A more modern protocol compared to the KDE one.
2026-04-04 23:59:33 +09:00
dependabot[bot]
b5252f1366 chore: bump actions/configure-pages (#4550) 2026-03-28 07:44:01 +01:00
Mads Marquart
f93a223da9 Clean up cargo-deny
Remove the matrix in the CI action; EmbarkStudios/cargo-deny#324 hasn't been resolved yet, but since we've split Winit out into multiple crates, there's still value in minimizing dependency conflicts even if they won't be hit by users, since e.g. Rust-Analyzer will by default check the entire workspace (and thus download and compile duplicate dependencies).
2026-03-28 10:28:31 +09:00
dependabot[bot]
d75a0dada0 chore: bump actions/deploy-pages from 4 to 5 (#4545) 2026-03-26 06:32:41 +01:00
Mads Marquart
9bf46af6f7 AppKit: Use fn_addr_eq now that it's in MSRV (#4532) 2026-03-26 05:50:16 +01:00
Mads Marquart
464c37a94e Remove leftover apple/appkit/mod.rs (#4533) 2026-03-26 05:49:49 +01:00
Mads Marquart
557d285170 Remove symlinking between winit-appkit and winit-uikit (#4530) 2026-03-26 05:49:12 +01:00
Mads Marquart
ba856e127a Fix CI (#4546)
* Fix cargo-deny erroring on new jni-sys version
* Fix unicode-segmentation bumping MSRV
* Fix cargo-deny finding new script in android-activity
2026-03-26 05:26:54 +01:00
Charlie Tonneslan
0ffd303db6 fix(typo): dependant -> dependent (#4540) 2026-03-24 15:54:31 +01:00
Mads Marquart
5a74bf0aab Android: Add further scancode conversions (#4023)
Firefox' source at:
https://searchfox.org/mozilla-central/rev/c597e9c789ad36af84a0370d395be066b7dc94f4/widget/NativeKeyToDOMCodeName.h

Seems to use some other form of scan code? So instead we try to map as
many of the codes available on `Keycode` that they do as possible.
2026-03-19 02:27:23 +01:00
Mads Marquart
117ec364f9 examples: Always use tracing helper module 2026-03-18 22:55:47 +01:00
Mads Marquart
9f789e56ee examples: Use tracing macros instead of println!
This allows the examples to work a bit better in WASM and on iOS.
2026-03-18 22:55:47 +01:00
Mads Marquart
91558169d2 example: Fix tracing registration in pump_events
Tracing subscribers must be set up before `EventLoop::new()`.
2026-03-18 22:55:47 +01:00
Mads Marquart
4998cb990f AppKit: Trace sendEvent: calls
Most events in AppKit go through `sendEvent:`, and they contain a lot of
information, so it's nice to surface this when debugging.

We could override `sendEvent:` in UIKit and track this in there too, but
that's much less important, since there the relevant events are fairly
narrowly scoped, see the link below, other events go through CFRunLoop.
https://developer.apple.com/documentation/uikit/uievent/eventtype
2026-03-18 22:55:47 +01:00
Mads Marquart
98692641c4 UIKit: Add tracing spans 2026-03-18 22:55:47 +01:00
Mads Marquart
a630b5333c Apple: Track spans across queued closures 2026-03-18 22:55:47 +01:00
Mads Marquart
ca7735f10b Apple: Use tracing spans instead of custom trace_scope! macro
Spans are more powerful, and can even optionally be emitted as events by
using `.with_span_events(tracing_subscriber::fmt::format::FmtSpan::*)`.
2026-03-18 22:55:47 +01:00
Mads Marquart
7adb805011 Apple: Trace CFRunLoop activities
Add two run loop observers that:
- Create a TRACE-level span when the run loop enters a new state.
- Drops the span when the run loop exits that state.

These spans attach information to events, such that e.g. resizing a view
produces messages like:
```
TRACE inside runloop{mode=NSEventTrackingRunLoopMode}:timers:
  winit_appkit::util: Triggered `drawRect:` target="winit_appkit::view"
TRACE inside runloop{mode=NSEventTrackingRunLoopMode}:timers:
  winit_appkit::util: Completed `drawRect:` target="winit_appkit::view"
```
2026-03-18 22:55:47 +01:00
Mads Marquart
a8c7d809b9 Use new macOS 15 cursors for resize icons (#4422)
These look slightly different from the old ones. Verified that we now
use the same cursor icons as those used in Safari 26.
2026-03-18 22:33:53 +01:00
RandomScientist
4d81f4aa62 internal(macOS) use NSTrackingArea instead of trackingRect (#4514) 2026-03-18 02:03:16 +01:00
Mads Marquart
4f29aed5ee Synchronize changelogs with v0.30.13 2026-03-17 23:22:47 +09:00
Mads Marquart
7864d02077 Remove indentation in fill.rs helper 2026-03-17 04:34:15 +01:00
Mads Marquart
0f2d59cbba Allow building examples using softbuffer on Android
These probably aren't runnable in the current state, but it should at
least allow us to get rid of a bunch of cfgs.
2026-03-17 04:34:15 +01:00
Mads Marquart
f0c4adc58c Update to softbuffer v0.4.8 2026-03-17 04:34:15 +01:00
junglie85
8e38112ad3 Add Send and Sync to OwnedDisplayHandle (#4509) 2026-03-17 03:54:42 +01:00
RandomScientist
6f3d763650 Document potential user expectation of ctrl+left click behavior on macOS. (#4518) 2026-03-17 03:39:12 +01:00
Ali
7a11ff8766 Add missing scancodes for orbital (#4515)
add missing keymaps
2026-03-17 03:29:27 +01:00
jgcodes2020
211ae8f6c7 winit-x11: replace XQueryKeymap with equivalent x11rb call (#4497)
And slightly optimize the first_bit helper function.
2026-03-17 03:15:48 +01:00
RandomScientist
0b6b794f01 fix(macOS) "fix" toggling IME on appkit backend (#4512)
Co-authored-by: Random-Scientist <Random-Scientist@users.noreply.github.com>
2026-03-14 02:50:13 +01:00
Marijn Suijten
5e2f421e34 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:37:30 +09:00
Pedro Macedo
41f4265957 wayland: implement resize increments 2026-03-01 22:28:35 +09:00
Kotomine Shiki
c535968128 win32: fix ime setcontext lparam
Fixes #3893.
2026-03-01 22:08:45 +09:00
Takaranoao
85ff2fa093 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-01 21:55:06 +09:00
voxelmagpie
44865be79d wayland: fix panic when fancy wacom tablet is connected
With some setups certain events were not handled leading to crashes,
thus add missing handles.

Fixes #4493.
2026-02-28 15:07:40 +09:00
SuchAFuriousDeath
f3fb2fe3a6 winit-x11: fix debug mode overflow panic in set_timestamp
Fixes #4484
2026-02-24 23:15:49 +09:00
Mads Marquart
fa10ca1764 Refactor usage of CFRunLoopObserver (#4349)
Added a common interface that:
- Uses closures instead of static functions. This should allow easier
  refactoring in the future.
- Returns a handle which is invalidated on `Drop`. This should avoid
  situations where the event loop has exited, but an observer is still
  called because the user spawned the application later on.
- Is properly main-thread safe.

This interface is placed in winit-common, to allow using it in both
winit-appkit and winit-uikit.
2026-02-17 16:21:18 +01:00
Mads Marquart
4fda048729 Reduce duplication in dpi module (#2148)
* Deduplicate Pixel impl for integers using a macro
* Deduplicate [Logical|Physical][Size|Position] From impls using a macro

Co-authored-by: Osspial <osspial@gmail.com>
2026-02-17 15:45:53 +01:00
Tarek Abdel Sater
983e50971d macOS: fix borderless game resetting after switching spaces (#4482) 2026-02-11 22:14:10 +01:00
DorotaC
cf9daedb18 wayland IME: Discard completed delete request
Co-authored-by: dcz <gilapfco.dcz@porcupinefactory.org>
2026-02-07 00:31:07 +09:00
isan
5218e11e55 reafactor(orbital): Reimplement Orbital support using std::fs and redox_event (#4470)
Implemented Send and Sync for EventQueue
2026-02-03 16:23:40 +01:00
John Nunley
b8f28efd04 wayland: Move hash algorithm to foldhash
At the moment, the wayland code uses ahash to perform hashing
in its various hash mas. This was done because ahash was seen as
the best default in the Rust community at the time. However, most
Rust crates (including `hashbrown`) have since moved to using
foldhash instead.

This move is done for two primary reasons:

- This reduces the number of dependencies in the tree for most GUI
  projects. As other projects use foldhash now, this removes ahash
  (as well as its five dependencies) from the tree.
- In most cases, foldhash is faster than ahash.

Signed-off-by: John Nunley <dev@notgull.net>
2026-01-29 09:15:25 +09:00
dependabot[bot]
eced28824b chore: bump taiki-e/cache-cargo-install-action (#4456) 2026-01-28 19:34:50 +01:00
Jack
b3c7635bba Add Apple Pencil support on iOS (#4449)
With force, altitude, and azimuth data handling
2026-01-28 19:24:38 +01:00
Mads Marquart
4aac778d2a chore: appease clippy (#4471) 2026-01-28 19:12:39 +01:00
Colin Finck
043c8635a9 Upgrade windows-sys dependency to 0.61 (#4464) 2026-01-20 20:49:03 +01:00
dependabot[bot]
da6220060e chore: bump the github-actions version 2025-12-21 19:44:09 +09:00
Anhad Singh
50f9b84f6b fix(winit-orbital): handle EINTR when reading from event_socket
Signed-off-by: Anhad Singh <andypython@protonmail.com>
2025-12-18 18:42:54 +09:00
itsamine27
56333064ac winit-win32: Fix ABI mismatch in INIT_MAIN_THREAD_ID
Fixes #4435.
2025-12-16 22:50:14 +09:00
Silico_Biomancer
de78ffdfed 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>
2025-11-25 11:06:39 +09:00
Mads Marquart
2d56ed1099 Fix issue template labels (#4421) 2025-11-24 13:50:34 +01:00
Diggory Hardy
7b1dd52ad1 winit-core/ime: implement Error for ImeSurroundingTextError 2025-11-20 19:18:48 +09:00
richerfu
6a46610632 feat(core): add keyboard for ohos 2025-11-17 15:56:20 +09:00
Kirill Chibisov
27233f575f Bump version on master
This commit does not represent a release and only synchronizes CHANGELOG
from the latest release.
2025-11-17 15:45:12 +09:00
Kirill Chibisov
f2ba68771f winit-core: fix set_ime_allowed always panicing 2025-11-16 16:39:15 +09:00
Mads Marquart
cc23fbf3be winit/event_loop: Add register_app and run_app_never_return
To allow users to explicitly choose the run semantics that they want.
2025-11-16 16:25:44 +09:00
Mads Marquart
c98d880a1c winit-web: return immediately from run_app on web
This avoids using JavaScript exceptions to support `EventLoop::run_app`
on the web, which is a huge hack, and doesn't work with the Exception
Handling Proposal for WebAssembly:
https://github.com/WebAssembly/exception-handling

This needs the application handler passed to `run_app` to be `'static`,
but that works better on iOS too anyhow (since you can't accidentally
forget to pass in state that then wouldn't be dropped when terminating).
2025-11-16 16:25:44 +09:00
Kirill Chibisov
165c163f01 Bump version on master
This commit does not represent a release and only synchronizes CHANGELOG
from the latest release.
2025-11-16 13:53:23 +09:00
91 changed files with 2200 additions and 1319 deletions

View File

@@ -1,8 +1,8 @@
name: MacOS bug
name: macOS bug
description: Create a macOS-specific bug report
labels:
- B - bug
- DS - macos
- DS - appkit
body:
- type: markdown
attributes:

View File

@@ -1,8 +1,8 @@
name: iOS bug
description: Create an iOS-specific bug report
description: Create an iOS/UIKit-specific bug report
labels:
- B - bug
- DS - ios
- DS - uikit
body:
- type: markdown
attributes:

View File

@@ -2,7 +2,7 @@ name: Windows bug
description: Create a Windows-specific bug report
labels:
- B - bug
- DS - windows
- DS - win32
body:
- type: markdown
attributes:

View File

@@ -124,7 +124,7 @@ jobs:
# the cache has been downloaded.
#
# This could be avoided if we added Cargo.lock to the repository.
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
# https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci
path: |
@@ -136,7 +136,7 @@ jobs:
- name: Generate lockfile
# Also updates the crates.io index
run: cargo generate-lockfile && cargo update -p smol_str --precise 0.3.2
run: cargo generate-lockfile && cargo update -p smol_str --precise 0.3.2 && cargo update -p unicode-segmentation --precise 1.12.0 && cargo update -p wayland-protocols --precise 0.32.12
- name: Install GCC Multilib
if: (matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686')
@@ -145,7 +145,7 @@ jobs:
- name: Cache cargo-apk
if: contains(matrix.platform.target, 'android')
id: cargo-apk-cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.cargo/bin/cargo-apk
# Change this key if we update the required cargo-apk version
@@ -160,7 +160,7 @@ jobs:
if: contains(matrix.platform.target, 'android') && (steps.cargo-apk-cache.outputs.cache-hit != 'true')
run: cargo install cargo-apk --version=^0.9.7 --locked
- uses: taiki-e/cache-cargo-install-action@v2
- uses: taiki-e/cache-cargo-install-action@v3
if: contains(matrix.platform.target, 'wasm32') && matrix.toolchain == 'nightly'
with:
tool: wasm-bindgen-cli
@@ -281,7 +281,7 @@ jobs:
# See restore step above
- name: Save cache of cargo folder
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
path: |
~/.cargo/registry/index/
@@ -290,31 +290,14 @@ jobs:
key: cargo-${{ matrix.toolchain }}-${{ matrix.platform.name }}-${{ hashFiles('Cargo.lock') }}
cargo-deny:
name: Run cargo-deny on ${{ matrix.platform.name }}
name: Run cargo-deny
runs-on: ubuntu-latest
# TODO: remove this matrix when https://github.com/EmbarkStudios/cargo-deny/issues/324 is resolved
strategy:
fail-fast: false
matrix:
platform:
- { name: 'Android', target: aarch64-linux-android }
- { name: 'iOS', target: aarch64-apple-ios }
- { name: 'Linux', target: x86_64-unknown-linux-gnu }
- { name: 'macOS', target: aarch64-apple-darwin }
- { name: 'Redox OS', target: x86_64-unknown-redox }
- { name: 'Web', target: wasm32-unknown-unknown }
- { name: 'Windows GNU', target: x86_64-pc-windows-gnu }
- { name: 'Windows MSVC', target: x86_64-pc-windows-msvc }
steps:
- uses: taiki-e/checkout-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
log-level: error
manifest-path: winit/Cargo.toml
arguments: --all-features --target ${{ matrix.platform.target }}
eslint:
name: ESLint

View File

@@ -19,7 +19,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
@@ -32,7 +32,7 @@ jobs:
cargo doc --no-deps -Z rustdoc-map -Z rustdoc-scrape-examples --features=serde,mint,android-native-activity
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Fix permissions
run: |
@@ -47,4 +47,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

View File

@@ -39,7 +39,7 @@ tracing = { version = "0.1.40", default-features = false }
# Dev dependencies.
image = { version = "0.25.0", default-features = false }
softbuffer = { version = "0.4.6", default-features = false, features = [
softbuffer = { version = "0.4.8", default-features = false, features = [
"x11",
"x11-dlopen",
"wayland",
@@ -64,12 +64,12 @@ objc2-ui-kit = { version = "0.3.2", default-features = false }
# Windows dependencies.
unicode-segmentation = "1.7.1"
windows-sys = "0.59.0"
windows-sys = "0.61"
# Linux dependencies.
ahash = { version = "0.8.7", features = ["no-rng"] }
bytemuck = { version = "1.13.1", default-features = false }
calloop = "0.14.3"
foldhash = { version = "0.2.0", default-features = false, features = ["std"] }
libc = "0.2.64"
memmap2 = "0.9.0"
percent-encoding = "2.0"
@@ -79,8 +79,9 @@ x11rb = { version = "0.13.0", default-features = false }
xkbcommon-dl = "0.4.2"
# Orbital dependencies.
libredox = "0.1.12"
orbclient = { version = "0.3.47", default-features = false }
redox_syscall = "0.5.7"
redox_event = { package = "redox_event", version = "0.4.5" }
# Web dependencies.
atomic-waker = "1"

View File

@@ -1,4 +1,11 @@
# Using allow-invalid because this is platform-specific code
disallowed-macros = [
{ path = "std::print", reason = "works badly on web", replacement = "tracing::info" },
{ path = "std::println", reason = "works badly on web", replacement = "tracing::info" },
{ path = "std::eprint", reason = "works badly on web", replacement = "tracing::error" },
{ path = "std::eprintln", reason = "works badly on web", replacement = "tracing::error" },
{ path = "std::dbg", reason = "leftover debugging aid, remove it or use tracing" },
]
disallowed-methods = [
{ allow-invalid = true, path = "objc2_app_kit::NSView::visibleRect", reason = "We expose a render target to the user, and visibility is not really relevant to that (and can break if you don't use the rectangle position as well). Use `frame` instead." },
{ allow-invalid = true, path = "objc2_app_kit::NSWindow::setFrameTopLeftPoint", reason = "Not sufficient when working with Winit's coordinate system, use `flip_window_screen_coordinates` instead" },

View File

@@ -1,27 +1,25 @@
# https://embarkstudios.github.io/cargo-deny
# cargo install cargo-deny
# 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
# cargo update && cargo deny check
[graph]
all-features = true
exclude-dev = true
targets = [
{ triple = "aarch64-apple-darwin" },
{ triple = "aarch64-apple-ios" },
{ triple = "aarch64-linux-android" },
{ triple = "i686-pc-windows-gnu" },
{ triple = "i686-pc-windows-msvc" },
{ triple = "i686-unknown-linux-gnu" },
"aarch64-apple-darwin",
"aarch64-apple-ios",
"aarch64-linux-android",
"i686-pc-windows-gnu",
"i686-pc-windows-msvc",
"i686-unknown-linux-gnu",
{ triple = "wasm32-unknown-unknown", features = [
"atomics",
] },
{ triple = "x86_64-apple-darwin" },
{ triple = "x86_64-apple-ios" },
{ triple = "x86_64-pc-windows-gnu" },
{ triple = "x86_64-pc-windows-msvc" },
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "x86_64-unknown-redox" },
"x86_64-apple-darwin",
"x86_64-apple-ios",
"x86_64-pc-windows-gnu",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-redox",
]
[licenses]
@@ -32,6 +30,8 @@ allow = [
"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
"Zlib", # https://spdx.org/licenses/Zlib.html
"MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/
]
confidence-threshold = 1.0
private = { ignore = true }
@@ -39,46 +39,23 @@ private = { ignore = true }
[bans]
multiple-versions = "deny"
skip = [
{ 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" },
{ crate = "jni-sys@0.3", reason = "uses the semver trick to depend on v0.4, but `ndk` hasn't been updated to v0.4 yet" },
{ crate = "thiserror@1.0", reason = "dep of `ndk` crate, yet to be updated" },
{ crate = "thiserror-impl@1.0", reason = "dep of `thiserror`" },
{ crate = "objc2@0.5", reason = "used by crossfont" },
{ crate = "objc2-foundation@0.2", reason = "used by crossfont" },
]
skip-tree = [
{ crate = "windows-sys", reason = "foundational but bumps fairly often, nothing we can do about it not having a shared version" },
]
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
[bans.build]
bypass = [
{ crate = "android-activity", allow-globs = ["android-games-sdk/import-games-sdk.sh"] },
{ crate = "freetype-sys", allow-globs = ["freetype2/*"] },
# `crossfont` still depends (partially transitively) on `winapi`.
{ crate = "winapi-i686-pc-windows-gnu", allow-globs = ["lib/lib*.a"] },
{ crate = "winapi-x86_64-pc-windows-gnu", allow-globs = ["lib/lib*.a"] },
]
include-archives = true
interpreted = "deny"
[[bans.build.bypass]]
allow = [
{ path = "generate-bindings.sh", checksum = "268ec23248218d779e33853cdc60e2985e70214ff004716cd734270de1f6b561" },
]
crate = "android-activity"
[[bans.build.bypass]]
allow-globs = ["ci/*", "githooks/*"]
crate = "zerocopy"
[[bans.build.bypass]]
allow-globs = ["cherry-pick-stable.sh"]
crate = "libc"
[[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

@@ -84,36 +84,18 @@ pub trait Pixel: Copy + Into<f64> {
}
}
impl Pixel for u8 {
fn from_f64(f: f64) -> Self {
round(f) as u8
}
}
impl Pixel for u16 {
fn from_f64(f: f64) -> Self {
round(f) as u16
}
}
impl Pixel for u32 {
fn from_f64(f: f64) -> Self {
round(f) as u32
}
}
impl Pixel for i8 {
fn from_f64(f: f64) -> Self {
round(f) as i8
}
}
impl Pixel for i16 {
fn from_f64(f: f64) -> Self {
round(f) as i16
}
}
impl Pixel for i32 {
fn from_f64(f: f64) -> Self {
round(f) as i32
}
macro_rules! pixel_int_impl {
($($t:ty),*) => {$(
impl Pixel for $t {
fn from_f64(f: f64) -> Self {
round(f) as $t
}
}
)*}
}
pixel_int_impl!(u8, u16, u32, i8, i16, i32);
impl Pixel for f32 {
fn from_f64(f: f64) -> Self {
f as f32
@@ -378,6 +360,48 @@ impl<P: Pixel> From<LogicalUnit<P>> for PixelUnit {
}
}
macro_rules! vec2_from_impls {
($t:ident, $a:ident, $b:ident, $mint_ty:ident) => {
impl<P: Pixel, X: Pixel> From<(X, X)> for $t<P> {
fn from(($a, $b): (X, X)) -> Self {
Self::new($a.cast(), $b.cast())
}
}
impl<P: Pixel, X: Pixel> From<$t<P>> for (X, X) {
fn from(p: $t<P>) -> Self {
(p.$a.cast(), p.$b.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for $t<P> {
fn from([$a, $b]: [X; 2]) -> Self {
Self::new($a.cast(), $b.cast())
}
}
impl<P: Pixel, X: Pixel> From<$t<P>> for [X; 2] {
fn from(p: $t<P>) -> Self {
[p.$a.cast(), p.$b.cast()]
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<mint::$mint_ty<P>> for $t<P> {
fn from(p: mint::$mint_ty<P>) -> Self {
Self::new(p.x, p.y)
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<$t<P>> for mint::$mint_ty<P> {
fn from(p: $t<P>) -> Self {
Self { x: p.$a, y: p.$b }
}
}
};
}
/// A position represented in logical pixels.
///
/// The position is stored as floats, so please be careful. Casting floats to integers truncates the
@@ -420,43 +444,7 @@ impl<P: Pixel> LogicalPosition<P> {
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalPosition<P> {
fn from((x, y): (X, X)) -> LogicalPosition<P> {
LogicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalPosition<P>> for (X, X) {
fn from(p: LogicalPosition<P>) -> (X, X) {
(p.x.cast(), p.y.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalPosition<P> {
fn from([x, y]: [X; 2]) -> LogicalPosition<P> {
LogicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalPosition<P>> for [X; 2] {
fn from(p: LogicalPosition<P>) -> [X; 2] {
[p.x.cast(), p.y.cast()]
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<mint::Point2<P>> for LogicalPosition<P> {
fn from(p: mint::Point2<P>) -> Self {
Self::new(p.x, p.y)
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<LogicalPosition<P>> for mint::Point2<P> {
fn from(p: LogicalPosition<P>) -> Self {
mint::Point2 { x: p.x, y: p.y }
}
}
vec2_from_impls!(LogicalPosition, x, y, Point2);
/// A position represented in physical pixels.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)]
@@ -496,43 +484,7 @@ impl<P: Pixel> PhysicalPosition<P> {
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalPosition<P> {
fn from((x, y): (X, X)) -> PhysicalPosition<P> {
PhysicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalPosition<P>> for (X, X) {
fn from(p: PhysicalPosition<P>) -> (X, X) {
(p.x.cast(), p.y.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalPosition<P> {
fn from([x, y]: [X; 2]) -> PhysicalPosition<P> {
PhysicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalPosition<P>> for [X; 2] {
fn from(p: PhysicalPosition<P>) -> [X; 2] {
[p.x.cast(), p.y.cast()]
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<mint::Point2<P>> for PhysicalPosition<P> {
fn from(p: mint::Point2<P>) -> Self {
Self::new(p.x, p.y)
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<PhysicalPosition<P>> for mint::Point2<P> {
fn from(p: PhysicalPosition<P>) -> Self {
mint::Point2 { x: p.x, y: p.y }
}
}
vec2_from_impls!(PhysicalPosition, x, y, Point2);
/// A size represented in logical pixels.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)]
@@ -572,43 +524,7 @@ impl<P: Pixel> LogicalSize<P> {
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalSize<P> {
fn from((x, y): (X, X)) -> LogicalSize<P> {
LogicalSize::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalSize<P>> for (X, X) {
fn from(s: LogicalSize<P>) -> (X, X) {
(s.width.cast(), s.height.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalSize<P> {
fn from([x, y]: [X; 2]) -> LogicalSize<P> {
LogicalSize::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalSize<P>> for [X; 2] {
fn from(s: LogicalSize<P>) -> [X; 2] {
[s.width.cast(), s.height.cast()]
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<mint::Vector2<P>> for LogicalSize<P> {
fn from(v: mint::Vector2<P>) -> Self {
Self::new(v.x, v.y)
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<LogicalSize<P>> for mint::Vector2<P> {
fn from(s: LogicalSize<P>) -> Self {
mint::Vector2 { x: s.width, y: s.height }
}
}
vec2_from_impls!(LogicalSize, width, height, Vector2);
/// A size represented in physical pixels.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)]
@@ -645,43 +561,7 @@ impl<P: Pixel> PhysicalSize<P> {
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalSize<P> {
fn from((x, y): (X, X)) -> PhysicalSize<P> {
PhysicalSize::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalSize<P>> for (X, X) {
fn from(s: PhysicalSize<P>) -> (X, X) {
(s.width.cast(), s.height.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalSize<P> {
fn from([x, y]: [X; 2]) -> PhysicalSize<P> {
PhysicalSize::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalSize<P>> for [X; 2] {
fn from(s: PhysicalSize<P>) -> [X; 2] {
[s.width.cast(), s.height.cast()]
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<mint::Vector2<P>> for PhysicalSize<P> {
fn from(v: mint::Vector2<P>) -> Self {
Self::new(v.x, v.y)
}
}
#[cfg(feature = "mint")]
impl<P: Pixel> From<PhysicalSize<P>> for mint::Vector2<P> {
fn from(s: PhysicalSize<P>) -> Self {
mint::Vector2 { x: s.width, y: s.height }
}
}
vec2_from_impls!(PhysicalSize, width, height, Vector2);
/// A size that's either physical or logical.
#[derive(Debug, Copy, Clone, PartialEq)]

View File

@@ -473,16 +473,23 @@ impl EventLoop {
&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::WindowEvent::KeyboardInput {
device_id: Some(DeviceId::from_raw(key.device_id() as i64)),
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_with_all_modifiers: None,
text: text.clone(),
text_with_all_modifiers: text,
key_without_modifiers: keycodes::to_logical(key_char, keycode),
},
is_synthetic: false,

View File

@@ -109,6 +109,7 @@ pub fn to_physical_key(keycode: Keycode) -> PhysicalKey {
Keycode::MediaStop => KeyCode::MediaStop,
Keycode::MediaNext => KeyCode::MediaTrackNext,
Keycode::MediaPrevious => KeyCode::MediaTrackPrevious,
Keycode::MediaEject => KeyCode::Eject,
Keycode::Plus => KeyCode::Equal,
Keycode::Minus => KeyCode::Minus,
@@ -131,7 +132,11 @@ pub fn to_physical_key(keycode: Keycode) -> PhysicalKey {
// These are exactly the same
Keycode::ScrollLock => KeyCode::ScrollLock,
Keycode::Eisu => KeyCode::Lang2,
Keycode::Muhenkan => KeyCode::NonConvert,
Keycode::Henkan => KeyCode::Convert,
Keycode::Yen => KeyCode::IntlYen,
Keycode::Ro => KeyCode::IntlRo,
Keycode::Kana => KeyCode::Lang1,
Keycode::KatakanaHiragana => KeyCode::KanaMode,
@@ -154,6 +159,14 @@ pub fn to_physical_key(keycode: Keycode) -> PhysicalKey {
Keycode::Sleep => KeyCode::Sleep, // what about SoftSleep?
Keycode::Wakeup => KeyCode::WakeUp,
Keycode::CapsLock => KeyCode::CapsLock,
Keycode::Help => KeyCode::Help,
Keycode::Back => KeyCode::BrowserBack,
Keycode::Forward => KeyCode::BrowserForward,
Keycode::Refresh => KeyCode::BrowserRefresh,
Keycode::Search => KeyCode::BrowserSearch,
keycode => return PhysicalKey::Unidentified(NativeKeyCode::Android(keycode.into())),
})
}

View File

@@ -51,6 +51,7 @@ objc2-app-kit = { workspace = true, features = [
"NSScreen",
"NSTextInputClient",
"NSTextInputContext",
"NSTrackingArea",
"NSToolbar",
"NSView",
"NSWindow",
@@ -106,7 +107,7 @@ objc2-foundation = { workspace = true, features = [
"NSThread",
"NSValue",
] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] }
[dev-dependencies]
winit.workspace = true

View File

@@ -1,14 +1,15 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::Cell;
use std::mem;
use std::rc::Rc;
use std::{mem, ptr};
use dispatch2::MainThreadBound;
use objc2::runtime::{Imp, Sel};
use objc2::sel;
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType};
use objc2_foundation::MainThreadMarker;
use tracing::trace_span;
use winit_core::event::{DeviceEvent, ElementState};
use super::app_state::AppState;
@@ -21,6 +22,10 @@ static ORIGINAL: MainThreadBound<Cell<Option<SendEvent>>> = {
};
extern "C-unwind" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) {
// This can be a bit noisy, since `event` is fairly large. Note that you can use
// `RUST_LOG='trace,winit_appkit::app=warn'` if you're debugging and want TRACE-level logs but
// not this.
let _entered = trace_span!("sendEvent:", ?event).entered();
let mtm = MainThreadMarker::from(app);
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
@@ -75,9 +80,7 @@ pub(crate) fn override_send_event(global_app: &NSApplication) {
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() {
if ptr::fn_addr_eq(overridden, method.implementation()) {
return;
}

View File

@@ -8,7 +8,7 @@ use dispatch2::MainThreadBound;
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication};
use objc2_foundation::NSNotification;
use winit_common::core_foundation::EventLoopProxy;
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
use winit_common::event_handler::EventHandler;
use winit_core::application::ApplicationHandler;
use winit_core::event::{StartCause, WindowEvent};
@@ -17,7 +17,7 @@ use winit_core::window::WindowId;
use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately};
use super::menu;
use super::observer::{EventLoopWaker, RunLoop};
use super::observer::EventLoopWaker;
#[derive(Debug)]
pub(super) struct AppState {
@@ -25,7 +25,7 @@ pub(super) struct AppState {
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
run_loop: RunLoop,
run_loop: MainRunLoop,
event_loop_proxy: Arc<EventLoopProxy>,
event_handler: EventHandler,
stop_on_launch: Cell<bool>,
@@ -68,7 +68,7 @@ impl AppState {
activation_policy,
default_menu,
activate_ignoring_other_apps,
run_loop: RunLoop::main(mtm),
run_loop: MainRunLoop::get(mtm),
event_loop_proxy,
event_handler: EventHandler::new(),
stop_on_launch: Cell::new(false),
@@ -99,7 +99,6 @@ impl AppState {
// NOTE: This notification will, globally, only be emitted once,
// no matter how many `EventLoop`s the user creates.
pub fn did_finish_launching(self: &Rc<Self>, _notification: &NSNotification) {
trace_scope!("NSApplicationDidFinishLaunchingNotification");
self.is_launched.set(true);
let app = NSApplication::sharedApplication(self.mtm);
@@ -154,7 +153,6 @@ impl AppState {
}
pub fn will_terminate(self: &Rc<Self>, _notification: &NSNotification) {
trace_scope!("NSApplicationWillTerminateNotification");
let app = NSApplication::sharedApplication(self.mtm);
notify_windows_of_exit(&app);
self.event_handler.terminate();
@@ -265,7 +263,7 @@ impl AppState {
if !pending_redraw.contains(&window_id) {
pending_redraw.push(window_id);
}
self.run_loop.wakeup();
self.run_loop.wake_up();
}
#[track_caller]
@@ -310,6 +308,7 @@ impl AppState {
// Called by RunLoopObserver after finishing waiting for new events
pub fn wakeup(self: &Rc<Self>) {
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
// (we have registered to observe all modes, including modal event loops).
if !self.event_handler.ready() || !self.is_running() {
return;
}
@@ -338,8 +337,7 @@ impl AppState {
// Called by RunLoopObserver before waiting for new events
pub fn cleared(self: &Rc<Self>) {
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
// XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if
// we're about to return to the `CFRunLoop` to poll for new events?
// (we have registered to observe all modes, including modal event loops).
if !self.event_handler.ready() || !self.is_running() {
return;
}

View File

@@ -5,7 +5,10 @@ use std::sync::OnceLock;
use objc2::rc::Retained;
use objc2::runtime::Sel;
use objc2::{AllocAnyThread, ClassType, available, msg_send, sel};
use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
use objc2_app_kit::{
NSBitmapImageRep, NSCursor, NSCursorFrameResizeDirections, NSCursorFrameResizePosition,
NSDeviceRGBColorSpace, NSImage,
};
use objc2_foundation::{
NSData, NSDictionary, NSNumber, NSObject, NSPoint, NSSize, NSString, ns_string,
};
@@ -204,23 +207,155 @@ pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained<NSCursor> {
CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(),
CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(),
CursorIcon::Crosshair => NSCursor::crosshairCursor(),
CursorIcon::EResize => NSCursor::resizeRightCursor(),
CursorIcon::NResize => NSCursor::resizeUpCursor(),
CursorIcon::WResize => NSCursor::resizeLeftCursor(),
CursorIcon::SResize => NSCursor::resizeDownCursor(),
CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(),
CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(),
CursorIcon::EResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Right,
NSCursorFrameResizeDirections::Outward,
)
} else {
NSCursor::resizeRightCursor()
}
},
CursorIcon::NResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Top,
NSCursorFrameResizeDirections::Outward,
)
} else {
NSCursor::resizeUpCursor()
}
},
CursorIcon::WResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Left,
NSCursorFrameResizeDirections::Outward,
)
} else {
NSCursor::resizeLeftCursor()
}
},
CursorIcon::SResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Bottom,
NSCursorFrameResizeDirections::Outward,
)
} else {
NSCursor::resizeDownCursor()
}
},
CursorIcon::EwResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Right,
NSCursorFrameResizeDirections::All,
)
} else {
NSCursor::resizeLeftRightCursor()
}
},
CursorIcon::NsResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::Top,
NSCursorFrameResizeDirections::All,
)
} else {
NSCursor::resizeUpDownCursor()
}
},
CursorIcon::NeResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::TopRight,
NSCursorFrameResizeDirections::Outward,
)
} else {
_windowResizeNorthEastCursor()
}
},
CursorIcon::NwResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::TopLeft,
NSCursorFrameResizeDirections::Outward,
)
} else {
_windowResizeNorthWestCursor()
}
},
CursorIcon::SeResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::BottomRight,
NSCursorFrameResizeDirections::Outward,
)
} else {
_windowResizeSouthEastCursor()
}
},
CursorIcon::SwResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::BottomLeft,
NSCursorFrameResizeDirections::Outward,
)
} else {
_windowResizeSouthWestCursor()
}
},
CursorIcon::NeswResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::TopRight,
NSCursorFrameResizeDirections::All,
)
} else {
_windowResizeNorthEastSouthWestCursor()
}
},
CursorIcon::NwseResize => {
if available!(macos = 15.0) {
NSCursor::frameResizeCursorFromPosition_inDirections(
NSCursorFrameResizePosition::TopLeft,
NSCursorFrameResizeDirections::All,
)
} else {
_windowResizeNorthWestSouthEastCursor()
}
},
CursorIcon::ColResize => {
if available!(macos = 15.0) {
NSCursor::columnResizeCursor()
} else {
NSCursor::resizeLeftRightCursor()
}
},
CursorIcon::RowResize => {
if available!(macos = 15.0) {
NSCursor::rowResizeCursor()
} else {
NSCursor::resizeUpDownCursor()
}
},
CursorIcon::ZoomIn => {
if available!(macos = 15.0) {
NSCursor::zoomInCursor()
} else {
_zoomInCursor()
}
},
CursorIcon::ZoomOut => {
if available!(macos = 15.0) {
NSCursor::zoomOutCursor()
} else {
_zoomOutCursor()
}
},
CursorIcon::Help => _helpCursor(),
CursorIcon::ZoomIn if available!(macos = 15.0) => NSCursor::zoomInCursor(),
CursorIcon::ZoomIn => _zoomInCursor(),
CursorIcon::ZoomOut if available!(macos = 15.0) => NSCursor::zoomOutCursor(),
CursorIcon::ZoomOut => _zoomOutCursor(),
CursorIcon::NeResize => _windowResizeNorthEastCursor(),
CursorIcon::NwResize => _windowResizeNorthWestCursor(),
CursorIcon::SeResize => _windowResizeSouthEastCursor(),
CursorIcon::SwResize => _windowResizeSouthWestCursor(),
CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(),
CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(),
// This is the wrong semantics for `Wait`, but it's the same as
// what's used in Safari and Chrome.
CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(),

View File

@@ -9,8 +9,12 @@ use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
NSApplicationWillTerminateNotification, NSWindow,
};
use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes};
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
use rwh_06::HasDisplayHandle;
use tracing::debug_span;
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
use winit_common::foundation::create_observer;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, RequestError};
@@ -27,8 +31,6 @@ use super::app_state::AppState;
use super::cursor::CustomCursor;
use super::event::dummy_event;
use super::monitor;
use super::notification_center::create_observer;
use super::observer::setup_control_flow_observers;
use crate::ActivationPolicy;
use crate::window::Window;
@@ -150,6 +152,10 @@ pub struct EventLoop {
// Though we do still need to keep the observers around to prevent them from being deallocated.
_did_finish_launching_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_will_terminate_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>,
_before_waiting_observer: MainRunLoopObserver,
_after_waiting_observer: MainRunLoopObserver,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -199,6 +205,7 @@ impl EventLoop {
// `applicationDidFinishLaunching:`
unsafe { NSApplicationDidFinishLaunchingNotification },
move |notification| {
let _entered = debug_span!("NSApplicationDidFinishLaunchingNotification").entered();
if let Some(app_state) = weak_app_state.upgrade() {
app_state.did_finish_launching(notification);
}
@@ -211,13 +218,45 @@ impl EventLoop {
// `applicationWillTerminate:`
unsafe { NSApplicationWillTerminateNotification },
move |notification| {
let _entered = debug_span!("NSApplicationWillTerminateNotification").entered();
if let Some(app_state) = weak_app_state.upgrade() {
app_state.will_terminate(notification);
}
},
);
setup_control_flow_observers(mtm);
let main_loop = MainRunLoop::get(mtm);
let mode = unsafe { kCFRunLoopCommonModes }.unwrap();
// Tracing observers have the lowest and highest orderings.
let _tracing_observers = tracing_observers(mtm).inspect(|(start, end)| {
main_loop.add_observer(start, mode);
main_loop.add_observer(end, mode);
});
let app_state_clone = Rc::clone(&app_state);
let _before_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::BeforeWaiting,
true,
// Queued with the second-lowest priority (tracing observers use the lowest) to ensure
// it is processed after other observers.
CFIndex::MAX - 1,
move |_| app_state_clone.cleared(),
);
main_loop.add_observer(&_before_waiting_observer, mode);
let app_state_clone = Rc::clone(&app_state);
let _after_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::AfterWaiting,
true,
// Queued with the second-highest priority (tracing observers use the highest) to
// ensure it is processed before other observers.
CFIndex::MIN + 1,
move |_| app_state_clone.wakeup(),
);
main_loop.add_observer(&_after_waiting_observer, mode);
Ok(EventLoop {
app,
@@ -225,6 +264,9 @@ impl EventLoop {
window_target: ActiveEventLoop { app_state, mtm },
_did_finish_launching_observer,
_will_terminate_observer,
_tracing_observers,
_before_waiting_observer,
_after_waiting_observer,
})
}

View File

@@ -76,7 +76,6 @@ mod event_loop;
mod ffi;
mod menu;
mod monitor;
mod notification_center;
mod observer;
mod view;
mod window;

View File

@@ -1,171 +1,10 @@
//! 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::ptr;
use std::time::Instant;
use objc2::MainThreadMarker;
use objc2_core_foundation::{
CFAbsoluteTimeGetCurrent, CFIndex, CFRetained, CFRunLoop, CFRunLoopActivity, CFRunLoopObserver,
CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopTimer, kCFRunLoopCommonModes,
kCFRunLoopDefaultMode,
CFAbsoluteTimeGetCurrent, CFRetained, CFRunLoop, CFRunLoopTimer, kCFRunLoopCommonModes,
};
use tracing::error;
use super::app_state::AppState;
// begin is queued with the highest priority to ensure it is processed before other observers
extern "C-unwind" fn control_flow_begin_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_info: *mut c_void,
) {
match activity {
CFRunLoopActivity::AfterWaiting => {
AppState::get(MainThreadMarker::new().unwrap()).wakeup();
},
_ => unreachable!(),
}
}
// end is queued with the lowest priority to ensure it is processed after other observers
// without that, LoopExiting would get sent after AboutToWait
extern "C-unwind" fn control_flow_end_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_info: *mut c_void,
) {
match activity {
CFRunLoopActivity::BeforeWaiting => {
AppState::get(MainThreadMarker::new().unwrap()).cleared();
},
CFRunLoopActivity::Exit => (), // unimplemented!(), // not expected to ever happen
_ => unreachable!(),
}
}
#[derive(Debug)]
pub struct RunLoop(CFRetained<CFRunLoop>);
impl RunLoop {
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(CFRunLoop::main().unwrap())
}
pub fn wakeup(&self) {
self.0.wake_up();
}
unsafe fn add_observer(
&self,
flags: CFRunLoopActivity,
// The lower the value, the sooner this will run
priority: CFIndex,
handler: CFRunLoopObserverCallBack,
context: *mut CFRunLoopObserverContext,
) {
let observer =
unsafe { CFRunLoopObserver::new(None, flags.0, true, priority, handler, context) }
.unwrap();
self.0.add_observer(Some(&observer), unsafe { 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) {
// 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.unwrap() };
// SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`.
unsafe { self.0.perform_block(Some(mode), Some(&block)) }
}
}
pub fn setup_control_flow_observers(mtm: MainThreadMarker) {
let run_loop = RunLoop::main(mtm);
unsafe {
let mut context = CFRunLoopObserverContext {
info: ptr::null_mut(),
version: 0,
retain: None,
release: None,
copyDescription: None,
};
run_loop.add_observer(
CFRunLoopActivity::AfterWaiting,
CFIndex::MIN,
Some(control_flow_begin_handler),
&mut context as *mut _,
);
run_loop.add_observer(
CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting,
CFIndex::MAX,
Some(control_flow_end_handler),
&mut context as *mut _,
);
}
}
#[derive(Debug)]
pub struct EventLoopWaker {

View File

@@ -1,37 +1,10 @@
use objc2_core_graphics::CGError;
use tracing::trace;
use winit_core::error::OsError;
macro_rules! os_error {
($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }};
}
macro_rules! trace_scope {
($s:literal) => {
let _crate = $crate::util::TraceGuard::new(module_path!(), $s);
};
}
pub(crate) struct TraceGuard {
module_path: &'static str,
called_from_fn: &'static str,
}
impl TraceGuard {
#[inline]
pub(crate) fn new(module_path: &'static str, called_from_fn: &'static str) -> Self {
trace!(target = module_path, "Triggered `{}`", called_from_fn);
Self { module_path, called_from_fn }
}
}
impl Drop for TraceGuard {
#[inline]
fn drop(&mut self) {
trace!(target = self.module_path, "Completed `{}`", self.called_from_fn);
}
}
#[track_caller]
pub(crate) fn cgerr(err: CGError) -> Result<(), OsError> {
if err == CGError::Success { Ok(()) } else { Err(os_error!(format!("CGError {err:?}"))) }

View File

@@ -1,22 +1,22 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::ptr;
use std::rc::Rc;
use dpi::{LogicalPosition, LogicalSize};
use dpi::{LogicalPosition, PhysicalSize};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Sel};
use objc2::{DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient,
NSTrackingRectTag, NSView, NSWindow,
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSViewLayerContentsRedrawPolicy, NSWindow,
};
use objc2_core_foundation::CGRect;
use objc2_foundation::{
NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString,
NSNotFound, NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
};
use tracing::{debug_span, trace_span};
use winit_core::event::{
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
PointerKind, PointerSource, TouchPhase, WindowEvent,
@@ -119,7 +119,6 @@ pub struct ViewState {
ime_size: Cell<NSSize>,
modifiers: Cell<Modifiers>,
phys_modifiers: RefCell<HashMap<Key, ModLocationMask>>,
tracking_rect: Cell<Option<NSTrackingRectTag>>,
ime_state: Cell<ImeState>,
input_source: RefCell<String>,
@@ -131,7 +130,6 @@ pub struct ViewState {
/// True if the current key event should be forwarded
/// to the application, even during IME
forward_key_to_app: Cell<bool>,
marked_text: RefCell<Retained<NSMutableAttributedString>>,
accepts_first_mouse: bool,
@@ -153,49 +151,35 @@ define_class!(
true
}
#[unsafe(method(viewDidMoveToWindow))]
fn view_did_move_to_window(&self) {
trace_scope!("viewDidMoveToWindow");
if let Some(tracking_rect) = self.ivars().tracking_rect.take() {
self.removeTrackingRect(tracking_rect);
}
let rect = self.frame();
let tracking_rect = unsafe {
self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false)
};
assert_ne!(tracking_rect, 0, "failed adding tracking rect");
self.ivars().tracking_rect.set(Some(tracking_rect));
}
// Not a normal method on `NSView`, it's triggered by `NSViewFrameDidChangeNotification`.
#[unsafe(method(viewFrameDidChangeNotification:))]
fn frame_did_change(&self, _notification: Option<&AnyObject>) {
trace_scope!("NSViewFrameDidChangeNotification");
if let Some(tracking_rect) = self.ivars().tracking_rect.take() {
self.removeTrackingRect(tracking_rect);
}
let rect = self.frame();
let tracking_rect = unsafe {
self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false)
};
assert_ne!(tracking_rect, 0, "failed adding tracking rect");
self.ivars().tracking_rect.set(Some(tracking_rect));
let _entered = debug_span!("NSViewFrameDidChangeNotification").entered();
// Emit resize event here rather than from windowDidResize because:
// 1. When a new window is created as a tab, the frame size may change without a window
// resize occurring.
// 2. Even when a window resize does occur on a new tabbed window, it contains the wrong
// size (includes tab height).
let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64);
let size = logical_size.to_physical::<u32>(self.scale_factor());
self.queue_event(WindowEvent::SurfaceResized(size));
self.surface_resized();
// During live resize, AppKit may not let the normal event loop reach its next redraw
// point before stretching the current layer contents. Redraw immediately after the
// app has observed the new surface size.
self.redraw_during_live_resize();
}
#[unsafe(method(viewDidChangeBackingProperties))]
fn view_did_change_backing_properties(&self) {
let _entered = debug_span!("viewDidChangeBackingProperties").entered();
// Moving between displays or changing scale can alter the drawable backing size
// without a matching frame-size change.
self.surface_resized();
self.redraw_during_live_resize();
}
#[unsafe(method(drawRect:))]
fn draw_rect(&self, _rect: NSRect) {
trace_scope!("drawRect:");
let _entered = debug_span!("drawRect:").entered();
self.ivars().app_state.handle_redraw(window_id(&self.window()));
@@ -204,7 +188,7 @@ define_class!(
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
trace_scope!("acceptsFirstResponder");
let _entered = trace_span!("acceptsFirstResponder").entered();
true
}
@@ -218,13 +202,13 @@ define_class!(
// extension for using `NSTouchBar`
#[unsafe(method_id(touchBar))]
fn touch_bar(&self) -> Option<Retained<NSObject>> {
trace_scope!("touchBar");
let _entered = debug_span!("touchBar").entered();
None
}
#[unsafe(method(resetCursorRects))]
fn reset_cursor_rects(&self) {
trace_scope!("resetCursorRects");
let _entered = debug_span!("resetCursorRects").entered();
let bounds = self.bounds();
let cursor_state = self.ivars().cursor_state.borrow();
// We correctly invoke `addCursorRect` only from inside `resetCursorRects`
@@ -239,13 +223,13 @@ define_class!(
unsafe impl NSTextInputClient for WinitView {
#[unsafe(method(hasMarkedText))]
fn has_marked_text(&self) -> bool {
trace_scope!("hasMarkedText");
let _entered = debug_span!("hasMarkedText").entered();
self.ivars().marked_text.borrow().length() > 0
}
#[unsafe(method(markedRange))]
fn marked_range(&self) -> NSRange {
trace_scope!("markedRange");
let _entered = debug_span!("markedRange").entered();
let length = self.ivars().marked_text.borrow().length();
if length > 0 {
NSRange::new(0, length)
@@ -257,7 +241,7 @@ define_class!(
#[unsafe(method(selectedRange))]
fn selected_range(&self) -> NSRange {
trace_scope!("selectedRange");
let _entered = debug_span!("selectedRange").entered();
// Documented to return `{NSNotFound, 0}` if there is no selection.
NSRange::new(NSNotFound as NSUInteger, 0)
}
@@ -270,7 +254,7 @@ define_class!(
_replacement_range: NSRange,
) {
// TODO: Use _replacement_range, requires changing the event to report surrounding text.
trace_scope!("setMarkedText:selectedRange:replacementRange:");
let _entered = debug_span!("setMarkedText:selectedRange:replacementRange:").entered();
let (marked_text, string) = if let Some(string) =
string.downcast_ref::<NSAttributedString>()
@@ -299,26 +283,31 @@ define_class!(
self.ivars().ime_state.set(ImeState::Ground);
}
let string = string.to_string();
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 {
// Convert the selected range from UTF-16 indices to UTF-8 indices.
let sub_string_a = string.substringToIndex(selected_range.location);
let sub_string_b = string.substringToIndex(selected_range.end());
let lowerbound_utf8 = sub_string_a.len();
let upperbound_utf8 = sub_string_b.len();
// Convert the selected range from UTF-16 code unit indices to UTF-8 byte
// offsets. `utf16_to_utf8_offset` is defensive: it snaps an offset that would
// split a surrogate pair down to the character boundary and clamps an
// out-of-bounds offset to the string length, so no `NSRangeException` is
// possible and the resulting range can never be inverted (`lower <= upper`).
// IMEs are known to send both mid-surrogate and out-of-bounds offsets (e.g.
// native Pinyin, see https://github.com/alacritty/alacritty/issues/8791).
let lowerbound_utf8 = utf16_to_utf8_offset(&string, selected_range.location);
let upperbound_utf8 = utf16_to_utf8_offset(&string, selected_range.end());
Some((lowerbound_utf8, upperbound_utf8))
};
// Send WindowEvent for updating marked text
self.queue_event(WindowEvent::Ime(Ime::Preedit(string.to_string(), cursor_range)));
self.queue_event(WindowEvent::Ime(Ime::Preedit(string, cursor_range)));
}
#[unsafe(method(unmarkText))]
fn unmark_text(&self) {
trace_scope!("unmarkText");
let _entered = debug_span!("unmarkText").entered();
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
let input_context = self.inputContext().expect("input context");
@@ -335,7 +324,7 @@ define_class!(
#[unsafe(method_id(validAttributesForMarkedText))]
fn valid_attributes_for_marked_text(&self) -> Retained<NSArray<NSAttributedStringKey>> {
trace_scope!("validAttributesForMarkedText");
let _entered = trace_span!("validAttributesForMarkedText").entered();
NSArray::new()
}
@@ -345,13 +334,14 @@ define_class!(
_range: NSRange,
_actual_range: *mut NSRange,
) -> Option<Retained<NSAttributedString>> {
trace_scope!("attributedSubstringForProposedRange:actualRange:");
let _entered =
trace_span!("attributedSubstringForProposedRange:actualRange:").entered();
None
}
#[unsafe(method(characterIndexForPoint:))]
fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger {
trace_scope!("characterIndexForPoint:");
let _entered = debug_span!("characterIndexForPoint:").entered();
0
}
@@ -361,7 +351,7 @@ define_class!(
_range: NSRange,
_actual_range: *mut NSRange,
) -> NSRect {
trace_scope!("firstRectForCharacterRange:actualRange:");
let _entered = debug_span!("firstRectForCharacterRange:actualRange:").entered();
// Guard when the view is no longer in a window during teardown.
let Some(window) = (**self).window() else {
@@ -377,7 +367,7 @@ define_class!(
#[unsafe(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:");
let _entered = debug_span!("insertText:replacementRange:").entered();
let string = if let Some(string) = string.downcast_ref::<NSAttributedString>() {
string.string().to_string()
@@ -402,7 +392,7 @@ define_class!(
// "human readable" character happens, i.e. newlines, tabs, and Ctrl+C.
#[unsafe(method(doCommandBySelector:))]
fn do_command_by_selector(&self, command: Sel) {
trace_scope!("doCommandBySelector:");
let _entered = debug_span!("doCommandBySelector:").entered();
// We shouldn't forward any character from just committed text, since we'll end up
// sending it twice with some IMEs like Korean one. We'll also always send
@@ -441,7 +431,7 @@ define_class!(
impl WinitView {
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
trace_scope!("keyDown:");
let _entered = debug_span!("keyDown:").entered();
{
let mut prev_input_source = self.ivars().input_source.borrow_mut();
let current_input_source = self.current_input_source();
@@ -500,7 +490,7 @@ define_class!(
#[unsafe(method(keyUp:))]
fn key_up(&self, event: &NSEvent) {
trace_scope!("keyUp:");
let _entered = debug_span!("keyUp:").entered();
let event = replace_event(event, self.option_as_alt());
self.update_modifiers(&event, false);
@@ -517,14 +507,14 @@ define_class!(
#[unsafe(method(flagsChanged:))]
fn flags_changed(&self, event: &NSEvent) {
trace_scope!("flagsChanged:");
let _entered = debug_span!("flagsChanged:").entered();
self.update_modifiers(event, true);
}
#[unsafe(method(insertTab:))]
fn insert_tab(&self, _sender: Option<&AnyObject>) {
trace_scope!("insertTab:");
let _entered = debug_span!("insertTab:").entered();
let window = self.window();
if let Some(first_responder) = window.firstResponder() {
if *first_responder == ***self {
@@ -535,7 +525,7 @@ define_class!(
#[unsafe(method(insertBackTab:))]
fn insert_back_tab(&self, _sender: Option<&AnyObject>) {
trace_scope!("insertBackTab:");
let _entered = debug_span!("insertBackTab:").entered();
let window = self.window();
if let Some(first_responder) = window.firstResponder() {
if *first_responder == ***self {
@@ -549,7 +539,7 @@ define_class!(
#[unsafe(method(cancelOperation:))]
fn cancel_operation(&self, _sender: Option<&AnyObject>) {
let mtm = MainThreadMarker::from(self);
trace_scope!("cancelOperation:");
let _entered = debug_span!("cancelOperation:").entered();
let event = NSApplication::sharedApplication(mtm)
.currentEvent()
@@ -578,71 +568,73 @@ define_class!(
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &NSEvent) {
trace_scope!("mouseDown:");
let _entered = debug_span!("mouseDown:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Pressed);
}
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, event: &NSEvent) {
trace_scope!("mouseUp:");
let _entered = debug_span!("mouseUp:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Released);
}
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, event: &NSEvent) {
trace_scope!("rightMouseDown:");
let _entered = debug_span!("rightMouseDown:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Pressed);
}
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, event: &NSEvent) {
trace_scope!("rightMouseUp:");
let _entered = debug_span!("rightMouseUp:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Released);
}
#[unsafe(method(otherMouseDown:))]
fn other_mouse_down(&self, event: &NSEvent) {
trace_scope!("otherMouseDown:");
let _entered = debug_span!("otherMouseDown:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Pressed);
}
#[unsafe(method(otherMouseUp:))]
fn other_mouse_up(&self, event: &NSEvent) {
trace_scope!("otherMouseUp:");
let _entered = debug_span!("otherMouseUp:").entered();
self.mouse_motion(event);
self.mouse_click(event, ElementState::Released);
}
// No tracing on these because that would be overly verbose
#[unsafe(method(mouseMoved:))]
fn mouse_moved(&self, event: &NSEvent) {
let _entered = debug_span!("mouseMoved:").entered();
self.mouse_motion(event);
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &NSEvent) {
let _entered = debug_span!("mouseDragged:").entered();
self.mouse_motion(event);
}
#[unsafe(method(rightMouseDragged:))]
fn right_mouse_dragged(&self, event: &NSEvent) {
let _entered = debug_span!("rightMouseDragged:").entered();
self.mouse_motion(event);
}
#[unsafe(method(otherMouseDragged:))]
fn other_mouse_dragged(&self, event: &NSEvent) {
let _entered = debug_span!("otherMouseDragged:").entered();
self.mouse_motion(event);
}
#[unsafe(method(mouseEntered:))]
fn mouse_entered(&self, event: &NSEvent) {
trace_scope!("mouseEntered:");
let _entered = debug_span!("mouseEntered:").entered();
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
@@ -656,7 +648,7 @@ define_class!(
#[unsafe(method(mouseExited:))]
fn mouse_exited(&self, event: &NSEvent) {
trace_scope!("mouseExited:");
let _entered = debug_span!("mouseExited:").entered();
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
@@ -670,7 +662,7 @@ define_class!(
#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, event: &NSEvent) {
trace_scope!("scrollWheel:");
let _entered = debug_span!("scrollWheel:").entered();
self.mouse_motion(event);
@@ -709,7 +701,7 @@ define_class!(
#[unsafe(method(magnifyWithEvent:))]
fn magnify_with_event(&self, event: &NSEvent) {
trace_scope!("magnifyWithEvent:");
let _entered = debug_span!("magnifyWithEvent:").entered();
self.mouse_motion(event);
@@ -731,7 +723,7 @@ define_class!(
#[unsafe(method(smartMagnifyWithEvent:))]
fn smart_magnify_with_event(&self, event: &NSEvent) {
trace_scope!("smartMagnifyWithEvent:");
let _entered = debug_span!("smartMagnifyWithEvent:").entered();
self.mouse_motion(event);
@@ -740,7 +732,7 @@ define_class!(
#[unsafe(method(rotateWithEvent:))]
fn rotate_with_event(&self, event: &NSEvent) {
trace_scope!("rotateWithEvent:");
let _entered = debug_span!("rotateWithEvent:").entered();
self.mouse_motion(event);
@@ -762,7 +754,7 @@ define_class!(
#[unsafe(method(pressureChangeWithEvent:))]
fn pressure_change_with_event(&self, event: &NSEvent) {
trace_scope!("pressureChangeWithEvent:");
let _entered = debug_span!("pressureChangeWithEvent:").entered();
self.queue_event(WindowEvent::TouchpadPressure {
device_id: None,
@@ -776,13 +768,13 @@ define_class!(
// https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816
#[unsafe(method(_wantsKeyDownForEvent:))]
fn wants_key_down_for_event(&self, _event: &NSEvent) -> bool {
trace_scope!("_wantsKeyDownForEvent:");
let _entered = debug_span!("_wantsKeyDownForEvent:").entered();
true
}
#[unsafe(method(acceptsFirstMouse:))]
fn accepts_first_mouse(&self, _event: &NSEvent) -> bool {
trace_scope!("acceptsFirstMouse:");
let _entered = debug_span!("acceptsFirstMouse:").entered();
self.ivars().accepts_first_mouse
}
}
@@ -802,7 +794,6 @@ impl WinitView {
ime_size: Default::default(),
modifiers: Default::default(),
phys_modifiers: Default::default(),
tracking_rect: Default::default(),
ime_state: Default::default(),
input_source: Default::default(),
ime_capabilities: Default::default(),
@@ -812,9 +803,56 @@ impl WinitView {
option_as_alt: Cell::new(option_as_alt),
});
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
*this.ivars().input_source.borrow_mut() = this.current_input_source();
// Ask AppKit to redisplay the layer while the view is being resized so layer-backed
// surfaces keep painting.
this.setLayerContentsRedrawPolicy(NSViewLayerContentsRedrawPolicy::DuringViewResize);
// `MouseEnteredAndExited` enables receiving events through `mouseEntered:` and
// `mouseExited:`.
//
// `MouseMoved` enables receiving events through `mouseMoved:`
//
// We do not set `CursorUpdate` because it is part of the "flexible" alternative to
// `cursorRect` based cursor image updates, and we currently still use
// `cursorRect`s. We also can't really switch to this approach because "The
// cursorUpdate(with:) message is not sent when the NSTrackingCursorUpdate option is
// specified along with [`ActiveAlways`]."
//
// `ActiveAlways` indicates we want to receive events when the window is not
// focused ("key window" in Cocoa terms), which matches the behavior on other
// platforms.
//
// We do not set `AssumeInside` because we want to avoid emitting `Left` events without a
// correspondering `Entered` to our consumers, and not setting this flag tells AppKit to
// handle this for us by synthesizing entry and exit events in some cases.
//
// `InVisibleRect` instructs the tracking area's `owner` (our `NSView`) to ignore the value
// we provide in `rect` and keep the tracking area's bounds up to date with the
// current view bounds automatically.
//
// We do not set `EnabledDuringMouseDrag` to match the platform behavior on Windows
// and Wayland, since neither emit events while being dragged over with an empty
// cursor without focus.
//
// See also https://developer.apple.com/documentation/appkit/nstrackingareaoptions.
// Safety: the type of `owner` should be `NSView` and is.
// The type of `user_info` is irrelevant because it is None.
this.addTrackingArea(&*unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
NSRect::ZERO,
NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect,
Some(&this),
None,
)
});
this
}
@@ -829,6 +867,37 @@ impl WinitView {
});
}
fn surface_resized(&self) {
let Some(window) = (**self).window() else {
return;
};
let size = self.surface_size();
let window_id = window_id(&window);
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, WindowEvent::SurfaceResized(size));
});
}
/// Returns the drawable size from the view's backing-coordinate bounds.
pub(super) fn surface_size(&self) -> PhysicalSize<u32> {
// The view bounds are authoritative for full-size content views and during live resize.
// Deriving this from the window frame can exclude custom titlebar content or be stale.
let backing_bounds = self.convertRectToBacking(self.bounds());
PhysicalSize::new(
backing_bounds.size.width.round().max(0.0) as u32,
backing_bounds.size.height.round().max(0.0) as u32,
)
}
fn redraw_during_live_resize(&self) {
let Some(window) = (**self).window() else {
return;
};
if window.inLiveResize() {
self.ivars().app_state.handle_redraw(window_id(&window));
}
}
fn scale_factor(&self) -> f64 {
self.window().backingScaleFactor() as f64
}
@@ -866,24 +935,33 @@ impl WinitView {
false
}
}
pub(super) fn enable_ime(&self, capabilities: ImeCapabilities) {
// This seems reasonable but the prior behavior of `set_ime_allowed` doesn't do this
// (it was also broken but let's not break things worse)
pub(super) fn set_ime_allowed(&self, capabilities: Option<ImeCapabilities>) {
if self.ivars().ime_capabilities.get().is_some() {
return;
}
self.ivars().ime_capabilities.set(capabilities);
if capabilities.is_some() {
return;
}
// Clear markedText
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
// if self.ivars().ime_capabilities.get().is_none() {
// self.ivars().ime_state.set(ImeState::Ground);
// }
// why are we disabling things in an enable fn? who knows. it's what the previous one did
// though
if self.ivars().ime_state.get() != ImeState::Disabled {
self.ivars().ime_state.set(ImeState::Disabled);
self.queue_event(WindowEvent::Ime(Ime::Disabled));
}
self.ivars().ime_capabilities.set(Some(capabilities));
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
}
pub(super) fn disable_ime(&self) {
// see above
self.ivars().ime_capabilities.set(None);
if self.ivars().ime_state.get() != ImeState::Disabled {
self.ivars().ime_state.set(ImeState::Disabled);
self.queue_event(WindowEvent::Ime(Ime::Disabled));
}
// we probably don't need to do this, but again this mirrors the prior behavior of
// `set_ime_allowed`
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
}
pub(super) fn ime_capabilities(&self) -> Option<ImeCapabilities> {
@@ -1136,3 +1214,92 @@ fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained<NSEven
event.copy()
}
}
/// Convert a UTF-16 code unit offset into the corresponding UTF-8 byte offset within `s`.
///
/// IMEs are not required to send well-formed offsets, so this is defensive: an offset that
/// would split a surrogate pair is snapped down to the start of that character, and an
/// out-of-bounds offset is clamped to the end of the string (e.g. native Pinyin sends
/// out-of-bounds indices, see <https://github.com/alacritty/alacritty/issues/8791>).
///
/// The mapping is monotone non-decreasing, so applying it to the location and end of an
/// `NSRange` (where `location <= end`) can never produce an inverted byte range.
fn utf16_to_utf8_offset(s: &str, utf16_offset: usize) -> usize {
let mut utf16_pos = 0;
for (utf8_pos, ch) in s.char_indices() {
if utf16_pos >= utf16_offset {
return utf8_pos;
}
utf16_pos += ch.len_utf16();
// The target offset lands strictly inside this character's UTF-16 representation,
// i.e. it splits a surrogate pair: snap down to the character boundary.
if utf16_pos > utf16_offset {
return utf8_pos;
}
}
s.len()
}
#[cfg(test)]
mod tests {
use super::*;
/// Apply the UTF-16 -> UTF-8 conversion to both ends of a `selectedRange {loc, len}`,
/// mirroring what `set_marked_text` does for the emitted `Ime::Preedit` cursor range.
fn convert(s: &str, loc: usize, len: usize) -> (usize, usize) {
(utf16_to_utf8_offset(s, loc), utf16_to_utf8_offset(s, loc + len))
}
#[test]
fn mid_surrogate_offset_snaps_down() {
// "😀a": 😀 is one char = 2 UTF-16 units = 4 UTF-8 bytes; offset 1 is mid-pair.
assert_eq!(utf16_to_utf8_offset("\u{1F600}a", 1), 0);
// Offset 2 is the boundary just after the pair.
assert_eq!(utf16_to_utf8_offset("\u{1F600}a", 2), 4);
}
#[test]
fn no_longer_inverted() {
// "a😀b" with selectedRange {1,1}: previously emitted (1, 0) -- lower > upper, a
// slice-panic vector. The boundary-snapping conversion keeps lower <= upper.
assert_eq!(convert("a\u{1F600}b", 1, 1), (1, 1));
}
#[test]
fn prefix_preserved_on_mid_pair_collapse() {
// "a😀b" with selectedRange {2,0}: previously collapsed to (0, 0), discarding the
// valid "a" prefix; now snaps to the char boundary after "a".
assert_eq!(convert("a\u{1F600}b", 2, 0), (1, 1));
}
#[test]
fn out_of_bounds_clamps_to_len() {
// Subsumes the #4494 `.min(len)` clamp: an out-of-bounds offset maps to the string
// length instead of triggering an NSRangeException.
assert_eq!(convert("\u{1F600}a", 99, 0), (5, 5));
}
#[test]
fn well_formed_inputs_are_identity() {
// The common case (well-formed boundary indices) must be byte-for-byte unchanged.
assert_eq!(convert("a\u{1F600}b", 3, 0), (5, 5));
assert_eq!(convert("a\u{1F600}b", 4, 0), (6, 6));
// BMP multi-byte (Japanese): each char is 1 UTF-16 unit and 3 UTF-8 bytes.
assert_eq!(convert("\u{3053}\u{3093}", 1, 1), (3, 6));
}
#[test]
fn monotone_non_decreasing() {
// Sweep every UTF-16 offset (including out-of-bounds) over a string mixing BMP and
// non-BMP characters and assert the conversion never goes backwards, which is what
// guarantees `lower <= upper` for any `NSRange`.
let s = "a\u{1F600}b\u{3053}\u{1F4A9}c";
let mut prev = 0;
for off in 0..=20 {
let cur = utf16_to_utf8_offset(s, off);
assert!(cur >= prev, "non-monotone at offset {off}: {cur} < {prev}");
assert!(cur <= s.len(), "offset {off} mapped past end: {cur} > {}", s.len());
prev = cur;
}
}
}

View File

@@ -8,6 +8,7 @@ use objc2::rc::{Retained, autoreleasepool};
use objc2::{MainThreadMarker, Message, define_class};
use objc2_app_kit::{NSPanel, NSResponder, NSWindow};
use objc2_foundation::NSObject;
use tracing::trace_span;
use winit_core::cursor::Cursor;
use winit_core::error::RequestError;
use winit_core::icon::Icon;
@@ -350,13 +351,13 @@ define_class!(
impl WinitWindow {
#[unsafe(method(canBecomeMainWindow))]
fn can_become_main_window(&self) -> bool {
trace_scope!("canBecomeMainWindow");
let _entered = trace_span!("canBecomeMainWindow").entered();
true
}
#[unsafe(method(canBecomeKeyWindow))]
fn can_become_key_window(&self) -> bool {
trace_scope!("canBecomeKeyWindow");
let _entered = trace_span!("canBecomeKeyWindow").entered();
true
}
}
@@ -374,7 +375,7 @@ define_class!(
// it doesn't if window doesn't have NSWindowStyleMask::Titled
#[unsafe(method(canBecomeKeyWindow))]
fn can_become_key_window(&self) -> bool {
trace_scope!("canBecomeKeyWindow");
let _entered = trace_span!("canBecomeKeyWindow").entered();
true
}
}

View File

@@ -41,7 +41,8 @@ use objc2_foundation::{
NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint,
NSRect, NSSize, NSString, ns_string,
};
use tracing::{trace, warn};
use tracing::{debug_span, trace, warn};
use winit_common::core_foundation::MainRunLoop;
use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
@@ -56,7 +57,6 @@ use super::app_state::AppState;
use super::cursor::{CustomCursor, cursor_from_icon};
use super::ffi;
use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_display_id};
use super::observer::RunLoop;
use super::util::cgerr;
use super::view::WinitView;
use super::window::{WinitPanel, WinitWindow, window_id};
@@ -121,14 +121,14 @@ define_class!(
unsafe impl NSWindowDelegate for WindowDelegate {
#[unsafe(method(windowShouldClose:))]
fn window_should_close(&self, _: Option<&AnyObject>) -> bool {
trace_scope!("windowShouldClose:");
let _entered = debug_span!("windowShouldClose:").entered();
self.queue_event(WindowEvent::CloseRequested);
false
}
#[unsafe(method(windowWillClose:))]
fn window_will_close(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillClose:");
let _entered = debug_span!("windowWillClose:").entered();
// `setDelegate:` retains the previous value and then autoreleases it
autoreleasepool(|_| {
// Since El Capitan, we need to be careful that delegate methods can't
@@ -140,14 +140,14 @@ define_class!(
#[unsafe(method(windowDidResize:))]
fn window_did_resize(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidResize:");
let _entered = debug_span!("windowDidResize:").entered();
// NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification.
self.emit_move_event();
}
#[unsafe(method(windowWillStartLiveResize:))]
fn window_will_start_live_resize(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillStartLiveResize:");
let _entered = debug_span!("windowWillStartLiveResize:").entered();
let increments = self.ivars().surface_resize_increments.get();
self.set_resize_increments_inner(increments);
@@ -155,20 +155,20 @@ define_class!(
#[unsafe(method(windowDidEndLiveResize:))]
fn window_did_end_live_resize(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidEndLiveResize:");
let _entered = debug_span!("windowDidEndLiveResize:").entered();
self.set_resize_increments_inner(NSSize::new(1., 1.));
}
// This won't be triggered if the move was part of a resize.
#[unsafe(method(windowDidMove:))]
fn window_did_move(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidMove:");
let _entered = debug_span!("windowDidMove:").entered();
self.emit_move_event();
}
#[unsafe(method(windowDidChangeBackingProperties:))]
fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeBackingProperties:");
let _entered = debug_span!("windowDidChangeBackingProperties:").entered();
let scale_factor = self.scale_factor();
if scale_factor == self.ivars().previous_scale_factor.get() {
return;
@@ -177,14 +177,14 @@ define_class!(
let mtm = MainThreadMarker::from(self);
let this = self.retain();
RunLoop::main(mtm).queue_closure(move || {
MainRunLoop::get(mtm).queue_closure(move || {
this.handle_scale_factor_changed(scale_factor);
});
}
#[unsafe(method(windowDidBecomeKey:))]
fn window_did_become_key(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidBecomeKey:");
let _entered = debug_span!("windowDidBecomeKey:").entered();
// TODO: center the cursor if the window had mouse grab when it
// lost focus
self.queue_event(WindowEvent::Focused(true));
@@ -192,7 +192,7 @@ define_class!(
#[unsafe(method(windowDidResignKey:))]
fn window_did_resign_key(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidResignKey:");
let _entered = debug_span!("windowDidResignKey:").entered();
// It happens rather often, e.g. when the user is Cmd+Tabbing, that the
// NSWindowDelegate will receive a didResignKey event despite no event
// being received when the modifiers are released. This is because
@@ -208,7 +208,7 @@ define_class!(
/// Invoked when before enter fullscreen
#[unsafe(method(windowWillEnterFullScreen:))]
fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillEnterFullScreen:");
let _entered = debug_span!("windowWillEnterFullScreen:").entered();
self.ivars().maximized.set(self.is_zoomed());
let mut fullscreen = self.ivars().fullscreen.borrow_mut();
@@ -236,7 +236,7 @@ define_class!(
/// Invoked when before exit fullscreen
#[unsafe(method(windowWillExitFullScreen:))]
fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillExitFullScreen:");
let _entered = debug_span!("windowWillExitFullScreen:").entered();
self.ivars().in_fullscreen_transition.set(true);
}
@@ -247,7 +247,7 @@ define_class!(
_: Option<&AnyObject>,
proposed_options: NSApplicationPresentationOptions,
) -> NSApplicationPresentationOptions {
trace_scope!("window:willUseFullScreenPresentationOptions:");
let _entered = debug_span!("window:willUseFullScreenPresentationOptions:").entered();
// Generally, games will want to disable the menu bar and the dock. Ideally,
// this would be configurable by the user. Unfortunately because of our
// `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is
@@ -270,7 +270,7 @@ define_class!(
/// Invoked when entered fullscreen
#[unsafe(method(windowDidEnterFullScreen:))]
fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidEnterFullScreen:");
let _entered = debug_span!("windowDidEnterFullScreen:").entered();
self.ivars().initial_fullscreen.set(false);
self.ivars().in_fullscreen_transition.set(false);
if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() {
@@ -281,7 +281,7 @@ define_class!(
/// Invoked when exited fullscreen
#[unsafe(method(windowDidExitFullScreen:))]
fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidExitFullScreen:");
let _entered = debug_span!("windowDidExitFullScreen:").entered();
self.restore_state_from_fullscreen();
self.ivars().in_fullscreen_transition.set(false);
@@ -308,7 +308,7 @@ define_class!(
/// work you may have done to prepare to enter full-screen mode.
#[unsafe(method(windowDidFailToEnterFullScreen:))]
fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidFailToEnterFullScreen:");
let _entered = debug_span!("windowDidFailToEnterFullScreen:").entered();
self.ivars().in_fullscreen_transition.set(false);
self.ivars().target_fullscreen.replace(None);
if self.ivars().initial_fullscreen.get() {
@@ -327,14 +327,28 @@ define_class!(
// Invoked when the occlusion state of the window changes
#[unsafe(method(windowDidChangeOcclusionState:))]
fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeOcclusionState:");
let _entered = debug_span!("windowDidChangeOcclusionState:").entered();
let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible);
self.queue_event(WindowEvent::Occluded(!visible));
// We do this here rather than in `window:willUseFullScreenPresentationOptions`
// because doing it there leaves the menu bar interactable despite being hidden.
// Moving it here produces the most consistent results.
if self.is_borderless_game()
&& matches!(*self.ivars().fullscreen.borrow(), Some(Fullscreen::Borderless(_)))
{
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
app.setPresentationOptions(
NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar,
);
}
}
#[unsafe(method(windowDidChangeScreen:))]
fn window_did_change_screen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeScreen:");
let _entered = debug_span!("windowDidChangeScreen:").entered();
let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get();
if is_simple_fullscreen {
if let Some(screen) = self.window().screen() {
@@ -348,7 +362,7 @@ define_class!(
/// Invoked when the dragged image enters destination bounds or frame
#[unsafe(method(draggingEntered:))]
fn dragging_entered(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
trace_scope!("draggingEntered:");
let _entered = debug_span!("draggingEntered:").entered();
use std::path::PathBuf;
@@ -379,7 +393,7 @@ define_class!(
#[unsafe(method(wantsPeriodicDraggingUpdates))]
fn wants_periodic_dragging_updates(&self) -> bool {
trace_scope!("wantsPeriodicDraggingUpdates:");
let _entered = debug_span!("wantsPeriodicDraggingUpdates:").entered();
true
}
@@ -387,7 +401,7 @@ define_class!(
/// modification of the dragging operation or mouse-pointer position.
#[unsafe(method(draggingUpdated:))]
fn dragging_updated(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
trace_scope!("draggingUpdated:");
let _entered = debug_span!("draggingUpdated:").entered();
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
@@ -402,14 +416,14 @@ define_class!(
/// Invoked when the image is released
#[unsafe(method(prepareForDragOperation:))]
fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool {
trace_scope!("prepareForDragOperation:");
let _entered = debug_span!("prepareForDragOperation:").entered();
true
}
/// Invoked after the released image has been removed from the screen
#[unsafe(method(performDragOperation:))]
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
trace_scope!("performDragOperation:");
let _entered = debug_span!("performDragOperation:").entered();
use std::path::PathBuf;
@@ -441,13 +455,13 @@ define_class!(
/// Invoked when the dragging operation is complete
#[unsafe(method(concludeDragOperation:))]
fn conclude_drag_operation(&self, _sender: Option<&NSObject>) {
trace_scope!("concludeDragOperation:");
let _entered = debug_span!("concludeDragOperation:").entered();
}
/// Invoked when the dragging operation is cancelled
#[unsafe(method(draggingExited:))]
fn dragging_exited(&self, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
trace_scope!("draggingExited:");
let _entered = debug_span!("draggingExited:").entered();
let position = sender.map(|sender| {
let dl = sender.draggingLocation();
@@ -469,7 +483,7 @@ define_class!(
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
trace_scope!("observeValueForKeyPath:ofObject:change:context:");
let _entered = debug_span!("observeValueForKeyPath:ofObject:change:context:").entered();
// 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")) {
@@ -898,10 +912,7 @@ impl WindowDelegate {
fn handle_scale_factor_changed(&self, scale_factor: CGFloat) {
let window = self.window();
let content_size = window.contentRectForFrameRect(window.frame()).size;
let content_size = LogicalSize::new(content_size.width, content_size.height);
let suggested_size = content_size.to_physical(scale_factor);
let suggested_size = self.view().surface_size();
let new_surface_size = Arc::new(Mutex::new(suggested_size));
self.queue_event(WindowEvent::ScaleFactorChanged {
scale_factor,
@@ -1026,9 +1037,7 @@ impl WindowDelegate {
#[inline]
pub fn surface_size(&self) -> PhysicalSize<u32> {
let content_rect = self.window().contentRectForFrameRect(self.window().frame());
let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height);
logical.to_physical(self.scale_factor())
self.view().surface_size()
}
#[inline]
@@ -1534,7 +1543,7 @@ impl WindowDelegate {
}
match (old_fullscreen, fullscreen) {
(None, Some(fullscreen)) => {
(None, Some(_)) => {
// `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
// set a normal style temporarily. The previous state will be
// restored in `WindowDelegate::window_did_exit_fullscreen`.
@@ -1545,16 +1554,6 @@ impl WindowDelegate {
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::HideDock
| NSApplicationPresentationOptions::HideMenuBar;
app.setPresentationOptions(presentation_options);
}
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Borderless(_)), None) => {
@@ -1682,7 +1681,7 @@ impl WindowDelegate {
if current_caps.is_some() {
return Err(ImeRequestError::AlreadyEnabled);
}
self.view().set_ime_allowed(Some(capabilities));
self.view().enable_ime(capabilities);
request_data
},
ImeRequest::Update(request_data) => {
@@ -1692,7 +1691,7 @@ impl WindowDelegate {
request_data
},
ImeRequest::Disable => {
self.view().set_ime_allowed(None);
self.view().disable_ime();
return Ok(());
},
};

View File

@@ -18,7 +18,10 @@ x11 = ["xkbcommon-dl?/x11", "dep:x11-dl"]
xkb = ["dep:xkbcommon-dl", "dep:smol_str"]
# CoreFoundation
core-foundation = ["dep:objc2", "dep:objc2-core-foundation"]
core-foundation = ["dep:block2", "dep:objc2", "dep:objc2-core-foundation"]
# Foundation
foundation = ["dep:block2", "dep:objc2", "dep:objc2-foundation"]
[dependencies]
smol_str = { workspace = true, optional = true }
@@ -30,13 +33,23 @@ memmap2 = { workspace = true, optional = true }
x11-dl = { workspace = true, optional = true }
xkbcommon-dl = { workspace = true, optional = true }
# CoreFoundation
# Foundation / CoreFoundation
block2 = { workspace = true, optional = true }
objc2 = { workspace = true, optional = true }
objc2-core-foundation = { workspace = true, optional = true, features = [
"std",
"block2",
"objc2",
"CFRunLoop",
"CFString",
] }
objc2-foundation = { workspace = true, optional = true, features = [
"std",
"block2",
"NSNotification",
"NSString",
"NSOperation",
] }
[package.metadata.docs.rs]
all-features = true

View File

@@ -0,0 +1,136 @@
//! Various utilities for interfacing with the main run loop.
//!
//! These allow for using closures without the `Send + Sync` requirement that is otherwise required
//! when interfacing with run loops.
//!
//! See also <https://github.com/madsmtm/objc2/issues/696> for work on this at a lower level.
use std::cell::Cell;
use objc2::MainThreadMarker;
use objc2_core_foundation::{CFRetained, CFRunLoop, CFRunLoopMode, kCFRunLoopDefaultMode};
use tracing::{Span, error};
use super::MainRunLoopObserver;
/// Wrapper around [`CFRunLoop::main`].
#[derive(Debug)]
pub struct MainRunLoop {
/// This is on the main thread.
_mtm: MainThreadMarker,
/// A cached reference to the main run loop.
main_run_loop: CFRetained<CFRunLoop>,
}
impl MainRunLoop {
/// Get the main run loop.
pub fn get(_mtm: MainThreadMarker) -> Self {
let main_run_loop = CFRunLoop::main().unwrap();
Self { _mtm, main_run_loop }
}
/// Get a reference to the underlying [`CFRunLoop`].
pub fn run_loop(&self) -> &CFRunLoop {
&self.main_run_loop
}
/// Wake the main run loop.
pub fn wake_up(&self) {
self.main_run_loop.wake_up();
}
/// 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:]` to queue a closure to run
/// the next time runloop sources are processed.
///
/// 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) {
// We use this to run a closure asynchronously at a later point, so it also makes sense to
// re-enter the current span when running the queued closure.
let span = Span::current();
// Convert `FnOnce()` to `Block<dyn Fn()>`.
let closure = Cell::new(Some(closure));
let block = block2::RcBlock::new(move || {
// Running this block happens inside a `CFRunLoopSource`, but the spans that we emit for
// that are (intentionally) overwritten by entering the span from before.
let _enter = span.enter();
debug_assert!(MainThreadMarker::new().is_some());
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.unwrap() };
let _ = self._mtm;
// SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`.
//
// Additionally, we have a `MainThreadMarker` here, which means we know we're on the main
// thread. We also know that the run loop is the main-thread run loop, so scheduling a
// non-`Send` block to that is allowed.
unsafe { self.main_run_loop.perform_block(Some(mode), Some(&block)) }
}
/// Add an observer to the main run loop.
pub fn add_observer(&self, observer: &MainRunLoopObserver, mode: &CFRunLoopMode) {
// Accessing the `MainObserver`'s observer is fine here, since we're adding it to the main
// run loop (which is on the same thread that the observer was created on).
self.main_run_loop.add_observer(Some(&observer.observer), Some(mode));
}
/// Remove an observer from the main run loop.
///
/// This is also done automatically when the [`MainRunLoopObserver`] is dropped.
pub fn remove_observer(&self, observer: &MainRunLoopObserver, mode: &CFRunLoopMode) {
// Same as in `add_observer`, accessing the main loop's observer is fine.
self.main_run_loop.add_observer(Some(&observer.observer), Some(mode));
}
}

View File

@@ -0,0 +1,55 @@
use block2::RcBlock;
use objc2::MainThreadMarker;
use objc2_core_foundation::{
CFIndex, CFRetained, CFRunLoopActivity, CFRunLoopObserver, kCFAllocatorDefault,
};
/// A [`CFRunLoopObserver`] on the main thread.
///
/// This type has "ownership" semantics, and invalidates the observer when dropped.
#[derive(Debug)]
pub struct MainRunLoopObserver {
/// This must be private, otherwise the user might add it to an arbitrary run loop (but this
/// observer is not designed to only be on the main thread).
pub(crate) observer: CFRetained<CFRunLoopObserver>,
}
impl MainRunLoopObserver {
/// Create a new run loop observer that observes the main run loop.
pub fn new(
mtm: MainThreadMarker,
activities: CFRunLoopActivity,
repeats: bool,
// The lower the value, the sooner this will run (inverse of a "priority").
order: CFIndex,
callback: impl Fn(CFRunLoopActivity) + 'static,
) -> Self {
let block = RcBlock::new(move |_: *mut _, activity| {
debug_assert!(MainThreadMarker::new().is_some());
callback(activity)
});
let _ = mtm;
// SAFETY: The callback is not Send + Sync, which would normally be unsound, but since we
// restrict the callback to only ever be on the main thread (by taking `MainThreadMarker`,
// and in `MainRunLoop::add_observer`), the callback doesn't have to be thread safe.
let observer = unsafe {
CFRunLoopObserver::with_handler(
kCFAllocatorDefault,
activities.0,
repeats,
order,
Some(&block),
)
}
.unwrap();
Self { observer }
}
}
impl Drop for MainRunLoopObserver {
fn drop(&mut self) {
self.observer.invalidate();
}
}

View File

@@ -1,3 +1,14 @@
mod event_loop_proxy;
//! Various wrappers around [`CFRunLoop`][objc2_core_foundation::CFRunLoop].
//!
//! See Apple's documentation on Run Loops for details:
//! <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html>
pub use event_loop_proxy::*;
mod event_loop_proxy;
mod main_run_loop;
mod main_run_loop_observer;
mod tracing_observers;
pub use self::event_loop_proxy::*;
pub use self::main_run_loop::*;
pub use self::main_run_loop_observer::*;
pub use self::tracing_observers::*;

View File

@@ -0,0 +1,146 @@
use std::cell::RefCell;
use std::rc::Rc;
use objc2::MainThreadMarker;
use objc2_core_foundation::{CFIndex, CFRunLoop, CFRunLoopActivity, kCFRunLoopDefaultMode};
use tracing::span::EnteredSpan;
use tracing::{Level, error, field, span_enabled, trace_span};
use crate::core_foundation::MainRunLoopObserver;
/// Create two run loop observers that add TRACE-level [spans][tracing::span].
///
/// This is useful when debugging run loops, it makes it easier to see in which run loop activity an
/// event is triggered inside (if any).
///
/// When debugging these interactions, it can also be useful to configure your tracing subscriber
/// with `.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)` to emit events upon
/// entering and exiting these stages.
pub fn tracing_observers(
mtm: MainThreadMarker,
) -> Option<(MainRunLoopObserver, MainRunLoopObserver)> {
// Observers are a bit costly, so don't create them if the tracing-level for this module is
// configured to disable them.
if !span_enabled!(Level::TRACE) {
return None;
}
/// The state that we think the runloop is currently in.
///
/// The order of activities we observe if waiting twice looks something like:
/// - CFRunLoopActivity::Entry
/// - CFRunLoopActivity::BeforeTimers
/// - CFRunLoopActivity::BeforeSources
/// - CFRunLoopActivity::BeforeWaiting
/// - CFRunLoopActivity::AfterWaiting
/// - CFRunLoopActivity::BeforeTimers
/// - CFRunLoopActivity::BeforeSources
/// - CFRunLoopActivity::BeforeWaiting
/// - CFRunLoopActivity::AfterWaiting
/// - CFRunLoopActivity::Exit
///
/// And if not waiting, it looks something like:
/// - CFRunLoopActivity::Entry
/// - CFRunLoopActivity::BeforeTimers
/// - CFRunLoopActivity::BeforeSources
/// - CFRunLoopActivity::Exit
#[derive(Default)]
#[allow(unused)] // EnteredSpans are kept around
enum RunLoopState {
/// Currently processing `Entry`/`Exit` observers.
#[default]
Entered,
/// Currently processing timers or `BeforeTimers` observers.
Timers(EnteredSpan),
/// Currently processing sources or `BeforeSources` observers.
Sources(EnteredSpan),
/// Currently waiting or processing `BeforeWaiting`/`AfterWaiting` observers.
Waiting(EnteredSpan),
}
// A list of currently entered (outer) spans and their state.
//
// This is a list because runloops can be run recursively.
let spans: Rc<RefCell<Vec<(EnteredSpan, RunLoopState)>>> = Rc::new(RefCell::new(Vec::new()));
let spans_clone = Rc::clone(&spans);
// An observer at the start of run loop activities.
let activities = CFRunLoopActivity::Entry
| CFRunLoopActivity::BeforeTimers
| CFRunLoopActivity::BeforeSources
| CFRunLoopActivity::BeforeWaiting;
let start = MainRunLoopObserver::new(mtm, activities, true, CFIndex::MIN, move |activity| {
match activity {
// Add an outer span for each runloop iteration.
CFRunLoopActivity::Entry => {
let span = trace_span!("inside runloop", mode = field::Empty);
// Get the mode dynamically, the observer may added to multiple different modes.
let mode = CFRunLoop::current().unwrap().current_mode().unwrap();
// Mode isn't interesting if it's the default mode.
if &*mode != unsafe { kCFRunLoopDefaultMode }.unwrap() {
span.record("mode", field::display(mode));
}
let entered = span.entered();
spans.borrow_mut().push((entered, RunLoopState::Entered));
},
// Add inner spans that help inspecting the state the runloop is in.
CFRunLoopActivity::BeforeTimers => {
if let Some((_, state)) = spans.borrow_mut().last_mut() {
*state = RunLoopState::Entered; // Drop any previous spans.
*state = RunLoopState::Timers(trace_span!("processing timers").entered());
} else {
error!("unbalanced observer invocations");
}
},
CFRunLoopActivity::BeforeSources => {
if let Some((_, state)) = spans.borrow_mut().last_mut() {
*state = RunLoopState::Entered; // Drop any previous spans.
*state = RunLoopState::Sources(trace_span!("processing sources").entered());
} else {
error!("unbalanced observer invocations");
}
},
CFRunLoopActivity::BeforeWaiting => {
if let Some((_, state)) = spans.borrow_mut().last_mut() {
*state = RunLoopState::Entered; // Drop any previous spans.
*state = RunLoopState::Waiting(trace_span!("waiting").entered());
} else {
error!("unbalanced observer invocations");
}
},
activity => unreachable!("unexpected activity: {activity:?}"),
}
});
// An observer at the end of run loop activities.
let activities = CFRunLoopActivity::AfterWaiting | CFRunLoopActivity::Exit;
let end = MainRunLoopObserver::new(mtm, activities, true, CFIndex::MAX, move |activity| {
match activity {
CFRunLoopActivity::AfterWaiting => {
if let Some((_, state)) = spans_clone.borrow_mut().last_mut() {
// Transition from the waiting state to the initial state.
*state = RunLoopState::Entered;
} else {
error!("unbalanced observer invocations");
}
},
CFRunLoopActivity::Exit => {
if let Some((span, state)) = spans_clone.borrow_mut().pop() {
drop(state); // Explicitly exit and drop inner span.
drop(span); // Explicitly exit and drop outer span.
} else {
error!("unbalanced observer invocations");
}
},
activity => unreachable!("unexpected activity: {activity:?}"),
}
});
Some((start, end))
}

View File

@@ -79,6 +79,10 @@ impl EventHandler {
// Allowed, happens if the handler was cleared manually
// elsewhere (such as in `applicationWillTerminate:`).
},
// We use `eprintln!` here over `tracing::error!`, since we're going to abort
// immediately after this, and it'd be annoying for the user if they didn't get
// any feedback on that if they don't have a tracing subscriber.
#[allow(clippy::disallowed_macros)]
Err(_) => {
// Note: This is not expected to ever happen, this
// module generally controls the `RefCell`, and

View File

@@ -0,0 +1,3 @@
mod notification_center;
pub use self::notification_center::*;

View File

@@ -1,4 +1,3 @@
// NOTE: This is symlinked to be contained in both the AppKit and UIKit implementations.
use std::ptr::NonNull;
use block2::RcBlock;
@@ -12,7 +11,7 @@ use objc2_foundation::{
///
/// 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(crate) fn create_observer(
pub fn create_observer(
center: &NSNotificationCenter,
name: &NSNotificationName,
handler: impl Fn(&NSNotification) + 'static,

View File

@@ -4,5 +4,7 @@
pub mod core_foundation;
#[cfg(feature = "event-handler")]
pub mod event_handler;
#[cfg(feature = "foundation")]
pub mod foundation;
#[cfg(feature = "xkb")]
pub mod xkb;

View File

@@ -156,6 +156,8 @@ pub enum WindowEvent {
Ime(Ime),
/// The pointer has moved on the window.
///
/// Should be emitted regardless of window focus.
PointerMoved {
device_id: Option<DeviceId>,
@@ -184,6 +186,8 @@ pub enum WindowEvent {
},
/// The pointer has entered the window.
///
/// Should be emitted regardless of window focus.
PointerEntered {
device_id: Option<DeviceId>,
@@ -209,6 +213,8 @@ pub enum WindowEvent {
},
/// The pointer has left the window.
///
/// Should be emitted regardless of window focus.
PointerLeft {
device_id: Option<DeviceId>,
@@ -263,6 +269,20 @@ pub enum WindowEvent {
button: ButtonSource,
},
/// Multi-finger hold gesture on the touchpad or touchscreen without movement.
///
/// The `phase` field indicates the lifecycle of the hold gesture:
/// - `Started`: One or more fingers are in contact with the touchpad/touchscreen.
/// - `Ended`: All fingers have been lifted from the touchpad/touchscreen.
/// - `Cancelled`: The hold gesture was interrupted, for example when another finger touches the
/// touchpad (causing a new `Started` event with more fingers), or when movement begins and
/// transitions to other gestures like pinch, pan, or rotation.
///
/// ## Platform-specific
///
/// - Only available on **Wayland**.
HoldGesture { device_id: Option<DeviceId>, phase: TouchPhase },
/// Two-finger pinch gesture, often used for magnification.
///
/// ## Platform-specific
@@ -516,6 +536,19 @@ impl From<PointerSource> for PointerKind {
/// system.
#[derive(Clone, Debug, PartialEq)]
pub enum ButtonSource {
/// ## Platform-specific
///
/// ### macOS
///
/// Users may expect holding [<kbd>CTRL</kbd>](ModifiersState::CONTROL) while
/// clicking [`MouseButton::Left`] to result in a "secondary" click, but the way these
/// clicks behave natively is slightly different from how a physical secondary
/// button press would, depending on the content under the cursor when clicked. If
/// applications want this behavior they should implement it themselves by interpreting
/// [`Left`](MouseButton::Left) clicks as secondary when
/// [<kbd>CTRL</kbd>](ModifiersState::CONTROL) is held and their internal logic deems it
/// appropriate for the content under the pointer.
/// See also https://github.com/rust-windowing/winit/issues/4469.
Mouse(MouseButton),
/// See [`PointerSource::Touch`] for more details.
///
@@ -791,7 +824,7 @@ pub struct KeyEvent {
///
/// ## Platform-specific
///
/// - **Android:** Unimplemented, this field is always the same value as `text`.
/// - **Android:** This field is always the same value as `text`.
/// - **iOS:** Unimplemented, this field is always the same value as `text`.
/// - **Web:** Unsupported, this field is always the same value as `text`.
pub text_with_all_modifiers: Option<SmolStr>,
@@ -987,9 +1020,17 @@ pub enum Ime {
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TouchPhase {
/// Initial touch contact or gesture start, for example when one or more fingers touch the
/// screen or touchpad.
Started,
/// The touch contact point changed, for example without lifting the finger.
Moved,
/// All touch contact points have been lifted from the touchscreen or touchpad.
///
/// This event is important as it should clear any state or event in flight that was
/// generated by the preceding `Started` and `Moved` events.
Ended,
/// The event was cancelled and should cancel any event in flight and clear state.
Cancelled,
}

View File

@@ -176,11 +176,11 @@ pub trait EventLoopProxyProvider: Send + Sync + Debug {
/// - A reference-counted pointer to the underlying type.
#[derive(Clone)]
pub struct OwnedDisplayHandle {
pub(crate) handle: Arc<dyn HasDisplayHandle>,
pub(crate) handle: Arc<dyn HasDisplayHandle + Send + Sync>,
}
impl OwnedDisplayHandle {
pub fn new(handle: Arc<dyn HasDisplayHandle>) -> Self {
pub fn new(handle: Arc<dyn HasDisplayHandle + Send + Sync>) -> Self {
Self { handle }
}
}

View File

@@ -29,11 +29,13 @@ pub enum NativeKeyCode {
Windows(u16),
/// An XKB "keycode".
Xkb(u32),
/// An OpenHarmony "scancode".
Ohos(u32),
}
impl std::fmt::Debug for NativeKeyCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use NativeKeyCode::{Android, MacOS, Unidentified, Windows, Xkb};
use NativeKeyCode::{Android, MacOS, Ohos, Unidentified, Windows, Xkb};
let mut debug_tuple;
match self {
Unidentified => {
@@ -55,6 +57,10 @@ impl std::fmt::Debug for NativeKeyCode {
debug_tuple = f.debug_tuple("Xkb");
debug_tuple.field(&format_args!("0x{code:04X}"));
},
Ohos(code) => {
debug_tuple = f.debug_tuple("OpenHarmony");
debug_tuple.field(&format_args!("0x{code:04X}"));
},
}
debug_tuple.finish()
}
@@ -85,11 +91,13 @@ pub enum NativeKey {
Xkb(u32),
/// A "key value string".
Web(SmolStr),
/// An OpenHarmony "keycode", which is similar to a "virtual-key code" on Windows.
Ohos(u32),
}
impl std::fmt::Debug for NativeKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use NativeKey::{Android, MacOS, Unidentified, Web, Windows, Xkb};
use NativeKey::{Android, MacOS, Ohos, Unidentified, Web, Windows, Xkb};
let mut debug_tuple;
match self {
Unidentified => {
@@ -115,6 +123,10 @@ impl std::fmt::Debug for NativeKey {
debug_tuple = f.debug_tuple("Web");
debug_tuple.field(code);
},
Ohos(code) => {
debug_tuple = f.debug_tuple("OpenHarmony");
debug_tuple.field(code);
},
}
debug_tuple.finish()
}
@@ -129,6 +141,7 @@ impl From<NativeKeyCode> for NativeKey {
NativeKeyCode::MacOS(x) => NativeKey::MacOS(x),
NativeKeyCode::Windows(x) => NativeKey::Windows(x),
NativeKeyCode::Xkb(x) => NativeKey::Xkb(x),
NativeKeyCode::Ohos(x) => NativeKey::Ohos(x),
}
}
}

View File

@@ -834,7 +834,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
///
/// ## Platform-specific
///
/// - **iOS / Android / Web / Wayland / Orbital:** Always returns [`None`].
/// - **iOS / Android / Web / Orbital:** Always returns [`None`].
fn surface_resize_increments(&self) -> Option<PhysicalSize<u32>>;
/// Sets resize increments of the surface.
@@ -846,7 +846,6 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
///
/// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole
/// numbers.
/// - **Wayland:** Not implemented.
/// - **iOS / Android / Web / Orbital:** Unsupported.
fn set_surface_resize_increments(&self, increments: Option<Size>);
@@ -881,7 +880,8 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
/// ## Platform-specific
///
/// - **Android / iOS / X11 / Web / Windows:** Unsupported.
/// - **Wayland:** Only works with org_kde_kwin_blur_manager protocol.
/// - **Wayland:** Only works with `org_kde_kwin_blur_manager` or
/// `ext_background_effect_manager_v1` protocol.
fn set_blur(&self, blur: bool);
/// Modifies the window's visibility.
@@ -1691,6 +1691,22 @@ pub enum ImeSurroundingTextError {
AnchorBadPosition,
}
impl fmt::Display for ImeSurroundingTextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImeSurroundingTextError::TextTooLong => write!(f, "text exceeds maximum length"),
ImeSurroundingTextError::CursorBadPosition => {
write!(f, "cursor is not at a valid text index")
},
ImeSurroundingTextError::AnchorBadPosition => {
write!(f, "anchor is not at a valid text index")
},
}
}
}
impl std::error::Error for ImeSurroundingTextError {}
/// Defines the text surrounding the caret
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

View File

@@ -21,5 +21,6 @@ tracing.workspace = true
winit-core.workspace = true
# Platform-specific
libredox.workspace = true
orbclient.workspace = true
redox_syscall.workspace = true
redox_event.workspace = true

View File

@@ -10,6 +10,7 @@ use orbclient::{
ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, MouseRelativeEvent,
MoveEvent, QuitEvent, ResizeEvent, ScrollEvent, TextInputEvent,
};
use redox_event::{EventFlags, EventQueue};
use smol_str::SmolStr;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
@@ -100,6 +101,7 @@ fn convert_scancode(scancode: u8) -> (PhysicalKey, Option<NamedKey>) {
orbclient::K_LEFT => (KeyCode::ArrowLeft, Some(NamedKey::ArrowLeft)),
orbclient::K_LEFT_SHIFT => (KeyCode::ShiftLeft, Some(NamedKey::Shift)),
orbclient::K_MINUS => (KeyCode::Minus, None),
orbclient::K_NUM_0 => (KeyCode::Numpad0, None),
orbclient::K_NUM_1 => (KeyCode::Numpad1, None),
orbclient::K_NUM_2 => (KeyCode::Numpad2, None),
@@ -110,12 +112,20 @@ fn convert_scancode(scancode: u8) -> (PhysicalKey, Option<NamedKey>) {
orbclient::K_NUM_7 => (KeyCode::Numpad7, None),
orbclient::K_NUM_8 => (KeyCode::Numpad8, None),
orbclient::K_NUM_9 => (KeyCode::Numpad9, None),
orbclient::K_NUM_ASTERISK => (KeyCode::NumpadMultiply, None),
orbclient::K_NUM_ENTER => (KeyCode::NumpadEnter, Some(NamedKey::Enter)),
orbclient::K_NUM_MINUS => (KeyCode::NumpadSubtract, None),
orbclient::K_NUM_PLUS => (KeyCode::NumpadAdd, None),
orbclient::K_NUM_SLASH => (KeyCode::NumpadDivide, None),
orbclient::K_NUM_PERIOD => (KeyCode::NumpadDecimal, None),
orbclient::K_PERIOD => (KeyCode::Period, None),
orbclient::K_PGDN => (KeyCode::PageDown, Some(NamedKey::PageDown)),
orbclient::K_PGUP => (KeyCode::PageUp, Some(NamedKey::PageUp)),
orbclient::K_QUOTE => (KeyCode::Quote, None),
orbclient::K_RIGHT => (KeyCode::ArrowRight, Some(NamedKey::ArrowRight)),
orbclient::K_RIGHT_SHIFT => (KeyCode::ShiftRight, Some(NamedKey::Shift)),
orbclient::K_RIGHT_SUPER => (KeyCode::MetaRight, Some(NamedKey::Meta)),
orbclient::K_SEMICOLON => (KeyCode::Semicolon, None),
orbclient::K_SLASH => (KeyCode::Slash, None),
orbclient::K_SPACE => (KeyCode::Space, None),
@@ -127,6 +137,20 @@ fn convert_scancode(scancode: u8) -> (PhysicalKey, Option<NamedKey>) {
orbclient::K_VOLUME_TOGGLE => (KeyCode::AudioVolumeMute, Some(NamedKey::AudioVolumeMute)),
orbclient::K_VOLUME_UP => (KeyCode::AudioVolumeUp, Some(NamedKey::AudioVolumeUp)),
orbclient::K_INS => (KeyCode::Insert, Some(NamedKey::Insert)),
orbclient::K_PRTSC => (KeyCode::PrintScreen, Some(NamedKey::PrintScreen)),
orbclient::K_NUM => (KeyCode::NumLock, Some(NamedKey::NumLock)),
orbclient::K_SCROLL => (KeyCode::ScrollLock, Some(NamedKey::ScrollLock)),
orbclient::K_APP => (KeyCode::ContextMenu, Some(NamedKey::ContextMenu)),
orbclient::K_MEDIA_FAST_FORWARD => {
(KeyCode::MediaFastForward, Some(NamedKey::MediaFastForward))
},
orbclient::K_MEDIA_REWIND => (KeyCode::MediaRewind, Some(NamedKey::MediaRewind)),
orbclient::K_MEDIA_STOP => (KeyCode::MediaStop, Some(NamedKey::MediaStop)),
orbclient::K_POWER => (KeyCode::Power, Some(NamedKey::Power)),
_ => return (PhysicalKey::Unidentified(NativeKeyCode::Unidentified), None),
};
(PhysicalKey::Code(key_code), named_key_opt)
@@ -288,16 +312,12 @@ impl EventLoop {
let (user_events_sender, user_events_receiver) = mpsc::sync_channel(1);
let event_socket =
Arc::new(RedoxSocket::event().map_err(|error| os_error!(format!("{error}")))?);
Arc::new(EventQueue::new().map_err(|error| os_error!(format!("{error}")))?);
let wake_socket = TimeSocket::open().map_err(|error| os_error!(format!("{error}")))?;
event_socket
.write(&syscall::Event {
id: wake_socket.0.fd,
flags: syscall::EventFlags::EVENT_READ,
data: wake_socket.0.fd,
})
.subscribe(wake_socket.0.fd(), EventSource::Time, EventFlags::READ)
.map_err(|error| os_error!(format!("{error}")))?;
Ok(Self {
@@ -498,7 +518,7 @@ impl EventLoop {
let mut creates = self.window_target.creates.lock().unwrap();
creates.pop_front()
} {
let window_id = WindowId::from_raw(window.fd);
let window_id = WindowId::from_raw(window.fd());
let mut buf: [u8; 4096] = [0; 4096];
let path = window.fpath(&mut buf).expect("failed to read properties");
@@ -522,18 +542,18 @@ impl EventLoop {
} {
app.window_event(&self.window_target, destroy_id, event::WindowEvent::Destroyed);
self.windows
.retain(|(window, _event_state)| WindowId::from_raw(window.fd) != destroy_id);
.retain(|(window, _event_state)| WindowId::from_raw(window.fd()) != destroy_id);
}
// Handle window events.
let mut i = 0;
// While loop is used here because the same window may be processed more than once.
while let Some((window, event_state)) = self.windows.get_mut(i) {
let window_id = WindowId::from_raw(window.fd);
let window_id = WindowId::from_raw(window.fd());
let mut event_buf = [0u8; 16 * mem::size_of::<orbclient::Event>()];
let count =
syscall::read(window.fd, &mut event_buf).expect("failed to read window events");
let count = libredox::call::read(window.fd(), &mut event_buf)
.expect("failed to read window events");
// Safety: orbclient::Event is a packed struct designed to be transferred over a
// socket.
let events = unsafe {
@@ -613,11 +633,7 @@ impl EventLoop {
self.window_target
.event_socket
.write(&syscall::Event {
id: timeout_socket.0.fd,
flags: syscall::EventFlags::EVENT_READ,
data: 0,
})
.subscribe(timeout_socket.0.fd(), EventSource::Time, EventFlags::READ)
.unwrap();
let start = Instant::now();
@@ -626,7 +642,7 @@ impl EventLoop {
if let Some(duration) = instant.checked_duration_since(start) {
time.tv_sec += duration.as_secs() as i64;
time.tv_nsec += duration.subsec_nanos() as i32;
time.tv_nsec += duration.subsec_nanos() as i64;
// Normalize timespec so tv_nsec is not greater than one second.
while time.tv_nsec >= 1_000_000_000 {
time.tv_sec += 1;
@@ -638,12 +654,22 @@ impl EventLoop {
}
// Wait for event if needed.
let mut event = syscall::Event::default();
self.window_target.event_socket.read(&mut event).unwrap();
let event = loop {
match self.window_target.event_socket.next_event() {
Ok(event) => break event,
Err(err) if err.is_interrupt() => continue,
Err(err) => {
return Err(os_error!(format!("failed to read event: {err}")).into());
},
}
};
// TODO: handle spurious wakeups (redraw caused wakeup but redraw already handled)
match requested_resume {
Some(requested_resume) if event.id == timeout_socket.0.fd => {
Some(requested_resume)
if event.fd == timeout_socket.0.fd()
&& matches!(event.user_data, EventSource::Time) =>
{
// If the event is from the special timeout socket, report that resume
// time was reached.
start_cause = StartCause::ResumeTimeReached { start, requested_resume };
@@ -681,6 +707,13 @@ impl EventLoopProxyProvider for EventLoopProxy {
impl Unpin for EventLoopProxy {}
redox_event::user_data! {
pub enum EventSource {
Orbital,
Time,
}
}
#[derive(Debug)]
pub struct ActiveEventLoop {
control_flow: Cell<ControlFlow>,
@@ -688,7 +721,7 @@ pub struct ActiveEventLoop {
pub(super) creates: Mutex<VecDeque<Arc<RedoxSocket>>>,
pub(super) redraws: Arc<Mutex<VecDeque<WindowId>>>,
pub(super) destroys: Arc<Mutex<VecDeque<WindowId>>>,
pub(super) event_socket: Arc<RedoxSocket>,
pub(super) event_socket: Arc<EventQueue<EventSource>>,
pub(super) event_loop_proxy: Arc<EventLoopProxy>,
}

View File

@@ -3,7 +3,12 @@
//! Redox OS has some functionality not yet present that will be implemented
//! when its orbital display server provides it.
use std::{fmt, str};
use std::fs::{File, OpenOptions};
use std::io::{Read, Result, Write};
use std::os::fd::AsRawFd;
use std::{fmt, mem, slice, str};
use libredox::data::TimeSpec;
pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes};
@@ -16,15 +21,11 @@ pub mod window;
#[derive(Debug)]
struct RedoxSocket {
fd: usize,
fd: File,
}
impl RedoxSocket {
fn event() -> syscall::Result<Self> {
Self::open_raw("/scheme/event")
}
fn orbital(properties: &WindowProperties<'_>) -> syscall::Result<Self> {
fn orbital(properties: &WindowProperties<'_>) -> Result<Self> {
Self::open_raw(&format!("{properties}"))
}
@@ -32,30 +33,27 @@ impl RedoxSocket {
// non-socket path is used, it could cause read and write to not function as expected. For
// example, the seek would change in a potentially unpredictable way if either read or write
// were called at the same time by multiple threads.
fn open_raw(path: &str) -> syscall::Result<Self> {
let fd = syscall::open(path, syscall::O_RDWR | syscall::O_CLOEXEC)?;
fn open_raw(path: &str) -> Result<Self> {
let fd = OpenOptions::new().read(true).write(true).open(path)?;
Ok(Self { fd })
}
fn read(&self, buf: &mut [u8]) -> syscall::Result<()> {
let count = syscall::read(self.fd, buf)?;
if count == buf.len() { Ok(()) } else { Err(syscall::Error::new(syscall::EINVAL)) }
fn fd(&self) -> usize {
self.fd.as_raw_fd() as usize
}
fn write(&self, buf: &[u8]) -> syscall::Result<()> {
let count = syscall::write(self.fd, buf)?;
if count == buf.len() { Ok(()) } else { Err(syscall::Error::new(syscall::EINVAL)) }
fn read(&self, buf: &mut [u8]) -> Result<()> {
(&self.fd).read_exact(buf)
}
fn fpath<'a>(&self, buf: &'a mut [u8]) -> syscall::Result<&'a str> {
let count = syscall::fpath(self.fd, buf)?;
str::from_utf8(&buf[..count]).map_err(|_err| syscall::Error::new(syscall::EINVAL))
fn write(&self, buf: &[u8]) -> Result<()> {
(&self.fd).write_all(buf)
}
}
impl Drop for RedoxSocket {
fn drop(&mut self) {
let _ = syscall::close(self.fd);
fn fpath<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str> {
let count = libredox::call::fpath(self.fd(), buf)?;
str::from_utf8(&buf[..count])
.map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))
}
}
@@ -63,26 +61,36 @@ impl Drop for RedoxSocket {
struct TimeSocket(RedoxSocket);
impl TimeSocket {
fn open() -> syscall::Result<Self> {
fn open() -> Result<Self> {
RedoxSocket::open_raw("/scheme/time/4").map(Self)
}
// Read current time.
fn current_time(&self) -> syscall::Result<syscall::TimeSpec> {
let mut timespec = syscall::TimeSpec::default();
self.0.read(&mut timespec)?;
fn current_time(&self) -> Result<TimeSpec> {
let mut timespec: libredox::data::TimeSpec = unsafe { mem::zeroed() };
let timespec_bytes = unsafe {
slice::from_raw_parts_mut(
&mut timespec as *mut _ as *mut u8,
mem::size_of::<TimeSpec>(),
)
};
self.0.read(timespec_bytes)?;
Ok(timespec)
}
// Write a timeout.
fn timeout(&self, timespec: &syscall::TimeSpec) -> syscall::Result<()> {
self.0.write(timespec)
fn timeout(&self, timespec: &TimeSpec) -> Result<()> {
let timespec_bytes = unsafe {
slice::from_raw_parts(timespec as *const _ as *const u8, mem::size_of::<TimeSpec>())
};
self.0.write(timespec_bytes)
}
// Wake immediately.
fn wake(&self) -> syscall::Result<()> {
fn wake(&self) -> Result<()> {
// Writing a default TimeSpec will always trigger a time event.
self.timeout(&syscall::TimeSpec::default())
let timespec: TimeSpec = unsafe { mem::zeroed() };
self.timeout(&timespec)
}
}
@@ -97,7 +105,7 @@ struct WindowProperties<'a> {
impl<'a> WindowProperties<'a> {
fn new(path: &'a str) -> Self {
// orbital:flags/x/y/w/h/t
// /scheme/orbital/flags/x/y/w/h/t
let mut parts = path.splitn(6, '/');
let flags = parts.next().unwrap_or("");
let x = parts.next().map_or(0, |part| part.parse::<i32>().unwrap_or(0));

View File

@@ -3,12 +3,13 @@ use std::iter;
use std::sync::{Arc, Mutex};
use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size};
use redox_event::EventFlags;
use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
use winit_core::window::{self, Window as CoreWindow, WindowId};
use crate::event_loop::{ActiveEventLoop, EventLoopProxy};
use crate::event_loop::{ActiveEventLoop, EventLoopProxy, EventSource};
use crate::{RedoxSocket, WindowProperties};
// These values match the values uses in the `window_new` function in orbital:
@@ -103,13 +104,7 @@ impl Window {
.expect("failed to open window");
// Add to event socket.
el.event_socket
.write(&syscall::Event {
id: window.fd,
flags: syscall::EventFlags::EVENT_READ,
data: window.fd,
})
.unwrap();
el.event_socket.subscribe(window.fd(), EventSource::Orbital, EventFlags::READ).unwrap();
let window_socket = Arc::new(window);
@@ -146,7 +141,7 @@ impl Window {
#[inline]
fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let handle = rwh_06::OrbitalWindowHandle::new({
let window = self.window_socket.fd as *mut _;
let window = self.window_socket.fd() as *mut _;
std::ptr::NonNull::new(window).expect("orbital fd should never be null")
});
Ok(rwh_06::RawWindowHandle::Orbital(handle))
@@ -160,7 +155,7 @@ impl Window {
impl CoreWindow for Window {
fn id(&self) -> WindowId {
WindowId::from_raw(self.window_socket.fd)
WindowId::from_raw(self.window_socket.fd())
}
fn ime_capabilities(&self) -> Option<window::ImeCapabilities> {

View File

@@ -69,7 +69,7 @@ objc2-ui-kit = { workspace = true, features = [
"UIViewController",
"UIWindow",
] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] }
[package.metadata.docs.rs]
all-features = true

View File

@@ -1,13 +1,9 @@
use std::ffi::c_void;
use std::ptr;
use std::sync::Arc;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{ClassType, MainThreadMarker, msg_send};
use objc2_core_foundation::{
CFIndex, CFRunLoop, CFRunLoopActivity, CFRunLoopObserver, kCFRunLoopDefaultMode,
};
use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopDefaultMode};
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
use objc2_ui_kit::{
UIApplication, UIApplicationDidBecomeActiveNotification,
@@ -16,6 +12,9 @@ use objc2_ui_kit::{
UIApplicationWillResignActiveNotification, UIApplicationWillTerminateNotification, UIScreen,
};
use rwh_06::HasDisplayHandle;
use tracing::debug_span;
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
use winit_common::foundation::create_observer;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
@@ -27,7 +26,6 @@ use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
use winit_core::window::{Theme, Window as CoreWindow};
use super::app_state::{AppState, send_occluded_event_for_all_windows};
use super::notification_center::create_observer;
use crate::monitor::MonitorHandle;
use crate::window::Window;
use crate::{app_state, monitor};
@@ -136,6 +134,11 @@ pub struct EventLoop {
_did_enter_background_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_will_terminate_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_did_receive_memory_warning_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>,
_wakeup_observer: MainRunLoopObserver,
_main_events_cleared_observer: MainRunLoopObserver,
_events_cleared_observer: MainRunLoopObserver,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -151,9 +154,6 @@ impl EventLoop {
return Err(EventLoopError::RecreationAttempt);
}
// this line sets up the main run loop before `UIApplicationMain`
setup_control_flow_observers();
let center = NSNotificationCenter::defaultCenter();
let _did_finish_launching_observer = create_observer(
@@ -161,6 +161,7 @@ impl EventLoop {
// `application:didFinishLaunchingWithOptions:`
unsafe { UIApplicationDidFinishLaunchingNotification },
move |_| {
let _entered = debug_span!("UIApplicationDidFinishLaunchingNotification").entered();
app_state::did_finish_launching(mtm);
},
);
@@ -168,19 +169,27 @@ impl EventLoop {
&center,
// `applicationDidBecomeActive:`
unsafe { UIApplicationDidBecomeActiveNotification },
move |_| app_state::handle_resumed(mtm),
move |_| {
let _entered = debug_span!("UIApplicationDidBecomeActiveNotification").entered();
app_state::handle_resumed(mtm)
},
);
let _will_resign_active_observer = create_observer(
&center,
// `applicationWillResignActive:`
unsafe { UIApplicationWillResignActiveNotification },
move |_| app_state::handle_suspended(mtm),
move |_| {
let _entered = debug_span!("UIApplicationWillResignActiveNotification").entered();
app_state::handle_suspended(mtm)
},
);
let _will_enter_foreground_observer = create_observer(
&center,
// `applicationWillEnterForeground:`
unsafe { UIApplicationWillEnterForegroundNotification },
move |notification| {
let _entered =
debug_span!("UIApplicationWillEnterForegroundNotification").entered();
let app = notification.object().expect(
"UIApplicationWillEnterForegroundNotification to have application object",
);
@@ -195,6 +204,7 @@ impl EventLoop {
// `applicationDidEnterBackground:`
unsafe { UIApplicationDidEnterBackgroundNotification },
move |notification| {
let _entered = debug_span!("UIApplicationDidEnterBackgroundNotification").entered();
let app = notification.object().expect(
"UIApplicationDidEnterBackgroundNotification to have application object",
);
@@ -209,6 +219,7 @@ impl EventLoop {
// `applicationWillTerminate:`
unsafe { UIApplicationWillTerminateNotification },
move |notification| {
let _entered = debug_span!("UIApplicationWillTerminateNotification").entered();
let app = notification
.object()
.expect("UIApplicationWillTerminateNotification to have application object");
@@ -222,9 +233,65 @@ impl EventLoop {
&center,
// `applicationDidReceiveMemoryWarning:`
unsafe { UIApplicationDidReceiveMemoryWarningNotification },
move |_| app_state::handle_memory_warning(mtm),
move |_| {
let _entered =
debug_span!("UIApplicationDidReceiveMemoryWarningNotification").entered();
app_state::handle_memory_warning(mtm)
},
);
let main_loop = MainRunLoop::get(mtm);
let mode = unsafe { kCFRunLoopDefaultMode }.unwrap();
// Tracing observers have the lowest and highest orderings.
let _tracing_observers = tracing_observers(mtm).inspect(|(start, end)| {
main_loop.add_observer(start, mode);
main_loop.add_observer(end, mode);
});
let _wakeup_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::AfterWaiting,
true,
// Queued with the second-highest priority (tracing observers use the highest) to
// ensure it is processed before other observers.
CFIndex::MIN + 1,
move |_| app_state::handle_wakeup_transition(mtm),
);
main_loop.add_observer(&_wakeup_observer, mode);
let _main_events_cleared_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::BeforeWaiting,
true,
// Core Animation registers its `CFRunLoopObserver` that performs drawing operations in
// `CA::Transaction::ensure_implicit` with a priority of `2000000`. We set the
// main_end priority to be 0, in order to send `AboutToWait` before `RedrawRequested`.
// This value was chosen conservatively to guard against apple using different
// priorities for their redraw observers in different OS's or on different devices. If
// it so happens that it's too conservative, the main symptom would be non-redraw
// events coming in after `AboutToWait`.
//
// The value of `2000000` was determined by inspecting stack traces and the associated
// registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4.
//
// Also tested to be `2000000` on iPhone 8, iOS 13 beta 4.
0,
move |_| app_state::handle_main_events_cleared(mtm),
);
main_loop.add_observer(&_main_events_cleared_observer, mode);
let _events_cleared_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::BeforeWaiting,
true,
// Queued with the second-lowest priority (tracing observers use the lowest) to ensure
// it is processed after other observers.
CFIndex::MAX - 1,
move |_| app_state::handle_events_cleared(mtm),
);
main_loop.add_observer(&_events_cleared_observer, mode);
Ok(EventLoop {
mtm,
window_target: ActiveEventLoop { mtm },
@@ -235,6 +302,10 @@ impl EventLoop {
_did_enter_background_observer,
_will_terminate_observer,
_did_receive_memory_warning_observer,
_tracing_observers,
_wakeup_observer,
_main_events_cleared_observer,
_events_cleared_observer,
})
}
@@ -262,97 +333,3 @@ impl EventLoop {
&self.window_target
}
}
fn setup_control_flow_observers() {
unsafe {
// begin is queued with the highest priority to ensure it is processed before other
// observers
extern "C-unwind" fn control_flow_begin_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_: *mut c_void,
) {
let mtm = MainThreadMarker::new().unwrap();
#[allow(non_upper_case_globals)]
match activity {
CFRunLoopActivity::AfterWaiting => app_state::handle_wakeup_transition(mtm),
_ => unreachable!(),
}
}
// Core Animation registers its `CFRunLoopObserver` that performs drawing operations in
// `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end
// priority to be 0, in order to send AboutToWait before RedrawRequested. This value was
// chosen conservatively to guard against apple using different priorities for their redraw
// observers in different OS's or on different devices. If it so happens that it's too
// conservative, the main symptom would be non-redraw events coming in after `AboutToWait`.
//
// The value of `0x1e8480` was determined by inspecting stack traces and the associated
// registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4.
//
// Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4.
extern "C-unwind" fn control_flow_main_end_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_: *mut c_void,
) {
let mtm = MainThreadMarker::new().unwrap();
#[allow(non_upper_case_globals)]
match activity {
CFRunLoopActivity::BeforeWaiting => app_state::handle_main_events_cleared(mtm),
CFRunLoopActivity::Exit => {}, // may happen when running on macOS
_ => unreachable!(),
}
}
// end is queued with the lowest priority to ensure it is processed after other observers
extern "C-unwind" fn control_flow_end_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_: *mut c_void,
) {
let mtm = MainThreadMarker::new().unwrap();
#[allow(non_upper_case_globals)]
match activity {
CFRunLoopActivity::BeforeWaiting => app_state::handle_events_cleared(mtm),
CFRunLoopActivity::Exit => {}, // may happen when running on macOS
_ => unreachable!(),
}
}
let main_loop = CFRunLoop::main().unwrap();
let begin_observer = CFRunLoopObserver::new(
None,
CFRunLoopActivity::AfterWaiting.0,
true,
CFIndex::MIN,
Some(control_flow_begin_handler),
ptr::null_mut(),
)
.unwrap();
main_loop.add_observer(Some(&begin_observer), kCFRunLoopDefaultMode);
let main_end_observer = CFRunLoopObserver::new(
None,
(CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting).0,
true,
0, // see comment on `control_flow_main_end_handler`
Some(control_flow_main_end_handler),
ptr::null_mut(),
)
.unwrap();
main_loop.add_observer(Some(&main_end_observer), kCFRunLoopDefaultMode);
let end_observer = CFRunLoopObserver::new(
None,
(CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting).0,
true,
CFIndex::MAX,
Some(control_flow_end_handler),
ptr::null_mut(),
)
.unwrap();
main_loop.add_observer(Some(&end_observer), kCFRunLoopDefaultMode);
}
}

View File

@@ -103,7 +103,6 @@
mod app_state;
mod event_loop;
mod monitor;
mod notification_center;
mod view;
mod view_controller;
mod window;

View File

@@ -1 +0,0 @@
../../winit-appkit/src/notification_center.rs

View File

@@ -13,10 +13,10 @@ use objc2_ui_kit::{
UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITextInputTraits, UITouch,
UITouchPhase, UITouchType, UITraitEnvironment, UIView,
};
use tracing::debug;
use tracing::{debug, debug_span, trace_span};
use winit_core::event::{
ButtonSource, ElementState, FingerId, Force, KeyEvent, PointerKind, PointerSource, TouchPhase,
WindowEvent,
ButtonSource, ElementState, FingerId, Force, KeyEvent, PointerKind, PointerSource,
TabletToolAngle, TabletToolButton, TabletToolData, TabletToolKind, TouchPhase, WindowEvent,
};
use winit_core::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey};
@@ -48,6 +48,7 @@ define_class!(
impl WinitView {
#[unsafe(method(drawRect:))]
fn draw_rect(&self, rect: CGRect) {
let _entered = debug_span!("drawRect:").entered();
let mtm = MainThreadMarker::new().unwrap();
let window = self.window().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::Window {
@@ -59,6 +60,7 @@ define_class!(
#[unsafe(method(layoutSubviews))]
fn layout_subviews(&self) {
let _entered = debug_span!("layoutSubviews").entered();
let mtm = MainThreadMarker::new().unwrap();
let _: () = unsafe { msg_send![super(self), layoutSubviews] };
@@ -79,6 +81,7 @@ define_class!(
#[unsafe(method(setContentScaleFactor:))]
fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) {
let _entered = debug_span!("setContentScaleFactor:").entered();
let mtm = MainThreadMarker::new().unwrap();
let _: () =
unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] };
@@ -124,6 +127,7 @@ define_class!(
#[unsafe(method(safeAreaInsetsDidChange))]
fn safe_area_changed(&self) {
let _entered = debug_span!("safeAreaInsetsDidChange").entered();
debug!("safeAreaInsetsDidChange was called, requesting redraw");
// When the safe area changes we want to make sure to emit a redraw event
self.setNeedsDisplay();
@@ -131,26 +135,31 @@ define_class!(
#[unsafe(method(touchesBegan:withEvent:))]
fn touches_began(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
let _entered = debug_span!("touchesBegan:withEvent:").entered();
self.handle_touches(touches)
}
#[unsafe(method(touchesMoved:withEvent:))]
fn touches_moved(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
let _entered = debug_span!("touchesMoved:withEvent:").entered();
self.handle_touches(touches)
}
#[unsafe(method(touchesEnded:withEvent:))]
fn touches_ended(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
let _entered = debug_span!("touchesEnded:withEvent:").entered();
self.handle_touches(touches)
}
#[unsafe(method(touchesCancelled:withEvent:))]
fn touches_cancelled(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
let _entered = debug_span!("touchesCancelled:withEvent:").entered();
self.handle_touches(touches)
}
#[unsafe(method(pinchGesture:))]
fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) {
let _entered = debug_span!("pinchGesture:").entered();
let window = self.window().unwrap();
let (phase, delta) = match recognizer.state() {
@@ -185,6 +194,7 @@ define_class!(
#[unsafe(method(doubleTapGesture:))]
fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) {
let _entered = debug_span!("doubleTapGesture:").entered();
let window = self.window().unwrap();
if recognizer.state() == UIGestureRecognizerState::Ended {
@@ -200,6 +210,7 @@ define_class!(
#[unsafe(method(rotationGesture:))]
fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) {
let _entered = debug_span!("rotationGesture:").entered();
let window = self.window().unwrap();
let (phase, delta) = match recognizer.state() {
@@ -244,6 +255,7 @@ define_class!(
#[unsafe(method(panGesture:))]
fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) {
let _entered = debug_span!("panGesture:").entered();
let window = self.window().unwrap();
let translation = recognizer.translationInView(Some(self));
@@ -296,6 +308,7 @@ define_class!(
#[unsafe(method(canBecomeFirstResponder))]
fn can_become_first_responder(&self) -> bool {
let _entered = trace_span!("canBecomeFirstResponder").entered();
true
}
}
@@ -309,6 +322,10 @@ define_class!(
_gesture_recognizer: &UIGestureRecognizer,
_other_gesture_recognizer: &UIGestureRecognizer,
) -> bool {
let _entered = trace_span!(
"gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:"
)
.entered();
true
}
}
@@ -318,16 +335,19 @@ define_class!(
unsafe impl UIKeyInput for WinitView {
#[unsafe(method(hasText))]
fn has_text(&self) -> bool {
let _entered = debug_span!("hasText").entered();
true
}
#[unsafe(method(insertText:))]
fn insert_text(&self, text: &NSString) {
let _entered = debug_span!("insertText:").entered();
self.handle_insert_text(text)
}
#[unsafe(method(deleteBackward))]
fn delete_backward(&self) {
let _entered = debug_span!("deleteBackward").entered();
self.handle_delete_backward()
}
}
@@ -461,19 +481,18 @@ impl WinitView {
let window = self.window().unwrap();
let mut touch_events = Vec::new();
for touch in touches {
if let UITouchType::Pencil = touch.r#type() {
continue;
}
let logical_location = touch.locationInView(None);
let touch_type = touch.r#type();
let force = if let UITouchType::Pencil = touch_type {
None
} else if available!(ios = 9.0, tvos = 9.0, visionos = 1.0) {
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 {
let force = if available!(ios = 9.0, tvos = 9.0, visionos = 1.0) {
let use_force = match touch_type {
UITouchType::Pencil => true,
_ => {
let trait_collection = self.traitCollection();
trait_collection.forceTouchCapability() == UIForceTouchCapability::Available
},
};
if use_force {
let force = touch.force();
let max_possible_force = touch.maximumPossibleForce();
Some(Force::Calibrated {
@@ -529,7 +548,7 @@ impl WinitView {
primary,
position,
kind: if let UITouchType::Pencil = touch_type {
PointerKind::Unknown
PointerKind::TabletTool(TabletToolKind::Pencil)
} else {
PointerKind::Touch(finger_id)
},
@@ -543,7 +562,12 @@ impl WinitView {
state: ElementState::Pressed,
position,
button: if let UITouchType::Pencil = touch_type {
ButtonSource::Unknown(0)
let tool_data = self.tablet_tool_data_for_pencil(&touch);
ButtonSource::TabletTool {
kind: TabletToolKind::Pencil,
button: TabletToolButton::Contact,
data: tool_data,
}
} else {
ButtonSource::Touch { finger_id, force }
},
@@ -552,7 +576,11 @@ impl WinitView {
},
UITouchPhase::Moved => {
let (primary, source) = if let UITouchType::Pencil = touch_type {
(true, PointerSource::Unknown)
let tool_data = self.tablet_tool_data_for_pencil(&touch);
(true, PointerSource::TabletTool {
kind: TabletToolKind::Pencil,
data: tool_data,
})
} else {
(ivars.primary_finger.get().unwrap() == finger_id, PointerSource::Touch {
finger_id,
@@ -592,7 +620,12 @@ impl WinitView {
state: ElementState::Released,
position,
button: if let UITouchType::Pencil = touch_type {
ButtonSource::Unknown(0)
let tool_data = self.tablet_tool_data_for_pencil(&touch);
ButtonSource::TabletTool {
kind: TabletToolKind::Pencil,
button: TabletToolButton::Contact,
data: tool_data,
}
} else {
ButtonSource::Touch { finger_id, force }
},
@@ -607,7 +640,7 @@ impl WinitView {
primary,
position: Some(position),
kind: if let UITouchType::Pencil = touch_type {
PointerKind::Unknown
PointerKind::TabletTool(TabletToolKind::Pencil)
} else {
PointerKind::Touch(finger_id)
},
@@ -621,6 +654,32 @@ impl WinitView {
app_state::handle_nonuser_events(mtm, touch_events);
}
fn tablet_tool_data_for_pencil(&self, touch: &UITouch) -> TabletToolData {
let force = if available!(ios = 9.0, tvos = 9.0, visionos = 1.0) {
let force_val = touch.force();
let max_force = touch.maximumPossibleForce();
Some(Force::Calibrated { force: force_val as _, max_possible_force: max_force as _ })
} else {
None
};
let angle = if available!(ios = 9.1, tvos = 9.0, visionos = 1.0) {
let altitude = touch.altitudeAngle();
let azimuth = touch.azimuthAngleInView(Some(self));
Some(TabletToolAngle { altitude: altitude as _, azimuth: azimuth as _ })
} else {
None
};
TabletToolData {
force,
tangential_force: None, // iOS doesn't provide barrel pressure
twist: None, // iOS doesn't provide rotation/twist
tilt: None, // Will be calculated from angle if needed
angle,
}
}
fn handle_insert_text(&self, text: &NSString) {
let window = self.window().unwrap();
let window_id = window.id();

View File

@@ -7,6 +7,7 @@ use objc2_ui_kit::{
UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle,
UIUserInterfaceIdiom, UIView, UIViewController,
};
use tracing::trace_span;
use crate::{ScreenEdge, StatusBarStyle, ValidOrientations, WindowAttributesIos};
@@ -28,31 +29,37 @@ define_class!(
impl WinitViewController {
#[unsafe(method(shouldAutorotate))]
fn should_autorotate(&self) -> bool {
let _entered = trace_span!("shouldAutorotate").entered();
true
}
#[unsafe(method(prefersStatusBarHidden))]
fn prefers_status_bar_hidden(&self) -> bool {
let _entered = trace_span!("prefersStatusBarHidden").entered();
self.ivars().prefers_status_bar_hidden.get()
}
#[unsafe(method(preferredStatusBarStyle))]
fn preferred_status_bar_style(&self) -> UIStatusBarStyle {
let _entered = trace_span!("preferredStatusBarStyle").entered();
self.ivars().preferred_status_bar_style.get()
}
#[unsafe(method(prefersHomeIndicatorAutoHidden))]
fn prefers_home_indicator_auto_hidden(&self) -> bool {
let _entered = trace_span!("prefersHomeIndicatorAutoHidden").entered();
self.ivars().prefers_home_indicator_auto_hidden.get()
}
#[unsafe(method(supportedInterfaceOrientations))]
fn supported_orientations(&self) -> UIInterfaceOrientationMask {
let _entered = trace_span!("supportedInterfaceOrientations").entered();
self.ivars().supported_orientations.get()
}
#[unsafe(method(preferredScreenEdgesDeferringSystemGestures))]
fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge {
let _entered = trace_span!("preferredScreenEdgesDeferringSystemGestures").entered();
self.ivars().preferred_screen_edges_deferring_system_gestures.get()
}
}

View File

@@ -16,7 +16,7 @@ use objc2_ui_kit::{
UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen,
UIScreenOverscanCompensation, UIViewController, UIWindow,
};
use tracing::{debug, warn};
use tracing::{debug, debug_span, warn};
use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::WindowEvent;
@@ -46,6 +46,7 @@ define_class!(
impl WinitUIWindow {
#[unsafe(method(becomeKeyWindow))]
fn become_key_window(&self) {
let _entered = debug_span!("becomeKeyWindow").entered();
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::Window {
window_id: self.id(),
@@ -56,6 +57,7 @@ define_class!(
#[unsafe(method(resignKeyWindow))]
fn resign_key_window(&self) {
let _entered = debug_span!("resignKeyWindow").entered();
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, EventWrapper::Window {
window_id: self.id(),

View File

@@ -29,8 +29,8 @@ tracing.workspace = true
winit-core.workspace = true
# Platform-specific
ahash.workspace = true
calloop.workspace = true
foldhash.workspace = true
libc.workspace = true
memmap2.workspace = true
rustix = { workspace = true, features = ["std", "system", "thread", "process", "event", "pipe"] }
@@ -40,7 +40,7 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-feature
sctk-adwaita = { version = "0.11.0", default-features = false, optional = true }
wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] }
wayland-client = "0.31.10"
wayland-protocols = { version = "0.32.8", features = ["staging"] }
wayland-protocols = { version = "0.32.11", features = ["staging", "unstable"] }
wayland-protocols-plasma = { version = "0.3.8", features = ["client"] }
winit-common = { workspace = true, features = ["xkb", "wayland"] }

View File

@@ -14,6 +14,9 @@
//! * `wayland-csd-adwaita-crossfont`.
//! * `wayland-csd-adwaita-notitle`.
//! * `wayland-csd-adwaita-notitlebar`.
#![allow(clippy::mutable_key_type)]
use std::ffi::c_void;
use std::ptr::NonNull;

View File

@@ -2,7 +2,7 @@
use std::sync::Arc;
use ahash::AHashMap;
use foldhash::HashMap;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_touch::WlTouch;
@@ -12,6 +12,7 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::
use sctk::seat::pointer::{ThemeSpec, ThemedPointer};
use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState};
use tracing::warn;
use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_hold_v1::ZwpPointerGestureHoldV1;
use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2;
use winit_core::event::WindowEvent;
@@ -43,7 +44,7 @@ pub struct WinitSeatState {
touch: Option<WlTouch>,
/// The mapping from touched points to the surfaces they're present.
touch_map: AHashMap<i32, TouchPoint>,
touch_map: HashMap<i32, TouchPoint>,
/// Id of the first touch event.
first_touch_id: Option<i32>,
@@ -60,6 +61,9 @@ pub struct WinitSeatState {
/// The pinch pointer gesture bound on the seat.
pointer_gesture_pinch: Option<ZwpPointerGesturePinchV1>,
/// The hold pointer gesture bound on the seat.
pointer_gesture_hold: Option<ZwpPointerGestureHoldV1>,
/// The keyboard bound on the seat.
keyboard_state: Option<KeyboardState>,
@@ -141,6 +145,14 @@ impl SeatHandler for WinitState {
)
});
seat_state.pointer_gesture_hold = self.pointer_gestures.as_ref().map(|manager| {
manager.get_hold_gesture(
themed_pointer.pointer(),
queue_handle,
PointerGestureData::default(),
)
});
let themed_pointer = Arc::new(themed_pointer);
// Register cursor surface.
@@ -209,6 +221,10 @@ impl SeatHandler for WinitState {
pointer_gesture_pinch.destroy();
}
if let Some(pointer_gesture_hold) = seat_state.pointer_gesture_hold.take() {
pointer_gesture_hold.destroy();
}
if let Some(pointer) = seat_state.pointer.take() {
let pointer_data = pointer.pointer().winit_data();

View File

@@ -7,9 +7,12 @@ use sctk::globals::GlobalData;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, delegate_dispatch};
use sctk::reexports::protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::{
Event, ZwpPointerGesturePinchV1,
Event as PinchEvent, ZwpPointerGesturePinchV1,
};
use sctk::reexports::protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gestures_v1::ZwpPointerGesturesV1;
use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_hold_v1::{
Event as HoldEvent, ZwpPointerGestureHoldV1,
};
use winit_core::event::{TouchPhase, WindowEvent};
use winit_core::window::WindowId;
@@ -27,7 +30,7 @@ impl PointerGesturesState {
globals: &GlobalList,
queue_handle: &QueueHandle<WinitState>,
) -> Result<Self, BindError> {
let pointer_gestures = globals.bind(queue_handle, 1..=1, GlobalData)?;
let pointer_gestures = globals.bind(queue_handle, 3..=3, GlobalData)?;
Ok(Self { pointer_gestures })
}
}
@@ -70,6 +73,49 @@ impl Dispatch<ZwpPointerGesturesV1, GlobalData, WinitState> for PointerGesturesS
}
}
impl Dispatch<ZwpPointerGestureHoldV1, PointerGestureData, WinitState> for PointerGesturesState {
fn event(
state: &mut WinitState,
_proxy: &ZwpPointerGestureHoldV1,
event: <ZwpPointerGestureHoldV1 as wayland_client::Proxy>::Event,
data: &PointerGestureData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
let mut pointer_gesture_data = data.inner.lock().unwrap();
let (window_id, phase) = match event {
HoldEvent::Begin { surface, fingers, .. } => {
if fingers < 2 {
return;
}
let window_id = crate::make_wid(&surface);
pointer_gesture_data.window_id = Some(window_id);
(window_id, TouchPhase::Started)
},
HoldEvent::End { cancelled, .. } => {
let window_id = match pointer_gesture_data.window_id {
Some(window_id) => window_id,
_ => return,
};
// Reset the state.
*pointer_gesture_data = Default::default();
let phase = if cancelled == 0 { TouchPhase::Ended } else { TouchPhase::Cancelled };
(window_id, phase)
},
_ => return,
};
state
.events_sink
.push_window_event(WindowEvent::HoldGesture { device_id: None, phase }, window_id);
}
}
impl Dispatch<ZwpPointerGesturePinchV1, PointerGestureData, WinitState> for PointerGesturesState {
fn event(
state: &mut WinitState,
@@ -81,7 +127,7 @@ impl Dispatch<ZwpPointerGesturePinchV1, PointerGestureData, WinitState> for Poin
) {
let mut pointer_gesture_data = data.inner.lock().unwrap();
let (window_id, phase, pan_delta, pinch_delta, rotation_delta) = match event {
Event::Begin { surface, fingers, .. } => {
PinchEvent::Begin { surface, fingers, .. } => {
// We only support two fingers for now.
if fingers != 2 {
return;
@@ -100,7 +146,7 @@ impl Dispatch<ZwpPointerGesturePinchV1, PointerGestureData, WinitState> for Poin
(window_id, TouchPhase::Started, PhysicalPosition::new(0., 0.), 0., 0.)
},
Event::Update { dx, dy, scale: pinch, rotation, .. } => {
PinchEvent::Update { dx, dy, scale: pinch, rotation, .. } => {
let window_id = match pointer_gesture_data.window_id {
Some(window_id) => window_id,
_ => return,
@@ -121,7 +167,7 @@ impl Dispatch<ZwpPointerGesturePinchV1, PointerGestureData, WinitState> for Poin
let rotation_delta = -rotation as f32;
(window_id, TouchPhase::Moved, pan_delta, pinch_delta, rotation_delta)
},
Event::End { cancelled, .. } => {
PinchEvent::End { cancelled, .. } => {
let window_id = match pointer_gesture_data.window_id {
Some(window_id) => window_id,
_ => return,
@@ -155,3 +201,4 @@ impl Dispatch<ZwpPointerGesturePinchV1, PointerGestureData, WinitState> for Poin
delegate_dispatch!(WinitState: [ZwpPointerGesturesV1: GlobalData] => PointerGesturesState);
delegate_dispatch!(WinitState: [ZwpPointerGesturePinchV1: PointerGestureData] => PointerGesturesState);
delegate_dispatch!(WinitState: [ZwpPointerGestureHoldV1: PointerGestureData] => PointerGesturesState);

View File

@@ -150,7 +150,7 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
// 6. Place cursor inside preedit text.
if let Some(DeleteSurroundingText { before, after }) =
text_input_data.pending_delete
text_input_data.pending_delete.take()
{
state.events_sink.push_window_event(
WindowEvent::Ime(Ime::DeleteSurrounding {

View File

@@ -2,7 +2,7 @@ use std::cell::RefCell;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use ahash::AHashMap;
use foldhash::HashMap;
use sctk::compositor::{CompositorHandler, CompositorState};
use sctk::output::{OutputHandler, OutputState};
use sctk::reexports::calloop::LoopHandle;
@@ -29,7 +29,7 @@ use crate::seat::{
PointerConstraintsState, PointerGesturesState, RelativePointerState, TextInputState,
WinitPointerData, WinitPointerDataExt, WinitSeatState,
};
use crate::types::kwin_blur::KWinBlurManager;
use crate::types::bgr_effects::BgrEffectManager;
use crate::types::wp_fractional_scaling::FractionalScalingManager;
use crate::types::wp_tablet_input_v2::TabletManager;
use crate::types::wp_viewporter::ViewporterState;
@@ -62,10 +62,10 @@ pub struct WinitState {
pub xdg_shell: XdgShell,
/// The currently present windows.
pub windows: RefCell<AHashMap<WindowId, Arc<Mutex<WindowState>>>>,
pub windows: RefCell<HashMap<WindowId, Arc<Mutex<WindowState>>>>,
/// The requests from the `Window` to EventLoop, such as close operations and redraw requests.
pub window_requests: RefCell<AHashMap<WindowId, Arc<WindowRequests>>>,
pub window_requests: RefCell<HashMap<WindowId, Arc<WindowRequests>>>,
/// The events that were generated directly from the window.
pub window_events_sink: Arc<Mutex<EventSink>>,
@@ -74,10 +74,10 @@ pub struct WinitState {
pub window_compositor_updates: Vec<WindowCompositorUpdate>,
/// Currently handled seats.
pub seats: AHashMap<ObjectId, WinitSeatState>,
pub seats: HashMap<ObjectId, WinitSeatState>,
/// Currently present cursor surfaces.
pub pointer_surfaces: AHashMap<ObjectId, Arc<ThemedPointer<WinitPointerData>>>,
pub pointer_surfaces: HashMap<ObjectId, Arc<ThemedPointer<WinitPointerData>>>,
/// The state of the text input on the client.
pub text_input_state: Option<TextInputState>,
@@ -116,8 +116,8 @@ pub struct WinitState {
/// Fractional scaling manager.
pub fractional_scaling_manager: Option<FractionalScalingManager>,
/// KWin blur manager.
pub kwin_blur_manager: Option<KWinBlurManager>,
/// Blur manager.
pub blur_manager: Option<BgrEffectManager>,
/// Loop handle to re-register event sources, such as keyboard repeat.
pub loop_handle: LoopHandle<'static, Self>,
@@ -156,7 +156,7 @@ impl WinitState {
let seat_state = SeatState::new(globals, queue_handle);
let mut seats = AHashMap::default();
let mut seats = HashMap::default();
for seat in seat_state.seats() {
seats.insert(seat.id(), WinitSeatState::new());
}
@@ -192,7 +192,7 @@ impl WinitState {
window_events_sink: Default::default(),
viewporter_state,
fractional_scaling_manager,
kwin_blur_manager: KWinBlurManager::new(globals, queue_handle).ok(),
blur_manager: BgrEffectManager::new(globals, queue_handle).ok(),
seats,
text_input_state: TextInputState::new(globals, queue_handle).ok(),

View File

@@ -0,0 +1,85 @@
use sctk::compositor::Region;
use sctk::reexports::client::QueueHandle;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::protocols::ext::background_effect::v1::client::ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use crate::state::WinitState;
use crate::types::ext_background_effect::ExtBackgroundEffectManager;
use crate::types::kwin_blur::KWinBlurManager;
/// Wrapper around various background effects for [`WlSurface`].
#[derive(Debug, Clone)]
pub enum BgrEffectManager {
Ext(ExtBackgroundEffectManager),
KWin(KWinBlurManager),
}
impl BgrEffectManager {
pub fn new(
globals: &GlobalList,
queue_handle: &QueueHandle<WinitState>,
) -> Result<Self, BindError> {
ExtBackgroundEffectManager::new(globals, queue_handle)
.map(Self::Ext)
.or_else(|_| KWinBlurManager::new(globals, queue_handle).map(Self::KWin))
}
/// Creates a new blur effect for the surface.
pub fn new_blur_effect(
&mut self,
surface: &WlSurface,
queue_handle: &QueueHandle<WinitState>,
) -> SurfaceBlurEffect {
match self {
BgrEffectManager::Ext(mgr) => SurfaceBlurEffect::Ext(mgr.blur(surface, queue_handle)),
BgrEffectManager::KWin(mgr) => SurfaceBlurEffect::Kwin(
mgr.blur(surface, queue_handle),
mgr.clone(),
surface.clone(),
),
}
}
}
#[derive(Debug)]
pub enum SurfaceBlurEffect {
Ext(ExtBackgroundEffectSurfaceV1),
Kwin(OrgKdeKwinBlur, KWinBlurManager, WlSurface),
}
impl SurfaceBlurEffect {
/// Returns `true` if the main surface commit is required.
///
/// `None` clears the blur.
#[must_use]
pub fn set_blur(&self, region: Option<&Region>) -> bool {
let region = region.map(|region| region.wl_region());
match self {
SurfaceBlurEffect::Ext(surface) => {
surface.set_blur_region(region);
true
},
SurfaceBlurEffect::Kwin(blur, ..) => {
blur.set_region(region);
blur.commit();
true
},
}
}
}
impl Drop for SurfaceBlurEffect {
fn drop(&mut self) {
match self {
SurfaceBlurEffect::Ext(surface) => surface.destroy(),
SurfaceBlurEffect::Kwin(blur, mgr, wl_surface) => {
blur.set_region(None);
blur.commit();
blur.release();
mgr.unset(wl_surface);
},
}
}
}

View File

@@ -0,0 +1,59 @@
use sctk::globals::GlobalData;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, delegate_dispatch};
use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_manager_v1::ExtBackgroundEffectManagerV1;
use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1;
use crate::state::WinitState;
#[derive(Debug, Clone)]
pub struct ExtBackgroundEffectManager {
manager: ExtBackgroundEffectManagerV1,
}
impl ExtBackgroundEffectManager {
pub fn new(
globals: &GlobalList,
queue_handle: &QueueHandle<WinitState>,
) -> Result<Self, BindError> {
let manager = globals.bind(queue_handle, 1..=1, GlobalData)?;
Ok(Self { manager })
}
pub fn blur(
&mut self,
surface: &WlSurface,
queue_handle: &QueueHandle<WinitState>,
) -> ExtBackgroundEffectSurfaceV1 {
self.manager.get_background_effect(surface, queue_handle, ())
}
}
impl Dispatch<ExtBackgroundEffectManagerV1, GlobalData, WinitState> for ExtBackgroundEffectManager {
fn event(
_: &mut WinitState,
_: &ExtBackgroundEffectManagerV1,
_: <ExtBackgroundEffectManagerV1 as Proxy>::Event,
_: &GlobalData,
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ExtBackgroundEffectSurfaceV1, (), WinitState> for ExtBackgroundEffectManager {
fn event(
_: &mut WinitState,
_: &ExtBackgroundEffectSurfaceV1,
_: <ExtBackgroundEffectSurfaceV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
// There is no event
}
}
delegate_dispatch!(WinitState: [ExtBackgroundEffectManagerV1: GlobalData] => ExtBackgroundEffectManager);
delegate_dispatch!(WinitState: [ExtBackgroundEffectSurfaceV1: ()] => ExtBackgroundEffectManager);

View File

@@ -1,6 +1,8 @@
//! Wayland protocol implementation boilerplate.
pub mod bgr_effects;
pub mod cursor;
pub mod ext_background_effect;
pub mod kwin_blur;
pub mod wp_fractional_scaling;
pub mod wp_tablet_input_v2;

View File

@@ -21,6 +21,13 @@ use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::{
ButtonState, Event as ToolEvent, Type as ToolType, ZwpTabletToolV2,
};
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_v2::ZwpTabletV2;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_dial_v2::ZwpTabletPadDialV2;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::{
self, ZwpTabletPadGroupV2,
};
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2;
use winit_core::event::{
ButtonSource, ElementState, Force, PointerKind, PointerSource, TabletToolButton,
TabletToolData as CoreTabletToolData, TabletToolKind, TabletToolTilt, WindowEvent,
@@ -324,6 +331,10 @@ impl Dispatch<ZwpTabletV2, (), WinitState> for TabletManager {
}
impl Dispatch<ZwpTabletPadV2, (), WinitState> for TabletManager {
event_created_child!(WinitState, ZwpTabletPadV2, [
zwp_tablet_pad_v2::EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, Default::default()),
]);
fn event(
_: &mut WinitState,
_: &ZwpTabletPadV2,
@@ -335,9 +346,67 @@ impl Dispatch<ZwpTabletPadV2, (), WinitState> for TabletManager {
}
}
impl Dispatch<ZwpTabletPadGroupV2, (), WinitState> for TabletManager {
event_created_child!(WinitState, ZwpTabletPadGroupV2, [
zwp_tablet_pad_group_v2::EVT_RING_OPCODE => (ZwpTabletPadRingV2, Default::default()),
zwp_tablet_pad_group_v2::EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, Default::default()),
zwp_tablet_pad_group_v2::EVT_DIAL_OPCODE => (ZwpTabletPadDialV2, Default::default()),
]);
fn event(
_: &mut WinitState,
_: &ZwpTabletPadGroupV2,
_: <ZwpTabletPadGroupV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ZwpTabletPadRingV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletPadRingV2,
_: <ZwpTabletPadRingV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ZwpTabletPadStripV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletPadStripV2,
_: <ZwpTabletPadStripV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ZwpTabletPadDialV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletPadDialV2,
_: <ZwpTabletPadDialV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
delegate_dispatch!(WinitState: [ZwpTabletManagerV2: GlobalData] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletManagerV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletSeatV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletToolV2: TabletToolData] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadGroupV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadRingV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadStripV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadDialV2: ()] => TabletManager);

View File

@@ -127,7 +127,8 @@ impl Window {
// Set transparency hint.
window_state.set_transparent(attributes.transparent);
window_state.set_blur(attributes.blur);
// Set blur.
let _ = window_state.set_blur(attributes.blur);
// Set the decorations hint.
window_state.set_decorate(attributes.decorations);
@@ -171,6 +172,12 @@ impl Window {
Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor),
}
// Apply resize increments.
if let Some(increments) = attributes.surface_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(), activation_token) {
xdg_activation.activate(token.into_raw(), &surface);
@@ -370,11 +377,18 @@ impl CoreWindow for Window {
}
fn surface_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))
}
fn set_surface_resize_increments(&self, _increments: Option<Size>) {
warn!("`set_surface_resize_increments` is not implemented for Wayland");
fn set_surface_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);
}
fn set_title(&self, title: &str) {
@@ -485,7 +499,9 @@ impl CoreWindow for Window {
#[inline]
fn set_blur(&self, blur: bool) {
self.window_state.lock().unwrap().set_blur(blur);
if self.window_state.lock().unwrap().set_blur(blur) {
self.request_redraw();
}
}
#[inline]

View File

@@ -4,8 +4,8 @@ use std::num::NonZeroU32;
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use ahash::HashSet;
use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size};
use foldhash::HashSet;
use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt};
use sctk::globals::GlobalData;
use sctk::reexports::client::backend::ObjectId;
@@ -29,7 +29,6 @@ use sctk::shm::slot::SlotPool;
use sctk::subcompositor::SubcompositorState;
use tracing::{info, warn};
use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager_v1::XdgToplevelIconManagerV1;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor};
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::window::{
@@ -43,8 +42,8 @@ use crate::seat::{
ZwpTextInputV3Ext,
};
use crate::state::{WindowCompositorUpdate, WinitState};
use crate::types::bgr_effects::{BgrEffectManager, SurfaceBlurEffect};
use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor};
use crate::types::kwin_blur::KWinBlurManager;
use crate::types::xdg_toplevel_icon_manager::ToplevelIcon;
#[cfg(feature = "sctk-adwaita")]
@@ -140,6 +139,7 @@ pub struct WindowState {
/// Min size.
min_surface_size: LogicalSize<u32>,
max_surface_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
@@ -155,8 +155,8 @@ pub struct WindowState {
viewport: Option<WpViewport>,
fractional_scale: Option<WpFractionalScaleV1>,
blur: Option<OrgKdeKwinBlur>,
blur_manager: Option<KWinBlurManager>,
blur: Option<SurfaceBlurEffect>,
blur_manager: Option<BgrEffectManager>,
/// Whether the client side decorations have pending move operations.
///
@@ -205,7 +205,7 @@ impl WindowState {
toplevel_icon: None,
xdg_toplevel_icon_manager,
blur: None,
blur_manager: winit_state.kwin_blur_manager.clone(),
blur_manager: winit_state.blur_manager.clone(),
compositor,
handle,
csd_fails: false,
@@ -223,6 +223,7 @@ impl WindowState {
last_configure: None,
max_surface_size: None,
min_surface_size: MIN_WINDOW_SIZE,
resize_increments: None,
pointer_constraints,
pointers: Default::default(),
queue_handle: queue_handle.clone(),
@@ -361,6 +362,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_surface_size.width),
new_size.height.saturating_sub(self.min_surface_size.height),
);
let width = self.min_surface_size.width
+ (delta_width / increments.width) * increments.width;
let height = self.min_surface_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);
@@ -704,6 +741,13 @@ impl WindowState {
// Set surface size without the borders.
viewport.set_destination(self.size.width as _, self.size.height as _);
}
// Update blur region with new size.
if self.blur.is_some() {
// NOTE: either user resized or configure, in both cases
// the redraw scheduling is done on the caller side.
let _ = self.set_blur(true);
}
}
/// Get the scale factor of the window.
@@ -749,6 +793,18 @@ 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, data| {
let surface = pointer.surface();
@@ -1063,20 +1119,37 @@ impl WindowState {
}
}
/// Make window background blurred
#[inline]
pub fn set_blur(&mut self, blurred: bool) {
if blurred && self.blur.is_none() {
if let Some(blur_manager) = self.blur_manager.as_ref() {
let blur = blur_manager.blur(self.window.wl_surface(), &self.queue_handle);
blur.commit();
self.blur = Some(blur);
} else {
info!("Blur manager unavailable, unable to change blur")
}
} else if !blurred && self.blur.is_some() {
self.blur_manager.as_ref().unwrap().unset(self.window.wl_surface());
self.blur.take().unwrap().release();
/// Make window background blurred.
///
/// Returns `true` if redraw is required.
#[must_use]
pub fn set_blur(&mut self, blurred: bool) -> bool {
if !blurred {
self.blur = None;
return true;
}
let mgr = match self.blur_manager.as_mut() {
Some(mgr) => mgr,
None => {
info!("Blur manager unavailable, unable to change blur");
return false;
},
};
let blur = match self.blur.as_ref() {
Some(blur) => blur,
None => {
self.blur = Some(mgr.new_blur_effect(self.window.wl_surface(), &self.queue_handle));
self.blur.as_ref().unwrap()
},
};
if let Ok(region) = Region::new(&*self.compositor) {
region.add(0, 0, i32::MAX, i32::MAX);
blur.set_blur(Some(&region))
} else {
false
}
}
@@ -1174,10 +1247,6 @@ impl WindowState {
impl Drop for WindowState {
fn drop(&mut self) {
if let Some(blur) = self.blur.take() {
blur.release();
}
if let Some(fs) = self.fractional_scale.take() {
fs.destroy();
}

View File

@@ -526,7 +526,7 @@ impl MonitorHandler {
},
};
// Notifying `Future`s is not dependant on the lifetime of the runner,
// Notifying `Future`s is not dependent on the lifetime of the runner,
// because they can outlive it.
if let Some(runner) = runner.upgrade() {
if let Some(details) = details {
@@ -761,7 +761,7 @@ impl MonitorPermissionFuture {
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(details) => {
// Notifying `Future`s is not dependant on the lifetime of the runner, because
// Notifying `Future`s is not dependent on the lifetime of the runner, because
// they can outlive it.
notifier.notify(Ok(()));

View File

@@ -3,7 +3,7 @@ use std::sync::LazyLock;
/// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode
use std::{ffi::c_void, ptr};
use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, S_OK, WPARAM};
use windows_sys::Win32::Foundation::{HWND, LPARAM, S_OK, WPARAM};
use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
use windows_sys::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTA};
use windows_sys::Win32::UI::Controls::SetWindowTheme;
@@ -11,7 +11,7 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::WindowsAndMessaging::{
DefWindowProcW, SPI_GETHIGHCONTRAST, SystemParametersInfoA, WM_NCACTIVATE,
};
use windows_sys::core::{PCSTR, PCWSTR};
use windows_sys::core::{BOOL, PCSTR, PCWSTR};
use windows_sys::w;
use winit_core::window::Theme;

View File

@@ -3,9 +3,9 @@
use std::ffi::c_void;
use windows_sys::Win32::Foundation::{BOOL, HWND, POINTL};
use windows_sys::Win32::Foundation::{HWND, POINTL};
use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM};
use windows_sys::core::{GUID, HRESULT};
use windows_sys::core::{BOOL, GUID, HRESULT};
pub type IUnknown = *mut c_void;
pub type IAdviseSink = *mut c_void;

View File

@@ -51,12 +51,12 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
SystemParametersInfoW, TranslateMessage, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE,
WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO,
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION,
WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE,
WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN,
WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS,
WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH,
WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM,
WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT,
WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR,
WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP,
WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM,
WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT,
WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
@@ -528,9 +528,11 @@ fn main_thread_id() -> u32 {
//
// See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute
#[unsafe(link_section = ".CRT$XCU")]
static INIT_MAIN_THREAD_ID: unsafe fn() = {
unsafe fn initer() {
unsafe { MAIN_THREAD_ID = GetCurrentThreadId() };
static INIT_MAIN_THREAD_ID: unsafe extern "C" fn() = {
unsafe extern "C" fn initer() {
unsafe {
MAIN_THREAD_ID = GetCurrentThreadId();
}
}
initer
};
@@ -1501,8 +1503,23 @@ unsafe fn public_window_callback_inner(
},
WM_IME_SETCONTEXT => {
// Hide composing text drawn by IME.
let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize);
// IME UI visibility flags are in lparam.
let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize);
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
},
WM_INPUTLANGCHANGE => {
// Refresh the cached keyboard layout for the newly activated input
// language. This message is sent (by Windows or by layout switchers
// such as Punto Switcher) after the layout changes. Refreshing the
// cache here prevents a freeze that otherwise occurs when switching
// layout via such tools. We still defer to `DefWindowProc` so the
// message keeps propagating to first-level child windows, as the
// Win32 documentation requires.
{
let mut layouts = LAYOUT_CACHE.lock().unwrap();
layouts.get_current_layout();
}
result = ProcResult::DefWindowProc(wparam);
},

View File

@@ -387,11 +387,7 @@ impl LayoutCache {
let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id);
let key = match unicode {
ToUnicodeResult::Str(str) => Key::Character(SmolStr::new(str)),
ToUnicodeResult::Dead(dead_char) => {
// println!("{:?} - {:?} produced dead {:?}", key_code, mod_state,
// dead_char);
Key::Dead(dead_char)
},
ToUnicodeResult::Dead(dead_char) => Key::Dead(dead_char),
ToUnicodeResult::None => {
let has_alt = mod_state.contains(WindowsModifiers::ALT);
let has_ctrl = mod_state.contains(WindowsModifiers::CONTROL);

View File

@@ -4,13 +4,14 @@ use std::num::{NonZeroU16, NonZeroU32};
use std::{io, iter, mem, ptr};
use dpi::{PhysicalPosition, PhysicalSize};
use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT};
use windows_sys::Win32::Foundation::{HWND, LPARAM, POINT, RECT};
use windows_sys::Win32::Graphics::Gdi::{
DEVMODEW, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH,
ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, HDC,
HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTOPRIMARY, MONITORINFO, MONITORINFOEXW,
MonitorFromPoint, MonitorFromWindow,
};
use windows_sys::core::BOOL;
use winit_core::monitor::{MonitorHandleProvider, VideoMode};
use super::util::decode_wide;

View File

@@ -6,7 +6,7 @@ use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{io, mem, ptr};
use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, NTSTATUS, POINT, RECT};
use windows_sys::Win32::Foundation::{HANDLE, HMODULE, HWND, NTSTATUS, POINT, RECT};
use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR};
use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
@@ -23,7 +23,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_MAXIMIZE,
ShowCursor, WINDOW_LONG_PTR_INDEX, WINDOWPLACEMENT,
};
use windows_sys::core::{HRESULT, PCWSTR};
use windows_sys::core::{BOOL, HRESULT, PCWSTR};
use winit_core::cursor::CursorIcon;
use winit_core::event::DeviceId;

View File

@@ -1757,7 +1757,7 @@ impl EventProcessor {
.find(|prev_monitor| prev_monitor.name == new_monitor.name)
.map(|prev_monitor| prev_monitor.scale_factor);
if Some(new_monitor.scale_factor) != maybe_prev_scale_factor {
for window in self.target.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) {
for window in self.target.windows.borrow().values().filter_map(|w| w.upgrade()) {
window.refresh_dpi_for_monitor(
&new_monitor,
maybe_prev_scale_factor,

View File

@@ -50,8 +50,13 @@ 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)
if !self.is_destroyed {
if let Some(im) = &self.im {
unsafe { close_im(&self.xconn, im.im) }?;
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}

View File

@@ -60,16 +60,18 @@ impl Iterator for KeymapIter<'_> {
impl XConnection {
pub fn query_keymap(&self) -> Keymap {
let mut keys = [0; 32];
unsafe {
(self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char);
}
let keys = self
.xcb_connection()
.query_keymap()
.expect("Failed to query keymap")
.reply()
.expect("Missing reply")
.keys;
Keymap { keys }
}
}
fn first_bit(b: u8) -> u8 {
1 << b.trailing_zeros()
b & b.wrapping_neg()
}

View File

@@ -66,7 +66,6 @@ impl XConnection {
// All util functions that abstract an async function will return a `Flusher`.
pub fn flush_requests(&self) -> Result<(), XError> {
unsafe { (self.xlib.XFlush)(self.display) };
// println!("XFlush");
// This isn't necessarily a useful time to check for errors (since our request hasn't
// necessarily been processed yet)
self.check_errors()
@@ -74,7 +73,6 @@ impl XConnection {
pub fn sync_with_server(&self) -> Result<(), XError> {
unsafe { (self.xlib.XSync)(self.display, ffi::False) };
// println!("XSync");
self.check_errors()
}
}

View File

@@ -26,10 +26,9 @@ use winit_core::window::{
};
use x11rb::connection::{Connection, RequestConnection};
use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification};
use x11rb::protocol::shape::SK;
use x11rb::protocol::shape::{ConnectionExt as ShapeExt, SK, SO};
use x11rb::protocol::sync::{ConnectionExt as _, Int64};
use x11rb::protocol::xfixes::{ConnectionExt, RegionWrapper};
use x11rb::protocol::xproto::{self, ConnectionExt as _, Rectangle};
use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle};
use x11rb::protocol::{randr, xinput};
use crate::atoms::*;
@@ -1967,6 +1966,15 @@ impl UnownedWindow {
#[inline]
pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> {
// 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.surface_size();
@@ -1977,11 +1985,17 @@ impl UnownedWindow {
height: size.height as u16,
})
}
let region = RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles)
.map_err(|_e| RequestError::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| RequestError::Ignored)?;
self.shared_state_lock().cursor_hittest = Some(hittest);
Ok(())

View File

@@ -250,9 +250,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

@@ -78,8 +78,10 @@ winit-core.workspace = true
image = { workspace = true, features = ["png"] }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
[target.'cfg(not(target_os = "android"))'.dev-dependencies]
# Launching a window without drawing to it has unpredictable results varying from platform to
# platform. We use the `softbuffer` crate in our examples because of its ease of use to avoid
# confusion around this. `glutin` or `wgpu` could also be used to fill the window buffer, but they
# are more complicated to set up.
softbuffer.workspace = true
[target.'cfg(target_os = "android")'.dependencies]

View File

@@ -1,7 +1,9 @@
use cfg_aliases::cfg_aliases;
// Only relevant for examples and Winit, our usage of println! is fine here.
#[allow(clippy::disallowed_macros)]
fn main() {
// The script doesn't depend on our code.
// Dummy invocation to enable change-tracking in build scripts.
println!("cargo:rerun-if-changed=build.rs");
// Setup cfg aliases.

View File

@@ -7,28 +7,24 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Debug;
#[cfg(not(android_platform))]
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
#[cfg(all(not(android_platform), not(web_platform)))]
#[cfg(not(web_platform))]
use std::time::Instant;
use std::{fmt, mem};
use cursor_icon::CursorIcon;
#[cfg(not(android_platform))]
use rwh_06::{DisplayHandle, HasDisplayHandle};
#[cfg(not(android_platform))]
use softbuffer::{Context, Surface};
use tracing::{error, info};
#[cfg(all(web_platform, not(android_platform)))]
#[cfg(web_platform)]
use web_time::Instant;
use winit::application::ApplicationHandler;
use winit::cursor::{Cursor, CustomCursor, CustomCursorSource};
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::error::RequestError;
use winit::event::{DeviceEvent, DeviceId, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::icon::{Icon, RgbaIcon};
use winit::keyboard::{Key, ModifiersState};
use winit::monitor::Fullscreen;
@@ -48,9 +44,6 @@ use winit_core::application::macos::ApplicationHandlerExtMacOS;
#[path = "util/tracing.rs"]
mod tracing_init;
#[path = "util/fill.rs"]
mod fill;
/// The amount of points to around the window for drag resize direction calculations.
const BORDER_SIZE: f64 = 20.;
@@ -97,22 +90,12 @@ struct Application {
/// Drawing context.
///
/// With OpenGL it could be EGLDisplay.
#[cfg(not(android_platform))]
context: Option<Context<DisplayHandle<'static>>>,
context: Context<OwnedDisplayHandle>,
}
impl Application {
fn new(event_loop: &EventLoop, receiver: Receiver<Action>, sender: Sender<Action>) -> Self {
// SAFETY: we drop the context right before the event loop is stopped, thus making it safe.
#[cfg(not(android_platform))]
let context = Some(
Context::new(unsafe {
std::mem::transmute::<DisplayHandle<'_>, DisplayHandle<'static>>(
event_loop.display_handle().unwrap(),
)
})
.unwrap(),
);
let context = Context::new(event_loop.owned_display_handle()).unwrap();
// You'll have to choose an icon size at your own discretion. On X11, the desired size
// varies by WM, and on Windows, you still have to account for screen scaling. Here
@@ -130,15 +113,7 @@ impl Application {
.into_iter()
.collect();
Self {
receiver,
sender,
#[cfg(not(android_platform))]
context,
custom_cursors,
icon,
windows: Default::default(),
}
Self { receiver, sender, context, custom_cursors, icon, windows: Default::default() }
}
fn create_window(
@@ -251,6 +226,12 @@ impl Application {
Action::ToggleSimpleFullscreen => {
window.window.set_simple_fullscreen(!window.window.simple_fullscreen());
},
#[cfg(macos_platform)]
Action::ToggleBorderlessGame => {
let current = window.window.is_borderless_game();
window.window.set_borderless_game(!current);
info!("Borderless game: {}", !current);
},
Action::ToggleMaximize => window.toggle_maximize(),
Action::Minimize => window.minimize(),
Action::NextCursor => window.next_cursor(),
@@ -548,16 +529,7 @@ impl ApplicationHandler for Application {
WindowEvent::DoubleTapGesture { .. } => {
info!("Smart zoom");
},
WindowEvent::TouchpadPressure { .. }
| WindowEvent::DragLeft { .. }
| WindowEvent::KeyboardInput { .. }
| WindowEvent::PointerEntered { .. }
| WindowEvent::DragEntered { .. }
| WindowEvent::DragMoved { .. }
| WindowEvent::DragDropped { .. }
| WindowEvent::Destroyed
| WindowEvent::Ime(_)
| WindowEvent::Moved(_) => (),
_ => (),
}
}
@@ -612,10 +584,7 @@ impl ApplicationHandlerExtMacOS for Application {
/// State of the window.
struct WindowState {
/// Render surface.
///
/// NOTE: This surface must be dropped before the `Window`.
#[cfg(not(android_platform))]
surface: Surface<DisplayHandle<'static>, Arc<dyn Window>>,
surface: Surface<OwnedDisplayHandle, Arc<dyn Window>>,
/// The actual winit Window.
window: Arc<dyn Window>,
/// The window theme we're drawing with.
@@ -623,7 +592,6 @@ struct WindowState {
/// Fill the window with animated color
animated_fill_color: bool,
/// The application start time. Used for color fill animation
#[cfg(not(android_platform))]
start_time: Instant,
/// Redraw continuously
continuous_redraw: bool,
@@ -657,10 +625,7 @@ impl WindowState {
fn new(app: &Application, window: Box<dyn Window>) -> Result<Self, Box<dyn Error>> {
let window: Arc<dyn Window> = Arc::from(window);
// SAFETY: the surface is dropped before the `window` which provided it with handle, thus
// it doesn't outlive it.
#[cfg(not(android_platform))]
let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?;
let surface = Surface::new(&app.context, Arc::clone(&window))?;
let theme = window.theme().unwrap_or(Theme::Dark);
info!("Theme: {theme:?}");
@@ -674,14 +639,12 @@ impl WindowState {
custom_idx: app.custom_cursors.as_ref().map(Vec::len).unwrap_or(1) - 1,
cursor_grab: CursorGrabMode::None,
named_idx,
#[cfg(not(android_platform))]
surface,
window,
theme,
animated_fill_color: false,
continuous_redraw: false,
emit_surface_size: false,
#[cfg(not(android_platform))]
start_time: Instant::now(),
cursor_position: Default::default(),
cursor_hidden: Default::default(),
@@ -853,15 +816,12 @@ impl WindowState {
/// Resize the surface to the new size.
fn resize(&mut self, size: PhysicalSize<u32>) {
info!("Surface resized to {size:?}");
#[cfg(not(android_platform))]
{
let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
{
(Some(width), Some(height)) => (width, height),
_ => return,
};
self.surface.resize(width, height).expect("failed to resize inner buffer");
}
let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) {
(Some(width), Some(height)) => (width, height),
_ => return,
};
self.surface.resize(width, height).expect("failed to resize inner buffer");
self.window.request_redraw();
}
@@ -946,42 +906,46 @@ impl WindowState {
}
/// Draw the window contents.
#[cfg(not(android_platform))]
fn draw(&mut self) -> Result<(), Box<dyn Error>> {
if self.occluded {
info!("Skipping drawing occluded window={:?}", self.window.id());
return Ok(());
}
if self.animated_fill_color {
fill::fill_window_with_animated_color(&*self.window, self.start_time);
return Ok(());
}
let mut buffer = self.surface.buffer_mut()?;
// Draw a different color inside the safe area
let surface_size = self.window.surface_size();
let insets = self.window.safe_area();
for y in 0..surface_size.height {
for x in 0..surface_size.width {
let index = y as usize * surface_size.width as usize + x as usize;
if insets.left <= x
&& x <= (surface_size.width - insets.right)
&& insets.top <= y
&& y <= (surface_size.height - insets.bottom)
{
// In safe area
buffer[index] = match self.theme {
Theme::Light => 0xffe8e8e8, // Light gray
Theme::Dark => 0xff525252, // Medium gray
};
} else {
// Outside safe area
buffer[index] = match self.theme {
Theme::Light => 0xffffffff, // White
Theme::Dark => 0xff181818, // Dark gray
};
if self.animated_fill_color {
// Fill the entire buffer with a single color.
let time = self.start_time.elapsed().as_secs_f32() * 1.5;
let blue = (time.sin() * 255.0) as u32;
let green = ((time.cos() * 255.0) as u32) << 8;
let red = ((1.0 - time.sin() * 255.0) as u32) << 16;
let color = red | green | blue;
buffer.fill(color);
} else {
// Draw a different color inside the safe area
let surface_size = self.window.surface_size();
let insets = self.window.safe_area();
for y in 0..surface_size.height {
for x in 0..surface_size.width {
let index = y as usize * surface_size.width as usize + x as usize;
if insets.left <= x
&& x <= (surface_size.width - insets.right)
&& insets.top <= y
&& y <= (surface_size.height - insets.bottom)
{
// In safe area
buffer[index] = match self.theme {
Theme::Light => 0xffe8e8e8, // Light gray
Theme::Dark => 0xff525252, // Medium gray
};
} else {
// Outside safe area
buffer[index] = match self.theme {
Theme::Light => 0xffffffff, // White
Theme::Dark => 0xff181818, // Dark gray
};
}
}
}
}
@@ -992,12 +956,6 @@ impl WindowState {
Ok(())
}
#[cfg(android_platform)]
fn draw(&mut self) -> Result<(), Box<dyn Error>> {
info!("Drawing but without rendering...");
Ok(())
}
}
struct Binding<T: Eq> {
@@ -1027,6 +985,8 @@ enum Action {
ToggleFullscreen,
#[cfg(macos_platform)]
ToggleSimpleFullscreen,
#[cfg(macos_platform)]
ToggleBorderlessGame,
ToggleMaximize,
Minimize,
NextCursor,
@@ -1064,6 +1024,8 @@ impl Action {
Action::ToggleFullscreen => "Toggle fullscreen",
#[cfg(macos_platform)]
Action::ToggleSimpleFullscreen => "Toggle simple fullscreen",
#[cfg(macos_platform)]
Action::ToggleBorderlessGame => "Toggle borderless game mode",
Action::ToggleMaximize => "Maximize",
Action::Minimize => "Minimize",
Action::ToggleResizeIncrements => "Use resize increments when resizing window",
@@ -1315,6 +1277,8 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
Binding::new("T", ModifiersState::META, Action::CreateNewTab),
#[cfg(macos_platform)]
Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt),
#[cfg(macos_platform)]
Binding::new("B", ModifiersState::CONTROL, Action::ToggleBorderlessGame),
Binding::new("S", ModifiersState::ALT, Action::EmitSurfaceSize),
Binding::new("S", ModifiersState::CONTROL, Action::Message),
];

View File

@@ -3,10 +3,12 @@
fn main() -> Result<(), impl std::error::Error> {
use std::collections::HashMap;
use softbuffer::{Context, Surface};
use tracing::info;
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize, Position};
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::raw_window_handle::HasRawWindowHandle;
use winit::window::{Window, WindowAttributes, WindowId};
@@ -15,18 +17,20 @@ fn main() -> Result<(), impl std::error::Error> {
#[derive(Debug)]
struct WindowData {
window: Box<dyn Window>,
surface: Surface<OwnedDisplayHandle, Box<dyn Window>>,
color: u32,
}
impl WindowData {
fn new(window: Box<dyn Window>, color: u32) -> Self {
Self { window, color }
fn new(context: &Context<OwnedDisplayHandle>, window: Box<dyn Window>, color: u32) -> Self {
let surface = Surface::new(context, window).unwrap();
Self { surface, color }
}
}
#[derive(Default, Debug)]
#[derive(Debug)]
struct Application {
context: Context<OwnedDisplayHandle>,
parent_window_id: Option<WindowId>,
windows: HashMap<WindowId, WindowData>,
}
@@ -38,10 +42,10 @@ fn main() -> Result<(), impl std::error::Error> {
.with_position(Position::Logical(LogicalPosition::new(0.0, 0.0)))
.with_surface_size(LogicalSize::new(640.0f32, 480.0f32));
let window = event_loop.create_window(attributes).unwrap();
println!("Parent window id: {:?})", window.id());
info!("Parent window id: {:?})", window.id());
self.parent_window_id = Some(window.id());
self.windows.insert(window.id(), WindowData::new(window, 0xffbbbbbb));
self.windows.insert(window.id(), WindowData::new(&self.context, window, 0xffbbbbbb));
}
fn window_event(
@@ -56,12 +60,12 @@ fn main() -> Result<(), impl std::error::Error> {
event_loop.exit();
},
WindowEvent::PointerEntered { device_id: _, .. } => {
// On x11, println when the cursor entered in a window even if the child window
// On x11, log when the cursor entered in a window even if the child window
// is created by some key inputs.
// the child windows are always placed at (0, 0) with size (200, 200) in the
// parent window, so we also can see this log when we move
// the cursor around (200, 200) in parent window.
println!("cursor entered in the window {window_id:?}");
info!("cursor entered in the window {window_id:?}");
},
WindowEvent::KeyboardInput {
event: KeyEvent { state: ElementState::Pressed, .. },
@@ -72,18 +76,24 @@ fn main() -> Result<(), impl std::error::Error> {
0xff000000 + 3_u32.pow((child_index + 2).rem_euclid(16) as u32);
let parent_window = self.windows.get(&self.parent_window_id.unwrap()).unwrap();
let child_window =
spawn_child_window(parent_window.window.as_ref(), event_loop, child_index);
let child_window = spawn_child_window(
parent_window.surface.window().as_ref(),
event_loop,
child_index,
);
let child_id = child_window.id();
println!("Child window created with id: {child_id:?}");
self.windows.insert(child_id, WindowData::new(child_window, child_color));
info!("Child window created with id: {child_id:?}");
self.windows.insert(
child_id,
WindowData::new(&self.context, child_window, child_color),
);
},
WindowEvent::RedrawRequested => {
if let Some(window) = self.windows.get(&window_id) {
if let Some(window) = self.windows.get_mut(&window_id) {
if window_id == self.parent_window_id.unwrap() {
fill::fill_window(window.window.as_ref());
fill::fill(&mut window.surface);
} else {
fill::fill_window_with_color(window.window.as_ref(), window.color);
fill::fill_with_color(&mut window.surface, window.color);
}
}
},
@@ -117,7 +127,8 @@ fn main() -> Result<(), impl std::error::Error> {
}
let event_loop = EventLoop::new().unwrap();
event_loop.run_app(Application::default())
let context = Context::new(event_loop.owned_display_handle()).unwrap();
event_loop.run_app(Application { context, parent_window_id: None, windows: HashMap::new() })
}
#[cfg(not(any(x11_platform, macos_platform, windows_platform)))]

View File

@@ -4,12 +4,13 @@ use std::thread;
#[cfg(not(web_platform))]
use std::time;
use ::tracing::{info, warn};
use softbuffer::{Context, Surface};
use tracing::{info, warn};
#[cfg(web_platform)]
use web_time as time;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, KeyEvent, StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, OwnedDisplayHandle};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowAttributes, WindowId};
@@ -52,7 +53,7 @@ struct ControlFlowDemo {
request_redraw: bool,
wait_cancelled: bool,
close_requested: bool,
window: Option<Box<dyn Window>>,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
}
impl ApplicationHandler for ControlFlowDemo {
@@ -69,7 +70,10 @@ impl ApplicationHandler for ControlFlowDemo {
let window_attributes = WindowAttributes::default().with_title(
"Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.",
);
self.window = Some(event_loop.create_window(window_attributes).unwrap());
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window).unwrap();
self.surface = Some(surface);
}
fn window_event(
@@ -112,9 +116,9 @@ impl ApplicationHandler for ControlFlowDemo {
_ => (),
},
WindowEvent::RedrawRequested => {
let window = self.window.as_ref().unwrap();
window.pre_present_notify();
fill::fill_window(window.as_ref());
let surface = self.surface.as_mut().unwrap();
surface.window().pre_present_notify();
fill::fill(surface);
},
_ => (),
}
@@ -122,7 +126,7 @@ impl ApplicationHandler for ControlFlowDemo {
fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
if self.request_redraw && !self.wait_cancelled && !self.close_requested {
self.window.as_ref().unwrap().request_redraw();
self.surface.as_ref().unwrap().window().request_redraw();
}
match self.mode {

View File

@@ -1,8 +1,10 @@
use std::error::Error;
use softbuffer::{Context, Surface};
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
@@ -15,27 +17,24 @@ fn main() -> Result<(), Box<dyn Error>> {
let event_loop = EventLoop::new()?;
let app = Application::new();
let app = Application::default();
Ok(event_loop.run_app(app)?)
}
/// Application state and event handling.
#[derive(Debug)]
#[derive(Default, Debug)]
struct Application {
window: Option<Box<dyn Window>>,
}
impl Application {
fn new() -> Self {
Self { window: None }
}
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
}
impl ApplicationHandler for Application {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes =
WindowAttributes::default().with_title("Drag and drop files on me!");
self.window = Some(event_loop.create_window(window_attributes).unwrap());
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window).unwrap();
self.surface = Some(surface);
}
fn window_event(
@@ -49,12 +48,12 @@ impl ApplicationHandler for Application {
| WindowEvent::DragEntered { .. }
| WindowEvent::DragMoved { .. }
| WindowEvent::DragDropped { .. } => {
println!("{event:?}");
info!("{event:?}");
},
WindowEvent::RedrawRequested => {
let window = self.window.as_ref().unwrap();
window.pre_present_notify();
fill::fill_window(window.as_ref());
let surface = self.surface.as_mut().unwrap();
surface.window().pre_present_notify();
fill::fill(surface);
},
WindowEvent::CloseRequested => {
event_loop.exit();

View File

@@ -9,10 +9,11 @@ use std::cmp;
use std::error::Error;
use dpi::{LogicalPosition, PhysicalSize};
use softbuffer::{Context, Surface};
use tracing::{error, info};
use winit::application::ApplicationHandler;
use winit::event::{Ime, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::keyboard::{Key, ModifiersState, NamedKey};
#[cfg(web_platform)]
use winit::platform::web::WindowAttributesWeb;
@@ -30,7 +31,7 @@ const IME_CURSOR_SIZE: PhysicalSize<u32> = PhysicalSize::new(20, 20);
#[derive(Debug)]
struct App {
window: Option<Box<dyn Window>>,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
input_state: TextInputState,
modifiers: ModifiersState,
}
@@ -73,14 +74,12 @@ impl ApplicationHandler for App {
#[cfg(web_platform)]
let window_attributes = WindowAttributes::default()
.with_platform_attributes(Box::new(WindowAttributesWeb::default().with_append(true)));
self.window = match event_loop.create_window(window_attributes) {
Ok(window) => Some(window),
Err(err) => {
eprintln!("error creating window: {err}");
event_loop.exit();
return;
},
};
let window = event_loop.create_window(window_attributes).expect("failed creating window");
let context =
Context::new(event_loop.owned_display_handle()).expect("failed creating context");
let surface = Surface::new(&context, window).expect("failed creating surface");
self.surface = Some(surface);
// Allow IME out of the box.
let enable_request = ImeEnableRequest::new(
@@ -101,11 +100,13 @@ impl ApplicationHandler for App {
match event {
WindowEvent::CloseRequested => {
info!("Close was requested; stopping");
self.window = None;
self.surface = None;
event_loop.exit();
},
WindowEvent::SurfaceResized(_) => {
self.window.as_ref().expect("resize event without a window").request_redraw();
WindowEvent::SurfaceResized(surface_size) => {
let surface = self.surface.as_mut().expect("resize event without a surface");
fill::resize(surface, surface_size);
surface.window().request_redraw();
},
WindowEvent::RedrawRequested => {
// Redraw the application.
@@ -114,13 +115,13 @@ impl ApplicationHandler for App {
// this event rather than in AboutToWait, since rendering in here allows
// the program to gracefully handle redraws requested by the OS.
let window = self.window.as_ref().expect("redraw request without a window");
let surface = self.surface.as_mut().expect("redraw event without a surface");
// Notify that you're about to draw.
window.pre_present_notify();
surface.window().pre_present_notify();
// Draw.
fill::fill_window(window.as_ref());
fill::fill(surface);
// For contiguous redraw loop you can request a redraw from here.
// window.request_redraw();
@@ -214,14 +215,14 @@ impl App {
}
fn handle_ime_event(&mut self, event: Ime) {
let window = self.window.as_ref().expect("IME request without a window");
let surface = self.surface.as_ref().expect("IME request without a window");
match event {
Ime::Enabled => info!("IME enabled for Window={:?}", window.id()),
Ime::Enabled => info!("IME enabled for Window={:?}", surface.window().id()),
Ime::Preedit(text, caret_pos) => info!("Preedit: {text}, with caret at {caret_pos:?}"),
Ime::Commit(text) => {
self.input_state.append_text(&text);
let request_data = self.get_ime_update();
window.request_ime_update(ImeRequest::Update(request_data)).unwrap();
surface.window().request_ime_update(ImeRequest::Update(request_data)).unwrap();
self.print_input_state();
},
Ime::DeleteSurrounding { before_bytes, after_bytes } => {
@@ -246,7 +247,7 @@ impl App {
error!("Buggy IME tried to delete with indices not on char boundary.");
}
},
Ime::Disabled => info!("IME disabled for Window={:?}", window.id()),
Ime::Disabled => info!("IME disabled for Window={:?}", surface.window().id()),
}
}
@@ -312,7 +313,7 @@ impl App {
}
fn window(&self) -> &dyn Window {
self.window.as_ref().unwrap().as_ref()
self.surface.as_ref().unwrap().window().as_ref()
}
}
@@ -346,7 +347,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let event_loop = EventLoop::new()?;
println!(
info!(
r#"This showcases the use of an input method engine (IME) by emulating a text edit field.
Use CTRL+i to toggle IME support.
Use CTRL+p to cycle content purpose values.
@@ -355,7 +356,7 @@ Use CTRL+h to cycle content hint permutations.
);
let app = App {
window: None,
surface: None,
input_state: TextInputState {
ime_enabled: true,
contents: String::new(),

View File

@@ -7,24 +7,31 @@ fn main() -> std::process::ExitCode {
use std::thread::sleep;
use std::time::Duration;
use softbuffer::{Context, Surface};
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::pump_events::{EventLoopExtPumpEvents, PumpStatus};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Default, Debug)]
struct PumpDemo {
window: Option<Box<dyn Window>>,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
}
impl ApplicationHandler for PumpDemo {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes = WindowAttributes::default().with_title("A fantastic window!");
self.window = Some(event_loop.create_window(window_attributes).unwrap());
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
self.surface = Some(Surface::new(&context, window).unwrap());
}
fn window_event(
@@ -33,27 +40,27 @@ fn main() -> std::process::ExitCode {
_window_id: WindowId,
event: WindowEvent,
) {
println!("{event:?}");
info!("{event:?}");
let window = match self.window.as_ref() {
Some(window) => window,
let surface = match self.surface.as_mut() {
Some(surface) => surface,
None => return,
};
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
fill::fill_window(window.as_ref());
window.request_redraw();
fill::fill(surface);
surface.window().request_redraw();
},
_ => (),
}
}
}
let mut event_loop = EventLoop::new().unwrap();
tracing::init();
tracing_subscriber::fmt::init();
let mut event_loop = EventLoop::new().unwrap();
let mut app = PumpDemo::default();
@@ -69,12 +76,12 @@ fn main() -> std::process::ExitCode {
//
// Since `pump_events` doesn't block it will be important to
// throttle the loop in the app somehow.
println!("Update()");
info!("Update()");
sleep(Duration::from_millis(16));
}
}
#[cfg(any(ios_platform, web_platform, orbital_platform))]
fn main() {
println!("This platform doesn't support pump_events.");
panic!("This platform doesn't support pump_events.")
}

View File

@@ -5,36 +5,43 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::time::Duration;
use softbuffer::{Context, Surface};
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Default, Debug)]
#[derive(Debug)]
struct App {
context: Context<OwnedDisplayHandle>,
idx: usize,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
window_id: Option<WindowId>,
window: Option<Box<dyn Window>>,
}
impl ApplicationHandler for App {
fn about_to_wait(&mut self, _event_loop: &dyn ActiveEventLoop) {
if let Some(window) = self.window.as_ref() {
window.request_redraw();
if let Some(surface) = self.surface.as_ref() {
surface.window().request_redraw();
}
}
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes = WindowAttributes::default()
.with_title("Fantastic window number one!")
.with_title(format!("Fantastic window number {}!", self.idx))
.with_surface_size(winit::dpi::LogicalSize::new(128.0, 128.0));
let window = event_loop.create_window(window_attributes).unwrap();
self.window_id = Some(window.id());
self.window = Some(window);
let surface = Surface::new(&self.context, window).unwrap();
self.surface = Some(surface);
}
fn window_event(
@@ -44,52 +51,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event: WindowEvent,
) {
if event == WindowEvent::Destroyed && self.window_id == Some(window_id) {
println!(
"--------------------------------------------------------- Window {} Destroyed",
self.idx
);
info!("Window {} Destroyed", self.idx);
self.window_id = None;
event_loop.exit();
return;
}
let window = match self.window.as_mut() {
Some(window) => window,
None => return,
let Some(surface) = self.surface.as_mut() else {
return;
};
match event {
WindowEvent::CloseRequested => {
println!(
"--------------------------------------------------------- Window {} \
CloseRequested",
self.idx
);
fill::cleanup_window(window.as_ref());
self.window = None;
info!("Window {} CloseRequested", self.idx);
self.surface = None;
},
WindowEvent::RedrawRequested => {
fill::fill_window(window.as_ref());
fill::fill(surface);
},
_ => (),
}
}
}
tracing_subscriber::fmt::init();
tracing::init();
let mut event_loop = EventLoop::new().unwrap();
let mut app = App { idx: 1, ..Default::default() };
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let mut app = App { context, idx: 1, surface: None, window_id: None };
event_loop.run_app_on_demand(&mut app)?;
println!("--------------------------------------------------------- Finished first loop");
println!("--------------------------------------------------------- Waiting 5 seconds");
info!("Finished first loop");
info!("Waiting 5 seconds");
std::thread::sleep(Duration::from_secs(5));
app.idx += 1;
event_loop.run_app_on_demand(&mut app)?;
println!("--------------------------------------------------------- Finished second loop");
info!("Finished second loop");
Ok(())
}
@@ -101,5 +100,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
orbital_platform
)))]
fn main() {
println!("This example is not supported on this platform");
panic!("This example is not supported on this platform")
}

View File

@@ -1,156 +1,43 @@
//! Fill the window buffer with a solid color.
//!
//! Launching a window without drawing to it has unpredictable results varying from platform to
//! platform. In order to have well-defined examples, this module provides an easy way to
//! fill the window buffer with a solid color.
//!
//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could
//! also be used to fill the window buffer, but they are more complicated to use.
use std::num::NonZeroU32;
#[allow(unused_imports)]
pub use platform::cleanup_window;
#[allow(unused_imports)]
pub use platform::fill_window;
#[allow(unused_imports)]
pub use platform::fill_window_with_animated_color;
#[allow(unused_imports)]
pub use platform::fill_window_with_color;
use rwh_06::{HasDisplayHandle, HasWindowHandle};
use softbuffer::Surface;
use winit::window::Window;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod platform {
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem;
use std::mem::ManuallyDrop;
use std::num::NonZeroU32;
#[cfg(all(not(android_platform), not(web_platform)))]
use std::time::Instant;
/// Resize the surface.
pub fn resize(
surface: &mut Surface<impl HasDisplayHandle, impl HasWindowHandle>,
surface_size: dpi::PhysicalSize<u32>,
) {
// Handle zero-sized buffers.
//
// FIXME(madsmtm): This should be done by softbuffer internally in the future:
// https://github.com/rust-windowing/softbuffer/issues/238
let (Some(width), Some(height)) =
(NonZeroU32::new(surface_size.width), NonZeroU32::new(surface_size.height))
else {
return;
};
use softbuffer::{Context, Surface};
#[cfg(all(web_platform, not(android_platform)))]
use web_time::Instant;
use winit::window::{Window, WindowId};
thread_local! {
// NOTE: You should never do things like that, create context and drop it before
// you drop the event loop. We do this for brevity to not blow up examples. We use
// ManuallyDrop to prevent destructors from running.
//
// A static, thread-local map of graphics contexts to open windows.
static GC: ManuallyDrop<RefCell<Option<GraphicsContext>>> = const { ManuallyDrop::new(RefCell::new(None)) };
}
/// The graphics context used to draw to a window.
struct GraphicsContext {
/// The global softbuffer context.
context: RefCell<Context<&'static dyn Window>>,
/// The hash map of window IDs to surfaces.
surfaces: HashMap<WindowId, Surface<&'static dyn Window, &'static dyn Window>>,
}
impl GraphicsContext {
fn new(w: &dyn Window) -> Self {
Self {
context: RefCell::new(
Context::new(unsafe {
mem::transmute::<&'_ dyn Window, &'static dyn Window>(w)
})
.expect("Failed to create a softbuffer context"),
),
surfaces: HashMap::new(),
}
}
fn create_surface(
&mut self,
window: &dyn Window,
) -> &mut Surface<&'static dyn Window, &'static dyn Window> {
self.surfaces.entry(window.id()).or_insert_with(|| {
Surface::new(&self.context.borrow(), unsafe {
mem::transmute::<&'_ dyn Window, &'static dyn Window>(window)
})
.expect("Failed to create a softbuffer surface")
})
}
fn destroy_surface(&mut self, window: &dyn Window) {
self.surfaces.remove(&window.id());
}
}
pub fn fill_window_with_color(window: &dyn Window, color: u32) {
GC.with(|gc| {
let size = window.surface_size();
let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
else {
return;
};
// Either get the last context used or create a new one.
let mut gc = gc.borrow_mut();
let surface =
gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window);
// Fill a buffer with a solid color
surface.resize(width, height).expect("Failed to resize the softbuffer surface");
let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer");
buffer.fill(color);
buffer.present().expect("Failed to present the softbuffer buffer");
})
}
#[allow(dead_code)]
pub fn fill_window(window: &dyn Window) {
fill_window_with_color(window, 0xff181818);
}
#[allow(dead_code)]
pub fn fill_window_with_animated_color(window: &dyn Window, start: Instant) {
let time = start.elapsed().as_secs_f32() * 1.5;
let blue = (time.sin() * 255.0) as u32;
let green = ((time.cos() * 255.0) as u32) << 8;
let red = ((1.0 - time.sin() * 255.0) as u32) << 16;
let color = red | green | blue;
fill_window_with_color(window, color);
}
#[allow(dead_code)]
pub fn cleanup_window(window: &dyn Window) {
GC.with(|gc| {
let mut gc = gc.borrow_mut();
if let Some(context) = gc.as_mut() {
context.destroy_surface(window);
}
});
}
surface.resize(width, height).expect("Failed to resize the softbuffer surface");
}
#[cfg(any(target_os = "android", target_os = "ios"))]
mod platform {
#[allow(dead_code)]
pub fn fill_window(_window: &dyn winit::window::Window) {
// No-op on mobile platforms.
}
/// Fill the window buffer with a solid color.
pub fn fill_with_color(
surface: &mut Surface<impl HasDisplayHandle, impl HasWindowHandle + AsRef<dyn Window>>,
color: u32,
) {
let surface_size = surface.window().as_ref().surface_size();
resize(surface, surface_size);
#[allow(dead_code)]
pub fn fill_window_with_color(_window: &dyn winit::window::Window, _color: u32) {
// No-op on mobile platforms.
}
#[allow(dead_code)]
pub fn fill_window_with_animated_color(
_window: &dyn winit::window::Window,
_start: std::time::Instant,
) {
// No-op on mobile platforms.
}
#[allow(dead_code)]
pub fn cleanup_window(_window: &dyn winit::window::Window) {
// No-op on mobile platforms.
}
let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer");
buffer.fill(color);
buffer.present().expect("Failed to present the softbuffer buffer");
}
#[allow(dead_code)]
pub fn fill(
surface: &mut Surface<impl HasDisplayHandle, impl HasWindowHandle + AsRef<dyn Window>>,
) {
fill_with_color(surface, 0xff181818);
}

View File

@@ -23,3 +23,6 @@ pub fn init() {
)
.init();
}
#[allow(unused_imports)]
pub use ::tracing::*;

View File

@@ -2,9 +2,11 @@
use std::error::Error;
use softbuffer::{Context, Surface};
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
#[cfg(web_platform)]
use winit::platform::web::WindowAttributesWeb;
use winit::window::{Window, WindowAttributes, WindowId};
@@ -16,7 +18,7 @@ mod tracing;
#[derive(Default, Debug)]
struct App {
window: Option<Box<dyn Window>>,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
}
impl ApplicationHandler for App {
@@ -26,25 +28,25 @@ impl ApplicationHandler for App {
#[cfg(web_platform)]
let window_attributes = WindowAttributes::default()
.with_platform_attributes(Box::new(WindowAttributesWeb::default().with_append(true)));
self.window = match event_loop.create_window(window_attributes) {
Ok(window) => Some(window),
Err(err) => {
eprintln!("error creating window: {err}");
event_loop.exit();
return;
},
}
let window = event_loop.create_window(window_attributes).expect("failed creating window");
let context =
Context::new(event_loop.owned_display_handle()).expect("failed creating context");
let surface = Surface::new(&context, window).expect("failed creating surface");
self.surface = Some(surface);
}
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) {
println!("{event:?}");
info!("{event:?}");
match event {
WindowEvent::CloseRequested => {
println!("Close was requested; stopping");
info!("Close was requested; stopping");
event_loop.exit();
},
WindowEvent::SurfaceResized(_) => {
self.window.as_ref().expect("resize event without a window").request_redraw();
WindowEvent::SurfaceResized(surface_size) => {
let surface = self.surface.as_mut().expect("resize event without a surface");
fill::resize(surface, surface_size);
surface.window().request_redraw();
},
WindowEvent::RedrawRequested => {
// Redraw the application.
@@ -53,13 +55,15 @@ impl ApplicationHandler for App {
// this event rather than in AboutToWait, since rendering in here allows
// the program to gracefully handle redraws requested by the OS.
let window = self.window.as_ref().expect("redraw request without a window");
let surface = self.surface.as_mut().expect("redraw event without a surface");
// Notify that you're about to draw.
window.pre_present_notify();
surface.window().pre_present_notify();
// Draw.
fill::fill_window(window.as_ref());
let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer");
buffer.fill(0xff181818);
buffer.present().expect("Failed to present the softbuffer buffer");
// For contiguous redraw loop you can request a redraw from here.
// window.request_redraw();

View File

@@ -3,19 +3,22 @@ use std::error::Error;
#[cfg(x11_platform)]
fn main() -> Result<(), Box<dyn Error>> {
use softbuffer::{Context, Surface};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::platform::x11::WindowAttributesX11;
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Debug)]
pub struct XEmbedDemo {
parent_window_id: u32,
window: Option<Box<dyn Window>>,
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
}
impl ApplicationHandler for XEmbedDemo {
@@ -27,7 +30,10 @@ fn main() -> Result<(), Box<dyn Error>> {
WindowAttributesX11::default().with_embed_parent_window(self.parent_window_id);
window_attributes = window_attributes.with_platform_attributes(Box::new(x11_attrs));
self.window = Some(event_loop.create_window(window_attributes).unwrap());
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window).unwrap();
self.surface = Some(surface);
}
fn window_event(
@@ -36,19 +42,19 @@ fn main() -> Result<(), Box<dyn Error>> {
_window_id: WindowId,
event: WindowEvent,
) {
let window = self.window.as_ref().unwrap();
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
window.pre_present_notify();
fill::fill_window(window.as_ref());
let surface = self.surface.as_mut().unwrap();
surface.window().pre_present_notify();
fill::fill(surface);
},
_ => (),
}
}
fn about_to_wait(&mut self, _event_loop: &dyn ActiveEventLoop) {
self.window.as_ref().unwrap().request_redraw();
self.surface.as_ref().unwrap().window().request_redraw();
}
}
@@ -58,14 +64,13 @@ fn main() -> Result<(), Box<dyn Error>> {
.ok_or("Expected a 32-bit X11 window ID as the first argument.")?
.parse::<u32>()?;
tracing_subscriber::fmt::init();
tracing::init();
let event_loop = EventLoop::new()?;
Ok(event_loop.run_app(XEmbedDemo { parent_window_id, window: None })?)
Ok(event_loop.run_app(XEmbedDemo { parent_window_id, surface: None })?)
}
#[cfg(not(x11_platform))]
fn main() -> Result<(), Box<dyn Error>> {
println!("This example is only supported on X11 platforms.");
Ok(())
panic!("This example is only supported on X11 platforms.")
}

View File

@@ -39,3 +39,31 @@ The migration guide could reference other migration examples in the current
changelog entry.
## Unreleased
### Added
- Add `keyboard` support for OpenHarmony.
- On iOS, add Apple Pencil support with force, altitude, and azimuth data.
- On Redox, add support for missing keyboard scancodes.
- Implement `Send` and `Sync` for `OwnedDisplayHandle`.
- Use new macOS 15 cursors for resize icons.
- On Android, added scancode conversions for more obscure key codes.
- On Wayland, added `HoldGesture` event for multi-finger hold gestures
- On Wayland, added ext-background-effect-v1 support.
### Changed
- Updated `windows-sys` to `v0.61`.
- On older macOS versions (tested up to 12.7.6), applications now receive mouse movement events for unfocused windows, matching the behavior on other platforms.
### Fixed
- On Windows, fix a freeze that occurs when the keyboard layout is switched by
tools such as Punto Switcher. The `WM_INPUTLANGCHANGE` message is now handled
to refresh the cached keyboard layout, while still deferring to
`DefWindowProc` for normal propagation.
- On Redox, handle `EINTR` when reading from `event_socket` instead of panicking.
- On Wayland, switch from using the `ahash` hashing algorithm to `foldhash`.
- On macOS, fix borderless game presentation options not sticking after switching spaces.
- On macOS, fix IME being locked on (regardless of requests to disable) after being enabled once.
- On macOS, fix a panic and incorrect cursor position in Ime::Preedit when the preedit string contains special characters (ie. emojis) caused by incorrect UTF-16 to UTF-8 offset conversion.

View File

@@ -1,3 +1,18 @@
## 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

View File

@@ -249,4 +249,3 @@
- On Web, device events are emitted regardless of cursor type.
- On Wayland, `axis_value120` scroll events now generate `MouseScrollDelta::LineDelta`
- On X11, mouse scroll button events no longer cause duplicated `WindowEvent::MouseWheel` events.
- On macOS, fixed crash when dragging non-file content onto window.

View File

@@ -29,7 +29,7 @@ use crate::platform_impl;
///
/// To wake up an `EventLoop` from a another thread, see the [`EventLoopProxy`] docs.
///
/// Note that this cannot be shared across threads (due to platform-dependant logic
/// Note that this cannot be shared across threads (due to platform-dependent logic
/// forbidding it), as such it is neither [`Send`] nor [`Sync`]. If you need cross-thread access,
/// the [`Window`] created from this _can_ be sent to an other thread, and the
/// [`EventLoopProxy`] allows you to wake up an `EventLoop` from another thread.
@@ -88,7 +88,7 @@ impl EventLoopBuilder {
)]
#[inline]
pub fn build(&mut self) -> Result<EventLoop, EventLoopError> {
let _span = tracing::debug_span!("winit::EventLoopBuilder::build").entered();
let _entered = tracing::debug_span!("winit::EventLoopBuilder::build").entered();
// Certain platforms accept a mutable reference in their API.
#[allow(clippy::unnecessary_mut_passed)]
@@ -262,7 +262,7 @@ impl EventLoop {
///
/// [`DeviceEvent`]: crate::event::DeviceEvent
pub fn listen_device_events(&self, allowed: DeviceEvents) {
let _span = tracing::debug_span!(
let _entered = tracing::debug_span!(
"winit::EventLoop::listen_device_events",
allowed = ?allowed
)

View File

@@ -28,3 +28,8 @@ fn custom_cursor_send() {
needs_send::<winit::cursor::CustomCursorSource>();
needs_send::<winit::cursor::CustomCursor>();
}
#[test]
fn owned_display_handle_send() {
needs_send::<winit::event_loop::OwnedDisplayHandle>();
}

View File

@@ -21,3 +21,8 @@ fn custom_cursor_sync() {
needs_sync::<winit::cursor::CustomCursorSource>();
needs_sync::<winit::cursor::CustomCursor>();
}
#[test]
fn owned_display_handle_sync() {
needs_sync::<winit::event_loop::OwnedDisplayHandle>();
}