mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 23:13:13 -04:00
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* Closes #7809
* Closes #7876
* Closes #7908
* Supersedes #7877
* Supersedes #7898
* The author of the PR above replaced it with #7914, which additionally
fixes another IME issue. I believe that fix deserves a separate PR.
* Reverts #4794
* [x] I have followed the instructions in the PR template
This approach is better than #7898 (#7914) because it correctly handles
all three major IME types (Chinese, Japanese, and Korean) without
requiring a predefined “IME mode”.
## Environments I haved tested this PR in
<details><summary>macOS 15.7.3 (AArch64, Host of other virtual
machines)</summary>
Run command: `cargo run -p egui_demo_app --release`
Tested IMEs:
- builtin Chinese IME (Shuangpin - Simplified)
- builtin Japanese IME (Romaji)
- builtin Korean IME (2-Set)
</details>
<details><summary>Windows 11 25H2 (AArch64, Virtual Machine)</summary>
Build command: `cargo build --release -p egui_demo_app
--target=x86_64-pc-windows-gnu --features=glow --no-default-features`
(I cannot use `wgpu` due to [this
bug](https://github.com/emilk/egui/issues/4381), which prevents
debugging inside the VM. Anyways, the rendering backend should be
irrelevant here.)
Tested IMEs:
- builtin Chinese IME (Shuangpin)
- Sogou IME (Chinese Shuangpin)
- WeType IME (Chinese Shuangpin)
- builtin Japanese IME (Hiragana)
- builtin Korean IME (2 Beolsik)
</details>
<details><summary>Linux [Wayland + IBus] (AArch64, Virtual
Machine)</summary>
Fedora KDE Plasma Desktop 43 [Wayland + IBus 1.5.33-rc2]
(Not working at the moment because of [another
issue](https://github.com/emilk/egui/issues/7485) that will be fixed by
#7983. It is [a complicated
story](https://github.com/emilk/egui/pull/7973#issuecomment-4074627603).
)
> [!NOTE]
>
> IBus is partially broken in this system. The Input Method Selector
refuses to select IBus. As a workaround, I have to open System Settings
-> Virtual Keyboard and select “IBus Wayland” to start an IBus instance
that works in egui.
>
> The funny thing is: the Chinese Intelligent Pinyin IME is broken in
native Apps like System Settings and KWrite, but works correctly in
egui!
>
> <details><summary>Screencast: What</summary>
>
> 
> </details>
Build command: `cross build --release -p egui_demo_app
--target=aarch64-unknown-linux-gnu --features=wayland,wgpu
--no-default-features`
(The Linux toolchain on my mac is somehow broken, so I used `cross`
instead.)
Tested IMEs:
- Chinese Intelligent Pinyin IME (Shuangpin)
- Japanese Anthy IME (Hiragana)
- Korean Hangul IME
</details>
<details><summary>Linux [X11 + Fcitx5] (AArch64, Virtual
Machine)</summary>
Debian 13 [Cinnamon 6.4.10 + X11 + Fcitx5 5.1.2]
Build command: `cross build --release -p egui_demo_app
--target=aarch64-unknown-linux-gnu --features=x11,wgpu
--no-default-features`
Tested IMEs:
- Chinese Shuangpin IME
- Chinese Rime IME with `luna-pinyin`
- Japanese Mozc IME (Hiragana)
- Korean Hangul IME
Unlike macOS and Linux + Wayland, key-release events for keys processed
by the IME are still forwarded to `egui`. These appear to be harmless in
practice.
Unlike on Windows, however, they cannot be filtered reliably because
there are no corresponding key-press events marked as “processed by
IME”.
</details>
---
There are too many possible combinations to test (Operating Systems ×
[Desktop
Environment](https://en.wikipedia.org/wiki/Desktop_environment)s ×
[Windowing System](https://en.wikipedia.org/wiki/Windowing_system)s ×
[IMF](https://wiki.archlinux.org/title/Input_method#Input_method_framework)s
× [IME](https://en.wikipedia.org/wiki/Input_method)s × …), and I only
have access to a limited subset. For example, Google Japanese Input
refused to install on my Windows VM, and some paid Japanese IMEs are not
accessible to me. Therefore, I would appreciate feedback from people
other than me using all kinds of environments.
## Details
There are two possible approaches to removing keyboard events that have
already been processed by an IME:
* Approach 1: Filter out events inside `egui` that appear to have been
received during IME composition.
* Approach 2: Filter out such events in the platform backend
(terminology [borrowed from
imgui](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md#using-standard-backends),
e.g. the `egui-winit` crate or the code under `web/` in the `eframe`
crate.).
Both approaches already exist in `egui`:
* #4794 uses the first approach, filtering these events in the
`TextEdit`-related code.
* `eframe` uses the second approach in its web integration. See:
<14afefa252/crates/eframe/src/web/events.rs (L173-L176)>
Compared to the first approach, the second has a clear advantage: when
events are passed from the platform backends into `egui`, they are
simplified and lose information. In contrast, events in the platform
backends are the original events, which allows them to be handled more
flexibly. This is also why #7898 (#7914), which attempts to address the
issue from within the `egui` crate, struggles to make all IMEs work
correctly at the same time and requires manually selecting an “IME
mode”: the events received by `egui` have already been reduced and
therefore lack necessary information.
A more appropriate solution is to consistently follow the second
approach, explicitly requiring platform backends not to forward events
that have already been processed by the IME to `egui`. This is the
method used in this PR. Specifically, this PR works within the
`egui-winit` crate, where the original `KeyboardInput` events can be
accessed. At least for key press events, these can be used directly to
determine whether the event has already been processed by the IME on
Windows (by checking whether `logical_key` equals
`winit:⌨️:NamedKey::Process`). This makes it straightforward to
ensure that all IMEs work correctly at the same time.
This PR also reverts #4794, which took the first approach. It filters
out some events that merely look like they were received during IME
composition but actually are not. It also messes up the order of those
events along the way.
As a result, it caused several IME-related issues. One of the sections
in the Demonstrations below will illustrate these problems.
## Demonstrations
<details><summary>Changes not included in this PR for displaying Unicode
characters in demonstrations</summary>
Download `unifont-17.0.03.otf` from
<https://unifoundry.com/pub/unifont/unifont-17.0.03/font-builds/>, and
place it at `crates/egui_demo_app/src/unifont-17.0.03.otf`.
In `crates/egui_demo_app/src/wrap_app.rs`, add these lines at the
beginning of `impl WrapApp`'s `pub fn new`:
```rust
{
const MAIN_FONT: &'static [u8] = include_bytes!("./unifont-17.0.03.otf");
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"main-font".to_owned(),
std::sync::Arc::new(egui::FontData::from_static(MAIN_FONT)),
);
let proportional = fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default();
proportional.insert(0, "main-font".to_owned());
cc.egui_ctx.set_fonts(fonts);
}
```
(I took this from somewhere, but I forgot where it is. Sorry…)
</details>
[GNU Unifont](https://unifoundry.com/unifont/index.html) is licensed
under [OFL-1.1](https://unifoundry.com/OFL-1.1.txt).
### This PR Fixes: Focus on a single-line `TextEdit` is lost after
completing candidate selection with Japanese IME on Windows (#7809)
<details><summary>Screencast: ✅ Japanese IME now behaves correctly while
Korean IME behaves as before</summary>

</details>
### This PR Fixes: Committing Japanese IME text with <kbd>Enter</kbd>
inserts an unintended newline in multiline `TextEdit` on Windows (#7876)
<details><summary>Screencast: ✅ Japanese IME now behaves correctly while
Korean IME behaves as before</summary>

</details>
### This PR Fixes: Backspacing deletes characters during composition in
certain Chinese IMEs (e.g., Sogou) on Windows (#7908)
<details><summary>Screencast: ✅ Sogou IME now behaves
correctly</summary>

</details>
### This PR Obsoletes #4794, because `egui` receives only IME events
during composition from now on
On Windows, “incompatible” events are filtered in `egui-winit`, aligning
the behavior with other systems.
<details><summary>Screencasts</summary>
Some Chinese IMEs on Windows:

The default Japanese IMEs on Windows:

</details>
The 2-set Korean IMEs handle arrow keys differently. It will be
discussed in the next section.
### This PR Reverts #4794, because it introduced several bugs
Some of its bugs have already been worked around in the past, but those
workarounds might also be problematic. For example, #4912 is a
workaround for a bug (#4908) introduced by #4794, and that workaround is
in fact the root cause of the macOS backspacing bug I have worked around
with #7810. (The reversion of #4912 is out of the scope of this PR, I
will do that in #7983.)
#### It Caused: Arrow keys are incorrectly blocked during typical Korean
IME composition
When composing Korean text using 2-Set IMEs, users should still be able
to move the cursor with arrow keys regardless if the composition is
committed.
##### Correct behavior
<details><summary>Screencasts</summary>
macOS TextEdit:

Windows Notepad:

With #4794 reverted, `egui` also behaves correctly (tested on Linux +
Wayland, macOS, and Windows):

</details>
##### Incorrect behavior caused by #4794
`remove_ime_incompatible_events` removed arrow-key events in such cases.
As a result, the first arrow key press only commits the composition, and
users need to press the arrow key again to move the cursor:
<details><summary>Screencast</summary>

</details>
This is essentially the same issue described here:
https://github.com/emilk/egui/pull/7877#issuecomment-3852719948
#### It Caused: Backspacing leaves the last character in Korean IME
pre-edit text not removed on macOS
<details><summary>Screencasts</summary>
Before this PR:

After this PR:

</details>
### Korean IMEs also use <kbd>Enter</kbd> to confirm Hanja selections,
and will not work properly in the Korean “IME mode” proposed by #7898
(#7914)
<details><summary>Screencast: Korean IME using <kbd>Enter</kbd> and
<kbd>Space</kbd> for confirmation (IBus Korean Hangul IME)</summary>
The screencast below demonstrates that some Korean IMEs handle Hanja
selection in a way similar to Japanese IMEs: the
<kbd>Up</kbd>/<kbd>Down</kbd> arrow keys are used to navigate
candidates, and <kbd>Enter</kbd> confirms the selected candidate.

</details>
<details><summary>Screencasts: Another example</summary>
Using the built-in Korean IME on Windows, I type two lines: the first
line in Hangul, and the second line as the same word converted to Hanja.
Correct behavior in Notepad (reference):

Behavior after applying this PR, which matches the Notepad behavior:

Behavior after applying #7914 with the “IME mode” set to Korean (which
is also the behavior before this PR being applied):

On the second line, each time a Hanja character is confirmed, an
unintended newline is inserted. This mirrors the Japanese IME issues
that are supposed to be fixed by setting the “IME mode” to Japanese.
(These Japanese IME issues are fixed in this PR as mentioned before.)
</details>
2144 lines
84 KiB
Rust
2144 lines
84 KiB
Rust
//! [`egui`] bindings for [`winit`](https://github.com/rust-windowing/winit).
|
|
//!
|
|
//! The library translates winit events to egui, handled copy/paste,
|
|
//! updates the cursor, open links clicked in egui, etc.
|
|
//!
|
|
//! ## Feature flags
|
|
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
|
//!
|
|
|
|
#![expect(clippy::manual_range_contains)]
|
|
|
|
#[cfg(target_os = "windows")]
|
|
use std::collections::HashSet;
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
pub use accesskit_winit;
|
|
pub use egui;
|
|
#[cfg(feature = "accesskit")]
|
|
use egui::accesskit;
|
|
use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo};
|
|
pub use winit;
|
|
|
|
pub mod clipboard;
|
|
mod safe_area;
|
|
mod window_settings;
|
|
|
|
pub use window_settings::WindowSettings;
|
|
|
|
use raw_window_handle::HasDisplayHandle;
|
|
|
|
use winit::{
|
|
dpi::{PhysicalPosition, PhysicalSize},
|
|
event::ElementState,
|
|
event_loop::ActiveEventLoop,
|
|
window::{CursorGrabMode, Window, WindowButtons, WindowLevel},
|
|
};
|
|
|
|
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
|
|
let size = if cfg!(target_os = "ios") {
|
|
// `outer_size` Includes the area behind the "dynamic island".
|
|
// It is up to the eframe user to make sure the dynamic island doesn't cover anything important.
|
|
// That will be easier once https://github.com/rust-windowing/winit/pull/3890 lands
|
|
window.outer_size()
|
|
} else {
|
|
window.inner_size()
|
|
};
|
|
egui::vec2(size.width as f32, size.height as f32)
|
|
}
|
|
|
|
/// Calculate the `pixels_per_point` for a given window, given the current egui zoom factor
|
|
pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 {
|
|
let native_pixels_per_point = window.scale_factor() as f32;
|
|
let egui_zoom_factor = egui_ctx.zoom_factor();
|
|
egui_zoom_factor * native_pixels_per_point
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#[must_use]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct EventResponse {
|
|
/// If true, egui consumed this event, i.e. wants exclusive use of this event
|
|
/// (e.g. a mouse click on an egui window, or entering text into a text field).
|
|
///
|
|
/// For instance, if you use egui for a game, you should only
|
|
/// pass on the events to your game when [`Self::consumed`] is `false`.
|
|
///
|
|
/// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs.
|
|
pub consumed: bool,
|
|
|
|
/// Do we need an egui refresh because of this event?
|
|
pub repaint: bool,
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// Handles the integration between egui and a winit Window.
|
|
///
|
|
/// Instantiate one of these per viewport/window.
|
|
pub struct State {
|
|
/// Shared clone.
|
|
egui_ctx: egui::Context,
|
|
|
|
viewport_id: ViewportId,
|
|
start_time: web_time::Instant,
|
|
egui_input: egui::RawInput,
|
|
pointer_pos_in_points: Option<egui::Pos2>,
|
|
any_pointer_button_down: bool,
|
|
current_cursor_icon: Option<egui::CursorIcon>,
|
|
|
|
clipboard: clipboard::Clipboard,
|
|
|
|
/// If `true`, mouse inputs will be treated as touches.
|
|
/// Useful for debugging touch support in egui.
|
|
///
|
|
/// Creates duplicate touches, if real touch inputs are coming.
|
|
simulate_touch_screen: bool,
|
|
|
|
/// Is Some(…) when a touch is being translated to a pointer.
|
|
///
|
|
/// Only one touch will be interpreted as pointer at any time.
|
|
pointer_touch_id: Option<u64>,
|
|
|
|
/// track ime state
|
|
has_sent_ime_enabled: bool,
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
pub accesskit: Option<accesskit_winit::Adapter>,
|
|
|
|
allow_ime: bool,
|
|
ime_rect_px: Option<egui::Rect>,
|
|
|
|
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
|
|
/// release events that should be filtered out. See comments in that method
|
|
/// for details.
|
|
#[cfg(target_os = "windows")]
|
|
pressed_processed_physical_keys: HashSet<winit::keyboard::PhysicalKey>,
|
|
}
|
|
|
|
impl State {
|
|
/// Construct a new instance
|
|
pub fn new(
|
|
egui_ctx: egui::Context,
|
|
viewport_id: ViewportId,
|
|
display_target: &dyn HasDisplayHandle,
|
|
native_pixels_per_point: Option<f32>,
|
|
theme: Option<winit::window::Theme>,
|
|
max_texture_side: Option<usize>,
|
|
) -> Self {
|
|
profiling::function_scope!();
|
|
|
|
let egui_input = egui::RawInput {
|
|
focused: false, // winit will tell us when we have focus
|
|
..Default::default()
|
|
};
|
|
|
|
let mut slf = Self {
|
|
egui_ctx,
|
|
viewport_id,
|
|
start_time: web_time::Instant::now(),
|
|
egui_input,
|
|
pointer_pos_in_points: None,
|
|
any_pointer_button_down: false,
|
|
current_cursor_icon: None,
|
|
|
|
clipboard: clipboard::Clipboard::new(
|
|
display_target.display_handle().ok().map(|h| h.as_raw()),
|
|
),
|
|
|
|
simulate_touch_screen: false,
|
|
pointer_touch_id: None,
|
|
|
|
has_sent_ime_enabled: false,
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
accesskit: None,
|
|
|
|
allow_ime: false,
|
|
ime_rect_px: None,
|
|
#[cfg(target_os = "windows")]
|
|
pressed_processed_physical_keys: HashSet::new(),
|
|
};
|
|
|
|
slf.egui_input
|
|
.viewports
|
|
.entry(ViewportId::ROOT)
|
|
.or_default()
|
|
.native_pixels_per_point = native_pixels_per_point;
|
|
slf.egui_input.system_theme = theme.map(to_egui_theme);
|
|
|
|
if let Some(max_texture_side) = max_texture_side {
|
|
slf.set_max_texture_side(max_texture_side);
|
|
}
|
|
slf
|
|
}
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
window: &Window,
|
|
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
|
) {
|
|
profiling::function_scope!();
|
|
|
|
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
|
|
event_loop,
|
|
window,
|
|
event_loop_proxy,
|
|
));
|
|
}
|
|
|
|
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
|
/// that egui will use.
|
|
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
|
self.egui_input.max_texture_side = Some(max_texture_side);
|
|
}
|
|
|
|
/// Fetches text from the clipboard and returns it.
|
|
pub fn clipboard_text(&mut self) -> Option<String> {
|
|
self.clipboard.get()
|
|
}
|
|
|
|
/// Places the text onto the clipboard.
|
|
pub fn set_clipboard_text(&mut self, text: String) {
|
|
self.clipboard.set_text(text);
|
|
}
|
|
|
|
/// Returns [`false`] or the last value that [`Window::set_ime_allowed()`] was called with, used for debouncing.
|
|
pub fn allow_ime(&self) -> bool {
|
|
self.allow_ime
|
|
}
|
|
|
|
/// Set the last value that [`Window::set_ime_allowed()`] was called with.
|
|
pub fn set_allow_ime(&mut self, allow: bool) {
|
|
self.allow_ime = allow;
|
|
}
|
|
|
|
#[inline]
|
|
pub fn egui_ctx(&self) -> &egui::Context {
|
|
&self.egui_ctx
|
|
}
|
|
|
|
/// The current input state.
|
|
/// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`].
|
|
#[inline]
|
|
pub fn egui_input(&self) -> &egui::RawInput {
|
|
&self.egui_input
|
|
}
|
|
|
|
/// The current input state.
|
|
/// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`].
|
|
#[inline]
|
|
pub fn egui_input_mut(&mut self) -> &mut egui::RawInput {
|
|
&mut self.egui_input
|
|
}
|
|
|
|
/// Prepare for a new frame by extracting the accumulated input,
|
|
///
|
|
/// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect).
|
|
///
|
|
/// You need to set [`egui::RawInput::viewports`] yourself though.
|
|
/// Use [`update_viewport_info`] to update the info for each
|
|
/// viewport.
|
|
pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput {
|
|
profiling::function_scope!();
|
|
|
|
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
|
|
|
|
// On Windows, a minimized window will have 0 width and height.
|
|
// See: https://github.com/rust-windowing/winit/issues/208
|
|
// This solves an issue where egui window positions would be changed when minimizing on Windows.
|
|
let screen_size_in_pixels = screen_size_in_pixels(window);
|
|
let screen_size_in_points =
|
|
screen_size_in_pixels / pixels_per_point(&self.egui_ctx, window);
|
|
|
|
self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
|
|
&& screen_size_in_points.y > 0.0)
|
|
.then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
|
|
|
|
// Tell egui which viewport is now active:
|
|
self.egui_input.viewport_id = self.viewport_id;
|
|
|
|
self.egui_input
|
|
.viewports
|
|
.entry(self.viewport_id)
|
|
.or_default()
|
|
.native_pixels_per_point = Some(window.scale_factor() as f32);
|
|
|
|
self.egui_input.take()
|
|
}
|
|
|
|
/// Call this when there is a new event.
|
|
///
|
|
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
|
pub fn on_window_event(
|
|
&mut self,
|
|
window: &Window,
|
|
event: &winit::event::WindowEvent,
|
|
) -> EventResponse {
|
|
profiling::function_scope!(short_window_event_description(event));
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
if let Some(accesskit) = self.accesskit.as_mut() {
|
|
accesskit.process_event(window, event);
|
|
}
|
|
|
|
use winit::event::WindowEvent;
|
|
|
|
#[cfg(target_os = "ios")]
|
|
match &event {
|
|
WindowEvent::Resized(_)
|
|
| WindowEvent::ScaleFactorChanged { .. }
|
|
| WindowEvent::Focused(true)
|
|
| WindowEvent::Occluded(false) => {
|
|
// Once winit v0.31 has been released this can be reworked to get the safe area from
|
|
// `Window::safe_area`, and updated from a new event which is being discussed in
|
|
// https://github.com/rust-windowing/winit/issues/3911.
|
|
self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match event {
|
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
|
let native_pixels_per_point = *scale_factor as f32;
|
|
|
|
self.egui_input
|
|
.viewports
|
|
.entry(self.viewport_id)
|
|
.or_default()
|
|
.native_pixels_per_point = Some(native_pixels_per_point);
|
|
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::MouseInput { state, button, .. } => {
|
|
self.on_mouse_button_input(*state, *button);
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_pointer_input(),
|
|
}
|
|
}
|
|
WindowEvent::MouseWheel { delta, phase, .. } => {
|
|
self.on_mouse_wheel(window, *delta, *phase);
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_pointer_input(),
|
|
}
|
|
}
|
|
WindowEvent::CursorMoved { position, .. } => {
|
|
self.on_cursor_moved(window, *position);
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_is_using_pointer(),
|
|
}
|
|
}
|
|
WindowEvent::CursorLeft { .. } => {
|
|
self.pointer_pos_in_points = None;
|
|
self.egui_input.events.push(egui::Event::PointerGone);
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
// WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO(emilk)
|
|
WindowEvent::Touch(touch) => {
|
|
self.on_touch(window, touch);
|
|
let consumed = match touch.phase {
|
|
winit::event::TouchPhase::Started
|
|
| winit::event::TouchPhase::Ended
|
|
| winit::event::TouchPhase::Cancelled => {
|
|
self.egui_ctx.egui_wants_pointer_input()
|
|
}
|
|
winit::event::TouchPhase::Moved => self.egui_ctx.egui_is_using_pointer(),
|
|
};
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed,
|
|
}
|
|
}
|
|
|
|
WindowEvent::Ime(ime) => {
|
|
self.on_ime(ime);
|
|
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_keyboard_input(),
|
|
}
|
|
}
|
|
WindowEvent::KeyboardInput {
|
|
event,
|
|
is_synthetic,
|
|
..
|
|
} => {
|
|
if *is_synthetic && event.state == ElementState::Pressed {
|
|
// Winit generates fake "synthetic" KeyboardInput events when the focus
|
|
// is changed to the window, or away from it. Synthetic key presses
|
|
// represent no real key presses and should be ignored.
|
|
// See https://github.com/rust-windowing/winit/issues/3543
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
} else {
|
|
let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
|
|
|
|
if let Some(response) =
|
|
self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
|
|
{
|
|
response
|
|
} else {
|
|
self.on_keyboard_input(event);
|
|
|
|
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
|
|
let consumed = egui_wants_keyboard_input
|
|
|| event.logical_key
|
|
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WindowEvent::Focused(focused) => {
|
|
let focused = if cfg!(target_os = "macos") {
|
|
// TODO(emilk): remove this work-around once we update winit
|
|
// https://github.com/rust-windowing/winit/issues/4371
|
|
// https://github.com/emilk/egui/issues/7588
|
|
window.has_focus()
|
|
} else {
|
|
*focused
|
|
};
|
|
|
|
self.egui_input.focused = focused;
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::WindowFocused(focused));
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::ThemeChanged(winit_theme) => {
|
|
self.egui_input.system_theme = Some(to_egui_theme(*winit_theme));
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::HoveredFile(path) => {
|
|
self.egui_input.hovered_files.push(egui::HoveredFile {
|
|
path: Some(path.clone()),
|
|
..Default::default()
|
|
});
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::HoveredFileCancelled => {
|
|
self.egui_input.hovered_files.clear();
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::DroppedFile(path) => {
|
|
self.egui_input.hovered_files.clear();
|
|
self.egui_input.dropped_files.push(egui::DroppedFile {
|
|
path: Some(path.clone()),
|
|
..Default::default()
|
|
});
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
WindowEvent::ModifiersChanged(state) => {
|
|
let state = state.state();
|
|
|
|
let alt = state.alt_key();
|
|
let ctrl = state.control_key();
|
|
let shift = state.shift_key();
|
|
let super_ = state.super_key();
|
|
|
|
self.egui_input.modifiers.alt = alt;
|
|
self.egui_input.modifiers.ctrl = ctrl;
|
|
self.egui_input.modifiers.shift = shift;
|
|
self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_;
|
|
self.egui_input.modifiers.command = if cfg!(target_os = "macos") {
|
|
super_
|
|
} else {
|
|
ctrl
|
|
};
|
|
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
}
|
|
}
|
|
|
|
// Things that may require repaint:
|
|
WindowEvent::RedrawRequested
|
|
| WindowEvent::CursorEntered { .. }
|
|
| WindowEvent::Destroyed
|
|
| WindowEvent::Occluded(_)
|
|
| WindowEvent::Resized(_)
|
|
| WindowEvent::Moved(_)
|
|
| WindowEvent::TouchpadPressure { .. }
|
|
| WindowEvent::CloseRequested => EventResponse {
|
|
repaint: true,
|
|
consumed: false,
|
|
},
|
|
|
|
// Things we completely ignore:
|
|
WindowEvent::ActivationTokenDone { .. }
|
|
| WindowEvent::AxisMotion { .. }
|
|
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
|
|
repaint: false,
|
|
consumed: false,
|
|
},
|
|
|
|
WindowEvent::PinchGesture { delta, .. } => {
|
|
// Positive delta values indicate magnification (zooming in).
|
|
// Negative delta values indicate shrinking (zooming out).
|
|
let zoom_factor = (*delta as f32).exp();
|
|
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_pointer_input(),
|
|
}
|
|
}
|
|
|
|
WindowEvent::RotationGesture { delta, .. } => {
|
|
// Positive delta values indicate counterclockwise rotation
|
|
// Negative delta values indicate clockwise rotation
|
|
// This is opposite of egui's sign convention for angles
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Rotate(-delta.to_radians()));
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_pointer_input(),
|
|
}
|
|
}
|
|
|
|
WindowEvent::PanGesture { delta, phase, .. } => {
|
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
|
|
|
self.egui_input.events.push(egui::Event::MouseWheel {
|
|
unit: egui::MouseWheelUnit::Point,
|
|
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
|
phase: to_egui_touch_phase(*phase),
|
|
modifiers: self.egui_input.modifiers,
|
|
});
|
|
EventResponse {
|
|
repaint: true,
|
|
consumed: self.egui_ctx.egui_wants_pointer_input(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
#[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
|
|
#[inline(always)]
|
|
fn try_on_ime_processed_keyboard_input(
|
|
&mut self,
|
|
_event: &winit::event::KeyEvent,
|
|
_egui_wants_keyboard_input: bool,
|
|
) -> Option<EventResponse> {
|
|
// `KeyboardInput` events processed by the IME are not emitted by
|
|
// `winit` on non-Windows platforms, so we don't need to do anything
|
|
// here.
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[inline(always)]
|
|
fn try_on_ime_processed_keyboard_input(
|
|
&mut self,
|
|
event: &winit::event::KeyEvent,
|
|
egui_wants_keyboard_input: bool,
|
|
) -> Option<EventResponse> {
|
|
if !self.allow_ime {
|
|
None
|
|
} else if event.logical_key == winit::keyboard::NamedKey::Process {
|
|
// On Windows, the current version of `winit` (0.30.12) has a bug
|
|
// where `KeyboardInput` events processed by the IME are still
|
|
// emitted. [^1]
|
|
//
|
|
// As a workaround, we detect these events by checking whether their
|
|
// `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
|
|
// them out to keep behavior consistent with other platforms.
|
|
//
|
|
// `winit::keyboard::NamedKey::Process` is not documented in
|
|
// `winit`. Reading through its source code, we find that it is
|
|
// mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
|
|
// note, Web is the only other platform that also uses it [^3].)
|
|
// According to Microsoft, “the IME sets the virtual key value
|
|
// to `VK_PROCESSKEY` after processing a key input message” [^4].
|
|
// See also [^5].
|
|
// (I can't find a documentation page dedicated to this value.)
|
|
//
|
|
// TODO(umajho): Remove this workaround once the `winit` bug is fixed
|
|
// and we've updated to a version that includes the fix. NOTE: Don't
|
|
// forget to also remove the `pressed_processed_physical_keys` field
|
|
// and its related code.
|
|
//
|
|
// [^1]: https://github.com/rust-windowing/winit/issues/4508
|
|
// [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
|
|
// [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
// [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
|
|
// [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
|
|
|
|
self.pressed_processed_physical_keys
|
|
.insert(event.physical_key);
|
|
|
|
Some(EventResponse {
|
|
repaint: false,
|
|
consumed: egui_wants_keyboard_input,
|
|
})
|
|
} else if event.state == ElementState::Released
|
|
&& self
|
|
.pressed_processed_physical_keys
|
|
.remove(&event.physical_key)
|
|
{
|
|
// Unlike key-presses, we can not tell whether a key-release event
|
|
// is processed by the IME or not by looking at its `logical_key`,
|
|
// because their `logical_key` is the original value (e.g.
|
|
// `winit::keyboard::Key::Character(…)`) rather than
|
|
// `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
|
|
// (See the screencast for Windows in [^1].)
|
|
// So we track the physical keys of processed key-presses and
|
|
// filter out the corresponding key-releases.
|
|
//
|
|
// [^1]: https://github.com/rust-windowing/winit/issues/4508
|
|
|
|
Some(EventResponse {
|
|
repaint: false,
|
|
consumed: egui_wants_keyboard_input,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// ## NOTE
|
|
///
|
|
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
|
|
/// So no need to check `is_mac_cmd`.
|
|
///
|
|
/// ### How events are emitted by [`winit`] across different setups in various situations
|
|
///
|
|
/// This is done by uncommenting the code block at the top of this method
|
|
/// and checking console outputs.
|
|
///
|
|
/// winit version: 0.30.12.
|
|
///
|
|
/// #### Setups
|
|
///
|
|
/// - `a-macos15-apple_shuangpin`: macOS 15.7.3 `aarch64`, IME: builtin Chinese Shuangpin - Simplified. (Demo app shows: renderer: `wgpu`, backend: `Metal`.)
|
|
/// - `b-debian13_gnome48_wayland-fcitx5_shuangpin`: Debian 13 `aarch64`, Gnome 48, Wayland, IME: Fcitx5 with fcitx5-chinese-addons's Shuangpin. (Demo app shows: renderer: `wgpu`, backend: `Gl`.)
|
|
/// - `c-windows11-ms_pinyin`: Windows11 23H2 `x86_64`, IME: builtin Microsoft Pinyin. (Demo app shows: renderer: `wgpu`, backend: `Vulkan` & `Dx12`, others: `Dx12` & `Gl`.)
|
|
///
|
|
/// #### Situation: pressed space to select the first candidate "测试"
|
|
///
|
|
/// | Setup | Events in Order |
|
|
/// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
/// | a-macos15-apple_shuangpin | `Preedit("", None)` -> `Commit("测试")` |
|
|
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("测试")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
|
|
/// | c-windows11-ms_pinyin | `Preedit("测试", Some(…))` -> `Preedit("", None)` -> `Commit("测试")` -> `Disabled` |
|
|
///
|
|
/// #### Situation: pressed backspace to delete the last character in the composition
|
|
///
|
|
/// | Setup | Events in Order |
|
|
/// | a-macos15-apple_shuangpin | `Preedit("", None)` |
|
|
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
|
|
/// | c-windows11-ms_pinyin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled` |
|
|
///
|
|
/// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce"
|
|
///
|
|
/// | Setup | Events in Order |
|
|
/// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
/// | a-macos15-apple_shuangpin | nothing emitted |
|
|
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
|
|
/// | c-windows11-ms_pinyin | nothing emitted |
|
|
fn on_ime(&mut self, ime: &winit::event::Ime) {
|
|
// // code for inspecting ime events emitted by winit:
|
|
// {
|
|
// static LAST_IME: std::sync::Mutex<Option<winit::event::Ime>> =
|
|
// std::sync::Mutex::new(None);
|
|
// static IS_LAST_DUPLICATE: std::sync::atomic::AtomicBool =
|
|
// std::sync::atomic::AtomicBool::new(false);
|
|
// let mut last_ime_guard = LAST_IME.lock().unwrap();
|
|
// if { last_ime_guard.as_ref().cloned() }.as_ref() != Some(ime) {
|
|
// println!("IME={ime:?}");
|
|
// *last_ime_guard = Some(ime.clone());
|
|
// IS_LAST_DUPLICATE.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
// } else if !IS_LAST_DUPLICATE.load(std::sync::atomic::Ordering::Relaxed) {
|
|
// println!("IME=(duplicate)");
|
|
// IS_LAST_DUPLICATE.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
// }
|
|
// }
|
|
|
|
match ime {
|
|
winit::event::Ime::Enabled => {
|
|
if cfg!(target_os = "linux") {
|
|
// This event means different things in X11 and Wayland, but we can just
|
|
// ignore it and enable IME on the preedit event.
|
|
// See <https://github.com/rust-windowing/winit/issues/2498>
|
|
} else {
|
|
self.ime_event_enable();
|
|
}
|
|
}
|
|
winit::event::Ime::Preedit(text, Some(_cursor)) => {
|
|
self.ime_event_enable();
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
|
|
}
|
|
winit::event::Ime::Commit(text) => {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
|
|
self.ime_event_disable();
|
|
}
|
|
winit::event::Ime::Disabled => {
|
|
self.ime_event_disable();
|
|
}
|
|
winit::event::Ime::Preedit(_, None) => {
|
|
if cfg!(target_os = "macos") {
|
|
// On macOS, when the user presses backspace to delete the
|
|
// last character in an IME composition, `winit` only emits
|
|
// `winit::event::Ime::Preedit("", None)` without a
|
|
// preceding `winit::event::Ime::Preedit("", Some(0, 0))`.
|
|
//
|
|
// The current implementation of `egui::TextEdit` relies on
|
|
// receiving an `egui::ImeEvent::Preedit("")` to remove the
|
|
// last character in the composition in this case, so we
|
|
// emit it here.
|
|
//
|
|
// This is guarded to macOS-only, as applying it on other
|
|
// platforms is unnecessary and can cause undesired
|
|
// behavior.
|
|
// See: https://github.com/emilk/egui/pull/7973
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
|
|
}
|
|
|
|
self.ime_event_disable();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn ime_event_enable(&mut self) {
|
|
if !self.has_sent_ime_enabled {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Ime(egui::ImeEvent::Enabled));
|
|
self.has_sent_ime_enabled = true;
|
|
}
|
|
}
|
|
|
|
pub fn ime_event_disable(&mut self) {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Ime(egui::ImeEvent::Disabled));
|
|
self.has_sent_ime_enabled = false;
|
|
}
|
|
|
|
/// Returns `true` if the event was sent to egui.
|
|
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
|
|
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
|
|
return false;
|
|
}
|
|
|
|
self.egui_input.events.push(egui::Event::MouseMoved(Vec2 {
|
|
x: delta.0 as f32,
|
|
y: delta.1 as f32,
|
|
}));
|
|
true
|
|
}
|
|
|
|
/// Returns `true` when the pointer is currently inside the window.
|
|
pub fn is_pointer_in_window(&self) -> bool {
|
|
self.pointer_pos_in_points.is_some()
|
|
}
|
|
|
|
/// Returns `true` if any pointer button is currently held down.
|
|
pub fn is_any_pointer_button_down(&self) -> bool {
|
|
self.any_pointer_button_down
|
|
}
|
|
|
|
/// Call this when there is a new [`accesskit::ActionRequest`].
|
|
///
|
|
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
|
#[cfg(feature = "accesskit")]
|
|
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::AccessKitActionRequest(request));
|
|
}
|
|
|
|
fn on_mouse_button_input(
|
|
&mut self,
|
|
state: winit::event::ElementState,
|
|
button: winit::event::MouseButton,
|
|
) {
|
|
if let Some(pos) = self.pointer_pos_in_points
|
|
&& let Some(button) = translate_mouse_button(button)
|
|
{
|
|
let pressed = state == winit::event::ElementState::Pressed;
|
|
|
|
self.egui_input.events.push(egui::Event::PointerButton {
|
|
pos,
|
|
button,
|
|
pressed,
|
|
modifiers: self.egui_input.modifiers,
|
|
});
|
|
|
|
if self.simulate_touch_screen {
|
|
if pressed {
|
|
self.any_pointer_button_down = true;
|
|
|
|
self.egui_input.events.push(egui::Event::Touch {
|
|
device_id: egui::TouchDeviceId(0),
|
|
id: egui::TouchId(0),
|
|
phase: egui::TouchPhase::Start,
|
|
pos,
|
|
force: None,
|
|
});
|
|
} else {
|
|
self.any_pointer_button_down = false;
|
|
|
|
self.egui_input.events.push(egui::Event::PointerGone);
|
|
|
|
self.egui_input.events.push(egui::Event::Touch {
|
|
device_id: egui::TouchDeviceId(0),
|
|
id: egui::TouchId(0),
|
|
phase: egui::TouchPhase::End,
|
|
pos,
|
|
force: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_cursor_moved(
|
|
&mut self,
|
|
window: &Window,
|
|
pos_in_pixels: winit::dpi::PhysicalPosition<f64>,
|
|
) {
|
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
|
|
|
let pos_in_points = egui::pos2(
|
|
pos_in_pixels.x as f32 / pixels_per_point,
|
|
pos_in_pixels.y as f32 / pixels_per_point,
|
|
);
|
|
self.pointer_pos_in_points = Some(pos_in_points);
|
|
|
|
if self.simulate_touch_screen {
|
|
if self.any_pointer_button_down {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::PointerMoved(pos_in_points));
|
|
|
|
self.egui_input.events.push(egui::Event::Touch {
|
|
device_id: egui::TouchDeviceId(0),
|
|
id: egui::TouchId(0),
|
|
phase: egui::TouchPhase::Move,
|
|
pos: pos_in_points,
|
|
force: None,
|
|
});
|
|
}
|
|
} else {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::PointerMoved(pos_in_points));
|
|
}
|
|
}
|
|
|
|
fn on_touch(&mut self, window: &Window, touch: &winit::event::Touch) {
|
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
|
|
|
// Emit touch event
|
|
self.egui_input.events.push(egui::Event::Touch {
|
|
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
|
id: egui::TouchId::from(touch.id),
|
|
phase: to_egui_touch_phase(touch.phase),
|
|
pos: egui::pos2(
|
|
touch.location.x as f32 / pixels_per_point,
|
|
touch.location.y as f32 / pixels_per_point,
|
|
),
|
|
force: match touch.force {
|
|
Some(winit::event::Force::Normalized(force)) => Some(force as f32),
|
|
Some(winit::event::Force::Calibrated {
|
|
force,
|
|
max_possible_force,
|
|
..
|
|
}) => Some((force / max_possible_force) as f32),
|
|
None => None,
|
|
},
|
|
});
|
|
// If we're not yet translating a touch or we're translating this very
|
|
// touch …
|
|
if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap_or_default() == touch.id
|
|
{
|
|
// … emit PointerButton resp. PointerMoved events to emulate mouse
|
|
match touch.phase {
|
|
winit::event::TouchPhase::Started => {
|
|
self.pointer_touch_id = Some(touch.id);
|
|
// First move the pointer to the right location
|
|
self.on_cursor_moved(window, touch.location);
|
|
self.on_mouse_button_input(
|
|
winit::event::ElementState::Pressed,
|
|
winit::event::MouseButton::Left,
|
|
);
|
|
}
|
|
winit::event::TouchPhase::Moved => {
|
|
self.on_cursor_moved(window, touch.location);
|
|
}
|
|
winit::event::TouchPhase::Ended => {
|
|
self.pointer_touch_id = None;
|
|
self.on_mouse_button_input(
|
|
winit::event::ElementState::Released,
|
|
winit::event::MouseButton::Left,
|
|
);
|
|
// The pointer should vanish completely to not get any
|
|
// hover effects
|
|
self.pointer_pos_in_points = None;
|
|
self.egui_input.events.push(egui::Event::PointerGone);
|
|
}
|
|
winit::event::TouchPhase::Cancelled => {
|
|
self.pointer_touch_id = None;
|
|
self.pointer_pos_in_points = None;
|
|
self.egui_input.events.push(egui::Event::PointerGone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_mouse_wheel(
|
|
&mut self,
|
|
window: &Window,
|
|
delta: winit::event::MouseScrollDelta,
|
|
phase: winit::event::TouchPhase,
|
|
) {
|
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
|
|
|
{
|
|
let (unit, delta) = match delta {
|
|
winit::event::MouseScrollDelta::LineDelta(x, y) => {
|
|
(egui::MouseWheelUnit::Line, egui::vec2(x, y))
|
|
}
|
|
winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition {
|
|
x,
|
|
y,
|
|
}) => (
|
|
egui::MouseWheelUnit::Point,
|
|
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
|
),
|
|
};
|
|
let phase = to_egui_touch_phase(phase);
|
|
let modifiers = self.egui_input.modifiers;
|
|
self.egui_input.events.push(egui::Event::MouseWheel {
|
|
unit,
|
|
delta,
|
|
phase,
|
|
modifiers,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn on_keyboard_input(&mut self, event: &winit::event::KeyEvent) {
|
|
let winit::event::KeyEvent {
|
|
// Represents the position of a key independent of the currently active layout.
|
|
//
|
|
// It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode).
|
|
// The most prevalent use case for this is games. For example the default keys for the player
|
|
// to move around might be the W, A, S, and D keys on a US layout. The position of these keys
|
|
// is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY"
|
|
// layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.)
|
|
physical_key,
|
|
|
|
// Represents the results of a keymap, i.e. what character a certain key press represents.
|
|
// When telling users "Press Ctrl-F to find", this is where we should
|
|
// look for the "F" key, because they may have a dvorak layout on
|
|
// a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position.
|
|
logical_key: winit_logical_key,
|
|
|
|
text,
|
|
|
|
state,
|
|
|
|
location: _, // e.g. is it on the numpad?
|
|
repeat: _, // egui will figure this out for us
|
|
..
|
|
} = event;
|
|
|
|
let pressed = *state == winit::event::ElementState::Pressed;
|
|
|
|
let physical_key = if let winit::keyboard::PhysicalKey::Code(keycode) = *physical_key {
|
|
key_from_key_code(keycode)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let logical_key = key_from_winit_key(winit_logical_key);
|
|
|
|
// Helpful logging to enable when adding new key support
|
|
log::trace!(
|
|
"logical {:?} -> {:?}, physical {:?} -> {:?}",
|
|
event.logical_key,
|
|
logical_key,
|
|
event.physical_key,
|
|
physical_key
|
|
);
|
|
|
|
// "Logical OR physical key" is a fallback mechanism for keyboard layouts without Latin characters: it lets them
|
|
// emit events as if the corresponding keys from the Latin layout were pressed. In this case, clipboard shortcuts
|
|
// are mapped to the physical keys that normally contain C, X, V, etc.
|
|
// See also: https://github.com/emilk/egui/issues/3653
|
|
if let Some(active_key) = logical_key.or(physical_key) {
|
|
if pressed {
|
|
if is_cut_command(self.egui_input.modifiers, active_key) {
|
|
self.egui_input.events.push(egui::Event::Cut);
|
|
return;
|
|
} else if is_copy_command(self.egui_input.modifiers, active_key) {
|
|
self.egui_input.events.push(egui::Event::Copy);
|
|
return;
|
|
} else if is_paste_command(self.egui_input.modifiers, active_key) {
|
|
if let Some(contents) = self.clipboard.get() {
|
|
let contents = contents.replace("\r\n", "\n");
|
|
if !contents.is_empty() {
|
|
self.egui_input.events.push(egui::Event::Paste(contents));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.egui_input.events.push(egui::Event::Key {
|
|
key: active_key,
|
|
physical_key,
|
|
pressed,
|
|
repeat: false, // egui will fill this in for us!
|
|
modifiers: self.egui_input.modifiers,
|
|
});
|
|
}
|
|
|
|
if let Some(text) = text
|
|
.as_ref()
|
|
.map(|t| t.as_str())
|
|
.or_else(|| winit_logical_key.to_text())
|
|
{
|
|
// Make sure there is text, and that it is not control characters
|
|
// (e.g. delete is sent as "\u{f728}" on macOS).
|
|
if !text.is_empty() && text.chars().all(is_printable_char) {
|
|
// On some platforms we get here when the user presses Cmd-C (copy), ctrl-W, etc.
|
|
// We need to ignore these characters that are side-effects of commands.
|
|
// Also make sure the key is pressed (not released). On Linux, text might
|
|
// contain some data even when the key is released.
|
|
let is_cmd = self.egui_input.modifiers.ctrl
|
|
|| self.egui_input.modifiers.command
|
|
|| self.egui_input.modifiers.mac_cmd;
|
|
if pressed && !is_cmd {
|
|
self.egui_input
|
|
.events
|
|
.push(egui::Event::Text(text.to_owned()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Call with the output given by `egui`.
|
|
///
|
|
/// This will, if needed:
|
|
/// * update the cursor
|
|
/// * copy text to the clipboard
|
|
/// * open any clicked urls
|
|
/// * update the IME
|
|
/// *
|
|
pub fn handle_platform_output(
|
|
&mut self,
|
|
window: &Window,
|
|
platform_output: egui::PlatformOutput,
|
|
) {
|
|
profiling::function_scope!();
|
|
|
|
let egui::PlatformOutput {
|
|
commands,
|
|
cursor_icon,
|
|
events: _, // handled elsewhere
|
|
mutable_text_under_cursor: _, // only used in eframe web
|
|
ime,
|
|
accesskit_update,
|
|
num_completed_passes: _, // `egui::Context::run` handles this
|
|
request_discard_reasons: _, // `egui::Context::run` handles this
|
|
} = platform_output;
|
|
|
|
for command in commands {
|
|
match command {
|
|
egui::OutputCommand::CopyText(text) => {
|
|
self.clipboard.set_text(text);
|
|
}
|
|
egui::OutputCommand::CopyImage(image) => {
|
|
self.clipboard.set_image(&image);
|
|
}
|
|
egui::OutputCommand::OpenUrl(open_url) => {
|
|
open_url_in_browser(&open_url.url);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.set_cursor_icon(window, cursor_icon);
|
|
|
|
let allow_ime = ime.is_some();
|
|
if self.allow_ime != allow_ime {
|
|
self.allow_ime = allow_ime;
|
|
#[cfg(target_os = "windows")]
|
|
if !self.allow_ime {
|
|
// Defensively clear the set to avoid unexpected behavior.
|
|
//
|
|
// We don't do the same in `ime_event_disable` because the key
|
|
// release events for IME confirmation keys arrive after
|
|
// `winit::event::Ime::Disabled`.
|
|
self.pressed_processed_physical_keys.clear();
|
|
}
|
|
|
|
profiling::scope!("set_ime_allowed");
|
|
window.set_ime_allowed(allow_ime);
|
|
}
|
|
|
|
if let Some(ime) = ime {
|
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
|
let ime_rect_px = pixels_per_point * ime.rect;
|
|
if self.ime_rect_px != Some(ime_rect_px)
|
|
|| self.egui_ctx.input(|i| !i.events.is_empty())
|
|
{
|
|
self.ime_rect_px = Some(ime_rect_px);
|
|
profiling::scope!("set_ime_cursor_area");
|
|
window.set_ime_cursor_area(
|
|
winit::dpi::PhysicalPosition {
|
|
x: ime_rect_px.min.x,
|
|
y: ime_rect_px.min.y,
|
|
},
|
|
winit::dpi::PhysicalSize {
|
|
width: ime_rect_px.width(),
|
|
height: ime_rect_px.height(),
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
self.ime_rect_px = None;
|
|
}
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
if let Some(accesskit) = self.accesskit.as_mut()
|
|
&& let Some(update) = accesskit_update
|
|
{
|
|
profiling::scope!("accesskit");
|
|
accesskit.update_if_active(|| update);
|
|
}
|
|
|
|
#[cfg(not(feature = "accesskit"))]
|
|
let _ = accesskit_update;
|
|
}
|
|
|
|
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
|
|
if self.current_cursor_icon == Some(cursor_icon) {
|
|
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
|
// On other platforms: just early-out to save CPU.
|
|
return;
|
|
}
|
|
|
|
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
|
if is_pointer_in_window {
|
|
self.current_cursor_icon = Some(cursor_icon);
|
|
|
|
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
|
window.set_cursor_visible(true);
|
|
window.set_cursor(winit_cursor_icon);
|
|
} else {
|
|
window.set_cursor_visible(false);
|
|
}
|
|
} else {
|
|
// Remember to set the cursor again once the cursor returns to the screen:
|
|
self.current_cursor_icon = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
|
|
match phase {
|
|
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
|
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
|
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
|
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
|
}
|
|
}
|
|
|
|
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
|
match theme {
|
|
winit::window::Theme::Dark => Theme::Dark,
|
|
winit::window::Theme::Light => Theme::Light,
|
|
}
|
|
}
|
|
|
|
pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> {
|
|
let inner_pos_px = window.inner_position().ok()?;
|
|
let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32);
|
|
|
|
let inner_size_px = window.inner_size();
|
|
let inner_size_px = egui::vec2(inner_size_px.width as f32, inner_size_px.height as f32);
|
|
|
|
let inner_rect_px = egui::Rect::from_min_size(inner_pos_px, inner_size_px);
|
|
|
|
Some(inner_rect_px / pixels_per_point)
|
|
}
|
|
|
|
pub fn outer_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> {
|
|
let outer_pos_px = window.outer_position().ok()?;
|
|
let outer_pos_px = egui::pos2(outer_pos_px.x as f32, outer_pos_px.y as f32);
|
|
|
|
let outer_size_px = window.outer_size();
|
|
let outer_size_px = egui::vec2(outer_size_px.width as f32, outer_size_px.height as f32);
|
|
|
|
let outer_rect_px = egui::Rect::from_min_size(outer_pos_px, outer_size_px);
|
|
|
|
Some(outer_rect_px / pixels_per_point)
|
|
}
|
|
|
|
/// Update the given viewport info with the current state of the window.
|
|
///
|
|
/// Call before [`State::take_egui_input`].
|
|
///
|
|
/// If this is called right after window creation, `is_init` should be `true`, otherwise `false`.
|
|
pub fn update_viewport_info(
|
|
viewport_info: &mut ViewportInfo,
|
|
egui_ctx: &egui::Context,
|
|
window: &Window,
|
|
is_init: bool,
|
|
) {
|
|
profiling::function_scope!();
|
|
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
|
|
|
let has_a_position = match window.is_minimized() {
|
|
Some(true) => false,
|
|
Some(false) | None => true,
|
|
};
|
|
|
|
let inner_rect = if has_a_position {
|
|
inner_rect_in_points(window, pixels_per_point)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let outer_rect = if has_a_position {
|
|
outer_rect_in_points(window, pixels_per_point)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let monitor_size = {
|
|
profiling::scope!("monitor_size");
|
|
if let Some(monitor) = window.current_monitor() {
|
|
let size = monitor.size().to_logical::<f32>(pixels_per_point.into());
|
|
Some(egui::vec2(size.width, size.height))
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
viewport_info.title = Some(window.title());
|
|
viewport_info.native_pixels_per_point = Some(window.scale_factor() as f32);
|
|
|
|
viewport_info.monitor_size = monitor_size;
|
|
viewport_info.inner_rect = inner_rect;
|
|
viewport_info.outer_rect = outer_rect;
|
|
|
|
if is_init || !cfg!(target_os = "macos") {
|
|
// Asking for minimized/maximized state at runtime leads to a deadlock on Mac when running
|
|
// `cargo run -p custom_window_frame`.
|
|
// See https://github.com/emilk/egui/issues/3494
|
|
viewport_info.maximized = Some(window.is_maximized());
|
|
viewport_info.minimized = Some(window.is_minimized().unwrap_or(false));
|
|
}
|
|
|
|
viewport_info.fullscreen = Some(window.fullscreen().is_some());
|
|
viewport_info.focused = Some(window.has_focus());
|
|
}
|
|
|
|
fn open_url_in_browser(_url: &str) {
|
|
#[cfg(feature = "webbrowser")]
|
|
if let Err(err) = webbrowser::open(_url) {
|
|
log::warn!("Failed to open url: {err}");
|
|
}
|
|
|
|
#[cfg(not(feature = "webbrowser"))]
|
|
{
|
|
log::warn!("Cannot open url - feature \"links\" not enabled.");
|
|
}
|
|
}
|
|
|
|
/// Winit sends special keys (backspace, delete, F1, …) as characters.
|
|
/// Ignore those.
|
|
/// We also ignore '\r', '\n', '\t'.
|
|
/// Newlines are handled by the `Key::Enter` event.
|
|
fn is_printable_char(chr: char) -> bool {
|
|
let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}'
|
|
|| '\u{f0000}' <= chr && chr <= '\u{ffffd}'
|
|
|| '\u{100000}' <= chr && chr <= '\u{10fffd}';
|
|
|
|
!is_in_private_use_area && !chr.is_ascii_control()
|
|
}
|
|
|
|
fn is_cut_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
|
|
keycode == egui::Key::Cut
|
|
|| (modifiers.command && keycode == egui::Key::X)
|
|
|| (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Delete)
|
|
}
|
|
|
|
fn is_copy_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
|
|
keycode == egui::Key::Copy
|
|
|| (modifiers.command && keycode == egui::Key::C)
|
|
|| (cfg!(target_os = "windows") && modifiers.ctrl && keycode == egui::Key::Insert)
|
|
}
|
|
|
|
fn is_paste_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
|
|
keycode == egui::Key::Paste
|
|
|| (modifiers.command && keycode == egui::Key::V)
|
|
|| (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Insert)
|
|
}
|
|
|
|
fn translate_mouse_button(button: winit::event::MouseButton) -> Option<egui::PointerButton> {
|
|
match button {
|
|
winit::event::MouseButton::Left => Some(egui::PointerButton::Primary),
|
|
winit::event::MouseButton::Right => Some(egui::PointerButton::Secondary),
|
|
winit::event::MouseButton::Middle => Some(egui::PointerButton::Middle),
|
|
winit::event::MouseButton::Back => Some(egui::PointerButton::Extra1),
|
|
winit::event::MouseButton::Forward => Some(egui::PointerButton::Extra2),
|
|
winit::event::MouseButton::Other(_) => None,
|
|
}
|
|
}
|
|
|
|
fn key_from_winit_key(key: &winit::keyboard::Key) -> Option<egui::Key> {
|
|
match key {
|
|
winit::keyboard::Key::Named(named_key) => key_from_named_key(*named_key),
|
|
winit::keyboard::Key::Character(str) => egui::Key::from_name(str.as_str()),
|
|
winit::keyboard::Key::Unidentified(_) | winit::keyboard::Key::Dead(_) => None,
|
|
}
|
|
}
|
|
|
|
fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key> {
|
|
use egui::Key;
|
|
use winit::keyboard::NamedKey;
|
|
|
|
Some(match named_key {
|
|
NamedKey::Enter => Key::Enter,
|
|
NamedKey::Tab => Key::Tab,
|
|
NamedKey::ArrowDown => Key::ArrowDown,
|
|
NamedKey::ArrowLeft => Key::ArrowLeft,
|
|
NamedKey::ArrowRight => Key::ArrowRight,
|
|
NamedKey::ArrowUp => Key::ArrowUp,
|
|
NamedKey::End => Key::End,
|
|
NamedKey::Home => Key::Home,
|
|
NamedKey::PageDown => Key::PageDown,
|
|
NamedKey::PageUp => Key::PageUp,
|
|
NamedKey::Backspace => Key::Backspace,
|
|
NamedKey::Delete => Key::Delete,
|
|
NamedKey::Insert => Key::Insert,
|
|
NamedKey::Escape => Key::Escape,
|
|
NamedKey::Cut => Key::Cut,
|
|
NamedKey::Copy => Key::Copy,
|
|
NamedKey::Paste => Key::Paste,
|
|
|
|
NamedKey::Space => Key::Space,
|
|
|
|
NamedKey::F1 => Key::F1,
|
|
NamedKey::F2 => Key::F2,
|
|
NamedKey::F3 => Key::F3,
|
|
NamedKey::F4 => Key::F4,
|
|
NamedKey::F5 => Key::F5,
|
|
NamedKey::F6 => Key::F6,
|
|
NamedKey::F7 => Key::F7,
|
|
NamedKey::F8 => Key::F8,
|
|
NamedKey::F9 => Key::F9,
|
|
NamedKey::F10 => Key::F10,
|
|
NamedKey::F11 => Key::F11,
|
|
NamedKey::F12 => Key::F12,
|
|
NamedKey::F13 => Key::F13,
|
|
NamedKey::F14 => Key::F14,
|
|
NamedKey::F15 => Key::F15,
|
|
NamedKey::F16 => Key::F16,
|
|
NamedKey::F17 => Key::F17,
|
|
NamedKey::F18 => Key::F18,
|
|
NamedKey::F19 => Key::F19,
|
|
NamedKey::F20 => Key::F20,
|
|
NamedKey::F21 => Key::F21,
|
|
NamedKey::F22 => Key::F22,
|
|
NamedKey::F23 => Key::F23,
|
|
NamedKey::F24 => Key::F24,
|
|
NamedKey::F25 => Key::F25,
|
|
NamedKey::F26 => Key::F26,
|
|
NamedKey::F27 => Key::F27,
|
|
NamedKey::F28 => Key::F28,
|
|
NamedKey::F29 => Key::F29,
|
|
NamedKey::F30 => Key::F30,
|
|
NamedKey::F31 => Key::F31,
|
|
NamedKey::F32 => Key::F32,
|
|
NamedKey::F33 => Key::F33,
|
|
NamedKey::F34 => Key::F34,
|
|
NamedKey::F35 => Key::F35,
|
|
|
|
NamedKey::BrowserBack => Key::BrowserBack,
|
|
_ => {
|
|
log::trace!("Unknown key: {named_key:?}");
|
|
return None;
|
|
}
|
|
})
|
|
}
|
|
|
|
fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option<egui::Key> {
|
|
use egui::Key;
|
|
use winit::keyboard::KeyCode;
|
|
|
|
Some(match key {
|
|
KeyCode::ArrowDown => Key::ArrowDown,
|
|
KeyCode::ArrowLeft => Key::ArrowLeft,
|
|
KeyCode::ArrowRight => Key::ArrowRight,
|
|
KeyCode::ArrowUp => Key::ArrowUp,
|
|
|
|
KeyCode::Escape => Key::Escape,
|
|
KeyCode::Tab => Key::Tab,
|
|
KeyCode::Backspace => Key::Backspace,
|
|
KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter,
|
|
|
|
KeyCode::Insert => Key::Insert,
|
|
KeyCode::Delete => Key::Delete,
|
|
KeyCode::Home => Key::Home,
|
|
KeyCode::End => Key::End,
|
|
KeyCode::PageUp => Key::PageUp,
|
|
KeyCode::PageDown => Key::PageDown,
|
|
|
|
// Punctuation
|
|
KeyCode::Space => Key::Space,
|
|
KeyCode::Comma => Key::Comma,
|
|
KeyCode::Period => Key::Period,
|
|
// KeyCode::Colon => Key::Colon, // NOTE: there is no physical colon key on an american keyboard
|
|
KeyCode::Semicolon => Key::Semicolon,
|
|
KeyCode::Backslash => Key::Backslash,
|
|
KeyCode::Slash | KeyCode::NumpadDivide => Key::Slash,
|
|
KeyCode::BracketLeft => Key::OpenBracket,
|
|
KeyCode::BracketRight => Key::CloseBracket,
|
|
KeyCode::Backquote => Key::Backtick,
|
|
KeyCode::Quote => Key::Quote,
|
|
|
|
KeyCode::Cut => Key::Cut,
|
|
KeyCode::Copy => Key::Copy,
|
|
KeyCode::Paste => Key::Paste,
|
|
KeyCode::Minus | KeyCode::NumpadSubtract => Key::Minus,
|
|
KeyCode::NumpadAdd => Key::Plus,
|
|
KeyCode::Equal => Key::Equals,
|
|
|
|
KeyCode::Digit0 | KeyCode::Numpad0 => Key::Num0,
|
|
KeyCode::Digit1 | KeyCode::Numpad1 => Key::Num1,
|
|
KeyCode::Digit2 | KeyCode::Numpad2 => Key::Num2,
|
|
KeyCode::Digit3 | KeyCode::Numpad3 => Key::Num3,
|
|
KeyCode::Digit4 | KeyCode::Numpad4 => Key::Num4,
|
|
KeyCode::Digit5 | KeyCode::Numpad5 => Key::Num5,
|
|
KeyCode::Digit6 | KeyCode::Numpad6 => Key::Num6,
|
|
KeyCode::Digit7 | KeyCode::Numpad7 => Key::Num7,
|
|
KeyCode::Digit8 | KeyCode::Numpad8 => Key::Num8,
|
|
KeyCode::Digit9 | KeyCode::Numpad9 => Key::Num9,
|
|
|
|
KeyCode::KeyA => Key::A,
|
|
KeyCode::KeyB => Key::B,
|
|
KeyCode::KeyC => Key::C,
|
|
KeyCode::KeyD => Key::D,
|
|
KeyCode::KeyE => Key::E,
|
|
KeyCode::KeyF => Key::F,
|
|
KeyCode::KeyG => Key::G,
|
|
KeyCode::KeyH => Key::H,
|
|
KeyCode::KeyI => Key::I,
|
|
KeyCode::KeyJ => Key::J,
|
|
KeyCode::KeyK => Key::K,
|
|
KeyCode::KeyL => Key::L,
|
|
KeyCode::KeyM => Key::M,
|
|
KeyCode::KeyN => Key::N,
|
|
KeyCode::KeyO => Key::O,
|
|
KeyCode::KeyP => Key::P,
|
|
KeyCode::KeyQ => Key::Q,
|
|
KeyCode::KeyR => Key::R,
|
|
KeyCode::KeyS => Key::S,
|
|
KeyCode::KeyT => Key::T,
|
|
KeyCode::KeyU => Key::U,
|
|
KeyCode::KeyV => Key::V,
|
|
KeyCode::KeyW => Key::W,
|
|
KeyCode::KeyX => Key::X,
|
|
KeyCode::KeyY => Key::Y,
|
|
KeyCode::KeyZ => Key::Z,
|
|
|
|
KeyCode::F1 => Key::F1,
|
|
KeyCode::F2 => Key::F2,
|
|
KeyCode::F3 => Key::F3,
|
|
KeyCode::F4 => Key::F4,
|
|
KeyCode::F5 => Key::F5,
|
|
KeyCode::F6 => Key::F6,
|
|
KeyCode::F7 => Key::F7,
|
|
KeyCode::F8 => Key::F8,
|
|
KeyCode::F9 => Key::F9,
|
|
KeyCode::F10 => Key::F10,
|
|
KeyCode::F11 => Key::F11,
|
|
KeyCode::F12 => Key::F12,
|
|
KeyCode::F13 => Key::F13,
|
|
KeyCode::F14 => Key::F14,
|
|
KeyCode::F15 => Key::F15,
|
|
KeyCode::F16 => Key::F16,
|
|
KeyCode::F17 => Key::F17,
|
|
KeyCode::F18 => Key::F18,
|
|
KeyCode::F19 => Key::F19,
|
|
KeyCode::F20 => Key::F20,
|
|
KeyCode::F21 => Key::F21,
|
|
KeyCode::F22 => Key::F22,
|
|
KeyCode::F23 => Key::F23,
|
|
KeyCode::F24 => Key::F24,
|
|
KeyCode::F25 => Key::F25,
|
|
KeyCode::F26 => Key::F26,
|
|
KeyCode::F27 => Key::F27,
|
|
KeyCode::F28 => Key::F28,
|
|
KeyCode::F29 => Key::F29,
|
|
KeyCode::F30 => Key::F30,
|
|
KeyCode::F31 => Key::F31,
|
|
KeyCode::F32 => Key::F32,
|
|
KeyCode::F33 => Key::F33,
|
|
KeyCode::F34 => Key::F34,
|
|
KeyCode::F35 => Key::F35,
|
|
|
|
_ => {
|
|
return None;
|
|
}
|
|
})
|
|
}
|
|
|
|
fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::CursorIcon> {
|
|
match cursor_icon {
|
|
egui::CursorIcon::None => None,
|
|
|
|
egui::CursorIcon::Alias => Some(winit::window::CursorIcon::Alias),
|
|
egui::CursorIcon::AllScroll => Some(winit::window::CursorIcon::AllScroll),
|
|
egui::CursorIcon::Cell => Some(winit::window::CursorIcon::Cell),
|
|
egui::CursorIcon::ContextMenu => Some(winit::window::CursorIcon::ContextMenu),
|
|
egui::CursorIcon::Copy => Some(winit::window::CursorIcon::Copy),
|
|
egui::CursorIcon::Crosshair => Some(winit::window::CursorIcon::Crosshair),
|
|
egui::CursorIcon::Default => Some(winit::window::CursorIcon::Default),
|
|
egui::CursorIcon::Grab => Some(winit::window::CursorIcon::Grab),
|
|
egui::CursorIcon::Grabbing => Some(winit::window::CursorIcon::Grabbing),
|
|
egui::CursorIcon::Help => Some(winit::window::CursorIcon::Help),
|
|
egui::CursorIcon::Move => Some(winit::window::CursorIcon::Move),
|
|
egui::CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop),
|
|
egui::CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed),
|
|
egui::CursorIcon::PointingHand => Some(winit::window::CursorIcon::Pointer),
|
|
egui::CursorIcon::Progress => Some(winit::window::CursorIcon::Progress),
|
|
|
|
egui::CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize),
|
|
egui::CursorIcon::ResizeNeSw => Some(winit::window::CursorIcon::NeswResize),
|
|
egui::CursorIcon::ResizeNwSe => Some(winit::window::CursorIcon::NwseResize),
|
|
egui::CursorIcon::ResizeVertical => Some(winit::window::CursorIcon::NsResize),
|
|
|
|
egui::CursorIcon::ResizeEast => Some(winit::window::CursorIcon::EResize),
|
|
egui::CursorIcon::ResizeSouthEast => Some(winit::window::CursorIcon::SeResize),
|
|
egui::CursorIcon::ResizeSouth => Some(winit::window::CursorIcon::SResize),
|
|
egui::CursorIcon::ResizeSouthWest => Some(winit::window::CursorIcon::SwResize),
|
|
egui::CursorIcon::ResizeWest => Some(winit::window::CursorIcon::WResize),
|
|
egui::CursorIcon::ResizeNorthWest => Some(winit::window::CursorIcon::NwResize),
|
|
egui::CursorIcon::ResizeNorth => Some(winit::window::CursorIcon::NResize),
|
|
egui::CursorIcon::ResizeNorthEast => Some(winit::window::CursorIcon::NeResize),
|
|
egui::CursorIcon::ResizeColumn => Some(winit::window::CursorIcon::ColResize),
|
|
egui::CursorIcon::ResizeRow => Some(winit::window::CursorIcon::RowResize),
|
|
|
|
egui::CursorIcon::Text => Some(winit::window::CursorIcon::Text),
|
|
egui::CursorIcon::VerticalText => Some(winit::window::CursorIcon::VerticalText),
|
|
egui::CursorIcon::Wait => Some(winit::window::CursorIcon::Wait),
|
|
egui::CursorIcon::ZoomIn => Some(winit::window::CursorIcon::ZoomIn),
|
|
egui::CursorIcon::ZoomOut => Some(winit::window::CursorIcon::ZoomOut),
|
|
}
|
|
}
|
|
|
|
// Helpers for egui Viewports
|
|
// ---------------------------------------------------------------------------
|
|
#[derive(PartialEq, Eq, Hash, Debug)]
|
|
pub enum ActionRequested {
|
|
Screenshot(egui::UserData),
|
|
Cut,
|
|
Copy,
|
|
Paste,
|
|
}
|
|
|
|
pub fn process_viewport_commands(
|
|
egui_ctx: &egui::Context,
|
|
info: &mut ViewportInfo,
|
|
commands: impl IntoIterator<Item = ViewportCommand>,
|
|
window: &Window,
|
|
actions_requested: &mut Vec<ActionRequested>,
|
|
) {
|
|
for command in commands {
|
|
process_viewport_command(egui_ctx, window, command, info, actions_requested);
|
|
}
|
|
}
|
|
|
|
fn process_viewport_command(
|
|
egui_ctx: &egui::Context,
|
|
window: &Window,
|
|
command: ViewportCommand,
|
|
info: &mut ViewportInfo,
|
|
actions_requested: &mut Vec<ActionRequested>,
|
|
) {
|
|
profiling::function_scope!(&format!("{command:?}"));
|
|
|
|
use winit::window::ResizeDirection;
|
|
|
|
log::trace!("Processing ViewportCommand::{command:?}");
|
|
|
|
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
|
|
|
match command {
|
|
ViewportCommand::Close => {
|
|
info.events.push(egui::ViewportEvent::Close);
|
|
}
|
|
ViewportCommand::CancelClose => {
|
|
// Need to be handled elsewhere
|
|
}
|
|
ViewportCommand::StartDrag => {
|
|
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
|
|
if window.has_focus()
|
|
&& let Err(err) = window.drag_window()
|
|
{
|
|
log::warn!("{command:?}: {err}");
|
|
}
|
|
}
|
|
ViewportCommand::InnerSize(size) => {
|
|
let width_px = pixels_per_point * size.x.max(1.0);
|
|
let height_px = pixels_per_point * size.y.max(1.0);
|
|
let requested_size = PhysicalSize::new(width_px, height_px);
|
|
if let Some(_returned_inner_size) = window.request_inner_size(requested_size) {
|
|
// On platforms where the size is entirely controlled by the user the
|
|
// applied size will be returned immediately, resize event in such case
|
|
// may not be generated.
|
|
// e.g. Linux
|
|
|
|
// On platforms where resizing is disallowed by the windowing system, the current
|
|
// inner size is returned immediately, and the user one is ignored.
|
|
// e.g. Android, iOS, …
|
|
|
|
// However, comparing the results is prone to numerical errors
|
|
// because the linux backend converts physical to logical and back again.
|
|
// So let's just assume it worked:
|
|
|
|
info.inner_rect = inner_rect_in_points(window, pixels_per_point);
|
|
info.outer_rect = outer_rect_in_points(window, pixels_per_point);
|
|
} else {
|
|
// e.g. macOS, Windows
|
|
// The request went to the display system,
|
|
// and the actual size will be delivered later with the [`WindowEvent::Resized`].
|
|
}
|
|
}
|
|
ViewportCommand::BeginResize(direction) => {
|
|
if let Err(err) = window.drag_resize_window(match direction {
|
|
egui::viewport::ResizeDirection::North => ResizeDirection::North,
|
|
egui::viewport::ResizeDirection::South => ResizeDirection::South,
|
|
egui::viewport::ResizeDirection::East => ResizeDirection::East,
|
|
egui::viewport::ResizeDirection::West => ResizeDirection::West,
|
|
egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast,
|
|
egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast,
|
|
egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest,
|
|
egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest,
|
|
}) {
|
|
log::warn!("{command:?}: {err}");
|
|
}
|
|
}
|
|
ViewportCommand::Title(title) => {
|
|
window.set_title(&title);
|
|
}
|
|
ViewportCommand::Transparent(v) => window.set_transparent(v),
|
|
ViewportCommand::Visible(v) => window.set_visible(v),
|
|
ViewportCommand::OuterPosition(pos) => {
|
|
window.set_outer_position(PhysicalPosition::new(
|
|
pixels_per_point * pos.x,
|
|
pixels_per_point * pos.y,
|
|
));
|
|
}
|
|
ViewportCommand::MinInnerSize(s) => {
|
|
window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some(
|
|
PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y),
|
|
));
|
|
}
|
|
ViewportCommand::MaxInnerSize(s) => {
|
|
window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some(
|
|
PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y),
|
|
));
|
|
}
|
|
ViewportCommand::ResizeIncrements(s) => {
|
|
window.set_resize_increments(
|
|
s.map(|s| PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y)),
|
|
);
|
|
}
|
|
ViewportCommand::Resizable(v) => window.set_resizable(v),
|
|
ViewportCommand::EnableButtons {
|
|
close,
|
|
minimized,
|
|
maximize,
|
|
} => window.set_enabled_buttons(
|
|
if close {
|
|
WindowButtons::CLOSE
|
|
} else {
|
|
WindowButtons::empty()
|
|
} | if minimized {
|
|
WindowButtons::MINIMIZE
|
|
} else {
|
|
WindowButtons::empty()
|
|
} | if maximize {
|
|
WindowButtons::MAXIMIZE
|
|
} else {
|
|
WindowButtons::empty()
|
|
},
|
|
),
|
|
ViewportCommand::Minimized(v) => {
|
|
window.set_minimized(v);
|
|
info.minimized = Some(v);
|
|
}
|
|
ViewportCommand::Maximized(v) => {
|
|
window.set_maximized(v);
|
|
info.maximized = Some(v);
|
|
}
|
|
ViewportCommand::Fullscreen(v) => {
|
|
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
|
|
}
|
|
ViewportCommand::Decorations(v) => window.set_decorations(v),
|
|
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
|
|
egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
|
|
egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
|
|
egui::viewport::WindowLevel::Normal => WindowLevel::Normal,
|
|
}),
|
|
ViewportCommand::Icon(icon) => {
|
|
let winit_icon = icon.and_then(|icon| to_winit_icon(&icon));
|
|
window.set_window_icon(winit_icon);
|
|
}
|
|
ViewportCommand::IMERect(rect) => {
|
|
window.set_ime_cursor_area(
|
|
PhysicalPosition::new(pixels_per_point * rect.min.x, pixels_per_point * rect.min.y),
|
|
PhysicalSize::new(
|
|
pixels_per_point * rect.size().x,
|
|
pixels_per_point * rect.size().y,
|
|
),
|
|
);
|
|
}
|
|
ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
|
|
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p {
|
|
egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password,
|
|
egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
|
|
egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
|
|
}),
|
|
ViewportCommand::Focus => {
|
|
if !window.has_focus() {
|
|
window.focus_window();
|
|
}
|
|
}
|
|
ViewportCommand::RequestUserAttention(a) => {
|
|
window.request_user_attention(match a {
|
|
egui::UserAttentionType::Reset => None,
|
|
egui::UserAttentionType::Critical => {
|
|
Some(winit::window::UserAttentionType::Critical)
|
|
}
|
|
egui::UserAttentionType::Informational => {
|
|
Some(winit::window::UserAttentionType::Informational)
|
|
}
|
|
});
|
|
}
|
|
ViewportCommand::SetTheme(t) => window.set_theme(match t {
|
|
egui::SystemTheme::Light => Some(winit::window::Theme::Light),
|
|
egui::SystemTheme::Dark => Some(winit::window::Theme::Dark),
|
|
egui::SystemTheme::SystemDefault => None,
|
|
}),
|
|
ViewportCommand::ContentProtected(v) => window.set_content_protected(v),
|
|
ViewportCommand::CursorPosition(pos) => {
|
|
if let Err(err) = window.set_cursor_position(PhysicalPosition::new(
|
|
pixels_per_point * pos.x,
|
|
pixels_per_point * pos.y,
|
|
)) {
|
|
log::warn!("{command:?}: {err}");
|
|
}
|
|
}
|
|
ViewportCommand::CursorGrab(o) => {
|
|
if let Err(err) = window.set_cursor_grab(match o {
|
|
egui::viewport::CursorGrab::None => CursorGrabMode::None,
|
|
egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined,
|
|
egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked,
|
|
}) {
|
|
log::warn!("{command:?}: {err}");
|
|
}
|
|
}
|
|
ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v),
|
|
ViewportCommand::MousePassthrough(passthrough) => {
|
|
if let Err(err) = window.set_cursor_hittest(!passthrough) {
|
|
log::warn!("{command:?}: {err}");
|
|
}
|
|
}
|
|
ViewportCommand::Screenshot(user_data) => {
|
|
actions_requested.push(ActionRequested::Screenshot(user_data));
|
|
}
|
|
ViewportCommand::RequestCut => {
|
|
actions_requested.push(ActionRequested::Cut);
|
|
}
|
|
ViewportCommand::RequestCopy => {
|
|
actions_requested.push(ActionRequested::Copy);
|
|
}
|
|
ViewportCommand::RequestPaste => {
|
|
actions_requested.push(ActionRequested::Paste);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build and intitlaize a window.
|
|
///
|
|
/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.
|
|
///
|
|
/// # Errors
|
|
/// Possible causes of error include denied permission, incompatible system, and lack of memory.
|
|
pub fn create_window(
|
|
egui_ctx: &egui::Context,
|
|
event_loop: &ActiveEventLoop,
|
|
viewport_builder: &ViewportBuilder,
|
|
) -> Result<Window, winit::error::OsError> {
|
|
profiling::function_scope!();
|
|
|
|
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
|
let window = event_loop.create_window(window_attributes)?;
|
|
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
|
Ok(window)
|
|
}
|
|
|
|
pub fn create_winit_window_attributes(
|
|
egui_ctx: &egui::Context,
|
|
viewport_builder: ViewportBuilder,
|
|
) -> winit::window::WindowAttributes {
|
|
profiling::function_scope!();
|
|
|
|
let ViewportBuilder {
|
|
title,
|
|
position,
|
|
inner_size,
|
|
min_inner_size,
|
|
max_inner_size,
|
|
fullscreen,
|
|
maximized,
|
|
resizable,
|
|
transparent,
|
|
decorations,
|
|
icon,
|
|
active,
|
|
visible,
|
|
close_button,
|
|
minimize_button,
|
|
maximize_button,
|
|
window_level,
|
|
|
|
// macOS:
|
|
fullsize_content_view: _fullsize_content_view,
|
|
movable_by_window_background: _movable_by_window_background,
|
|
title_shown: _title_shown,
|
|
titlebar_buttons_shown: _titlebar_buttons_shown,
|
|
titlebar_shown: _titlebar_shown,
|
|
has_shadow: _has_shadow,
|
|
|
|
// Windows:
|
|
drag_and_drop: _drag_and_drop,
|
|
taskbar: _taskbar,
|
|
|
|
// wayland:
|
|
app_id: _app_id,
|
|
|
|
// x11
|
|
window_type: _window_type,
|
|
override_redirect: _override_redirect,
|
|
|
|
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
|
|
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
|
|
} = viewport_builder;
|
|
|
|
let mut window_attributes = winit::window::WindowAttributes::default()
|
|
.with_title(title.unwrap_or_else(|| "egui window".to_owned()))
|
|
.with_transparent(transparent.unwrap_or(false))
|
|
.with_decorations(decorations.unwrap_or(true))
|
|
.with_resizable(resizable.unwrap_or(true))
|
|
.with_visible(visible.unwrap_or(true))
|
|
.with_maximized(if cfg!(target_os = "ios") {
|
|
true
|
|
} else {
|
|
maximized.unwrap_or(false)
|
|
})
|
|
.with_window_level(match window_level.unwrap_or_default() {
|
|
egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
|
|
egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
|
|
egui::viewport::WindowLevel::Normal => WindowLevel::Normal,
|
|
})
|
|
.with_fullscreen(
|
|
fullscreen.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))),
|
|
)
|
|
.with_enabled_buttons({
|
|
let mut buttons = WindowButtons::empty();
|
|
if minimize_button.unwrap_or(true) {
|
|
buttons |= WindowButtons::MINIMIZE;
|
|
}
|
|
if maximize_button.unwrap_or(true) {
|
|
buttons |= WindowButtons::MAXIMIZE;
|
|
}
|
|
if close_button.unwrap_or(true) {
|
|
buttons |= WindowButtons::CLOSE;
|
|
}
|
|
buttons
|
|
})
|
|
.with_active(active.unwrap_or(true));
|
|
|
|
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
|
|
// zoom factor into account. We don't have a good way to get physical size here,
|
|
// and trying to do it anyway leads to weird bugs on Wayland, see:
|
|
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
|
|
// https://github.com/rust-windowing/winit/issues/4266
|
|
#[expect(
|
|
clippy::disallowed_types,
|
|
reason = "zoom factor is manually accounted for"
|
|
)]
|
|
#[cfg(not(target_os = "ios"))]
|
|
{
|
|
use winit::dpi::{LogicalPosition, LogicalSize};
|
|
let zoom_factor = egui_ctx.zoom_factor();
|
|
|
|
if let Some(size) = inner_size {
|
|
window_attributes = window_attributes
|
|
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
|
}
|
|
|
|
if let Some(size) = min_inner_size {
|
|
window_attributes = window_attributes
|
|
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
|
}
|
|
|
|
if let Some(size) = max_inner_size {
|
|
window_attributes = window_attributes
|
|
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
|
}
|
|
|
|
if let Some(pos) = position {
|
|
window_attributes = window_attributes.with_position(LogicalPosition::new(
|
|
zoom_factor * pos.x,
|
|
zoom_factor * pos.y,
|
|
));
|
|
}
|
|
}
|
|
#[cfg(target_os = "ios")]
|
|
{
|
|
// Unused:
|
|
_ = egui_ctx;
|
|
_ = pixels_per_point;
|
|
_ = position;
|
|
_ = inner_size;
|
|
_ = min_inner_size;
|
|
_ = max_inner_size;
|
|
}
|
|
|
|
if let Some(icon) = icon {
|
|
let winit_icon = to_winit_icon(&icon);
|
|
window_attributes = window_attributes.with_window_icon(winit_icon);
|
|
}
|
|
|
|
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
|
if let Some(app_id) = _app_id {
|
|
use winit::platform::wayland::WindowAttributesExtWayland as _;
|
|
window_attributes = window_attributes.with_name(app_id, "");
|
|
}
|
|
|
|
#[cfg(all(feature = "x11", target_os = "linux"))]
|
|
{
|
|
use winit::platform::x11::WindowAttributesExtX11 as _;
|
|
if let Some(window_type) = _window_type {
|
|
use winit::platform::x11::WindowType;
|
|
window_attributes = window_attributes.with_x11_window_type(vec![match window_type {
|
|
egui::X11WindowType::Normal => WindowType::Normal,
|
|
egui::X11WindowType::Utility => WindowType::Utility,
|
|
egui::X11WindowType::Dock => WindowType::Dock,
|
|
egui::X11WindowType::Desktop => WindowType::Desktop,
|
|
egui::X11WindowType::Toolbar => WindowType::Toolbar,
|
|
egui::X11WindowType::Menu => WindowType::Menu,
|
|
egui::X11WindowType::Splash => WindowType::Splash,
|
|
egui::X11WindowType::Dialog => WindowType::Dialog,
|
|
egui::X11WindowType::DropdownMenu => WindowType::DropdownMenu,
|
|
egui::X11WindowType::PopupMenu => WindowType::PopupMenu,
|
|
egui::X11WindowType::Tooltip => WindowType::Tooltip,
|
|
egui::X11WindowType::Notification => WindowType::Notification,
|
|
egui::X11WindowType::Combo => WindowType::Combo,
|
|
egui::X11WindowType::Dnd => WindowType::Dnd,
|
|
}]);
|
|
}
|
|
if let Some(override_redirect) = _override_redirect {
|
|
window_attributes = window_attributes.with_override_redirect(override_redirect);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
use winit::platform::windows::WindowAttributesExtWindows as _;
|
|
if let Some(enable) = _drag_and_drop {
|
|
window_attributes = window_attributes.with_drag_and_drop(enable);
|
|
}
|
|
if let Some(show) = _taskbar {
|
|
window_attributes = window_attributes.with_skip_taskbar(!show);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use winit::platform::macos::WindowAttributesExtMacOS as _;
|
|
window_attributes = window_attributes
|
|
.with_title_hidden(!_title_shown.unwrap_or(true))
|
|
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
|
|
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
|
|
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
|
|
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
|
|
.with_has_shadow(_has_shadow.unwrap_or(true));
|
|
}
|
|
|
|
window_attributes
|
|
}
|
|
|
|
fn to_winit_icon(icon: &egui::IconData) -> Option<winit::window::Icon> {
|
|
if icon.is_empty() {
|
|
None
|
|
} else {
|
|
profiling::function_scope!();
|
|
match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
|
|
Ok(winit_icon) => Some(winit_icon),
|
|
Err(err) => {
|
|
log::warn!("Invalid IconData: {err}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Applies what `create_winit_window_builder` couldn't
|
|
pub fn apply_viewport_builder_to_window(
|
|
egui_ctx: &egui::Context,
|
|
window: &Window,
|
|
builder: &ViewportBuilder,
|
|
) {
|
|
if let Some(mouse_passthrough) = builder.mouse_passthrough
|
|
&& let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
|
|
{
|
|
log::warn!("set_cursor_hittest failed: {err}");
|
|
}
|
|
|
|
{
|
|
// In `create_winit_window_builder` we didn't know
|
|
// on what monitor the window would appear, so we didn't know
|
|
// how to translate egui ui point to native physical pixels.
|
|
// Now we do know:
|
|
|
|
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
|
|
|
if let Some(size) = builder.inner_size
|
|
&& window
|
|
.request_inner_size(PhysicalSize::new(
|
|
pixels_per_point * size.x,
|
|
pixels_per_point * size.y,
|
|
))
|
|
.is_some()
|
|
{
|
|
log::debug!("Failed to set window size");
|
|
}
|
|
if let Some(size) = builder.min_inner_size {
|
|
window.set_min_inner_size(Some(PhysicalSize::new(
|
|
pixels_per_point * size.x,
|
|
pixels_per_point * size.y,
|
|
)));
|
|
}
|
|
if let Some(size) = builder.max_inner_size {
|
|
window.set_max_inner_size(Some(PhysicalSize::new(
|
|
pixels_per_point * size.x,
|
|
pixels_per_point * size.y,
|
|
)));
|
|
}
|
|
if let Some(pos) = builder.position {
|
|
let pos = PhysicalPosition::new(pixels_per_point * pos.x, pixels_per_point * pos.y);
|
|
window.set_outer_position(pos);
|
|
}
|
|
if let Some(maximized) = builder.maximized {
|
|
window.set_maximized(maximized);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Short and fast description of a device event.
|
|
/// Useful for logging and profiling.
|
|
pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'static str {
|
|
use winit::event::DeviceEvent;
|
|
|
|
match event {
|
|
DeviceEvent::Added => "DeviceEvent::Added",
|
|
DeviceEvent::Removed => "DeviceEvent::Removed",
|
|
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
|
|
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
|
|
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
|
|
DeviceEvent::Button { .. } => "DeviceEvent::Button",
|
|
DeviceEvent::Key { .. } => "DeviceEvent::Key",
|
|
}
|
|
}
|
|
|
|
/// Short and fast description of a window event.
|
|
/// Useful for logging and profiling.
|
|
pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'static str {
|
|
use winit::event::WindowEvent;
|
|
|
|
match event {
|
|
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
|
|
WindowEvent::Resized { .. } => "WindowEvent::Resized",
|
|
WindowEvent::Moved { .. } => "WindowEvent::Moved",
|
|
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
|
|
WindowEvent::Destroyed => "WindowEvent::Destroyed",
|
|
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
|
|
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
|
|
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
|
|
WindowEvent::Focused { .. } => "WindowEvent::Focused",
|
|
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
|
|
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
|
|
WindowEvent::Ime { .. } => "WindowEvent::Ime",
|
|
WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved",
|
|
WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered",
|
|
WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft",
|
|
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
|
|
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
|
|
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
|
|
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
|
|
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
|
|
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
|
|
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
|
|
WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion",
|
|
WindowEvent::Touch { .. } => "WindowEvent::Touch",
|
|
WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged",
|
|
WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged",
|
|
WindowEvent::Occluded { .. } => "WindowEvent::Occluded",
|
|
WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture",
|
|
}
|
|
}
|