mirror of
https://github.com/Noratrieb/game-wip-dontplay.git
synced 2026-01-17 04:45:02 +01:00
vendor
This commit is contained in:
parent
12163d1338
commit
550b1644cb
363 changed files with 84081 additions and 16 deletions
235
egui/crates/eframe/CHANGELOG.md
Normal file
235
egui/crates/eframe/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Changelog for eframe
|
||||
All notable changes to the `eframe` crate.
|
||||
|
||||
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
|
||||
|
||||
|
||||
## Unreleased
|
||||
#### Desktop/Native:
|
||||
* Add `Frame::request_screenshot` and `Frame::screenshot` to communicate to the backend that a screenshot of the current frame should be exposed by `Frame` during `App::post_rendering` ([#2676](https://github.com/emilk/egui/pull/2676)).
|
||||
* Add `eframe::run_simple_native` - a simple API for simple apps ([#2453](https://github.com/emilk/egui/pull/2453)).
|
||||
* Fix bug where the eframe window is never destroyed on Linux when using `run_and_return` ([#2892](https://github.com/emilk/egui/issues/2892))
|
||||
|
||||
#### Web:
|
||||
* Bug fix: modifiers keys getting stuck on alt-tab ([#2857](https://github.com/emilk/egui/pull/2857)).
|
||||
|
||||
## 0.21.3 - 2023-02-15
|
||||
* Fix typing the letter 'P' on web ([#2740](https://github.com/emilk/egui/pull/2740)).
|
||||
|
||||
|
||||
## 0.21.2 - 2023-02-12
|
||||
* Allow compiling `eframe` with `--no-default-features` ([#2728](https://github.com/emilk/egui/pull/2728)).
|
||||
|
||||
|
||||
## 0.21.1 - 2023-02-12
|
||||
* Fixed crash when native 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 - Update to `winit` 0.28
|
||||
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
|
||||
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
|
||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
||||
* Fix bug where the cursor could get stuck using the wrong icon.
|
||||
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
|
||||
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
|
||||
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
|
||||
|
||||
#### Web:
|
||||
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11
|
||||
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||
* Allow empty textures with the glow renderer.
|
||||
|
||||
#### Desktop/Native:
|
||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
||||
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
|
||||
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
|
||||
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
|
||||
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
|
||||
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
|
||||
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
||||
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
||||
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
||||
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
|
||||
|
||||
#### Web:
|
||||
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
* Added `wgpu` rendering backed ([#1564](https://github.com/emilk/egui/pull/1564)):
|
||||
* Added features `wgpu` and `glow`.
|
||||
* Added `NativeOptions::renderer` to switch between the rendering backends.
|
||||
* `egui_glow`: remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)).
|
||||
* Added `App::post_rendering` for e.g. reading the framebuffer ([#1591](https://github.com/emilk/egui/pull/1591)).
|
||||
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
|
||||
* Fixed bug where the result returned from `App::on_exit_event` would sometimes be ignored ([#1696](https://github.com/emilk/egui/pull/1696)).
|
||||
* Added `NativeOptions::follow_system_theme` and `NativeOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
||||
* Selectively expose parts of the API based on target arch (`wasm32` or not) ([#1867](https://github.com/emilk/egui/pull/1867)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
|
||||
* Added ability to read window position and size with `frame.info().window_info` ([#1617](https://github.com/emilk/egui/pull/1617)).
|
||||
* Allow running on native without hardware accelerated rendering. Change with `NativeOptions::hardware_acceleration` ([#1681](https://github.com/emilk/egui/pull/1681), [#1693](https://github.com/emilk/egui/pull/1693)).
|
||||
* 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)).
|
||||
* Added `Frame::set_visible` ([#1808](https://github.com/emilk/egui/pull/1808)).
|
||||
* Added fullscreen support ([#1866](https://github.com/emilk/egui/pull/1866)).
|
||||
* You can now continue execution after closing the native desktop window ([#1889](https://github.com/emilk/egui/pull/1889)).
|
||||
* `Frame::quit` has been renamed to `Frame::close` and `App::on_exit_event` is now `App::on_close_event` ([#1943](https://github.com/emilk/egui/pull/1943)).
|
||||
|
||||
#### Web:
|
||||
* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
|
||||
* Added `WebOptions::follow_system_theme` and `WebOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
||||
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
|
||||
|
||||
|
||||
## 0.18.0 - 2022-04-30
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Removed `eframe::epi` - everything is now in `eframe` (`eframe::App`, `eframe::Frame` etc) ([#1545](https://github.com/emilk/egui/pull/1545)).
|
||||
* Removed `Frame::request_repaint` - just call `egui::Context::request_repaint` for the same effect ([#1366](https://github.com/emilk/egui/pull/1366)).
|
||||
* Changed app creation/setup ([#1363](https://github.com/emilk/egui/pull/1363)):
|
||||
* Removed `App::setup` and `App::name`.
|
||||
* Provide `CreationContext` when creating app with egui context, storage, integration info and glow context.
|
||||
* Change interface of `run_native` and `start_web`.
|
||||
* Added `Frame::storage()` and `Frame::storage_mut()` ([#1418](https://github.com/emilk/egui/pull/1418)).
|
||||
* You can now load/save state in `App::update`
|
||||
* Changed `App::update` to take `&mut Frame` instead of `&Frame`.
|
||||
* `Frame` is no longer `Clone` or `Sync`.
|
||||
* Added `glow` (OpenGL) context to `Frame` ([#1425](https://github.com/emilk/egui/pull/1425)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* Remove the `egui_glium` feature. `eframe` will now always use `egui_glow` as the native backend ([#1357](https://github.com/emilk/egui/pull/1357)).
|
||||
* Change default for `NativeOptions::drag_and_drop_support` to `true` ([#1329](https://github.com/emilk/egui/pull/1329)).
|
||||
* Added new `NativeOptions`: `vsync`, `multisampling`, `depth_buffer`, `stencil_buffer`.
|
||||
* `dark-light` (dark mode detection) is now an opt-in feature ([#1437](https://github.com/emilk/egui/pull/1437)).
|
||||
* Fixed potential scale bug when DPI scaling changes (e.g. when dragging a window between different displays) ([#1441](https://github.com/emilk/egui/pull/1441)).
|
||||
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
|
||||
* Moved app persistence to a background thread, allowing for smoother frame rates (on native).
|
||||
* Added `Frame::set_window_pos` ([#1505](https://github.com/emilk/egui/pull/1505)).
|
||||
|
||||
#### Web:
|
||||
* Use full browser width by default ([#1378](https://github.com/emilk/egui/pull/1378)).
|
||||
* egui code will no longer be called after panic ([#1306](https://github.com/emilk/egui/pull/1306)).
|
||||
|
||||
|
||||
## 0.17.0 - 2022-02-22
|
||||
* Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)).
|
||||
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
|
||||
* Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
|
||||
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
|
||||
* Fixed horizontal scrolling direction on Linux.
|
||||
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
|
||||
* Added `NativeOptions::initial_window_pos`.
|
||||
* Fixed `enable_drag` for Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
|
||||
|
||||
#### Web:
|
||||
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).
|
||||
* Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)).
|
||||
* Updated `eframe::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)).
|
||||
* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)).
|
||||
|
||||
|
||||
## 0.16.0 - 2021-12-29
|
||||
* `Frame` can now be cloned, saved, and passed to background threads ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
* Added `Frame::request_repaint` to replace `repaint_signal` ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
* Added `Frame::alloc_texture/free_texture` to replace `tex_allocator` ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
|
||||
#### Web:
|
||||
* Fixed [dark rendering in WebKitGTK](https://github.com/emilk/egui/issues/794) ([#888](https://github.com/emilk/egui/pull/888/)).
|
||||
* Added feature `glow` to switch to a [`glow`](https://github.com/grovesNL/glow) based painter ([#868](https://github.com/emilk/egui/pull/868)).
|
||||
|
||||
|
||||
## 0.15.0 - 2021-10-24
|
||||
* `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
|
||||
* `Frame` now provides `set_decorations` to set whether to show window decorations.
|
||||
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
||||
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
||||
|
||||
#### Desktop/Native:
|
||||
* Increase native scroll speed.
|
||||
* Added new backend `egui_glow` as an alternative to `egui_glium`. Enable with `default-features = false, features = ["default_fonts", "egui_glow"]`.
|
||||
|
||||
#### Web:
|
||||
* Implement `eframe::NativeTexture` trait for the WebGL painter.
|
||||
* Deprecate `Painter::register_webgl_texture.
|
||||
* Fixed multiline paste.
|
||||
* Fixed painting with non-opaque backgrounds.
|
||||
* Improve text input on mobile and for IME.
|
||||
|
||||
|
||||
## 0.14.0 - 2021-08-24
|
||||
* Added dragging and dropping files into egui.
|
||||
* Improve http fetch API.
|
||||
* `run_native` now returns when the app is closed.
|
||||
* Web: Made text thicker and less pixelated.
|
||||
|
||||
|
||||
## 0.13.1 - 2021-06-24
|
||||
* Fixed `http` feature flag and docs
|
||||
|
||||
|
||||
## 0.13.0 - 2021-06-24
|
||||
* `App::setup` now takes a `Frame` and `Storage` by argument.
|
||||
* `App::load` has been removed. Implement `App::setup` instead.
|
||||
* Web: Default to light visuals unless the system reports a preference for dark mode.
|
||||
* Web: Improve alpha blending, making fonts look much better (especially in light mode)
|
||||
* Web: Fix double-paste bug
|
||||
|
||||
|
||||
## 0.12.0 - 2021-05-10
|
||||
* Moved options out of `trait App` into new `NativeOptions`.
|
||||
* Added option for `always_on_top`.
|
||||
* Web: Scroll faster when scrolling with mouse wheel.
|
||||
|
||||
|
||||
## 0.11.0 - 2021-04-05
|
||||
* You can now turn your window transparent with the `App::transparent` option.
|
||||
* You can now disable window decorations with the `App::decorated` option.
|
||||
* Web: [Fix mobile and IME text input](https://github.com/emilk/egui/pull/253)
|
||||
* Web: Hold down a modifier key when clicking a link to open it in a new tab.
|
||||
|
||||
Contributors: [n2](https://github.com/n2)
|
||||
|
||||
|
||||
## 0.10.0 - 2021-02-28
|
||||
* [You can now set your own app icons](https://github.com/emilk/egui/pull/193).
|
||||
* You can control the initial size of the native window with `App::initial_window_size`.
|
||||
* You can control the maximum egui web canvas size with `App::max_size_points`.
|
||||
* `Frame::tex_allocator()` no longer returns an `Option` (there is always a texture allocator).
|
||||
|
||||
|
||||
## 0.9.0 - 2021-02-07
|
||||
* [Added support for HTTP body](https://github.com/emilk/egui/pull/139).
|
||||
* Web: Right-clicks will no longer open browser context menu.
|
||||
* Web: Fix a bug where one couldn't select items in a combo box on a touch screen.
|
||||
|
||||
|
||||
## 0.8.0 - 2021-01-17
|
||||
* Simplify `TextureAllocator` interface.
|
||||
* WebGL2 is now supported, with improved texture sampler. WebGL1 will be used as a fallback.
|
||||
* Web: Slightly improved alpha-blending (work-around for non-existing linear-space blending).
|
||||
* Web: Call `prevent_default` for arrow keys when entering text
|
||||
|
||||
|
||||
## 0.7.0 - 2021-01-04
|
||||
* Initial release of `eframe`
|
||||
182
egui/crates/eframe/Cargo.toml
Normal file
182
egui/crates/eframe/Cargo.toml
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
[package]
|
||||
name = "eframe"
|
||||
version = "0.21.3"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["accesskit", "default_fonts", "glow"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||
|
||||
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
||||
##
|
||||
## See also [`NativeOptions::follow_system_theme`] and [`NativeOptions::default_theme`].
|
||||
dark-light = ["dep:dark-light"]
|
||||
|
||||
## If set, egui will use `include_bytes!` to bundle some fonts.
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
|
||||
glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
|
||||
|
||||
## Enables wayland support and fixes clipboard issue.
|
||||
wayland = ["egui-winit/wayland"]
|
||||
|
||||
## Enable saving app state to disk.
|
||||
persistence = [
|
||||
"directories-next",
|
||||
"egui-winit/serde",
|
||||
"egui/persistence",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
##
|
||||
## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers.
|
||||
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
|
||||
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
|
||||
|
||||
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||
##
|
||||
## For other platforms, use the "accesskit" feature instead.
|
||||
web_screen_reader = ["tts"]
|
||||
|
||||
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
|
||||
## This is used to generate images for the examples.
|
||||
__screenshot = ["dep:image"]
|
||||
|
||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
||||
## This overrides the `glow` feature.
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
|
||||
# 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 `egui-winit` on Android
|
||||
android-native-activity = [ "egui-winit/android-native-activity" ]
|
||||
## Enable the `game-activity` backend via `egui-winit` on Android
|
||||
android-game-activity = [ "egui-winit/android-game-activity" ]
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
||||
"bytemuck",
|
||||
"tracing",
|
||||
] }
|
||||
thiserror = "1.0.37"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
|
||||
glow = { version = "0.12", optional = true }
|
||||
ron = { version = "0.8", optional = true, features = ["integer128"] }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# -------------------------------------------
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
|
||||
"clipboard",
|
||||
"links",
|
||||
] }
|
||||
raw-window-handle = { version = "0.5.0" }
|
||||
winit = "0.28.1"
|
||||
|
||||
# optional native:
|
||||
dark-light = { version = "1.0", optional = true }
|
||||
directories-next = { version = "2", optional = true }
|
||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
|
||||
"winit",
|
||||
] } # if wgpu is used, use it with winit
|
||||
pollster = { version = "0.3", optional = true } # needed for wgpu
|
||||
|
||||
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
|
||||
# this can be done at the same time we expose x11/wayland features of winit crate.
|
||||
glutin = { version = "0.30", optional = true }
|
||||
glutin-winit = { version = "0.3.0", optional = true }
|
||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||
"png",
|
||||
] }
|
||||
puffin = { version = "0.14", optional = true }
|
||||
wgpu = { version = "0.15.0", optional = true }
|
||||
|
||||
# -------------------------------------------
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
bytemuck = "1.7"
|
||||
js-sys = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
wasm-bindgen = "=0.2.84"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3.58", features = [
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"Clipboard",
|
||||
"ClipboardEvent",
|
||||
"CompositionEvent",
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"DataTransfer",
|
||||
"DataTransferItem",
|
||||
"DataTransferItemList",
|
||||
"Document",
|
||||
"DomRect",
|
||||
"DragEvent",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"ExtSRgb",
|
||||
"File",
|
||||
"FileList",
|
||||
"FocusEvent",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"InputEvent",
|
||||
"KeyboardEvent",
|
||||
"Location",
|
||||
"MediaQueryList",
|
||||
"MediaQueryListEvent",
|
||||
"MouseEvent",
|
||||
"Navigator",
|
||||
"Performance",
|
||||
"Storage",
|
||||
"Touch",
|
||||
"TouchEvent",
|
||||
"TouchList",
|
||||
"WebGl2RenderingContext",
|
||||
"WebglDebugRendererInfo",
|
||||
"WebGlRenderingContext",
|
||||
"WheelEvent",
|
||||
"Window",
|
||||
] }
|
||||
|
||||
# optional web:
|
||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||
tts = { version = "0.25", optional = true, default-features = false }
|
||||
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }
|
||||
63
egui/crates/eframe/README.md
Normal file
63
egui/crates/eframe/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# eframe: the [`egui`](https://github.com/emilk/egui) framework
|
||||
|
||||
[](https://crates.io/crates/eframe)
|
||||
[](https://docs.rs/eframe)
|
||||

|
||||

|
||||
|
||||
`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (cross platform) or be compiled to a web app (using WASM).
|
||||
|
||||
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
|
||||
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
|
||||
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
|
||||
|
||||
For how to use `egui`, see [the egui docs](https://docs.rs/egui).
|
||||
|
||||
---
|
||||
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
|
||||
|
||||
To use on Linux, first run:
|
||||
|
||||
```
|
||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
||||
```
|
||||
|
||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
|
||||
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
|
||||
|
||||
|
||||
## Alternatives
|
||||
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
|
||||
|
||||
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
|
||||
|
||||
|
||||
## Problems with running egui on the web
|
||||
`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
|
||||
|
||||
* Rendering: Getting pixel-perfect rendering right on the web is very difficult.
|
||||
* Search: you cannot search an egui web page like you would a normal web page.
|
||||
* Bringing up an on-screen keyboard on mobile: there is no JS function to do this, so `eframe` fakes it by adding some invisible DOM elements. It doesn't always work.
|
||||
* Mobile text editing is not as good as for a normal web app.
|
||||
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
|
||||
* No integration with browser settings for colors and fonts.
|
||||
|
||||
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
|
||||
|
||||
The suggested use for `eframe` are for web apps where performance and responsiveness are more important than accessibility and mobile text editing.
|
||||
|
||||
|
||||
## Companion crates
|
||||
Not all rust crates work when compiled to WASM, but here are some useful crates have been designed to work well both natively and as WASM:
|
||||
|
||||
* Audio: [`cpal`](https://github.com/RustAudio/cpal).
|
||||
* HTTP client: [`ehttp`](https://github.com/emilk/ehttp) and [`reqwest`](https://github.com/seanmonstar/reqwest).
|
||||
* Time: [`chrono`](https://github.com/chronotope/chrono).
|
||||
* WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock).
|
||||
|
||||
|
||||
## Name
|
||||
The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`frame` is a framework, `egui` is a library).
|
||||
1157
egui/crates/eframe/src/epi.rs
Normal file
1157
egui/crates/eframe/src/epi.rs
Normal file
File diff suppressed because it is too large
Load diff
323
egui/crates/eframe/src/lib.rs
Normal file
323
egui/crates/eframe/src/lib.rs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
//! eframe - the [`egui`] framework crate
|
||||
//!
|
||||
//! If you are planning to write an app for web or native,
|
||||
//! and want to use [`egui`] for everything, then `eframe` is for you!
|
||||
//!
|
||||
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
|
||||
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
//!
|
||||
//! In short, you implement [`App`] (especially [`App::update`]) and then
|
||||
//! call [`crate::run_native`] from your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`.
|
||||
//!
|
||||
//! ## Usage, native:
|
||||
//! ``` no_run
|
||||
//! use eframe::egui;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let native_options = eframe::NativeOptions::default();
|
||||
//! eframe::run_native("My egui App", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Default)]
|
||||
//! struct MyEguiApp {}
|
||||
//!
|
||||
//! impl MyEguiApp {
|
||||
//! fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
//! // Restore app state using cc.storage (requires the "persistence" feature).
|
||||
//! // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
//! // for e.g. egui::PaintCallback.
|
||||
//! Self::default()
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl eframe::App for MyEguiApp {
|
||||
//! fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
//! egui::CentralPanel::default().show(ctx, |ui| {
|
||||
//! ui.heading("Hello World!");
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Usage, web:
|
||||
//! ``` no_run
|
||||
//! #[cfg(target_arch = "wasm32")]
|
||||
//! use wasm_bindgen::prelude::*;
|
||||
//!
|
||||
//! /// Call this once from the HTML.
|
||||
//! #[cfg(target_arch = "wasm32")]
|
||||
//! #[wasm_bindgen]
|
||||
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
|
||||
//! let web_options = eframe::WebOptions::default();
|
||||
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Simplified usage
|
||||
//! If your app is only for native, and you don't need advanced features like state persistence,
|
||||
//! then you can use the simpler function [`run_simple_native`].
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
// Re-export all useful libraries:
|
||||
pub use {egui, egui::emath, egui::epaint};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
|
||||
// Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is:
|
||||
pub use epi::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// When compiling for web
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod web;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web::AppRunnerRef;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web_sys;
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and start running the given app.
|
||||
///
|
||||
/// ``` no_run
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// use wasm_bindgen::prelude::*;
|
||||
///
|
||||
/// /// This is the entry-point for all the web-assembly.
|
||||
/// /// This is called from the HTML.
|
||||
/// /// It loads the app, installs some callbacks, then returns.
|
||||
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
|
||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub struct WebHandle {
|
||||
/// handle: AppRunnerRef,
|
||||
/// }
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
|
||||
/// let web_options = eframe::WebOptions::default();
|
||||
/// eframe::start_web(
|
||||
/// canvas_id,
|
||||
/// web_options,
|
||||
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
|
||||
/// )
|
||||
/// .await
|
||||
/// .map(|handle| WebHandle { handle })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Failing to initialize WebGL graphics.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn start_web(
|
||||
canvas_id: &str,
|
||||
web_options: WebOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||
let handle = web::start(canvas_id, web_options, app_creator).await?;
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
mod native;
|
||||
|
||||
/// This is how you start a native (desktop) app.
|
||||
///
|
||||
/// The first argument is name of your app, used for the title bar of the native window
|
||||
/// and the save location of persistence (see [`App::save`]).
|
||||
///
|
||||
/// Call from `fn main` like this:
|
||||
/// ``` no_run
|
||||
/// use eframe::egui;
|
||||
///
|
||||
/// fn main() -> eframe::Result<()> {
|
||||
/// let native_options = eframe::NativeOptions::default();
|
||||
/// eframe::run_native("MyApp", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyEguiApp {}
|
||||
///
|
||||
/// impl MyEguiApp {
|
||||
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
/// // Restore app state using cc.storage (requires the "persistence" feature).
|
||||
/// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
/// // for e.g. egui::PaintCallback.
|
||||
/// Self::default()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl eframe::App for MyEguiApp {
|
||||
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
/// egui::CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.heading("Hello World!");
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> Result<()> {
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
||||
);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
tracing::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
tracing::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The simplest way to get started when writing a native app.
|
||||
///
|
||||
/// This does NOT support persistence. For that you need to use [`run_native`].
|
||||
///
|
||||
/// # Example
|
||||
/// ``` no_run
|
||||
/// fn main() -> eframe::Result<()> {
|
||||
/// // Our application state:
|
||||
/// let mut name = "Arthur".to_owned();
|
||||
/// let mut age = 42;
|
||||
///
|
||||
/// let options = eframe::NativeOptions::default();
|
||||
/// eframe::run_simple_native("My egui App", options, move |ctx, _frame| {
|
||||
/// egui::CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.heading("My egui Application");
|
||||
/// ui.horizontal(|ui| {
|
||||
/// let name_label = ui.label("Your name: ");
|
||||
/// ui.text_edit_singleline(&mut name)
|
||||
/// .labelled_by(name_label.id);
|
||||
/// });
|
||||
/// ui.add(egui::Slider::new(&mut age, 0..=120).text("age"));
|
||||
/// if ui.button("Click each year").clicked() {
|
||||
/// age += 1;
|
||||
/// }
|
||||
/// ui.label(format!("Hello '{}', age {}", name, age));
|
||||
/// });
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub fn run_simple_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static,
|
||||
) -> Result<()> {
|
||||
struct SimpleApp<U> {
|
||||
update_fun: U,
|
||||
}
|
||||
impl<U: FnMut(&egui::Context, &mut Frame)> App for SimpleApp<U> {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
|
||||
(self.update_fun)(ctx, frame);
|
||||
}
|
||||
}
|
||||
|
||||
run_native(
|
||||
app_name,
|
||||
native_options,
|
||||
Box::new(|_cc| Box::new(SimpleApp { update_fun })),
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The different problems that can occur when trying to run `eframe`.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("winit error: {0}")]
|
||||
Winit(#[from] winit::error::OsError),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("glutin error: {0}")]
|
||||
Glutin(#[from] glutin::error::Error),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
|
||||
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[error("WGPU error: {0}")]
|
||||
Wgpu(#[from] egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
mod profiling_scopes {
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_function {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_function!($($arg)*);
|
||||
};
|
||||
}
|
||||
pub(crate) use profile_function;
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_scope {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_scope!($($arg)*);
|
||||
};
|
||||
}
|
||||
pub(crate) use profile_scope;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub(crate) use profiling_scopes::*;
|
||||
595
egui/crates/eframe/src/native/epi_integration.rs
Normal file
595
egui/crates/eframe/src/native/epi_integration.rs
Normal file
|
|
@ -0,0 +1,595 @@
|
|||
use winit::event_loop::EventLoopWindowTarget;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
use egui::NumExt as _;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
||||
|
||||
use crate::{epi, Theme, WindowInfo};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WindowState {
|
||||
// We cannot simply call `winit::Window::is_minimized/is_maximized`
|
||||
// because that deadlocks on mac.
|
||||
pub minimized: bool,
|
||||
pub maximized: bool,
|
||||
}
|
||||
|
||||
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||
winit::dpi::LogicalSize {
|
||||
width: points.x as f64,
|
||||
height: points.y as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_window_info(
|
||||
window: &winit::window::Window,
|
||||
pixels_per_point: f32,
|
||||
window_state: &WindowState,
|
||||
) -> WindowInfo {
|
||||
let position = window
|
||||
.outer_position()
|
||||
.ok()
|
||||
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
|
||||
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
|
||||
|
||||
let monitor = window.current_monitor().is_some();
|
||||
let monitor_size = if monitor {
|
||||
let size = window
|
||||
.current_monitor()
|
||||
.unwrap()
|
||||
.size()
|
||||
.to_logical::<f32>(pixels_per_point.into());
|
||||
Some(egui::vec2(size.width, size.height))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let size = window
|
||||
.inner_size()
|
||||
.to_logical::<f32>(pixels_per_point.into());
|
||||
|
||||
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
|
||||
|
||||
WindowInfo {
|
||||
position,
|
||||
fullscreen: window.fullscreen().is_some(),
|
||||
minimized: window_state.minimized,
|
||||
maximized: window_state.maximized,
|
||||
focused: window.has_focus(),
|
||||
size: egui::Vec2 {
|
||||
x: size.width,
|
||||
y: size.height,
|
||||
},
|
||||
monitor_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_builder<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
title: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
window_settings: Option<WindowSettings>,
|
||||
) -> winit::window::WindowBuilder {
|
||||
let epi::NativeOptions {
|
||||
maximized,
|
||||
decorated,
|
||||
fullscreen,
|
||||
#[cfg(target_os = "macos")]
|
||||
fullsize_content,
|
||||
drag_and_drop_support,
|
||||
icon_data,
|
||||
initial_window_pos,
|
||||
initial_window_size,
|
||||
min_window_size,
|
||||
max_window_size,
|
||||
resizable,
|
||||
transparent,
|
||||
centered,
|
||||
active,
|
||||
..
|
||||
} = native_options;
|
||||
|
||||
let window_icon = icon_data.clone().and_then(load_icon);
|
||||
|
||||
let mut window_builder = winit::window::WindowBuilder::new()
|
||||
.with_title(title)
|
||||
.with_decorations(*decorated)
|
||||
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
||||
.with_maximized(*maximized)
|
||||
.with_resizable(*resizable)
|
||||
.with_transparent(*transparent)
|
||||
.with_window_icon(window_icon)
|
||||
.with_active(*active)
|
||||
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||
// We must also keep the window hidden until AccessKit is initialized.
|
||||
.with_visible(false);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if *fullsize_content {
|
||||
window_builder = window_builder
|
||||
.with_title_hidden(true)
|
||||
.with_titlebar_transparent(true)
|
||||
.with_fullsize_content_view(true);
|
||||
}
|
||||
|
||||
if let Some(min_size) = *min_window_size {
|
||||
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
|
||||
}
|
||||
if let Some(max_size) = *max_window_size {
|
||||
window_builder = window_builder.with_max_inner_size(points_to_size(max_size));
|
||||
}
|
||||
|
||||
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
||||
|
||||
let inner_size_points = if let Some(mut window_settings) = window_settings {
|
||||
// Restore pos/size from previous session
|
||||
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
|
||||
#[cfg(windows)]
|
||||
window_settings.clamp_window_to_sane_position(event_loop);
|
||||
window_builder = window_settings.initialize_window(window_builder);
|
||||
window_settings.inner_size_points()
|
||||
} else {
|
||||
if let Some(pos) = *initial_window_pos {
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||
x: pos.x as f64,
|
||||
y: pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(initial_window_size) = *initial_window_size {
|
||||
let initial_window_size =
|
||||
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
||||
}
|
||||
|
||||
*initial_window_size
|
||||
};
|
||||
|
||||
if *centered {
|
||||
if let Some(monitor) = event_loop.available_monitors().next() {
|
||||
let monitor_size = monitor.size().to_logical::<f64>(monitor.scale_factor());
|
||||
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
|
||||
if monitor_size.width > 0.0 && monitor_size.height > 0.0 {
|
||||
let x = (monitor_size.width - inner_size.x as f64) / 2.0;
|
||||
let y = (monitor_size.height - inner_size.y as f64) / 2.0;
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition { x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
window_builder
|
||||
}
|
||||
|
||||
pub fn apply_native_options_to_window(
|
||||
window: &winit::window::Window,
|
||||
native_options: &crate::NativeOptions,
|
||||
) {
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if native_options.always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
}
|
||||
|
||||
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
|
||||
let mut max_size = egui::Vec2::ZERO;
|
||||
|
||||
for monitor in event_loop.available_monitors() {
|
||||
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
|
||||
let size = egui::vec2(size.width, size.height);
|
||||
max_size = max_size.max(size);
|
||||
}
|
||||
|
||||
if max_size == egui::Vec2::ZERO {
|
||||
egui::Vec2::splat(16000.0)
|
||||
} else {
|
||||
max_size
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
||||
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
use winit::platform::windows::WindowBuilderExtWindows as _;
|
||||
window_builder.with_drag_and_drop(enable)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
_enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
// drag and drop can only be disabled on windows
|
||||
window_builder
|
||||
}
|
||||
|
||||
pub fn handle_app_output(
|
||||
window: &winit::window::Window,
|
||||
current_pixels_per_point: f32,
|
||||
app_output: epi::backend::AppOutput,
|
||||
window_state: &mut WindowState,
|
||||
) {
|
||||
let epi::backend::AppOutput {
|
||||
close: _,
|
||||
window_size,
|
||||
window_title,
|
||||
decorated,
|
||||
fullscreen,
|
||||
drag_window,
|
||||
window_pos,
|
||||
visible: _, // handled in post_present
|
||||
always_on_top,
|
||||
screenshot_requested: _, // handled by the rendering backend,
|
||||
minimized,
|
||||
maximized,
|
||||
focus,
|
||||
} = app_output;
|
||||
|
||||
if let Some(decorated) = decorated {
|
||||
window.set_decorations(decorated);
|
||||
}
|
||||
|
||||
if let Some(window_size) = window_size {
|
||||
window.set_inner_size(
|
||||
winit::dpi::PhysicalSize {
|
||||
width: (current_pixels_per_point * window_size.x).round(),
|
||||
height: (current_pixels_per_point * window_size.y).round(),
|
||||
}
|
||||
.to_logical::<f32>(native_pixels_per_point(window) as f64),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(fullscreen) = fullscreen {
|
||||
window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
|
||||
if let Some(window_title) = window_title {
|
||||
window.set_title(&window_title);
|
||||
}
|
||||
|
||||
if let Some(window_pos) = window_pos {
|
||||
window.set_outer_position(winit::dpi::LogicalPosition {
|
||||
x: window_pos.x as f64,
|
||||
y: window_pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if drag_window {
|
||||
let _ = window.drag_window();
|
||||
}
|
||||
|
||||
if let Some(always_on_top) = always_on_top {
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(minimized) = minimized {
|
||||
window.set_minimized(minimized);
|
||||
window_state.minimized = minimized;
|
||||
}
|
||||
|
||||
if let Some(maximized) = maximized {
|
||||
window.set_maximized(maximized);
|
||||
window_state.maximized = maximized;
|
||||
}
|
||||
|
||||
if focus == Some(true) {
|
||||
window.focus_window();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// For loading/saving app state and/or egui memory to disk.
|
||||
pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) {
|
||||
return Some(Box::new(storage));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Everything needed to make a winit-based integration for [`epi`].
|
||||
pub struct EpiIntegration {
|
||||
pub frame: epi::Frame,
|
||||
last_auto_save: std::time::Instant,
|
||||
pub egui_ctx: egui::Context,
|
||||
pending_full_output: egui::FullOutput,
|
||||
egui_winit: egui_winit::State,
|
||||
/// When set, it is time to close the native window.
|
||||
close: bool,
|
||||
can_drag_window: bool,
|
||||
window_state: WindowState,
|
||||
follow_system_theme: bool,
|
||||
}
|
||||
|
||||
impl EpiIntegration {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
max_texture_side: usize,
|
||||
window: &winit::window::Window,
|
||||
system_theme: Option<Theme>,
|
||||
follow_system_theme: bool,
|
||||
storage: Option<Box<dyn epi::Storage>>,
|
||||
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
) -> Self {
|
||||
let egui_ctx = egui::Context::default();
|
||||
|
||||
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
||||
egui_ctx.memory_mut(|mem| *mem = memory);
|
||||
|
||||
let native_pixels_per_point = window.scale_factor() as f32;
|
||||
|
||||
let window_state = WindowState {
|
||||
minimized: window.is_minimized().unwrap_or(false),
|
||||
maximized: window.is_maximized(),
|
||||
};
|
||||
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo {
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point),
|
||||
window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state),
|
||||
},
|
||||
output: epi::backend::AppOutput {
|
||||
visible: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
storage,
|
||||
#[cfg(feature = "glow")]
|
||||
gl,
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state,
|
||||
screenshot: std::cell::Cell::new(None),
|
||||
};
|
||||
|
||||
let mut egui_winit = egui_winit::State::new(event_loop);
|
||||
egui_winit.set_max_texture_side(max_texture_side);
|
||||
egui_winit.set_pixels_per_point(native_pixels_per_point);
|
||||
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: std::time::Instant::now(),
|
||||
egui_ctx,
|
||||
egui_winit,
|
||||
pending_full_output: Default::default(),
|
||||
close: false,
|
||||
can_drag_window: false,
|
||||
window_state,
|
||||
follow_system_theme,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
|
||||
) {
|
||||
let egui_ctx = self.egui_ctx.clone();
|
||||
self.egui_winit
|
||||
.init_accesskit(window, event_loop_proxy, move || {
|
||||
// This function is called when an accessibility client
|
||||
// (e.g. screen reader) makes its first request. If we got here,
|
||||
// we know that an accessibility tree is actually wanted.
|
||||
egui_ctx.enable_accesskit();
|
||||
// Enqueue a repaint so we'll receive a full tree update soon.
|
||||
egui_ctx.request_repaint();
|
||||
egui_ctx.accesskit_placeholder_tree_update()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
crate::profile_function!();
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|mem| mem.set_everything_is_visible(true));
|
||||
let full_output = self.update(app, window);
|
||||
self.pending_full_output.append(full_output); // Handle it next frame
|
||||
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
|
||||
/// If `true`, it is time to close the native window.
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.close
|
||||
}
|
||||
|
||||
pub fn on_event(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
event: &winit::event::WindowEvent<'_>,
|
||||
) -> EventResponse {
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
WindowEvent::Destroyed => {
|
||||
tracing::debug!("Received WindowEvent::Destroyed");
|
||||
self.close = true;
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
button: MouseButton::Left,
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => self.can_drag_window = true,
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
self.frame.info.native_pixels_per_point = Some(*scale_factor as _);
|
||||
}
|
||||
WindowEvent::ThemeChanged(winit_theme) if self.follow_system_theme => {
|
||||
let theme = theme_from_winit_theme(*winit_theme);
|
||||
self.frame.info.system_theme = Some(theme);
|
||||
self.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||
self.egui_winit.on_accesskit_action_request(request);
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
window: &winit::window::Window,
|
||||
) -> egui::FullOutput {
|
||||
let frame_start = std::time::Instant::now();
|
||||
|
||||
self.frame.info.window_info =
|
||||
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
|
||||
let raw_input = self.egui_winit.take_egui_input(window);
|
||||
|
||||
// Run user code:
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
crate::profile_scope!("App::update");
|
||||
app.update(egui_ctx, &mut self.frame);
|
||||
});
|
||||
|
||||
self.pending_full_output.append(full_output);
|
||||
let full_output = std::mem::take(&mut self.pending_full_output);
|
||||
|
||||
{
|
||||
let mut app_output = self.frame.take_app_output();
|
||||
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
|
||||
self.can_drag_window = false;
|
||||
if app_output.close {
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
||||
self.frame.output.screenshot_requested = app_output.screenshot_requested;
|
||||
handle_app_output(
|
||||
window,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
app_output,
|
||||
&mut self.window_state,
|
||||
);
|
||||
}
|
||||
|
||||
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
|
||||
self.frame.info.cpu_usage = Some(frame_time);
|
||||
|
||||
full_output
|
||||
}
|
||||
|
||||
pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let inner_size = window.inner_size();
|
||||
let window_size_px = [inner_size.width, inner_size.height];
|
||||
|
||||
app.post_rendering(window_size_px, &self.frame);
|
||||
}
|
||||
|
||||
pub fn post_present(&mut self, window: &winit::window::Window) {
|
||||
if let Some(visible) = self.frame.output.visible.take() {
|
||||
window.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_platform_output(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
self.egui_winit
|
||||
.handle_platform_output(window, &self.egui_ctx, platform_output);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Persistence stuff:
|
||||
|
||||
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let now = std::time::Instant::now();
|
||||
if now - self.last_auto_save > app.auto_save_interval() {
|
||||
self.save(app, window);
|
||||
self.last_auto_save = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
crate::profile_function!();
|
||||
|
||||
if _app.persist_native_window() {
|
||||
crate::profile_scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_display(_window),
|
||||
);
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
crate::profile_scope!("egui_memory");
|
||||
self.egui_ctx
|
||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||
}
|
||||
{
|
||||
crate::profile_scope!("App::save");
|
||||
_app.save(storage);
|
||||
}
|
||||
|
||||
crate::profile_scope!("Storage::flush");
|
||||
storage.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_WINDOW_KEY: &str = "window";
|
||||
|
||||
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn theme_from_winit_theme(theme: winit::window::Theme) -> Theme {
|
||||
match theme {
|
||||
winit::window::Theme::Dark => Theme::Dark,
|
||||
winit::window::Theme::Light => Theme::Light,
|
||||
}
|
||||
}
|
||||
117
egui/crates/eframe/src/native/file_storage.rs
Normal file
117
egui/crates/eframe/src/native/file_storage.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk.
|
||||
/// Used to restore egui state, glium window position/size and app state.
|
||||
pub struct FileStorage {
|
||||
ron_filepath: PathBuf,
|
||||
kv: HashMap<String, String>,
|
||||
dirty: bool,
|
||||
last_save_join_handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for FileStorage {
|
||||
fn drop(&mut self) {
|
||||
if let Some(join_handle) = self.last_save_join_handle.take() {
|
||||
join_handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileStorage {
|
||||
/// Store the state in this .ron file.
|
||||
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
tracing::debug!("Loading app state from {:?}…", ron_filepath);
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
||||
dirty: false,
|
||||
last_save_join_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a good place to put the files that the OS likes.
|
||||
pub fn from_app_name(app_name: &str) -> Option<Self> {
|
||||
if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) {
|
||||
let data_dir = proj_dirs.data_dir().to_path_buf();
|
||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||
tracing::warn!(
|
||||
"Saving disabled: Failed to create app path at {:?}: {}",
|
||||
data_dir,
|
||||
err
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(Self::from_ron_filepath(data_dir.join("app.ron")))
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Saving disabled: Failed to find path to data_dir.");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Storage for FileStorage {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
self.kv.get(key).cloned()
|
||||
}
|
||||
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
if self.kv.get(key) != Some(&value) {
|
||||
self.kv.insert(key.to_owned(), value);
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if self.dirty {
|
||||
self.dirty = false;
|
||||
|
||||
let file_path = self.ron_filepath.clone();
|
||||
let kv = self.kv.clone();
|
||||
|
||||
if let Some(join_handle) = self.last_save_join_handle.take() {
|
||||
// wait for previous save to complete.
|
||||
join_handle.join().ok();
|
||||
}
|
||||
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
let file = std::fs::File::create(&file_path).unwrap();
|
||||
let config = Default::default();
|
||||
ron::ser::to_writer_pretty(file, &kv, config).unwrap();
|
||||
tracing::trace!("Persisted to {:?}", file_path);
|
||||
});
|
||||
|
||||
self.last_save_join_handle = Some(join_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
match std::fs::File::open(ron_path) {
|
||||
Ok(file) => {
|
||||
let reader = std::io::BufReader::new(file);
|
||||
match ron::de::from_reader(reader) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse RON: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
// File probably doesn't exist. That's fine.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
6
egui/crates/eframe/src/native/mod.rs
Normal file
6
egui/crates/eframe/src/native/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mod epi_integration;
|
||||
pub mod run;
|
||||
|
||||
/// File storage which can be used by native backends.
|
||||
#[cfg(feature = "persistence")]
|
||||
pub mod file_storage;
|
||||
1462
egui/crates/eframe/src/native/run.rs
Normal file
1462
egui/crates/eframe/src/native/run.rs
Normal file
File diff suppressed because it is too large
Load diff
598
egui/crates/eframe/src/web/backend.rs
Normal file
598
egui/crates/eframe/src/web/backend.rs
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
use egui::{
|
||||
mutex::{Mutex, MutexGuard},
|
||||
TexturesDelta,
|
||||
};
|
||||
|
||||
use crate::{epi, App};
|
||||
|
||||
use super::{web_painter::WebPainter, *};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Data gathered between frames.
|
||||
#[derive(Default)]
|
||||
pub struct WebInput {
|
||||
/// Required because we don't get a position on touched
|
||||
pub latest_touch_pos: Option<egui::Pos2>,
|
||||
|
||||
/// Required to maintain a stable touch position for multi-touch gestures.
|
||||
pub latest_touch_pos_id: Option<egui::TouchId>,
|
||||
|
||||
pub raw: egui::RawInput,
|
||||
}
|
||||
|
||||
impl WebInput {
|
||||
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
|
||||
egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
|
||||
pixels_per_point: Some(native_pixels_per_point()), // We ALWAYS use the native pixels-per-point
|
||||
time: Some(now_sec()),
|
||||
..self.raw.take()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_web_page_focus_change(&mut self, has_focus: bool) {
|
||||
self.raw.modifiers = egui::Modifiers::default();
|
||||
self.raw.has_focus = has_focus;
|
||||
self.latest_touch_pos = None;
|
||||
self.latest_touch_pos_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
|
||||
/// Stores when to do the next repaint.
|
||||
pub struct NeedRepaint(Mutex<f64>);
|
||||
|
||||
impl Default for NeedRepaint {
|
||||
fn default() -> Self {
|
||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedRepaint {
|
||||
/// Returns the time (in [`now_sec`] scale) when
|
||||
/// we should next repaint.
|
||||
pub fn when_to_repaint(&self) -> f64 {
|
||||
*self.0.lock()
|
||||
}
|
||||
|
||||
/// Unschedule repainting.
|
||||
pub fn clear(&self) {
|
||||
*self.0.lock() = f64::INFINITY;
|
||||
}
|
||||
|
||||
pub fn repaint_after(&self, num_seconds: f64) {
|
||||
let mut repaint_time = self.0.lock();
|
||||
*repaint_time = repaint_time.min(now_sec() + num_seconds);
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.0.lock() = f64::NEG_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsDestroyed(std::sync::atomic::AtomicBool);
|
||||
|
||||
impl Default for IsDestroyed {
|
||||
fn default() -> Self {
|
||||
Self(false.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IsDestroyed {
|
||||
pub fn fetch(&self) -> bool {
|
||||
self.0.load(SeqCst)
|
||||
}
|
||||
|
||||
pub fn set_true(&self) {
|
||||
self.0.store(true, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn user_agent() -> Option<String> {
|
||||
web_sys::window()?.navigator().user_agent().ok()
|
||||
}
|
||||
|
||||
fn web_location() -> epi::Location {
|
||||
let location = web_sys::window().unwrap().location();
|
||||
|
||||
let hash = percent_decode(&location.hash().unwrap_or_default());
|
||||
|
||||
let query = location
|
||||
.search()
|
||||
.unwrap_or_default()
|
||||
.strip_prefix('?')
|
||||
.map(percent_decode)
|
||||
.unwrap_or_default();
|
||||
|
||||
let query_map = parse_query_map(&query)
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
|
||||
.collect();
|
||||
|
||||
epi::Location {
|
||||
url: percent_decode(&location.href().unwrap_or_default()),
|
||||
protocol: percent_decode(&location.protocol().unwrap_or_default()),
|
||||
host: percent_decode(&location.host().unwrap_or_default()),
|
||||
hostname: percent_decode(&location.hostname().unwrap_or_default()),
|
||||
port: percent_decode(&location.port().unwrap_or_default()),
|
||||
hash,
|
||||
query,
|
||||
query_map,
|
||||
origin: percent_decode(&location.origin().unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
|
||||
query
|
||||
.split('&')
|
||||
.filter_map(|pair| {
|
||||
if pair.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(if let Some((key, value)) = pair.split_once('=') {
|
||||
(key, value)
|
||||
} else {
|
||||
(pair, "")
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_query() {
|
||||
assert_eq!(parse_query_map(""), BTreeMap::default());
|
||||
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
|
||||
assert_eq!(
|
||||
parse_query_map("foo=bar"),
|
||||
BTreeMap::from_iter([("foo", "bar")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo=bar&baz=42"),
|
||||
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo&baz=42"),
|
||||
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo&baz&&"),
|
||||
BTreeMap::from_iter([("foo", ""), ("baz", "")])
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct AppRunner {
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: ActiveWebPainter,
|
||||
pub(crate) input: WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
|
||||
last_save_time: f64,
|
||||
screen_reader: super::screen_reader::ScreenReader,
|
||||
pub(crate) text_cursor_pos: Option<egui::Pos2>,
|
||||
pub(crate) mutable_text_under_cursor: bool,
|
||||
textures_delta: TexturesDelta,
|
||||
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl Drop for AppRunner {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("AppRunner has fully dropped");
|
||||
}
|
||||
}
|
||||
|
||||
impl AppRunner {
|
||||
/// # Errors
|
||||
/// Failure to initialize WebGL renderer.
|
||||
pub async fn new(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<Self, String> {
|
||||
let painter = ActiveWebPainter::new(canvas_id, &web_options).await?;
|
||||
|
||||
let system_theme = if web_options.follow_system_theme {
|
||||
super::system_theme()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: epi::WebInfo {
|
||||
user_agent: user_agent().unwrap_or_default(),
|
||||
location: web_location(),
|
||||
},
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point()),
|
||||
};
|
||||
let storage = LocalStorage::default();
|
||||
|
||||
let egui_ctx = egui::Context::default();
|
||||
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
|
||||
&user_agent().unwrap_or_default(),
|
||||
));
|
||||
load_memory(&egui_ctx);
|
||||
|
||||
let theme = system_theme.unwrap_or(web_options.default_theme);
|
||||
egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
let app = app_creator(&epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
});
|
||||
|
||||
let frame = epi::Frame {
|
||||
info,
|
||||
output: Default::default(),
|
||||
storage: Some(Box::new(storage)),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
|
||||
{
|
||||
let needs_repaint = needs_repaint.clone();
|
||||
egui_ctx.set_request_repaint_callback(move || {
|
||||
needs_repaint.repaint_asap();
|
||||
});
|
||||
}
|
||||
|
||||
let mut runner = Self {
|
||||
frame,
|
||||
egui_ctx,
|
||||
painter,
|
||||
input: Default::default(),
|
||||
app,
|
||||
needs_repaint,
|
||||
is_destroyed: Default::default(),
|
||||
last_save_time: now_sec(),
|
||||
screen_reader: Default::default(),
|
||||
text_cursor_pos: None,
|
||||
mutable_text_under_cursor: false,
|
||||
textures_delta: Default::default(),
|
||||
events_to_unsubscribe: Default::default(),
|
||||
};
|
||||
|
||||
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
|
||||
|
||||
Ok(runner)
|
||||
}
|
||||
|
||||
pub fn egui_ctx(&self) -> &egui::Context {
|
||||
&self.egui_ctx
|
||||
}
|
||||
|
||||
/// Get mutable access to the concrete [`App`] we enclose.
|
||||
///
|
||||
/// This will panic if your app does not implement [`App::as_any_mut`].
|
||||
pub fn app_mut<ConcreteApp: 'static + App>(&mut self) -> &mut ConcreteApp {
|
||||
self.app
|
||||
.as_any_mut()
|
||||
.expect("Your app must implement `as_any_mut`, but it doesn't")
|
||||
.downcast_mut::<ConcreteApp>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn auto_save(&mut self) {
|
||||
let now = now_sec();
|
||||
let time_since_last_save = now - self.last_save_time;
|
||||
|
||||
if time_since_last_save > self.app.auto_save_interval().as_secs_f64() {
|
||||
if self.app.persist_egui_memory() {
|
||||
save_memory(&self.egui_ctx);
|
||||
}
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
self.app.save(storage);
|
||||
}
|
||||
self.last_save_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canvas_id(&self) -> &str {
|
||||
self.painter.canvas_id()
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
||||
if self.app.warm_up_enabled() {
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|m| m.set_everything_is_visible(true));
|
||||
self.logic()?;
|
||||
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self) -> Result<(), JsValue> {
|
||||
let is_destroyed_already = self.is_destroyed.fetch();
|
||||
|
||||
if is_destroyed_already {
|
||||
tracing::warn!("App was destroyed already");
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::debug!("Destroying");
|
||||
for x in self.events_to_unsubscribe.drain(..) {
|
||||
x.unsubscribe()?;
|
||||
}
|
||||
|
||||
self.painter.destroy();
|
||||
self.is_destroyed.set_true();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how long to wait until the next repaint.
|
||||
///
|
||||
/// Call [`Self::paint`] later to paint
|
||||
pub fn logic(&mut self) -> Result<(std::time::Duration, Vec<egui::ClippedPrimitive>), JsValue> {
|
||||
let frame_start = now_sec();
|
||||
|
||||
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
|
||||
let canvas_size = canvas_size_in_points(self.canvas_id());
|
||||
let raw_input = self.input.new_frame(canvas_size);
|
||||
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
self.app.update(egui_ctx, &mut self.frame);
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = full_output;
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
let clipped_primitives = self.egui_ctx.tessellate(shapes);
|
||||
|
||||
{
|
||||
let app_output = self.frame.take_app_output();
|
||||
let epi::backend::AppOutput {} = app_output;
|
||||
}
|
||||
|
||||
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
|
||||
Ok((repaint_after, clipped_primitives))
|
||||
}
|
||||
|
||||
/// Paint the results of the last call to [`Self::logic`].
|
||||
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
|
||||
let textures_delta = std::mem::take(&mut self.textures_delta);
|
||||
|
||||
self.painter.paint_and_update_textures(
|
||||
self.app.clear_color(&self.egui_ctx.style().visuals),
|
||||
clipped_primitives,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
&textures_delta,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
|
||||
let egui::PlatformOutput {
|
||||
cursor_icon,
|
||||
open_url,
|
||||
copied_text,
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
} = platform_output;
|
||||
|
||||
set_cursor_icon(cursor_icon);
|
||||
if let Some(open) = open_url {
|
||||
super::open_url(&open.url, open.new_tab);
|
||||
}
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
if !copied_text.is_empty() {
|
||||
set_clipboard_text(&copied_text);
|
||||
}
|
||||
|
||||
#[cfg(not(web_sys_unstable_apis))]
|
||||
let _ = copied_text;
|
||||
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
|
||||
if self.text_cursor_pos != text_cursor_pos {
|
||||
text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
|
||||
self.text_cursor_pos = text_cursor_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
|
||||
|
||||
pub struct TargetEvent {
|
||||
target: EventTarget,
|
||||
event_name: String,
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
pub struct IntervalHandle {
|
||||
pub handle: i32,
|
||||
pub closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
pub enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
#[allow(dead_code)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
impl EventToUnsubscribe {
|
||||
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||
match self {
|
||||
EventToUnsubscribe::TargetEvent(handle) => {
|
||||
handle.target.remove_event_listener_with_callback(
|
||||
handle.event_name.as_str(),
|
||||
handle.closure.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
EventToUnsubscribe::IntervalHandle(handle) => {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.clear_interval_with_handle(handle.handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppRunnerContainer {
|
||||
pub runner: AppRunnerRef,
|
||||
|
||||
/// Set to `true` if there is a panic.
|
||||
/// Used to ignore callbacks after a panic.
|
||||
pub panicked: Arc<AtomicBool>,
|
||||
pub events: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl AppRunnerContainer {
|
||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||
/// are dealt with in the same way
|
||||
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
||||
&mut self,
|
||||
target: &EventTarget,
|
||||
event_name: &'static str,
|
||||
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
||||
) -> Result<(), JsValue> {
|
||||
// Create a JS closure based on the FnMut provided
|
||||
let closure = Closure::wrap({
|
||||
// Clone atomics
|
||||
let runner_ref = self.runner.clone();
|
||||
let panicked = self.panicked.clone();
|
||||
|
||||
Box::new(move |event: web_sys::Event| {
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
// Cast the event to the expected event type
|
||||
let event = event.unchecked_into::<E>();
|
||||
|
||||
closure(event, runner_ref.lock());
|
||||
}
|
||||
}) as Box<dyn FnMut(web_sys::Event)>
|
||||
});
|
||||
|
||||
// Add the event listener to the target
|
||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
|
||||
let handle = TargetEvent {
|
||||
target: target.clone(),
|
||||
event_name: event_name.to_owned(),
|
||||
closure,
|
||||
};
|
||||
|
||||
self.events.push(EventToUnsubscribe::TargetEvent(handle));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and start running the given app.
|
||||
pub async fn start(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<AppRunnerRef, JsValue> {
|
||||
#[cfg(not(web_sys_unstable_apis))]
|
||||
tracing::warn!(
|
||||
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
|
||||
);
|
||||
let follow_system_theme = web_options.follow_system_theme;
|
||||
|
||||
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
|
||||
runner.warm_up()?;
|
||||
start_runner(runner, follow_system_theme)
|
||||
}
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and starts running the given [`AppRunner`].
|
||||
fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result<AppRunnerRef, JsValue> {
|
||||
let mut runner_container = AppRunnerContainer {
|
||||
runner: Arc::new(Mutex::new(app_runner)),
|
||||
panicked: Arc::new(AtomicBool::new(false)),
|
||||
events: Vec::with_capacity(20),
|
||||
};
|
||||
|
||||
super::events::install_canvas_events(&mut runner_container)?;
|
||||
super::events::install_document_events(&mut runner_container)?;
|
||||
super::events::install_window_events(&mut runner_container)?;
|
||||
text_agent::install_text_agent(&mut runner_container)?;
|
||||
|
||||
if follow_system_theme {
|
||||
super::events::install_color_scheme_change_event(&mut runner_container)?;
|
||||
}
|
||||
|
||||
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
||||
|
||||
// Disable all event handlers on panic
|
||||
let previous_hook = std::panic::take_hook();
|
||||
|
||||
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
tracing::info!("egui disabled all event handlers due to panic");
|
||||
runner_container.panicked.store(true, SeqCst);
|
||||
|
||||
// Propagate panic info to the previously registered panic hook
|
||||
previous_hook(panic_info);
|
||||
}));
|
||||
|
||||
Ok(runner_container.runner)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
struct LocalStorage {}
|
||||
|
||||
impl epi::Storage for LocalStorage {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
local_storage_get(key)
|
||||
}
|
||||
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
local_storage_set(key, &value);
|
||||
}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
597
egui/crates/eframe/src/web/events.rs
Normal file
597
egui/crates/eframe/src/web/events.rs
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use egui::Key;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct IsDestroyed(pub bool);
|
||||
|
||||
pub fn paint_and_schedule(
|
||||
runner_ref: &AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
|
||||
let mut runner_lock = runner_ref.lock();
|
||||
let is_destroyed = runner_lock.is_destroyed.fetch();
|
||||
|
||||
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
|
||||
runner_lock.needs_repaint.clear();
|
||||
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
|
||||
runner_lock.paint(&clipped_primitives)?;
|
||||
runner_lock
|
||||
.needs_repaint
|
||||
.repaint_after(repaint_after.as_secs_f64());
|
||||
runner_lock.auto_save();
|
||||
}
|
||||
|
||||
Ok(IsDestroyed(is_destroyed))
|
||||
}
|
||||
|
||||
fn request_animation_frame(
|
||||
runner_ref: AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
|
||||
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
closure.forget(); // We must forget it, or else the callback is canceled on drop
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Only paint and schedule if there has been no panic
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
let is_destroyed = paint_if_needed(runner_ref)?;
|
||||
if !is_destroyed.0 {
|
||||
request_animation_frame(runner_ref.clone(), panicked)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
{
|
||||
// Avoid sticky modifier keys on alt-tab:
|
||||
let clear_modifiers = ["blur", "focus"];
|
||||
|
||||
for event_name in clear_modifiers {
|
||||
let closure =
|
||||
move |_event: web_sys::MouseEvent,
|
||||
mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
let has_focus = event_name == "focus";
|
||||
runner_lock.input.on_web_page_focus_change(has_focus);
|
||||
runner_lock.egui_ctx().request_repaint();
|
||||
// tracing::debug!("{event_name:?}");
|
||||
};
|
||||
|
||||
runner_container.add_event_listener(&document, event_name, closure)?;
|
||||
}
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"keydown",
|
||||
|event: web_sys::KeyboardEvent, mut runner_lock| {
|
||||
if event.is_composing() || event.key_code() == 229 {
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiers = modifiers_from_event(&event);
|
||||
runner_lock.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
let egui_key = translate_key(&key);
|
||||
|
||||
if let Some(key) = egui_key {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
if !modifiers.ctrl
|
||||
&& !modifiers.command
|
||||
&& !should_ignore_key(&key)
|
||||
// When text agent is shown, it sends text event instead.
|
||||
&& text_agent::text_agent().hidden()
|
||||
{
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(key));
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
||||
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
let prevent_default = if egui_key == Some(Key::Tab) {
|
||||
// Always prevent moving cursor to url bar.
|
||||
// egui wants to use tab to move to the next text field.
|
||||
true
|
||||
} else if egui_key == Some(Key::P) {
|
||||
#[allow(clippy::needless_bool)]
|
||||
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
|
||||
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
|
||||
} else {
|
||||
false // let normal P:s through
|
||||
}
|
||||
} else if egui_wants_keyboard {
|
||||
matches!(
|
||||
event.key().as_str(),
|
||||
"Backspace" // so we don't go back to previous page when deleting text
|
||||
| "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58)
|
||||
)
|
||||
} else {
|
||||
// We never want to prevent:
|
||||
// * F5 / cmd-R (refresh)
|
||||
// * cmd-shift-C (debug tools)
|
||||
// * cmd/ctrl-c/v/x (or we stop copy/past/cut events)
|
||||
false
|
||||
};
|
||||
|
||||
// tracing::debug!(
|
||||
// "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}",
|
||||
// event.key().as_str(),
|
||||
// egui_wants_keyboard,
|
||||
// prevent_default
|
||||
// );
|
||||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"keyup",
|
||||
|event: web_sys::KeyboardEvent, mut runner_lock| {
|
||||
let modifiers = modifiers_from_event(&event);
|
||||
runner_lock.input.raw.modifiers = modifiers;
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: false,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"paste",
|
||||
|event: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
if let Some(data) = event.clipboard_data() {
|
||||
if let Ok(text) = data.get_data("text") {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
if !text.is_empty() {
|
||||
runner_lock.input.raw.events.push(egui::Event::Paste(text));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"cut",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Cut);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"copy",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Copy);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_window_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
|
||||
for event_name in &["load", "pagehide", "pageshow", "resize"] {
|
||||
runner_container.add_event_listener(
|
||||
&window,
|
||||
event_name,
|
||||
|_: web_sys::Event, runner_lock| {
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&window,
|
||||
"hashchange",
|
||||
|_: web_sys::Event, mut runner_lock| {
|
||||
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
||||
runner_lock.frame.info.web_info.location.hash = location_hash();
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_color_scheme_change_event(
|
||||
runner_container: &mut AppRunnerContainer,
|
||||
) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
|
||||
if let Some(media_query_list) = prefers_color_scheme_dark(&window)? {
|
||||
runner_container.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|event, mut runner_lock| {
|
||||
let theme = theme_from_dark_mode(event.matches());
|
||||
runner_lock.frame.info.system_theme = Some(theme);
|
||||
runner_lock.egui_ctx().set_visuals(theme.egui_visuals());
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||
|
||||
{
|
||||
let prevent_default_events = [
|
||||
// By default, right-clicks open a context menu.
|
||||
// We don't want to do that (right clicks is handled by egui):
|
||||
"contextmenu",
|
||||
// Allow users to use ctrl-p for e.g. a command palette:
|
||||
"afterprint",
|
||||
];
|
||||
|
||||
for event_name in prevent_default_events {
|
||||
let closure =
|
||||
move |event: web_sys::MouseEvent,
|
||||
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
// tracing::debug!("Preventing event {event_name:?}");
|
||||
};
|
||||
|
||||
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||
}
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mousedown",
|
||||
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mousemove",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mouseup",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
text_agent::update_text_agent(runner_lock);
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mouseleave",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchstart",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
|
||||
let pos =
|
||||
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner_lock.input.latest_touch_pos = Some(pos);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchmove",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
|
||||
let pos =
|
||||
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner_lock.input.latest_touch_pos = Some(pos);
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchend",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
if let Some(pos) = runner_lock.input.latest_touch_pos {
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
// First release mouse to click:
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
// Then remove hover effect:
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Finally, focus or blur text agent to toggle mobile keyboard:
|
||||
text_agent::update_text_agent(runner_lock);
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchcancel",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"wheel",
|
||||
|event: web_sys::WheelEvent, mut runner_lock| {
|
||||
let unit = match event.delta_mode() {
|
||||
web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point,
|
||||
web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line,
|
||||
web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page,
|
||||
_ => return,
|
||||
};
|
||||
// delta sign is flipped to match native (winit) convention.
|
||||
let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
|
||||
runner_lock.input.raw.events.push(egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
modifiers,
|
||||
});
|
||||
|
||||
let scroll_multiplier = match unit {
|
||||
egui::MouseWheelUnit::Page => canvas_size_in_points(runner_lock.canvas_id()).y,
|
||||
egui::MouseWheelUnit::Line => {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||
points_per_scroll_line
|
||||
}
|
||||
egui::MouseWheelUnit::Point => 1.0,
|
||||
};
|
||||
|
||||
let mut delta = scroll_multiplier * delta;
|
||||
|
||||
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
|
||||
// This if-statement is equivalent to how `Modifiers.command` is determined in
|
||||
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
|
||||
if event.ctrl_key() || event.meta_key() {
|
||||
let factor = (delta.y / 200.0).exp();
|
||||
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
|
||||
} else {
|
||||
if event.shift_key() {
|
||||
// Treat as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = egui::vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::Scroll(delta));
|
||||
}
|
||||
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"dragover",
|
||||
|event: web_sys::DragEvent, mut runner_lock| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
for i in 0..data_transfer.items().length() {
|
||||
if let Some(item) = data_transfer.items().get(i) {
|
||||
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
|
||||
mime: item.type_(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"dragleave",
|
||||
|event: web_sys::DragEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(&canvas, "drop", {
|
||||
let runner_ref = runner_container.runner.clone();
|
||||
|
||||
move |event: web_sys::DragEvent, mut runner_lock| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
// Unlock the runner so it can be locked after a future await point
|
||||
drop(runner_lock);
|
||||
|
||||
if let Some(files) = data_transfer.files() {
|
||||
for i in 0..files.length() {
|
||||
if let Some(file) = files.get(i) {
|
||||
let name = file.name();
|
||||
let last_modified = std::time::UNIX_EPOCH
|
||||
+ std::time::Duration::from_millis(file.last_modified() as u64);
|
||||
|
||||
tracing::debug!("Loading {:?} ({} bytes)…", name, file.size());
|
||||
|
||||
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
|
||||
|
||||
let runner_ref = runner_ref.clone();
|
||||
let future = async move {
|
||||
match future.await {
|
||||
Ok(array_buffer) => {
|
||||
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
tracing::debug!(
|
||||
"Loaded {:?} ({} bytes).",
|
||||
name,
|
||||
bytes.len()
|
||||
);
|
||||
|
||||
// Re-lock the mutex on the other side of the await point
|
||||
let mut runner_lock = runner_ref.lock();
|
||||
runner_lock.input.raw.dropped_files.push(
|
||||
egui::DroppedFile {
|
||||
name,
|
||||
last_modified: Some(last_modified),
|
||||
bytes: Some(bytes.into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to read file: {:?}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
}
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
217
egui/crates/eframe/src/web/input.rs
Normal file
217
egui/crates/eframe/src/web/input.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
use super::{canvas_element, canvas_origin, AppRunner};
|
||||
|
||||
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
egui::Pos2 {
|
||||
x: event.client_x() as f32 - rect.left() as f32,
|
||||
y: event.client_y() as f32 - rect.top() as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
|
||||
match event.button() {
|
||||
0 => Some(egui::PointerButton::Primary),
|
||||
1 => Some(egui::PointerButton::Middle),
|
||||
2 => Some(egui::PointerButton::Secondary),
|
||||
3 => Some(egui::PointerButton::Extra1),
|
||||
4 => Some(egui::PointerButton::Extra2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
|
||||
/// should not jump to a different position. Therefore, we do not calculate the average position
|
||||
/// of all touches, but we keep using the same touch as long as it is available.
|
||||
///
|
||||
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
|
||||
/// pointer position.
|
||||
pub fn pos_from_touch_event(
|
||||
canvas_id: &str,
|
||||
event: &web_sys::TouchEvent,
|
||||
touch_id_for_pos: &mut Option<egui::TouchId>,
|
||||
) -> egui::Pos2 {
|
||||
let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos {
|
||||
// search for the touch we previously used for the position
|
||||
// (unfortunately, `event.touches()` is not a rust collection):
|
||||
(0..event.touches().length())
|
||||
.into_iter()
|
||||
.map(|i| event.touches().get(i).unwrap())
|
||||
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Use the touch found above or pick the first, or return a default position if there is no
|
||||
// touch at all. (The latter is not expected as the current method is only called when there is
|
||||
// at least one touch.)
|
||||
touch_for_pos
|
||||
.or_else(|| event.touches().get(0))
|
||||
.map_or(Default::default(), |touch| {
|
||||
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
|
||||
pos_from_touch(canvas_origin(canvas_id), &touch)
|
||||
})
|
||||
}
|
||||
|
||||
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
|
||||
egui::Pos2 {
|
||||
x: touch.page_x() as f32 - canvas_origin.x,
|
||||
y: touch.page_y() as f32 - canvas_origin.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
|
||||
let canvas_origin = canvas_origin(runner.canvas_id());
|
||||
for touch_idx in 0..event.changed_touches().length() {
|
||||
if let Some(touch) = event.changed_touches().item(touch_idx) {
|
||||
runner.input.raw.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId::from(touch.identifier()),
|
||||
phase,
|
||||
pos: pos_from_touch(canvas_origin, &touch),
|
||||
force: touch.force(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web sends all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn should_ignore_key(key: &str) -> bool {
|
||||
let is_function_key = key.starts_with('F') && key.len() > 1;
|
||||
is_function_key
|
||||
|| matches!(
|
||||
key,
|
||||
"Alt"
|
||||
| "ArrowDown"
|
||||
| "ArrowLeft"
|
||||
| "ArrowRight"
|
||||
| "ArrowUp"
|
||||
| "Backspace"
|
||||
| "CapsLock"
|
||||
| "ContextMenu"
|
||||
| "Control"
|
||||
| "Delete"
|
||||
| "End"
|
||||
| "Enter"
|
||||
| "Esc"
|
||||
| "Escape"
|
||||
| "GroupNext" // https://github.com/emilk/egui/issues/510
|
||||
| "Help"
|
||||
| "Home"
|
||||
| "Insert"
|
||||
| "Meta"
|
||||
| "NumLock"
|
||||
| "PageDown"
|
||||
| "PageUp"
|
||||
| "Pause"
|
||||
| "ScrollLock"
|
||||
| "Shift"
|
||||
| "Tab"
|
||||
)
|
||||
}
|
||||
|
||||
/// Web sends all all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn translate_key(key: &str) -> Option<egui::Key> {
|
||||
use egui::Key;
|
||||
|
||||
match key {
|
||||
"ArrowDown" => Some(Key::ArrowDown),
|
||||
"ArrowLeft" => Some(Key::ArrowLeft),
|
||||
"ArrowRight" => Some(Key::ArrowRight),
|
||||
"ArrowUp" => Some(Key::ArrowUp),
|
||||
|
||||
"Esc" | "Escape" => Some(Key::Escape),
|
||||
"Tab" => Some(Key::Tab),
|
||||
"Backspace" => Some(Key::Backspace),
|
||||
"Enter" => Some(Key::Enter),
|
||||
"Space" | " " => Some(Key::Space),
|
||||
|
||||
"Help" | "Insert" => Some(Key::Insert),
|
||||
"Delete" => Some(Key::Delete),
|
||||
"Home" => Some(Key::Home),
|
||||
"End" => Some(Key::End),
|
||||
"PageUp" => Some(Key::PageUp),
|
||||
"PageDown" => Some(Key::PageDown),
|
||||
|
||||
"-" => Some(Key::Minus),
|
||||
"+" | "=" => Some(Key::PlusEquals),
|
||||
|
||||
"0" => Some(Key::Num0),
|
||||
"1" => Some(Key::Num1),
|
||||
"2" => Some(Key::Num2),
|
||||
"3" => Some(Key::Num3),
|
||||
"4" => Some(Key::Num4),
|
||||
"5" => Some(Key::Num5),
|
||||
"6" => Some(Key::Num6),
|
||||
"7" => Some(Key::Num7),
|
||||
"8" => Some(Key::Num8),
|
||||
"9" => Some(Key::Num9),
|
||||
|
||||
"a" | "A" => Some(Key::A),
|
||||
"b" | "B" => Some(Key::B),
|
||||
"c" | "C" => Some(Key::C),
|
||||
"d" | "D" => Some(Key::D),
|
||||
"e" | "E" => Some(Key::E),
|
||||
"f" | "F" => Some(Key::F),
|
||||
"g" | "G" => Some(Key::G),
|
||||
"h" | "H" => Some(Key::H),
|
||||
"i" | "I" => Some(Key::I),
|
||||
"j" | "J" => Some(Key::J),
|
||||
"k" | "K" => Some(Key::K),
|
||||
"l" | "L" => Some(Key::L),
|
||||
"m" | "M" => Some(Key::M),
|
||||
"n" | "N" => Some(Key::N),
|
||||
"o" | "O" => Some(Key::O),
|
||||
"p" | "P" => Some(Key::P),
|
||||
"q" | "Q" => Some(Key::Q),
|
||||
"r" | "R" => Some(Key::R),
|
||||
"s" | "S" => Some(Key::S),
|
||||
"t" | "T" => Some(Key::T),
|
||||
"u" | "U" => Some(Key::U),
|
||||
"v" | "V" => Some(Key::V),
|
||||
"w" | "W" => Some(Key::W),
|
||||
"x" | "X" => Some(Key::X),
|
||||
"y" | "Y" => Some(Key::Y),
|
||||
"z" | "Z" => Some(Key::Z),
|
||||
|
||||
"F1" => Some(Key::F1),
|
||||
"F2" => Some(Key::F2),
|
||||
"F3" => Some(Key::F3),
|
||||
"F4" => Some(Key::F4),
|
||||
"F5" => Some(Key::F5),
|
||||
"F6" => Some(Key::F6),
|
||||
"F7" => Some(Key::F7),
|
||||
"F8" => Some(Key::F8),
|
||||
"F9" => Some(Key::F9),
|
||||
"F10" => Some(Key::F10),
|
||||
"F11" => Some(Key::F11),
|
||||
"F12" => Some(Key::F12),
|
||||
"F13" => Some(Key::F13),
|
||||
"F14" => Some(Key::F14),
|
||||
"F15" => Some(Key::F15),
|
||||
"F16" => Some(Key::F16),
|
||||
"F17" => Some(Key::F17),
|
||||
"F18" => Some(Key::F18),
|
||||
"F19" => Some(Key::F19),
|
||||
"F20" => Some(Key::F20),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
|
||||
egui::Modifiers {
|
||||
alt: event.alt_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
shift: event.shift_key(),
|
||||
|
||||
// Ideally we should know if we are running or mac or not,
|
||||
// but this works good enough for now.
|
||||
mac_cmd: event.meta_key(),
|
||||
|
||||
// Ideally we should know if we are running or mac or not,
|
||||
// but this works good enough for now.
|
||||
command: event.ctrl_key() || event.meta_key(),
|
||||
}
|
||||
}
|
||||
271
egui/crates/eframe/src/web/mod.rs
Normal file
271
egui/crates/eframe/src/web/mod.rs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
//! [`egui`] bindings for web apps (compiling to WASM).
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
|
||||
|
||||
pub mod backend;
|
||||
mod events;
|
||||
mod input;
|
||||
pub mod screen_reader;
|
||||
pub mod storage;
|
||||
mod text_agent;
|
||||
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||
|
||||
mod web_painter;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
mod web_painter_glow;
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod web_painter_wgpu;
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
pub use events::*;
|
||||
pub use storage::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use egui::Vec2;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{EventTarget, MediaQueryList};
|
||||
|
||||
use input::*;
|
||||
|
||||
use crate::Theme;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Current time in seconds (since undefined point in time).
|
||||
///
|
||||
/// Monotonically increasing.
|
||||
pub fn now_sec() -> f64 {
|
||||
web_sys::window()
|
||||
.expect("should have a Window")
|
||||
.performance()
|
||||
.expect("should have a Performance")
|
||||
.now()
|
||||
/ 1000.0
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
|
||||
let window = web_sys::window()?;
|
||||
Some(egui::vec2(
|
||||
window.inner_width().ok()?.as_f64()? as f32,
|
||||
window.inner_height().ok()?.as_f64()? as f32,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn native_pixels_per_point() -> f32 {
|
||||
let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32;
|
||||
if pixels_per_point > 0.0 && pixels_per_point.is_finite() {
|
||||
pixels_per_point
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_theme() -> Option<Theme> {
|
||||
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(theme_from_dark_mode(dark_mode))
|
||||
}
|
||||
|
||||
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
window.match_media("(prefers-color-scheme: dark)")
|
||||
}
|
||||
|
||||
fn theme_from_dark_mode(dark_mode: bool) -> Theme {
|
||||
if dark_mode {
|
||||
Theme::Dark
|
||||
} else {
|
||||
Theme::Light
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||
let document = web_sys::window()?.document()?;
|
||||
let canvas = document.get_element_by_id(canvas_id)?;
|
||||
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
||||
}
|
||||
|
||||
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||
canvas_element(canvas_id)
|
||||
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
|
||||
}
|
||||
|
||||
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
||||
let rect = canvas_element(canvas_id)
|
||||
.unwrap()
|
||||
.get_bounding_client_rect();
|
||||
egui::pos2(rect.left() as f32, rect.top() as f32)
|
||||
}
|
||||
|
||||
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
egui::vec2(
|
||||
canvas.width() as f32 / pixels_per_point,
|
||||
canvas.height() as f32 / pixels_per_point,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let parent = canvas.parent_element()?;
|
||||
|
||||
// Prefer the client width and height so that if the parent
|
||||
// element is resized that the egui canvas resizes appropriately.
|
||||
let width = parent.client_width();
|
||||
let height = parent.client_height();
|
||||
|
||||
let canvas_real_size = Vec2 {
|
||||
x: width as f32,
|
||||
y: height as f32,
|
||||
};
|
||||
|
||||
if width <= 0 || height <= 0 {
|
||||
tracing::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height);
|
||||
}
|
||||
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
|
||||
let max_size_pixels = pixels_per_point * max_size_points;
|
||||
|
||||
let canvas_size_pixels = pixels_per_point * canvas_real_size;
|
||||
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
|
||||
let canvas_size_points = canvas_size_pixels / pixels_per_point;
|
||||
|
||||
// Make sure that the height and width are always even numbers.
|
||||
// otherwise, the page renders blurry on some platforms.
|
||||
// See https://github.com/emilk/egui/issues/103
|
||||
fn round_to_even(v: f32) -> f32 {
|
||||
(v / 2.0).round() * 2.0
|
||||
}
|
||||
|
||||
canvas
|
||||
.style()
|
||||
.set_property(
|
||||
"width",
|
||||
&format!("{}px", round_to_even(canvas_size_points.x)),
|
||||
)
|
||||
.ok()?;
|
||||
canvas
|
||||
.style()
|
||||
.set_property(
|
||||
"height",
|
||||
&format!("{}px", round_to_even(canvas_size_points.y)),
|
||||
)
|
||||
.ok()?;
|
||||
canvas.set_width(round_to_even(canvas_size_pixels.x) as u32);
|
||||
canvas.set_height(round_to_even(canvas_size_pixels.y) as u32);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
|
||||
let document = web_sys::window()?.document()?;
|
||||
document
|
||||
.body()?
|
||||
.style()
|
||||
.set_property("cursor", cursor_web_name(cursor))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
pub fn set_clipboard_text(s: &str) {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(clipboard) = window.navigator().clipboard() {
|
||||
let promise = clipboard.write_text(s);
|
||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
||||
let future = async move {
|
||||
if let Err(err) = future.await {
|
||||
tracing::error!("Copy/cut action denied: {:?}", err);
|
||||
}
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str {
|
||||
match cursor {
|
||||
egui::CursorIcon::Alias => "alias",
|
||||
egui::CursorIcon::AllScroll => "all-scroll",
|
||||
egui::CursorIcon::Cell => "cell",
|
||||
egui::CursorIcon::ContextMenu => "context-menu",
|
||||
egui::CursorIcon::Copy => "copy",
|
||||
egui::CursorIcon::Crosshair => "crosshair",
|
||||
egui::CursorIcon::Default => "default",
|
||||
egui::CursorIcon::Grab => "grab",
|
||||
egui::CursorIcon::Grabbing => "grabbing",
|
||||
egui::CursorIcon::Help => "help",
|
||||
egui::CursorIcon::Move => "move",
|
||||
egui::CursorIcon::NoDrop => "no-drop",
|
||||
egui::CursorIcon::None => "none",
|
||||
egui::CursorIcon::NotAllowed => "not-allowed",
|
||||
egui::CursorIcon::PointingHand => "pointer",
|
||||
egui::CursorIcon::Progress => "progress",
|
||||
egui::CursorIcon::ResizeHorizontal => "ew-resize",
|
||||
egui::CursorIcon::ResizeNeSw => "nesw-resize",
|
||||
egui::CursorIcon::ResizeNwSe => "nwse-resize",
|
||||
egui::CursorIcon::ResizeVertical => "ns-resize",
|
||||
|
||||
egui::CursorIcon::ResizeEast => "e-resize",
|
||||
egui::CursorIcon::ResizeSouthEast => "se-resize",
|
||||
egui::CursorIcon::ResizeSouth => "s-resize",
|
||||
egui::CursorIcon::ResizeSouthWest => "sw-resize",
|
||||
egui::CursorIcon::ResizeWest => "w-resize",
|
||||
egui::CursorIcon::ResizeNorthWest => "nw-resize",
|
||||
egui::CursorIcon::ResizeNorth => "n-resize",
|
||||
egui::CursorIcon::ResizeNorthEast => "ne-resize",
|
||||
egui::CursorIcon::ResizeColumn => "col-resize",
|
||||
egui::CursorIcon::ResizeRow => "row-resize",
|
||||
|
||||
egui::CursorIcon::Text => "text",
|
||||
egui::CursorIcon::VerticalText => "vertical-text",
|
||||
egui::CursorIcon::Wait => "wait",
|
||||
egui::CursorIcon::ZoomIn => "zoom-in",
|
||||
egui::CursorIcon::ZoomOut => "zoom-out",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
|
||||
let name = if new_tab { "_blank" } else { "_self" };
|
||||
|
||||
web_sys::window()?
|
||||
.open_with_url_and_target(url, name)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
|
||||
///
|
||||
/// Percent decoded
|
||||
pub fn location_hash() -> String {
|
||||
percent_decode(
|
||||
&web_sys::window()
|
||||
.unwrap()
|
||||
.location()
|
||||
.hash()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn percent_decode(s: &str) -> String {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8_lossy()
|
||||
.to_string()
|
||||
}
|
||||
49
egui/crates/eframe/src/web/screen_reader.rs
Normal file
49
egui/crates/eframe/src/web/screen_reader.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
pub struct ScreenReader {
|
||||
#[cfg(feature = "tts")]
|
||||
tts: Option<tts::Tts>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::derivable_impls)] // False positive
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
let tts = match tts::Tts::default() {
|
||||
Ok(screen_reader) => {
|
||||
tracing::debug!("Initialized screen reader.");
|
||||
Some(screen_reader)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to load screen reader: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
Self { tts }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenReader {
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn speak(&mut self, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
pub fn speak(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(tts) = &mut self.tts {
|
||||
tracing::debug!("Speaking: {:?}", text);
|
||||
let interrupt = true;
|
||||
if let Err(err) = tts.speak(text, interrupt) {
|
||||
tracing::warn!("Failed to read: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
egui/crates/eframe/src/web/storage.rs
Normal file
43
egui/crates/eframe/src/web/storage.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
fn local_storage() -> Option<web_sys::Storage> {
|
||||
web_sys::window()?.local_storage().ok()?
|
||||
}
|
||||
|
||||
pub fn local_storage_get(key: &str) -> Option<String> {
|
||||
local_storage().map(|storage| storage.get_item(key).ok())??
|
||||
}
|
||||
|
||||
pub fn local_storage_set(key: &str, value: &str) {
|
||||
local_storage().map(|storage| storage.set_item(key, value));
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn load_memory(ctx: &egui::Context) {
|
||||
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
||||
match ron::from_str(&memory_string) {
|
||||
Ok(memory) => {
|
||||
ctx.memory_mut(|m| *m = memory);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to parse memory RON: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
pub fn load_memory(_: &egui::Context) {}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn save_memory(ctx: &egui::Context) {
|
||||
match ctx.memory(|mem| ron::to_string(mem)) {
|
||||
Ok(ron) => {
|
||||
local_storage_set("egui_memory_ron", &ron);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to serialize memory as RON: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
pub fn save_memory(_: &egui::Context) {}
|
||||
225
egui/crates/eframe/src/web/text_agent.rs
Normal file
225
egui/crates/eframe/src/web/text_agent.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
//! The text agent is an `<input>` element used to trigger
|
||||
//! mobile keyboard and IME input.
|
||||
|
||||
use super::{canvas_element, AppRunner, AppRunnerContainer};
|
||||
use egui::mutex::MutexGuard;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
static AGENT_ID: &str = "egui_text_agent";
|
||||
|
||||
pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.get_element_by_id(AGENT_ID)
|
||||
.unwrap()
|
||||
.dyn_into()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Text event handler,
|
||||
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let body = document.body().expect("document should have a body");
|
||||
let input = document
|
||||
.create_element("input")?
|
||||
.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||
let input = std::rc::Rc::new(input);
|
||||
input.set_id(AGENT_ID);
|
||||
let is_composing = Rc::new(Cell::new(false));
|
||||
{
|
||||
let style = input.style();
|
||||
// Transparent
|
||||
style.set_property("opacity", "0").unwrap();
|
||||
// Hide under canvas
|
||||
style.set_property("z-index", "-1").unwrap();
|
||||
}
|
||||
// Set size as small as possible, in case user may click on it.
|
||||
input.set_size(1);
|
||||
input.set_autofocus(true);
|
||||
input.set_hidden(true);
|
||||
|
||||
// When IME is off
|
||||
runner_container.add_event_listener(&input, "input", {
|
||||
let input_clone = input.clone();
|
||||
let is_composing = is_composing.clone();
|
||||
|
||||
move |_event: web_sys::InputEvent, mut runner_lock| {
|
||||
let text = input_clone.value();
|
||||
if !text.is_empty() && !is_composing.get() {
|
||||
input_clone.set_value("");
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(text));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
{
|
||||
// When IME is on, handle composition event
|
||||
runner_container.add_event_listener(&input, "compositionstart", {
|
||||
let input_clone = input.clone();
|
||||
let is_composing = is_composing.clone();
|
||||
|
||||
move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
is_composing.set(true);
|
||||
input_clone.set_value("");
|
||||
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::CompositionStart);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&input,
|
||||
"compositionupdate",
|
||||
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(&input, "compositionend", {
|
||||
let input_clone = input.clone();
|
||||
|
||||
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
is_composing.set(false);
|
||||
input_clone.set_value("");
|
||||
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
// When input lost focus, focus on it again.
|
||||
// It is useful when user click somewhere outside canvas.
|
||||
runner_container.add_event_listener(
|
||||
&input,
|
||||
"focusout",
|
||||
move |_event: web_sys::MouseEvent, _| {
|
||||
// Delay 10 ms, and focus again.
|
||||
let func = js_sys::Function::new_no_args(&format!(
|
||||
"document.getElementById('{}').focus()",
|
||||
AGENT_ID
|
||||
));
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
|
||||
.unwrap();
|
||||
},
|
||||
)?;
|
||||
|
||||
body.append_child(&input)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Focus or blur text agent to toggle mobile keyboard.
|
||||
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
|
||||
use web_sys::HtmlInputElement;
|
||||
let window = web_sys::window()?;
|
||||
let document = window.document()?;
|
||||
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
|
||||
let canvas_style = canvas_element(runner.canvas_id())?.style();
|
||||
|
||||
if runner.mutable_text_under_cursor {
|
||||
let is_already_editing = input.hidden();
|
||||
if is_already_editing {
|
||||
input.set_hidden(false);
|
||||
input.focus().ok()?;
|
||||
|
||||
// Move up canvas so that text edit is shown at ~30% of screen height.
|
||||
// Only on touch screens, when keyboard popups.
|
||||
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
|
||||
let window_height = window.inner_height().ok()?.as_f64()? as f32;
|
||||
let current_rel = latest_touch_pos.y / window_height;
|
||||
|
||||
// estimated amount of screen covered by keyboard
|
||||
let keyboard_fraction = 0.5;
|
||||
|
||||
if current_rel > keyboard_fraction {
|
||||
// below the keyboard
|
||||
|
||||
let target_rel = 0.3;
|
||||
|
||||
// Note: `delta` is negative, since we are moving the canvas UP
|
||||
let delta = target_rel - current_rel;
|
||||
|
||||
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
|
||||
|
||||
let new_pos_percent = format!("{}%", (delta * 100.0).round());
|
||||
|
||||
canvas_style.set_property("position", "absolute").ok()?;
|
||||
canvas_style.set_property("top", &new_pos_percent).ok()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Drop runner lock
|
||||
drop(runner);
|
||||
|
||||
// Holding the runner lock while calling input.blur() causes a panic.
|
||||
// This is most probably caused by the browser running the event handler
|
||||
// for the triggered blur event synchronously, meaning that the mutex
|
||||
// lock does not get dropped by the time another event handler is called.
|
||||
//
|
||||
// Why this didn't exist before #1290 is a mystery to me, but it exists now
|
||||
// and this apparently is the fix for it
|
||||
//
|
||||
// ¯\_(ツ)_/¯ - @DusterTheFirst
|
||||
input.blur().ok()?;
|
||||
|
||||
input.set_hidden(true);
|
||||
canvas_style.set_property("position", "absolute").ok()?;
|
||||
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// If context is running under mobile device?
|
||||
fn is_mobile() -> Option<bool> {
|
||||
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
|
||||
|
||||
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
|
||||
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
|
||||
Some(is_mobile)
|
||||
}
|
||||
|
||||
// Move text agent to text cursor's position, on desktop/laptop,
|
||||
// candidate window moves following text element (agent),
|
||||
// so it appears that the IME candidate window moves with text cursor.
|
||||
// On mobile devices, there is no need to do that.
|
||||
pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
|
||||
let style = text_agent().style();
|
||||
// Note: movint agent on mobile devices will lead to unpredictable scroll.
|
||||
if is_mobile() == Some(false) {
|
||||
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let bounding_rect = text_agent().get_bounding_client_rect();
|
||||
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
|
||||
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
|
||||
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
|
||||
// Canvas is translated 50% horizontally in html.
|
||||
let x = (x - canvas.offset_width() as f32 / 2.0)
|
||||
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
|
||||
style.set_property("position", "absolute").ok()?;
|
||||
style.set_property("top", &format!("{}px", y)).ok()?;
|
||||
style.set_property("left", &format!("{}px", x)).ok()
|
||||
})
|
||||
} else {
|
||||
style.set_property("position", "absolute").ok()?;
|
||||
style.set_property("top", "0px").ok()?;
|
||||
style.set_property("left", "0px").ok()
|
||||
}
|
||||
}
|
||||
29
egui/crates/eframe/src/web/web_painter.rs
Normal file
29
egui/crates/eframe/src/web/web_painter.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// Renderer for a browser canvas.
|
||||
/// As of writing we're not allowing to decide on the painter at runtime,
|
||||
/// therefore this trait is merely there for specifying and documenting the interface.
|
||||
pub(crate) trait WebPainter {
|
||||
// Create a new web painter targeting a given canvas.
|
||||
// fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String>
|
||||
// where
|
||||
// Self: Sized;
|
||||
|
||||
/// Id of the canvas in use.
|
||||
fn canvas_id(&self) -> &str;
|
||||
|
||||
/// Maximum size of a texture in one direction.
|
||||
fn max_texture_side(&self) -> usize;
|
||||
|
||||
/// Update all internal textures and paint gui.
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue>;
|
||||
|
||||
/// Destroy all resources.
|
||||
fn destroy(&mut self);
|
||||
}
|
||||
184
egui/crates/eframe/src/web/web_painter_glow.rs
Normal file
184
egui/crates/eframe/src/web/web_painter_glow.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui_glow::glow;
|
||||
|
||||
use crate::{WebGlContextOption, WebOptions};
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterGlow {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
painter: egui_glow::Painter,
|
||||
}
|
||||
|
||||
impl WebPainterGlow {
|
||||
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let (gl, shader_prefix) =
|
||||
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
|
||||
let gl = std::sync::Arc::new(gl);
|
||||
|
||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
|
||||
.map_err(|error| format!("Error starting glow painter: {}", error))?;
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
painter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainter for WebPainterGlow {
|
||||
fn max_texture_side(&self) -> usize {
|
||||
self.painter.max_texture_side()
|
||||
}
|
||||
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
}
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
self.painter.set_texture(*id, image_delta);
|
||||
}
|
||||
|
||||
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
||||
self.painter
|
||||
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
|
||||
|
||||
for &id in &textures_delta.free {
|
||||
self.painter.free_texture(id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.painter.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns glow context and shader prefix.
|
||||
fn init_glow_context_from_canvas(
|
||||
canvas: &HtmlCanvasElement,
|
||||
options: WebGlContextOption,
|
||||
) -> Result<(glow::Context, &'static str), String> {
|
||||
let result = match options {
|
||||
// Force use WebGl1
|
||||
WebGlContextOption::WebGl1 => init_webgl1(canvas),
|
||||
// Force use WebGl2
|
||||
WebGlContextOption::WebGl2 => init_webgl2(canvas),
|
||||
// Trying WebGl2 first
|
||||
WebGlContextOption::BestFirst => init_webgl2(canvas).or_else(|| init_webgl1(canvas)),
|
||||
// Trying WebGl1 first (useful for testing).
|
||||
WebGlContextOption::CompatibilityFirst => {
|
||||
init_webgl1(canvas).or_else(|| init_webgl2(canvas))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(result) = result {
|
||||
Ok(result)
|
||||
} else {
|
||||
Err("WebGL isn't supported".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl1_ctx = canvas
|
||||
.get_context("webgl")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl1_ctx = gl1_ctx?;
|
||||
tracing::debug!("WebGL1 selected.");
|
||||
|
||||
let gl1_ctx = gl1_ctx
|
||||
.dyn_into::<web_sys::WebGlRenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) {
|
||||
tracing::debug!("Enabling webkitGTK brightening workaround.");
|
||||
"#define APPLY_BRIGHTENING_GAMMA"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let gl = glow::Context::from_webgl1_context(gl1_ctx);
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl2_ctx = canvas
|
||||
.get_context("webgl2")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl2_ctx = gl2_ctx?;
|
||||
tracing::debug!("WebGL2 selected.");
|
||||
|
||||
let gl2_ctx = gl2_ctx
|
||||
.dyn_into::<web_sys::WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
let gl = glow::Context::from_webgl2_context(gl2_ctx);
|
||||
let shader_prefix = "";
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// See https://github.com/emilk/egui/issues/794
|
||||
|
||||
// detect WebKitGTK
|
||||
|
||||
// WebKitGTK use WebKit default unmasked vendor and renderer
|
||||
// but safari use same vendor and renderer
|
||||
// so exclude "Mac OS X" user-agent.
|
||||
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
|
||||
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
|
||||
}
|
||||
|
||||
/// detecting Safari and `webkitGTK`.
|
||||
///
|
||||
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
|
||||
///
|
||||
/// If we detect safari or `webkitGTKs` returns true.
|
||||
///
|
||||
/// This function used to avoid displaying linear color with `sRGB` supported systems.
|
||||
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
|
||||
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
|
||||
// TODO(emilk): do something smart based on user agent?
|
||||
if gl
|
||||
.get_extension("WEBGL_debug_renderer_info")
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(renderer) =
|
||||
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
|
||||
{
|
||||
if let Some(renderer) = renderer.as_string() {
|
||||
if renderer.contains("Apple") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
282
egui/crates/eframe/src/web/web_painter_wgpu.rs
Normal file
282
egui/crates/eframe/src/web/web_painter_wgpu.rs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui::mutex::RwLock;
|
||||
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
||||
|
||||
use crate::WebOptions;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
surface: wgpu::Surface,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
limits: wgpu::Limits,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
pub fn generate_depth_texture_view(
|
||||
&self,
|
||||
render_state: &RenderState,
|
||||
width_in_pixels: u32,
|
||||
height_in_pixels: u32,
|
||||
) -> Option<wgpu::TextureView> {
|
||||
let device = &render_state.device;
|
||||
self.depth_format.map(|depth_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
tracing::debug!("Creating wgpu painter");
|
||||
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: options.wgpu_options.backends,
|
||||
dx12_shader_compiler: Default::default(),
|
||||
});
|
||||
let surface = instance
|
||||
.create_surface_from_canvas(&canvas)
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: options.wgpu_options.power_preference,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: None,
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&options.wgpu_options.device_descriptor,
|
||||
None, // Capture doesn't work in the browser environment.
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
|
||||
|
||||
let target_format =
|
||||
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
|
||||
|
||||
let depth_format = options.wgpu_options.depth_format;
|
||||
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
|
||||
let render_state = RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
};
|
||||
|
||||
let surface_configuration = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: target_format,
|
||||
width: 0,
|
||||
height: 0,
|
||||
present_mode: options.wgpu_options.present_mode,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![target_format],
|
||||
};
|
||||
|
||||
tracing::debug!("wgpu painter initialized.");
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
depth_format,
|
||||
depth_texture_view: None,
|
||||
limits: options.wgpu_options.device_descriptor.limits.clone(),
|
||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainter for WebPainterWgpu {
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
}
|
||||
|
||||
fn max_texture_side(&self) -> usize {
|
||||
self.limits.max_texture_dimension_2d as _
|
||||
}
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
let render_state = if let Some(render_state) = &self.render_state {
|
||||
render_state
|
||||
} else {
|
||||
return Err(JsValue::from_str(
|
||||
"Can't paint, wgpu renderer was already disposed",
|
||||
));
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("egui_webpainter_paint_and_update_textures"),
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
let screen_descriptor = ScreenDescriptor {
|
||||
size_in_pixels,
|
||||
pixels_per_point,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
renderer.update_texture(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
*id,
|
||||
image_delta,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.update_buffers(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
&mut encoder,
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
)
|
||||
};
|
||||
|
||||
// Resize surface if needed
|
||||
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
|
||||
let frame = if is_zero_sized_surface {
|
||||
None
|
||||
} else {
|
||||
if size_in_pixels[0] != self.surface_configuration.width
|
||||
|| size_in_pixels[1] != self.surface_configuration.height
|
||||
{
|
||||
self.surface_configuration.width = size_in_pixels[0];
|
||||
self.surface_configuration.height = size_in_pixels[1];
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
self.depth_texture_view = self.generate_depth_texture_view(
|
||||
render_state,
|
||||
size_in_pixels[0],
|
||||
size_in_pixels[1],
|
||||
);
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
#[allow(clippy::single_match_else)]
|
||||
Err(e) => match (*self.on_surface_error)(e) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
let frame_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: false,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}
|
||||
}),
|
||||
label: Some("egui_render"),
|
||||
});
|
||||
|
||||
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
|
||||
}
|
||||
|
||||
Some(frame)
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state.queue.submit(
|
||||
user_cmd_bufs
|
||||
.into_iter()
|
||||
.chain(std::iter::once(encoder.finish())),
|
||||
);
|
||||
|
||||
if let Some(frame) = frame {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.render_state = None;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue