This commit is contained in:
nora 2023-04-18 15:38:14 +02:00
parent 12163d1338
commit 550b1644cb
363 changed files with 84081 additions and 16 deletions

View file

@ -0,0 +1,64 @@
# Changelog for egui-winit
All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
* Fix unsafe API: remove `State::new_with_wayland_display`; change `Clipboard::new` to take `&EventLoopWindowTarget<T>`.
## 0.21.1 - 2023-02-12
* Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
## 0.21.0 - 2023-02-08
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
* Fix bug where the cursor could get stuck using the wrong icon.
## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)).
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
* Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
* Use the new `RawInput::has_focus` field to indicate whether the window has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
## 0.18.0 - 2022-04-30
* Reexport `egui` crate
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Removed the features `dark-light` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)).
## 0.17.0 - 2022-02-22
* Fixed horizontal scrolling direction on Linux.
* Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatibility ([#1023](https://github.com/emilk/egui/pull/1023))
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Fixed `enable_drag` on Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)).
## 0.16.0 - 2021-12-29
* Added helper `EpiIntegration` ([#871](https://github.com/emilk/egui/pull/871)).
* Fixed shift key getting stuck enabled with the X11 option `shift:both_capslock` enabled ([#849](https://github.com/emilk/egui/pull/849)).
* Removed `State::is_quit_event` and `State::is_quit_shortcut` ([#881](https://github.com/emilk/egui/pull/881)).
* Updated `winit` to 0.26 ([#930](https://github.com/emilk/egui/pull/930)).
## 0.15.0 - 2021-10-24
First stand-alone release. Previously part of `egui_glium`.

View file

@ -0,0 +1,88 @@
[package]
name = "egui-winit"
version = "0.21.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
[features]
default = ["clipboard", "links", "wayland", "winit/default"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"]
## Enable cut/copy/paste to OS clipboard.
## If disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["arboard", "smithay-clipboard"]
## Enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde"]
## Enables Wayland support.
wayland = ["winit/wayland"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link)
# - It's also important that we don't impose an android-activity backend by taking this choice away from applications.
## Enable the `native-activity` backend via Winit on Android
android-native-activity = [ "winit/android-native-activity" ]
## Enable the `game-activity` backend via Winit on Android
android-game-activity = [ "winit/android-game-activity" ]
[dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"tracing",
] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
winit = { version = "0.28", default-features = false }
#! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.12.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.8.3", optional = true }
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
instant = { version = "0.1" }
[target.'cfg(target_arch="wasm32")'.dependencies]
instant = { version = "0.1", features = [
"wasm-bindgen",
] } # We use instant so we can (maybe) compile for web
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.6.3", optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.2", optional = true, default-features = false }

View file

@ -0,0 +1,10 @@
# egui-winit
[![Latest version](https://img.shields.io/crates/v/egui-winit.svg)](https://crates.io/crates/egui-winit)
[![Documentation](https://docs.rs/egui-winit/badge.svg)](https://docs.rs/egui-winit)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [`winit`](https://crates.io/crates/winit).
The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc.

View file

@ -0,0 +1,163 @@
use winit::event_loop::EventLoopWindowTarget;
/// Handles interfacing with the OS clipboard.
///
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works works within the same app is used instead.
pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: Option<arboard::Clipboard>,
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: Option<smithay_clipboard::Clipboard>,
/// Fallback manual clipboard.
clipboard: String,
}
impl Clipboard {
/// Construct a new instance
///
/// # Safety
///
/// The returned `Clipboard` must not outlive the input `_event_loop`.
pub fn new<T>(_event_loop: &EventLoopWindowTarget<T>) -> Self {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(),
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: init_smithay_clipboard(_event_loop),
clipboard: Default::default(),
}
}
pub fn get(&mut self) -> Option<String> {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
return match clipboard.load() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("smithay paste error: {err}");
None
}
};
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("arboard paste error: {err}");
None
}
};
}
Some(self.clipboard.clone())
}
pub fn set(&mut self, text: String) {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
clipboard.store(text);
return;
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
tracing::error!("arboard copy/cut error: {err}");
}
return;
}
self.clipboard = text;
}
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> {
tracing::debug!("Initializing arboard clipboard…");
match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(err) => {
tracing::warn!("Failed to initialize arboard clipboard: {err}");
None
}
}
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
fn init_smithay_clipboard<T>(
_event_loop: &EventLoopWindowTarget<T>,
) -> Option<smithay_clipboard::Clipboard> {
// Note: ideally "smithay-clipboard" would imply "wayland", but it doesn't.
#[cfg(feature = "wayland")]
{
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
if let Some(display) = _event_loop.wayland_display() {
tracing::debug!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
} else {
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
None
}
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!(
"You need to enable the 'wayland' feature of 'egui-winit' to get a working clipboard"
);
None
}
}

View file

@ -0,0 +1,917 @@
//! [`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!())]
//!
#![allow(clippy::manual_range_contains)]
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
#[cfg(feature = "accesskit")]
use egui::accesskit;
pub use winit;
pub mod clipboard;
mod window_settings;
pub use window_settings::WindowSettings;
use winit::event_loop::EventLoopWindowTarget;
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
window.scale_factor() as f32
}
pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 {
let size = window.inner_size();
egui::vec2(size.width as f32, size.height as f32)
}
// ----------------------------------------------------------------------------
#[must_use]
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 winit.
pub struct State {
start_time: instant::Instant,
egui_input: egui::RawInput,
pointer_pos_in_points: Option<egui::Pos2>,
any_pointer_button_down: bool,
current_cursor_icon: Option<egui::CursorIcon>,
/// What egui uses.
current_pixels_per_point: f32,
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
input_method_editor_started: bool,
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
}
impl State {
/// Construct a new instance
///
/// # Safety
///
/// The returned `State` must not outlive the input `_event_loop`.
pub fn new<T>(event_loop: &EventLoopWindowTarget<T>) -> Self {
let egui_input = egui::RawInput {
has_focus: false, // winit will tell us when we have focus
..Default::default()
};
Self {
start_time: instant::Instant::now(),
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
current_pixels_per_point: 1.0,
clipboard: clipboard::Clipboard::new(event_loop),
simulate_touch_screen: false,
pointer_touch_id: None,
input_method_editor_started: false,
#[cfg(feature = "accesskit")]
accesskit: None,
}
}
#[cfg(feature = "accesskit")]
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
) {
self.accesskit = Some(accesskit_winit::Adapter::new(
window,
initial_tree_update_factory,
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);
}
/// Call this when a new native Window is created for rendering to initialize the `pixels_per_point`
/// for that window.
///
/// In particular, on Android it is necessary to call this after each `Resumed` lifecycle
/// event, each time a new native window is created.
///
/// Once this has been initialized for a new window then this state will be maintained by handling
/// [`winit::event::WindowEvent::ScaleFactorChanged`] events.
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
self.egui_input.pixels_per_point = Some(pixels_per_point);
self.current_pixels_per_point = pixels_per_point;
}
/// The number of physical pixels per logical point,
/// as configured on the current egui context (see [`egui::Context::pixels_per_point`]).
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.current_pixels_per_point
}
/// The current input state.
/// This is changed by [`Self::on_event`] and cleared by [`Self::take_egui_input`].
#[inline]
pub fn egui_input(&self) -> &egui::RawInput {
&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).
pub fn take_egui_input(&mut self, window: &winit::window::Window) -> egui::RawInput {
let pixels_per_point = self.pixels_per_point();
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_input.screen_rect =
if screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0 {
Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
screen_size_in_points,
))
} else {
None
};
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_event(
&mut self,
egui_ctx: &egui::Context,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
use winit::event::WindowEvent;
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let pixels_per_point = *scale_factor as f32;
self.egui_input.pixels_per_point = Some(pixels_per_point);
self.current_pixels_per_point = pixels_per_point;
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::MouseInput { state, button, .. } => {
self.on_mouse_button_input(*state, *button);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(*delta);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
WindowEvent::CursorMoved { position, .. } => {
self.on_cursor_moved(*position);
EventResponse {
repaint: true,
consumed: egui_ctx.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
WindowEvent::Touch(touch) => {
self.on_touch(touch);
let consumed = match touch.phase {
winit::event::TouchPhase::Started
| winit::event::TouchPhase::Ended
| winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(),
winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(),
};
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::ReceivedCharacter(ch) => {
// On Mac 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.
let is_mac_cmd = cfg!(target_os = "macos")
&& (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd);
let consumed = if is_printable_char(*ch) && !is_mac_cmd {
self.egui_input
.events
.push(egui::Event::Text(ch.to_string()));
egui_ctx.wants_keyboard_input()
} else {
false
};
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::Ime(ime) => {
// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
// How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS
// and Windows.
//
// - On Windows, before and after each Commit will produce an Enable/Disabled
// event.
// - On MacOS, only when user explicit enable/disable ime. No Disabled
// after Commit.
//
// We use input_method_editor_started to manually insert CompositionStart
// between Commits.
match ime {
winit::event::Ime::Enabled | winit::event::Ime::Disabled => (),
winit::event::Ime::Commit(text) => {
self.input_method_editor_started = false;
self.egui_input
.events
.push(egui::Event::CompositionEnd(text.clone()));
}
winit::event::Ime::Preedit(text, ..) => {
if !self.input_method_editor_started {
self.input_method_editor_started = true;
self.egui_input.events.push(egui::Event::CompositionStart);
}
self.egui_input
.events
.push(egui::Event::CompositionUpdate(text.clone()));
}
};
EventResponse {
repaint: true,
consumed: egui_ctx.wants_keyboard_input(),
}
}
WindowEvent::KeyboardInput { input, .. } => {
self.on_keyboard_input(input);
let consumed = egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::Focused(has_focus) => {
self.egui_input.has_focus = *has_focus;
// We will not be given a KeyboardInput event when the modifiers are released while
// the window does not have focus. Unset all modifier state to be safe.
self.egui_input.modifiers = egui::Modifiers::default();
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) => {
self.egui_input.modifiers.alt = state.alt();
self.egui_input.modifiers.ctrl = state.ctrl();
self.egui_input.modifiers.shift = state.shift();
self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && state.logo();
self.egui_input.modifiers.command = if cfg!(target_os = "macos") {
state.logo()
} else {
state.ctrl()
};
EventResponse {
repaint: true,
consumed: false,
}
}
// Things that may require repaint:
WindowEvent::CloseRequested
| WindowEvent::CursorEntered { .. }
| WindowEvent::Destroyed
| WindowEvent::Occluded(_)
| WindowEvent::Resized(_)
| WindowEvent::ThemeChanged(_)
| WindowEvent::TouchpadPressure { .. } => EventResponse {
repaint: true,
consumed: false,
},
// Things we completely ignore:
WindowEvent::AxisMotion { .. }
| WindowEvent::Moved(_)
| WindowEvent::SmartMagnify { .. }
| WindowEvent::TouchpadRotate { .. } => EventResponse {
repaint: false,
consumed: false,
},
WindowEvent::TouchpadMagnify { 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: egui_ctx.wants_pointer_input(),
}
}
}
}
/// 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 {
if 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: 0.0,
});
} 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: 0.0,
});
};
}
}
}
}
fn on_cursor_moved(&mut self, pos_in_pixels: winit::dpi::PhysicalPosition<f64>) {
let pos_in_points = egui::pos2(
pos_in_pixels.x as f32 / self.pixels_per_point(),
pos_in_pixels.y as f32 / self.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: 0.0,
});
}
} else {
self.egui_input
.events
.push(egui::Event::PointerMoved(pos_in_points));
}
}
fn on_touch(&mut self, touch: &winit::event::Touch) {
// 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: match touch.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,
},
pos: egui::pos2(
touch.location.x as f32 / self.pixels_per_point(),
touch.location.y as f32 / self.pixels_per_point(),
),
force: match touch.force {
Some(winit::event::Force::Normalized(force)) => force as f32,
Some(winit::event::Force::Calibrated {
force,
max_possible_force,
..
}) => (force / max_possible_force) as f32,
None => 0_f32,
},
});
// 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() == 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(touch.location);
self.on_mouse_button_input(
winit::event::ElementState::Pressed,
winit::event::MouseButton::Left,
);
}
winit::event::TouchPhase::Moved => {
self.on_cursor_moved(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, delta: winit::event::MouseScrollDelta) {
{
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) / self.pixels_per_point(),
),
};
let modifiers = self.egui_input.modifiers;
self.egui_input.events.push(egui::Event::MouseWheel {
unit,
delta,
modifiers,
});
}
let delta = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
egui::vec2(x, y) * points_per_scroll_line
}
winit::event::MouseScrollDelta::PixelDelta(delta) => {
egui::vec2(delta.x as f32, delta.y as f32) / self.pixels_per_point()
}
};
if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command {
// Treat as zoom instead:
let factor = (delta.y / 200.0).exp();
self.egui_input.events.push(egui::Event::Zoom(factor));
} else if self.egui_input.modifiers.shift {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
self.egui_input
.events
.push(egui::Event::Scroll(egui::vec2(delta.x + delta.y, 0.0)));
} else {
self.egui_input.events.push(egui::Event::Scroll(delta));
}
}
fn on_keyboard_input(&mut self, input: &winit::event::KeyboardInput) {
if let Some(keycode) = input.virtual_keycode {
let pressed = input.state == winit::event::ElementState::Pressed;
if pressed {
// VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
// so we detect these things manually:
if is_cut_command(self.egui_input.modifiers, keycode) {
self.egui_input.events.push(egui::Event::Cut);
} else if is_copy_command(self.egui_input.modifiers, keycode) {
self.egui_input.events.push(egui::Event::Copy);
} else if is_paste_command(self.egui_input.modifiers, keycode) {
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));
}
}
}
}
if let Some(key) = translate_virtual_key_code(keycode) {
self.egui_input.events.push(egui::Event::Key {
key,
pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers,
});
}
}
}
/// 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: &winit::window::Window,
egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput,
) {
let egui::PlatformOutput {
cursor_icon,
open_url,
copied_text,
events: _, // handled above
mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = platform_output;
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
}
if !copied_text.is_empty() {
self.clipboard.set(copied_text);
}
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
}
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_ref() {
if let Some(update) = accesskit_update {
accesskit.update_if_active(|| update);
}
}
}
fn set_cursor_icon(&mut self, window: &winit::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_icon(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 open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) {
tracing::warn!("Failed to open url: {}", err);
}
#[cfg(not(feature = "webbrowser"))]
{
tracing::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: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::X)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == winit::event::VirtualKeyCode::Delete)
}
fn is_copy_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::C)
|| (cfg!(target_os = "windows")
&& modifiers.ctrl
&& keycode == winit::event::VirtualKeyCode::Insert)
}
fn is_paste_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::V)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == winit::event::VirtualKeyCode::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::Other(1) => Some(egui::PointerButton::Extra1),
winit::event::MouseButton::Other(2) => Some(egui::PointerButton::Extra2),
winit::event::MouseButton::Other(_) => None,
}
}
fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui::Key> {
use egui::Key;
use winit::event::VirtualKeyCode;
Some(match key {
VirtualKeyCode::Down => Key::ArrowDown,
VirtualKeyCode::Left => Key::ArrowLeft,
VirtualKeyCode::Right => Key::ArrowRight,
VirtualKeyCode::Up => Key::ArrowUp,
VirtualKeyCode::Escape => Key::Escape,
VirtualKeyCode::Tab => Key::Tab,
VirtualKeyCode::Back => Key::Backspace,
VirtualKeyCode::Return => Key::Enter,
VirtualKeyCode::Space => Key::Space,
VirtualKeyCode::Insert => Key::Insert,
VirtualKeyCode::Delete => Key::Delete,
VirtualKeyCode::Home => Key::Home,
VirtualKeyCode::End => Key::End,
VirtualKeyCode::PageUp => Key::PageUp,
VirtualKeyCode::PageDown => Key::PageDown,
VirtualKeyCode::Minus => Key::Minus,
// Using Mac the key with the Plus sign on it is reported as the Equals key
// (with both English and Swedish keyboard).
VirtualKeyCode::Equals => Key::PlusEquals,
VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0,
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1,
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2,
VirtualKeyCode::Key3 | VirtualKeyCode::Numpad3 => Key::Num3,
VirtualKeyCode::Key4 | VirtualKeyCode::Numpad4 => Key::Num4,
VirtualKeyCode::Key5 | VirtualKeyCode::Numpad5 => Key::Num5,
VirtualKeyCode::Key6 | VirtualKeyCode::Numpad6 => Key::Num6,
VirtualKeyCode::Key7 | VirtualKeyCode::Numpad7 => Key::Num7,
VirtualKeyCode::Key8 | VirtualKeyCode::Numpad8 => Key::Num8,
VirtualKeyCode::Key9 | VirtualKeyCode::Numpad9 => Key::Num9,
VirtualKeyCode::A => Key::A,
VirtualKeyCode::B => Key::B,
VirtualKeyCode::C => Key::C,
VirtualKeyCode::D => Key::D,
VirtualKeyCode::E => Key::E,
VirtualKeyCode::F => Key::F,
VirtualKeyCode::G => Key::G,
VirtualKeyCode::H => Key::H,
VirtualKeyCode::I => Key::I,
VirtualKeyCode::J => Key::J,
VirtualKeyCode::K => Key::K,
VirtualKeyCode::L => Key::L,
VirtualKeyCode::M => Key::M,
VirtualKeyCode::N => Key::N,
VirtualKeyCode::O => Key::O,
VirtualKeyCode::P => Key::P,
VirtualKeyCode::Q => Key::Q,
VirtualKeyCode::R => Key::R,
VirtualKeyCode::S => Key::S,
VirtualKeyCode::T => Key::T,
VirtualKeyCode::U => Key::U,
VirtualKeyCode::V => Key::V,
VirtualKeyCode::W => Key::W,
VirtualKeyCode::X => Key::X,
VirtualKeyCode::Y => Key::Y,
VirtualKeyCode::Z => Key::Z,
VirtualKeyCode::F1 => Key::F1,
VirtualKeyCode::F2 => Key::F2,
VirtualKeyCode::F3 => Key::F3,
VirtualKeyCode::F4 => Key::F4,
VirtualKeyCode::F5 => Key::F5,
VirtualKeyCode::F6 => Key::F6,
VirtualKeyCode::F7 => Key::F7,
VirtualKeyCode::F8 => Key::F8,
VirtualKeyCode::F9 => Key::F9,
VirtualKeyCode::F10 => Key::F10,
VirtualKeyCode::F11 => Key::F11,
VirtualKeyCode::F12 => Key::F12,
VirtualKeyCode::F13 => Key::F13,
VirtualKeyCode::F14 => Key::F14,
VirtualKeyCode::F15 => Key::F15,
VirtualKeyCode::F16 => Key::F16,
VirtualKeyCode::F17 => Key::F17,
VirtualKeyCode::F18 => Key::F18,
VirtualKeyCode::F19 => Key::F19,
VirtualKeyCode::F20 => Key::F20,
_ => {
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::Hand),
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),
}
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
#[allow(unused_imports)]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
#[allow(unused_imports)]
pub(crate) use profile_scope;

View file

@ -0,0 +1,144 @@
/// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WindowSettings {
/// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform.
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>,
fullscreen: bool,
/// Inner size of window in logical pixels
inner_size_points: Option<egui::Vec2>,
}
impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows.
window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else {
// Other platforms use the outer position.
window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};
Self {
position,
fullscreen: window.fullscreen().is_some(),
inner_size_points: Some(egui::vec2(
inner_size_points.width,
inner_size_points.height,
)),
}
}
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
self.inner_size_points
}
pub fn initialize_window(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the clamping behavior is managed by the function
// clamp_window_to_sane_position.
if let Some(pos) = self.position {
window = window.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
if let Some(inner_size_points) = self.inner_size_points {
window
.with_inner_size(winit::dpi::LogicalSize {
width: inner_size_points.x as f64,
height: inner_size_points.y as f64,
})
.with_fullscreen(
self.fullscreen
.then_some(winit::window::Fullscreen::Borderless(None)),
)
} else {
window
}
}
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
use egui::NumExt as _;
if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows
let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size);
*size = size.at_most(max_size);
}
}
pub fn clamp_window_to_sane_position<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
}
}
}