Compare commits

..

211 Commits

Author SHA1 Message Date
Kirill Chibisov
9135eb4024 Winit version 0.29.7 2023-12-27 10:10:59 +04:00
John Nunley
23b3c127fd bugfix: Change value sent to X server during minimize
Closes #3327

Signed-off-by: John Nunley <dev@notgull.net>
2023-12-27 10:10:59 +04:00
John Nunley
b343f45500 bugfix: Reload Xft database on DPI change
Closes #1228
2023-12-27 10:10:59 +04:00
Kirill Chibisov
572d61f9ba Winit version 0.29.6 2023-12-24 23:55:50 +04:00
Uli Schlachter
87fc19826b On X11, simplify available_monitors() impl
This code confused me. I tried to understand it. I tried to simplify it
while keeping the functional style. But in the end, this just seems too
complicated for its own good. Just doing the exact same thing with a
match statement and the question mark operator makes it sooo much more
obvious what is happening.

Signed-off-by: Uli Schlachter <psychon@znc.in>
2023-12-24 23:55:50 +04:00
Kirill Chibisov
11d1b7a980 Fix run_on_demand exiting on consequent call
Fixes #3284.
2023-12-24 23:55:50 +04:00
Kirill Chibisov
5ca810ba8f On Wayland, fix WindowEvent::Destroyed delivery 2023-12-24 23:55:50 +04:00
Alex Butler
2d1607b3f7 bugfix(rwh): Bump rwh_05 min version to 0.5.2
Correct min version to support "std" feature
2023-12-24 23:55:50 +04:00
Markus Siglreithmaier
a32e232020 On Windows, remove internal WindowWrapper (#3294)
HWND in windows-sys doesn't require a newtype wrapper for Send/Sync.
2023-12-24 23:55:50 +04:00
daxpedda
9b03bb7276 Fix some doc nits (#3274) 2023-12-24 23:55:50 +04:00
Markus Siglreithmaier
e39596151c On Windows, refactor dynamic function definitions and raw input keyboard handling (#3286) 2023-12-24 23:55:50 +04:00
daxpedda
5289b4f206 On Web, fix context menu not being disabled 2023-12-24 23:55:50 +04:00
Kirill Chibisov
380dc4c451 Winit version 0.29.5 2023-12-22 00:39:22 +04:00
wjian23
6fbdbce6dd On Windows, fix IME area not working 2023-12-22 00:39:22 +04:00
Kirill Chibisov
cafcaa2cdc On Wayland, ensure initial resize delivery
While we correctly configure the sizes, we also need to actually resize
the frame on initial configure and send geometry.

Fixes #3277.
2023-12-22 00:39:22 +04:00
Kirill Chibisov
e00204e626 On windows, remove empty file 2023-12-22 00:39:22 +04:00
Kirill Chibisov
a5b89bfe5a On Wayland, fix resize being sent on focus change
Fixes #3263.
2023-12-22 00:39:22 +04:00
Amr Bashir
44052a093e On Windows, fix set_fullscreen early return for Fullscreen::Borderless(None) 2023-12-22 00:39:22 +04:00
Friz64
40cee238e2 Update sctk-adwaita to 0.8 2023-12-22 00:39:22 +04:00
Héctor Ramón
3dc5c42387 Fix typo in get_xft_dpi 2023-12-22 00:39:22 +04:00
Marijn Suijten
8c4a6ddcb4 On Wayland, make wl_subcompositor protocol optional
This protocol is only used for (optional) Client Side Decorations
(where) the compositor still takes the burden of compositing various
window parts together, via subsurfaces that all belong to a single
window.

If this core protocol is not available, as is the case on gamescope,
disable CSD.
2023-12-22 00:39:22 +04:00
Uli Schlachter
5011a67f6d m: Update to x11rb 0.13.0
The only breaking change is that x11rb no longer reports an error when
querying the WmSizeHints of a window that does not have this property
set. For this reason, the return type of WmSizeHintsCookie::Reply()
changed from Result<WmSizeHints, SomeError> to
Result<Option<WmSizeHints>, SomeError>.

In update_normal_hints(), previously a cryptic error would be reported
to the caller. Instead, this now uses unwrap_or_default() to get a
WmSizeHints. All fields of WmSizeHints are Options, so this produces an
empty object.

resize_increments() queries a value from the window and returns an
Option. Previously, the error for "missing property" was turned into
None via .ok(). This commit adds a call to flatten() to also turn
"property not set" into None.

Finally, request_user_attention() queries a window's WmHints property
and updates one field of it. The code already uses unwrap_or_default()
to deal with missing properties, so just a call to flatten() is needed
to merge "missing property" and "error while querying" into one.

Other changes in x11rb do not seem to affect this crate.

x11rb's MSRV increased from 1.56 to 1.63, which is still below the MSRV
of this crate, which is 1.65.

Signed-off-by: Uli Schlachter <psychon@znc.in>
2023-12-22 00:39:22 +04:00
Fredrik Fornwall
d621ab5018 On Windows, avoid panic in video_modes() 2023-12-22 00:39:22 +04:00
Leon
966c033a6c Changes and improvements to the documentation (#3253)
* FEATURES.md improvements

* docs improvements

* typo fix: 'mean' -> 'main'

Co-authored-by: daxpedda <daxpedda@gmail.com>

---------

Co-authored-by: daxpedda <daxpedda@gmail.com>
2023-12-22 00:39:22 +04:00
Xiaopeng Li
1681410ca8 fix refresh_rate_millihertz on macOS (#3254)
* fix refresh_rate_millihertz on macOS

* round after conversion to mHz

* add changelog entry
2023-12-22 00:39:22 +04:00
John Nunley
a82327c73f bugfix(x11): Use the right atom type in focus_window()
Closes #3248 by removing an Xlibism I forgot about

Signed-off-by: John Nunley <dev@notgull.net>
2023-12-22 00:39:22 +04:00
John Nunley
e71f765dea bugfix(x11): Properly interpret float data in drag ops
Closes #3245

notgull forgot to properly interpret float data from the X server,
making him tonight's biggest loser.

Signed-off-by: John Nunley <dev@notgull.net>
2023-12-22 00:39:22 +04:00
Mads Marquart
0738528931 Remove unused .gitmodules 2023-12-22 00:39:22 +04:00
Emil Ernerfeldt
8119c72d64 On macOS, remove spurious error logging when handling Fn
Fixes #3246.
2023-12-22 00:39:22 +04:00
Kirill Chibisov
43f29f0481 Winit version 0.29.4 2023-11-24 18:13:46 +04:00
John Nunley
266219f27f On X11, reload DPI on PropertyChange
Signed-off-by: John Nunley <dev@notgull.net>
Fixes #1228.
2023-11-24 18:13:46 +04:00
Kirill Chibisov
7449534ba2 Fix infinite recursion in BadIcon reporting (#3237) 2023-11-24 18:13:46 +04:00
Mads Marquart
7aa202b872 Make Android docs build on docs.rs (#3236) 2023-11-24 18:13:46 +04:00
Arend van Beelen jr
06cec065d4 Fix crash when running iPad build on macOS 2023-11-24 18:13:46 +04:00
Kirill Chibisov
f968e64ac8 On macOS, fix assertion when pressing Fn key 2023-11-24 18:13:46 +04:00
Kirill Chibisov
a97309690e On Wayland, fix wl_surface being dropped first
The surface was automatically dropped due to new RAII type in SCTK
when dropping the Window, which was not the case at some point with
SCTK.

Thus destroying objects associated with it where causing issues
with some window managers.

Links: https://github.com/neovide/neovide/issues/2109
2023-11-24 18:13:46 +04:00
Olivier Goffart
dec45ce0ff On Windows, fix set_control_flow from `AboutToWait
In case the AboutToWait event sets the control flow to another value
it's not being used on this iteration.

Fixes #3215.
2023-11-24 18:13:46 +04:00
Nathan Lilienthal
f709ac667f On macOS, send a Resized event after ScaleFactorChanged
Fixes #3213.
2023-11-24 18:13:46 +04:00
Kirill Chibisov
ecbe04caa7 On X11, try alternative cursor icon names as well
This should cover more icons.
2023-11-24 18:13:46 +04:00
Marijn Suijten
7103514ae8 Disable default-features for the ndk crate
We decided to add `rwh_06` to the `default` list of features in the
`ndk` to [nudge users to upgrade], but this forces `winit` to always
(transitively) include `raw-window-handle 0.6` even if the user has
set a different `rwh_xx` feature on the `winit` crate.  `winit` already
forwards the respective `rwh_xx` feaure to the `ndk` crate anyway, so
this default should just be turned off.

At the time of writing this is the only `default` feature of the `ndk`.

Links: https://github.com/rust-mobile/ndk/pull/434#issuecomment-1752089087
2023-11-24 18:13:46 +04:00
DevJac
8b5aa33a88 Fix typo in pre_present_notify docs
Fix typo and other small grammar corrections.
2023-11-24 18:13:46 +04:00
Linda_pp
7a3b486965 Fix crash when minimizing example on Windows 2023-11-24 18:13:46 +04:00
Jasper Bekkers
fc9c78cb56 On Windows, fix MT safety when starting drag 2023-11-24 18:13:46 +04:00
Kirill Chibisov
525219716c Winit version 0.29.3 2023-10-28 20:55:35 +04:00
Kirill Chibisov
7f851fe433 Clarify scale_factor docs
Wayland scales each window individually, thus make it clear. Also
recommend against using the `MonitorHandle::scale_factor`.

Fixes #3183.
2023-10-28 20:55:35 +04:00
Kirill Chibisov
5dea2a4734 On macOS, add support for Window::set_blur 2023-10-28 20:55:35 +04:00
Kirill Chibisov
821fc63a9c On Windows, add support for Window::set_transparent 2023-10-28 20:55:35 +04:00
Kirill Chibisov
8e9a3d2dd3 On Wayland, improve initial user size handling
Keep the user provided size in the original values and convert only
when we're getting a `configure` event. On some compositors will
have a scale available, so it'll work, however with some we'll
still have old 'pick 1` as default.

Also configure_bounds when compositor tells the user to pick the size,
that will ensure that initial `with_inner_size` won't grow beyond the
working area.

Fixes #3187.
2023-10-28 20:55:35 +04:00
Kirill Chibisov
70a77b8534 On Wayland, fix RedrawRequsted loop
The `dirty` is never cleared when decorations are hidden without
`sctk-adwaita`.

Fixes #3177.
2023-10-28 20:55:35 +04:00
Marijn Suijten
a5bb6d67f7 On wasm, provide intradoc-link for spawn() function in EventLoop docs (#3178) 2023-10-28 20:55:35 +04:00
J-P Nurmi
33a2e4cebd On macOS, fix deadlock during nested event loops (e.g. rfd) 2023-10-28 20:55:35 +04:00
Kirill Chibisov
0ee26986d8 On Winows, Fix deedlock with WM_MOUSEMOVE
The lock was still present in `None` path.

Fixes: d37d1a03b(On Windows, fix deadlock during `Cursor{Enter,Leave}`)
2023-10-28 20:55:35 +04:00
Kirill Chibisov
ec41dddd0d Fix unused import warnings on nightly 2023-10-28 20:55:35 +04:00
Kirill Chibisov
7a872903a4 On Windows, fix deadlock during Cursor{Enter,Leave}
The lock was not released when calling back to the user.

Fixes #3171.
2023-10-28 20:55:35 +04:00
Kirill Chibisov
d82886bddc Winit version 0.29.2 2023-10-21 11:40:41 +04:00
Kirill Chibisov
08edda1b0b On X11, fix cursor_hittest not reloaded on Resize
The cursor hittest was not reloaded on window size changes, only
when `Window::request_inner_size` was called leading to regions
of the window being not clickable.

Also, don't try to apply hittest logic when user never requested a
hittest.

Links: https://github.com/alacritty/alacritty/pull/7220
2023-10-21 11:40:41 +04:00
Diggory Hardy
7de33bca40 Fix rwhd_05 doc links 2023-10-21 11:40:41 +04:00
Valaphee The Meerkat
40ba9a7ce7 feat(windows): Fix inconsistency in mouse button device events, add hwheel device event on Windows
While working with device events, I noticed that there was an inconsistency in the mouse button device events between Windows/X11 and for example web, because web uses the same ids/order as the MouseButton enum, and Windows/X11 are using the X11 ids, and hwheel device event was ignored on Windows.

Mouse button device events are now using the same order as the MouseButton enum, and I also added hwheel device events for Windows.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
0656c54c3b On Windows, fix IME APIs MT-safety
Execute the calls to the IME from the main thread.

Fixes #3123.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
74fcf7f9c0 On Windows, fix RedrawRequested delivery
When calling `Window::request_redraw` from the `RedrawRequested`
handler the `RedrawWindow` won't result in `WM_PAINT` being delivered
due since user callback is run before `DefWindowProcW` is called.

Track whether the user called `Window::request_redraw` and ask for
`RedrawWindow` after running the said function during `WM_PAINT`
handling.

Fixes #3150.
2023-10-21 11:40:41 +04:00
Diggory Hardy
0bc8f5e33a Implement Ord/PartialOrd for ModifiersState 2023-10-21 11:40:41 +04:00
Xiaopeng Li
f6cc6c1472 On Windows, fix invalid hmonitor panic
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-10-21 11:40:41 +04:00
Arend van Beelen jr
20384d2f02 On iOS, add configuration for status bar style
Co-authored-by: Mads Marquart <mads@marquart.dk>
2023-10-21 11:40:41 +04:00
Kirill Chibisov
cdee616812 On macOS, fix tabGroup misuse
The property is marked as `Weak`, however we used strong `Id`.

Links: https://github.com/alacritty/alacritty/issues/7249
2023-10-21 11:40:41 +04:00
Kirill Chibisov
f58fb69446 Remove garbage from README
The docs are in the src/lib.rs anyway and are present on docs.rs.
2023-10-21 11:40:41 +04:00
Diggory Hardy
6b445219c1 Revise Key and KeyCode enums
Split `Key` into clear categories, like `Named`, `Dead`, Character`, `Unidentified`
removing the `#[non_exhaustive]` from the `Key` itself.

Similar action was done for the `KeyCode`.

Fixes: #2995
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-10-21 11:40:41 +04:00
Kirill Chibisov
18b8569161 Ensure that DISPLAY vars are non-empty before using
It's common to disable Wayland by `WAYLAND_DISPLAY= <application>`.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
08b0464ac3 Fix examples not render on Wayland
The `rwh_05` feature was not enabled.

Fixes: e41fac825c (Update to new raw-window-handle strategy)
2023-10-21 11:40:41 +04:00
YouKnow
df2f5adfba On Windows, fix CursorEntered/CursorLeft not sent during mouse grab
Fixes #3153.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
99f86d729f On macOS, fix globe key triggering assertion
Sometimes FlagsChanged events don't carry any KeyCode information, thus
we can't create a synthetic presses events for them.

However in such cases, modifiers information is still accurate, thus
propagate it.

Fixes #2872.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
d06deeecf6 Make WindowBuilder Send + Sync
Window builder is always accessed by winit on the thread event loop
is on, thus it's safe to mark the data it gets as `Send + Sync`.
Each unsafe object is marked individually as `Send + Sync` instead
of just implementing `Send` and `Sync` for the whole builder.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
e6d2fd7287 Remove resolved deny.toml entries 2023-10-21 11:40:41 +04:00
Marijn Suijten
f2edd23542 Upgrade to ndk 0.8, ndk-sys 0.5 + android-activity 0.5 releases
Fixes #2905.
Co-authored-by: Robert Bragg <robert@sixbynine.org>
2023-10-21 11:40:41 +04:00
daxpedda
70e6ddd210 Web Async Rework (#3082) 2023-10-21 11:40:41 +04:00
Kirill Chibisov
f3fb27c17b Add a note on Window::request_redraw on Windows
Fixing this could require a massive rework to how redraw is handled
on windows to the point of removing `WM_PAINT`, since it's not reliable
by any means for our use case.

For now at least document that the API is broken. It was broken like
that for a long while.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
75b463a368 Implement AsFd/AsRawFd for EventLoop<T>
This should help other crates to integrate winit's event loop into
their bigger event loop without adding an extra thread.
2023-10-21 11:40:41 +04:00
John Nunley
ea8604e175 Fix potentially unaligned references in X11 device
Fixes #3125
Signed-off-by: John Nunley <dev@notgull.net>
2023-10-21 11:40:41 +04:00
Kirill Chibisov
b1bd0f77fb Update SCTK to 0.18.0
The update is pretty minor, however we support now
`WindowEvent::Occluded` when xdg-shell v6 is available.

It also adds support for `Window::show_window_menu`.

Fixes #2927.
2023-10-21 11:40:41 +04:00
Kirill Chibisov
1fded249d0 Fix ndk deps versions 2023-10-21 11:40:41 +04:00
John Nunley
349a3e7b8c Update to new raw-window-handle strategy
Signed-off-by: John Nunley <dev@notgull.net>
Co-authored-by: TornaxO7 <tornax@proton.me>
2023-10-21 11:40:41 +04:00
Ryan Hileman
f2d277e599 feat: Implement set_cursor_hittest for X11 2023-10-21 11:40:41 +04:00
Kirill Chibisov
8d5d612456 Fix CHANGELOG entry for Event::MemoryWarning
While the changelog entries for beta releases doesn't really matter. The
change wasn't marked as breaking, while it is.

Fixes: 93f1000a0 (Add Occluded and MemoryWarning events for iOS/Android)
2023-10-21 11:40:41 +04:00
François
5788319632 Add Occluded and MemoryWarning events for iOS/Android
Hook `Occluded` event to foreground/background evens on iOS.

This commit also enabled the `MemoryWarning` event, since it's
emitted from the windowing system.

Co-authored-by: Dusty DeWeese <dustin.deweese@gmail.com>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-10-21 11:40:41 +04:00
YouKnow
976023bfc0 Add Window::show_window_menu
Add a method to request a system menu. The implementation
is provided only on Windows for now.

Co-authored-by: daxpedda <daxpedda@gmail.com>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-10-21 11:40:41 +04:00
daxpedda
0f9b95814e Fix reset to Poll after the event loop starts 2023-10-21 11:40:41 +04:00
baneyue
112dcc808a On Wayland, fix MonitorHandle position 2023-10-21 11:40:41 +04:00
Kirill Chibisov
4a381fb1db Remove obsolete docs about wayland CSD env variable
The env variable was removed a while ago, yet it was still present in
the user docs.
2023-10-21 11:40:41 +04:00
daxpedda
8339ddf368 Web: fix ControlFlow::WaitUntil to never wake up **before** the given time (#3133) 2023-10-21 11:40:41 +04:00
Dmitry Sharshakov
b41f01c990 Add Window::set_blur
Allow clients to request blur behind their window, implemented on
Wayland for now.
2023-10-21 11:40:41 +04:00
daxpedda
570f3101e5 Web: remove unnecessary usage of once_cell::unsync::Lazy (#3134) 2023-10-21 11:40:41 +04:00
daxpedda
3923c59fd8 Update Clippy to v1.73 (#3135) 2023-10-21 11:40:41 +04:00
epimeletes
75ae402a24 Rename run_ondemand to run_on_demand 2023-10-21 11:40:41 +04:00
Fredrik Fornwall
4385c17cbb Make DeviceId contain device id's on Android 2023-10-21 11:40:41 +04:00
Mads Marquart
3af256260e Link to areweguiyet.com and arewegameyet.rs for extra deps 2023-10-21 11:40:41 +04:00
Mads Marquart
d9363219e1 X11: Add #[deny(unsafe_op_in_unsafe_fn)] (#3121)
* X11: Add #[deny(unsafe_op_in_unsafe_fn)]

* Enable #![deny(unsafe_op_in_unsafe_fn)] everywhere
2023-10-21 11:40:41 +04:00
Mads Marquart
a52a6d47ca Windows: Add #[deny(unsafe_op_in_unsafe_fn)] (#3070) 2023-10-21 11:40:41 +04:00
Mads Marquart
ec83de3938 Bump version on master (#3119)
This commit does not represent a release and only synchronizes CHANGELOG from the latest release.
2023-10-21 11:40:41 +04:00
Neil Macneale V
43d6eac871 Fix transparent windows on X11 2023-10-21 11:40:41 +04:00
Kirill Chibisov
1f101b2654 Remove DeviceEvent::Text event
The event is never constructed inside the winit.
2023-10-21 11:40:41 +04:00
lucasmerlin
709929fcab Pass force on touch events on android 2023-10-21 11:40:41 +04:00
daxpedda
220a2d32d5 Make ControlFlow::Wait the default (#3106) 2023-10-21 11:40:41 +04:00
Kirill Chibisov
c5cef46060 Remove old docs about EventLoop::run 2023-10-21 11:40:41 +04:00
Pavel Strakhov
367a2ae057 On X11, fix WaitUntil and Poll behavior
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-10-21 11:40:41 +04:00
StarStarJ
e038597e81 Implement PartialOrd and Ord for MouseButton 2023-10-21 11:40:41 +04:00
Kirill Chibisov
27cd20739d Correct Wayland section in dpi docs
Fixes #3100.
2023-10-21 11:40:41 +04:00
daxpedda
8d9fd3d3d6 Install cargo-apk with the stable toolchain 2023-10-21 11:40:41 +04:00
daxpedda
56427e47a7 Ignore foreign-types* duplicate deps on macOS
The dependency is duplicated due to examples, yet we still need to
exclude checking it.

Fixes #3093.
2023-10-21 11:40:41 +04:00
daxpedda
c744b9aea5 Rename PollType to PollStrategy (#3089) 2023-10-21 11:40:41 +04:00
John Nunley
ef9ed71f1b Add an MSRV policy to the README (#3046) 2023-10-21 11:40:41 +04:00
daxpedda
dda8053bd3 Add Window.requestIdleCallback() support (#3084) 2023-10-21 11:40:41 +04:00
Fredrik Fornwall
779212da33 Correct set_exit() -> exit() in the changelog (#3088) 2023-10-21 11:40:41 +04:00
John Nunley
28552c9cc1 Revert select_xkb_events to its previous impl
The new implementation of select_xkb_events apparently misconfigures
the server. This commit does a temporary fix by just reverting it to its
previous implementation.

This is temporary until I can figure out what Xlib is doing behind the
scenes or until I read xkbproto.pdf.

Fixes: #3079
Signed-off-by: John Nunley <dev@notgull.net>
2023-10-21 11:40:41 +04:00
daxpedda
48647b506f Move ControlFlow to EventLoopWindowTarget
Fixes #3042.
2023-10-21 11:40:41 +04:00
John Nunley
42243ce288 Allow the user to force X11 under Wayland
Use forced backend over the env variables. 

Signed-off-by: John Nunley <dev@notgull.net>
Fixes: #3057
2023-10-21 11:40:41 +04:00
daxpedda
84d9bfd59e Remove T from EventLoopTargetWindow (#3081)
Co-authored-by: nerditation <12248559+nerditation@users.noreply.github.com>
2023-10-21 11:40:41 +04:00
Kirill Chibisov
cd5c1fb724 Mark startup_notify unsafe functions as safe
They are safe, since they use the rust `std::env` stuff. Making them
safe lets downstream to determine that `std::env` is used and not the
`libc` env manipulation routines, which are unsafe.
2023-10-21 11:40:41 +04:00
Mads Marquart
8455f3415e Slightly reduce number of cfgs (#3071)
* Make Linux platforms less dependent on the root monitor handle

* Add various functions to the Wayland platform to reduce cfgs

* Don't use a cfg in listen_device_events

* Don't use a cfg in set_content_protected

* Fix instance of a target_os cfg
2023-10-21 11:40:41 +04:00
Kirill Chibisov
c59d6bc809 On Wayland, fix TouchPhase::Canceled sent for Move
Fixes #3035.
2023-10-21 11:40:41 +04:00
Mads Marquart
4681133eca Make EventLoopWindowTarget independent of the user type on Orbital (#3055) 2023-10-21 11:40:41 +04:00
Mads Marquart
b278aa859f Ensure that winit initializes NSApplication (#3069) 2023-10-21 11:40:41 +04:00
Mads Marquart
ee4ec43cf3 Fix missing quote (#3068) 2023-10-21 11:40:41 +04:00
John Nunley
25b629f117 Implement X11 extensions using x11rb instead of Xlib
Removes Xlib code by replacing it with the x11rb equivalent,
the commit handles xrandr, xinput, xinput2, and xkb.

Signed-off-by: John Nunley <dev@notgull.net>
2023-10-21 11:40:41 +04:00
daxpedda
4b30f9ce22 Web: Fullscreen Overhaul (#3063) 2023-10-21 11:40:41 +04:00
daxpedda
2428224c09 Enable event propagation (#3062) 2023-10-21 11:40:41 +04:00
Mads Marquart
2d9b852a95 Fix macOS deminiaturize (#3054) 2023-10-21 11:40:41 +04:00
Kirill Chibisov
246d53d5a1 Lock the cargo-apk deps on CI 2023-10-21 11:40:41 +04:00
Mads Marquart
865afd22be Make iOS fully thread safe (#3045)
* macOS & iOS: Refactor EventWrapper

* macOS & iOS: Make EventLoopWindowTarget independent of the user event

* iOS: Use MainThreadMarker instead of marking functions unsafe

* Make iOS thread safe
2023-10-21 11:40:41 +04:00
Mads Marquart
05130cb329 Improve CI caching, and give each job names
This improves CI performance by around 20% (down from 10 to 8 minutes).
2023-10-21 11:40:41 +04:00
daxpedda
a1a6f7baf9 Move Event::RedrawRequested to WindowEvent (#3049) 2023-10-21 11:40:41 +04:00
daxpedda
f160a6003c On Web, never return a MonitorHandle (#3051) 2023-10-21 11:40:41 +04:00
daxpedda
a24d092fa1 Use setTimeout() trick instead of Window.requestIdleCallback() (#3044) 2023-10-21 11:40:41 +04:00
Mads Marquart
a8a0462c0d Use frame instead of visibleRect (#3043) 2023-10-21 11:40:41 +04:00
Mads Marquart
647c320ca7 Fix recent CI failures (#3041)
* Fix new clippy lints

* Fix nightly documentation warnings
2023-10-21 11:40:41 +04:00
StarStarJ
5144337253 Implement PartialOrd/Ord for KeyCode/NativeKeyCode 2023-10-21 11:40:41 +04:00
Kirill Chibisov
7f1aaa652d Winit version 0.29.1-beta
Cargo automatically pulls pre-releases, so bump semver for each
pre-release as well, since those can't be resolved automatically.

We don't do betas for the patch bumps, so it should work just fine.
2023-08-16 15:57:50 +04:00
Kirill Chibisov
00b5de0a68 Winit version 0.29.0-beta.1 2023-08-16 12:40:18 +04:00
Kirill Chibisov
80d1e49354 Use beta versions of android crates 2023-08-16 12:39:57 +04:00
Kirill Chibisov
07dd45f8e3 Bump MSRV to 1.65 2023-08-16 12:39:56 +04:00
Mads Marquart
4e6ce00ec5 Improve macOS/iOS/Web thread safety
Co-authored-by: daxpedda <daxpedda@gmail.com>
2023-08-15 13:10:02 +04:00
Kirill Chibisov
65c2482d74 Pin android-activity git dependency 2023-08-15 13:10:02 +04:00
Kirill Chibisov
ba2bfd064f Reexport raw-window-handle in window module
We use raw-window-handle extensive in the public API as well as we
force the users to use it to get some essential data for interop, thus
reexport it.

Fixes: #2913.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
08ad3f19e2 Propagate error from EventLoop creation
Inner panics could make it hard to trouble shoot the issues and for some
users it's not desirable.

The inner panics were left only when they are used to `assert!` during
development.

This reverts commit 9f91bc413fe20618bd7090829832bb074aab15c3 which
reverted the original patch which was merged without a proper review.

Fixes: #500.
2023-08-15 13:10:02 +04:00
lucasmerlin
e3fbfd6792 Fix touch force for Apple Pencil 2023-08-15 13:10:02 +04:00
John Nunley
c40af0062b Add a way to embed the X11 window into another
Signed-off-by: John Nunley <dev@notgull.net>
Tested-by: Kirill Chibisov <contact@kchibisov.com>
2023-08-15 13:10:02 +04:00
Fredrik Fornwall
511bf53889 iOS: Use NSOperatingSystemVersion from icrate (#3019) 2023-08-15 13:10:02 +04:00
Robert Bragg
7451c4b88c Android: Support unicode character mapping + dead keys
Up until now the Android backend has been directly mapping key codes
which essentially just represent the "physical" cap of the key (quoted
since this also related to virtual keyboards).

Since we didn't account for any meta keys either it meant the backend
only supported a 1:1 mapping from key codes, which only covers a tiny
subset of characters. For example you couldn't type a colon since
there's no keycode for that and we didn't try and map Shift+Semicolon
into a colon character.

This has been tricky to support because the `NativeActivity` class doesn't
have direct access to the Java `KeyEvent` object which exposes a more
convenient `getUnicodeChar` API.

It is now possible to query a `KeyCharcterMap` for the device associated
with a `KeyEvent` via the `AndroidApp::device_key_character_map` API
which provides a binding to the SDK `KeyCharacterMap` API in Java:

 https://developer.android.com/reference/android/view/KeyCharacterMap

This is effectively what `getUnicodeChar` is implemented based on and is
a bit more general purpose.

`KeyCharacterMap` lets us map a key_code + meta_state from a `KeyEvent`
into either a unicode character or dead key accent that can be combined
with the following key. This mapping is done based on the user's chosen
layout for the keyboard.

To enable support for key character maps the
`AndroidApp::input_events()` API was replaced by
`AndroidApp::input_events_iter()` which returns a (lending) iterator for
events. This was changed because the previous design made it difficult
to allow other AndroidApp APIs to be used while iterating events (mainly
because AndroidApp held a lock over the backend during iteration)
2023-08-15 13:10:02 +04:00
Kirill Chibisov
42ecef7b31 Fix event loop not waking up due to repeat source
Force the wake up from the repeat source as well.

Fixes: cad327755 (On Wayland, reduce amount of spurious wakeups)
2023-08-15 13:10:02 +04:00
Kirill Chibisov
5d9ce7f5f4 On X11, set visual_id in raw-window-handle
Fixes #2681.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
ef5b71d658 Revert "Propagate error from EventLoop creation" (#3010)
This reverts commit ed26dd58fd.
The patched was merged with a review by accident.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
4ab36f336c Propagate error from EventLoop creation
Inner panics could make it hard to trouble shoot the issues and for some
users ints not desirable.

The inner panics were left only when they are used to `assert!` during
development.
2023-08-15 13:10:02 +04:00
John Nunley
2791cbd65e Make with_x11_visual take ID instead of a pointer
At the moment, the with_x11_visual function takes a pointer and
immediately dereferences it to get the visual info inside. As it is safe
to pass a null pointer to this function, it is unsound. This commit
replaces the pointer parameter with a visual ID, and then uses that ID
to look up the actual visual under
the X11 setup. As this is what was already practically happening before,
this change shouldn't cause any performance downgrades.

This is a breaking change, but it's done in the name of soundness so it
should be okay. It should be trivial for end users to accommodate it,
as it's just a matter of getting the visual ID from the pointer to the
visual before passing it in.

Signed-off-by: John Nunley <dev@notgull.net>
2023-08-15 13:10:02 +04:00
Kirill Chibisov
03bf83f45e Remove 'static requirement on run
There's no need to force the static on the users, given that internally
some backends were not using static in the first place.

Co-authored-by: daxpedda <daxpedda@gmail.com>
2023-08-15 13:10:02 +04:00
Kirill Chibisov
02870202cb On Wayland, reduce amount of spurious wakeups
Mark it as breaking, since some clients relied on that behavior, simply
because dispatching clients queue always woke up a winit, meaning that
they won't be able to use user events for this sake.
2023-08-15 13:10:02 +04:00
Mads Marquart
c268922def Remove functionality already exposed through raw-window-handle
Nothing changed from the user point of view, other than they should
use the `raw-window-handle`, which is objectively better, given that
it reduces the amount of `cfg` guards in downstream code.
2023-08-15 13:10:02 +04:00
John Nunley
61b921c466 Increase test coverage for generic modules 2023-08-15 13:10:02 +04:00
dAxpeDDa
794d0c1f73 On Web, use requestAnimationFrame for RedrawRequested 2023-08-15 13:10:02 +04:00
Kirill Chibisov
8ce58c7053 On Wayland, use frame callbacks to throttle RedrawRequested
Throttle RedrawRequested events by the frame callbacks, so the users
could render at the display refresh rate.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
cff9b01052 Add Window::on_present_notify to ack about drawing
That's a way to communicate to winit that you'll present to the window.
While it's a no-op for now, it'll be used to throttle drawing.
2023-08-15 13:10:02 +04:00
Diggory Hardy
7e9dc147d8 Export smol_str and impl Ord for Key
Fixes #2996.
2023-08-15 13:10:02 +04:00
Mads Marquart
d7827b36d3 Update icrate to v0.0.4 (#2992) 2023-08-15 13:10:02 +04:00
Marijn Suijten
5b90a4e194 On X11, remove the now-unrefrenced events.rs source file
#2662 renamed `VirtualKeyCode` to `Key` yet references to the former
type still exist in `src/platform_impl/linux/x11/events.rs`.  As it
turns out the `mod events;` in `x11/mod.rs` was removed in the same PR,
but the file accidentally stuck around without being referenced anywhere
else.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
281077a0d8 Remove lifetime from the Event
Lifetimes don't work nicely when dealing with multithreaded environments
in the current design of the existing winit's event handling model, so
remove it in favor of `InnerSizeWriter` fences passed to client, so they
could try to update the size.

Fixes #1387.
2023-08-15 13:10:02 +04:00
Tobias Hunger
d21395bb3f On Windows, keep window maximized when setting size bounds (#2899) 2023-08-15 13:10:02 +04:00
Géraud-Loup
f69616ac2c On Windows, add option to customize window class name (#2978) 2023-08-15 13:10:02 +04:00
Mads Marquart
645b1ff00f Update objc2 version (#2936)
* Upgrade to objc2 v0.4.0 and icrate v0.0.3

* Fix `touchBar` method

* Use ClassType::alloc

* Use #[method_id(...)] functionality in declare_class!
2023-08-15 13:10:02 +04:00
Robert Bragg
3925281652 Remove RedrawEventsCleared + MainEventsCleared, and added AboutToWait
The idea that redraw events are dispatched with a specific ordering
that makes it possible to specifically report when we have finished
dispatching redraw events isn't portable and the way in which we
dispatched RedrawEventsCleared was inconsistent across backends.

More generally speaking, there is no inherent relationship between
redrawing and event loop iterations. An event loop may wake up at any
frequency depending on what sources of input events are being listened
to but redrawing is generally throttled and in some way synchronized
with the display frequency.

Similarly there's no inherent relationship between a single event loop
iteration and the dispatching of any specific kind of "main" event.

An event loop wakes up when there are events to read (e.g. input
events or responses from a display server / compositor) and goes back
to waiting when there's nothing else to read.

There isn't really a special kind of "main" event that is dispatched
in order with respect to other events.

What we can do more portably is emit an event when the event loop
is about to block and wait for new events.

In practice this is very similar to how MainEventsCleared was
implemented except it wasn't the very last event previously since
redraw events could be dispatched afterwards.

The main backend where we don't strictly know when we're going to
wait for events is Web (since the real event loop is internal to
the browser). For now we emulate AboutToWait on Web similar to how
MainEventsCleared was dispatched.

In practice most applications almost certainly shouldn't care about
AboutToWait because the frequency of event loop iterations is
essentially arbitrary and usually irrelevant.
2023-08-15 13:10:02 +04:00
Robert Bragg
3bf0fa9ec8 Rename LoopDestroyed to LoopExiting
Considering the possibility of re-running an event loop via run_ondemand
then it's more correct to say that the loop is about to exit without
assuming it's going to be destroyed.
2023-08-15 13:10:02 +04:00
François
7de2bc7ae6 iOS: Always set timer when polling to avoid slow waking (#2979)
* Always set timer when polling to avoid slow waking

* add comment and changelog

---------

Co-authored-by: Dusty DeWeese <dustin.deweese@gmail.com>
Co-authored-by: Mads Marquart <mads@marquart.dk>
2023-08-15 13:10:02 +04:00
Robert Bragg
3f44eb1fd9 Add timeout argument to pump_events
This renames all internal implementations of pump_events_with_timeout
to pump_events and makes them public.

Since all platforms that support pump_events support timeouts there's
no need to have a separate API.
2023-08-15 13:10:02 +04:00
Robert Bragg
456c735bfe Windows: implement pump_events_with_timeout internally 2023-08-15 13:10:02 +04:00
Robert Bragg
973e6ad400 MacOS: implement pump_events_with_timeout internally
This layers pump_events on a pump_events_with_timeout API, like we have
for Linux and Android.

This is just an internal implementation detail for now but we could
consider making pump_events_with_timeout public, or just making it so
that pump_events() takes the timeout argument.
2023-08-15 13:10:02 +04:00
Robert Bragg
07652c76fb window_ondemand: wait for Destroyed event before exiting app
Considering the strict requirement that applications can't keep windows
across run_ondemand calls, this tries to make the window_ondemand example
explicitly wait for its Window to be destroyed before exiting each
run_ondemand iteration.

This updates the example to only `.set_exit()` after it gets a
`Destroyed` event after the Window has been dropped.

On Windows this works to ensure the Window is destroyed before the
example waits for 5 seconds.

Unfortunately though:
1. The Wayland backend doesn't emit `Destroyed` events for windows
2. The macOS backend emits `Destroyed` events before the window is
   really destroyed.

and so the example isn't currently portable.
2023-08-15 13:10:02 +04:00
Robert Bragg
7d93c34e42 Linux: Sync with server/compositor before exiting run_ondemand
Although we document that applications can't keep windows between
separate run_ondemand calls it's possible that the application has only
just dropped their windows and we need to flush these requests to the
server/compositor.

This fixes the window_ondemand example - by ensuring the window from
the first loop really is destroyed before waiting for 5 seconds
and starting the second loop.
2023-08-15 13:10:02 +04:00
Robert Bragg
a2e1a0ac19 Update CHANGELOG.md 2023-08-15 13:10:02 +04:00
Robert Bragg
f1a64b3155 Add examples/window_ondemand
A minimal example that shows an application running the event loop more
than once via `run_ondemand`

There is a 5 second delay between each run to help highlight problems
with destroying the window from the first loop.
2023-08-15 13:10:02 +04:00
Robert Bragg
f8ffa314d0 Add examples/window_pump_events
A minimal example of an application based on an external event loop that
calls `pump_events` for each iteration of the external loop.
2023-08-15 13:10:02 +04:00
Robert Bragg
e28974bc04 Re-work event loop run() API so it can return a Result
This re-works the portable `run()` API that consumes the `EventLoop` and
runs the loop on the calling thread until the app exits.

This can be supported across _all_ platforms and compared to the
previous `run() -> !` API is now able to return a `Result` status on all
platforms except iOS and Web. Fixes: #2709

By moving away from `run() -> !` we stop calling `std::process::exit()`
internally as a means to kill the process without returning which means
it's possible to return an exit status and applications can return from
their `main()` function normally.

This also fixes Android support where an Activity runs in a thread but
we can't assume to have full ownership of the process (other services
could be running in separate threads).

Additionally all examples have generally been updated so that `main()`
returns a `Result` from `run()`

Fixes: #2709
2023-08-15 13:10:02 +04:00
Robert Bragg
93f5f1ac3c Remove EventLoopExtRunReturn 2023-08-15 13:10:02 +04:00
Robert Bragg
a02c680a87 Linux: Implement EventLoopExtPumpEvents and EventLoopExtRunOnDemand
Wayland:

I found the calloop abstraction a little awkward to work with while I was
trying to understand why there was surprising workaround code in the wayland
backend for manually dispatching pending events.

Investigating this further it looks like there may currently be several issues
with the calloop WaylandSource (with how prepare_read is used and with (not)
flushing writes before polling)

Considering the current minimal needs for polling in all winit backends I do
personally tend to think it would be simpler to just own the responsibility for
polling more directly, so the logic for wayland-client `prepare_read` wouldn't
be in a separate crate (and in this current situation would also be easier to fix)

I've tried to maintain the status quo with calloop + workarounds.

X11:

I found that the recent changes (4ac2006cbc) to port the X11 backend
from mio to calloop lost the ability to check for pending events before
needing to poll/dispatch. (The `has_pending` state being queried
before dispatching() was based on state that was filled in during
dispatching)

As part of the rebase this re-introduces the PeekableReceiver and
WakeSender which are small utilities on top of
`std::sync::mpsc::channel()`. This adds a calloop `PingSource`
so we can use a `Ping` as a generic event loop waker.

For taking into account false positive wake ups the X11 source now
tracks when the file descriptor is readable so after we poll via
calloop we can then specifically check if there are new X11 events
or pending redraw/user events when deciding whether to skip the
event loop iteration.
2023-08-15 13:10:02 +04:00
Robert Bragg
6bb62d0b13 MacOS: Implement EventLoopExtPumpEvents and EventLoopExtRunOnDemand
The implementation of `pump_events` essentially works by hooking into the
`RunLoopObserver` and requesting that the app should be stopped the next time
that the `RunLoop` prepares to wait for new events.

Originally I had thought I would poke the `CFRunLoop` for the app directly and
I was originally going to implement `pump_events` based on a timeout which I'd
seen SDL doing.

I found that `[NSApp run]` wasn't actually being stopped by asking the RunLoop
to stop directly and inferred that `NSApp run` will actually catch this and
re-start the loop.

Hooking into the observer and calling `[NSApp stop]` actually seems like a
better solution that doesn't need a hacky constant timeout.

The end result is quite similar to what happens with existing apps that
call `run_return` inside an external loop and cause the loop to exit for
each iteration (that also results in the `NSApp` stopping each
iteration).
2023-08-15 13:10:02 +04:00
Robert Bragg
fae4cbd2aa Windows: Implement EventLoopExtPumpEvents and EventLoopExtRunOnDemand
A surprising amount of work was required to enable these extensions
on Windows.

I had originally assumed that pump_events was going to be very similar
to run except would use PeekMessageW instead of GetMessageW to avoid
blocking the external loop but I found the Windows backend broke
several assumptions I had.

Overall I think these changes can hopefully be considered a quite a
significant simplification (I think it's a net deletion of a fair amount
of code) and I think it also helps bring it into slightly closer alignment
with other backends too

Key changes:
- I have removed the `wait_thread` that was a fairly fiddly way of handling
  `ControlFlow::WaitUntil` timeouts in favor of using `SetTimer` which works
  with the same messages picked up by `GetMessage` and `PeekMessage`.
- I have removed the ordering guarantees between `MainEventsCleared`,
  `RedrawRequested` and `RedrawEventsCleared` events due to the complexity in
  maintaining this artificial ordering, which is already not supported
  consistently across backends anyway (in particular this ordering already
  isn't compatible with how MacOS / iOS work).
- `RedrawRequested` events are now directly dispatched via `WM_PAINT` messages
  - comparable to how `RedrawRequested` is dispatched via `drawRect` in the
  MacOS backend.
- I have re-worked how `NewEvents`, `MainEventsCleared`, and `RedrawEventsCleared`
  get dispatched to be more in line with the MacOS backend and also more in line
  with how we have recently discussed defining them for all platforms.

  `NewEvents` is conceptually delivered when the event loop "wakes up" and
  `MainEventsCleared` gets dispatched when the event loop is about to ask the
  OS to wait for new events.

  This is a more portable model, and is already how these events work in the
  MacOS backend.

  `RedrawEventsCleared` are just delivered after `MainEventsCleared` but this
  event no longer has a useful meaning.

Probably the most controversial thing here is that this "breaks" the ordering
rules for redraw event handling, but since my changes interacted with how the
order is maintained I was very reluctant to figure out how to continue
maintaining something that we have recently been discussing changing:

https://github.com/rust-windowing/winit/issues/2640.

Additionally, since the MacOS backend already doesn't strictly maintain this
order it's somewhat academic to see this as a breakage if Winit applications
can't really rely on it already.

This updates the documentation for `request_redraw()` to reflect that we
no longer guarantee that `RedrawRequested` events must be dispatched
after `MainEventsCleared`.
2023-08-15 13:10:02 +04:00
Robert Bragg
7a954c7e08 Android: Implement EventLoopExtPumpEvents and EventLoopExtRunOnDemand 2023-08-15 13:10:02 +04:00
Robert Bragg
164dce2b8a Add EventLoopExtPumpEvents and EventLoopExtRunOnDemand
This adds two new extensions for running a Winit event loop which will
replace `EventLoopExtRunReturn`

The `run_return` API is trying to solve multiple problems and address
multiple, unrelated, use cases but in doing so it is not succeeding
at addressing any of them fully.

The notable use cases we have are:
1. Applications want to be able to implement their own external
   event loop and call some Winit API to poll / pump events, once
   per iteration of their own loop, without blocking the outer,
   external loop. Addressing #2706
2. Applications want to be able to re-run separate instantiations
   of some Winit-based GUI and want to allow the event loop to exit with
   a status, and then later be able to run the loop again for a new
   instantiation of their GUI. Addressing #2431

It's very notable that these use cases can't be supported across
all platforms and so they are extensions, similar to
`EventLoopExtRunReturn`

The intention is to support these extensions on:
- Windows
- Linux (X11 + Wayland)
- macOS
- Android

These extensions aren't compatible with Web or iOS though.

Each method of running the loop will behave consistently in terms of how
`NewEvents(Init)`, `Resumed` and `LoopDestroyed` events are dispatched
(so portable application code wouldn't necessarily need to have any awareness
of which method of running the loop was being used)

Once all backends have support for these extensions then we can
remove `EventLoopExtRunReturn`

For simplicity, the extensions are documented with the assumption that
the above platforms will be supported.

This patch makes no functional change, it only introduces these new
extensions so we can then handle adding platform-specific backends
in separate pull requests, so the work can be landed in stages.
2023-08-15 13:10:02 +04:00
daxpedda
0ba4283c29 Correctly detect that we don't support Emscripten (#2971) 2023-08-15 13:10:02 +04:00
John Nunley
62b4ba8b50 Replace libc with rustix in some modules
Unfortunately this isn't a total removal, for two reasons:

- We still need "libc" for the Xlib XIM implementation, for locales.
- BSD requires libc to check for main-threadedness.

First one we can likely resolve in the near future, not so sure about
the second one without using some weird pthreads trick.
2023-08-15 13:10:02 +04:00
Venceslas Duet
afebe2e7d1 On Windows, add drag_resize_window method support (#2966) 2023-08-15 13:10:02 +04:00
Kirill Chibisov
0efcfaf5a9 Add platform::startup_notify for Wayland/X11
The utils in this module should help the users to activate the windows
they create, as well as manage activation tokens environment variables.

The API is essential for Wayland in the first place, since some
compositors may decide initial focus of the window based on whether
the activation token was during the window creation.

Fixes #2279.

Co-authored-by: John Nunley <jtnunley01@gmail.com>
2023-08-15 13:10:02 +04:00
Venceslas Duet
d86ce9de9f On Wayland, fix Window::is_decorated with CSD 2023-08-15 13:10:02 +04:00
Kirill Chibisov
7fa7cea700 On Wayland, make the CSD frame double click reliable
It was discovered that on GNOME the click sometimes being swallowed
by the mutter's `wl_pointer::enter/leave` sequences. This was happening
due to `xdg_toplevel::move` making the pointer to leave the surface.

To make handling of that more robust, we could start the
`xdg_toplevel::move` when the actual pointer motion is being performed.

Links: https://github.com/alacritty/alacritty/issues/7011
Links: https://gitlab.gnome.org/GNOME/mutter/-/issues/2669#note_1790825
2023-08-15 13:10:02 +04:00
Kirill Chibisov
36ebad3246 On macOS, add a way to query amount of tabbed windows
This should provide a way to iterate all the tabs and select the last
tab. The tab indicies are now zero based as any other sane index.

Follow-up-to: c5941d105f (add tabbing API)
2023-08-15 13:10:02 +04:00
Sam
912c45e9f7 On macOS, set that we prefer tabbing
Winit now supports tabbing identifiers, thus set that we prefer tabbing,
in particular it'll make windows tab when using the same tabbing identifiers,
which is desirable for the end users.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
e2c71a4422 On macOS, move automatic tabbing setting to ELWT
Those are global for the application, so it's better to keep them
on EventLoopWindowTarget.
2023-08-15 13:10:02 +04:00
Kirill Chibisov
b50d9a0228 On macOS, add tabbing APIs
This should let the users control macOS tabbing and allow to create
windows in tab.

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
2023-08-15 13:10:02 +04:00
daxpedda
692f15c49f On Web, add WindowBuilderExtWebSys::with_append() (#2953) 2023-08-15 13:10:02 +04:00
daxpedda
5366694db2 Improve documentation of WindowBuilderExtWebSys methods (#2952) 2023-08-15 13:10:02 +04:00
John Nunley
7962271faa Replace parts of the Xlib backend with x11-rb 2023-08-15 13:10:02 +04:00
daxpedda
78b5f2feb8 On Web, remove Window::is_dark_mode() (#2951) 2023-08-15 13:10:02 +04:00
daxpedda
4baab2d93e Fix mentions of Wasm (#2950) 2023-08-15 13:10:02 +04:00
daxpedda
79385ecd1f On Web, implement and fix missing methods on Window(Builder) (#2949) 2023-08-15 13:10:02 +04:00
daxpedda
8d18043a3c Add Fullscreen API compatibility for Safari (#2948) 2023-08-15 13:10:02 +04:00
daxpedda
297c3f80eb On Web, cache commonly used values (#2947) 2023-08-15 13:10:02 +04:00
daxpedda
1d80005b91 Increase accuracy of various Web APIs (#2946) 2023-08-15 13:10:02 +04:00
daxpedda
fab0f62c5a Improve Web specific documentation for various APIs (#2941)
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
2023-08-15 13:10:02 +04:00
Kirill Chibisov
d83188befd Rename Window::set_inner_size to Window::request_inner_size
Some systems could resize the window immediately and we'd rather
inform the users right away if that was the case, so they could
create e.g. EGLSurface without waiting for resize, which is really
important for Wayland.

Fixes #2868.
2023-08-15 13:10:02 +04:00
daxpedda
b9d89e97ed Fix touch location accuracy (#2944) 2023-08-15 13:10:02 +04:00
daxpedda
5fa4b8f003 On Web, implement WindowEvent::Occluded (#2940) 2023-08-15 13:10:02 +04:00
Imbris
7a4ce631bd On X11, avoid false positive key repeats
Instead of a single `bool` indicating that a key press has occured and
no key has been released since then, we store the scancode of the last
pressed key (if it is a key that repeats when held). This fixes a bug
where pressing a new key while one is already held down will be flagged
as a repeat even though it is obviously not a repeat.
2023-08-15 13:10:02 +04:00
Mads Marquart
8d5f82f0c0 Stop using &mut in Objective-C delegate methods (#2925)
* Make iOS declared classes not use &mut

* Prepare `init` methods for not having access to &mut self

* Prepare WinitWindow methods for not having access to &mut self

* Convert a bit of WinitView's to use interior mutability

* Convert a bit more of WinitView's to use interior mutability

* Convert the rest of WinitView to use interior mutability

* Use interior mutability instead of a Mutex for the CursorState

* Use interior mutability in WinitWindowDelegate
2023-08-15 13:10:02 +04:00
daxpedda
08fe32eac3 Don't unnecessarily clone canvas on Web (#2934) 2023-08-15 13:10:02 +04:00
daxpedda
1cddc96a0b Fix typos on Web (#2933) 2023-08-15 13:10:02 +04:00
Kirill Chibisov
84ef89eb1c Winit version 0.29.0-beta.0 2023-06-30 23:33:29 +04:00
79 changed files with 3146 additions and 3521 deletions

View File

@@ -11,12 +11,19 @@ Unreleased` header.
# Unreleased
- On Windows, macOS, X11, Wayland and Web, implement setting images as cursors. See the `custom_cursors.rs` example.
- Add `Window::set_custom_cursor`
- Add `CustomCursor`
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
- On macOS, add services menu.
# 0.29.7
- On X11, fix `Xft.dpi` reload during runtime.
- On X11, fix window minimize.
# 0.29.6
- On Web, fix context menu not being disabled by `with_prevent_default(true)`.
- On Wayland, fix `WindowEvent::Destroyed` not being delivered after destroying window.
- Fix `EventLoopExtRunOnDemand::run_on_demand` not working for consequent invocation
# 0.29.5
- On macOS, remove spurious error logging when handling `Fn`.
- On X11, fix an issue where floating point data from the server is
misinterpreted during a drag and drop operation.
@@ -25,8 +32,8 @@ Unreleased` header.
- On Wayland, disable Client Side Decorations when `wl_subcompositor` is not supported.
- On X11, fix `Xft.dpi` detection from Xresources.
- On Windows, fix consecutive calls to `window.set_fullscreen(Some(Fullscreen::Borderless(None)))` resulting in losing previous window state when eventually exiting fullscreen using `window.set_fullscreen(None)`.
- On Web, remove queuing fullscreen request in absence of transient activation.
- On Web, fix setting cursor icon overriding cursor visibility.
- On Wayland, fix resize being sent on focus change.
- On Windows, fix `set_ime_cursor_area`.
# 0.29.4

View File

@@ -1,6 +1,6 @@
[package]
name = "winit"
version = "0.29.4"
version = "0.29.7"
authors = ["The winit contributors", "Pierre Krieger <pierre.krieger1708@gmail.com>"]
description = "Cross-platform window creation library."
edition = "2021"
@@ -43,7 +43,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb", "xim"]
x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"]
wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "wayland-protocols-plasma", "sctk", "ahash", "memmap2"]
wayland-dlopen = ["wayland-backend/dlopen"]
wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"]
@@ -66,7 +66,7 @@ log = "0.4"
mint = { version = "0.5.6", optional = true }
once_cell = "1.12"
rwh_04 = { package = "raw-window-handle", version = "0.4", optional = true }
rwh_05 = { package = "raw-window-handle", version = "0.5", features = ["std"], optional = true }
rwh_05 = { package = "raw-window-handle", version = "0.5.2", features = ["std"], optional = true }
rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"], optional = true }
serde = { version = "1", optional = true, features = ["serde_derive"] }
smol_str = "0.2.0"
@@ -167,7 +167,6 @@ wayland-protocols = { version = "0.31.0", features = [ "staging"], optional = tr
wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optional = true }
x11-dl = { version = "2.18.5", optional = true }
x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true }
xim = { version = "0.3.0", features = ["x11rb-client", "client"], optional = true }
xkbcommon-dl = "0.4.0"
[target.'cfg(target_os = "redox")'.dependencies]
@@ -180,7 +179,6 @@ version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'Blob',
'console',
'CssStyleDeclaration',
'Document',
@@ -192,10 +190,6 @@ features = [
'FocusEvent',
'HtmlCanvasElement',
'HtmlElement',
'ImageBitmap',
'ImageBitmapOptions',
'ImageBitmapRenderingContext',
'ImageData',
'IntersectionObserver',
'IntersectionObserverEntry',
'KeyboardEvent',
@@ -205,7 +199,6 @@ features = [
'Node',
'PageTransitionEvent',
'PointerEvent',
'PremultiplyAlpha',
'ResizeObserver',
'ResizeObserverBoxOptions',
'ResizeObserverEntry',
@@ -213,8 +206,7 @@ features = [
'ResizeObserverSize',
'VisibilityState',
'Window',
'WheelEvent',
'Url',
'WheelEvent'
]
[target.'cfg(target_family = "wasm")'.dependencies]
@@ -232,6 +224,3 @@ web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
members = [
"run-wasm",
]
[patch.crates-io]
xim = { git = "https://github.com/forkgull/xim-rs", branch = "x11rb-13" }

View File

@@ -106,7 +106,6 @@ If your PR makes notable changes to Winit's features, please update this section
- **Cursor locking**: Locking the cursor inside the window so it cannot move.
- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them.
- **Cursor icon**: Changing the cursor icon or hiding the cursor.
- **Cursor image**: Changing the cursor to your own image.
- **Cursor hittest**: Handle or ignore mouse events for a window.
- **Touch events**: Single-touch events.
- **Touch pressure**: Touch events contain information about the amount of force being applied.
@@ -207,7 +206,6 @@ Legend:
|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ |
|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ |
|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** |
|Cursor image |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** |
|Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ |
|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** |
|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** |

View File

@@ -6,7 +6,7 @@
```toml
[dependencies]
winit = "0.29.4"
winit = "0.29.7"
```
## [Documentation](https://docs.rs/winit)
@@ -156,7 +156,7 @@ For more details, refer to these `android-activity` [example applications](https
If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk`, then the minimal changes would be:
1. Remove `ndk-glue` from your `Cargo.toml`
2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.29.4", features = [ "android-native-activity" ] }`
2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.29.7", features = [ "android-native-activity" ] }`
3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize logging as above).
4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your event loop (as shown above).

View File

@@ -6,7 +6,6 @@ disallowed-methods = [
{ path = "web_sys::HtmlCanvasElement::set_height", reason = "Winit shouldn't touch the internal canvas size" },
{ path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::HtmlElement::style", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::Element::request_fullscreen", reason = "Doesn't account for compatibility with Safari" },
{ path = "web_sys::Document::exit_fullscreen", reason = "Doesn't account for compatibility with Safari" },
{ path = "web_sys::Document::fullscreen_element", reason = "Doesn't account for compatibility with Safari" },

View File

@@ -1,92 +0,0 @@
#![allow(clippy::single_match, clippy::disallowed_methods)]
#[cfg(not(wasm_platform))]
use simple_logger::SimpleLogger;
use winit::{
event::{ElementState, Event, KeyEvent, WindowEvent},
event_loop::EventLoop,
keyboard::Key,
window::{CustomCursor, WindowBuilder},
};
fn decode_cursor(bytes: &[u8]) -> CustomCursor {
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
let samples = img.into_flat_samples();
let (_, w, h) = samples.extents();
let (w, h) = (w as u16, h as u16);
CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
}
#[cfg(not(wasm_platform))]
#[path = "util/fill.rs"]
mod fill;
fn main() -> Result<(), impl std::error::Error> {
#[cfg(not(wasm_platform))]
SimpleLogger::new()
.with_level(log::LevelFilter::Info)
.init()
.unwrap();
#[cfg(wasm_platform)]
console_log::init_with_level(log::Level::Debug).unwrap();
let event_loop = EventLoop::new().unwrap();
let builder = WindowBuilder::new().with_title("A fantastic window!");
#[cfg(wasm_platform)]
let builder = {
use winit::platform::web::WindowBuilderExtWebSys;
builder.with_append(true)
};
let window = builder.build(&event_loop).unwrap();
let mut cursor_idx = 0;
let mut cursor_visible = true;
let custom_cursors = [
decode_cursor(include_bytes!("data/cross.png")),
decode_cursor(include_bytes!("data/cross2.png")),
];
event_loop.run(move |event, _elwt| match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::KeyboardInput {
event:
KeyEvent {
state: ElementState::Pressed,
logical_key: key,
..
},
..
} => match key.as_ref() {
Key::Character("1") => {
log::debug!("Setting cursor to {:?}", cursor_idx);
window.set_custom_cursor(&custom_cursors[cursor_idx]);
cursor_idx = (cursor_idx + 1) % 2;
}
Key::Character("2") => {
log::debug!("Setting cursor icon to default");
window.set_cursor_icon(Default::default());
}
Key::Character("3") => {
cursor_visible = !cursor_visible;
log::debug!("Setting cursor visibility to {:?}", cursor_visible);
window.set_cursor_visible(cursor_visible);
}
_ => {}
},
WindowEvent::RedrawRequested => {
#[cfg(not(wasm_platform))]
fill::fill_window(&window);
}
WindowEvent::CloseRequested => {
#[cfg(not(wasm_platform))]
_elwt.exit();
}
_ => (),
},
Event::AboutToWait => {
window.request_redraw();
}
_ => {}
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

View File

@@ -7,17 +7,30 @@
//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could
//! also be used to fill the window buffer, but they are more complicated to use.
use winit::window::Window;
#[allow(unused_imports)]
pub use platform::cleanup_window;
pub use platform::fill_window;
#[cfg(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios"))))]
pub(super) fn fill_window(window: &Window) {
use softbuffer::{Context, Surface};
mod platform {
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::num::NonZeroU32;
use softbuffer::{Context, Surface};
use winit::window::Window;
use winit::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>>> = ManuallyDrop::new(RefCell::new(None));
}
/// The graphics context used to draw to a window.
struct GraphicsContext {
/// The global softbuffer context.
@@ -35,55 +48,69 @@ pub(super) fn fill_window(window: &Window) {
}
}
fn surface(&mut self, w: &Window) -> &mut Surface {
self.surfaces.entry(w.id()).or_insert_with(|| {
unsafe { Surface::new(&self.context, w) }
fn create_surface(&mut self, window: &Window) -> &mut Surface {
self.surfaces.entry(window.id()).or_insert_with(|| {
unsafe { Surface::new(&self.context, window) }
.expect("Failed to create a softbuffer surface")
})
}
fn destroy_surface(&mut self, window: &Window) {
self.surfaces.remove(&window.id());
}
}
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>>> = ManuallyDrop::new(RefCell::new(None));
pub fn fill_window(window: &Window) {
GC.with(|gc| {
let size = window.inner_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.
const DARK_GRAY: u32 = 0xFF181818;
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(DARK_GRAY);
buffer
.present()
.expect("Failed to present the softbuffer buffer");
})
}
GC.with(|gc| {
let size = window.inner_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))
.surface(window);
// Fill a buffer with a solid color.
const DARK_GRAY: u32 = 0xFF181818;
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(DARK_GRAY);
buffer
.present()
.expect("Failed to present the softbuffer buffer");
})
#[allow(dead_code)]
pub fn cleanup_window(window: &Window) {
GC.with(|gc| {
let mut gc = gc.borrow_mut();
if let Some(context) = gc.as_mut() {
context.destroy_surface(window);
}
});
}
}
#[cfg(not(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios")))))]
pub(super) fn fill_window(_window: &Window) {
// No-op on mobile platforms.
mod platform {
pub fn fill_window(_window: &winit::window::Window) {
// No-op on mobile platforms.
}
#[allow(dead_code)]
pub fn cleanup_window(_window: &winit::window::Window) {
// No-op on mobile platforms.
}
}

View File

@@ -40,6 +40,7 @@ fn main() -> Result<(), impl std::error::Error> {
window_id,
} if window.id() == window_id => {
println!("--------------------------------------------------------- Window {idx} CloseRequested");
fill::cleanup_window(window);
app.window = None;
}
Event::AboutToWait => window.request_redraw(),

View File

@@ -1,198 +0,0 @@
use core::fmt;
use std::{error::Error, sync::Arc};
use crate::platform_impl::PlatformCustomCursor;
/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`].
pub const MAX_CURSOR_SIZE: u16 = 2048;
const PIXEL_SIZE: usize = 4;
/// Use a custom image as a cursor (mouse pointer).
///
/// ## Platform-specific
///
/// **Web**: Some browsers have limits on cursor sizes usually at 128x128.
///
/// # Example
///
/// ```
/// use winit::window::CustomCursor;
///
/// let w = 10;
/// let h = 10;
/// let rgba = vec![255; (w * h * 4) as usize];
/// let custom_cursor = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
///
/// #[cfg(target_family = "wasm")]
/// let custom_cursor_url = {
/// use winit::platform::web::CustomCursorExtWebSys;
/// CustomCursor::from_url("http://localhost:3000/cursor.png", 0, 0).unwrap()
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCursor {
pub(crate) inner: Arc<PlatformCustomCursor>,
}
impl CustomCursor {
/// Creates a new cursor from an rgba buffer.
///
/// ## Platform-specific
///
/// - **Web:** Setting cursor could be delayed due to the creation of `Blob` objects,
/// which are async by nature.
pub fn from_rgba(
rgba: impl Into<Vec<u8>>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
Ok(Self {
inner: PlatformCustomCursor::from_rgba(
rgba.into(),
width,
height,
hotspot_x,
hotspot_y,
)?
.into(),
})
}
}
/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments.
#[derive(Debug, Clone)]
pub enum BadImage {
/// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
/// guarantee that the cursor will work, but should avoid many platform and device specific
/// limits.
TooLarge { width: u16, height: u16 },
/// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
/// safely interpreted as 32bpp RGBA pixels.
ByteCountNotDivisibleBy4 { byte_count: usize },
/// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
/// At least one of your arguments is incorrect.
DimensionsVsPixelCount {
width: u16,
height: u16,
width_x_height: u64,
pixel_count: u64,
},
/// Produced when the hotspot is outside the image bounds
HotspotOutOfBounds {
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
},
}
impl fmt::Display for BadImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BadImage::TooLarge { width, height } => write!(f,
"The specified dimensions ({width:?}x{height:?}) are too large. The maximum is {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
),
BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(f,
"The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.",
),
BadImage::DimensionsVsPixelCount {
width,
height,
width_x_height,
pixel_count,
} => write!(f,
"The specified dimensions ({width:?}x{height:?}) don't match the number of pixels supplied by the `rgba` argument ({pixel_count:?}). For those dimensions, the expected pixel count is {width_x_height:?}.",
),
BadImage::HotspotOutOfBounds {
width,
height,
hotspot_x,
hotspot_y,
} => write!(f,
"The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds ({width:?}x{height:?}).",
),
}
}
}
impl Error for BadImage {}
/// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CursorImage {
pub(crate) rgba: Vec<u8>,
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) hotspot_x: u16,
pub(crate) hotspot_y: u16,
}
#[allow(dead_code)]
impl CursorImage {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
return Err(BadImage::TooLarge { width, height });
}
if rgba.len() % PIXEL_SIZE != 0 {
return Err(BadImage::ByteCountNotDivisibleBy4 {
byte_count: rgba.len(),
});
}
let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
let width_x_height = width as u64 * height as u64;
if pixel_count != width_x_height {
return Err(BadImage::DimensionsVsPixelCount {
width,
height,
width_x_height,
pixel_count,
});
}
if hotspot_x >= width || hotspot_y >= height {
return Err(BadImage::HotspotOutOfBounds {
width,
height,
hotspot_x,
hotspot_y,
});
}
Ok(CursorImage {
rgba,
width,
height,
hotspot_x,
hotspot_y,
})
}
}
// Platforms that don't support cursors will export this as `PlatformCustomCursor`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NoCustomCursor;
#[allow(dead_code)]
impl NoCustomCursor {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?;
Ok(Self)
}
}

View File

@@ -172,7 +172,6 @@ extern crate bitflags;
pub mod dpi;
#[macro_use]
pub mod error;
mod cursor;
pub mod event;
pub mod event_loop;
mod icon;

View File

@@ -76,6 +76,14 @@ impl<T> EventLoopExtRunOnDemand for EventLoop<T> {
where
F: FnMut(Event<Self::UserEvent>, &EventLoopWindowTarget<Self::UserEvent>),
{
self.event_loop.window_target().clear_exit();
self.event_loop.run_on_demand(event_handler)
}
}
impl<T> EventLoopWindowTarget<T> {
/// Clear exit status.
pub(crate) fn clear_exit(&self) {
self.p.clear_exit()
}
}

View File

@@ -27,11 +27,9 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
use crate::cursor::CustomCursor;
use crate::event::Event;
use crate::event_loop::EventLoop;
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursor;
use crate::window::{Window, WindowBuilder};
use crate::SendSyncWrapper;
@@ -202,25 +200,3 @@ pub enum PollStrategy {
#[default]
Scheduler,
}
pub trait CustomCursorExtWebSys {
/// Creates a new cursor from a URL pointing to an image.
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
///
/// [PNG]: https://en.wikipedia.org/wiki/PNG
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self;
}
impl CustomCursorExtWebSys for CustomCursor {
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self {
Self {
inner: PlatformCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
}
.into(),
}
}
}

View File

@@ -18,7 +18,6 @@ use android_activity::{
use once_cell::sync::Lazy;
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error,
event::{self, Force, InnerSizeWriter, StartCause},
@@ -714,6 +713,10 @@ impl<T: 'static> EventLoopWindowTarget<T> {
self.exit.set(true)
}
pub(crate) fn clear_exit(&self) {
self.exit.set(false)
}
pub(crate) fn exiting(&self) -> bool {
self.exit.get()
}
@@ -907,8 +910,6 @@ impl Window {
pub fn set_cursor_icon(&self, _: window::CursorIcon) {}
pub fn set_custom_cursor(&self, _: CustomCursor) {}
pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> {
Err(error::ExternalError::NotSupported(
error::NotSupportedError::new(),
@@ -1034,7 +1035,6 @@ impl Display for OsError {
}
}
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]

View File

@@ -77,7 +77,6 @@ pub(crate) use self::{
};
use self::uikit::UIScreen;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

View File

@@ -11,7 +11,6 @@ use super::app_state::EventWrapper;
use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation};
use super::view::{WinitUIWindow, WinitView, WinitViewController};
use crate::{
cursor::CustomCursor,
dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
event::{Event, WindowEvent},
@@ -178,10 +177,6 @@ impl Inner {
debug!("`Window::set_cursor_icon` ignored on iOS")
}
pub fn set_custom_cursor(&self, _: CustomCursor) {
debug!("`Window::set_custom_cursor` ignored on iOS")
}
pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> {
Err(ExternalError::NotSupported(NotSupportedError::new()))
}

View File

@@ -14,7 +14,6 @@ use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex};
use once_cell::sync::Lazy;
use smol_str::SmolStr;
use crate::cursor::CustomCursor;
#[cfg(x11_platform)]
use crate::platform::x11::XlibErrorHook;
use crate::{
@@ -41,7 +40,6 @@ pub use x11::XNotSupported;
#[cfg(x11_platform)]
use x11::{util::WindowType as XWindowType, X11Error, XConnection, XError};
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;
@@ -426,11 +424,6 @@ impl Window {
x11_or_wayland!(match self; Window(w) => w.set_cursor_icon(cursor))
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
x11_or_wayland!(match self; Window(w) => w.set_custom_cursor(cursor))
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode))
@@ -925,6 +918,10 @@ impl<T> EventLoopWindowTarget<T> {
x11_or_wayland!(match self; Self(evlp) => evlp.control_flow())
}
pub(crate) fn clear_exit(&self) {
x11_or_wayland!(match self; Self(evlp) => evlp.clear_exit())
}
pub(crate) fn exit(&self) {
x11_or_wayland!(match self; Self(evlp) => evlp.exit())
}

View File

@@ -467,44 +467,44 @@ impl<T: 'static> EventLoop<T> {
});
for window_id in window_ids.drain(..) {
let request_redraw = self.with_state(|state| {
let event = self.with_state(|state| {
let window_requests = state.window_requests.get_mut();
if window_requests.get(&window_id).unwrap().take_closed() {
mem::drop(window_requests.remove(&window_id));
mem::drop(state.windows.get_mut().remove(&window_id));
false
} else {
let mut window = state
.windows
.get_mut()
.get_mut(&window_id)
.unwrap()
.lock()
.unwrap();
if window.frame_callback_state() == FrameCallbackState::Requested {
false
} else {
// Reset the frame callbacks state.
window.frame_callback_reset();
let mut redraw_requested = window_requests
.get(&window_id)
.unwrap()
.take_redraw_requested();
// Redraw the frame while at it.
redraw_requested |= window.refresh_frame();
redraw_requested
}
return Some(WindowEvent::Destroyed);
}
let mut window = state
.windows
.get_mut()
.get_mut(&window_id)
.unwrap()
.lock()
.unwrap();
if window.frame_callback_state() == FrameCallbackState::Requested {
return None;
}
// Reset the frame callbacks state.
window.frame_callback_reset();
let mut redraw_requested = window_requests
.get(&window_id)
.unwrap()
.take_redraw_requested();
// Redraw the frame while at it.
redraw_requested |= window.refresh_frame();
redraw_requested.then_some(WindowEvent::RedrawRequested)
});
if request_redraw {
if let Some(event) = event {
callback(
Event::WindowEvent {
window_id: crate::window::WindowId(window_id),
event: WindowEvent::RedrawRequested,
event,
},
&self.window_target,
);
@@ -629,6 +629,34 @@ pub struct EventLoopWindowTarget<T> {
}
impl<T> EventLoopWindowTarget<T> {
pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) {
self.control_flow.set(control_flow)
}
pub(crate) fn control_flow(&self) -> ControlFlow {
self.control_flow.get()
}
pub(crate) fn exit(&self) {
self.exit.set(Some(0))
}
pub(crate) fn clear_exit(&self) {
self.exit.set(None)
}
pub(crate) fn exiting(&self) -> bool {
self.exit.get().is_some()
}
pub(crate) fn set_exit_code(&self, code: i32) {
self.exit.set(Some(code))
}
pub(crate) fn exit_code(&self) -> Option<i32> {
self.exit.get()
}
#[inline]
pub fn listen_device_events(&self, _allowed: DeviceEvents) {}

View File

@@ -4,7 +4,6 @@ use sctk::reexports::client::Proxy;
use sctk::output::OutputData;
use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
use crate::event_loop::ControlFlow;
use crate::platform_impl::platform::VideoMode as PlatformVideoMode;
use super::event_loop::EventLoopWindowTarget;
@@ -24,30 +23,6 @@ impl<T> EventLoopWindowTarget<T> {
// There's no primary monitor on Wayland.
None
}
pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) {
self.control_flow.set(control_flow)
}
pub(crate) fn control_flow(&self) -> ControlFlow {
self.control_flow.get()
}
pub(crate) fn exit(&self) {
self.exit.set(Some(0))
}
pub(crate) fn exiting(&self) -> bool {
self.exit.get().is_some()
}
pub(crate) fn set_exit_code(&self, code: i32) {
self.exit.set(Some(code))
}
pub(crate) fn exit_code(&self) -> Option<i32> {
self.exit.get()
}
}
#[derive(Clone, Debug)]

View File

@@ -19,7 +19,6 @@ use sctk::seat::SeatState;
use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler};
use sctk::shell::xdg::XdgShell;
use sctk::shell::WaylandSurface;
use sctk::shm::slot::SlotPool;
use sctk::shm::{Shm, ShmHandler};
use sctk::subcompositor::SubcompositorState;
@@ -59,9 +58,6 @@ pub struct WinitState {
/// The shm for software buffers, such as cursors.
pub shm: Shm,
/// The pool where custom cursors are allocated.
pub custom_cursor_pool: Arc<Mutex<SlotPool>>,
/// The XDG shell that is used for widnows.
pub xdg_shell: XdgShell,
@@ -157,17 +153,13 @@ impl WinitState {
(None, None)
};
let shm = Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?;
let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap()));
Ok(Self {
registry_state,
compositor_state: Arc::new(compositor_state),
subcompositor_state: subcompositor_state.map(Arc::new),
output_state,
seat_state,
shm,
custom_cursor_pool,
shm: Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?,
xdg_shell: XdgShell::bind(globals, queue_handle).map_err(WaylandError::Bind)?,
xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(),
@@ -307,7 +299,11 @@ impl WindowHandler for WinitState {
&mut self.events_sink,
);
self.window_compositor_updates[pos].size = Some(new_size);
// NOTE: Only update when the value is `Some` to not override consequent configures with
// the same sizes.
if new_size.is_some() {
self.window_compositor_updates[pos].size = new_size;
}
}
}

View File

@@ -1,56 +0,0 @@
use cursor_icon::CursorIcon;
use sctk::reexports::client::protocol::wl_shm::Format;
use sctk::shm::slot::{Buffer, SlotPool};
use crate::cursor::CursorImage;
#[derive(Debug)]
pub enum SelectedCursor {
Named(CursorIcon),
Custom(CustomCursor),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
#[derive(Debug)]
pub struct CustomCursor {
pub buffer: Buffer,
pub w: i32,
pub h: i32,
pub hotspot_x: i32,
pub hotspot_y: i32,
}
impl CustomCursor {
pub fn new(pool: &mut SlotPool, image: &CursorImage) -> Self {
let (buffer, canvas) = pool
.create_buffer(
image.width as i32,
image.height as i32,
4 * (image.width as i32),
Format::Argb8888,
)
.unwrap();
for (canvas_chunk, rgba_chunk) in canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4))
{
canvas_chunk[0] = rgba_chunk[2];
canvas_chunk[1] = rgba_chunk[1];
canvas_chunk[2] = rgba_chunk[0];
canvas_chunk[3] = rgba_chunk[3];
}
CustomCursor {
buffer,
w: image.width as i32,
h: image.height as i32,
hotspot_x: image.hotspot_x as i32,
hotspot_y: image.hotspot_y as i32,
}
}
}

View File

@@ -1,6 +1,5 @@
//! Wayland protocol implementation boilerplate.
pub mod cursor;
pub mod kwin_blur;
pub mod wp_fractional_scaling;
pub mod wp_viewporter;

View File

@@ -15,7 +15,6 @@ use sctk::shell::xdg::window::Window as SctkWindow;
use sctk::shell::xdg::window::WindowDecorations;
use sctk::shell::WaylandSurface;
use crate::cursor::CustomCursor;
use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError};
use crate::event::{Ime, WindowEvent};
@@ -507,11 +506,6 @@ impl Window {
self.window_state.lock().unwrap().set_cursor(cursor);
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
self.window_state.lock().unwrap().set_custom_cursor(cursor);
}
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
self.window_state

View File

@@ -1,7 +1,7 @@
//! The state of the window, which is shared with the event-loop.
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex, Weak};
use std::sync::{Arc, Weak};
use std::time::Duration;
use log::{info, warn};
@@ -18,23 +18,20 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::
use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge;
use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt};
use sctk::seat::pointer::{PointerDataExt, ThemedPointer};
use sctk::compositor::{CompositorState, Region};
use sctk::seat::pointer::ThemedPointer;
use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure};
use sctk::shell::xdg::XdgSurface;
use sctk::shell::WaylandSurface;
use sctk::shm::slot::SlotPool;
use sctk::shm::Shm;
use sctk::subcompositor::SubcompositorState;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use crate::cursor::CustomCursor as RootCustomCursor;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
use crate::error::{ExternalError, NotSupportedError};
use crate::event::WindowEvent;
use crate::platform_impl::wayland::event_loop::sink::EventSink;
use crate::platform_impl::wayland::make_wid;
use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor};
use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager;
use crate::platform_impl::WindowId;
use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme};
@@ -63,16 +60,14 @@ pub struct WindowState {
/// The `Shm` to set cursor.
pub shm: WlShm,
// A shared pool where to allocate custom cursors.
custom_cursor_pool: Arc<Mutex<SlotPool>>,
/// The last received configure.
pub last_configure: Option<WindowConfigure>,
/// The pointers observed on the window.
pub pointers: Vec<Weak<ThemedPointer<WinitPointerData>>>,
selected_cursor: SelectedCursor,
/// Cursor icon.
pub cursor_icon: CursorIcon,
/// Wether the cursor is visible.
pub cursor_visible: bool,
@@ -183,7 +178,7 @@ impl WindowState {
connection,
csd_fails: false,
cursor_grab_mode: GrabState::new(),
selected_cursor: Default::default(),
cursor_icon: CursorIcon::Default,
cursor_visible: true,
decorate: true,
fractional_scale,
@@ -202,7 +197,6 @@ impl WindowState {
resizable: true,
scale_factor: 1.,
shm: winit_state.shm.wl_shm().clone(),
custom_cursor_pool: winit_state.custom_cursor_pool.clone(),
size: initial_size.to_logical(1.),
stateless_size: initial_size.to_logical(1.),
initial_size: Some(initial_size),
@@ -262,7 +256,7 @@ impl WindowState {
shm: &Shm,
subcompositor: &Option<Arc<SubcompositorState>>,
event_sink: &mut EventSink,
) -> LogicalSize<u32> {
) -> Option<LogicalSize<u32>> {
// NOTE: when using fractional scaling or wl_compositor@v6 the scaling
// should be delivered before the first configure, thus apply it to
// properly scale the physical sizes provided by the users.
@@ -325,14 +319,9 @@ impl WindowState {
match configure.new_size {
(Some(width), Some(height)) => {
let (width, height) = frame.subtract_borders(width, height);
(
(
width.map(|w| w.get()).unwrap_or(1),
height.map(|h| h.get()).unwrap_or(1),
)
.into(),
false,
)
let width = width.map(|w| w.get()).unwrap_or(1);
let height = height.map(|h| h.get()).unwrap_or(1);
((width, height).into(), false)
}
(_, _) if stateless => (self.stateless_size, true),
_ => (self.size, true),
@@ -358,13 +347,31 @@ impl WindowState {
.unwrap_or(new_size.height);
}
// XXX Set the configure before doing a resize.
let new_state = configure.state;
let old_state = self
.last_configure
.as_ref()
.map(|configure| configure.state);
let state_change_requires_resize = old_state
.map(|old_state| {
!old_state
.symmetric_difference(new_state)
.difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED)
.is_empty()
})
// NOTE: `None` is present for the initial configure, thus we must always resize.
.unwrap_or(true);
// NOTE: Set the configure before doing a resize, since we query it during it.
self.last_configure = Some(configure);
// XXX Update the new size right away.
self.resize(new_size);
new_size
if state_change_requires_resize || new_size != self.inner_size() {
self.resize(new_size);
Some(new_size)
} else {
None
}
}
/// Compute the bounds for the inner size of the surface.
@@ -609,10 +616,7 @@ impl WindowState {
/// Reload the cursor style on the given window.
pub fn reload_cursor_style(&mut self) {
if self.cursor_visible {
match &self.selected_cursor {
SelectedCursor::Named(icon) => self.set_cursor(*icon),
SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor),
}
self.set_cursor(self.cursor_icon);
} else {
self.set_cursor_visible(self.cursor_visible);
}
@@ -698,8 +702,10 @@ impl WindowState {
}
/// Set the cursor icon.
///
/// Providing `None` will hide the cursor.
pub fn set_cursor(&mut self, cursor_icon: CursorIcon) {
self.selected_cursor = SelectedCursor::Named(cursor_icon);
self.cursor_icon = cursor_icon;
if !self.cursor_visible {
return;
@@ -712,54 +718,6 @@ impl WindowState {
})
}
/// Set the custom cursor icon.
pub fn set_custom_cursor(&mut self, cursor: RootCustomCursor) {
let cursor = {
let mut pool = self.custom_cursor_pool.lock().unwrap();
CustomCursor::new(&mut pool, &cursor.inner)
};
if self.cursor_visible {
self.apply_custom_cursor(&cursor);
}
self.selected_cursor = SelectedCursor::Custom(cursor);
}
fn apply_custom_cursor(&self, cursor: &CustomCursor) {
self.apply_on_poiner(|pointer, _| {
let surface = pointer.surface();
let scale = surface
.data::<SurfaceData>()
.unwrap()
.surface_data()
.scale_factor();
surface.set_buffer_scale(scale);
surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0);
if surface.version() >= 4 {
surface.damage_buffer(0, 0, cursor.w, cursor.h);
} else {
surface.damage(0, 0, cursor.w / scale, cursor.h / scale);
}
surface.commit();
let serial = pointer
.pointer()
.data::<WinitPointerData>()
.and_then(|data| data.pointer_data().latest_enter_serial())
.unwrap();
pointer.pointer().set_cursor(
serial,
Some(surface),
cursor.hotspot_x / scale,
cursor.hotspot_y / scale,
);
});
}
/// Set maximum inner window size.
pub fn set_min_inner_size(&mut self, size: Option<LogicalSize<u32>>) {
// Ensure that the window has the right minimum size.
@@ -894,10 +852,7 @@ impl WindowState {
self.cursor_visible = cursor_visible;
if self.cursor_visible {
match &self.selected_cursor {
SelectedCursor::Named(icon) => self.set_cursor(*icon),
SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor),
}
self.set_cursor(self.cursor_icon);
} else {
for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) {
let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial();
@@ -970,7 +925,7 @@ impl WindowState {
/// Set the IME position.
pub fn set_ime_cursor_area(&self, position: LogicalPosition<u32>, size: LogicalSize<u32>) {
// XXX This won't fly unless user will have a way to request IME window per seat, since
// FIXME: This won't fly unless user will have a way to request IME window per seat, since
// the ime windows will be overlapping, but winit doesn't expose API to specify for
// which seat we're setting IME position.
let (x, y) = (position.x as i32, position.y as i32);
@@ -1001,7 +956,7 @@ impl WindowState {
pub fn set_scale_factor(&mut self, scale_factor: f64) {
self.scale_factor = scale_factor;
// XXX when fractional scaling is not used update the buffer scale.
// NOTE: When fractional scaling is not used update the buffer scale.
if self.fractional_scale.is_none() {
let _ = self.window.set_buffer_scale(self.scale_factor as _);
}
@@ -1149,7 +1104,7 @@ impl From<ResizeDirection> for XdgResizeEdge {
}
}
// XXX rust doesn't allow from `Option`.
// NOTE: Rust doesn't allow `From<Option<Theme>>`.
#[cfg(feature = "sctk-adwaita")]
fn into_sctk_adwaita_config(theme: Option<Theme>) -> sctk_adwaita::FrameConfig {
match theme {

View File

@@ -40,6 +40,7 @@ atom_manager! {
WM_DELETE_WINDOW,
WM_PROTOCOLS,
WM_STATE,
XIM_SERVERS,
// Assorted ICCCM Atoms
_NET_WM_ICON,

View File

@@ -44,7 +44,7 @@ impl From<io::Error> for DndDataParseError {
pub(crate) struct Dnd {
xconn: Arc<XConnection>,
// Populated by XdndEnter event handler
pub version: Option<u32>,
pub version: Option<c_long>,
pub type_list: Option<Vec<xproto::Atom>>,
// Populated by XdndPosition event handler
pub source_window: Option<xproto::Window>,

File diff suppressed because it is too large Load Diff

View File

@@ -1,862 +0,0 @@
//! IME handler, using the xim-rs crate.
use super::{X11Error, X11rbConnection, XConnection};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::Window;
use x11rb::protocol::Event;
use xim::x11rb::{HasConnection, X11rbClient};
use xim::{AttributeName, Client as _, ClientError, ClientHandler, InputStyle, Point};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
impl HasConnection for XConnection {
type Connection = X11rbConnection;
fn conn(&self) -> &Self::Connection {
self.xcb_connection()
}
}
/// A collection of the IME events that can occur.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ImeEvent {
Enabled,
Start,
Update(String, Option<usize>),
Commit(String),
End,
Disabled,
}
/// Invalid states that an IME client can enter.
#[derive(Debug, Clone)]
pub enum InvalidImeState {
/// The IME has no style information.
NoStyle,
/// No windows in the pending window queue.
NoWindows,
/// Invalid input context.
InvalidIc(u16),
}
impl fmt::Display for InvalidImeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidImeState::NoStyle => write!(f, "IME has no style information"),
InvalidImeState::NoWindows => {
write!(f, "IME has no windows in the pending window queue")
}
InvalidImeState::InvalidIc(ic) => write!(f, "IME has invalid input context {}", ic),
}
}
}
/// Request to control XIM handler from the window.
pub enum ImeRequest {
/// Set IME spot position for given `window_id`.
Position(Window, i16, i16),
/// Allow IME input for the given `window_id`.
Allow(Window, bool),
}
/// The IME data for winit.
pub(super) struct ImeData {
/// The XIM client manager.
client: X11rbClient<Arc<XConnection>>,
/// Relevant IME data.
handler: ImeHandler,
}
/// Inner IME handler.
struct ImeHandler {
/// Handle to the event queue.
event_queue: Rc<RefCell<VecDeque<Event>>>,
/// Whether IME is currently disconnected.
disconnected: bool,
/// IME events waiting to be read.
ime_events: VecDeque<(Window, ImeEvent)>,
/// Windows waiting to be assigned an input context.
pending_windows: VecDeque<WindowData>,
/// Currently registered input styles.
styles: Option<(Style, Style)>,
/// The input method for the display, if there is one.
input_method: Option<u16>,
/// Hash map between input contexts and their associated data.
input_contexts: HashMap<u16, IcData>,
/// Map between window IDs and their associated input contexts.
window_contexts: HashMap<Window, u16>,
}
/// Data relevant for each input context.
struct IcData {
/// Data associated with the window.
window: WindowData,
/// Newly set point for the context.
new_spot: Option<Point>,
/// The current preedit string.
///
/// We use a `Vec<char>` here instead of a string because the IME indices operate on chars,
/// not bytes.
text: Vec<char>,
/// The current cursor position in the preedit string.
cursor: usize,
}
/// Windows waiting for IME events.
struct WindowData {
/// The window ID.
id: Window,
/// The style of the window.
style: Style,
/// Current "spot" for the context.
spot: Point,
}
#[derive(Copy, Clone)]
enum Style {
Preedit,
Nothing,
None,
}
impl ImeData {
/// Creates the IME data for the display.
pub(super) fn new(
conn: &Arc<XConnection>,
screen: usize,
event_queue: &Rc<RefCell<VecDeque<Event>>>,
) -> Result<Self, X11Error> {
// IM servers to try, in order:
// - None, which defaults to the environment variable `XMODIFIERS` in xim's impl.
// - "local", which is the default for most IMEs.
// - empty string, which may work in some cases.
let input_methods = [None, Some("local"), Some("")];
let mut last_error = X11Error::Ime(ClientError::NoXimServer);
for im in input_methods {
// Try to initialize a client here.
match X11rbClient::init(conn.clone(), screen, im) {
Ok(client) => {
return Ok(Self {
client,
handler: ImeHandler {
event_queue: event_queue.clone(),
disconnected: true,
ime_events: VecDeque::new(),
pending_windows: VecDeque::new(),
styles: None,
input_method: None,
input_contexts: HashMap::new(),
window_contexts: HashMap::new(),
},
})
}
Err(err) => {
log::warn!("Failed to create XIM client for {:?}: {err}", ImData(im));
last_error = X11Error::Ime(err);
}
}
}
Err(last_error)
}
/// Filter an event.
pub(super) fn filter_event(&mut self, event: &Event) -> Result<bool, X11Error> {
self.client
.filter_event(event, &mut self.handler)
.map_err(X11Error::Ime)
}
/// Connection to the X server.
fn conn(&self) -> &X11rbConnection {
self.client.conn()
}
/// Get an IME event.
pub(super) fn next_ime_event(&mut self) -> Option<(Window, ImeEvent)> {
self.handler.ime_events.pop_front()
}
/// Create a new IME context for the provided window.
pub(super) fn create_context(
&mut self,
window: Window,
with_preedit: bool,
spot: Option<Point>,
) -> Result<bool, X11Error> {
// If we aren't connected, nothing can be done.
if self.handler.disconnected {
return Ok(false);
}
let method = match self.handler.input_method {
Some(im) => im,
None => return Ok(false),
};
// Get the current style.
let style = match (self.handler.styles, with_preedit) {
(None, _) => return Err(X11Error::InvalidImeState(InvalidImeState::NoStyle)),
(Some((preedit_style, _)), true) => preedit_style,
(Some((_, none_style)), false) => none_style,
};
// Setup IC attributes.
let ic_attributes = {
let mut ic_attributes = self
.client
.build_ic_attributes()
.push(AttributeName::ClientWindow, window);
let ic_style = match style {
Style::Preedit => InputStyle::PREEDIT_POSITION | InputStyle::STATUS_NOTHING,
Style::Nothing => InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING,
Style::None => InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE,
};
if let Some(spot) = spot.clone() {
ic_attributes = ic_attributes.push(AttributeName::SpotLocation, spot);
}
ic_attributes
.push(AttributeName::InputStyle, ic_style)
.build()
};
// Create the IC.
self.client.create_ic(method, ic_attributes)?;
// Add to the waiting window list.
self.handler.pending_windows.push_back(WindowData {
id: window,
style,
spot: spot.unwrap_or(Point { x: 0, y: 0 }),
});
Ok(true)
}
/// Remove an IME context for a window.
pub(super) fn remove_context(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = match self.handler.input_method {
Some(im) => im,
None => return Ok(false),
};
// Remove the pending window if it's still pending.
let mut removed = false;
self.handler.pending_windows.retain(|pending| {
if pending.id == window {
removed = true;
false
} else {
true
}
});
if removed {
return Ok(true);
}
// Remove the IC if it's already created.
if let Some(ic) = self.handler.window_contexts.remove(&window) {
self.handler.input_contexts.remove(&ic);
// Destroy the IC.
self.client.destroy_ic(method, ic)?;
}
Ok(false)
}
/// Focus an IME context.
pub(super) fn focus_window(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
self.client.set_focus(method, ic)?;
return Ok(true);
}
Ok(false)
}
/// Unfocus an IME context.
pub(super) fn unfocus_window(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
self.client.unset_focus(method, ic)?;
return Ok(true);
}
Ok(false)
}
/// Set the spot for an IME context.
pub(super) fn set_spot(&mut self, window: Window, x: i16, y: i16) -> Result<(), X11Error> {
if self.handler.disconnected {
return Ok(());
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
// If the IC is not available, or if the spot is the same, then we don't need to update.
let ic_data = match self.handler.input_contexts.get_mut(&ic) {
Some(ic_data) => ic_data,
None => return Ok(()),
};
let new_point = Point { x, y };
if !matches!(ic_data.window.style, Style::None) || ic_data.window.spot == new_point {
return Ok(());
}
let new_attrs = self
.client
.build_ic_attributes()
.push(AttributeName::SpotLocation, new_point.clone())
.build();
self.client.set_ic_values(method, ic, new_attrs)?;
// Indicate that we have a new spot.
debug_assert!(ic_data.new_spot.is_none());
ic_data.new_spot = Some(new_point);
}
Ok(())
}
pub(super) fn set_ime_allowed(
&mut self,
window: Window,
allowed: bool,
) -> Result<(), X11Error> {
if self.handler.disconnected {
return Ok(());
}
// Get the client info.
let _ = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
let mut spot = None;
// See if we need to update the allowed state.
if let Some(ic_data) = self.handler.input_contexts.get(&ic) {
spot = Some(ic_data.window.spot.clone());
if matches!(ic_data.window.style, Style::None) != allowed {
return Ok(());
}
}
// Delete and re-install the IC.
self.remove_context(window)?;
self.create_context(window, allowed, spot)?;
}
Ok(())
}
/// Wait for the input method to be set.
fn wait_for_method(&mut self) -> Result<u16, X11Error> {
loop {
if let Some(im) = self.handler.input_method {
return Ok(im);
}
// Wait and hope the input method is set.
self.block_for_ime()?;
}
}
/// Wait for an input context to be set.
fn wait_for_context(&mut self, window: Window) -> Result<Option<u16>, X11Error> {
if let Some(cid) = self.handler.window_contexts.get(&window) {
return Ok(Some(*cid));
}
// If the window isn't in our pending windows queue, there's no way for it to get an IC.
if !self
.handler
.pending_windows
.iter()
.any(|WindowData { id, .. }| *id == window)
{
return Ok(None);
}
loop {
self.block_for_ime()?;
if let Some(cid) = self.handler.window_contexts.get(&window) {
return Ok(Some(*cid));
}
}
}
/// Wait until we've acted on an IME event.
fn block_for_ime(&mut self) -> Result<(), X11Error> {
let mut last_event = self.conn().poll_for_event()?;
loop {
if let Some(last_event) = last_event.as_ref() {
if self.filter_event(last_event)? {
return Ok(());
}
}
// This scope keeps track of the event queue handle.
{
// Check the event queue for events.
let event_queue = self.handler.event_queue.clone();
let mut event_queue = event_queue.borrow_mut();
// Check the event queue for events we can use.
let mut found_event = false;
let mut last_err = None;
event_queue.retain(|event| match self.filter_event(event) {
Ok(false) => {
found_event = true;
true
}
Ok(true) => false,
Err(err) => {
last_err = Some(err);
true
}
});
// Push our own event to the queue.
if let Some(last_event) = last_event.take() {
event_queue.push_back(last_event);
}
// Check for errors.
if let Some(err) = last_err {
return Err(err);
}
// If we found an event, then we're done.
if found_event {
return Ok(());
}
}
log::info!("Waiting for IME event");
last_event = Some(self.conn().wait_for_event()?);
}
}
}
impl<C: xim::Client> ClientHandler<C> for ImeHandler {
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
// We have been connected, now request a new input method for our current locale.
self.disconnected = false;
client.open(&locale())
}
fn handle_disconnect(&mut self) {
// We are now disconnected.
self.disconnected = true;
}
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
// We now have an input method.
debug_assert!(self.input_method.is_none());
self.input_method = Some(input_method_id);
// Ask for the IM's attributes.
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
}
fn handle_close(&mut self, _client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
// No more input method.
debug_assert_eq!(self.input_method, Some(input_method_id));
self.input_method = None;
Ok(())
}
fn handle_get_im_values(
&mut self,
_client: &mut C,
input_method_id: u16,
mut attributes: xim::AHashMap<xim::AttributeName, Vec<u8>>,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the input styles.
let mut preedit_style = None;
let mut none_style = None;
let styles = {
let style = attributes
.remove(&AttributeName::QueryInputStyle)
.expect("No query input style");
let mut result = vec![0u32; style.len() / 4];
bytemuck::cast_slice_mut::<u32, u8>(&mut result).copy_from_slice(&style);
result
};
{
// The styles that we're looking for.
let lu_preedit_style = InputStyle::PREEDIT_CALLBACKS | InputStyle::STATUS_NOTHING;
let lu_nothing_style = InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING;
let lu_none_style = InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE;
for style in styles {
let style = InputStyle::from_bits_truncate(style);
if style == lu_preedit_style {
preedit_style = Some(Style::Preedit);
} else if style == lu_nothing_style {
preedit_style = Some(Style::Nothing);
} else if style == lu_none_style {
none_style = Some(Style::None);
}
}
}
let (preedit_style, none_style) = match (preedit_style, none_style) {
(None, None) => {
log::error!("No supported input styles found");
return Ok(());
}
(Some(style), None) | (None, Some(style)) => (style, style),
(Some(preedit_style), Some(none_style)) => (preedit_style, none_style),
};
self.styles = Some((preedit_style, none_style));
Ok(())
}
fn handle_create_ic(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the window that wanted the IC context.
let window = self
.pending_windows
.pop_front()
.ok_or_else(|| invalid_state(InvalidImeState::NoWindows))?;
// Create the IC data.
let ic_data = IcData {
window,
new_spot: None,
text: Vec::new(),
cursor: 0,
};
// Store the context.
let (window, style) = (ic_data.window.id, ic_data.window.style);
self.input_contexts.insert(input_context_id, ic_data);
self.window_contexts.insert(window, input_context_id);
// Indicate our status.
let event = if matches!(style, Style::Nothing) {
ImeEvent::Disabled
} else {
ImeEvent::Enabled
};
self.ime_events.push_back((window, event));
Ok(())
}
fn handle_destroy_ic(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
) -> Result<(), ClientError> {
// This is already handled by the higher-level function.
Ok(())
}
fn handle_set_ic_values(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the IC data.
let ic_data = self
.input_contexts
.get_mut(&input_context_id)
.ok_or_else(|| invalid_state(InvalidImeState::InvalidIc(input_context_id)))?;
// Move up the new spot
if let Some(spot) = ic_data.new_spot.take() {
ic_data.window.spot = spot;
}
Ok(())
}
fn handle_preedit_start(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Start a pre-edit.
ic_data.text.clear();
ic_data.cursor = 0;
// Indicate the start.
self.ime_events
.push_back((ic_data.window.id, ImeEvent::Start));
}
Ok(())
}
fn handle_preedit_draw(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
caret: i32,
chg_first: i32,
chg_len: i32,
_status: xim::PreeditDrawStatus,
preedit_string: &str,
_feedbacks: Vec<xim::Feedback>,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Set the cursor.
ic_data.cursor = caret as usize;
// Figure out the range of text to change.
let change_range = chg_first as usize..(chg_first + chg_len) as usize;
// If the range doesn't fit our current text, warn and return.
if change_range.start > ic_data.text.len() || change_range.end > ic_data.text.len() {
warn!(
"Preedit draw range {}..{} doesn't fit text of length {}",
change_range.start,
change_range.end,
ic_data.text.len()
);
return Ok(());
}
// Update the text in the changed range.
{
let text = &mut ic_data.text;
let mut old_text_tail = text.split_off(change_range.end);
text.truncate(change_range.start);
text.extend(preedit_string.chars());
text.append(&mut old_text_tail);
}
// Send the event.
let cursor_byte_pos = calc_byte_position(&ic_data.text, ic_data.cursor);
let event = ImeEvent::Update(ic_data.text.iter().collect(), Some(cursor_byte_pos));
self.ime_events.push_back((ic_data.window.id, event));
}
Ok(())
}
fn handle_preedit_caret(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
position: &mut i32,
direction: xim::CaretDirection,
_style: xim::CaretStyle,
) -> Result<(), ClientError> {
// We only care about absolute position.
if matches!(direction, xim::CaretDirection::AbsolutePosition) {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
ic_data.cursor = *position as usize;
// Send the event
let event =
ImeEvent::Update(ic_data.text.iter().collect(), Some(*position as usize));
self.ime_events.push_back((ic_data.window.id, event));
}
}
Ok(())
}
fn handle_preedit_done(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the client data.
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// We're done with a preedit.
ic_data.text.clear();
ic_data.cursor = 0;
// Send a message to the window.
let window = ic_data.window.id;
self.ime_events.push_back((window, ImeEvent::End));
}
Ok(())
}
fn handle_commit(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the client data.
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Send a message to the window.
let window = ic_data.window.id;
self.ime_events
.push_back((window, ImeEvent::Commit(text.to_owned())));
}
Ok(())
}
fn handle_query_extension(
&mut self,
_client: &mut C,
_extensions: &[xim::Extension],
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
fn handle_forward_event(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_flag: xim::ForwardEventFlag,
_xev: C::XEvent,
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
fn handle_set_event_mask(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_forward_event_mask: u32,
_synchronous_event_mask: u32,
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
}
#[inline(always)]
fn invalid_state(state: InvalidImeState) -> ClientError {
ClientError::Other(Box::new(X11Error::InvalidImeState(state)))
}
struct ImData(Option<&'static str>);
impl fmt::Debug for ImData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(name) => write!(f, "\"{}\"", name),
None => write!(f, "default input method"),
}
}
}
/// Get the current locale.
fn locale() -> String {
use std::ffi::CStr;
const EN_US: &str = "en_US.UTF-8";
// Get the pointer to the current locale.
let locale_ptr = unsafe { libc::setlocale(libc::LC_CTYPE, std::ptr::null()) };
// If locale_ptr is null, just default to en_US.UTF-8.
if locale_ptr.is_null() {
return EN_US.to_owned();
}
// Convert the pointer to a CStr.
let locale_cstr = unsafe { CStr::from_ptr(locale_ptr) };
// Convert the CStr to a String to prevent the result from getting clobbered.
locale_cstr.to_str().unwrap_or(EN_US).to_owned()
}
fn calc_byte_position(text: &[char], pos: usize) -> usize {
text.iter().take(pos).map(|c| c.len_utf8()).sum()
}

View File

@@ -0,0 +1,215 @@
use std::{collections::HashMap, os::raw::c_char, ptr, sync::Arc};
use super::{ffi, XConnection, XError};
use super::{
context::{ImeContext, ImeContextCreationError},
inner::{close_im, ImeInner},
input_method::PotentialInputMethods,
};
pub(crate) unsafe fn xim_set_callback(
xconn: &Arc<XConnection>,
xim: ffi::XIM,
field: *const c_char,
callback: *mut ffi::XIMCallback,
) -> Result<(), XError> {
// It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize
// access that isn't type-checked.
unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) };
xconn.check_errors()
}
// Set a callback for when an input method matching the current locale modifiers becomes
// available. Note that this has nothing to do with what input methods are open or able to be
// opened, and simply uses the modifiers that are set when the callback is set.
// * This is called per locale modifier, not per input method opened with that locale modifier.
// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt
// input contexts would always silently fail to use the input method.
pub(crate) unsafe fn set_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
unsafe {
(xconn.xlib.XRegisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
)
};
xconn.check_errors()
}
pub(crate) unsafe fn unset_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
unsafe {
(xconn.xlib.XUnregisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
)
};
xconn.check_errors()
}
pub(crate) unsafe fn set_destroy_callback(
xconn: &Arc<XConnection>,
im: ffi::XIM,
inner: &ImeInner,
) -> Result<(), XError> {
unsafe {
xim_set_callback(
xconn,
im,
ffi::XNDestroyCallback_0.as_ptr() as *const _,
&inner.destroy_callback as *const _ as *mut _,
)
}
}
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
enum ReplaceImError {
// Boxed to prevent large error type
MethodOpenFailed(Box<PotentialInputMethods>),
ContextCreationFailed(ImeContextCreationError),
SetDestroyCallbackFailed(XError),
}
// Attempt to replace current IM (which may or may not be presently valid) with a new one. This
// includes replacing all existing input contexts and free'ing resources as necessary. This only
// modifies existing state if all operations succeed.
unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
let xconn = unsafe { &(*inner).xconn };
let (new_im, is_fallback) = {
let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) };
let is_fallback = new_im.is_fallback();
(
new_im.ok().ok_or_else(|| {
ReplaceImError::MethodOpenFailed(Box::new(unsafe {
(*inner).potential_input_methods.clone()
}))
})?,
is_fallback,
)
};
// It's important to always set a destroy callback, since there's otherwise potential for us
// to try to use or free a resource that's already been destroyed on the server.
{
let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) };
if result.is_err() {
let _ = unsafe { close_im(xconn, new_im.im) };
}
result
}
.map_err(ReplaceImError::SetDestroyCallbackFailed)?;
let mut new_contexts = HashMap::new();
for (window, old_context) in unsafe { (*inner).contexts.iter() } {
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
// Check if the IME was allowed on that context.
let is_allowed = old_context
.as_ref()
.map(|old_context| old_context.is_allowed())
.unwrap_or_default();
// We can't use the style from the old context here, since it may change on reload, so
// pick style from the new XIM based on the old state.
let style = if is_allowed {
new_im.preedit_style
} else {
new_im.none_style
};
let new_context = {
let result = unsafe {
ImeContext::new(
xconn,
new_im.im,
style,
*window,
spot,
(*inner).event_sender.clone(),
)
};
if result.is_err() {
let _ = unsafe { close_im(xconn, new_im.im) };
}
result.map_err(ReplaceImError::ContextCreationFailed)?
};
new_contexts.insert(*window, Some(new_context));
}
// If we've made it this far, everything succeeded.
unsafe {
let _ = (*inner).destroy_all_contexts_if_necessary();
let _ = (*inner).close_im_if_necessary();
(*inner).im = Some(new_im);
(*inner).contexts = new_contexts;
(*inner).is_destroyed = false;
(*inner).is_fallback = is_fallback;
}
Ok(())
}
pub unsafe extern "C" fn xim_instantiate_callback(
_display: *mut ffi::Display,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
let xconn = unsafe { &(*inner).xconn };
match unsafe { replace_im(inner) } {
Ok(()) => unsafe {
let _ = unset_instantiate_callback(xconn, client_data);
(*inner).is_fallback = false;
},
Err(err) => unsafe {
if (*inner).is_destroyed {
// We have no usable input methods!
panic!("Failed to reopen input method: {err:?}");
}
},
}
}
}
// This callback is triggered when the input method is closed on the server end. When this
// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been
// free'd (attempting to do so causes our connection to freeze).
pub unsafe extern "C" fn xim_destroy_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
unsafe { (*inner).is_destroyed = true };
let xconn = unsafe { &(*inner).xconn };
if unsafe { !(*inner).is_fallback } {
let _ = unsafe { set_instantiate_callback(xconn, client_data) };
// Attempt to open fallback input method.
match unsafe { replace_im(inner) } {
Ok(()) => unsafe { (*inner).is_fallback = true },
Err(err) => {
// We have no usable input methods!
panic!("Failed to open fallback input method: {err:?}");
}
}
}
}
}

View File

@@ -0,0 +1,385 @@
use std::ffi::CStr;
use std::os::raw::c_short;
use std::sync::Arc;
use std::{mem, ptr};
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
use super::{ffi, util, XConnection, XError};
/// IME creation error.
#[derive(Debug)]
pub enum ImeContextCreationError {
/// Got the error from Xlib.
XError(XError),
/// Got null pointer from Xlib but without exact reason.
Null,
}
/// The callback used by XIM preedit functions.
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
/// Wrapper for creating XIM callbacks.
#[inline]
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
XIMCallback {
client_data,
callback: Some(callback),
}
}
/// The server started preedit.
extern "C" fn preedit_start_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) -> i32 {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
client_data.text.clear();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::Start))
.expect("failed to send preedit start event");
-1
}
/// Done callback is used when the preedit should be hidden.
extern "C" fn preedit_done_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
// Drop text buffer and reset cursor position on done.
client_data.text = Vec::new();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::End))
.expect("failed to send preedit end event");
}
fn calc_byte_position(text: &[char], pos: usize) -> usize {
text.iter()
.take(pos)
.fold(0, |byte_pos, text| byte_pos + text.len_utf8())
}
/// Preedit text information to be drawn inline by the client.
extern "C" fn preedit_draw_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
client_data.cursor_pos = call_data.caret as usize;
let chg_range =
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
warn!(
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
client_data.text.len(),
call_data.chg_first,
call_data.chg_length
);
return;
}
// NULL indicate text deletion
let mut new_chars = if call_data.text.is_null() {
Vec::new()
} else {
let xim_text = unsafe { &mut *(call_data.text) };
if xim_text.encoding_is_wchar > 0 {
return;
}
let new_text = unsafe { xim_text.string.multi_byte };
if new_text.is_null() {
return;
}
let new_text = unsafe { CStr::from_ptr(new_text) };
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
.chars()
.collect()
};
let mut old_text_tail = client_data.text.split_off(chg_range.end);
client_data.text.truncate(chg_range.start);
client_data.text.append(&mut new_chars);
client_data.text.append(&mut old_text_tail);
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
/// Handling of cursor movements in preedit text.
extern "C" fn preedit_caret_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
client_data.cursor_pos = call_data.position as usize;
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
}
/// Struct to simplify callback creation and latter passing into Xlib XIM.
struct PreeditCallbacks {
start_callback: ffi::XIMCallback,
done_callback: ffi::XIMCallback,
draw_callback: ffi::XIMCallback,
caret_callback: ffi::XIMCallback,
}
impl PreeditCallbacks {
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
let start_callback = create_xim_callback(client_data, unsafe {
mem::transmute(preedit_start_callback as usize)
});
let done_callback = create_xim_callback(client_data, preedit_done_callback);
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
PreeditCallbacks {
start_callback,
done_callback,
caret_callback,
draw_callback,
}
}
}
struct ImeContextClientData {
window: ffi::Window,
event_sender: ImeEventSender,
text: Vec<char>,
cursor_pos: usize,
}
// XXX: this struct doesn't destroy its XIC resource when dropped.
// This is intentional, as it doesn't have enough information to know whether or not the context
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
// through `ImeInner`.
pub struct ImeContext {
pub(crate) ic: ffi::XIC,
pub(crate) ic_spot: ffi::XPoint,
pub(crate) style: Style,
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
// there we keep the pointer to automatically deallocate it.
_client_data: Box<ImeContextClientData>,
}
impl ImeContext {
pub(crate) unsafe fn new(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: Style,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
event_sender: ImeEventSender,
) -> Result<Self, ImeContextCreationError> {
let client_data = Box::into_raw(Box::new(ImeContextClientData {
window,
event_sender,
text: Vec::new(),
cursor_pos: 0,
}));
let ic = match style as _ {
Style::Preedit(style) => unsafe {
ImeContext::create_preedit_ic(
xconn,
im,
style,
window,
client_data as ffi::XPointer,
)
},
Style::Nothing(style) => unsafe {
ImeContext::create_nothing_ic(xconn, im, style, window)
},
Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) },
}
.ok_or(ImeContextCreationError::Null)?;
xconn
.check_errors()
.map_err(ImeContextCreationError::XError)?;
let mut context = ImeContext {
ic,
ic_spot: ffi::XPoint { x: 0, y: 0 },
style,
_client_data: unsafe { Box::from_raw(client_data) },
};
// Set the spot location, if it's present.
if let Some(ic_spot) = ic_spot {
context.set_spot(xconn, ic_spot.x, ic_spot.y)
}
Ok(context)
}
unsafe fn create_none_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
unsafe fn create_preedit_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
client_data: ffi::XPointer,
) -> Option<ffi::XIC> {
let preedit_callbacks = PreeditCallbacks::new(client_data);
let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe {
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
&(preedit_callbacks.start_callback) as *const _,
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
&(preedit_callbacks.done_callback) as *const _,
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
&(preedit_callbacks.caret_callback) as *const _,
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
&(preedit_callbacks.draw_callback) as *const _,
ptr::null_mut::<()>(),
)
})
.expect("XVaCreateNestedList returned NULL");
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
preedit_attr.ptr,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
unsafe fn create_nothing_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
pub(crate) fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XSetICFocus)(self.ic);
}
xconn.check_errors()
}
pub(crate) fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XUnsetICFocus)(self.ic);
}
xconn.check_errors()
}
pub fn is_allowed(&self) -> bool {
!matches!(self.style, Style::None(_))
}
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
// are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
// window and couldn't be changed.
//
// For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
pub(crate) fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y {
return;
}
self.ic_spot = ffi::XPoint { x, y };
unsafe {
let preedit_attr = util::memory::XSmartPointer::new(
xconn,
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNSpotLocation_0.as_ptr(),
&self.ic_spot,
ptr::null_mut::<()>(),
),
)
.expect("XVaCreateNestedList returned NULL");
(xconn.xlib.XSetICValues)(
self.ic,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
preedit_attr.ptr,
ptr::null_mut::<()>(),
);
}
}
}

View File

@@ -0,0 +1,75 @@
use std::{collections::HashMap, mem, sync::Arc};
use super::{ffi, XConnection, XError};
use super::{
context::ImeContext,
input_method::{InputMethod, PotentialInputMethods},
};
use crate::platform_impl::platform::x11::ime::ImeEventSender;
pub(crate) unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
unsafe { (xconn.xlib.XCloseIM)(im) };
xconn.check_errors()
}
pub(crate) unsafe fn destroy_ic(xconn: &Arc<XConnection>, ic: ffi::XIC) -> Result<(), XError> {
unsafe { (xconn.xlib.XDestroyIC)(ic) };
xconn.check_errors()
}
pub(crate) struct ImeInner {
pub xconn: Arc<XConnection>,
pub im: Option<InputMethod>,
pub potential_input_methods: PotentialInputMethods,
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
// WARNING: this is initially zeroed!
pub destroy_callback: ffi::XIMCallback,
pub event_sender: ImeEventSender,
// Indicates whether or not the the input method was destroyed on the server end
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
pub is_destroyed: bool,
pub is_fallback: bool,
}
impl ImeInner {
pub(crate) fn new(
xconn: Arc<XConnection>,
potential_input_methods: PotentialInputMethods,
event_sender: ImeEventSender,
) -> Self {
ImeInner {
xconn,
im: None,
potential_input_methods,
contexts: HashMap::new(),
destroy_callback: unsafe { mem::zeroed() },
event_sender,
is_destroyed: false,
is_fallback: false,
}
}
pub unsafe fn close_im_if_necessary(&self) -> Result<bool, XError> {
if !self.is_destroyed && self.im.is_some() {
unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result<bool, XError> {
if !self.is_destroyed {
unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result<bool, XError> {
for context in self.contexts.values().flatten() {
unsafe { self.destroy_ic_if_necessary(context.ic)? };
}
Ok(!self.is_destroyed)
}
}

View File

@@ -0,0 +1,370 @@
use std::{
env,
ffi::{CStr, CString, IntoStringError},
fmt,
os::raw::{c_char, c_ulong, c_ushort},
ptr,
sync::{Arc, Mutex},
};
use super::{super::atoms::*, ffi, util, XConnection, XError};
use once_cell::sync::Lazy;
use x11rb::protocol::xproto;
static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(Default::default);
unsafe fn open_im(xconn: &Arc<XConnection>, locale_modifiers: &CStr) -> Option<ffi::XIM> {
let _lock = GLOBAL_LOCK.lock();
// XSetLocaleModifiers returns...
// * The current locale modifiers if it's given a NULL pointer.
// * The new locale modifiers if we succeeded in setting them.
// * NULL if the locale modifiers string is malformed or if the
// current locale is not supported by Xlib.
unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) };
let im = unsafe {
(xconn.xlib.XOpenIM)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if im.is_null() {
None
} else {
Some(im)
}
}
#[derive(Debug)]
pub struct InputMethod {
pub im: ffi::XIM,
pub preedit_style: Style,
pub none_style: Style,
_name: String,
}
impl InputMethod {
fn new(xconn: &Arc<XConnection>, im: ffi::XIM, name: String) -> Option<Self> {
let mut styles: *mut XIMStyles = std::ptr::null_mut();
// Query the styles supported by the XIM.
unsafe {
if !(xconn.xlib.XGetIMValues)(
im,
ffi::XNQueryInputStyle_0.as_ptr() as *const _,
(&mut styles) as *mut _,
std::ptr::null_mut::<()>(),
)
.is_null()
{
return None;
}
}
let mut preedit_style = None;
let mut none_style = None;
unsafe {
std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _)
.iter()
.for_each(|style| match *style {
XIM_PREEDIT_STYLE => {
preedit_style = Some(Style::Preedit(*style));
}
XIM_NOTHING_STYLE if preedit_style.is_none() => {
preedit_style = Some(Style::Nothing(*style))
}
XIM_NONE_STYLE => none_style = Some(Style::None(*style)),
_ => (),
});
(xconn.xlib.XFree)(styles.cast());
};
if preedit_style.is_none() && none_style.is_none() {
return None;
}
let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap());
let none_style = none_style.unwrap_or(preedit_style);
Some(InputMethod {
im,
_name: name,
preedit_style,
none_style,
})
}
}
const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle;
/// Style of the IME context.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style {
/// Preedit callbacks.
Preedit(XIMStyle),
/// Nothing.
Nothing(XIMStyle),
/// No IME.
None(XIMStyle),
}
impl Default for Style {
fn default() -> Self {
Style::None(XIM_NONE_STYLE)
}
}
#[repr(C)]
#[derive(Debug)]
struct XIMStyles {
count_styles: c_ushort,
supported_styles: *const XIMStyle,
}
pub(crate) type XIMStyle = c_ulong;
#[derive(Debug)]
pub enum InputMethodResult {
/// Input method used locale modifier from `XMODIFIERS` environment variable.
XModifiers(InputMethod),
/// Input method used internal fallback locale modifier.
Fallback(InputMethod),
/// Input method could not be opened using any locale modifier tried.
Failure,
}
impl InputMethodResult {
pub fn is_fallback(&self) -> bool {
matches!(self, InputMethodResult::Fallback(_))
}
pub fn ok(self) -> Option<InputMethod> {
use self::InputMethodResult::*;
match self {
XModifiers(im) | Fallback(im) => Some(im),
Failure => None,
}
}
}
#[derive(Debug, Clone)]
enum GetXimServersError {
XError(XError),
GetPropertyError(util::GetPropertyError),
InvalidUtf8(IntoStringError),
}
impl From<util::GetPropertyError> for GetXimServersError {
fn from(error: util::GetPropertyError) -> Self {
GetXimServersError::GetPropertyError(error)
}
}
// The root window has a property named XIM_SERVERS, which contains a list of atoms represeting
// the availabile XIM servers. For instance, if you're using ibus, it would contain an atom named
// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
// XMODIFIERS to `@server=ibus`?!?"
unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
let atoms = xconn.atoms();
let servers_atom = atoms[XIM_SERVERS];
let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) };
let mut atoms: Vec<ffi::Atom> = xconn
.get_property::<xproto::Atom>(
root as xproto::Window,
servers_atom,
xproto::Atom::from(xproto::AtomEnum::ATOM),
)
.map_err(GetXimServersError::GetPropertyError)?
.into_iter()
.map(ffi::Atom::from)
.collect::<Vec<_>>();
let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
unsafe {
(xconn.xlib.XGetAtomNames)(
xconn.display,
atoms.as_mut_ptr(),
atoms.len() as _,
names.as_mut_ptr() as _,
)
};
unsafe { names.set_len(atoms.len()) };
let mut formatted_names = Vec::with_capacity(names.len());
for name in names {
let string = unsafe { CStr::from_ptr(name) }
.to_owned()
.into_string()
.map_err(GetXimServersError::InvalidUtf8)?;
unsafe { (xconn.xlib.XFree)(name as _) };
formatted_names.push(string.replace("@server=", "@im="));
}
xconn.check_errors().map_err(GetXimServersError::XError)?;
Ok(formatted_names)
}
#[derive(Clone)]
struct InputMethodName {
c_string: CString,
string: String,
}
impl InputMethodName {
pub fn from_string(string: String) -> Self {
let c_string = CString::new(string.clone())
.expect("String used to construct CString contained null byte");
InputMethodName { c_string, string }
}
pub fn from_str(string: &str) -> Self {
let c_string =
CString::new(string).expect("String used to construct CString contained null byte");
InputMethodName {
c_string,
string: string.to_owned(),
}
}
}
impl fmt::Debug for InputMethodName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.string.fmt(f)
}
}
#[derive(Debug, Clone)]
struct PotentialInputMethod {
name: InputMethodName,
successful: Option<bool>,
}
impl PotentialInputMethod {
pub fn from_string(string: String) -> Self {
PotentialInputMethod {
name: InputMethodName::from_string(string),
successful: None,
}
}
pub fn from_str(string: &str) -> Self {
PotentialInputMethod {
name: InputMethodName::from_str(string),
successful: None,
}
}
pub fn reset(&mut self) {
self.successful = None;
}
pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
let im = unsafe { open_im(xconn, &self.name.c_string) };
self.successful = Some(im.is_some());
im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone()))
}
}
// By logging this struct, you get a sequential listing of every locale modifier tried, where it
// came from, and if it succeeded.
#[derive(Debug, Clone)]
pub(crate) struct PotentialInputMethods {
// On correctly configured systems, the XMODIFIERS environment variable tells us everything we
// need to know.
xmodifiers: Option<PotentialInputMethod>,
// We have some standard options at our disposal that should ostensibly always work. For users
// who only need compose sequences, this ensures that the program launches without a hitch
// For users who need more sophisticated IME features, this is more or less a silent failure.
// Logging features should be added in the future to allow both audiences to be effectively
// served.
fallbacks: [PotentialInputMethod; 2],
// For diagnostic purposes, we include the list of XIM servers that the server reports as
// being available.
_xim_servers: Result<Vec<String>, GetXimServersError>,
}
impl PotentialInputMethods {
pub fn new(xconn: &Arc<XConnection>) -> Self {
let xmodifiers = env::var("XMODIFIERS")
.ok()
.map(PotentialInputMethod::from_string);
PotentialInputMethods {
// Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
// XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
// running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
// defined in the profile (or parent environment) then that parent XMODIFIERS is used.
// If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
// XSetLocaleModifiers uses the default local input method. Note that defining
// XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
// that case, we get `None` and end up skipping ahead to the next method.
xmodifiers,
fallbacks: [
// This is a standard input method that supports compose sequences, which should
// always be available. `@im=none` appears to mean the same thing.
PotentialInputMethod::from_str("@im=local"),
// This explicitly specifies to use the implementation-dependent default, though
// that seems to be equivalent to just using the local input method.
PotentialInputMethod::from_str("@im="),
],
// The XIM_SERVERS property can have surprising values. For instance, when I exited
// ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
// that the fcitx input method could only be successfully opened using "@im=ibus".
// Presumably due to this quirk, it's actually possible to alternate between ibus and
// fcitx in a running application.
_xim_servers: unsafe { get_xim_servers(xconn) },
}
}
// This resets the `successful` field of every potential input method, ensuring we have
// accurate information when this struct is re-used by the destruction/instantiation callbacks.
fn reset(&mut self) {
if let Some(ref mut input_method) = self.xmodifiers {
input_method.reset();
}
for input_method in &mut self.fallbacks {
input_method.reset();
}
}
pub fn open_im(
&mut self,
xconn: &Arc<XConnection>,
callback: Option<&dyn Fn()>,
) -> InputMethodResult {
use self::InputMethodResult::*;
self.reset();
if let Some(ref mut input_method) = self.xmodifiers {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return XModifiers(im);
} else if let Some(ref callback) = callback {
callback();
}
}
for input_method in &mut self.fallbacks {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return Fallback(im);
}
}
Failure
}
}

View File

@@ -0,0 +1,249 @@
// Important: all XIM calls need to happen from the same thread!
mod callbacks;
mod context;
mod inner;
mod input_method;
use std::sync::{
mpsc::{Receiver, Sender},
Arc,
};
use super::{ffi, util, XConnection, XError};
pub use self::context::ImeContextCreationError;
use self::{
callbacks::*,
context::ImeContext,
inner::{close_im, ImeInner},
input_method::{PotentialInputMethods, Style},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ImeEvent {
Enabled,
Start,
Update(String, usize),
End,
Disabled,
}
pub type ImeReceiver = Receiver<ImeRequest>;
pub type ImeSender = Sender<ImeRequest>;
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
/// Request to control XIM handler from the window.
pub enum ImeRequest {
/// Set IME spot position for given `window_id`.
Position(ffi::Window, i16, i16),
/// Allow IME input for the given `window_id`.
Allow(ffi::Window, bool),
}
#[derive(Debug)]
pub(crate) enum ImeCreationError {
// Boxed to prevent large error type
OpenFailure(Box<PotentialInputMethods>),
SetDestroyCallbackFailed(XError),
}
pub(crate) struct Ime {
xconn: Arc<XConnection>,
// The actual meat of this struct is boxed away, since it needs to have a fixed location in
// memory so we can pass a pointer to it around.
inner: Box<ImeInner>,
}
impl Ime {
pub fn new(
xconn: Arc<XConnection>,
event_sender: ImeEventSender,
) -> Result<Self, ImeCreationError> {
let potential_input_methods = PotentialInputMethods::new(&xconn);
let (mut inner, client_data) = {
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
let inner_ptr = Box::into_raw(inner);
let client_data = inner_ptr as _;
let destroy_callback = ffi::XIMCallback {
client_data,
callback: Some(xim_destroy_callback),
};
inner = unsafe { Box::from_raw(inner_ptr) };
inner.destroy_callback = destroy_callback;
(inner, client_data)
};
let xconn = Arc::clone(&inner.xconn);
let input_method = inner.potential_input_methods.open_im(
&xconn,
Some(&|| {
let _ = unsafe { set_instantiate_callback(&xconn, client_data) };
}),
);
let is_fallback = input_method.is_fallback();
if let Some(input_method) = input_method.ok() {
inner.is_fallback = is_fallback;
unsafe {
let result = set_destroy_callback(&xconn, input_method.im, &inner)
.map_err(ImeCreationError::SetDestroyCallbackFailed);
if result.is_err() {
let _ = close_im(&xconn, input_method.im);
}
result?;
}
inner.im = Some(input_method);
Ok(Ime { xconn, inner })
} else {
Err(ImeCreationError::OpenFailure(Box::new(
inner.potential_input_methods,
)))
}
}
pub fn is_destroyed(&self) -> bool {
self.inner.is_destroyed
}
// This pattern is used for various methods here:
// Ok(_) indicates that nothing went wrong internally
// Ok(true) indicates that the action was actually performed
// Ok(false) indicates that the action is not presently applicable
pub fn create_context(
&mut self,
window: ffi::Window,
with_preedit: bool,
) -> Result<bool, ImeContextCreationError> {
let context = if self.is_destroyed() {
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
None
} else {
let im = self.inner.im.as_ref().unwrap();
let style = if with_preedit {
im.preedit_style
} else {
im.none_style
};
let context = unsafe {
ImeContext::new(
&self.inner.xconn,
im.im,
style,
window,
None,
self.inner.event_sender.clone(),
)?
};
// Check the state on the context, since it could fail to enable or disable preedit.
let event = if matches!(style, Style::None(_)) {
if with_preedit {
debug!("failed to create IME context with preedit support.")
}
ImeEvent::Disabled
} else {
if !with_preedit {
debug!("failed to create IME context without preedit support.")
}
ImeEvent::Enabled
};
self.inner
.event_sender
.send((window, event))
.expect("Failed to send enabled event");
Some(context)
};
self.inner.contexts.insert(window, context);
Ok(!self.is_destroyed())
}
pub fn get_context(&self, window: ffi::Window) -> Option<ffi::XIC> {
if self.is_destroyed() {
return None;
}
if let Some(Some(context)) = self.inner.contexts.get(&window) {
Some(context.ic)
} else {
None
}
}
pub fn remove_context(&mut self, window: ffi::Window) -> Result<bool, XError> {
if let Some(Some(context)) = self.inner.contexts.remove(&window) {
unsafe {
self.inner.destroy_ic_if_necessary(context.ic)?;
}
Ok(true)
} else {
Ok(false)
}
}
pub fn focus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.focus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn unfocus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.unfocus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.set_spot(&self.xconn, x as _, y as _);
}
}
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
if allowed == context.is_allowed() {
return;
}
}
// Remove context for that window.
let _ = self.remove_context(window);
// Create new context supporting IME input.
let _ = self.create_context(window, allowed);
}
}
impl Drop for Ime {
fn drop(&mut self) {
unsafe {
let _ = self.inner.destroy_all_contexts_if_necessary();
let _ = self.inner.close_im_if_necessary();
}
}
}

View File

@@ -28,11 +28,13 @@ use std::{
collections::{HashMap, HashSet},
ffi::CStr,
fmt,
mem::MaybeUninit,
ops::Deref,
os::{
raw::*,
unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd},
},
ptr,
rc::Rc,
slice, str,
sync::mpsc::{Receiver, Sender, TryRecvError},
@@ -40,14 +42,19 @@ use std::{
time::{Duration, Instant},
};
use libc::{self, setlocale, LC_CTYPE};
use atoms::*;
use x11rb::protocol::{
xinput::{self, ConnectionExt as _},
xkb,
xproto::{self, ConnectionExt as _},
};
use x11rb::x11_utils::X11Error as LogicalError;
use x11rb::{
connection::RequestConnection,
protocol::{
xinput::{self, ConnectionExt as _},
xkb,
xproto::{self, ConnectionExt as _},
},
};
use x11rb::{
errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError},
xcb_ffi::ReplyOrIdError,
@@ -56,6 +63,7 @@ use x11rb::{
use self::{
dnd::{Dnd, DndState},
event_processor::EventProcessor,
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
};
use super::{common::xkb_state::KbdState, ControlFlow, OsError};
use crate::{
@@ -139,18 +147,15 @@ pub struct EventLoopWindowTarget<T> {
xconn: Arc<XConnection>,
wm_delete_window: xproto::Atom,
net_wm_ping: xproto::Atom,
ime_sender: mpsc::Sender<ime::ImeRequest>,
ime_sender: ImeSender,
control_flow: Cell<ControlFlow>,
exit: Cell<Option<i32>>,
root: xproto::Window,
ime: RefCell<Ime>,
windows: RefCell<HashMap<WindowId, Weak<UnownedWindow>>>,
redraw_sender: WakeSender<WindowId>,
activation_sender: WakeSender<ActivationToken>,
device_events: Cell<DeviceEvents>,
/// State of IME.
ime: Option<RefCell<ime::ImeData>>,
_marker: ::std::marker::PhantomData<T>,
}
@@ -196,26 +201,57 @@ impl<T: 'static> EventLoop<T> {
let wm_delete_window = atoms[WM_DELETE_WINDOW];
let net_wm_ping = atoms[_NET_WM_PING];
// Create an event queue.
let event_queue = Rc::new(RefCell::new(std::collections::VecDeque::with_capacity(4)));
let dnd = Dnd::new(Arc::clone(&xconn))
.expect("Failed to call XInternAtoms when initializing drag and drop");
let (ime_sender, ime_receiver) = mpsc::channel();
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
// Input methods will open successfully without setting the locale, but it won't be
// possible to actually commit pre-edit sequences.
unsafe {
// Remember default locale to restore it if target locale is unsupported
// by Xlib
let default_locale = setlocale(LC_CTYPE, ptr::null());
setlocale(LC_CTYPE, b"\0".as_ptr() as *const _);
let ime = match ime::ImeData::new(&xconn, xconn.default_screen_index(), &event_queue) {
Ok(ime) => Some(ime),
Err(e) => {
log::error!("Failed to open IME: {e}");
None
// Check if set locale is supported by Xlib.
// If not, calls to some Xlib functions like `XSetLocaleModifiers`
// will fail.
let locale_supported = (xconn.xlib.XSupportsLocale)() == 1;
if !locale_supported {
let unsupported_locale = setlocale(LC_CTYPE, ptr::null());
warn!(
"Unsupported locale \"{}\". Restoring default locale \"{}\".",
CStr::from_ptr(unsupported_locale).to_string_lossy(),
CStr::from_ptr(default_locale).to_string_lossy()
);
// Restore default locale
setlocale(LC_CTYPE, default_locale);
}
};
}
let ime = RefCell::new({
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
panic!("Failed to open input method: {state:#?}");
}
result.expect("Failed to set input method destruction callback")
});
xconn
let randr_event_offset = xconn
.select_xrandr_input(root)
.expect("Failed to query XRandR extension");
let xi2ext = xconn
.xcb_connection()
.extension_information(xinput::X11_EXTENSION_NAME)
.expect("Failed to query XInput extension")
.expect("X server missing XInput extension");
let xkbext = xconn
.xcb_connection()
.extension_information(xkb::X11_EXTENSION_NAME)
.expect("Failed to query XKB extension")
.expect("X server missing XKB extension");
// Check for XInput2 support.
xconn
.xcb_connection()
@@ -267,12 +303,12 @@ impl<T: 'static> EventLoop<T> {
KbdState::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap();
let window_target = EventLoopWindowTarget {
ime,
root,
control_flow: Cell::new(ControlFlow::default()),
exit: Cell::new(None),
windows: Default::default(),
_marker: ::std::marker::PhantomData,
ime: ime.map(RefCell::new),
ime_sender,
xconn,
wm_delete_window,
@@ -297,11 +333,14 @@ impl<T: 'static> EventLoop<T> {
});
let event_processor = EventProcessor {
event_queue,
target: target.clone(),
dnd,
devices: Default::default(),
ime_requests: ime_receiver,
randr_event_offset,
ime_receiver,
ime_event_receiver,
xi2ext,
xkbext,
kb_state,
num_touch: 0,
held_key_press: None,
@@ -584,10 +623,12 @@ impl<T: 'static> EventLoop<T> {
F: FnMut(Event<T>, &RootELW<T>),
{
let target = &self.target;
let mut xev = MaybeUninit::uninit();
let wt = get_xtarget(&self.target);
while let Some(event) = self.event_processor.poll_one_event() {
self.event_processor.process_event(event, |event| {
while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } {
let mut xev = unsafe { xev.assume_init() };
self.event_processor.process_event(&mut xev, |event| {
if let Event::WindowEvent {
window_id: crate::window::WindowId(wid),
event: WindowEvent::RedrawRequested,
@@ -711,6 +752,10 @@ impl<T> EventLoopWindowTarget<T> {
self.exit.set(Some(0))
}
pub(crate) fn clear_exit(&self) {
self.exit.set(None)
}
pub(crate) fn exiting(&self) -> bool {
self.exit.get().is_some()
}
@@ -839,11 +884,8 @@ pub enum X11Error {
/// The XID range has been exhausted.
XidsExhausted(IdsExhausted),
/// An IME client error occurred.
Ime(xim::ClientError),
/// The IME client has entered an invalid state.
InvalidImeState(ime::InvalidImeState),
/// Got `null` from an Xlib function without a reason.
UnexpectedNull(&'static str),
/// Got an invalid activation token.
InvalidActivationToken(Vec<u8>),
@@ -862,9 +904,8 @@ impl fmt::Display for X11Error {
X11Error::Connect(e) => write!(f, "X11 connection error: {}", e),
X11Error::Connection(e) => write!(f, "X11 connection error: {}", e),
X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e),
X11Error::Ime(e) => write!(f, "An IME error occurred: {}", e),
X11Error::InvalidImeState(e) => write!(f, "Invalid IME state: {}", e),
X11Error::X11(e) => write!(f, "X11 error: {:?}", e),
X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s),
X11Error::InvalidActivationToken(s) => write!(
f,
"Invalid activation token: {}",
@@ -927,6 +968,15 @@ impl From<ReplyError> for X11Error {
}
}
impl From<ime::ImeContextCreationError> for X11Error {
fn from(value: ime::ImeContextCreationError) -> Self {
match value {
ime::ImeContextCreationError::XError(e) => e.into(),
ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"),
}
}
}
impl From<ReplyOrIdError> for X11Error {
fn from(value: ReplyOrIdError) -> Self {
match value {
@@ -937,18 +987,6 @@ impl From<ReplyOrIdError> for X11Error {
}
}
impl From<xim::ClientError> for X11Error {
fn from(e: xim::ClientError) -> Self {
match e {
xim::ClientError::Other(other) => match other.downcast::<X11Error>() {
Ok(x11) => *x11,
Err(other) => X11Error::Ime(xim::ClientError::Other(other)),
},
e => X11Error::Ime(e),
}
}
}
/// The underlying x11rb connection that we are using.
type X11rbConnection = x11rb::xcb_ffi::XCBConnection;
@@ -967,6 +1005,34 @@ impl<'a, E: fmt::Debug> CookieResultExt for Result<VoidCookie<'a>, E> {
}
}
/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. This is a wrapper to
/// extract the cookie from a GenericEvent XEvent and release the cookie data once it has been processed
struct GenericEventCookie<'a> {
xconn: &'a XConnection,
cookie: ffi::XGenericEventCookie,
}
impl<'a> GenericEventCookie<'a> {
fn from_event(xconn: &XConnection, event: ffi::XEvent) -> Option<GenericEventCookie<'_>> {
unsafe {
let mut cookie: ffi::XGenericEventCookie = From::from(event);
if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == ffi::True {
Some(GenericEventCookie { xconn, cookie })
} else {
None
}
}
}
}
impl<'a> Drop for GenericEventCookie<'a> {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie);
}
}
}
fn mkwid(w: xproto::Window) -> crate::window::WindowId {
crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _))
}
@@ -1068,15 +1134,8 @@ impl Device {
}
}
/// Convert the raw X11 representation for a 32-bit fixed point to a double.
/// Convert the raw X11 representation for a 32-bit floating point to a double.
#[inline]
fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 {
(fp as f64) / ((1 << 16) as f64)
}
/// Conver the raw X11 representation for a 64-bit fixed point number to a double.
#[inline]
fn xinput_fp3232_to_float(fp: xinput::Fp3232) -> f64 {
let xinput::Fp3232 { integral, frac } = fp;
integral as f64 + (frac as f64 / (1u64 << 32) as f64)
}

View File

@@ -284,22 +284,16 @@ impl XConnection {
pub fn available_monitors(&self) -> Result<Vec<MonitorHandle>, X11Error> {
let mut monitors_lock = self.monitor_handles.lock().unwrap();
(*monitors_lock)
.as_ref()
.cloned()
.map(Ok)
.or_else(|| {
self.query_monitor_list()
.map(|mon_list| {
let monitors = Some(mon_list);
if !DISABLE_MONITOR_LIST_CACHING {
(*monitors_lock) = monitors.clone();
}
monitors
})
.transpose()
})
.unwrap()
match *monitors_lock {
Some(ref monitors) => Ok(monitors.clone()),
None => {
let monitors = self.query_monitor_list()?;
if !DISABLE_MONITOR_LIST_CACHING {
*monitors_lock = Some(monitors.clone());
}
Ok(monitors)
}
}
}
#[inline]

View File

@@ -1,8 +1,9 @@
use std::{ffi::CString, iter, slice, sync::Arc};
use std::ffi::CString;
use std::iter;
use x11rb::connection::Connection;
use crate::{cursor::CursorImage, window::CursorIcon};
use crate::window::CursorIcon;
use super::*;
@@ -19,11 +20,6 @@ impl XConnection {
.expect("Failed to set cursor");
}
pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) {
self.update_cursor(window, cursor.inner.cursor)
.expect("Failed to set cursor");
}
fn create_empty_cursor(&self) -> ffi::Cursor {
let data = 0;
let pixmap = unsafe {
@@ -91,74 +87,3 @@ impl XConnection {
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectedCursor {
Custom(CustomCursor),
Named(CursorIcon),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCursor {
inner: Arc<CustomCursorInner>,
}
impl CustomCursor {
pub(crate) unsafe fn new(xconn: &Arc<XConnection>, image: &CursorImage) -> Self {
unsafe {
let ximage =
(xconn.xcursor.XcursorImageCreate)(image.width as i32, image.height as i32);
if ximage.is_null() {
panic!("failed to allocate cursor image");
}
(*ximage).xhot = image.hotspot_x as u32;
(*ximage).yhot = image.hotspot_y as u32;
(*ximage).delay = 0;
let dst = slice::from_raw_parts_mut((*ximage).pixels, image.rgba.len() / 4);
for (dst, chunk) in dst.iter_mut().zip(image.rgba.chunks_exact(4)) {
*dst = (chunk[0] as u32) << 16
| (chunk[1] as u32) << 8
| (chunk[2] as u32)
| (chunk[3] as u32) << 24;
}
let cursor = (xconn.xcursor.XcursorImageLoadCursor)(xconn.display, ximage);
(xconn.xcursor.XcursorImageDestroy)(ximage);
Self {
inner: Arc::new(CustomCursorInner {
xconn: xconn.clone(),
cursor,
}),
}
}
}
}
#[derive(Debug)]
struct CustomCursorInner {
xconn: Arc<XConnection>,
cursor: ffi::Cursor,
}
impl Drop for CustomCursorInner {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor);
}
}
}
impl PartialEq for CustomCursorInner {
fn eq(&self, other: &Self) -> bool {
self.cursor == other.cursor
}
}
impl Eq for CustomCursorInner {}
impl Default for SelectedCursor {
fn default() -> Self {
SelectedCursor::Named(Default::default())
}
}

View File

@@ -1,3 +1,4 @@
use std::{slice, str};
use x11rb::protocol::{
xinput::{self, ConnectionExt as _},
xkb,
@@ -8,6 +9,11 @@ use super::*;
pub const VIRTUAL_CORE_POINTER: u16 = 2;
pub const VIRTUAL_CORE_KEYBOARD: u16 = 3;
// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to
// re-allocate (and make another round-trip) in the *vast* majority of cases.
// To test if `lookup_utf8` works correctly, set this to 1.
const TEXT_BUFFER_SIZE: usize = 1024;
impl XConnection {
pub fn select_xinput_events(
&self,
@@ -54,4 +60,52 @@ impl XConnection {
.reply()
.map_err(Into::into)
}
fn lookup_utf8_inner(
&self,
ic: ffi::XIC,
key_event: &mut ffi::XKeyEvent,
buffer: *mut u8,
size: usize,
) -> (ffi::KeySym, ffi::Status, c_int) {
let mut keysym: ffi::KeySym = 0;
let mut status: ffi::Status = 0;
let count = unsafe {
(self.xlib.Xutf8LookupString)(
ic,
key_event,
buffer as *mut c_char,
size as c_int,
&mut keysym,
&mut status,
)
};
(keysym, status, count)
}
pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String {
// `assume_init` is safe here because the array consists of `MaybeUninit` values,
// which do not require initialization.
let mut buffer: [MaybeUninit<u8>; TEXT_BUFFER_SIZE] =
unsafe { MaybeUninit::uninit().assume_init() };
// If the buffer overflows, we'll make a new one on the heap.
let mut vec;
let (_, status, count) =
self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len());
let bytes = if status == ffi::XBufferOverflow {
vec = Vec::with_capacity(count as usize);
let (_, _, new_count) =
self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity());
debug_assert_eq!(count, new_count);
unsafe { vec.set_len(count as usize) };
&vec[..count as usize]
} else {
unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) }
};
str::from_utf8(bytes).unwrap_or("").to_string()
}
}

View File

@@ -7,6 +7,18 @@ pub(crate) struct XSmartPointer<'a, T> {
pub ptr: *mut T,
}
impl<'a, T> XSmartPointer<'a, T> {
// You're responsible for only passing things to this that should be XFree'd.
// Returns None if ptr is null.
pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option<Self> {
if !ptr.is_null() {
Some(XSmartPointer { xconn, ptr })
} else {
None
}
}
}
impl<'a, T> Deref for XSmartPointer<'a, T> {
type Target = T;

View File

@@ -13,10 +13,11 @@ mod randr;
mod window_property;
mod wm;
pub use self::{cursor::*, geometry::*, hint::*, input::*, window_property::*, wm::*};
pub use self::{geometry::*, hint::*, input::*, window_property::*, wm::*};
use std::{
mem::{self, MaybeUninit},
ops::BitAnd,
os::raw::*,
};
@@ -33,6 +34,13 @@ pub fn maybe_change<T: PartialEq>(field: &mut Option<T>, value: T) -> bool {
}
}
pub fn has_flag<T>(bitset: T, flag: T) -> bool
where
T: Copy + PartialEq + BitAnd<T, Output = T>,
{
bitset & flag == flag
}
impl XConnection {
// This is impoartant, so pay attention!
// Xlib has an output buffer, and tries to hide the async nature of X from you.

View File

@@ -4,15 +4,12 @@ use std::{
mem::replace,
os::raw::*,
path::Path,
sync::{mpsc, Arc, Mutex, MutexGuard},
sync::{Arc, Mutex, MutexGuard},
};
use crate::cursor::CustomCursor as RootCustomCursor;
use cursor_icon::CursorIcon;
use x11rb::{
connection::Connection,
properties::{WmHints, WmHintsState, WmSizeHints, WmSizeHintsSpecification},
properties::{WmHints, WmSizeHints, WmSizeHintsSpecification},
protocol::{
randr,
shape::SK,
@@ -36,16 +33,14 @@ use crate::{
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes,
WindowButtons, WindowLevel,
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
WindowAttributes, WindowButtons, WindowLevel,
},
};
use super::{
ffi,
ime::ImeRequest,
util::{self, CustomCursor, SelectedCursor},
CookieResultExt, EventLoopWindowTarget, VoidCookie, WindowId, XConnection,
ffi, util, CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId,
XConnection,
};
#[derive(Debug)]
@@ -131,11 +126,11 @@ pub(crate) struct UnownedWindow {
root: xproto::Window, // never changes
#[allow(dead_code)]
screen_id: i32, // never changes
selected_cursor: Mutex<SelectedCursor>,
cursor: Mutex<CursorIcon>,
cursor_grabbed_mode: Mutex<CursorGrabMode>,
#[allow(clippy::mutex_atomic)]
cursor_visible: Mutex<bool>,
ime_sender: Mutex<mpsc::Sender<ImeRequest>>,
ime_sender: Mutex<ImeSender>,
pub shared_state: Mutex<SharedState>,
redraw_sender: WakeSender<WindowId>,
activation_sender: WakeSender<super::ActivationToken>,
@@ -360,7 +355,7 @@ impl UnownedWindow {
visual,
root,
screen_id,
selected_cursor: Default::default(),
cursor: Default::default(),
cursor_grabbed_mode: Mutex::new(CursorGrabMode::None),
cursor_visible: Mutex::new(true),
ime_sender: Mutex::new(event_loop.ime_sender.clone()),
@@ -547,10 +542,11 @@ impl UnownedWindow {
.ignore_error();
{
if let Some(ime) = event_loop.ime.as_ref() {
let result = ime.borrow_mut().create_context(window.xwindow, false, None);
leap!(result);
}
let result = event_loop
.ime
.borrow_mut()
.create_context(window.xwindow as ffi::Window, false);
leap!(result);
}
// These properties must be set after mapping
@@ -991,7 +987,7 @@ impl UnownedWindow {
xproto::EventMask::SUBSTRUCTURE_REDIRECT
| xproto::EventMask::SUBSTRUCTURE_NOTIFY,
),
[WmHintsState::Iconic as u32, 0, 0, 0, 0],
[3u32, 0, 0, 0, 0],
)
} else {
self.xconn.send_client_msg(
@@ -1539,29 +1535,13 @@ impl UnownedWindow {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
let old_cursor = replace(
&mut *self.selected_cursor.lock().unwrap(),
SelectedCursor::Named(cursor),
);
let old_cursor = replace(&mut *self.cursor.lock().unwrap(), cursor);
#[allow(clippy::mutex_atomic)]
if SelectedCursor::Named(cursor) != old_cursor && *self.cursor_visible.lock().unwrap() {
if cursor != old_cursor && *self.cursor_visible.lock().unwrap() {
self.xconn.set_cursor_icon(self.xwindow, Some(cursor));
}
}
#[inline]
pub fn set_custom_cursor(&self, cursor: RootCustomCursor) {
let new_cursor = unsafe { CustomCursor::new(&self.xconn, &cursor.inner) };
#[allow(clippy::mutex_atomic)]
if *self.cursor_visible.lock().unwrap() {
self.xconn.set_custom_cursor(self.xwindow, &new_cursor);
}
*self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(new_cursor);
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap();
@@ -1648,23 +1628,13 @@ impl UnownedWindow {
return;
}
let cursor = if visible {
Some((*self.selected_cursor.lock().unwrap()).clone())
Some(*self.cursor.lock().unwrap())
} else {
None
};
*visible_lock = visible;
drop(visible_lock);
match cursor {
Some(SelectedCursor::Custom(cursor)) => {
self.xconn.set_custom_cursor(self.xwindow, &cursor);
}
Some(SelectedCursor::Named(cursor)) => {
self.xconn.set_cursor_icon(self.xwindow, Some(cursor));
}
None => {
self.xconn.set_cursor_icon(self.xwindow, None);
}
}
self.xconn.set_cursor_icon(self.xwindow, cursor);
}
#[inline]
@@ -1791,11 +1761,11 @@ impl UnownedWindow {
#[inline]
pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) {
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
let _ = self
.ime_sender
.lock()
.unwrap()
.send(ImeRequest::Position(self.xwindow, x, y));
let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position(
self.xwindow as ffi::Window,
x,
y,
));
}
#[inline]
@@ -1804,7 +1774,7 @@ impl UnownedWindow {
.ime_sender
.lock()
.unwrap()
.send(ImeRequest::Allow(self.xwindow, allowed));
.send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed));
}
#[inline]

View File

@@ -4,7 +4,7 @@ use std::{
fmt, ptr,
sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
Arc, Mutex, RwLock, RwLockReadGuard,
},
};
@@ -45,7 +45,7 @@ pub(crate) struct XConnection {
pub monitor_handles: Mutex<Option<Vec<MonitorHandle>>>,
/// The resource database.
database: resource_manager::Database,
database: RwLock<resource_manager::Database>,
pub latest_error: Mutex<Option<XError>>,
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, ffi::Cursor>>,
@@ -91,14 +91,6 @@ impl XConnection {
conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))?
};
// Make sure Xlib knows XCB is handling events.
unsafe {
(xlib_xcb.XSetEventQueueOwner)(
display,
x11_dl::xlib_xcb::XEventQueueOwner::XCBOwnsEventQueue,
);
}
// Get the default screen.
let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize;
@@ -123,7 +115,7 @@ impl XConnection {
timestamp: AtomicU32::new(0),
latest_error: Mutex::new(None),
monitor_handles: Mutex::new(None),
database,
database: RwLock::new(database),
cursor_cache: Default::default(),
})
}
@@ -167,8 +159,16 @@ impl XConnection {
/// Get the resource database.
#[inline]
pub fn database(&self) -> &resource_manager::Database {
&self.database
pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> {
self.database.read().unwrap_or_else(|e| e.into_inner())
}
/// Reload the resource database.
#[inline]
pub fn reload_database(&self) -> Result<(), super::X11Error> {
let database = resource_manager::new_from_default(self.xcb_connection())?;
*self.database.write().unwrap_or_else(|e| e.into_inner()) = database;
Ok(())
}
/// Get the latest timestamp.

View File

@@ -209,6 +209,10 @@ impl Handler {
self.exit.store(true, Ordering::Relaxed)
}
pub fn clear_exit(&self) {
self.exit.store(false, Ordering::Relaxed)
}
pub fn exiting(&self) -> bool {
self.exit.load(Ordering::Relaxed)
}
@@ -434,6 +438,10 @@ impl AppState {
HANDLER.exit()
}
pub fn clear_exit() {
HANDLER.clear_exit()
}
pub fn exiting() -> bool {
HANDLER.exiting()
}

View File

@@ -80,9 +80,6 @@ extern_methods!(
#[method(setMainMenu:)]
pub fn setMainMenu(&self, menu: &NSMenu);
#[method(setServicesMenu:)]
pub fn setServicesMenu(&self, menu: &NSMenu);
#[method_id(effectiveAppearance)]
pub fn effectiveAppearance(&self) -> Id<NSAppearance>;

View File

@@ -1,56 +0,0 @@
use std::ffi::c_uchar;
use icrate::Foundation::{NSInteger, NSObject, NSString};
use objc2::rc::Id;
use objc2::runtime::Bool;
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct NSImageRep;
unsafe impl ClassType for NSImageRep {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern "C" {
static NSDeviceRGBColorSpace: &'static NSString;
}
extern_class!(
// <https://developer.apple.com/documentation/appkit/nsbitmapimagerep?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSBitmapImageRep;
unsafe impl ClassType for NSBitmapImageRep {
type Super = NSImageRep;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSBitmapImageRep {
pub fn init_rgba(width: NSInteger, height: NSInteger) -> Id<Self> {
unsafe {
msg_send_id![Self::alloc(),
initWithBitmapDataPlanes: std::ptr::null_mut::<*mut c_uchar>(),
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8 as NSInteger,
samplesPerPixel: 4 as NSInteger,
hasAlpha: Bool::new(true),
isPlanar: Bool::new(false),
colorSpaceName: NSDeviceRGBColorSpace,
bytesPerRow: width * 4,
bitsPerPixel: 32 as NSInteger,
]
}
}
pub fn bitmap_data(&self) -> *mut u8 {
unsafe { msg_send![self, bitmapData] }
}
}
);

View File

@@ -2,14 +2,13 @@ use once_cell::sync::Lazy;
use icrate::ns_string;
use icrate::Foundation::{
NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, NSString,
NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSString,
};
use objc2::rc::{DefaultId, Id};
use objc2::runtime::Sel;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType};
use super::{NSBitmapImageRep, NSImage};
use crate::cursor::CursorImage;
use super::NSImage;
use crate::window::CursorIcon;
extern_class!(
@@ -233,23 +232,6 @@ impl NSCursor {
_ => Default::default(),
}
}
pub fn from_image(cursor: &CursorImage) -> Id<Self> {
let width = cursor.width;
let height = cursor.height;
let bitmap = NSBitmapImageRep::init_rgba(width as isize, height as isize);
let bitmap_data =
unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), cursor.rgba.len()) };
bitmap_data.copy_from_slice(&cursor.rgba);
let image = NSImage::init_with_size(NSSize::new(width.into(), height.into()));
image.add_representation(&bitmap);
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
NSCursor::new(&image, hotspot)
}
}
impl DefaultId for NSCursor {

View File

@@ -1,8 +1,6 @@
use icrate::Foundation::{NSData, NSObject, NSSize, NSString};
use icrate::Foundation::{NSData, NSObject, NSString};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType};
use super::NSBitmapImageRep;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
extern_class!(
// TODO: Can this be mutable?
@@ -34,13 +32,5 @@ extern_methods!(
pub fn new_with_data(data: &NSData) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithData: data] }
}
pub fn init_with_size(size: NSSize) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithSize: size] }
}
pub fn add_representation(&self, representation: &NSBitmapImageRep) {
unsafe { msg_send![self, addRepresentation: representation] }
}
}
);

View File

@@ -20,11 +20,7 @@ extern_methods!(
#[method_id(new)]
pub fn new() -> Id<Self>;
pub fn newWithTitle(
title: &NSString,
action: Option<Sel>,
key_equivalent: &NSString,
) -> Id<Self> {
pub fn newWithTitle(title: &NSString, action: Sel, key_equivalent: &NSString) -> Id<Self> {
unsafe {
msg_send_id![
Self::alloc(),

View File

@@ -13,7 +13,6 @@
mod appearance;
mod application;
mod bitmap_image_rep;
mod button;
mod color;
mod control;
@@ -37,7 +36,6 @@ pub(crate) use self::application::{
NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions,
NSRequestUserAttentionType,
};
pub(crate) use self::bitmap_image_rep::NSBitmapImageRep;
pub(crate) use self::button::NSButton;
pub(crate) use self::color::NSColor;
pub(crate) use self::control::NSControl;

View File

@@ -116,6 +116,10 @@ impl<T: 'static> EventLoopWindowTarget<T> {
AppState::exit()
}
pub(crate) fn clear_exit(&self) {
AppState::clear_exit()
}
pub(crate) fn exiting(&self) -> bool {
AppState::exiting()
}

View File

@@ -21,16 +21,7 @@ pub fn initialize() {
// About menu item
let about_item_title = ns_string!("About ").stringByAppendingString(&process_name);
let about_item = menu_item(
&about_item_title,
Some(sel!(orderFrontStandardAboutPanel:)),
None,
);
// Services menu item
let services_menu = NSMenu::new();
let services_item = menu_item(ns_string!("Services"), None, None);
services_item.setSubmenu(&services_menu);
let about_item = menu_item(&about_item_title, sel!(orderFrontStandardAboutPanel:), None);
// Seperator menu item
let sep_first = NSMenuItem::separatorItem();
@@ -39,7 +30,7 @@ pub fn initialize() {
let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name);
let hide_item = menu_item(
&hide_item_title,
Some(sel!(hide:)),
sel!(hide:),
Some(KeyEquivalent {
key: ns_string!("h"),
masks: None,
@@ -50,7 +41,7 @@ pub fn initialize() {
let hide_others_item_title = ns_string!("Hide Others");
let hide_others_item = menu_item(
hide_others_item_title,
Some(sel!(hideOtherApplications:)),
sel!(hideOtherApplications:),
Some(KeyEquivalent {
key: ns_string!("h"),
masks: Some(
@@ -61,11 +52,7 @@ pub fn initialize() {
// Show applications menu item
let show_all_item_title = ns_string!("Show All");
let show_all_item = menu_item(
show_all_item_title,
Some(sel!(unhideAllApplications:)),
None,
);
let show_all_item = menu_item(show_all_item_title, sel!(unhideAllApplications:), None);
// Seperator menu item
let sep = NSMenuItem::separatorItem();
@@ -74,7 +61,7 @@ pub fn initialize() {
let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name);
let quit_item = menu_item(
&quit_item_title,
Some(sel!(terminate:)),
sel!(terminate:),
Some(KeyEquivalent {
key: ns_string!("q"),
masks: None,
@@ -83,7 +70,6 @@ pub fn initialize() {
app_menu.addItem(&about_item);
app_menu.addItem(&sep_first);
app_menu.addItem(&services_item);
app_menu.addItem(&hide_item);
app_menu.addItem(&hide_others_item);
app_menu.addItem(&show_all_item);
@@ -92,13 +78,12 @@ pub fn initialize() {
app_menu_item.setSubmenu(&app_menu);
let app = NSApp();
app.setServicesMenu(&services_menu);
app.setMainMenu(&menubar);
}
fn menu_item(
title: &NSString,
selector: Option<Sel>,
selector: Sel,
key_equivalent: Option<KeyEquivalent<'_>>,
) -> Id<NSMenuItem> {
let (key, masks) = match key_equivalent {

View File

@@ -28,7 +28,6 @@ pub(crate) use self::{
use crate::event::DeviceId as RootDeviceId;
pub(crate) use self::window::Window;
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

View File

@@ -7,7 +7,6 @@ use std::os::raw::c_void;
use std::ptr::NonNull;
use std::sync::{Mutex, MutexGuard};
use crate::cursor::CustomCursor;
use crate::{
dpi::{
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical,
@@ -835,13 +834,6 @@ impl WinitWindow {
self.invalidateCursorRectsForView(&view);
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let view = self.view();
view.set_cursor_icon(NSCursor::from_image(&cursor.inner));
self.invalidateCursorRectsForView(&view);
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let associate_mouse_cursor = match mode {

View File

@@ -193,7 +193,6 @@ impl Display for OsError {
}
}
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]

View File

@@ -4,7 +4,6 @@ use std::{
};
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error,
platform_impl::Fullscreen,
@@ -353,8 +352,6 @@ impl Window {
#[inline]
pub fn set_cursor_icon(&self, _: window::CursorIcon) {}
pub fn set_custom_cursor(&self, _: CustomCursor) {}
#[inline]
pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> {
Err(error::ExternalError::NotSupported(

View File

@@ -1,364 +0,0 @@
use std::{
cell::{Cell, RefCell},
ops::Deref,
rc::{Rc, Weak},
};
use crate::cursor::{BadImage, CursorImage};
use cursor_icon::CursorIcon;
use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
Blob, Document, HtmlCanvasElement, ImageBitmap, ImageBitmapOptions,
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
};
use super::backend::Style;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebCustomCursor {
Image(CursorImage),
Url {
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
}
impl WebCustomCursor {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
Ok(Self::Image(CursorImage::from_rgba(
rgba, width, height, hotspot_x, hotspot_y,
)?))
}
pub(super) fn build(
&self,
window: &Window,
document: &Document,
style: &Style,
previous: SelectedCursor,
cursor_visible: Rc<Cell<bool>>,
) -> SelectedCursor {
let previous = previous.into();
match self {
WebCustomCursor::Image(image) => SelectedCursor::Image(CursorImageState::from_image(
window,
document.clone(),
style.clone(),
image,
previous,
cursor_visible,
)),
WebCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
} => {
let value = previous.style_with_url(url, *hotspot_x, *hotspot_y);
if cursor_visible.get() {
style.set("cursor", &value);
}
SelectedCursor::Url {
style: value,
previous,
url: url.clone(),
hotspot_x: *hotspot_x,
hotspot_y: *hotspot_y,
}
}
}
}
}
#[derive(Debug)]
pub enum SelectedCursor {
Named(CursorIcon),
Url {
style: String,
previous: Previous,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
Image(Rc<RefCell<Option<CursorImageState>>>),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
impl SelectedCursor {
pub fn set_style(&self, style: &Style) {
let value = match self {
SelectedCursor::Named(icon) => icon.name(),
SelectedCursor::Url { style, .. } => style,
SelectedCursor::Image(image) => {
let image = image.borrow();
let value = match image.deref().as_ref().unwrap() {
CursorImageState::Loading { previous, .. } => previous.style(),
CursorImageState::Failed(previous) => previous.style(),
CursorImageState::Ready { style, .. } => style,
};
return style.set("cursor", value);
}
};
style.set("cursor", value);
}
}
#[derive(Debug)]
pub enum Previous {
Named(CursorIcon),
Url {
style: String,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
Image {
style: String,
image: WebCursorImage,
},
}
impl Previous {
fn style(&self) -> &str {
match self {
Previous::Named(icon) => icon.name(),
Previous::Url { style: url, .. } => url,
Previous::Image { style, .. } => style,
}
}
fn style_with_url(&self, new_url: &str, new_hotspot_x: u16, new_hotspot_y: u16) -> String {
match self {
Previous::Named(icon) => format!("url({new_url}) {new_hotspot_x} {new_hotspot_y}, {}", icon.name()),
Previous::Url {
url,
hotspot_x,
hotspot_y,
..
}
| Previous::Image {
image:
WebCursorImage {
data_url: url,
hotspot_x,
hotspot_y,
..
},
..
} => format!(
"url({new_url}) {new_hotspot_x} {new_hotspot_y}, url({url}) {hotspot_x} {hotspot_y}, auto",
),
}
}
}
impl From<SelectedCursor> for Previous {
fn from(value: SelectedCursor) -> Self {
match value {
SelectedCursor::Named(icon) => Self::Named(icon),
SelectedCursor::Url {
style,
url,
hotspot_x,
hotspot_y,
..
} => Self::Url {
style,
url,
hotspot_x,
hotspot_y,
},
SelectedCursor::Image(image) => {
match Rc::try_unwrap(image).unwrap().into_inner().unwrap() {
CursorImageState::Loading { previous, .. } => previous,
CursorImageState::Failed(previous) => previous,
CursorImageState::Ready {
style,
image: current,
..
} => Self::Image {
style,
image: current,
},
}
}
}
}
}
#[derive(Debug)]
pub enum CursorImageState {
Loading {
style: Style,
cursor_visible: Rc<Cell<bool>>,
previous: Previous,
hotspot_x: u16,
hotspot_y: u16,
},
Failed(Previous),
Ready {
style: String,
image: WebCursorImage,
previous: Previous,
},
}
impl CursorImageState {
fn from_image(
window: &Window,
document: Document,
style: Style,
image: &CursorImage,
previous: Previous,
cursor_visible: Rc<Cell<bool>>,
) -> Rc<RefCell<Option<Self>>> {
// Can't create array directly when backed by SharedArrayBuffer.
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
#[cfg(target_feature = "atomics")]
let image_data = {
use js_sys::{Uint8Array, Uint8ClampedArray};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ImageData)]
type ImageDataExt;
#[wasm_bindgen(catch, constructor, js_class = ImageData)]
fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
}
let array = Uint8Array::new_with_length(image.rgba.len() as u32);
array.copy_from(&image.rgba);
let array = Uint8ClampedArray::new(&array);
ImageDataExt::new(array, image.width as u32)
.map(JsValue::from)
.map(ImageData::unchecked_from_js)
.unwrap()
};
#[cfg(not(target_feature = "atomics"))]
let image_data = ImageData::new_with_u8_clamped_array(
wasm_bindgen::Clamped(&image.rgba),
image.width as u32,
)
.unwrap();
let mut options = ImageBitmapOptions::new();
options.premultiply_alpha(PremultiplyAlpha::None);
let bitmap = JsFuture::from(
window
.create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options)
.unwrap(),
);
let state = Rc::new(RefCell::new(Some(Self::Loading {
style,
cursor_visible,
previous,
hotspot_x: image.hotspot_x,
hotspot_y: image.hotspot_y,
})));
wasm_bindgen_futures::spawn_local({
let weak = Rc::downgrade(&state);
let CursorImage { width, height, .. } = *image;
async move {
if weak.strong_count() == 0 {
return;
}
let bitmap: ImageBitmap = bitmap.await.unwrap().unchecked_into();
if weak.strong_count() == 0 {
return;
}
let canvas: HtmlCanvasElement =
document.create_element("canvas").unwrap().unchecked_into();
#[allow(clippy::disallowed_methods)]
canvas.set_width(width as u32);
#[allow(clippy::disallowed_methods)]
canvas.set_height(height as u32);
let context: ImageBitmapRenderingContext = canvas
.get_context("bitmaprenderer")
.unwrap()
.unwrap()
.unchecked_into();
context.transfer_from_image_bitmap(&bitmap);
thread_local! {
static CURRENT_STATE: RefCell<Option<Weak<RefCell<Option<CursorImageState>>>>> = RefCell::new(None);
// `HTMLCanvasElement.toBlob()` can't be interrupted. So we have to use a
// `Closure` that doesn't need to be garbage-collected.
static CALLBACK: Closure<dyn Fn(Option<Blob>)> = Closure::new(|blob| {
CURRENT_STATE.with(|weak| {
let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else {
return;
};
let mut state = state.borrow_mut();
// Extract old state.
let CursorImageState::Loading { style, cursor_visible, previous, hotspot_x, hotspot_y, .. } = state.take().unwrap() else {
unreachable!("found invalid state")
};
let Some(blob) = blob else {
*state = Some(CursorImageState::Failed(previous));
return;
};
let data_url = Url::create_object_url_with_blob(&blob).unwrap();
let value = previous.style_with_url(&data_url, hotspot_x, hotspot_y);
if cursor_visible.get() {
style.set("cursor", &value);
}
*state = Some(
CursorImageState::Ready {
style: value,
image: WebCursorImage{ data_url, hotspot_x, hotspot_y },
previous,
});
});
});
}
CURRENT_STATE.with(|state| *state.borrow_mut() = Some(weak));
CALLBACK
.with(|callback| canvas.to_blob(callback.as_ref().unchecked_ref()).unwrap());
}
});
state
}
}
#[derive(Debug)]
pub struct WebCursorImage {
data_url: String,
hotspot_x: u16,
hotspot_y: u16,
}
impl Drop for WebCursorImage {
fn drop(&mut self) {
Url::revoke_object_url(&self.data_url).unwrap();
}
}

View File

@@ -67,6 +67,7 @@ pub struct Execution {
on_key_press: OnEventHandle<KeyboardEvent>,
on_key_release: OnEventHandle<KeyboardEvent>,
on_visibility_change: OnEventHandle<web_sys::Event>,
on_touch_end: OnEventHandle<web_sys::Event>,
}
enum RunnerEnum {
@@ -180,6 +181,7 @@ impl Shared {
on_key_press: RefCell::new(None),
on_key_release: RefCell::new(None),
on_visibility_change: RefCell::new(None),
on_touch_end: RefCell::new(None),
}
}))
}
@@ -340,6 +342,8 @@ impl Shared {
self.window().clone(),
"pointerdown",
Closure::new(move |event: PointerEvent| {
runner.transient_activation();
if !runner.device_events() {
return;
}
@@ -363,6 +367,8 @@ impl Shared {
self.window().clone(),
"pointerup",
Closure::new(move |event: PointerEvent| {
runner.transient_activation();
if !runner.device_events() {
return;
}
@@ -386,6 +392,8 @@ impl Shared {
self.window().clone(),
"keydown",
Closure::new(move |event: KeyboardEvent| {
runner.transient_activation();
if !runner.device_events() {
return;
}
@@ -444,6 +452,14 @@ impl Shared {
}
}),
));
let runner = self.clone();
*self.0.on_touch_end.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"touchend",
Closure::new(move |_| {
runner.transient_activation();
}),
));
}
// Generate a strictly increasing ID
@@ -772,6 +788,18 @@ impl Shared {
}
}
fn transient_activation(&self) {
self.0
.all_canvases
.borrow()
.iter()
.for_each(|(_, canvas, _)| {
if let Some(canvas) = canvas.upgrade() {
canvas.borrow().transient_activation();
}
});
}
pub fn event_loop_recreation(&self, allow: bool) {
self.0.event_loop_recreation.set(allow)
}

View File

@@ -647,6 +647,10 @@ impl<T> EventLoopWindowTarget<T> {
let runner = self.runner.clone();
canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id)));
canvas.on_touch_end();
canvas.on_context_menu(prevent_default);
}
pub fn available_monitors(&self) -> VecDequeIter<MonitorHandle> {

View File

@@ -18,7 +18,6 @@
// compliant way.
mod r#async;
mod cursor;
mod device;
mod error;
mod event_loop;
@@ -40,4 +39,3 @@ pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId
pub(crate) use self::keyboard::KeyEventExtra;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;
pub(crate) use cursor::WebCustomCursor as PlatformCustomCursor;

View File

@@ -1,11 +1,12 @@
use std::cell::Cell;
use std::rc::Rc;
use std::rc::{Rc, Weak};
use std::sync::{Arc, Mutex};
use smol_str::SmolStr;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent,
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent,
PointerEvent, WheelEvent,
};
use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
@@ -18,10 +19,11 @@ use crate::window::{WindowAttributes, WindowId as RootWindowId};
use super::super::WindowId;
use super::animation_frame::AnimationFrameHandler;
use super::event_handle::EventListenerHandle;
use super::fullscreen::FullscreenHandler;
use super::intersection_handle::IntersectionObserverHandle;
use super::media_query_handle::MediaQueryListHandle;
use super::pointer::PointerHandler;
use super::{event, fullscreen, ButtonsState, ResizeScaleHandle};
use super::{event, ButtonsState, ResizeScaleHandle};
#[allow(dead_code)]
pub struct Canvas {
@@ -41,6 +43,7 @@ pub struct Canvas {
on_intersect: Option<IntersectionObserverHandle>,
animation_frame_handler: AnimationFrameHandler,
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_context_menu: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
}
pub struct Common {
@@ -48,15 +51,10 @@ pub struct Common {
pub document: Document,
/// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained.
pub raw: HtmlCanvasElement,
style: Style,
style: CssStyleDeclaration,
old_size: Rc<Cell<PhysicalSize<u32>>>,
current_size: Rc<Cell<PhysicalSize<u32>>>,
}
#[derive(Clone, Debug)]
pub struct Style {
read: CssStyleDeclaration,
write: CssStyleDeclaration,
fullscreen_handler: Rc<FullscreenHandler>,
}
impl Canvas {
@@ -94,7 +92,12 @@ impl Canvas {
.map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?;
}
let style = Style::new(&window, &canvas);
#[allow(clippy::disallowed_methods)]
let style = window
.get_computed_style(&canvas)
.expect("Failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("Invalid pseudo-element");
let common = Common {
window: window.clone(),
@@ -103,6 +106,7 @@ impl Canvas {
style,
old_size: Rc::default(),
current_size: Rc::default(),
fullscreen_handler: Rc::new(FullscreenHandler::new(document.clone(), canvas.clone())),
};
if let Some(size) = attr.inner_size {
@@ -126,7 +130,7 @@ impl Canvas {
}
if attr.fullscreen.0.is_some() {
fullscreen::request_fullscreen(&document, &canvas);
common.fullscreen_handler.request_fullscreen();
}
if attr.active {
@@ -150,6 +154,7 @@ impl Canvas {
on_intersect: None,
animation_frame_handler: AnimationFrameHandler::new(window),
on_touch_end: None,
on_context_menu: None,
})
}
@@ -176,7 +181,9 @@ impl Canvas {
y: bounds.y(),
};
if self.document().contains(Some(self.raw())) && self.style().get("display") != "none" {
if self.document().contains(Some(self.raw()))
&& self.style().get_property_value("display").unwrap() != "none"
{
position.x += super::style_size_property(self.style(), "border-left-width")
+ super::style_size_property(self.style(), "padding-left");
position.y += super::style_size_property(self.style(), "border-top-width")
@@ -222,7 +229,7 @@ impl Canvas {
}
#[inline]
pub fn style(&self) -> &Style {
pub fn style(&self) -> &CssStyleDeclaration {
&self.common.style
}
@@ -278,7 +285,7 @@ impl Canvas {
where
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
{
self.on_keyboard_press = Some(self.common.add_event(
self.on_keyboard_press = Some(self.common.add_transient_event(
"keydown",
move |event: KeyboardEvent| {
if prevent_default {
@@ -438,16 +445,31 @@ impl Canvas {
self.animation_frame_handler.on_animation_frame(f)
}
pub(crate) fn on_touch_end(&mut self) {
self.on_touch_end = Some(self.common.add_transient_event("touchend", |_| {}));
}
pub(crate) fn on_context_menu(&mut self, prevent_default: bool) {
self.on_context_menu = Some(self.common.add_event(
"contextmenu",
move |event: PointerEvent| {
if prevent_default {
event.prevent_default();
}
},
));
}
pub fn request_fullscreen(&self) {
fullscreen::request_fullscreen(self.document(), self.raw());
self.common.fullscreen_handler.request_fullscreen()
}
pub fn exit_fullscreen(&self) {
fullscreen::exit_fullscreen(self.document(), self.raw());
self.common.fullscreen_handler.exit_fullscreen()
}
pub fn is_fullscreen(&self) -> bool {
fullscreen::is_fullscreen(self.document(), self.raw())
self.common.fullscreen_handler.is_fullscreen()
}
pub fn request_animation_frame(&self) {
@@ -498,6 +520,10 @@ impl Canvas {
}
}
pub(crate) fn transient_activation(&self) {
self.common.fullscreen_handler.transient_activation()
}
pub fn remove_listeners(&mut self) {
self.on_touch_start = None;
self.on_focus = None;
@@ -511,6 +537,8 @@ impl Canvas {
self.on_intersect = None;
self.animation_frame_handler.cancel();
self.on_touch_end = None;
self.common.fullscreen_handler.cancel();
self.on_context_menu = None;
}
}
@@ -526,38 +554,27 @@ impl Common {
{
EventListenerHandle::new(self.raw.clone(), event_name, Closure::new(handler))
}
}
impl Style {
fn new(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> Self {
#[allow(clippy::disallowed_methods)]
let read = window
.get_computed_style(canvas)
.expect("Failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("Invalid pseudo-element");
// The difference between add_event and add_user_event is that the latter has a special meaning
// for browser security. A user event is a deliberate action by the user (like a mouse or key
// press) and is the only time things like a fullscreen request may be successfully completed.)
pub fn add_transient_event<E, F>(
&self,
event_name: &'static str,
mut handler: F,
) -> EventListenerHandle<dyn FnMut(E)>
where
E: 'static + AsRef<web_sys::Event> + wasm_bindgen::convert::FromWasmAbi,
F: 'static + FnMut(E),
{
let fullscreen_handler = Rc::downgrade(&self.fullscreen_handler);
#[allow(clippy::disallowed_methods)]
let write = canvas.style();
self.add_event(event_name, move |event: E| {
handler(event);
Self { read, write }
}
pub(crate) fn get(&self, property: &str) -> String {
self.read
.get_property_value(property)
.expect("Invalid property")
}
pub(crate) fn remove(&self, property: &str) {
self.write
.remove_property(property)
.expect("Property is read only");
}
pub(crate) fn set(&self, property: &str, value: &str) {
self.write
.set_property(property, value)
.expect("Property is read only");
if let Some(fullscreen_handler) = Weak::upgrade(&fullscreen_handler) {
fullscreen_handler.transient_activation()
}
})
}
}

View File

@@ -1,3 +1,6 @@
use std::cell::Cell;
use std::rc::Rc;
use js_sys::Promise;
use once_cell::unsync::OnceCell;
use wasm_bindgen::closure::Closure;
@@ -5,86 +8,138 @@ use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Document, Element, HtmlCanvasElement};
pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) {
if is_fullscreen(document, canvas) {
return;
}
use super::EventListenerHandle;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = HtmlCanvasElement)]
type RequestFullscreen;
#[wasm_bindgen(method, js_name = requestFullscreen)]
fn request_fullscreen(this: &RequestFullscreen) -> Promise;
#[wasm_bindgen(method, js_name = webkitRequestFullscreen)]
fn webkit_request_fullscreen(this: &RequestFullscreen);
}
let canvas: &RequestFullscreen = canvas.unchecked_ref();
if has_fullscreen_api_support(canvas) {
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|_| ());
}
REJECT_HANDLER.with(|handler| {
let _ = canvas.request_fullscreen().catch(handler);
});
} else {
canvas.webkit_request_fullscreen();
}
thread_local! {
static FULLSCREEN_API_SUPPORT: OnceCell<bool> = OnceCell::new();
}
pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool {
#[wasm_bindgen]
extern "C" {
type FullscreenElement;
#[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)]
fn webkit_fullscreen_element(this: &FullscreenElement) -> Option<Element>;
}
let element = if has_fullscreen_api_support(canvas) {
#[allow(clippy::disallowed_methods)]
document.fullscreen_element()
} else {
let document: &FullscreenElement = document.unchecked_ref();
document.webkit_fullscreen_element()
};
match element {
Some(element) => {
let canvas: &Element = canvas;
canvas == &element
}
None => false,
}
pub struct FullscreenHandler {
document: Document,
canvas: HtmlCanvasElement,
fullscreen_requested: Rc<Cell<bool>>,
_fullscreen_change: EventListenerHandle<dyn FnMut()>,
}
pub fn exit_fullscreen(document: &Document, canvas: &HtmlCanvasElement) {
#[wasm_bindgen]
extern "C" {
type ExitFullscreen;
impl FullscreenHandler {
pub fn new(document: Document, canvas: HtmlCanvasElement) -> Self {
let fullscreen_requested = Rc::new(Cell::new(false));
let fullscreen_change = EventListenerHandle::new(
canvas.clone(),
if has_fullscreen_api_support(&canvas) {
"fullscreenchange"
} else {
"webkitfullscreenchange"
},
Closure::new({
let fullscreen_requested = fullscreen_requested.clone();
move || {
// It doesn't matter if the canvas entered or exitted fullscreen mode,
// we don't want to request it again later.
fullscreen_requested.set(false);
}
}),
);
#[wasm_bindgen(method, js_name = webkitExitFullscreen)]
fn webkit_exit_fullscreen(this: &ExitFullscreen);
Self {
document,
canvas,
fullscreen_requested,
_fullscreen_change: fullscreen_change,
}
}
if has_fullscreen_api_support(canvas) {
#[allow(clippy::disallowed_methods)]
document.exit_fullscreen()
} else {
let document: &ExitFullscreen = document.unchecked_ref();
document.webkit_exit_fullscreen()
fn internal_request_fullscreen(&self) {
#[wasm_bindgen]
extern "C" {
type RequestFullscreen;
#[wasm_bindgen(method, js_name = requestFullscreen)]
fn request_fullscreen(this: &RequestFullscreen) -> Promise;
#[wasm_bindgen(method, js_name = webkitRequestFullscreen)]
fn webkit_request_fullscreen(this: &RequestFullscreen);
}
let canvas: &RequestFullscreen = self.canvas.unchecked_ref();
if has_fullscreen_api_support(&self.canvas) {
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|_| ());
}
REJECT_HANDLER.with(|handler| {
let _ = canvas.request_fullscreen().catch(handler);
});
} else {
canvas.webkit_request_fullscreen();
}
}
pub fn request_fullscreen(&self) {
if !self.is_fullscreen() {
self.internal_request_fullscreen();
self.fullscreen_requested.set(true);
}
}
pub fn transient_activation(&self) {
if self.fullscreen_requested.get() {
self.internal_request_fullscreen()
}
}
pub fn is_fullscreen(&self) -> bool {
#[wasm_bindgen]
extern "C" {
type FullscreenElement;
#[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)]
fn webkit_fullscreen_element(this: &FullscreenElement) -> Option<Element>;
}
let element = if has_fullscreen_api_support(&self.canvas) {
#[allow(clippy::disallowed_methods)]
self.document.fullscreen_element()
} else {
let document: &FullscreenElement = self.document.unchecked_ref();
document.webkit_fullscreen_element()
};
match element {
Some(element) => {
let canvas: &Element = &self.canvas;
canvas == &element
}
None => false,
}
}
pub fn exit_fullscreen(&self) {
#[wasm_bindgen]
extern "C" {
type ExitFullscreen;
#[wasm_bindgen(method, js_name = webkitExitFullscreen)]
fn webkit_exit_fullscreen(this: &ExitFullscreen);
}
if has_fullscreen_api_support(&self.canvas) {
#[allow(clippy::disallowed_methods)]
self.document.exit_fullscreen()
} else {
let document: &ExitFullscreen = self.document.unchecked_ref();
document.webkit_exit_fullscreen()
}
self.fullscreen_requested.set(false);
}
pub fn cancel(&self) {
self.fullscreen_requested.set(false);
}
}
fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool {
thread_local! {
static FULLSCREEN_API_SUPPORT: OnceCell<bool> = OnceCell::new();
}
FULLSCREEN_API_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]

View File

@@ -10,7 +10,6 @@ mod resize_scaling;
mod schedule;
pub use self::canvas::Canvas;
pub use self::canvas::Style;
pub use self::event::ButtonsState;
pub use self::event_handle::EventListenerHandle;
pub use self::resize_scaling::ResizeScaleHandle;
@@ -18,7 +17,9 @@ pub use self::schedule::Schedule;
use crate::dpi::{LogicalPosition, LogicalSize};
use wasm_bindgen::closure::Closure;
use web_sys::{Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState};
use web_sys::{
CssStyleDeclaration, Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState,
};
pub fn throw(msg: &str) {
wasm_bindgen::throw_str(msg);
@@ -50,8 +51,8 @@ pub fn scale_factor(window: &web_sys::Window) -> f64 {
window.device_pixel_ratio()
}
fn fix_canvas_size(style: &Style, mut size: LogicalSize<f64>) -> LogicalSize<f64> {
if style.get("box-sizing") == "border-box" {
fn fix_canvas_size(style: &CssStyleDeclaration, mut size: LogicalSize<f64>) -> LogicalSize<f64> {
if style.get_property_value("box-sizing").unwrap() == "border-box" {
size.width += style_size_property(style, "border-left-width")
+ style_size_property(style, "border-right-width")
+ style_size_property(style, "padding-left")
@@ -68,68 +69,76 @@ fn fix_canvas_size(style: &Style, mut size: LogicalSize<f64>) -> LogicalSize<f64
pub fn set_canvas_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
style: &CssStyleDeclaration,
new_size: LogicalSize<f64>,
) {
if !document.contains(Some(raw)) || style.get("display") == "none" {
if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" {
return;
}
let new_size = fix_canvas_size(style, new_size);
style.set("width", &format!("{}px", new_size.width));
style.set("height", &format!("{}px", new_size.height));
set_canvas_style_property(raw, "width", &format!("{}px", new_size.width));
set_canvas_style_property(raw, "height", &format!("{}px", new_size.height));
}
pub fn set_canvas_min_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
style: &CssStyleDeclaration,
dimensions: Option<LogicalSize<f64>>,
) {
if let Some(dimensions) = dimensions {
if !document.contains(Some(raw)) || style.get("display") == "none" {
if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" {
return;
}
let new_size = fix_canvas_size(style, dimensions);
style.set("min-width", &format!("{}px", new_size.width));
style.set("min-height", &format!("{}px", new_size.height));
set_canvas_style_property(raw, "min-width", &format!("{}px", new_size.width));
set_canvas_style_property(raw, "min-height", &format!("{}px", new_size.height));
} else {
style.remove("min-width");
style.remove("min-height");
style
.remove_property("min-width")
.expect("Property is read only");
style
.remove_property("min-height")
.expect("Property is read only");
}
}
pub fn set_canvas_max_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
style: &CssStyleDeclaration,
dimensions: Option<LogicalSize<f64>>,
) {
if let Some(dimensions) = dimensions {
if !document.contains(Some(raw)) || style.get("display") == "none" {
if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" {
return;
}
let new_size = fix_canvas_size(style, dimensions);
style.set("max-width", &format!("{}px", new_size.width));
style.set("max-height", &format!("{}px", new_size.height));
set_canvas_style_property(raw, "max-width", &format!("{}px", new_size.width));
set_canvas_style_property(raw, "max-height", &format!("{}px", new_size.height));
} else {
style.remove("max-width");
style.remove("max-height");
style
.remove_property("max-width")
.expect("Property is read only");
style
.remove_property("max-height")
.expect("Property is read only");
}
}
pub fn set_canvas_position(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
style: &CssStyleDeclaration,
mut position: LogicalPosition<f64>,
) {
if document.contains(Some(raw)) && style.get("display") != "none" {
if document.contains(Some(raw)) && style.get_property_value("display").unwrap() != "none" {
position.x -= style_size_property(style, "margin-left")
+ style_size_property(style, "border-left-width")
+ style_size_property(style, "padding-left");
@@ -138,21 +147,30 @@ pub fn set_canvas_position(
+ style_size_property(style, "padding-top");
}
style.set("position", "fixed");
style.set("left", &format!("{}px", position.x));
style.set("top", &format!("{}px", position.y));
set_canvas_style_property(raw, "position", "fixed");
set_canvas_style_property(raw, "left", &format!("{}px", position.x));
set_canvas_style_property(raw, "top", &format!("{}px", position.y));
}
/// This function will panic if the element is not inserted in the DOM
/// or is not a CSS property that represents a size in pixel.
pub fn style_size_property(style: &Style, property: &str) -> f64 {
let prop = style.get(property);
pub fn style_size_property(style: &CssStyleDeclaration, property: &str) -> f64 {
let prop = style
.get_property_value(property)
.expect("Found invalid property");
prop.strip_suffix("px")
.expect("Element was not inserted into the DOM or is not a size in pixel")
.parse()
.expect("CSS property is not a size in pixel")
}
pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) {
let style = raw.style();
style
.set_property(property, value)
.unwrap_or_else(|err| panic!("error: {err:?}\nFailed to set {property}"))
}
pub fn is_dark_mode(window: &web_sys::Window) -> Option<bool> {
window
.match_media("(prefers-color-scheme: dark)")

View File

@@ -80,7 +80,7 @@ impl PointerHandler {
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
{
let window = canvas_common.window.clone();
self.on_pointer_release = Some(canvas_common.add_event(
self.on_pointer_release = Some(canvas_common.add_transient_event(
"pointerup",
move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event);
@@ -118,7 +118,7 @@ impl PointerHandler {
{
let window = canvas_common.window.clone();
let canvas = canvas_common.raw.clone();
self.on_pointer_press = Some(canvas_common.add_event(
self.on_pointer_press = Some(canvas_common.add_transient_event(
"pointerdown",
move |event: PointerEvent| {
if prevent_default {

View File

@@ -2,14 +2,14 @@ use js_sys::{Array, Object};
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions,
ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window,
CssStyleDeclaration, Document, HtmlCanvasElement, MediaQueryList, ResizeObserver,
ResizeObserverBoxOptions, ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize,
Window,
};
use crate::dpi::{LogicalSize, PhysicalSize};
use super::super::backend;
use super::canvas::Style;
use super::media_query_handle::MediaQueryListHandle;
use std::cell::{Cell, RefCell};
@@ -22,7 +22,7 @@ impl ResizeScaleHandle {
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
style: CssStyleDeclaration,
scale_handler: S,
resize_handler: R,
) -> Self
@@ -51,7 +51,7 @@ struct ResizeScaleInternal {
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
style: CssStyleDeclaration,
mql: MediaQueryListHandle,
observer: ResizeObserver,
_observer_closure: Closure<dyn FnMut(Array, ResizeObserver)>,
@@ -65,7 +65,7 @@ impl ResizeScaleInternal {
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
style: CssStyleDeclaration,
scale_handler: S,
resize_handler: R,
) -> Rc<RefCell<Self>>
@@ -152,7 +152,9 @@ impl ResizeScaleInternal {
}
fn notify(&mut self) {
if !self.document.contains(Some(&self.canvas)) || self.style.get("display") == "none" {
if !self.document.contains(Some(&self.canvas))
|| self.style.get_property_value("display").unwrap() == "none"
{
let size = PhysicalSize::new(0, 0);
if self.notify_scale.replace(false) {
@@ -178,7 +180,7 @@ impl ResizeScaleInternal {
backend::style_size_property(&self.style, "height"),
);
if self.style.get("box-sizing") == "border-box" {
if self.style.get_property_value("box-sizing").unwrap() == "border-box" {
size.width -= backend::style_size_property(&self.style, "border-left-width")
+ backend::style_size_property(&self.style, "border-right-width")
+ backend::style_size_property(&self.style, "padding-left")
@@ -244,7 +246,10 @@ impl ResizeScaleInternal {
.get(0)
.unchecked_into();
let writing_mode = self.style.get("writing-mode");
let writing_mode = self
.style
.get_property_value("writing-mode")
.expect("`writing-mode` is a valid CSS property");
// means the canvas is not inserted into the DOM
if writing_mode.is_empty() {

View File

@@ -1,4 +1,3 @@
use crate::cursor::CustomCursor;
use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
use crate::icon::Icon;
@@ -8,12 +7,12 @@ use crate::window::{
};
use crate::SendSyncWrapper;
use super::cursor::SelectedCursor;
use super::r#async::Dispatcher;
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
use web_sys::HtmlCanvasElement;
use std::cell::{Cell, RefCell};
use super::r#async::Dispatcher;
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
@@ -25,8 +24,7 @@ pub struct Inner {
id: WindowId,
pub window: web_sys::Window,
canvas: Rc<RefCell<backend::Canvas>>,
selected_cursor: RefCell<SelectedCursor>,
cursor_visible: Rc<Cell<bool>>,
previous_pointer: RefCell<&'static str>,
destroy_fn: Option<Box<dyn FnOnce()>>,
}
@@ -55,8 +53,7 @@ impl Window {
id,
window: window.clone(),
canvas,
selected_cursor: Default::default(),
cursor_visible: Rc::new(Cell::new(true)),
previous_pointer: RefCell::new("auto"),
destroy_fn: Some(destroy_fn),
};
@@ -198,24 +195,8 @@ impl Inner {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
*self.selected_cursor.borrow_mut() = SelectedCursor::Named(cursor);
if self.cursor_visible.get() {
self.canvas.borrow().style().set("cursor", cursor.name());
}
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let canvas = self.canvas.borrow();
let new_cursor = cursor.inner.build(
canvas.window(),
canvas.document(),
canvas.style(),
self.selected_cursor.take(),
self.cursor_visible.clone(),
);
*self.selected_cursor.borrow_mut() = new_cursor;
*self.previous_pointer.borrow_mut() = cursor.name();
backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", cursor.name());
}
#[inline]
@@ -241,14 +222,14 @@ impl Inner {
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
if !visible && self.cursor_visible.get() {
self.canvas.borrow().style().set("cursor", "none");
self.cursor_visible.set(false);
} else if visible && !self.cursor_visible.get() {
self.selected_cursor
.borrow()
.set_style(self.canvas.borrow().style());
self.cursor_visible.set(true);
if !visible {
backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", "none");
} else {
backend::set_canvas_style_property(
self.canvas.borrow().raw(),
"cursor",
&self.previous_pointer.borrow(),
);
}
}

View File

@@ -9,8 +9,8 @@ use windows_sys::Win32::{
},
UI::{
HiDpi::{
DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, MDT_EFFECTIVE_DPI,
PROCESS_PER_MONITOR_DPI_AWARE,
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE,
},
WindowsAndMessaging::IsProcessDPIAware,
},
@@ -21,8 +21,6 @@ use crate::platform_impl::platform::util::{
SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT,
};
const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2: DPI_AWARENESS_CONTEXT = -4;
pub fn become_dpi_aware() {
static ENABLE_DPI_AWARENESS: Once = Once::new();
ENABLE_DPI_AWARENESS.call_once(|| {

View File

@@ -21,7 +21,7 @@ use once_cell::sync::Lazy;
use windows_sys::Win32::{
Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE,
Foundation::{BOOL, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM},
Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM},
Graphics::Gdi::{
GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient,
ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE,
@@ -35,13 +35,9 @@ use windows_sys::Win32::{
Input::{
Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW},
KeyboardAndMouse::{
MapVirtualKeyW, ReleaseCapture, SetCapture, TrackMouseEvent, MAPVK_VK_TO_VSC_EX,
TME_LEAVE, TRACKMOUSEEVENT, VK_NUMLOCK, VK_SHIFT,
},
Pointer::{
POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, POINTER_INFO,
POINTER_PEN_INFO, POINTER_TOUCH_INFO,
ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT,
},
Pointer::{POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE},
Touch::{
CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE,
TOUCHEVENTF_UP, TOUCHINPUT,
@@ -54,20 +50,19 @@ use windows_sys::Win32::{
RegisterClassExW, RegisterWindowMessageA, SetCursor, SetTimer, SetWindowPos,
TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA,
HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN,
PT_TOUCH, RI_KEY_E0, RI_KEY_E1, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE,
SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER,
WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY,
WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION,
WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT,
WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE,
WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY,
WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE,
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE,
WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED,
WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED,
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP,
WS_VISIBLE,
PT_TOUCH, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED,
SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS,
WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE,
WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION,
WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE,
WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE,
WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN,
WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR,
WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP,
WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP,
WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT,
WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
},
},
};
@@ -80,8 +75,8 @@ use crate::{
WindowEvent,
},
event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW},
keyboard::{KeyCode, ModifiersState, PhysicalKey},
platform::{pump_events::PumpStatus, scancode::PhysicalKeyExtScancode},
keyboard::ModifiersState,
platform::pump_events::PumpStatus,
platform_impl::platform::{
dark_mode::try_theme,
dpi::{become_dpi_aware, dpi_to_scale_factor},
@@ -101,38 +96,7 @@ use runner::{EventLoopRunner, EventLoopRunnerShared};
use self::runner::RunnerState;
use super::{window::set_skip_taskbar, SelectedCursor};
type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
entriesCount: *mut u32,
pointerCount: *mut u32,
pointerInfo: *mut POINTER_INFO,
) -> BOOL;
type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL;
type GetPointerDeviceRects = unsafe extern "system" fn(
device: HANDLE,
pointerDeviceRect: *mut RECT,
displayRect: *mut RECT,
) -> BOOL;
type GetPointerTouchInfo =
unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL;
type GetPointerPenInfo =
unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL;
static GET_POINTER_FRAME_INFO_HISTORY: Lazy<Option<GetPointerFrameInfoHistory>> =
Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory));
static SKIP_POINTER_FRAME_MESSAGES: Lazy<Option<SkipPointerFrameMessages>> =
Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages));
static GET_POINTER_DEVICE_RECTS: Lazy<Option<GetPointerDeviceRects>> =
Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects));
static GET_POINTER_TOUCH_INFO: Lazy<Option<GetPointerTouchInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo));
static GET_POINTER_PEN_INFO: Lazy<Option<GetPointerPenInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo));
use super::window::set_skip_taskbar;
pub(crate) struct WindowData<T: 'static> {
pub window_state: Arc<Mutex<WindowState>>,
@@ -566,6 +530,10 @@ impl<T> EventLoopWindowTarget<T> {
self.runner_shared.exit_code().is_some()
}
pub(crate) fn clear_exit(&self) {
self.runner_shared.clear_exit();
}
fn exit_code(&self) -> Option<i32> {
self.runner_shared.exit_code()
}
@@ -1831,9 +1799,9 @@ unsafe fn public_window_callback_inner<T: 'static>(
Some(SkipPointerFrameMessages),
Some(GetPointerDeviceRects),
) = (
*GET_POINTER_FRAME_INFO_HISTORY,
*SKIP_POINTER_FRAME_MESSAGES,
*GET_POINTER_DEVICE_RECTS,
*util::GET_POINTER_FRAME_INFO_HISTORY,
*util::SKIP_POINTER_FRAME_MESSAGES,
*util::GET_POINTER_DEVICE_RECTS,
) {
let pointer_id = super::loword(wparam as u32) as u32;
let mut entries_count = 0u32;
@@ -1915,7 +1883,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
let force = match pointer_info.pointerType {
PT_TOUCH => {
let mut touch_info = mem::MaybeUninit::uninit();
GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe {
GetPointerTouchInfo(
pointer_info.pointerId,
@@ -1931,7 +1899,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
}
PT_PEN => {
let mut pen_info = mem::MaybeUninit::uninit();
GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| {
util::GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| {
match unsafe {
GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr())
} {
@@ -2011,21 +1979,16 @@ unsafe fn public_window_callback_inner<T: 'static>(
// `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement.
let in_client_area = super::loword(lparam as u32) as u32 == HTCLIENT;
if in_client_area {
Some(window_state.mouse.selected_cursor.clone())
Some(window_state.mouse.cursor)
} else {
None
}
};
match set_cursor_to {
Some(selected_cursor) => {
let hcursor = match selected_cursor {
SelectedCursor::Named(cursor_icon) => unsafe {
LoadCursorW(0, util::to_windows_cursor(cursor_icon))
},
SelectedCursor::Custom(cursor) => cursor.as_raw_handle(),
};
unsafe { SetCursor(hcursor) };
Some(cursor) => {
let cursor = unsafe { LoadCursorW(0, util::to_windows_cursor(cursor)) };
unsafe { SetCursor(cursor) };
result = ProcResult::Value(0);
}
None => result = ProcResult::DefWindowProc(wparam),
@@ -2498,105 +2461,17 @@ unsafe fn handle_raw_input<T: 'static>(userdata: &ThreadMsgTargetData<T>, data:
return;
}
let state = if pressed { Pressed } else { Released };
let extension = {
if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) {
0xE000
} else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) {
0xE100
} else {
0x0000
}
};
let scancode = if keyboard.MakeCode == 0 {
// In some cases (often with media keys) the device reports a scancode of 0 but a
// valid virtual key. In these cases we obtain the scancode from the virtual key.
unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 }
} else {
keyboard.MakeCode | extension
};
if scancode == 0xE11D || scancode == 0xE02A {
// At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing
// Ctrl+NumLock.
// This equvalence means that if the user presses Pause, the keyboard will emit two
// subsequent keypresses:
// 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100)
// 2, 0x0045 - Which on its own can be interpreted as Pause
//
// There's another combination which isn't quite an equivalence:
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing
// PrtSc (print screen) produces the following sequence:
// 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000)
// 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on
// its own it can be interpreted as PrtSc
//
// For this reason, if we encounter the first keypress, we simply ignore it, trusting
// that there's going to be another event coming, from which we can extract the
// appropriate key.
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
return;
if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) {
let state = if pressed { Pressed } else { Released };
userdata.send_event(Event::DeviceEvent {
device_id,
event: Key(RawKeyEvent {
physical_key,
state,
}),
});
}
let physical_key = if keyboard.VKey == VK_NUMLOCK {
// Historically, the NumLock and the Pause key were one and the same physical key.
// The user could trigger Pause by pressing Ctrl+NumLock.
// Now these are often physically separate and the two keys can be differentiated by
// checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045.
//
// However in this event, both keys are reported as 0x0045 even on modern hardware.
// Therefore we use the virtual key instead to determine whether it's a NumLock and
// set the KeyCode accordingly.
//
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
PhysicalKey::Code(KeyCode::NumLock)
} else {
PhysicalKey::from_scancode(scancode as u32)
};
if keyboard.VKey == VK_SHIFT {
if let PhysicalKey::Code(code) = physical_key {
match code {
KeyCode::NumpadDecimal
| KeyCode::Numpad0
| KeyCode::Numpad1
| KeyCode::Numpad2
| KeyCode::Numpad3
| KeyCode::Numpad4
| KeyCode::Numpad5
| KeyCode::Numpad6
| KeyCode::Numpad7
| KeyCode::Numpad8
| KeyCode::Numpad9 => {
// On Windows, holding the Shift key makes numpad keys behave as if NumLock
// wasn't active. The way this is exposed to applications by the system is that
// the application receives a fake key release event for the shift key at the
// moment when the numpad key is pressed, just before receiving the numpad key
// as well.
//
// The issue is that in the raw device event (here), the fake shift release
// event reports the numpad key as the scancode. Unfortunately, the event doesn't
// have any information to tell whether it's the left shift or the right shift
// that needs to get the fake release (or press) event so we don't forward this
// event to the application at all.
//
// For more on this, read the article by Raymond Chen, titled:
// "The shift key overrides NumLock"
// https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953
return;
}
_ => (),
}
}
}
userdata.send_event(Event::DeviceEvent {
device_id,
event: Key(RawKeyEvent {
physical_key,
state,
}),
});
}
}

View File

@@ -163,6 +163,10 @@ impl<T> EventLoopRunner<T> {
self.exit.get()
}
pub fn clear_exit(&self) {
self.exit.set(None);
}
pub fn should_buffer(&self) -> bool {
let handler = self.event_handler.take();
let should_buffer = handler.is_none();

View File

@@ -1,23 +1,18 @@
use std::{ffi::c_void, fmt, io, mem, path::Path, sync::Arc};
use std::{fmt, io, mem, path::Path, sync::Arc};
use cursor_icon::CursorIcon;
use windows_sys::{
core::PCWSTR,
Win32::{
Foundation::HWND,
Graphics::Gdi::{
CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits,
},
UI::WindowsAndMessaging::{
CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW,
HCURSOR, HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE,
LR_LOADFROMFILE, WM_SETICON,
CreateIcon, DestroyIcon, LoadImageW, SendMessageW, HICON, ICON_BIG, ICON_SMALL,
IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON,
},
},
};
use crate::dpi::PhysicalSize;
use crate::icon::*;
use crate::{cursor::CursorImage, dpi::PhysicalSize};
use super::util;
@@ -165,92 +160,3 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) {
SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0);
}
}
#[derive(Debug, Clone)]
pub enum SelectedCursor {
Named(CursorIcon),
Custom(WinCursor),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
#[derive(Clone, Debug)]
pub struct WinCursor {
inner: Arc<RaiiCursor>,
}
impl WinCursor {
pub fn as_raw_handle(&self) -> HICON {
self.inner.handle
}
fn from_handle(handle: HCURSOR) -> Self {
Self {
inner: Arc::new(RaiiCursor { handle }),
}
}
pub fn new(image: &CursorImage) -> Result<Self, io::Error> {
let mut bgra = image.rgba.clone();
bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2));
let w = image.width as i32;
let h = image.height as i32;
unsafe {
let hdc_screen = GetDC(0);
if hdc_screen == 0 {
return Err(io::Error::last_os_error());
}
let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h);
ReleaseDC(0, hdc_screen);
if hbm_color == 0 {
return Err(io::Error::last_os_error());
}
if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 {
DeleteObject(hbm_color);
return Err(io::Error::last_os_error());
};
// Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters
let mask_bits: Vec<u8> = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize];
let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _);
if hbm_mask == 0 {
DeleteObject(hbm_color);
return Err(io::Error::last_os_error());
}
let icon_info = ICONINFO {
fIcon: 0,
xHotspot: image.hotspot_x as u32,
yHotspot: image.hotspot_y as u32,
hbmMask: hbm_mask,
hbmColor: hbm_color,
};
let handle = CreateIconIndirect(&icon_info as *const _);
DeleteObject(hbm_color);
DeleteObject(hbm_mask);
if handle == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self::from_handle(handle))
}
}
}
#[derive(Debug)]
struct RaiiCursor {
handle: HCURSOR,
}
impl Drop for RaiiCursor {
fn drop(&mut self) {
unsafe { DestroyCursor(self.handle) };
}
}

View File

@@ -10,9 +10,9 @@ use windows_sys::Win32::{
UI::{
Input::Ime::{
ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext,
ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM,
CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN,
IACE_DEFAULT,
ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED,
ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM,
GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT,
},
WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED},
},
@@ -124,7 +124,7 @@ impl ImeContext {
left: x,
top: y,
right: x + width,
bottom: y - height,
bottom: y + height,
};
let candidate_form = CANDIDATEFORM {
dwIndex: 0,
@@ -132,8 +132,16 @@ impl ImeContext {
ptCurrentPos: POINT { x, y },
rcArea: rc_area,
};
let composition_form = COMPOSITIONFORM {
dwStyle: CFS_POINT,
ptCurrentPos: POINT { x, y: y + height },
rcArea: rc_area,
};
unsafe { ImmSetCandidateWindow(self.himc, &candidate_form) };
unsafe {
ImmSetCompositionWindow(self.himc, &composition_form);
ImmSetCandidateWindow(self.himc, &candidate_form);
}
}
pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) {

View File

@@ -10,13 +10,12 @@ pub(crate) use self::{
event_loop::{
EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes,
},
icon::{SelectedCursor, WinIcon},
icon::WinIcon,
monitor::{MonitorHandle, VideoMode},
window::Window,
};
pub use self::icon::WinIcon as PlatformIcon;
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
use crate::platform_impl::Fullscreen;
use crate::event::DeviceId as RootDeviceId;

View File

@@ -11,21 +11,29 @@ use windows_sys::Win32::{
UI::{
Input::{
GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList,
KeyboardAndMouse::{MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT},
RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST,
RAWINPUTHEADER, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, RIDI_DEVICEINFO,
RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD,
RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
RAWINPUTHEADER, RAWKEYBOARD, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE,
RIDI_DEVICEINFO, RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID,
RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID,
RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
},
WindowsAndMessaging::{
RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, RI_MOUSE_BUTTON_2_DOWN,
RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP,
RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP, RI_MOUSE_BUTTON_5_DOWN,
RI_MOUSE_BUTTON_5_UP,
RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP,
RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN,
RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP,
RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP,
},
},
};
use crate::{event::ElementState, event_loop::DeviceEvents, platform_impl::platform::util};
use crate::{
event::ElementState,
event_loop::DeviceEvents,
keyboard::{KeyCode, PhysicalKey},
platform::scancode::PhysicalKeyExtScancode,
platform_impl::platform::util,
};
#[allow(dead_code)]
pub fn get_raw_input_device_list() -> Option<Vec<RAWINPUTDEVICELIST>> {
@@ -220,3 +228,99 @@ pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option<ElementState>; 5
button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP),
]
}
pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option<PhysicalKey> {
let extension = {
if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) {
0xE000
} else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) {
0xE100
} else {
0x0000
}
};
let scancode = if keyboard.MakeCode == 0 {
// In some cases (often with media keys) the device reports a scancode of 0 but a
// valid virtual key. In these cases we obtain the scancode from the virtual key.
unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 }
} else {
keyboard.MakeCode | extension
};
if scancode == 0xE11D || scancode == 0xE02A {
// At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing
// Ctrl+NumLock.
// This equvalence means that if the user presses Pause, the keyboard will emit two
// subsequent keypresses:
// 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100)
// 2, 0x0045 - Which on its own can be interpreted as Pause
//
// There's another combination which isn't quite an equivalence:
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing
// PrtSc (print screen) produces the following sequence:
// 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000)
// 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on
// its own it can be interpreted as PrtSc
//
// For this reason, if we encounter the first keypress, we simply ignore it, trusting
// that there's going to be another event coming, from which we can extract the
// appropriate key.
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
return None;
}
let physical_key = if keyboard.VKey == VK_NUMLOCK {
// Historically, the NumLock and the Pause key were one and the same physical key.
// The user could trigger Pause by pressing Ctrl+NumLock.
// Now these are often physically separate and the two keys can be differentiated by
// checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045.
//
// However in this event, both keys are reported as 0x0045 even on modern hardware.
// Therefore we use the virtual key instead to determine whether it's a NumLock and
// set the KeyCode accordingly.
//
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
PhysicalKey::Code(KeyCode::NumLock)
} else {
PhysicalKey::from_scancode(scancode as u32)
};
if keyboard.VKey == VK_SHIFT {
if let PhysicalKey::Code(code) = physical_key {
match code {
KeyCode::NumpadDecimal
| KeyCode::Numpad0
| KeyCode::Numpad1
| KeyCode::Numpad2
| KeyCode::Numpad3
| KeyCode::Numpad4
| KeyCode::Numpad5
| KeyCode::Numpad6
| KeyCode::Numpad7
| KeyCode::Numpad8
| KeyCode::Numpad9 => {
// On Windows, holding the Shift key makes numpad keys behave as if NumLock
// wasn't active. The way this is exposed to applications by the system is that
// the application receives a fake key release event for the shift key at the
// moment when the numpad key is pressed, just before receiving the numpad key
// as well.
//
// The issue is that in the raw device event (here), the fake shift release
// event reports the numpad key as the scancode. Unfortunately, the event doesn't
// have any information to tell whether it's the left shift or the right shift
// that needs to get the fake release (or press) event so we don't forward this
// event to the application at all.
//
// For more on this, read the article by Raymond Chen, titled:
// "The shift key overrides NumLock"
// https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953
return None;
}
_ => (),
}
}
}
Some(physical_key)
}

View File

@@ -13,7 +13,7 @@ use once_cell::sync::Lazy;
use windows_sys::{
core::{HRESULT, PCWSTR},
Win32::{
Foundation::{BOOL, HMODULE, HWND, RECT},
Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT},
Graphics::Gdi::{ClientToScreen, HMONITOR},
System::{
LibraryLoader::{GetProcAddress, LoadLibraryA},
@@ -21,7 +21,10 @@ use windows_sys::{
},
UI::{
HiDpi::{DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS},
Input::KeyboardAndMouse::GetActiveWindow,
Input::{
KeyboardAndMouse::GetActiveWindow,
Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO},
},
WindowsAndMessaging::{
ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement,
GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS,
@@ -191,7 +194,9 @@ pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR {
}
}
// Helper function to dynamically load function pointer.
// Helper function to dynamically load function pointer as some functions
// may not be available on all Windows platforms supported by winit.
//
// `library` and `function` must be zero-terminated.
pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> {
assert_eq!(library.chars().last(), Some('\0'));
@@ -237,6 +242,26 @@ pub type AdjustWindowRectExForDpi = unsafe extern "system" fn(
dpi: u32,
) -> BOOL;
pub type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
entriesCount: *mut u32,
pointerCount: *mut u32,
pointerInfo: *mut POINTER_INFO,
) -> BOOL;
pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL;
pub type GetPointerDeviceRects = unsafe extern "system" fn(
device: HANDLE,
pointerDeviceRect: *mut RECT,
displayRect: *mut RECT,
) -> BOOL;
pub type GetPointerTouchInfo =
unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL;
pub type GetPointerPenInfo =
unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL;
pub static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
pub static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy<Option<AdjustWindowRectExForDpi>> =
@@ -251,3 +276,13 @@ pub static SET_PROCESS_DPI_AWARENESS: Lazy<Option<SetProcessDpiAwareness>> =
Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness));
pub static SET_PROCESS_DPI_AWARE: Lazy<Option<SetProcessDPIAware>> =
Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware));
pub static GET_POINTER_FRAME_INFO_HISTORY: Lazy<Option<GetPointerFrameInfoHistory>> =
Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory));
pub static SKIP_POINTER_FRAME_MESSAGES: Lazy<Option<SkipPointerFrameMessages>> =
Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages));
pub static GET_POINTER_DEVICE_RECTS: Lazy<Option<GetPointerDeviceRects>> =
Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects));
pub static GET_POINTER_TOUCH_INFO: Lazy<Option<GetPointerTouchInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo));
pub static GET_POINTER_PEN_INFO: Lazy<Option<GetPointerPenInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo));

View File

@@ -55,7 +55,6 @@ use windows_sys::Win32::{
};
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
icon::Icon,
@@ -67,13 +66,13 @@ use crate::{
dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi},
drop_handler::FileDropHandler,
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
icon::{self, IconType, WinCursor},
icon::{self, IconType},
ime::ImeContext,
keyboard::KeyEventBuilder,
monitor::{self, MonitorHandle},
util,
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
Fullscreen, PlatformSpecificWindowBuilderAttributes, SelectedCursor, WindowId,
Fullscreen, PlatformSpecificWindowBuilderAttributes, WindowId,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -84,7 +83,7 @@ use crate::{
/// The Win32 implementation of the main `Window` object.
pub(crate) struct Window {
/// Main handle for the window.
window: WindowWrapper,
window: HWND,
/// The current window state.
window_state: Arc<Mutex<WindowState>>,
@@ -128,11 +127,11 @@ impl Window {
}
pub fn set_transparent(&self, transparent: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::TRANSPARENT, transparent)
});
});
@@ -142,11 +141,11 @@ impl Window {
#[inline]
pub fn set_visible(&self, visible: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::VISIBLE, visible)
});
});
@@ -154,7 +153,7 @@ impl Window {
#[inline]
pub fn is_visible(&self) -> Option<bool> {
Some(unsafe { IsWindowVisible(self.window.0) == 1 })
Some(unsafe { IsWindowVisible(self.window) == 1 })
}
#[inline]
@@ -190,10 +189,10 @@ impl Window {
let (x, y): (i32, i32) = position.to_physical::<i32>(self.scale_factor()).into();
let window_state = Arc::clone(&self.window_state);
let window = self.window.clone();
let window = self.window;
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MAXIMIZED, false)
});
});
@@ -247,10 +246,10 @@ impl Window {
if physical_size != self.inner_size() {
let window_state = Arc::clone(&self.window_state);
let window = self.window.clone();
let window = self.window;
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MAXIMIZED, false)
});
});
@@ -285,12 +284,12 @@ impl Window {
#[inline]
pub fn set_resizable(&self, resizable: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::RESIZABLE, resizable)
});
});
@@ -304,12 +303,12 @@ impl Window {
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(
WindowFlags::MINIMIZABLE,
buttons.contains(WindowButtons::MINIMIZE),
@@ -343,14 +342,14 @@ impl Window {
/// Returns the `hwnd` of this window.
#[inline]
pub fn hwnd(&self) -> HWND {
self.window.0
self.window
}
#[cfg(feature = "rwh_04")]
#[inline]
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
let mut window_handle = rwh_04::Win32Handle::empty();
window_handle.hwnd = self.window.0 as *mut _;
window_handle.hwnd = self.window as *mut _;
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = hinstance as *mut _;
rwh_04::RawWindowHandle::Win32(window_handle)
@@ -360,7 +359,7 @@ impl Window {
#[inline]
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
let mut window_handle = rwh_05::Win32WindowHandle::empty();
window_handle.hwnd = self.window.0 as *mut _;
window_handle.hwnd = self.window as *mut _;
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = hinstance as *mut _;
rwh_05::RawWindowHandle::Win32(window_handle)
@@ -377,8 +376,7 @@ impl Window {
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe {
// SAFETY: Handle will never be zero.
let window = self.window.0;
std::num::NonZeroIsize::new_unchecked(window)
std::num::NonZeroIsize::new_unchecked(self.window)
});
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = std::num::NonZeroIsize::new(hinstance);
@@ -397,28 +395,13 @@ impl Window {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
self.window_state_lock().mouse.selected_cursor = SelectedCursor::Named(cursor);
self.window_state_lock().mouse.cursor = cursor;
self.thread_executor.execute_in_thread(move || unsafe {
let cursor = LoadCursorW(0, util::to_windows_cursor(cursor));
SetCursor(cursor);
});
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let new_cursor = match WinCursor::new(&cursor.inner) {
Ok(cursor) => cursor,
Err(err) => {
warn!("Failed to create custom cursor: {err}");
return;
}
};
self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor.clone());
self.thread_executor.execute_in_thread(move || unsafe {
SetCursor(new_cursor.as_raw_handle());
});
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let confine = match mode {
@@ -429,7 +412,7 @@ impl Window {
}
};
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
let (tx, rx) = channel();
@@ -439,7 +422,7 @@ impl Window {
.lock()
.unwrap()
.mouse
.set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, confine))
.set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine))
.map_err(|e| ExternalError::Os(os_error!(e)));
let _ = tx.send(result);
});
@@ -448,7 +431,7 @@ impl Window {
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
let (tx, rx) = channel();
@@ -458,7 +441,7 @@ impl Window {
.lock()
.unwrap()
.mouse
.set_cursor_flags(window.0, |f| f.set(CursorFlags::HIDDEN, !visible))
.set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible))
.map_err(|e| e.to_string());
let _ = tx.send(result);
});
@@ -488,7 +471,7 @@ impl Window {
}
unsafe fn handle_os_dragging(&self, wparam: WPARAM) {
let window = self.window.clone();
let window = self.window;
let window_state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || {
@@ -516,7 +499,7 @@ impl Window {
unsafe {
PostMessageW(
window.0,
window,
WM_NCLBUTTONDOWN,
wparam,
&points as *const _ as LPARAM,
@@ -631,10 +614,10 @@ impl Window {
#[inline]
pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest)
});
});
@@ -649,7 +632,7 @@ impl Window {
#[inline]
pub fn set_minimized(&self, minimized: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
let is_minimized = util::is_minimized(self.hwnd());
@@ -659,7 +642,7 @@ impl Window {
WindowState::set_window_flags_in_place(&mut window_state.lock().unwrap(), |f| {
f.set(WindowFlags::MINIMIZED, is_minimized)
});
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MINIMIZED, minimized)
});
});
@@ -672,12 +655,12 @@ impl Window {
#[inline]
pub fn set_maximized(&self, maximized: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MAXIMIZED, maximized)
});
});
@@ -697,7 +680,7 @@ impl Window {
#[inline]
pub fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
let mut window_state_lock = window_state.lock().unwrap();
@@ -708,7 +691,7 @@ impl Window {
_ if old_fullscreen == fullscreen => return,
// Return if saved Borderless(monitor) is the same as current monitor when requested fullscreen is Borderless(None)
(Some(Fullscreen::Borderless(Some(monitor))), Some(Fullscreen::Borderless(None)))
if *monitor == monitor::current_monitor(window.0) =>
if *monitor == monitor::current_monitor(window) =>
{
return
}
@@ -777,7 +760,7 @@ impl Window {
}
// Update window style
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(
WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN,
matches!(fullscreen, Some(Fullscreen::Exclusive(_))),
@@ -793,7 +776,7 @@ impl Window {
// this needs to be called before the below fullscreen SetWindowPos as this itself
// will generate WM_SIZE messages of the old window size that can race with what we set below
unsafe {
taskbar_mark_fullscreen(window.0, fullscreen.is_some());
taskbar_mark_fullscreen(window, fullscreen.is_some());
}
// Update window bounds
@@ -802,7 +785,7 @@ impl Window {
// Save window bounds before entering fullscreen
let placement = unsafe {
let mut placement = mem::zeroed();
GetWindowPlacement(window.0, &mut placement);
GetWindowPlacement(window, &mut placement);
placement
};
@@ -811,7 +794,7 @@ impl Window {
let monitor = match &fullscreen {
Fullscreen::Exclusive(video_mode) => video_mode.monitor(),
Fullscreen::Borderless(Some(monitor)) => monitor.clone(),
Fullscreen::Borderless(None) => monitor::current_monitor(window.0),
Fullscreen::Borderless(None) => monitor::current_monitor(window),
};
let position: (i32, i32) = monitor.position().into();
@@ -819,7 +802,7 @@ impl Window {
unsafe {
SetWindowPos(
window.0,
window,
0,
position.0,
position.1,
@@ -827,7 +810,7 @@ impl Window {
size.1 as i32,
SWP_ASYNCWINDOWPOS | SWP_NOZORDER,
);
InvalidateRgn(window.0, 0, false.into());
InvalidateRgn(window, 0, false.into());
}
}
None => {
@@ -835,8 +818,8 @@ impl Window {
if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() {
drop(window_state_lock);
unsafe {
SetWindowPlacement(window.0, &placement);
InvalidateRgn(window.0, 0, false.into());
SetWindowPlacement(window, &placement);
InvalidateRgn(window, 0, false.into());
}
}
}
@@ -846,12 +829,12 @@ impl Window {
#[inline]
pub fn set_decorations(&self, decorations: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MARKER_DECORATIONS, decorations)
});
});
@@ -867,12 +850,12 @@ impl Window {
#[inline]
pub fn set_window_level(&self, level: WindowLevel) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(
WindowFlags::ALWAYS_ON_TOP,
level == WindowLevel::AlwaysOnTop,
@@ -921,21 +904,21 @@ impl Window {
#[inline]
pub fn set_ime_cursor_area(&self, spot: Position, size: Size) {
let window = self.window.clone();
let window = self.window;
let state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || unsafe {
let scale_factor = state.lock().unwrap().scale_factor;
ImeContext::current(window.0).set_ime_cursor_area(spot, size, scale_factor);
ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor);
});
}
#[inline]
pub fn set_ime_allowed(&self, allowed: bool) {
let window = self.window.clone();
let window = self.window;
let state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || unsafe {
state.lock().unwrap().ime_allowed = allowed;
ImeContext::set_ime_allowed(window.0, allowed);
ImeContext::set_ime_allowed(window, allowed);
})
}
@@ -944,14 +927,13 @@ impl Window {
#[inline]
pub fn request_user_attention(&self, request_type: Option<UserAttentionType>) {
let window = self.window.clone();
let window = self.window;
let active_window_handle = unsafe { GetActiveWindow() };
if window.0 == active_window_handle {
if window == active_window_handle {
return;
}
self.thread_executor.execute_in_thread(move || unsafe {
let _ = &window;
let (flags, count) = request_type
.map(|ty| match ty {
UserAttentionType::Critical => (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX),
@@ -961,7 +943,7 @@ impl Window {
let flash_info = FLASHWINFO {
cbSize: mem::size_of::<FLASHWINFO>() as u32,
hwnd: window.0,
hwnd: window,
dwFlags: flags,
uCount: count,
dwTimeout: 0,
@@ -972,7 +954,7 @@ impl Window {
#[inline]
pub fn set_theme(&self, theme: Option<Theme>) {
try_theme(self.window.0, theme);
try_theme(self.window, theme);
}
#[inline]
@@ -987,9 +969,9 @@ impl Window {
}
pub fn title(&self) -> String {
let len = unsafe { GetWindowTextLengthW(self.window.0) } + 1;
let len = unsafe { GetWindowTextLengthW(self.window) } + 1;
let mut buf = vec![0; len as usize];
unsafe { GetWindowTextW(self.window.0, buf.as_mut_ptr(), len) };
unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) };
util::decode_wide(&buf).to_string_lossy().to_string()
}
@@ -1001,12 +983,12 @@ impl Window {
#[inline]
pub fn set_undecorated_shadow(&self, shadow: bool) {
let window = self.window.clone();
let window = self.window;
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow)
});
});
@@ -1014,15 +996,14 @@ impl Window {
#[inline]
pub fn focus_window(&self) {
let window = self.window.clone();
let window_flags = self.window_state_lock().window_flags();
let is_visible = window_flags.contains(WindowFlags::VISIBLE);
let is_minimized = util::is_minimized(self.hwnd());
let is_foreground = window.0 == unsafe { GetForegroundWindow() };
let is_foreground = self.window == unsafe { GetForegroundWindow() };
if is_visible && !is_minimized && !is_foreground {
unsafe { force_window_active(window.0) };
unsafe { force_window_active(self.window) };
}
}
@@ -1072,18 +1053,6 @@ impl Drop for Window {
}
}
/// A simple non-owning wrapper around a window.
#[doc(hidden)]
#[derive(Clone)]
pub struct WindowWrapper(HWND);
// Send and Sync are not implemented for HWND and HDC, we have to wrap it and implement them manually.
// For more info see:
// https://github.com/retep998/winapi-rs/issues/360
// https://github.com/retep998/winapi-rs/issues/396
unsafe impl Sync for WindowWrapper {}
unsafe impl Send for WindowWrapper {}
pub(super) struct InitData<'a, T: 'static> {
// inputs
pub event_loop: &'a EventLoopWindowTarget<T>,
@@ -1131,7 +1100,7 @@ impl<'a, T: 'static> InitData<'a, T> {
unsafe { ImeContext::set_ime_allowed(window, false) };
Window {
window: WindowWrapper(window),
window,
window_state,
thread_executor: self.event_loop.create_thread_executor(),
}
@@ -1154,7 +1123,7 @@ impl<'a, T: 'static> InitData<'a, T> {
let file_drop_runner = self.event_loop.runner_shared.clone();
let file_drop_handler = FileDropHandler::new(
win.window.0,
win.window,
Box::new(move |event| {
if let Ok(e) = event.map_nonuser_event() {
file_drop_runner.send_event(e)
@@ -1166,7 +1135,7 @@ impl<'a, T: 'static> InitData<'a, T> {
unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void };
assert_eq!(
unsafe { RegisterDragDrop(win.window.0, handler_interface_ptr) },
unsafe { RegisterDragDrop(win.window, handler_interface_ptr) },
S_OK
);
Some(file_drop_handler)
@@ -1243,7 +1212,7 @@ impl<'a, T: 'static> InitData<'a, T> {
if attributes.fullscreen.0.is_some() {
win.set_fullscreen(attributes.fullscreen.0.map(Into::into));
unsafe { force_window_active(win.window.0) };
unsafe { force_window_active(win.window) };
} else {
let size = attributes
.inner_size

View File

@@ -2,8 +2,8 @@ use crate::{
dpi::{PhysicalPosition, PhysicalSize, Size},
icon::Icon,
keyboard::ModifiersState,
platform_impl::platform::{event_loop, util, Fullscreen, SelectedCursor},
window::{Theme, WindowAttributes},
platform_impl::platform::{event_loop, util, Fullscreen},
window::{CursorIcon, Theme, WindowAttributes},
};
use std::io;
use std::sync::MutexGuard;
@@ -67,7 +67,7 @@ pub struct SavedWindow {
#[derive(Clone)]
pub struct MouseProperties {
pub(crate) selected_cursor: SelectedCursor,
pub cursor: CursorIcon,
pub capture_count: u32,
cursor_flags: CursorFlags,
pub last_position: Option<PhysicalPosition<f64>>,
@@ -143,7 +143,7 @@ impl WindowState {
) -> WindowState {
WindowState {
mouse: MouseProperties {
selected_cursor: SelectedCursor::default(),
cursor: CursorIcon::default(),
capture_count: 0,
cursor_flags: CursorFlags::empty(),
last_position: None,

View File

@@ -9,7 +9,6 @@ use crate::{
platform_impl, SendSyncWrapper,
};
pub use crate::cursor::{BadImage, CustomCursor, MAX_CURSOR_SIZE};
pub use crate::icon::{BadIcon, Icon};
#[doc(inline)]
@@ -1078,7 +1077,8 @@ impl Window {
/// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request.
/// - **Windows:** Screen saver is disabled in fullscreen mode.
/// - **Android / Orbital:** Unsupported.
/// - **Web:** Does nothing without a [transient activation].
/// - **Web:** Does nothing without a [transient activation], but queues the request
/// for the next activation.
///
/// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
#[inline]
@@ -1336,7 +1336,6 @@ impl Window {
/// Cursor functions.
impl Window {
/// Modifies the cursor icon of the window.
/// Overwrites cursors set in [`Window::set_custom_cursor`].
///
/// ## Platform-specific
///
@@ -1347,19 +1346,6 @@ impl Window {
.maybe_queue_on_main(move |w| w.set_cursor_icon(cursor))
}
/// Modifies the cursor icon of the window with a custom cursor.
/// Overwrites cursors set in [`Window::set_cursor_icon`].
///
/// ## Platform-specific
///
/// - **iOS / Android / Orbital:** Unsupported.
#[inline]
pub fn set_custom_cursor(&self, cursor: &CustomCursor) {
let cursor = cursor.clone();
self.window
.maybe_queue_on_main(move |w| w.set_custom_cursor(cursor))
}
/// Changes the position of the cursor in window coordinates.
///
/// ```no_run
@@ -1489,10 +1475,6 @@ impl Window {
/// Returns the monitor on which the window currently resides.
///
/// Returns `None` if current monitor can't be detected.
///
/// ## Platform-specific
///
/// **iOS:** Can only be called on the main thread.
#[inline]
pub fn current_monitor(&self) -> Option<MonitorHandle> {
self.window
@@ -1503,10 +1485,6 @@ impl Window {
///
/// This is the same as [`EventLoopWindowTarget::available_monitors`], and is provided for convenience.
///
/// ## Platform-specific
///
/// **iOS:** Can only be called on the main thread.
///
/// [`EventLoopWindowTarget::available_monitors`]: crate::event_loop::EventLoopWindowTarget::available_monitors
#[inline]
pub fn available_monitors(&self) -> impl Iterator<Item = MonitorHandle> {
@@ -1525,8 +1503,7 @@ impl Window {
///
/// ## Platform-specific
///
/// **iOS:** Can only be called on the main thread.
/// **Wayland:** Always returns `None`.
/// **Wayland / Web:** Always returns `None`.
///
/// [`EventLoopWindowTarget::primary_monitor`]: crate::event_loop::EventLoopWindowTarget::primary_monitor
#[inline]

View File

@@ -28,8 +28,3 @@ fn ids_send() {
needs_send::<winit::event::DeviceId>();
needs_send::<winit::monitor::MonitorHandle>();
}
#[test]
fn custom_cursor_send() {
needs_send::<winit::window::CustomCursor>();
}

View File

@@ -11,8 +11,3 @@ fn window_sync() {
fn window_builder_sync() {
needs_sync::<winit::window::WindowBuilder>();
}
#[test]
fn custom_cursor_sync() {
needs_sync::<winit::window::CustomCursor>();
}