From fd24f7215a04cb61f8d371b424b84b1554fb631e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 6 Apr 2024 14:31:19 -0400 Subject: [PATCH] Update egui to 0.27.2 (#121) * merge: merge from upstream egui 0.27.2 * chore: update other deps and fix compilation errors * fix: repatch eframe's `request_animation_frame` method * chore: update winit to a non-yanked version * chore: remove invisible scroll bars from tab viewer * chore: clippy * fix: fix integration of eframe's alt-tab detection in web builds --- Cargo.lock | 42 ++--- Cargo.toml | 8 +- crates/core/Cargo.toml | 6 +- crates/core/src/tab.rs | 43 ++--- crates/eframe/CHANGELOG.md | 80 ++++++--- crates/eframe/Cargo.toml | 6 +- crates/eframe/README.md | 2 +- crates/eframe/src/epi.rs | 28 ++- crates/eframe/src/native/epi_integration.rs | 2 + crates/eframe/src/native/file_storage.rs | 1 - crates/eframe/src/native/glow_integration.rs | 73 ++++++-- crates/eframe/src/native/run.rs | 2 +- crates/eframe/src/native/wgpu_integration.rs | 49 ++++-- crates/eframe/src/web/app_runner.rs | 15 +- crates/eframe/src/web/backend.rs | 66 +++++--- crates/eframe/src/web/events.rs | 169 +++++++++++-------- crates/eframe/src/web/input.rs | 26 +-- crates/eframe/src/web/mod.rs | 106 ++++++------ crates/eframe/src/web/text_agent.rs | 5 +- crates/eframe/src/web/web_painter.rs | 3 + crates/eframe/src/web/web_painter_glow.rs | 13 +- crates/eframe/src/web/web_painter_wgpu.rs | 4 + crates/eframe/src/web/web_runner.rs | 28 ++- crates/egui-wgpu/CHANGELOG.md | 12 ++ crates/egui-wgpu/README.md | 2 +- src/app/mod.rs | 2 +- 26 files changed, 515 insertions(+), 278 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 951c5bc9..0ad9dd93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,9 +1415,9 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfe80b1890e1a8cdbffc6044d6872e814aaf6011835a2a5e2db0e5c5c4ef4e" +checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" dependencies = [ "bytemuck", "serde", @@ -1425,9 +1425,9 @@ dependencies = [ [[package]] name = "egui" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180f595432a5b615fc6b74afef3955249b86cfea72607b40740a4cd60d5297d0" +checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a" dependencies = [ "accesskit", "ahash", @@ -1441,27 +1441,27 @@ dependencies = [ [[package]] name = "egui-modal" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da17dbe55bb1b04fd8b4624ab1b68f6ec245f44aedc90893463f8dbc70f28231" +checksum = "738cdffefd15dbb6a5fff75d118eee82a9e894bbfa45e41c24d7de42519fa673" dependencies = [ "egui", ] [[package]] name = "egui-notify" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abbfff21281b41451a347f2c0938ce278fab65ee450eb7a495efffa0bc3bb95" +checksum = "319327faee7bb116bcdbe43af1b8cbea06dc5d9ddbb23d35e012949afbd76cde" dependencies = [ "egui", ] [[package]] name = "egui-winit" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4d44f8d89f70d4480545eb2346b76ea88c3022e9f4706cebc799dbe8b004a2" +checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609" dependencies = [ "accesskit_winit", "arboard", @@ -1478,9 +1478,9 @@ dependencies = [ [[package]] name = "egui_dock" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d14beb118bc4d114bb875f14d1d437736be7010939cedf60c9ee002d7d02d09f" +checksum = "c3b8d9a54c0ed60f2670ad387c269663b4771431f090fa586906cf5f0bc586f4" dependencies = [ "duplicate", "egui", @@ -1489,9 +1489,9 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4a6962241a76da5be5e64e41b851ee1c95fda11f76635522a3c82b119b5475" +checksum = "1b78779f35ded1a853786c9ce0b43fe1053e10a21ea3b23ebea411805ce41593" dependencies = [ "egui", "enum-map", @@ -1510,9 +1510,9 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "emath" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6916301ecf80448f786cdf3eb51d9dbdd831538732229d49119e2d4312eaaf09" +checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" dependencies = [ "bytemuck", "serde", @@ -1594,9 +1594,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b9fdf617dd7f58b0c8e6e9e4a1281f730cde0831d40547da446b2bb76a47af" +checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176" dependencies = [ "ab_glyph", "ahash", @@ -2461,7 +2461,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -6369,9 +6369,9 @@ checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" [[package]] name = "winit" -version = "0.29.11" +version = "0.29.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "272be407f804517512fdf408f0fe6c067bf24659a913c61af97af176bfd5aa92" +checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" dependencies = [ "ahash", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index 6c3cc0a2..d0f76e80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,9 +60,9 @@ categories = ["games"] # Shared dependencies [workspace.dependencies] -egui = "0.26.2" -egui_extras = { version = "0.26.2", features = ["svg", "image"] } -epaint = "0.26.2" +egui = "0.27.2" +egui_extras = { version = "0.27.2", features = ["svg", "image"] } +epaint = "0.27.2" luminol-eframe = { version = "0.4.0", path = "crates/eframe/", features = [ "wgpu", @@ -73,7 +73,7 @@ luminol-eframe = { version = "0.4.0", path = "crates/eframe/", features = [ "wayland", ], default-features = false } luminol-egui-wgpu = { version = "0.4.0", path = "crates/egui-wgpu/" } -egui-winit = "0.26.2" +egui-winit = "0.27.2" wgpu = { version = "0.19.1", features = ["naga-ir"] } glam = { version = "0.24.2", features = ["bytemuck"] } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index f413240a..b128e420 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,9 +19,9 @@ workspace = true [dependencies] egui.workspace = true -egui_dock = "0.11.2" -egui-notify = "0.13.0" -egui-modal = "0.3.4" +egui_dock = "0.12.0" +egui-notify = "0.14.0" +egui-modal = "0.3.6" poll-promise.workspace = true once_cell.workspace = true diff --git a/crates/core/src/tab.rs b/crates/core/src/tab.rs index b08ccfc9..92d265a0 100644 --- a/crates/core/src/tab.rs +++ b/crates/core/src/tab.rs @@ -84,31 +84,24 @@ impl Tabs { ui: &mut egui::Ui, update_state: &mut crate::UpdateState<'_>, ) { - // This scroll area with hidden scrollbars is a hacky workaround for - // https://github.com/Adanos020/egui_dock/issues/90 - // which, for us, seems to manifest when the user moves tabs around - egui::ScrollArea::vertical() - .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) - .show(ui, |ui| { - let mut style = egui_dock::Style::from_egui(ui.style()); - style.overlay.surface_fade_opacity = 1.; - - let focused_id = ui - .memory(|m| m.focus().is_none()) - .then_some(self.dock_state.find_active_focused().map(|(_, t)| t.id())) - .flatten(); - egui_dock::DockArea::new(&mut self.dock_state) - .id(self.id) - .style(style) - .show_inside( - ui, - &mut TabViewer { - update_state, - focused_id, - allowed_in_windows: self.allowed_in_windows, - }, - ); - }); + let mut style = egui_dock::Style::from_egui(ui.style()); + style.overlay.surface_fade_opacity = 1.; + + let focused_id = ui + .memory(|m| m.focused().is_none()) + .then_some(self.dock_state.find_active_focused().map(|(_, t)| t.id())) + .flatten(); + egui_dock::DockArea::new(&mut self.dock_state) + .id(self.id) + .style(style) + .show_inside( + ui, + &mut TabViewer { + update_state, + focused_id, + allowed_in_windows: self.allowed_in_windows, + }, + ); } /// Display all tabs. diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index f0566955..dfea5671 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,40 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +#### Desktop/Native +* Fix continuous repaint on Wayland when TextEdit is focused or IME output is set [#4269](https://github.com/emilk/egui/pull/4269) (thanks [@white-axe](https://github.com/white-axe)!) +* Remove a bunch of `unwrap()` [#4285](https://github.com/emilk/egui/pull/4285) + +#### Web +* Fix blurry rendering in some browsers [#4299](https://github.com/emilk/egui/pull/4299) +* Correctly identify if the browser tab has focus [#4280](https://github.com/emilk/egui/pull/4280) + + +## 0.27.1 - 2024-03-29 +* Web: repaint if the `#hash` in the URL changes [#4261](https://github.com/emilk/egui/pull/4261) +* Add web support for `zoom_factor` [#4260](https://github.com/emilk/egui/pull/4260) (thanks [@justusdieckmann](https://github.com/justusdieckmann)!) + + +## 0.27.0 - 2024-03-26 +* Update to document-features 0.2.8 [#4003](https://github.com/emilk/egui/pull/4003) +* Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events [#4008](https://github.com/emilk/egui/pull/4008) (thanks [@varphone](https://github.com/varphone)!) + +#### Desktop/Native +* Add with_taskbar to viewport builder [#3958](https://github.com/emilk/egui/pull/3958) (thanks [@AnotherNathan](https://github.com/AnotherNathan)!) +* Add `winuser` feature to `winapi` to fix unresolved import [#4037](https://github.com/emilk/egui/pull/4037) (thanks [@varphone](https://github.com/varphone)!) +* Add `get_proc_address` in CreationContext [#4145](https://github.com/emilk/egui/pull/4145) (thanks [@Chaojimengnan](https://github.com/Chaojimengnan)!) +* Don't clear modifier state on focus change [#4157](https://github.com/emilk/egui/pull/4157) (thanks [@ming08108](https://github.com/ming08108)!) +* Add x11 window type settings to viewport builder [#4175](https://github.com/emilk/egui/pull/4175) (thanks [@psethwick](https://github.com/psethwick)!) + +#### Web +* Add `webgpu` feature by default to wgpu [#4124](https://github.com/emilk/egui/pull/4124) (thanks [@ctaggart](https://github.com/ctaggart)!) +* Update kb modifiers from web mouse events [#4156](https://github.com/emilk/egui/pull/4156) (thanks [@ming08108](https://github.com/ming08108)!) +* Fix crash on `request_animation_frame` when destroying web runner [#4169](https://github.com/emilk/egui/pull/4169) (thanks [@jprochazk](https://github.com/jprochazk)!) +* Fix bug parsing url query with escaped & or = [#4172](https://github.com/emilk/egui/pull/4172) +* `Location::query_map`: support repeated key [#4183](https://github.com/emilk/egui/pull/4183) + + ## 0.26.2 - 2024-02-14 * Add `winuser` feature to `winapi` to fix unresolved import [#4037](https://github.com/emilk/egui/pull/4037) (thanks [@varphone](https://github.com/varphone)!) @@ -22,38 +56,38 @@ Changes since the last release can be found at [!IMPORTANT] -> luminol-eframe is currently based on emilk/egui@0.26.2 +> luminol-eframe is currently based on emilk/egui@0.27.2 > [!NOTE] > This is Luminol's modified version of eframe. The original version is dual-licensed under MIT and Apache 2.0. diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 7054bd52..a32ddc19 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -67,6 +67,10 @@ pub struct CreationContext<'s> { #[cfg(feature = "glow")] pub gl: Option>, + /// The `get_proc_address` wrapper of underlying GL context + #[cfg(feature = "glow")] + pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>, + /// The underlying WGPU render state. /// /// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`]. @@ -196,6 +200,24 @@ pub trait App { fn persist_egui_memory(&self) -> bool { true } + + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// + /// This function provides a way to modify or filter input events before they are processed by egui. + /// + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. + /// + /// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard. + /// + /// # Arguments + /// + /// * `_ctx` - The context of the egui, which provides access to the current state of the egui. + /// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes. + /// + /// # Note + /// + /// This function does not return a value. Any changes to the input should be made directly to `_raw_input`. + fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {} } /// Selects the level of hardware graphics acceleration. @@ -696,7 +718,7 @@ pub struct WebInfo { #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug)] pub struct Location { - /// The full URL (`location.href`) without the hash. + /// The full URL (`location.href`) without the hash, percent-decoded. /// /// Example: `"http://www.example.com:80/index.html?foo=bar"`. pub url: String, @@ -736,8 +758,8 @@ pub struct Location { /// The parsed "query" part of "www.example.com/index.html?query#fragment". /// - /// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}` - pub query_map: std::collections::BTreeMap, + /// "foo=hello&bar%20&foo=world" is parsed as `{"bar ": [""], "foo": ["hello", "world"]}` + pub query_map: std::collections::BTreeMap>, /// `location.origin` /// diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 1f12d1cc..e98c8921 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -274,6 +274,8 @@ impl EpiIntegration { let close_requested = raw_input.viewport().close_requested(); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 9652dc2f..51c668ab 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -104,7 +104,6 @@ impl crate::Storage for FileStorage { .spawn(move || { save_to_disk(&file_path, &kv); }); - match result { Ok(join_handle) => { self.last_save_join_handle = Some(join_handle); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index be30d853..ff45cb65 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -7,7 +7,7 @@ #![allow(clippy::arc_with_non_send_sync)] // glow::Context was accidentally non-Sync in glow 0.13, but that will be fixed in future releases of glow: https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e -use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; +use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; use glutin::{ config::GlConfig, @@ -22,9 +22,9 @@ use winit::{ }; use egui::{ - epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, - ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, - ViewportInfo, ViewportOutput, + epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, + ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, + ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -252,7 +252,7 @@ impl GlowWinitApp { #[cfg(feature = "accesskit")] { let event_loop_proxy = self.repaint_proxy.lock().clone(); - let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); + let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); // we always have a root if let Viewport { window: Some(window), egui_winit: Some(egui_winit), @@ -284,12 +284,14 @@ impl GlowWinitApp { // Use latest raw_window_handle for eframe compatibility use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; + let get_proc_address = |addr: &_| glutin.get_proc_address(addr); let window = glutin.window(ViewportId::ROOT); let cc = CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info().clone(), storage: integration.frame.storage(), gl: Some(gl), + get_proc_address: Some(&get_proc_address), #[cfg(feature = "wgpu")] wgpu_render_state: None, raw_display_handle: window.display_handle().map(|h| h.as_raw()), @@ -446,6 +448,33 @@ impl WinitApp for GlowWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport) = glutin + .focused_viewport + .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, @@ -515,13 +544,17 @@ impl GlowWinitRunning { let (raw_input, viewport_ui_cb) = { let mut glutin = self.glutin.borrow_mut(); let egui_ctx = glutin.egui_ctx.clone(); - let viewport = glutin.viewports.get_mut(&viewport_id).unwrap(); + let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; let Some(window) = viewport.window.as_ref() else { return EventResult::Wait; }; egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window); - let egui_winit = viewport.egui_winit.as_mut().unwrap(); + let Some(egui_winit) = viewport.egui_winit.as_mut() else { + return EventResult::Wait; + }; let mut raw_input = egui_winit.take_egui_input(window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); @@ -554,8 +587,12 @@ impl GlowWinitRunning { .. } = &mut *glutin; let viewport = &viewports[&viewport_id]; - let window = viewport.window.as_ref().unwrap(); - let gl_surface = viewport.gl_surface.as_ref().unwrap(); + let Some(window) = viewport.window.as_ref() else { + return EventResult::Wait; + }; + let Some(gl_surface) = viewport.gl_surface.as_ref() else { + return EventResult::Wait; + }; let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); @@ -605,7 +642,9 @@ impl GlowWinitRunning { .. } = &mut *glutin; - let viewport = viewports.get_mut(&viewport_id).unwrap(); + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; viewport.info.events.clear(); // they should have been processed let window = viewport.window.clone().unwrap(); let gl_surface = viewport.gl_surface.as_ref().unwrap(); @@ -668,7 +707,7 @@ impl GlowWinitRunning { #[cfg(feature = "__screenshot")] if integration.egui_ctx.frame_nr() == 2 { if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { - save_screeshot_and_exit(&path, &painter, screen_size_in_pixels); + save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); } } @@ -836,7 +875,7 @@ impl GlutinWindowContext { crate::HardwareAcceleration::Off => Some(false), }; let swap_interval = if native_options.vsync { - glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) + glutin::surface::SwapInterval::Wait(NonZeroU32::MIN) } else { glutin::surface::SwapInterval::DontWait }; @@ -1060,8 +1099,8 @@ impl GlutinWindowContext { // surface attributes let (width_px, height_px): (u32, u32) = window.inner_size().into(); - let width_px = std::num::NonZeroU32::new(width_px.at_least(1)).unwrap(); - let height_px = std::num::NonZeroU32::new(height_px.at_least(1)).unwrap(); + let width_px = NonZeroU32::new(width_px).unwrap_or(NonZeroU32::MIN); + let height_px = NonZeroU32::new(height_px).unwrap_or(NonZeroU32::MIN); let surface_attributes = { use rwh_05::HasRawWindowHandle as _; // glutin stuck on old version of raw-window-handle glutin::surface::SurfaceAttributesBuilder::::new() @@ -1140,8 +1179,8 @@ impl GlutinWindowContext { } fn resize(&mut self, viewport_id: ViewportId, physical_size: winit::dpi::PhysicalSize) { - let width_px = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); - let height_px = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); + let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN); + let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN); if let Some(viewport) = self.viewports.get(&viewport_id) { if let Some(gl_surface) = &viewport.gl_surface { @@ -1452,7 +1491,7 @@ fn render_immediate_viewport( } #[cfg(feature = "__screenshot")] -fn save_screeshot_and_exit( +fn save_screenshot_and_exit( path: &str, painter: &egui_glow::Painter, screen_size_in_pixels: [u32; 2], diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 370be7b8..27da2713 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -43,7 +43,7 @@ fn with_event_loop( mut native_options: epi::NativeOptions, f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, ) -> Result { - thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); + thread_local!(static EVENT_LOOP: RefCell>> = const { RefCell::new(None) }); EVENT_LOOP.with(|event_loop| { // Since we want to reference NativeOptions when creating the EventLoop we can't diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 70a12e01..ed02d416 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -5,7 +5,7 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; +use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; @@ -262,6 +262,8 @@ impl WgpuWinitApp { storage: integration.frame.storage(), #[cfg(feature = "glow")] gl: None, + #[cfg(feature = "glow")] + get_proc_address: None, wgpu_render_state, raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_window_handle: window.window_handle().map(|h| h.as_raw()), @@ -433,13 +435,12 @@ impl WinitApp for WgpuWinitApp { self.init_run_state(egui_ctx, event_loop, storage, window, builder)? }; - EventResult::RepaintNow( - running.shared.borrow().viewports[&ViewportId::ROOT] - .window - .as_ref() - .unwrap() - .id(), - ) + let viewport = &running.shared.borrow().viewports[&ViewportId::ROOT]; + if let Some(window) = &viewport.window { + EventResult::RepaintNow(window.id()) + } else { + EventResult::Wait + } } winit::event::Event::Suspended => { @@ -456,6 +457,33 @@ impl WinitApp for WgpuWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + if let Some(viewport) = shared + .focused_viewport + .and_then(|viewport| shared.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, @@ -584,7 +612,9 @@ impl WgpuWinitRunning { } } - let egui_winit = egui_winit.as_mut().unwrap(); + let Some(egui_winit) = egui_winit.as_mut() else { + return EventResult::Wait; + }; let mut raw_input = egui_winit.take_egui_input(window); integration.pre_update(); @@ -744,7 +774,6 @@ impl WgpuWinitRunning { // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where the app would panic when minimizing on Windows. if let Some(viewport_id) = viewport_id { - use std::num::NonZeroU32; if let (Some(width), Some(height)) = ( NonZeroU32::new(physical_size.width), NonZeroU32::new(physical_size.height), diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index cdcbdd91..206e5ce0 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -90,7 +90,9 @@ impl AppRunner { super::storage::load_memory(&egui_ctx).await; egui_ctx.options_mut(|o| { - // On web, the browser controls the zoom factor: + // On web by default egui follows the zoom factor of the browser, + // and lets the browser handle the zoom shortscuts. + // A user can still zoom egui separately by calling [`egui::Context::set_zoom_factor`]. o.zoom_with_keyboard = false; o.zoom_factor = 1.0; }); @@ -106,6 +108,9 @@ impl AppRunner { #[cfg(feature = "glow")] gl: Some(painter.gl().clone()), + #[cfg(feature = "glow")] + get_proc_address: None, + #[cfg(all(feature = "wgpu", not(feature = "glow")))] wgpu_render_state: painter.render_state(), #[cfg(all(feature = "wgpu", feature = "glow"))] @@ -188,6 +193,10 @@ impl AppRunner { self.last_save_time = now_sec(); } + pub fn canvas(&self) -> &web_sys::OffscreenCanvas { + self.painter.canvas() + } + pub fn destroy(mut self) { log::debug!("Destroying AppRunner"); self.painter.destroy(); @@ -230,6 +239,10 @@ impl AppRunner { } self.mutable_text_under_cursor = platform_output.mutable_text_under_cursor; + self.worker_options.channels.zoom_tx.store( + self.egui_ctx.zoom_factor(), + portable_atomic::Ordering::Relaxed, + ); self.worker_options .channels .send(super::WebRunnerOutput::PlatformOutput( diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index e949ea89..57e28cbb 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -35,16 +35,20 @@ impl WebInput { .native_pixels_per_point = Some(pixels_per_point); raw_input } + + /// On alt-tab and similar. + pub fn on_web_page_focus_change(&mut self, focused: bool) { + // log::debug!("on_web_page_focus_change: {focused}"); + self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: + self.raw.focused = focused; + self.raw.events.push(egui::Event::WindowFocused(focused)); + self.latest_touch_pos = None; + self.latest_touch_pos_id = None; + } } // ---------------------------------------------------------------------------- -// ensure that AtomicF64 is using atomic ops (otherwise it would use global locks, and that would be bad) -const _: [(); 0 - !{ - const ASSERT: bool = portable_atomic::AtomicF64::is_always_lock_free(); - ASSERT -} as usize] = []; - /// Stores when to do the next repaint. pub(crate) struct NeedRepaint(portable_atomic::AtomicF64); @@ -101,44 +105,45 @@ pub fn web_location() -> epi::Location { .search() .unwrap_or_default() .strip_prefix('?') - .map(percent_decode) - .unwrap_or_default(); - - let query_map = parse_query_map(&query) - .iter() - .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())) - .collect(); + .unwrap_or_default() + .to_owned(); epi::Location { + // TODO(emilk): should we really percent-decode the url? 🤷‍♂️ url: percent_decode(&location.href().unwrap_or_default()), protocol: percent_decode(&location.protocol().unwrap_or_default()), host: percent_decode(&location.host().unwrap_or_default()), hostname: percent_decode(&location.hostname().unwrap_or_default()), port: percent_decode(&location.port().unwrap_or_default()), hash, + query_map: parse_query_map(&query), query, - query_map, origin: percent_decode(&location.origin().unwrap_or_default()), } } -fn parse_query_map(query: &str) -> BTreeMap<&str, &str> { - query - .split('&') - .filter_map(|pair| { - if pair.is_empty() { - None +/// query is percent-encoded +fn parse_query_map(query: &str) -> BTreeMap> { + let mut map: BTreeMap> = Default::default(); + + for pair in query.split('&') { + if !pair.is_empty() { + if let Some((key, value)) = pair.split_once('=') { + map.entry(percent_decode(key)) + .or_default() + .push(percent_decode(value)); } else { - Some(if let Some((key, value)) = pair.split_once('=') { - (key, value) - } else { - (pair, "") - }) + map.entry(percent_decode(pair)) + .or_default() + .push(String::new()); } - }) - .collect() + } + } + + map } +// TODO(emilk): this test is never acgtually run, because this whole module is wasm32 only 🤦‍♂️ #[test] fn test_parse_query() { assert_eq!(parse_query_map(""), BTreeMap::default()); @@ -159,4 +164,11 @@ fn test_parse_query() { parse_query_map("foo&baz&&"), BTreeMap::from_iter([("foo", ""), ("baz", "")]) ); + assert_eq!( + parse_query_map("badger=data.rrd%3Fparam1%3Dfoo%26param2%3Dbar&mushroom=snake"), + BTreeMap::from_iter([ + ("badger", "data.rrd?param1=foo¶m2=bar"), + ("mushroom", "snake") + ]) + ); } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 65bbba27..1c07817c 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -5,7 +5,7 @@ use super::*; /// Calls `request_animation_frame` to schedule repaint. /// /// It will only paint if needed, but will always call `request_animation_frame` immediately. -fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { let mut width = runner_lock.painter.width; @@ -14,34 +14,51 @@ fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { let mut modifiers = runner_lock.input.raw.modifiers; let mut should_save = false; let mut touch = None; + let mut has_focus = None; + runner_lock.input.raw.events = Vec::new(); + let mut events = Vec::new(); - for event in runner_lock - .worker_options - .channels - .custom_event_rx - .try_iter() - { + for event in runner_lock.worker_options.channels.event_rx.try_iter() { match event { - WebRunnerCustomEvent::ScreenResize(new_width, new_height, new_pixel_ratio) => { + WebRunnerEvent::EguiEvent(event) => { + events.push(event); + } + + WebRunnerEvent::ScreenResize(new_width, new_height, new_pixel_ratio) => { width = new_width; height = new_height; pixel_ratio = new_pixel_ratio; } - WebRunnerCustomEvent::Modifiers(new_modifiers) => { + WebRunnerEvent::Modifiers(new_modifiers) => { modifiers = new_modifiers; } - WebRunnerCustomEvent::Save => { + WebRunnerEvent::Save => { should_save = true; } - WebRunnerCustomEvent::Touch(touch_id, touch_pos) => { + WebRunnerEvent::Touch(touch_id, touch_pos) => { touch = Some((touch_id, touch_pos)); } + + WebRunnerEvent::Focus(new_has_focus) => { + has_focus = Some(new_has_focus); + events.push(egui::Event::WindowFocused(new_has_focus)); + touch = None; + } } } + // If web page has been defocused/focused, update the focused state in the input, reset + // touch state and trigger a rerender + if let Some(has_focus) = has_focus { + runner_lock.input.raw.focused = has_focus; + runner_lock.input.latest_touch_pos_id = None; + runner_lock.input.latest_touch_pos = None; + runner_lock.needs_repaint.repaint_asap(); + } + // If a touch event has been detected, put it into the input and trigger a rerender if let Some((touch_id, touch_pos)) = touch { runner_lock.input.latest_touch_pos_id = touch_id; @@ -55,12 +72,7 @@ fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { runner_lock.needs_repaint.repaint_asap(); } - runner_lock.input.raw.events = runner_lock - .worker_options - .channels - .event_rx - .try_iter() - .collect(); + runner_lock.input.raw.events = events; if !runner_lock.input.raw.events.is_empty() { // Render immediately if there are any pending events runner_lock.needs_repaint.repaint_asap(); @@ -97,7 +109,7 @@ fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { paint_if_needed(&mut runner_lock); drop(runner_lock); - request_animation_frame(runner_ref.clone())?; + runner_ref.request_animation_frame()?; } Ok(()) } @@ -132,41 +144,26 @@ fn paint_if_needed(runner: &mut AppRunner) { runner.auto_save_if_needed(); } -pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { - let worker = luminol_web::bindings::worker().unwrap(); - let closure = Closure::once(move || paint_and_schedule(&runner_ref)); - worker.request_animation_frame(closure.as_ref().unchecked_ref())?; - closure.forget(); // We must forget it, or else the callback is canceled on drop - Ok(()) -} - // ------------------------------------------------------------------------ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); - { - // Avoid sticky modifier keys on alt-tab: - for event_name in ["blur", "focus"] { - let closure = move |event: web_sys::MouseEvent, state: &MainState| { - let has_focus = event_name == "focus"; - - if !has_focus { - // We lost focus - good idea to save - state.channels.send_custom(WebRunnerCustomEvent::Save); - } + for event_name in ["blur", "focus"] { + let closure = move |_event: web_sys::MouseEvent, state: &MainState| { + // log::debug!("{event_name:?}"); + let has_focus = event_name == "focus"; - //runner.input.on_web_page_focus_change(has_focus); - //runner.egui_ctx().request_repaint(); - // log::debug!("{event_name:?}"); + if !has_focus { + // We lost focus - good idea to save + state.channels.send_custom(WebRunnerEvent::Save); + } - state.channels.send_custom(WebRunnerCustomEvent::Modifiers( - modifiers_from_mouse_event(&event), - )); - }; + state.channels.send_custom(WebRunnerEvent::Focus(has_focus)); + //runner.egui_ctx().request_repaint(); + }; - state.add_event_listener(&document, event_name, closure)?; - } + state.add_event_listener(&document, event_name, closure)?; } state.add_event_listener( @@ -178,10 +175,10 @@ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> return; } - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); state .channels - .send_custom(WebRunnerCustomEvent::Modifiers(modifiers)); + .send_custom(WebRunnerEvent::Modifiers(modifiers)); let key = event.key(); let egui_key = translate_key(&key); @@ -189,7 +186,7 @@ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> if let Some(key) = egui_key { state.channels.send(egui::Event::Key { key, - physical_key: None, // TODO + physical_key: None, // TODO(fornwall) pressed: true, repeat: false, // egui will fill this in for us! modifiers, @@ -254,14 +251,14 @@ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> &document, "keyup", |event: web_sys::KeyboardEvent, state| { - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); state .channels - .send_custom(WebRunnerCustomEvent::Modifiers(modifiers)); + .send_custom(WebRunnerEvent::Modifiers(modifiers)); if let Some(key) = translate_key(&event.key()) { state.channels.send(egui::Event::Key { key, - physical_key: None, // TODO + physical_key: None, // TODO(fornwall) pressed: false, repeat: false, modifiers, @@ -330,6 +327,23 @@ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); + for event_name in ["blur", "focus"] { + let closure = move |_event: web_sys::MouseEvent, state: &MainState| { + // log::debug!("{event_name:?}"); + let has_focus = event_name == "focus"; + + if !has_focus { + // We lost focus - good idea to save + state.channels.send_custom(WebRunnerEvent::Save); + } + + state.channels.send_custom(WebRunnerEvent::Focus(has_focus)); + //runner.egui_ctx().request_repaint(); + }; + + state.add_event_listener(&window, event_name, closure)?; + } + /* // Save-on-close @@ -338,7 +352,8 @@ pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { })?; for event_name in &["load", "pagehide", "pageshow", "resize"] { - runner_ref.add_event_listener(&window, event_name, |_: web_sys::Event, runner| { + runner_ref.add_event_listener(&window, event_name, move |_: web_sys::Event, runner| { + // log::debug!("{event_name:?}"); runner.needs_repaint.repaint_asap(); })?; } @@ -346,6 +361,7 @@ pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| { // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here runner.frame.info.web_info.location.hash = location_hash(); + runner.needs_repaint.repaint_asap(); // tell the user about the new hash })?; */ @@ -369,11 +385,7 @@ pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { .set_attribute("height", height.to_string().as_str()); state .channels - .send_custom(WebRunnerCustomEvent::ScreenResize( - width, - height, - pixel_ratio, - )); + .send_custom(WebRunnerEvent::ScreenResize(width, height, pixel_ratio)); } }; closure(web_sys::Event::new("")?, state); @@ -428,8 +440,12 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { &state.canvas, "mousedown", |event: web_sys::MouseEvent, state| { + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(&state.canvas, &event); + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); let modifiers = modifiers_from_mouse_event(&event); state.channels.send(egui::Event::PointerButton { pos, @@ -454,7 +470,11 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { &state.canvas, "mousemove", |event: web_sys::MouseEvent, state| { - let pos = pos_from_mouse_event(&state.canvas, &event); + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); state.channels.send(egui::Event::PointerMoved(pos)); //runner.needs_repaint.repaint_asap(); event.stop_propagation(); @@ -466,9 +486,12 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { &state.canvas, "mouseup", |event: web_sys::MouseEvent, state| { + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(&state.canvas, &event); - let modifiers = modifiers_from_mouse_event(&event); + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); state.channels.send(egui::Event::PointerButton { pos, button, @@ -494,7 +517,7 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { &state.canvas, "mouseleave", |event: web_sys::MouseEvent, state| { - state.channels.send_custom(WebRunnerCustomEvent::Save); + state.channels.send_custom(WebRunnerEvent::Save); state.channels.send(egui::Event::PointerGone); //runner.needs_repaint.repaint_asap(); @@ -509,10 +532,15 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { |event: web_sys::TouchEvent, state| { let mut inner = state.inner.borrow_mut(); - inner.touch_pos = pos_from_touch_event(&state.canvas, &event, &mut inner.touch_id); + inner.touch_pos = pos_from_touch_event( + &state.canvas, + &event, + &mut inner.touch_id, + state.channels.zoom_factor(), + ); state .channels - .send_custom(WebRunnerCustomEvent::Touch(inner.touch_id, inner.touch_pos)); + .send_custom(WebRunnerEvent::Touch(inner.touch_id, inner.touch_pos)); let modifiers = modifiers_from_touch_event(&event); state.channels.send(egui::Event::PointerButton { pos: inner.touch_pos, @@ -534,10 +562,15 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { |event: web_sys::TouchEvent, state| { let mut inner = state.inner.borrow_mut(); - inner.touch_pos = pos_from_touch_event(&state.canvas, &event, &mut inner.touch_id); + inner.touch_pos = pos_from_touch_event( + &state.canvas, + &event, + &mut inner.touch_id, + state.channels.zoom_factor(), + ); state .channels - .send_custom(WebRunnerCustomEvent::Touch(inner.touch_id, inner.touch_pos)); + .send_custom(WebRunnerEvent::Touch(inner.touch_id, inner.touch_pos)); state .channels .send(egui::Event::PointerMoved(inner.touch_pos)); @@ -609,7 +642,9 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { }); let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => canvas_size_in_points(&state.canvas).y, + egui::MouseWheelUnit::Page => { + canvas_size_in_points(&state.canvas, state.channels.zoom_factor()).y + } egui::MouseWheelUnit::Line => { #[allow(clippy::let_and_return)] let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index d9fe87c1..f3bc9743 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,13 +1,14 @@ -use super::{canvas_element, canvas_origin, AppRunner}; +use super::{canvas_origin, AppRunner}; pub fn pos_from_mouse_event( canvas: &web_sys::HtmlCanvasElement, event: &web_sys::MouseEvent, + zoom_factor: f32, ) -> egui::Pos2 { let rect = canvas.get_bounding_client_rect(); egui::Pos2 { - x: event.client_x() as f32 - rect.left() as f32, - y: event.client_y() as f32 - rect.top() as f32, + x: (event.client_x() as f32 - rect.left() as f32) / zoom_factor, + y: (event.client_y() as f32 - rect.top() as f32) / zoom_factor, } } @@ -32,6 +33,7 @@ pub fn pos_from_touch_event( canvas: &web_sys::HtmlCanvasElement, event: &web_sys::TouchEvent, touch_id_for_pos: &mut Option, + zoom_factor: f32, ) -> egui::Pos2 { let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { // search for the touch we previously used for the position @@ -49,14 +51,18 @@ pub fn pos_from_touch_event( .or_else(|| event.touches().get(0)) .map_or(Default::default(), |touch| { *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_origin(canvas), &touch) + pos_from_touch(canvas_origin(canvas), &touch, zoom_factor) }) } -fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { +fn pos_from_touch( + canvas_origin: egui::Pos2, + touch: &web_sys::Touch, + zoom_factor: f32, +) -> egui::Pos2 { egui::Pos2 { - x: touch.page_x() as f32 - canvas_origin.x, - y: touch.page_y() as f32 - canvas_origin.y, + x: (touch.page_x() as f32 - canvas_origin.x) / zoom_factor, + y: (touch.page_y() as f32 - canvas_origin.y) / zoom_factor, } } @@ -72,7 +78,7 @@ pub fn push_touches( device_id: egui::TouchDeviceId(0), id: egui::TouchId::from(touch.identifier()), phase, - pos: pos_from_touch(canvas_origin, &touch), + pos: pos_from_touch(canvas_origin, &touch, state.channels.zoom_factor()), force: Some(touch.force()), }); } @@ -139,11 +145,11 @@ macro_rules! modifiers { }; } -pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { +pub fn modifiers_from_kb_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { modifiers!(event) } -pub(super) fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers { +pub fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers { modifiers!(event) } diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 5c04166c..9946639a 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -100,14 +100,14 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme { } } -fn canvas_element(canvas_id: &str) -> Option { +fn get_canvas_element_by_id(canvas_id: &str) -> Option { let document = web_sys::window()?.document()?; let canvas = document.get_element_by_id(canvas_id)?; canvas.dyn_into::().ok() } -fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { - canvas_element(canvas_id) +fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { + get_canvas_element_by_id(canvas_id) .unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}")) } @@ -116,8 +116,8 @@ fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 { egui::pos2(rect.left() as f32, rect.top() as f32) } -fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement) -> egui::Vec2 { - let pixels_per_point = native_pixels_per_point(); +fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, zoom_factor: f32) -> egui::Vec2 { + let pixels_per_point = zoom_factor * native_pixels_per_point(); egui::vec2( canvas.width() as f32 / pixels_per_point, canvas.height() as f32 / pixels_per_point, @@ -130,51 +130,46 @@ fn resize_canvas_to_screen_size( ) -> Option<()> { let parent = canvas.parent_element()?; + // In this function we use "pixel" to mean physical pixel, + // and "point" to mean "logical CSS pixel". + let pixels_per_point = native_pixels_per_point(); + // Prefer the client width and height so that if the parent // element is resized that the egui canvas resizes appropriately. - let width = parent.client_width(); - let height = parent.client_height(); - - let canvas_real_size = Vec2 { - x: width as f32, - y: height as f32, + let parent_size_points = Vec2 { + x: parent.client_width() as f32, + y: parent.client_height() as f32, }; - if width <= 0 || height <= 0 { - log::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height); + if parent_size_points.x <= 0.0 || parent_size_points.y <= 0.0 { + log::error!("The parent element of the egui canvas is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", parent_size_points.x, parent_size_points.y); } - let pixels_per_point = native_pixels_per_point(); - - let max_size_pixels = pixels_per_point * max_size_points; + // We take great care here to ensure the rendered canvas aligns + // perfectly to the physical pixel grid, lest we get blurry text. + // At the time of writing, we get pixel perfection on Chromium and Firefox on Mac, + // but Desktop Safari will be blurry on most zoom levels. + // See https://github.com/emilk/egui/issues/4241 for more. - let canvas_size_pixels = pixels_per_point * canvas_real_size; - let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels); - let canvas_size_points = canvas_size_pixels / pixels_per_point; + let canvas_size_pixels = pixels_per_point * parent_size_points.min(max_size_points); - // Make sure that the height and width are always even numbers. + // Make sure that the size is always an even number of pixels, // otherwise, the page renders blurry on some platforms. // See https://github.com/emilk/egui/issues/103 - fn round_to_even(v: f32) -> f32 { - (v / 2.0).round() * 2.0 - } + let canvas_size_pixels = (canvas_size_pixels / 2.0).round() * 2.0; + + let canvas_size_points = canvas_size_pixels / pixels_per_point; canvas .style() - .set_property( - "width", - &format!("{}px", round_to_even(canvas_size_points.x)), - ) + .set_property("width", &format!("{}px", canvas_size_points.x)) .ok()?; canvas .style() - .set_property( - "height", - &format!("{}px", round_to_even(canvas_size_points.y)), - ) + .set_property("height", &format!("{}px", canvas_size_points.y)) .ok()?; - canvas.set_width(round_to_even(canvas_size_pixels.x) as u32); - canvas.set_height(round_to_even(canvas_size_pixels.y) as u32); + canvas.set_width(canvas_size_pixels.x as u32); + canvas.set_height(canvas_size_pixels.y as u32); Some(()) } @@ -282,6 +277,13 @@ pub fn percent_decode(s: &str) -> String { // ---------------------------------------------------------------------------- +// ensure that AtomicF32 and AtomicF64 is using atomic ops (otherwise it would use global locks, and that would be bad) +const _: [(); 0 - !{ + const ASSERT: bool = portable_atomic::AtomicF32::is_always_lock_free() + && portable_atomic::AtomicF64::is_always_lock_free(); + ASSERT +} as usize] = []; + /// Options and state that will be sent to the web worker part of the web runner. #[derive(Clone)] pub struct WorkerOptions { @@ -298,11 +300,11 @@ pub struct WorkerOptions { #[derive(Clone)] pub struct WorkerChannels { /// The receiver used to receive egui events from the main thread. - event_rx: flume::Receiver, - /// The receiver used to receive custom events from the main thread. - custom_event_rx: flume::Receiver, + event_rx: flume::Receiver, /// The sender used to send outputs to the main thread. output_tx: flume::Sender, + /// This should be set to the app's current zoom factor every frame. + zoom_tx: std::sync::Arc, } impl WorkerChannels { @@ -345,11 +347,11 @@ pub struct MainStateInner { #[derive(Clone)] pub struct MainChannels { /// The sender used to send egui events to the worker thread. - event_tx: flume::Sender, - /// The sender used to send custom events to the worker thread. - custom_event_tx: flume::Sender, + event_tx: flume::Sender, /// The receiver used to receive outputs from the worker thread. output_rx: flume::Receiver, + /// This is set to the app's current zoom factor every frame. + zoom_rx: std::sync::Arc, } impl MainState { @@ -393,36 +395,43 @@ impl MainState { impl MainChannels { /// Send an egui event to the worker thread. fn send(&self, event: egui::Event) { - let _ = self.event_tx.send(event); + let _ = self.event_tx.send(WebRunnerEvent::EguiEvent(event)); } /// Send a custom event to the worker thread. - fn send_custom(&self, event: WebRunnerCustomEvent) { - let _ = self.custom_event_tx.send(event); + fn send_custom(&self, event: WebRunnerEvent) { + let _ = self.event_tx.send(event); + } + + /// Get the egui app's current zoom factor from the worker thread. + fn zoom_factor(&self) -> f32 { + self.zoom_rx.load(portable_atomic::Ordering::Relaxed) } } /// Create a new connected `(WorkerChannels, MainChannels)` pair for initializing a web runner. pub fn channels() -> (WorkerChannels, MainChannels) { let (event_tx, event_rx) = flume::unbounded(); - let (custom_event_tx, custom_event_rx) = flume::unbounded(); let (output_tx, output_rx) = flume::unbounded(); + let zoom_arc = std::sync::Arc::new(portable_atomic::AtomicF32::new(1.)); ( WorkerChannels { event_rx, - custom_event_rx, output_tx, + zoom_tx: zoom_arc.clone(), }, MainChannels { event_tx, - custom_event_tx, output_rx, + zoom_rx: zoom_arc, }, ) } /// A custom event that can be sent from the main thread to the worker thread. -enum WebRunnerCustomEvent { +enum WebRunnerEvent { + /// Misc egui events + EguiEvent(egui::Event), /// (window.innerWidth, window.innerHeight, window.devicePixelRatio) ScreenResize(u32, u32, f32), /// This should be sent whenever the modifiers change @@ -431,6 +440,9 @@ enum WebRunnerCustomEvent { Save, /// The browser detected a touchstart or touchmove event with this ID and position in canvas coordinates Touch(Option, egui::Pos2), + /// This should be sent whenever the web page gains or loses focus (true when focus is gained, + /// false when focus is lost) + Focus(bool), } /// A custom output that can be sent from the worker thread to the main thread. @@ -446,5 +458,5 @@ enum WebRunnerOutput { static PANIC_LOCK: once_cell::sync::OnceCell<()> = once_cell::sync::OnceCell::new(); thread_local! { - static EVENTS_TO_UNSUBSCRIBE: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); + static EVENTS_TO_UNSUBSCRIBE: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; } diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 8cb0c9ef..a6a05ac9 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc}; use wasm_bindgen::prelude::*; -use super::{canvas_element, AppRunner, WebRunner}; +use super::{AppRunner, WebRunner}; static AGENT_ID: &str = "egui_text_agent"; @@ -229,8 +229,7 @@ pub fn move_text_cursor( ime: Option, canvas: &web_sys::HtmlCanvasElement, ) -> Option<()> { - let input = text_agent(); - let style = input.style(); + let style = text_agent().style(); // Note: moving agent on mobile devices will lead to unpredictable scroll. if is_mobile() == Some(false) { ime.as_ref().and_then(|ime| { diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index 9050a5c8..22c59eec 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -9,6 +9,9 @@ pub(crate) trait WebPainter { // where // Self: Sized; + /// Reference to the canvas in use. + fn canvas(&self) -> &web_sys::OffscreenCanvas; + /// Maximum size of a texture in one direction. fn max_texture_side(&self) -> usize; diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index cd627586..b54f6f64 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -10,7 +10,6 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, - canvas_id: String, painter: egui_glow::Painter, } @@ -20,7 +19,7 @@ impl WebPainterGlow { } pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { - let canvas = super::canvas_element_or_die(canvas_id); + let canvas = super::get_canvas_element_by_id_or_die(canvas_id); let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; @@ -30,11 +29,7 @@ impl WebPainterGlow { let painter = egui_glow::Painter::new(gl, shader_prefix, None) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { - canvas, - canvas_id: canvas_id.to_owned(), - painter, - }) + Ok(Self { canvas, painter }) } } @@ -43,8 +38,8 @@ impl WebPainter for WebPainterGlow { self.painter.max_texture_side() } - fn canvas_id(&self) -> &str { - &self.canvas_id + fn canvas(&self) -> &HtmlCanvasElement { + &self.canvas } fn paint_and_update_textures( diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 39a24357..07f7ae2a 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -216,6 +216,10 @@ impl WebPainterWgpu { } impl WebPainter for WebPainterWgpu { + fn canvas(&self) -> &web_sys::OffscreenCanvas { + &self.canvas + } + fn max_texture_side(&self) -> usize { self.render_state.as_ref().map_or(0, |state| { state.device.limits().max_texture_dimension_2d as _ diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 7dd76435..3ac3560b 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,7 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; use wasm_bindgen::prelude::*; @@ -24,6 +27,9 @@ pub struct WebRunner { /// They have to be in a separate `Rc` so that we don't need to pass them to /// the panic handler, since they aren't `Send`. events_to_unsubscribe: Rc>>, + + /// Used in `destroy` to cancel a pending frame. + request_animation_frame_id: Cell>, } impl WebRunner { @@ -41,6 +47,7 @@ impl WebRunner { panic_handler, runner: Rc::new(RefCell::new(None)), events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), + request_animation_frame_id: Cell::new(None), } } @@ -127,7 +134,7 @@ impl WebRunner { self.runner.replace(Some(runner)); { - events::request_animation_frame(self.clone())?; + self.request_animation_frame()?; } Ok(()) @@ -164,6 +171,11 @@ impl WebRunner { pub fn destroy(&self) { self.unsubscribe_from_all_events(); + if let Some(id) = self.request_animation_frame_id.get() { + let window = web_sys::window().unwrap(); + window.cancel_animation_frame(id).ok(); + } + if let Some(runner) = self.runner.replace(None) { runner.destroy(); } @@ -235,6 +247,18 @@ impl WebRunner { Ok(()) } + + pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> { + let worker = luminol_web::bindings::worker().unwrap(); + let closure = Closure::once({ + let runner_ref = self.clone(); + move || events::paint_and_schedule(&runner_ref) + }); + let id = worker.request_animation_frame(closure.as_ref().unchecked_ref())?; + self.request_animation_frame_id.set(Some(id)); + closure.forget(); // We must forget it, or else the callback is canceled on drop + Ok(()) + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 56cecde1..48aa5cdb 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,18 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Nothing new + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* Improve panic message in egui-wgpu when failing to create buffers [#3986](https://github.com/emilk/egui/pull/3986) + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/egui-wgpu/README.md b/crates/egui-wgpu/README.md index 36b90d6d..d3aed846 100644 --- a/crates/egui-wgpu/README.md +++ b/crates/egui-wgpu/README.md @@ -1,5 +1,5 @@ > [!IMPORTANT] -> luminol-egui-wgpu is currently based on emilk/egui@0.26.2 +> luminol-egui-wgpu is currently based on emilk/egui@0.27.2 > [!NOTE] > This is Luminol's modified version of egui-wgpu. The original version is dual-licensed under MIT and Apache 2.0. diff --git a/src/app/mod.rs b/src/app/mod.rs index 2149586c..74c52e65 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -334,7 +334,7 @@ impl luminol_eframe::App for App { // If a file/folder picker is open, prevent the user from interacting with the application // with the mouse. if update_state.project_manager.is_picker_open() { - egui::Area::new("luminol_picker_overlay").show(ctx, |ui| { + egui::Area::new("luminol_picker_overlay".into()).show(ctx, |ui| { ui.allocate_response( ui.ctx().input(|i| i.screen_rect.size()), egui::Sense::click_and_drag(),