Refactor: move things into eframe (#1542)

* Move all epi-related code from egui_glow into eframe

* Move epi stuff from egui-winit into eframe

* Remove mention of epi in egui

* Remove mention of epi in egui_glium

* Remove trait epi::NativeTexture

* Remove confusing mentions of epi

* Refactor egui_web: break up into smaller files

* Clean up feature flags further, and update changelogs

* Clean up check.sh

* Small cleanup of egui_web/Cargo.toml

* Fix dependencies for pure_glow example

* Fix clippy false positive
This commit is contained in:
Emil Ernerfeldt 2022-04-29 08:17:49 +02:00 committed by GitHub
parent 5ea51c3f0b
commit ed002acc68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 797 additions and 803 deletions

9
Cargo.lock generated
View file

@ -1075,11 +1075,16 @@ checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
name = "eframe"
version = "0.17.0"
dependencies = [
"dark-light",
"egui",
"egui-winit",
"egui_glow",
"egui_web",
"epi",
"glow",
"glutin",
"puffin",
"winit",
]
[[package]]
@ -1099,10 +1104,7 @@ name = "egui-winit"
version = "0.17.0"
dependencies = [
"arboard",
"dark-light",
"egui",
"epi",
"glow",
"instant",
"puffin",
"serde",
@ -1178,7 +1180,6 @@ dependencies = [
"bytemuck",
"egui",
"egui-winit",
"epi",
"glow",
"glutin",
"memoffset",

View file

@ -19,9 +19,9 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
* Changed `App::update` to take `&mut Frame` instead of `&Frame`.
* `Frame` is no longer `Clone` or `Sync`.
* Add `glow` (OpenGL) context to `Frame` ([#1425](https://github.com/emilk/egui/pull/1425)).
* `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)).
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* `dark-light` (dark mode detection) is now an opt-in feature ([#1437](https://github.com/emilk/egui/pull/1437)).
* 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).

View file

@ -23,7 +23,7 @@ all-features = true
default = ["default_fonts"]
# detect dark mode system preference
dark-light = ["egui-winit/dark-light"]
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.
@ -31,8 +31,7 @@ default_fonts = ["egui/default_fonts"]
# Enable saving app state to disk.
persistence = [
"egui-winit/persistence",
"egui_glow/persistence",
"egui-winit/serde",
"egui/persistence",
"epi/persistence",
]
@ -40,12 +39,11 @@ persistence = [
# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin
# 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 = ["egui_glow/puffin"]
puffin = ["dep:puffin", "egui_glow/puffin"]
# enable screen reader support (requires `ctx.options().screen_reader = true;`)
screen_reader = [
"egui-winit/screen_reader",
"egui_glow/screen_reader",
"egui_web/screen_reader",
]
@ -58,11 +56,14 @@ epi = { version = "0.17.0", path = "../epi" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui_glow = { version = "0.17.0", path = "../egui_glow", default-features = false, features = [
"clipboard",
"epi",
"links",
"winit",
] }
egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false }
dark-light = { version = "0.2.1", optional = true }
egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] }
glow = "0.11"
glutin = { version = "0.28.0" }
puffin = { version = "0.13", optional = true }
winit = "0.26.1"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]

View file

@ -97,6 +97,9 @@ pub fn start_web(canvas_id: &str, app_creator: AppCreator) -> Result<(), wasm_bi
// ----------------------------------------------------------------------------
// When compiling natively
#[cfg(not(target_arch = "wasm32"))]
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
@ -135,5 +138,29 @@ pub fn start_web(canvas_id: &str, app_creator: AppCreator) -> Result<(), wasm_bi
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)]
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) -> ! {
egui_glow::run(app_name, &native_options, app_creator)
native::run(app_name, &native_options, app_creator)
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_scope;

View file

@ -1,3 +1,5 @@
use egui_winit::{native_pixels_per_point, WindowSettings};
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize {
width: points.x as f64,
@ -7,7 +9,7 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
pub fn window_builder(
native_options: &epi::NativeOptions,
window_settings: &Option<crate::WindowSettings>,
window_settings: &Option<WindowSettings>,
) -> winit::window::WindowBuilder {
let epi::NativeOptions {
always_on_top,
@ -109,7 +111,7 @@ pub fn handle_app_output(
width: (current_pixels_per_point * window_size.x).round(),
height: (current_pixels_per_point * window_size.y).round(),
}
.to_logical::<f32>(crate::native_pixels_per_point(window) as f64),
.to_logical::<f32>(native_pixels_per_point(window) as f64),
);
}
@ -145,10 +147,10 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
/// Everything needed to make a winit-based integration for [`epi`].
pub struct EpiIntegration {
pub frame: epi::Frame,
last_auto_save: instant::Instant,
last_auto_save: std::time::Instant,
pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput,
egui_winit: crate::State,
egui_winit: egui_winit::State,
/// When set, it is time to quit
quit: bool,
can_drag_window: bool,
@ -174,7 +176,7 @@ impl EpiIntegration {
web_info: None,
prefer_dark_mode,
cpu_usage: None,
native_pixels_per_point: Some(crate::native_pixels_per_point(window)),
native_pixels_per_point: Some(native_pixels_per_point(window)),
},
output: Default::default(),
storage,
@ -189,9 +191,9 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: instant::Instant::now(),
last_auto_save: std::time::Instant::now(),
egui_ctx,
egui_winit: crate::State::new(max_texture_side, window),
egui_winit: egui_winit::State::new(max_texture_side, window),
pending_full_output: Default::default(),
quit: false,
can_drag_window: false,
@ -235,7 +237,7 @@ impl EpiIntegration {
app: &mut dyn epi::App,
window: &winit::window::Window,
) -> egui::FullOutput {
let frame_start = instant::Instant::now();
let frame_start = std::time::Instant::now();
let raw_input = self.egui_winit.take_egui_input(window);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
@ -252,10 +254,10 @@ impl EpiIntegration {
if app_output.quit {
self.quit = app.on_exit_event();
}
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
}
let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32;
let frame_time = (std::time::Instant::now() - frame_start).as_secs_f64() as f32;
self.frame.info.cpu_usage = Some(frame_time);
full_output
@ -274,7 +276,7 @@ impl EpiIntegration {
// Persistance stuff:
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
let now = instant::Instant::now();
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;
@ -291,7 +293,7 @@ impl EpiIntegration {
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&crate::WindowSettings::from_display(_window),
&WindowSettings::from_display(_window),
);
}
if _app.persist_egui_memory() {
@ -314,7 +316,7 @@ 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<crate::WindowSettings> {
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)

4
eframe/src/native/mod.rs Normal file
View file

@ -0,0 +1,4 @@
mod epi_integration;
mod run;
pub use run::run;

View file

@ -1,3 +1,4 @@
use super::epi_integration;
use egui_winit::winit;
struct RequestRepaintEvent;
@ -37,18 +38,18 @@ pub use epi::NativeOptions;
/// Run an egui app
#[allow(unsafe_code)]
pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi::AppCreator) -> ! {
let storage = egui_winit::epi::create_storage(app_name);
let window_settings = egui_winit::epi::load_window_settings(storage.as_deref());
let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref());
let window_builder =
egui_winit::epi::window_builder(native_options, &window_settings).with_title(app_name);
epi_integration::window_builder(native_options, &window_settings).with_title(app_name);
let event_loop = winit::event_loop::EventLoop::with_user_event();
let (gl_window, gl) = create_display(native_options, window_builder, &event_loop);
let gl = std::rc::Rc::new(gl);
let mut painter = crate::Painter::new(gl.clone(), None, "")
let mut painter = egui_glow::Painter::new(gl.clone(), None, "")
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
let mut integration = egui_winit::epi::EpiIntegration::new(
let mut integration = epi_integration::EpiIntegration::new(
"egui_glow",
gl.clone(),
painter.max_texture_side(),
@ -94,7 +95,7 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi
crate::profile_scope!("frame");
let screen_size_in_pixels: [u32; 2] = gl_window.window().inner_size().into();
crate::painter::clear(&gl, screen_size_in_pixels, app.clear_color());
egui_glow::painter::clear(&gl, screen_size_in_pixels, app.clear_color());
let egui::FullOutput {
platform_output,

View file

@ -4,10 +4,11 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
* Reexport `egui` crate
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Removed the features `dark-light` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)).
## 0.17.0 - 2022-02-22

View file

@ -18,7 +18,7 @@ all-features = true
[features]
default = ["clipboard", "dark-light", "links"]
default = ["clipboard", "links"]
# implement bytemuck on most types.
bytemuck = ["egui/bytemuck"]
@ -27,22 +27,16 @@ bytemuck = ["egui/bytemuck"]
# if disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["arboard"]
# detect dark mode system preference
dark-light = ["dep:dark-light"]
# Only for `egui_glow` - the official eframe/epi backend.
epi_backend = ["epi", "glow"]
# enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]
# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin
# enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin
puffin = ["dep:puffin"]
# experimental support for a screen reader
screen_reader = ["tts"]
persistence = ["egui/serde", "serde", "epi?/persistence"]
# to serialize `WindowSettings`
serde = ["egui/serde", "dep:serde"]
@ -50,16 +44,11 @@ serde = ["egui/serde", "dep:serde"]
egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"tracing",
] }
instant = { version = "0.1", features = ["wasm-bindgen"] }
instant = { version = "0.1", features = ["wasm-bindgen"] } # We use instant so we can (maybe) compile for web
tracing = "0.1"
winit = "0.26.1"
# Optional:
epi = { version = "0.17.0", path = "../epi", optional = true }
arboard = { version = "2.1", optional = true, default-features = false }
dark-light = { version = "0.2.1", optional = true }
glow = { version = "0.11", optional = true }
puffin = { version = "0.13", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.6", optional = true }

View file

@ -12,9 +12,6 @@ pub mod clipboard;
pub mod screen_reader;
mod window_settings;
#[cfg(feature = "epi")]
pub mod epi;
pub use window_settings::WindowSettings;
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {

View file

@ -60,7 +60,7 @@ pub struct RawInput {
/// Dragged files dropped into egui.
///
/// Note: when using `eframe` on Windows you need to enable
/// drag-and-drop support using `epi::NativeOptions`.
/// drag-and-drop support using `eframe::NativeOptions`.
pub dropped_files: Vec<DroppedFile>,
}

View file

@ -24,23 +24,16 @@ all-features = true
[features]
default = ["clipboard", "default_fonts", "links", "persistence"]
default = ["clipboard", "links"]
# enable cut/copy/paste to OS clipboard.
# if disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["egui-winit/clipboard"]
# 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"]
# enable opening links in a browser when an egui hyperlink is clicked.
links = ["egui-winit/links"]
# enable persisting native window options and egui memory
persistence = ["egui-winit/persistence", "egui/persistence"]
# experimental support for a screen reader
# experimental support for a screen reader.
screen_reader = ["egui-winit/screen_reader"]
@ -54,5 +47,6 @@ ahash = "0.7"
bytemuck = "1.7"
glium = "0.31"
[dev-dependencies]
image = { version = "0.24", default-features = false, features = ["png"] }

View file

@ -1,5 +1,3 @@
//! Example how to use [`epi::NativeTexture`] with glium.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use glium::glutin;

View file

@ -1,4 +1,4 @@
//! Example how to use pure `egui_glium` without [`epi`].
//! Example how to use `egui_glium`.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

View file

@ -5,11 +5,11 @@ All notable changes to the `egui_glow` integration will be noted in this file.
## Unreleased
* Improved logging on rendering failures.
* Add 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)).
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* `clipboard`, `links`, `persistence`, `winit` are now all opt-in features ([#1467](https://github.com/emilk/egui/pull/1467)).
* `clipboard`, `links`, `winit` are now all opt-in features ([#1467](https://github.com/emilk/egui/pull/1467)).
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
* Removed the features `dark-light`, `default_fonts` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)).
## 0.17.0 - 2022-02-22

View file

@ -24,45 +24,31 @@ all-features = true
[features]
default = ["default_fonts"]
default = []
# enable cut/copy/paste to OS clipboard.
# for the winit integration:
# enable cut/copy/paste to os clipboard.
# if disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["egui-winit?/clipboard"]
# detect dark mode system preference
dark-light = ["egui-winit?/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"]
# for the winit integration:
# enable opening links in a browser when an egui hyperlink is clicked.
links = ["egui-winit?/links"]
# enable persisting native window options and egui memory
persistence = [
"egui-winit?/persistence",
"egui/persistence",
"epi?/persistence",
]
# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin
puffin = ["dep:puffin", "egui-winit?/puffin"]
# experimental support for a screen reader
# experimental support for a screen reader.
screen_reader = ["egui-winit?/screen_reader"]
# enable glutin/winit integration.
# if you want to use glow painter on web disable this feature.
winit = ["egui-winit", "glutin"]
# enable profiling with the puffin crate: https://github.com/embarkstudios/puffin
puffin = ["dep:puffin", "egui-winit?/puffin"]
# enable winit integration.
winit = ["egui-winit",]
[dependencies]
egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"bytemuck",
] }
epi = { version = "0.17.0", path = "../epi", optional = true }
bytemuck = "1.7"
glow = "0.11"
@ -71,13 +57,19 @@ tracing = "0.1"
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.17.0", path = "../egui-winit", optional = true, default-features = false, features = [
"epi_backend",
] }
glutin = { version = "0.28.0", optional = true }
egui-winit = { version = "0.17.0", path = "../egui-winit", optional = true, default-features = false }
puffin = { version = "0.13", optional = true }
# Web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["console"] }
wasm-bindgen = { version = "0.2" }
[dev-dependencies]
glutin = "0.28.0" # examples/pure_glow
[[example]]
name = "pure_glow"
required-features = ["winit"]

View file

@ -1,4 +1,4 @@
//! Example how to use pure `egui_glow` without [`epi`].
//! Example how to use pure `egui_glow`.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(unsafe_code)]

View file

@ -1,8 +1,7 @@
//! [`egui`] bindings for [`glow`](https://github.com/grovesNL/glow).
//!
//! The main type you want to use is [`EguiGlow`].
//! The main types you want to look are are [`Painter`] and [`EguiGlow`].
//!
//! This library is an [`epi`] backend.
//! If you are writing an app, you may want to look at [`eframe`](https://docs.rs/eframe) instead.
#![allow(clippy::float_cmp)]
@ -21,12 +20,6 @@ pub mod winit;
#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))]
pub use winit::*;
#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))]
mod epi_backend;
#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))]
pub use epi_backend::{run, NativeOptions};
/// Check for OpenGL error and report it using `tracing::error`.
///
/// ``` no_run

View file

@ -47,7 +47,6 @@ pub struct Painter {
textures: HashMap<egui::TextureId, glow::Texture>,
#[cfg(feature = "epi")]
next_native_tex_id: u64, // TODO: 128-bit texture space?
/// Stores outdated OpenGL textures that are yet to be deleted
@ -222,7 +221,6 @@ impl Painter {
vbo,
element_array_buffer,
textures: Default::default(),
#[cfg(feature = "epi")]
next_native_tex_id: 1 << 32,
textures_to_destroy: Vec::new(),
destroyed: false,
@ -603,6 +601,22 @@ impl Painter {
self.textures.get(&texture_id).copied()
}
#[allow(clippy::needless_pass_by_value)] // False positive
pub fn register_native_texture(&mut self, native: glow::Texture) -> egui::TextureId {
self.assert_not_destroyed();
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, native);
id
}
#[allow(clippy::needless_pass_by_value)] // False positive
pub fn replace_native_texture(&mut self, id: egui::TextureId, replacing: glow::Texture) {
if let Some(old_tex) = self.textures.insert(id, replacing) {
self.textures_to_destroy.push(old_tex);
}
}
unsafe fn destroy_gl(&self) {
self.gl.delete_program(self.program);
for tex in self.textures.values() {
@ -666,25 +680,6 @@ impl Drop for Painter {
}
}
#[cfg(feature = "epi")]
impl epi::NativeTexture for Painter {
type Texture = glow::Texture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
self.assert_not_destroyed();
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, native);
id
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let Some(old_tex) = self.textures.insert(id, replacing) {
self.textures_to_destroy.push(old_tex);
}
}
}
fn set_clip_rect(
gl: &glow::Context,
size_in_pixels: (u32, u32),

View file

@ -1,7 +1,7 @@
pub use egui_winit;
use egui_winit::winit;
/// Use [`egui`] from a [`glow`] app.
/// Use [`egui`] from a [`glow`] app based on [`winit`].
pub struct EguiGlow {
pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State,

View file

@ -60,9 +60,7 @@ ron = { version = "0.7", optional = true }
serde = { version = "1", optional = true }
tts = { version = "0.20", optional = true } # feature screen_reader
[dependencies.web-sys]
version = "0.3.52"
features = [
web-sys = { version = "0.3.52", features = [
"BinaryType",
"Blob",
"Clipboard",
@ -103,4 +101,4 @@ features = [
"WebGlRenderingContext",
"WheelEvent",
"Window",
]
] }

View file

@ -330,6 +330,57 @@ impl AppRunner {
}
}
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
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>,
}
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>(
&self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
// 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(_)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
// Bypass closure drop so that event handler can call the closure
closure.forget();
Ok(())
}
}
// ----------------------------------------------------------------------------
/// Install event listeners to register different input events
/// and start running the given app.
pub fn start(canvas_id: &str, app_creator: epi::AppCreator) -> Result<AppRunnerRef, JsValue> {
@ -346,12 +397,12 @@ fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
panicked: Arc::new(AtomicBool::new(false)),
};
install_canvas_events(&runner_container)?;
install_document_events(&runner_container)?;
crate::events::install_canvas_events(&runner_container)?;
crate::events::install_document_events(&runner_container)?;
text_agent::install_text_agent(&runner_container)?;
repaint_every_ms(&runner_container, 1000)?; // just in case. TODO: make it a parameter
crate::events::repaint_every_ms(&runner_container, 1000)?; // just in case. TODO: make it a parameter
paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
crate::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
// Disable all event handlers on panic
std::panic::set_hook(Box::new({
@ -375,3 +426,18 @@ fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
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) {}
}

544
egui_web/src/events.rs Normal file
View file

@ -0,0 +1,544 @@
use crate::*;
use std::sync::atomic::{AtomicBool, Ordering};
pub fn paint_and_schedule(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let mut runner_lock = runner_ref.lock();
if runner_lock.needs_repaint.fetch_and_clear() {
runner_lock.clear_color_buffer();
let (needs_repaint, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
if needs_repaint {
runner_lock.needs_repaint.set_true();
}
runner_lock.auto_save();
}
Ok(())
}
fn request_animation_frame(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
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) {
paint_if_needed(runner_ref)?;
request_animation_frame(runner_ref.clone(), panicked)?;
}
Ok(())
}
pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
runner_container.add_event_listener(
&document,
"keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| {
if event.is_composing() || event.key_code() == 229 {
// 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();
if let Some(key) = translate_key(&key) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
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.set_true();
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let prevent_default = if matches!(event.key().as_str(), "Tab") {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} 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();
}
},
)?;
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,
modifiers,
});
}
runner_lock.needs_repaint.set_true();
},
)?;
#[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.set_true();
}
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.set_true();
},
)?;
#[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.set_true();
},
)?;
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.set_true();
},
)?;
}
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
if let Some(web_info) = &mut runner_lock.frame.info.web_info {
web_info.location.hash = location_hash();
}
},
)?;
Ok(())
}
/// Repaint at least every `ms` milliseconds.
pub fn repaint_every_ms(
runner_container: &AppRunnerContainer,
milliseconds: i32,
) -> Result<(), JsValue> {
assert!(milliseconds >= 0);
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let closure = Closure::wrap(Box::new({
let runner = runner_container.runner.clone();
let panicked = runner_container.panicked.clone();
move || {
// Do not lock the runner if the code has panicked
if !panicked.load(Ordering::SeqCst) {
runner.lock().needs_repaint.set_true();
}
}
}) as Box<dyn FnMut()>);
window.set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
milliseconds,
)?;
closure.forget();
Ok(())
}
pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
runner_container.add_event_listener(
&canvas,
"mousedown",
|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: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
}
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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 scroll_multiplier = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PAGE => {
canvas_size_in_points(runner_lock.canvas_id()).y
}
web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL
};
let mut delta =
-scroll_multiplier * egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
// 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.set_true();
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.set_true();
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.set_true();
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.set_true();
// 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.set_true();
}
Err(err) => {
tracing::error!("Failed to read file: {:?}", err);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
event.stop_propagation();
event.prevent_default();
}
}
})?;
Ok(())
}

View file

@ -7,12 +7,16 @@
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
pub mod backend;
mod events;
mod glow_wrapping;
mod input;
pub mod screen_reader;
pub mod storage;
mod text_agent;
pub use backend::*;
pub use events::*;
pub use storage::*;
use egui::mutex::{Mutex, MutexGuard};
pub use wasm_bindgen;
@ -133,69 +137,6 @@ pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2
// ----------------------------------------------------------------------------
pub 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));
}
pub fn local_storage_remove(key: &str) {
local_storage().map(|storage| storage.remove_item(key));
}
#[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() = 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 ron::to_string(&*ctx.memory()) {
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) {}
#[derive(Default)]
pub 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) {}
}
// ----------------------------------------------------------------------------
pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
let document = web_sys::window()?.document()?;
document
@ -300,592 +241,6 @@ pub fn percent_decode(s: &str) -> String {
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
pub struct AppRunnerContainer {
runner: AppRunnerRef,
/// Set to `true` if there is a panic.
/// Used to ignore callbacks after a panic.
panicked: Arc<AtomicBool>,
}
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>(
&self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
// 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(_)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
// Bypass closure drop so that event handler can call the closure
closure.forget();
Ok(())
}
}
fn paint_and_schedule(runner_ref: &AppRunnerRef, panicked: Arc<AtomicBool>) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let mut runner_lock = runner_ref.lock();
if runner_lock.needs_repaint.fetch_and_clear() {
runner_lock.clear_color_buffer();
let (needs_repaint, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
if needs_repaint {
runner_lock.needs_repaint.set_true();
}
runner_lock.auto_save();
}
Ok(())
}
fn request_animation_frame(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
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) {
paint_if_needed(runner_ref)?;
request_animation_frame(runner_ref.clone(), panicked)?;
}
Ok(())
}
fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
runner_container.add_event_listener(
&document,
"keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| {
if event.is_composing() || event.key_code() == 229 {
// 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();
if let Some(key) = translate_key(&key) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
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.set_true();
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let prevent_default = if matches!(event.key().as_str(), "Tab") {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} 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();
}
},
)?;
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,
modifiers,
});
}
runner_lock.needs_repaint.set_true();
},
)?;
#[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.set_true();
}
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.set_true();
},
)?;
#[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.set_true();
},
)?;
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.set_true();
},
)?;
}
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
if let Some(web_info) = &mut runner_lock.frame.info.web_info {
web_info.location.hash = location_hash();
}
},
)?;
Ok(())
}
/// Repaint at least every `ms` milliseconds.
pub fn repaint_every_ms(
runner_container: &AppRunnerContainer,
milliseconds: i32,
) -> Result<(), JsValue> {
assert!(milliseconds >= 0);
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let closure = Closure::wrap(Box::new({
let runner = runner_container.runner.clone();
let panicked = runner_container.panicked.clone();
move || {
// Do not lock the runner if the code has panicked
if !panicked.load(Ordering::SeqCst) {
runner.lock().needs_repaint.set_true();
}
}
}) as Box<dyn FnMut()>);
window.set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
milliseconds,
)?;
closure.forget();
Ok(())
}
fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
runner_container.add_event_listener(
&canvas,
"mousedown",
|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: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
}
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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.set_true();
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 scroll_multiplier = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PAGE => {
canvas_size_in_points(runner_lock.canvas_id()).y
}
web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL
};
let mut delta =
-scroll_multiplier * egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
// 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.set_true();
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.set_true();
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.set_true();
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.set_true();
// 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.set_true();
}
Err(err) => {
tracing::error!("Failed to read file: {:?}", err);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
event.stop_propagation();
event.prevent_default();
}
}
})?;
Ok(())
}
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794

47
egui_web/src/storage.rs Normal file
View file

@ -0,0 +1,47 @@
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));
}
pub fn local_storage_remove(key: &str) {
local_storage().map(|storage| storage.remove_item(key));
}
#[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() = 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 ron::to_string(&*ctx.memory()) {
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) {}

View file

@ -425,18 +425,6 @@ pub struct IntegrationInfo {
pub native_pixels_per_point: Option<f32>,
}
/// Abstraction for platform dependent texture reference
pub trait NativeTexture {
/// The native texture type.
type Texture;
/// Bind native texture to an egui texture id.
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId;
/// Change what texture the given id refers to.
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture);
}
// ----------------------------------------------------------------------------
/// A place where you can store custom data in a way that persists when you restart the app.

View file

@ -21,18 +21,19 @@ cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit
cargo doc -p egui_web --target wasm32-unknown-unknown --lib --no-deps --all-features
cargo doc --document-private-items --no-deps --all-features
(cd emath && cargo check --no-default-features)
(cd epaint && cargo check --no-default-features)
(cd epaint && cargo check --no-default-features --release)
(cd egui && cargo check --no-default-features --features "serde")
(cd eframe && cargo check --no-default-features)
(cd epi && cargo check --no-default-features)
(cd egui && cargo check --no-default-features --features "serde")
(cd egui_demo_app && cargo check --no-default-features)
(cd egui_demo_lib && cargo check --no-default-features)
(cd egui_extras && cargo check --no-default-features)
(cd egui_web && cargo check --no-default-features)
(cd egui-winit && cargo check --no-default-features)
(cd egui_glium && cargo check --no-default-features)
(cd egui_glow && cargo check --no-default-features)
(cd egui_web && cargo check --no-default-features)
(cd egui-winit && cargo check --no-default-features)
(cd emath && cargo check --no-default-features)
(cd epaint && cargo check --no-default-features --release)
(cd epaint && cargo check --no-default-features)
(cd epi && cargo check --no-default-features)
(cd eframe && cargo check --all-features)