Compare commits

..

48 Commits

Author SHA1 Message Date
Mads Marquart
9efc50a262 AppKit: Launch app inside EventLoop::new
This moves the application launch from the beginning of
`EventLoop::run_app` to `EventLoop::new`, which:
- Feels more consistent, we now emit `new_events(StartCause::Init)` at
  the beginning of `run_app_on_demand` instead of conditionally
  depending on if the application has launched or not.
- Would allow us to bring back `event_loop.create_window` in some form
  on macOS, since the application would now be launched between
  `EventLoop::new` and `EventLoop::run_app`.

This differs semantically from iOS where we must still launch the
application in `EventLoop::run_app`, but iOS is the special snowflake
here, the other desktop platforms' are semantically launched after
`EventLoop::new`.

Doing it this way also matches what GLFW's init function does.
2026-03-19 01:47:39 +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
77 changed files with 1678 additions and 1141 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: |
@@ -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/

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:

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

@@ -32,6 +32,7 @@ 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
]
confidence-threshold = 1.0
private = { ignore = true }

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

@@ -51,6 +51,7 @@ objc2-app-kit = { workspace = true, features = [
"NSScreen",
"NSTextInputClient",
"NSTextInputContext",
"NSTrackingArea",
"NSToolbar",
"NSView",
"NSWindow",

View File

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

View File

@@ -6,9 +6,8 @@ use std::time::Instant;
use dispatch2::MainThreadBound;
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication};
use objc2_foundation::NSNotification;
use winit_common::core_foundation::EventLoopProxy;
use objc2_app_kit::NSApplication;
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
use winit_common::event_handler::EventHandler;
use winit_core::application::ApplicationHandler;
use winit_core::event::{StartCause, WindowEvent};
@@ -16,24 +15,17 @@ use winit_core::event_loop::ControlFlow;
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 {
mtm: MainThreadMarker,
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>,
stop_before_wait: Cell<bool>,
stop_after_wait: Cell<bool>,
stop_on_redraw: Cell<bool>,
/// Whether `applicationDidFinishLaunching:` has been run or not.
is_launched: Cell<bool>,
/// Whether an `EventLoop` is currently running.
is_running: Cell<bool>,
/// Whether the user has requested the event loop to exit.
@@ -53,29 +45,19 @@ static GLOBAL: MainThreadBound<OnceCell<Rc<AppState>>> =
MainThreadBound::new(OnceCell::new(), unsafe { MainThreadMarker::new_unchecked() });
impl AppState {
pub(super) fn setup_global(
mtm: MainThreadMarker,
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
) -> Option<Rc<Self>> {
pub(super) fn setup_global(mtm: MainThreadMarker) -> Option<Rc<Self>> {
let event_loop_proxy = Arc::new(EventLoopProxy::new(mtm, move || {
Self::get(mtm).with_handler(|app, event_loop| app.proxy_wake_up(event_loop));
}));
let this = Rc::new(Self {
mtm,
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),
stop_before_wait: Cell::new(false),
stop_after_wait: Cell::new(false),
stop_on_redraw: Cell::new(false),
is_launched: Cell::new(false),
is_running: Cell::new(false),
exit: Cell::new(false),
control_flow: Cell::new(ControlFlow::default()),
@@ -96,71 +78,6 @@ impl AppState {
.clone()
}
// 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);
// We need to delay setting the activation policy and activating the app
// until `applicationDidFinishLaunching` has been called. Otherwise the
// menu bar is initially unresponsive on macOS 10.15.
if let Some(activation_policy) = self.activation_policy {
app.setActivationPolicy(activation_policy);
} else {
// If no activation policy is explicitly provided, and the application
// is bundled, do not set the activation policy at all, to allow the
// package manifest to define the behavior via LSUIElement.
//
// See:
// - https://github.com/rust-windowing/winit/issues/261
// - https://github.com/rust-windowing/winit/issues/3958
let is_bundled =
NSRunningApplication::currentApplication().bundleIdentifier().is_some();
if !is_bundled {
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
}
}
#[allow(deprecated)]
app.activateIgnoringOtherApps(self.activate_ignoring_other_apps);
if self.default_menu {
// The menubar initialization should be before the `NewEvents` event, to allow
// overriding of the default menu even if it's created
menu::initialize(&app);
}
self.waker.borrow_mut().start();
self.set_is_running(true);
self.dispatch_init_events();
// If the application is being launched via `EventLoop::pump_app_events()` then we'll
// want to stop the app once it is launched (and return to the external loop)
//
// In this case we still want to consider Winit's `EventLoop` to be "running",
// so we call `start_running()` above.
if self.stop_on_launch.get() {
// NOTE: the original idea had been to only stop the underlying `RunLoop`
// for the app but that didn't work as expected (`-[NSApplication run]`
// effectively ignored the attempt to stop the RunLoop and re-started it).
//
// So we return from `pump_events` by stopping the application.
let app = NSApplication::sharedApplication(self.mtm);
stop_app_immediately(&app);
}
}
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();
self.internal_exit();
}
/// Place the event handler in the application state for the duration
/// of the given closure.
pub fn set_event_handler<R>(
@@ -171,15 +88,12 @@ impl AppState {
self.event_handler.set(Box::new(handler), closure)
}
pub fn event_loop_proxy(&self) -> &Arc<EventLoopProxy> {
&self.event_loop_proxy
pub fn terminate_event_handler(&self) {
self.event_handler.terminate();
}
/// If `pump_events` is called to progress the event loop then we
/// bootstrap the event loop via `-[NSApplication run]` but will use
/// `CFRunLoopRunInMode` for subsequent calls to `pump_events`.
pub fn set_stop_on_launch(&self) {
self.stop_on_launch.set(true);
pub fn event_loop_proxy(&self) -> &Arc<EventLoopProxy> {
&self.event_loop_proxy
}
pub fn set_stop_before_wait(&self, value: bool) {
@@ -210,10 +124,6 @@ impl AppState {
self.set_wait_timeout(None);
}
pub fn is_launched(&self) -> bool {
self.is_launched.get()
}
pub fn set_is_running(&self, value: bool) {
self.is_running.set(value)
}
@@ -265,7 +175,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 +220,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 +249,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

@@ -7,10 +7,13 @@ use objc2::runtime::ProtocolObject;
use objc2::{MainThreadMarker, available};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
NSApplicationWillTerminateNotification, NSWindow,
NSApplicationWillTerminateNotification, NSRunningApplication, 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_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, RequestError};
@@ -28,9 +31,8 @@ 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;
use crate::{ActivationPolicy, menu};
#[derive(Debug)]
pub struct ActiveEventLoop {
@@ -148,8 +150,11 @@ pub struct EventLoop {
// the system instead cleans it up next time it would have posted a notification to it.
//
// Though we do still need to keep the observers around to prevent them from being deallocated.
_did_finish_launching_observer: Retained<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)]
@@ -170,20 +175,8 @@ impl EventLoop {
let mtm = MainThreadMarker::new()
.expect("on macOS, `EventLoop` must be created on the main thread!");
let activation_policy = match attributes.activation_policy {
None => None,
Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular),
Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory),
Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited),
};
let app_state = AppState::setup_global(
mtm,
activation_policy,
attributes.default_menu,
attributes.activate_ignoring_other_apps,
)
.ok_or_else(|| EventLoopError::RecreationAttempt)?;
let app_state =
AppState::setup_global(mtm).ok_or_else(|| EventLoopError::RecreationAttempt)?;
// Initialize the application (if it has not already been).
let app = NSApplication::sharedApplication(mtm);
@@ -193,38 +186,164 @@ impl EventLoop {
let center = NSNotificationCenter::defaultCenter();
let weak_app_state = Rc::downgrade(&app_state);
let _did_finish_launching_observer = create_observer(
&center,
// `applicationDidFinishLaunching:`
unsafe { NSApplicationDidFinishLaunchingNotification },
move |notification| {
if let Some(app_state) = weak_app_state.upgrade() {
app_state.did_finish_launching(notification);
}
},
);
// Handle `terminate:`. This may happen if:
// - The user uses the context menu in the Dock icon.
// - Or the `Quit` menu item we install with the default menu (including via. the keyboard
// shortcut).
// - Maybe other cases?
//
// In these cases, AppKit is going to call `std::process::exit`, so we won't get the chance
// to return to the user from `EventLoop::run_app`. So we have to clean up and drop their
// windows and application here too.
let weak_app_state = Rc::downgrade(&app_state);
let _will_terminate_observer = create_observer(
&center,
// `applicationWillTerminate:`
unsafe { NSApplicationWillTerminateNotification },
move |notification| {
let _entered = debug_span!("applicationWillTerminate").entered();
let app = notification.object().unwrap().downcast::<NSApplication>().unwrap();
notify_windows_of_exit(&app);
if let Some(app_state) = weak_app_state.upgrade() {
app_state.will_terminate(notification);
app_state.terminate_event_handler();
app_state.internal_exit();
}
},
);
setup_control_flow_observers(mtm);
// Set up run loop observers for calling `new_events` and `about_to_wait`.
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);
// Run `finishLaunching` just in case it works.
app.finishLaunching();
// Now _ideally_, calling `finishLaunching` should be enough for the application to, you
// know, launch (create the a dock icon etc.), but unfortunately, this doesn't happen for
// various godforsaken reasons... The only way to make the application properly launch is by
// calling `NSApplication::run`.
//
// So we check if the application hasn't finished launching, and if it hasn't, we run it
// once to finish it.
//
// This is _very_ important, there's a _lot_ of weird and subtle state that requires that
// the application is launched properly, including window creation, the menu bar,
// activation, see:
// - https://github.com/rust-windowing/winit/pull/1903
// - https://github.com/rust-windowing/winit/pull/1922
// - https://github.com/rust-windowing/winit/issues/2238
// - https://github.com/rust-windowing/winit/issues/2051
// - https://github.com/rust-windowing/winit/issues/2087
// - https://developer.apple.com/forums/thread/772169
//
// This approach is similar to what other cross-platform windowing libraries do (except that
// we do it without a delegate to allow users to override that):
// - GLFW delegate: https://github.com/glfw/glfw/blob/3.4/src/cocoa_init.m#L439-L443
// - GLFW launch: https://github.com/glfw/glfw/blob/3.4/src/cocoa_init.m#L634-L635
// - FLTK delegate: https://github.com/fltk/fltk/blob/release-1.4.4/src/Fl_cocoa.mm#L1604-L1607
// - FLTK launch: https://github.com/fltk/fltk/blob/release-1.4.4/src/Fl_cocoa.mm#L1903-L1919
// - Stackoverflow issue: https://stackoverflow.com/questions/48020222/how-to-make-nsapp-run-not-block/67626393#67626393
if !NSRunningApplication::currentApplication().isFinishedLaunching() {
// Register an observer to stop the application immediately after launching.
//
// NOTE: This notification will, globally, only be emitted once, no matter how many
// `EventLoop`s the user creates. We detect it with `isFinishedLaunching` above.
let did_finish_launching_observer = create_observer(
&center,
unsafe { NSApplicationDidFinishLaunchingNotification },
move |notification| {
let _entered = debug_span!("applicationDidFinishLaunching").entered();
let app = notification.object().unwrap().downcast::<NSApplication>().unwrap();
// Stop the application, to make the `app.run()` call below return.
stop_app_immediately(&app);
},
);
// We call `stop_app_immediately` above, so this should return after launching.
app.run();
// The observer should've been called at this point.
drop(did_finish_launching_observer);
// We _could_ keep trying if we failed to initialize, but that would potentially lead
// to an infinite loop, it's probably better to just continue.
debug_assert!(NSRunningApplication::currentApplication().isFinishedLaunching());
}
// We need to delay setting the activation policy and activating the app until
// `applicationDidFinishLaunching:` has been called, otherwise the menu bar is initially
// unresponsive on macOS 10.15.
if let Some(activation_policy) = attributes.activation_policy {
app.setActivationPolicy(match activation_policy {
ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular,
ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory,
ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited,
});
} else {
// If no activation policy is explicitly provided, and the application
// is bundled, do not set the activation policy at all, to allow the
// package manifest to define the behavior via LSUIElement.
//
// See:
// - https://github.com/rust-windowing/winit/issues/261
// - https://github.com/rust-windowing/winit/issues/3958
let is_bundled =
NSRunningApplication::currentApplication().bundleIdentifier().is_some();
if !is_bundled {
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
}
}
// TODO: Use `app.activate()` instead on newer OS versions?
#[expect(deprecated)]
app.activateIgnoringOtherApps(attributes.activate_ignoring_other_apps);
if attributes.default_menu {
// The default menubar initialization should be before everything else, to allow
// overriding it even if it's created.
menu::initialize(&app);
}
Ok(EventLoop {
app,
app_state: app_state.clone(),
window_target: ActiveEventLoop { app_state, mtm },
_did_finish_launching_observer,
_will_terminate_observer,
_tracing_observers,
_before_waiting_observer,
_after_waiting_observer,
})
}
@@ -240,6 +359,7 @@ impl EventLoop {
&mut self,
app: A,
) -> Result<(), EventLoopError> {
let _entered = debug_span!("run_app_on_demand").entered();
self.app_state.clear_exit();
self.app_state.set_event_handler(app, || {
autoreleasepool(|_| {
@@ -249,11 +369,9 @@ impl EventLoop {
self.app_state.set_stop_after_wait(false);
self.app_state.set_stop_on_redraw(false);
if self.app_state.is_launched() {
debug_assert!(!self.app_state.is_running());
self.app_state.set_is_running(true);
self.app_state.dispatch_init_events();
}
debug_assert!(!self.app_state.is_running());
self.app_state.set_is_running(true);
self.app_state.dispatch_init_events();
// NOTE: Make sure to not run the application re-entrantly, as that'd be confusing.
self.app.run();
@@ -270,19 +388,10 @@ impl EventLoop {
timeout: Option<Duration>,
app: A,
) -> PumpStatus {
let _entered = debug_span!("pump_app_events").entered();
self.app_state.set_event_handler(app, || {
autoreleasepool(|_| {
// As a special case, if the application hasn't been launched yet then we at least
// run the loop until it has fully launched.
if !self.app_state.is_launched() {
debug_assert!(!self.app_state.is_running());
self.app_state.set_stop_on_launch();
self.app.run();
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application
// has launched
} else if !self.app_state.is_running() {
if !self.app_state.is_running() {
// Even though the application may have been launched, it's possible we aren't
// running if the `EventLoop` was run before and has since
// exited. This indicates that we just starting to re-run

View File

@@ -50,15 +50,13 @@
//! }
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let event_loop = EventLoop::new()?;
//!
//! // Register the delegate before Winit gets a chance to touch things.
//! let mtm = MainThreadMarker::new().unwrap();
//! let delegate = AppDelegate::new(mtm);
//! // Important: Call `sharedApplication` after `EventLoop::new`,
//! // doing it before is not yet supported.
//! let app = NSApplication::sharedApplication(mtm);
//! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
//!
//! let event_loop = EventLoop::new()?;
//! // event_loop.run_app(&mut my_app);
//! Ok(())
//! }

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 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, 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,41 +151,17 @@ 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 rect = self.frame();
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));
@@ -195,7 +169,7 @@ define_class!(
#[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 +178,7 @@ define_class!(
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
trace_scope!("acceptsFirstResponder");
let _entered = trace_span!("acceptsFirstResponder").entered();
true
}
@@ -218,13 +192,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 +213,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 +231,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 +244,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>()
@@ -304,9 +278,15 @@ define_class!(
// sending a `None` cursor range.
None
} else {
// Clamp to string length to avoid NSRangeException from out-of-bounds
// indices sent by macOS IME (e.g. native Pinyin, see
// https://github.com/alacritty/alacritty/issues/8791).
let len = string.length();
let location = selected_range.location.min(len);
let end = selected_range.end().min(len);
// Convert the selected range from UTF-16 indices to UTF-8 indices.
let sub_string_a = string.substringToIndex(selected_range.location);
let sub_string_b = string.substringToIndex(selected_range.end());
let sub_string_a = string.substringToIndex(location);
let sub_string_b = string.substringToIndex(end);
let lowerbound_utf8 = sub_string_a.len();
let upperbound_utf8 = sub_string_b.len();
Some((lowerbound_utf8, upperbound_utf8))
@@ -318,7 +298,7 @@ define_class!(
#[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 +315,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 +325,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 +342,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 +358,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 +383,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 +422,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 +481,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 +498,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 +516,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 +530,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 +559,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 +639,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 +653,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 +692,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 +714,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 +723,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 +745,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 +759,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 +785,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 +794,52 @@ 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();
// `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
}
@@ -866,24 +891,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> {

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")) {
@@ -1534,7 +1548,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 +1559,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 +1686,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 +1696,7 @@ impl WindowDelegate {
request_data
},
ImeRequest::Disable => {
self.view().set_ime_allowed(None);
self.view().disable_ime();
return Ok(());
},
};

View File

@@ -18,7 +18,7 @@ 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"]
[dependencies]
smol_str = { workspace = true, optional = true }
@@ -31,9 +31,12 @@ x11-dl = { workspace = true, optional = true }
xkbcommon-dl = { workspace = true, optional = true }
# CoreFoundation
block2 = { workspace = true, optional = true }
objc2 = { workspace = true, optional = true }
objc2-core-foundation = { workspace = true, optional = true, features = [
"std",
"block2",
"objc2",
"CFRunLoop",
"CFString",
] }

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

@@ -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>,
@@ -516,6 +522,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 +810,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>,

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>);
@@ -1691,6 +1690,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

@@ -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,8 @@ 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_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
@@ -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

@@ -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"] }

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;
@@ -43,7 +43,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>,

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;
@@ -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>,
@@ -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());
}

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

@@ -171,6 +171,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 +376,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) {

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;
@@ -140,6 +140,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
@@ -223,6 +224,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 +363,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);
@@ -749,6 +787,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();

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

@@ -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,9 +1503,9 @@ unsafe fn public_window_callback_inner(
},
WM_IME_SETCONTEXT => {
// Hide composing text drawn by IME.
let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize);
result = ProcResult::DefWindowProc(wparam);
// IME UI visibility flags are in lparam.
let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize);
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
},
// this is necessary for us to maintain minimize/restore state

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

@@ -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

@@ -76,12 +76,10 @@ winit-core.workspace = true
[dev-dependencies]
image = { workspace = true, features = ["png"] }
softbuffer.workspace = true
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
[target.'cfg(not(target_os = "android"))'.dev-dependencies]
softbuffer.workspace = true
[target.'cfg(target_os = "android")'.dependencies]
winit-android.workspace = true

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,21 +7,18 @@ 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};
@@ -97,14 +94,12 @@ struct Application {
/// Drawing context.
///
/// With OpenGL it could be EGLDisplay.
#[cfg(not(android_platform))]
context: Option<Context<DisplayHandle<'static>>>,
}
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>>(
@@ -130,15 +125,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 +238,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(),
@@ -614,7 +607,6 @@ struct WindowState {
/// Render surface.
///
/// NOTE: This surface must be dropped before the `Window`.
#[cfg(not(android_platform))]
surface: Surface<DisplayHandle<'static>, Arc<dyn Window>>,
/// The actual winit Window.
window: Arc<dyn Window>,
@@ -623,7 +615,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,
@@ -659,7 +650,6 @@ impl WindowState {
// 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 theme = window.theme().unwrap_or(Theme::Dark);
@@ -674,14 +664,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 +841,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,7 +931,6 @@ 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());
@@ -992,12 +976,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 +1005,8 @@ enum Action {
ToggleFullscreen,
#[cfg(macos_platform)]
ToggleSimpleFullscreen,
#[cfg(macos_platform)]
ToggleBorderlessGame,
ToggleMaximize,
Minimize,
NextCursor,
@@ -1064,6 +1044,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 +1297,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,6 +3,7 @@
fn main() -> Result<(), impl std::error::Error> {
use std::collections::HashMap;
use tracing::info;
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize, Position};
use winit::event::{ElementState, KeyEvent, WindowEvent};
@@ -38,7 +39,7 @@ 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));
@@ -56,12 +57,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, .. },
@@ -75,7 +76,7 @@ fn main() -> Result<(), impl std::error::Error> {
let child_window =
spawn_child_window(parent_window.window.as_ref(), event_loop, child_index);
let child_id = child_window.id();
println!("Child window created with id: {child_id:?}");
info!("Child window created with id: {child_id:?}");
self.windows.insert(child_id, WindowData::new(child_window, child_color));
},
WindowEvent::RedrawRequested => {

View File

@@ -4,7 +4,7 @@ use std::thread;
#[cfg(not(web_platform))]
use std::time;
use ::tracing::{info, warn};
use tracing::{info, warn};
#[cfg(web_platform)]
use web_time as time;
use winit::application::ApplicationHandler;

View File

@@ -1,5 +1,6 @@
use std::error::Error;
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
@@ -49,7 +50,7 @@ impl ApplicationHandler for Application {
| WindowEvent::DragEntered { .. }
| WindowEvent::DragMoved { .. }
| WindowEvent::DragDropped { .. } => {
println!("{event:?}");
info!("{event:?}");
},
WindowEvent::RedrawRequested => {
let window = self.window.as_ref().unwrap();

View File

@@ -76,7 +76,7 @@ impl ApplicationHandler for App {
self.window = match event_loop.create_window(window_attributes) {
Ok(window) => Some(window),
Err(err) => {
eprintln!("error creating window: {err}");
error!("error creating window: {err}");
event_loop.exit();
return;
},
@@ -346,7 +346,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.

View File

@@ -7,6 +7,7 @@ fn main() -> std::process::ExitCode {
use std::thread::sleep;
use std::time::Duration;
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::pump_events::{EventLoopExtPumpEvents, PumpStatus};
@@ -15,6 +16,8 @@ fn main() -> std::process::ExitCode {
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Default, Debug)]
struct PumpDemo {
@@ -33,7 +36,7 @@ fn main() -> std::process::ExitCode {
_window_id: WindowId,
event: WindowEvent,
) {
println!("{event:?}");
info!("{event:?}");
let window = match self.window.as_ref() {
Some(window) => window,
@@ -51,9 +54,9 @@ fn main() -> std::process::ExitCode {
}
}
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 +72,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,6 +5,7 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::time::Duration;
use tracing::info;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
@@ -13,6 +14,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Default, Debug)]
struct App {
@@ -44,10 +47,7 @@ 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;
@@ -60,11 +60,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
match event {
WindowEvent::CloseRequested => {
println!(
"--------------------------------------------------------- Window {} \
CloseRequested",
self.idx
);
info!("Window {} CloseRequested", self.idx);
fill::cleanup_window(window.as_ref());
self.window = None;
},
@@ -76,20 +72,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
tracing_subscriber::fmt::init();
tracing::init();
let mut event_loop = EventLoop::new().unwrap();
let mut app = App { idx: 1, ..Default::default() };
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 +97,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

@@ -7,150 +7,109 @@
//! 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.
#[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 std::cell::RefCell;
use std::collections::HashMap;
use std::mem;
use std::mem::ManuallyDrop;
use std::num::NonZeroU32;
#[cfg(not(web_platform))]
use std::time::Instant;
#[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;
use softbuffer::{Context, Surface};
#[cfg(web_platform)]
use web_time::Instant;
use winit::window::{Window, WindowId};
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)) };
}
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 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>>,
}
/// 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)
})
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());
),
surfaces: HashMap::new(),
}
}
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");
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")
})
}
#[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);
}
});
fn destroy_surface(&mut self, window: &dyn Window) {
self.surfaces.remove(&window.id());
}
}
#[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.
}
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;
};
#[allow(dead_code)]
pub fn fill_window_with_color(_window: &dyn winit::window::Window, _color: u32) {
// No-op on mobile platforms.
}
// 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);
#[allow(dead_code)]
pub fn fill_window_with_animated_color(
_window: &dyn winit::window::Window,
_start: std::time::Instant,
) {
// No-op on mobile platforms.
}
// Fill a buffer with a solid color
#[allow(dead_code)]
pub fn cleanup_window(_window: &dyn winit::window::Window) {
// No-op on mobile platforms.
}
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);
}
});
}

View File

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

View File

@@ -2,6 +2,7 @@
use std::error::Error;
use tracing::{error, info};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
@@ -29,7 +30,7 @@ impl ApplicationHandler for App {
self.window = match event_loop.create_window(window_attributes) {
Ok(window) => Some(window),
Err(err) => {
eprintln!("error creating window: {err}");
error!("error creating window: {err}");
event_loop.exit();
return;
},
@@ -37,10 +38,10 @@ impl ApplicationHandler for App {
}
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(_) => {

View File

@@ -11,6 +11,8 @@ fn main() -> Result<(), Box<dyn Error>> {
#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;
#[derive(Debug)]
pub struct XEmbedDemo {
@@ -58,7 +60,7 @@ 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 })?)
@@ -66,6 +68,5 @@ fn main() -> Result<(), Box<dyn Error>> {
#[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,24 @@ 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.
### 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.
- On macOS, the application is now launched in `EventLoop::new` instead of `EventLoop::run_app`. If you're registering a custom delegate, you should now register it before `EventLoop::new`.
### Fixed
- 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.

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

@@ -75,6 +75,9 @@ impl EventLoopBuilder {
/// `DISPLAY` respectively when building the event loop.
/// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling
/// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic.
/// - **macOS:** this will launch the application, so if you want to register a custom delegate,
/// or otherwise do stuff before `applicationDidFinishLaunching:`, you should do it before
/// this function is called.
///
/// [`platform`]: crate::platform
#[cfg_attr(
@@ -88,7 +91,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 +265,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>();
}