From ed002acc68c472ed3fdc6b0dd8079761753d1c12 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 29 Apr 2022 08:17:49 +0200 Subject: [PATCH] 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 --- Cargo.lock | 9 +- eframe/CHANGELOG.md | 2 +- eframe/Cargo.toml | 17 +- eframe/src/lib.rs | 29 +- .../src/native/epi_integration.rs | 28 +- eframe/src/native/mod.rs | 4 + .../src/native/run.rs | 13 +- egui-winit/CHANGELOG.md | 5 +- egui-winit/Cargo.toml | 19 +- egui-winit/src/lib.rs | 3 - egui/src/data/input.rs | 2 +- egui_glium/Cargo.toml | 12 +- egui_glium/examples/native_texture.rs | 2 - egui_glium/examples/pure_glium.rs | 2 +- egui_glow/CHANGELOG.md | 4 +- egui_glow/Cargo.toml | 48 +- egui_glow/examples/pure_glow.rs | 2 +- egui_glow/src/lib.rs | 9 +- egui_glow/src/painter.rs | 37 +- egui_glow/src/winit.rs | 2 +- egui_web/Cargo.toml | 6 +- egui_web/src/backend.rs | 74 +- egui_web/src/events.rs | 544 +++++++++++++++ egui_web/src/lib.rs | 653 +----------------- egui_web/src/storage.rs | 47 ++ epi/src/lib.rs | 12 - sh/check.sh | 15 +- 27 files changed, 797 insertions(+), 803 deletions(-) rename egui-winit/src/epi.rs => eframe/src/native/epi_integration.rs (92%) create mode 100644 eframe/src/native/mod.rs rename egui_glow/src/epi_backend.rs => eframe/src/native/run.rs (93%) create mode 100644 egui_web/src/events.rs create mode 100644 egui_web/src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index 8ec5953b..087d4c41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index 558878a4..5a3fe9ca 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -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). diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index 177f2f9a..2f5ccc5c 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -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] diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index f732ce0b..fc2822ab 100644 --- a/eframe/src/lib.rs +++ b/eframe/src/lib.rs @@ -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; diff --git a/egui-winit/src/epi.rs b/eframe/src/native/epi_integration.rs similarity index 92% rename from egui-winit/src/epi.rs rename to eframe/src/native/epi_integration.rs index 3bdcdf3e..2b9d80ec 100644 --- a/egui-winit/src/epi.rs +++ b/eframe/src/native/epi_integration.rs @@ -1,3 +1,5 @@ +use egui_winit::{native_pixels_per_point, WindowSettings}; + pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { winit::dpi::LogicalSize { width: points.x as f64, @@ -7,7 +9,7 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { pub fn window_builder( native_options: &epi::NativeOptions, - window_settings: &Option, + window_settings: &Option, ) -> 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::(crate::native_pixels_per_point(window) as f64), + .to_logical::(native_pixels_per_point(window) as f64), ); } @@ -145,10 +147,10 @@ pub fn create_storage(_app_name: &str) -> Option> { /// 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 { +pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) diff --git a/eframe/src/native/mod.rs b/eframe/src/native/mod.rs new file mode 100644 index 00000000..fb84f762 --- /dev/null +++ b/eframe/src/native/mod.rs @@ -0,0 +1,4 @@ +mod epi_integration; +mod run; + +pub use run::run; diff --git a/egui_glow/src/epi_backend.rs b/eframe/src/native/run.rs similarity index 93% rename from egui_glow/src/epi_backend.rs rename to eframe/src/native/run.rs index 4f50cebd..a746ffd5 100644 --- a/egui_glow/src/epi_backend.rs +++ b/eframe/src/native/run.rs @@ -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, diff --git a/egui-winit/CHANGELOG.md b/egui-winit/CHANGELOG.md index b7c711f6..c5b1e71e 100644 --- a/egui-winit/CHANGELOG.md +++ b/egui-winit/CHANGELOG.md @@ -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 diff --git a/egui-winit/Cargo.toml b/egui-winit/Cargo.toml index aa835295..0096d6a5 100644 --- a/egui-winit/Cargo.toml +++ b/egui-winit/Cargo.toml @@ -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 } diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index bd6e8c61..db1aeea9 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -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 { diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index eebe7096..426f879c 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -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, } diff --git a/egui_glium/Cargo.toml b/egui_glium/Cargo.toml index 033f6725..02561f0c 100644 --- a/egui_glium/Cargo.toml +++ b/egui_glium/Cargo.toml @@ -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"] } diff --git a/egui_glium/examples/native_texture.rs b/egui_glium/examples/native_texture.rs index 4750d48b..1a58d0a9 100644 --- a/egui_glium/examples/native_texture.rs +++ b/egui_glium/examples/native_texture.rs @@ -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; diff --git a/egui_glium/examples/pure_glium.rs b/egui_glium/examples/pure_glium.rs index 4fe900f7..b77365a2 100644 --- a/egui_glium/examples/pure_glium.rs +++ b/egui_glium/examples/pure_glium.rs @@ -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 diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index 70b62a7e..994da1fa 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -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 diff --git a/egui_glow/Cargo.toml b/egui_glow/Cargo.toml index 5889477f..887b8160 100644 --- a/egui_glow/Cargo.toml +++ b/egui_glow/Cargo.toml @@ -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"] diff --git a/egui_glow/examples/pure_glow.rs b/egui_glow/examples/pure_glow.rs index 5a2d280b..07b29fae 100644 --- a/egui_glow/examples/pure_glow.rs +++ b/egui_glow/examples/pure_glow.rs @@ -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)] diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index c9dd3b4d..1522efab 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -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 diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index ed41c979..10667e4d 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -47,7 +47,6 @@ pub struct Painter { textures: HashMap, - #[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), diff --git a/egui_glow/src/winit.rs b/egui_glow/src/winit.rs index 547e9065..65520d30 100644 --- a/egui_glow/src/winit.rs +++ b/egui_glow/src/winit.rs @@ -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, diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 950a65a1..c9e547ea 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -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", -] +] } diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 2f9982aa..0644e4bf 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -330,6 +330,57 @@ impl AppRunner { } } +// ---------------------------------------------------------------------------- + +pub type AppRunnerRef = Arc>; + +pub struct AppRunnerContainer { + pub runner: AppRunnerRef, + /// Set to `true` if there is a panic. + /// Used to ignore callbacks after a panic. + pub panicked: Arc, +} + +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( + &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::(); + + closure(event, runner_ref.lock()); + } + }) as Box + }); + + // 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 { @@ -346,12 +397,12 @@ fn start_runner(app_runner: AppRunner) -> Result { 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 { Ok(runner_container.runner) } + +// ---------------------------------------------------------------------------- + +#[derive(Default)] +struct LocalStorage {} + +impl epi::Storage for LocalStorage { + fn get_string(&self, key: &str) -> Option { + local_storage_get(key) + } + fn set_string(&mut self, key: &str, value: String) { + local_storage_set(key, &value); + } + fn flush(&mut self) {} +} diff --git a/egui_web/src/events.rs b/egui_web/src/events.rs new file mode 100644 index 00000000..83cb9155 --- /dev/null +++ b/egui_web/src/events.rs @@ -0,0 +1,544 @@ +use crate::*; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub fn paint_and_schedule( + runner_ref: &AppRunnerRef, + panicked: Arc, +) -> 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, + ) -> 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); + + 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); + 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(()) +} diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index e7c95896..72c05378 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -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::window()?.local_storage().ok()? -} - -pub fn local_storage_get(key: &str) -> Option { - 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 { - 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>; - -pub struct AppRunnerContainer { - runner: AppRunnerRef, - /// Set to `true` if there is a panic. - /// Used to ignore callbacks after a panic. - panicked: Arc, -} - -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( - &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::(); - - closure(event, runner_ref.lock()); - } - }) as Box - }); - - // 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) -> 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, - ) -> 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); - - 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); - 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 diff --git a/egui_web/src/storage.rs b/egui_web/src/storage.rs new file mode 100644 index 00000000..1b6cc123 --- /dev/null +++ b/egui_web/src/storage.rs @@ -0,0 +1,47 @@ +fn local_storage() -> Option { + web_sys::window()?.local_storage().ok()? +} + +pub fn local_storage_get(key: &str) -> Option { + 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) {} diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 88a72a05..41693d94 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -425,18 +425,6 @@ pub struct IntegrationInfo { pub native_pixels_per_point: Option, } -/// 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. diff --git a/sh/check.sh b/sh/check.sh index 08e4b1c9..5615b764 100755 --- a/sh/check.sh +++ b/sh/check.sh @@ -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)