1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 21:30:03 -04:00

Merge branch 'main' into theme_plugin

# Conflicts:
#	crates/egui/src/widget_style.rs
This commit is contained in:
Lucas Meurer
2026-08-21 11:33:30 +02:00
252 changed files with 4993 additions and 1936 deletions

View File

@@ -7,6 +7,24 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.36.1 - 2026-08-07
Nothing new
## 0.36.0 - 2026-08-05
### 🔧 Changed
* Improve robustness of text input handling for `eframe/web` [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho)
* Eframe: make webbrowser dependency optional [#8372](https://github.com/emilk/egui/pull/8372) by [@wyvernbw](https://github.com/wyvernbw)
* Store `web_sys::File` inside of `DroppedFile` [#8354](https://github.com/emilk/egui/pull/8354) by [@grtlr](https://github.com/grtlr)
### 🐛 Fixed
* Web: don't scroll host page when text agent or canvas grabs focus [#8296](https://github.com/emilk/egui/pull/8296) by [@emilk](https://github.com/emilk)
* Fix missing modifier events on eframe web, handle physical keys [#8345](https://github.com/emilk/egui/pull/8345) by [@lucasmerlin](https://github.com/lucasmerlin)
* Web: Avoid panic from lost texture updates when loaded on a background tab [#8313](https://github.com/emilk/egui/pull/8313) by [@kevinmehall](https://github.com/kevinmehall)
* Web: anchor the text agent to the canvas [#8297](https://github.com/emilk/egui/pull/8297) by [@emilk](https://github.com/emilk)
* Never run an egui pass when nothing will be shown [#8387](https://github.com/emilk/egui/pull/8387) by [@emilk](https://github.com/emilk)
## 0.35.0 - 2026-06-25
### ⭐ Added
* Add Context::set_cursor_image for OS-level custom cursors [#8155](https://github.com/emilk/egui/pull/8155) by [@all3f0r1](https://github.com/all3f0r1)

View File

@@ -28,6 +28,7 @@ workspace = true
default = [
"accesskit",
"default_fonts",
"links",
"wayland", # Required for Linux support (including CI!)
"web_screen_reader",
"wgpu",
@@ -66,7 +67,7 @@ experimental = ["egui/experimental"]
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
persistence = ["egui-winit/serde", "egui/persistence", "ron", "serde"]
## Enables wayland support and fixes clipboard issue.
##
@@ -128,6 +129,9 @@ __screenshot = []
## and capture screenshots. Off unless the env var is set; no-op on wasm.
inspection = ["dep:egui_inspection", "accesskit"]
## Enables the `links` feature on `egui-winit`, allowing for links to open in browser.
links = ["egui-winit/links"]
[dependencies]
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
@@ -151,7 +155,7 @@ serde = { workspace = true, optional = true }
# -------------------------------------------
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
egui-winit = { workspace = true, default-features = false, features = ["clipboard"] }
image = { workspace = true, features = ["png"] } # Needed for app icon
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
@@ -167,7 +171,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
"egl",
"wgl",
] }
home = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -212,7 +215,6 @@ image = { workspace = true, features = ["png"] } # For copying images
js-sys.workspace = true
percent-encoding.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
web-sys = { workspace = true, features = [
"AddEventListenerOptions",
"BinaryType",

View File

@@ -7,7 +7,7 @@
#![warn(missing_docs)] // Let's keep `epi` well-documented.
#[cfg(target_arch = "wasm32")]
use std::any::Any;
use core::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
@@ -41,7 +41,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
type DynError = Box<dyn std::error::Error + Send + Sync>;
type DynError = Box<dyn core::error::Error + Send + Sync>;
/// This is how your app is created.
///
@@ -73,7 +73,7 @@ pub struct CreationContext<'s> {
/// The `get_proc_address` wrapper of underlying GL context
#[cfg(feature = "glow")]
pub get_proc_address:
Option<std::sync::Arc<dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void + Send + Sync>>,
Option<std::sync::Arc<dyn Fn(&core::ffi::CStr) -> *const core::ffi::c_void + Send + Sync>>,
/// The underlying WGPU render state.
///
@@ -155,6 +155,12 @@ pub trait App {
///
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
///
/// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is
/// disturbed), and calls this via [`egui::Context::run_logic`] instead.
/// You can then still tell that the window is hidden with
/// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`]
/// (events, time, …) is that of the last shown frame.
///
/// The [`egui::Context`] can be cloned and saved if you like.
///
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
@@ -225,8 +231,8 @@ pub trait App {
// Settings:
/// Time between automatic calls to [`Self::save`]
fn auto_save_interval(&self) -> std::time::Duration {
std::time::Duration::from_secs(30)
fn auto_save_interval(&self) -> core::time::Duration {
core::time::Duration::from_secs(30)
}
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
@@ -615,8 +621,8 @@ impl Default for Renderer {
}
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for Renderer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#[cfg(feature = "glow")]
Self::Glow => "glow".fmt(f),
@@ -628,7 +634,7 @@ impl std::fmt::Display for Renderer {
}
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::str::FromStr for Renderer {
impl core::str::FromStr for Renderer {
type Err = String;
fn from_str(name: &str) -> Result<Self, String> {

View File

@@ -503,7 +503,7 @@ pub fn run_ui_native(
#[derive(Debug)]
pub enum Error {
/// Something went wrong in user code when creating the app.
AppCreation(Box<dyn std::error::Error + Send + Sync>),
AppCreation(Box<dyn core::error::Error + Send + Sync>),
/// An error from [`winit`].
#[cfg(not(target_arch = "wasm32"))]
@@ -519,7 +519,7 @@ pub enum Error {
/// An error from [`glutin`] when using [`glow`].
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn core::error::Error>),
/// An error from [`glutin`] when using [`glow`].
#[cfg(feature = "glow")]
@@ -530,7 +530,7 @@ pub enum Error {
Wgpu(egui_wgpu::WgpuError),
}
impl std::error::Error for Error {}
impl core::error::Error for Error {}
#[cfg(not(target_arch = "wasm32"))]
impl From<winit::error::OsError> for Error {
@@ -572,8 +572,8 @@ impl From<egui_wgpu::WgpuError> for Error {
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::AppCreation(err) => write!(f, "app creation error: {err}"),
@@ -614,4 +614,4 @@ impl std::fmt::Display for Error {
}
/// Short for `Result<T, eframe::Error>`.
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
pub type Result<T = (), E = Error> = core::result::Result<T, E>;

View File

@@ -123,7 +123,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
)
.is_err()
{
return std::ptr::null_mut();
return core::ptr::null_mut();
}
// SAFETY: Creating an HICON which should be readonly on our data.
@@ -161,16 +161,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
if icon_big.is_null() {
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad!
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
}
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
}
{
@@ -180,16 +180,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
if icon_small.is_null() {
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
return AppIconStatus::NotSetIgnored;
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
}
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
}

View File

@@ -83,7 +83,7 @@ pub fn viewport_builder(
}
}
match std::mem::take(&mut native_options.window_builder) {
match core::mem::take(&mut native_options.window_builder) {
Some(hook) => hook(viewport_builder),
None => viewport_builder,
}
@@ -156,6 +156,11 @@ pub struct EpiIntegration {
pub beginning: Instant,
is_first_frame: bool,
pub egui_ctx: egui::Context,
/// Input that we have received, but not yet given to egui,
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
pending_raw_input: egui::RawInput,
pending_full_output: egui::FullOutput,
/// When set, it is time to close the native window.
@@ -215,6 +220,7 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: Instant::now(),
pending_raw_input: Default::default(),
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
@@ -262,57 +268,109 @@ impl EpiIntegration {
/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
/// If `viewport_ui_cb` is None, we are in the root viewport and will call
/// [`crate::App::logic`] and [`crate::App::ui`].
///
/// Only call this when the ui will actually be shown;
/// use [`Self::update_logic_only`] otherwise.
pub fn update(
&mut self,
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
is_visible: bool,
raw_input: egui::RawInput,
) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
let raw_input = self.prepare_raw_input(app, raw_input);
let close_requested = raw_input.viewport().close_requested();
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let is_root_viewport = viewport_ui_cb.is_none();
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
if is_visible {
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
} else {
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}
if is_visible {
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
}
});
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport && close_requested {
let canceled = full_output.viewport_output[&ViewportId::ROOT]
.commands
.contains(&egui::ViewportCommand::CancelClose);
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
self.handle_close_request(canceled);
}
self.pending_full_output.append(full_output);
std::mem::take(&mut self.pending_full_output)
core::mem::take(&mut self.pending_full_output)
}
/// Let the app tick its logic without showing any ui,
/// because the window is minimized or occluded.
///
/// No egui pass is run, so all ui state is left untouched:
/// the app will find everything where it left it once the window is visible again.
///
/// Only call this for the root viewport: only it has [`crate::App::logic`].
pub fn update_logic_only(
&mut self,
app: &mut dyn epi::App,
raw_input: egui::RawInput,
) -> egui::LogicOutput {
let raw_input = self.prepare_raw_input(app, raw_input);
let close_requested = raw_input.viewport().close_requested();
let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
profiling::scope!("App::logic");
app.logic(ctx, &mut self.frame);
});
// No pass consumed the input, so save it for the next one:
self.pending_raw_input = raw_input;
if close_requested {
let canceled = logic_output
.viewport_commands
.get(&ViewportId::ROOT)
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
self.handle_close_request(canceled);
}
logic_output
}
/// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook.
fn prepare_raw_input(
&mut self,
app: &mut dyn epi::App,
new_input: egui::RawInput,
) -> egui::RawInput {
let mut raw_input = core::mem::take(&mut self.pending_raw_input);
raw_input.append(new_input); // The new input wins where they overlap
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
raw_input
}
fn handle_close_request(&mut self, canceled: bool) {
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
}
pub fn report_frame_time(&mut self, seconds: f32) {
@@ -321,7 +379,7 @@ impl EpiIntegration {
pub fn post_rendering(&mut self, window: &winit::window::Window) {
profiling::function_scope!();
if std::mem::take(&mut self.is_first_frame) {
if core::mem::take(&mut self.is_first_frame) {
// We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
window.set_visible(true);
}

View File

@@ -1,4 +1,4 @@
use std::cell::Cell;
use core::cell::Cell;
use winit::event_loop::ActiveEventLoop;
thread_local! {
@@ -14,7 +14,7 @@ impl EventLoopGuard {
cell.get().is_none(),
"Attempted to set a new event loop while one is already set"
);
cell.set(Some(std::ptr::from_ref::<ActiveEventLoop>(event_loop)));
cell.set(Some(core::ptr::from_ref::<ActiveEventLoop>(event_loop)));
});
Self
}

View File

@@ -21,7 +21,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
OS::Nix => var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| home::home_dir().map(|p| p.join(".local").join("share")))
.or_else(|| std::env::home_dir().map(|p| p.join(".local").join("share")))
.map(|p| {
p.join(
app_id
@@ -29,7 +29,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
.replace(|c: char| c.is_ascii_whitespace(), ""),
)
}),
OS::Mac => home::home_dir().map(|p| {
OS::Mac => std::env::home_dir().map(|p| {
p.join("Library")
.join("Application Support")
.join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-"))
@@ -44,10 +44,10 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use core::ptr;
use core::slice;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt as _;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
@@ -66,8 +66,8 @@ fn roaming_appdata() -> Option<PathBuf> {
SHGetKnownFolderPath(
&FOLDERID_RoamingAppData,
KF_FLAG_DONT_VERIFY as u32,
std::ptr::null_mut(),
&mut path_raw,
core::ptr::null_mut(),
&raw mut path_raw,
)
};

View File

@@ -8,7 +8,8 @@
#![expect(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::unwrap_used)]
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use core::{cell::RefCell, num::NonZeroU32};
use std::{rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
@@ -31,16 +32,17 @@ use egui::{
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use log::warn;
use super::{
epi_integration, event_loop_context,
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
use crate::epaint::textures::TexturesDelta;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
};
// ----------------------------------------------------------------------------
// Types:
@@ -73,6 +75,16 @@ struct GlowWinitRunning<'app> {
// NOTE: one painter shared by all viewports.
painter: Rc<RefCell<egui_glow::Painter>>,
/// Any not yet applied deltas for this app.
pending_deltas: TexturesDelta,
}
impl Drop for GlowWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// This struct will contain both persistent and temporary glutin state.
@@ -114,6 +126,9 @@ struct Viewport {
info: ViewportInfo,
actions_requested: Vec<egui_winit::ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// The user-callback that shows the ui.
/// None for immediate viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -125,6 +140,34 @@ struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Viewport {
/// Apply the commands, or defer them until we have a window.
fn process_commands(
&mut self,
egui_ctx: &egui::Context,
mut commands: Vec<egui::ViewportCommand>,
) {
self.deferred_commands.append(&mut commands);
if let Some(window) = &self.window {
egui_winit::process_viewport_commands(
egui_ctx,
&mut self.info,
core::mem::take(&mut self.deferred_commands),
window,
&mut self.actions_requested,
);
}
}
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> GlowWinitApp<'app> {
@@ -296,7 +339,7 @@ impl<'app> GlowWinitApp<'app> {
log::warn!("set_cursor_hittest(false) failed: {err}");
}
let app_creator = std::mem::take(&mut self.app_creator)
let app_creator = core::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone()));
@@ -353,6 +396,7 @@ impl<'app> GlowWinitApp<'app> {
app,
glutin,
painter,
pending_deltas: Default::default(),
}))
}
}
@@ -557,7 +601,7 @@ impl GlowWinitRunning<'_> {
}
}
let (raw_input, viewport_ui_cb, is_visible, run_ui) = {
let (raw_input, viewport_ui_cb, is_visible, show_ui) = {
let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
@@ -576,7 +620,7 @@ impl GlowWinitRunning<'_> {
let mut raw_input = egui_winit.take_egui_input(window);
let viewport_ui_cb = viewport.viewport_ui_cb.clone();
let run_ui =
let show_ui =
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);
self.integration.pre_update();
@@ -588,9 +632,58 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.collect();
(raw_input, viewport_ui_cb, is_visible, run_ui)
(raw_input, viewport_ui_cb, is_visible, show_ui)
};
if !show_ui {
// Nothing will be shown, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when this viewport becomes visible again.
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport {
// The app logic keeps ticking, so it can e.g. ask to be shown again:
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self
.integration
.update_logic_only(self.app.as_mut(), raw_input);
let mut glutin = self.glutin.borrow_mut();
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
viewport.info.events.clear(); // they should have been processed
if let Some(window) = viewport.window.clone()
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
{
egui_winit.handle_platform_output_with_event_loop(
&window,
event_loop,
platform_output,
);
}
}
for (id, commands) in viewport_commands {
if let Some(viewport) = glutin.viewports.get_mut(&id) {
viewport.process_commands(&self.integration.egui_ctx, commands);
}
}
}
sleep_if_invisible_or_minimized(
self.glutin
.borrow()
.viewports
.get(&viewport_id)
.and_then(|viewport| viewport.window.as_deref()),
);
return Ok(if self.integration.should_close() {
EventResult::CloseRequested
} else {
EventResult::Wait
});
}
// HACK: In order to get the right clear_color, the system theme needs to be set, which
// usually only happens in the `update` call. So we call Options::begin_pass early
// to set the right theme. Without this there would be a black flash on the first frame.
@@ -639,12 +732,9 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
let full_output = self.integration.update(
self.app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
run_ui,
);
let full_output =
self.integration
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
// ------------------------------------------------------------
@@ -653,6 +743,7 @@ impl GlowWinitRunning<'_> {
app,
glutin,
painter,
pending_deltas,
..
} = self;
@@ -666,6 +757,7 @@ impl GlowWinitRunning<'_> {
pixels_per_point,
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
glutin.remove_viewports_not_in(&viewport_output);
@@ -687,30 +779,28 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
// Upload textures even when not visible: the atlas dirty region is already
// consumed, so dropping the delta would desync the font texture.
let has_texture_updates = !textures_delta.set.is_empty() || !textures_delta.free.is_empty();
if is_visible || has_texture_updates {
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
for (id, image_delta) in &textures_delta.set {
painter.set_texture(*id, image_delta);
}
if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_primitives(screen_size_in_pixels, pixels_per_point, &clipped_primitives);
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
pending_deltas,
);
{
for action in viewport.actions_requested.drain(..) {
@@ -772,25 +862,13 @@ impl GlowWinitRunning<'_> {
}
}
// Free textures *after* painting, since they may still be used in the frame we just drew.
for id in &textures_delta.free {
painter.free_texture(*id);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
integration.maybe_autosave(app.as_mut(), Some(&window));
if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
sleep_if_invisible_or_minimized(Some(&window));
if integration.should_close() {
Ok(EventResult::CloseRequested)
@@ -1120,6 +1198,7 @@ impl GlutinWindowContext {
deferred_commands: vec![],
info: viewport_info,
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb: None,
gl_surface: None,
window: window.map(Arc::new),
@@ -1330,7 +1409,7 @@ impl GlutinWindowContext {
}
}
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
fn get_proc_address(&self, addr: &core::ffi::CStr) -> *const core::ffi::c_void {
self.gl_config.display().get_proc_address(addr)
}
@@ -1362,7 +1441,7 @@ impl GlutinWindowContext {
class,
builder,
viewport_ui_cb,
mut commands,
commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
@@ -1377,25 +1456,18 @@ impl GlutinWindowContext {
viewport_ui_cb,
);
if let Some(window) = &viewport.window {
let old_inner_size = window.inner_size();
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
viewport.deferred_commands.append(&mut commands);
viewport.process_commands(egui_ctx, commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
std::mem::take(&mut viewport.deferred_commands),
window,
&mut viewport.actions_requested,
);
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux")
&& let Some(window) = &viewport.window
&& let Some(old_inner_size) = old_inner_size
{
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
}
}
@@ -1436,6 +1508,7 @@ fn initialize_or_update_viewport(
deferred_commands: vec![],
info: Default::default(),
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb,
window: None,
egui_winit: None,
@@ -1584,8 +1657,10 @@ fn render_immediate_viewport(
} = &mut *glutin;
let Some(viewport) = viewports.get_mut(&viewport_id) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
@@ -1621,7 +1696,7 @@ fn render_immediate_viewport(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
);
{
@@ -1645,7 +1720,7 @@ fn save_screenshot_and_exit(
screen_size_in_pixels: [u32; 2],
) {
assert!(
path.ends_with(".png"),
egui::load::has_extension(path, "png"),
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
);
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);

View File

@@ -1,4 +1,5 @@
use std::time::{Duration, Instant};
use core::time::Duration;
use std::time::Instant;
use winit::{
application::ApplicationHandler,
@@ -41,7 +42,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoo
))
})?);
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
if let Some(hook) = core::mem::take(&mut native_options.event_loop_builder) {
hook(&mut builder);
}
@@ -58,7 +59,7 @@ fn with_event_loop<R>(
mut native_options: epi::NativeOptions,
f: impl FnOnce(&mut EventLoop<UserEvent>, epi::NativeOptions) -> R,
) -> Result<R> {
thread_local!(static EVENT_LOOP: std::cell::RefCell<Option<EventLoop<UserEvent>>> = const { std::cell::RefCell::new(None) });
thread_local!(static EVENT_LOOP: core::cell::RefCell<Option<EventLoop<UserEvent>>> = const { core::cell::RefCell::new(None) });
EVENT_LOOP.with(|event_loop| {
// Since we want to reference NativeOptions when creating the EventLoop we can't
@@ -206,7 +207,12 @@ impl<T: WinitApp> WinitAppWrapper<T> {
invisible_window_ids.push(*window_id);
} else {
log::trace!("request_redraw for {window_id:?}");
event_loop.set_control_flow(ControlFlow::Poll);
// Don't switch to `ControlFlow::Poll` here. `request_redraw`
// is enough to wake the event loop, and on Wayland the
// `RedrawRequested` event is only delivered once the
// compositor sends a frame callback. Polling in the meantime
// busy-loops a whole CPU core.
// See https://github.com/emilk/egui/issues/8326.
window.request_redraw();
}
} else {
@@ -236,10 +242,16 @@ impl<T: WinitApp> WinitAppWrapper<T> {
}
}
// Always set an explicit, sleeping control flow. Previously we only set
// `WaitUntil` when a repaint was already scheduled, which meant that a
// `ControlFlow::Poll` set earlier was never undone once the last timed
// repaint had been consumed, leaving the loop spinning.
// See https://github.com/emilk/egui/issues/8326.
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
}
event_loop.set_control_flow(match next_repaint_time {
Some(next_repaint_time) => ControlFlow::WaitUntil(next_repaint_time),
None => ControlFlow::Wait,
});
}
}
@@ -550,7 +562,7 @@ impl<'a> EframeWinitApplication<'a> {
pub fn pump_eframe_app(
&mut self,
event_loop: &mut EventLoop<UserEvent>,
timeout: Option<std::time::Duration>,
timeout: Option<core::time::Duration>,
) -> EframePumpStatus {
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};

View File

@@ -5,7 +5,8 @@
//! There is a bunch of improvements we could do,
//! like removing a bunch of `unwraps`.
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use core::{cell::RefCell, num::NonZeroU32};
use std::{rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use parking_lot::Mutex;
@@ -17,19 +18,20 @@ use winit::{
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta,
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use log::warn;
use winit_integration::UserEvent;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{
epi_integration::EpiIntegration,
winit_integration::{EventResult, is_invisible_or_minimized},
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
},
};
@@ -65,6 +67,15 @@ struct WgpuWinitRunning<'app> {
/// Wrapped in an `Rc<RefCell<…>>` so it can be re-entrantly shared via a weak-pointer.
shared: Rc<RefCell<SharedState>>,
pending_deltas: TexturesDelta,
}
impl Drop for WgpuWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// Everything needed by the immediate viewport renderer.\
@@ -91,6 +102,9 @@ pub struct Viewport {
info: ViewportInfo,
actions_requested: Vec<ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// `None` for sync viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -102,6 +116,13 @@ pub struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> WgpuWinitApp<'app> {
@@ -289,7 +310,7 @@ impl<'app> WgpuWinitApp<'app> {
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
}
let app_creator = std::mem::take(&mut self.app_creator)
let app_creator = core::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone()));
@@ -328,6 +349,7 @@ impl<'app> WgpuWinitApp<'app> {
viewport_ui_cb: None,
window: Some(window),
egui_winit: Some(egui_winit),
pending_delta: Default::default(),
},
);
@@ -358,6 +380,7 @@ impl<'app> WgpuWinitApp<'app> {
integration,
app,
shared,
pending_deltas: Default::default(),
}))
}
}
@@ -596,12 +619,13 @@ impl WgpuWinitRunning<'_> {
app,
integration,
shared,
pending_deltas,
} = self;
let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start();
let (viewport_ui_cb, raw_input, is_visible, run_ui) = {
let (viewport_ui_cb, raw_input, is_visible, show_ui) = {
profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut();
@@ -657,7 +681,7 @@ impl WgpuWinitRunning<'_> {
};
let mut raw_input = egui_winit.take_egui_input(window);
let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
integration.pre_update();
@@ -669,15 +693,67 @@ impl WgpuWinitRunning<'_> {
painter.handle_screenshots(&mut raw_input.events);
(viewport_ui_cb, raw_input, is_visible, run_ui)
(viewport_ui_cb, raw_input, is_visible, show_ui)
};
if !show_ui {
// Nothing will be shown, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when this viewport becomes visible again.
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport {
// The app logic keeps ticking, so it can e.g. ask to be shown again:
let egui::LogicOutput {
platform_output,
viewport_commands,
} = integration.update_logic_only(app.as_mut(), raw_input);
let mut shared_mut = shared.borrow_mut();
let SharedState { viewports, .. } = &mut *shared_mut;
if let Some(viewport) = viewports.get_mut(&viewport_id) {
viewport.info.events.clear(); // they should have been processed
if let Viewport {
window: Some(window),
egui_winit: Some(egui_winit),
..
} = viewport
{
egui_winit.handle_platform_output_with_event_loop(
window,
event_loop,
platform_output,
);
}
}
for (id, commands) in viewport_commands {
if let Some(viewport) = viewports.get_mut(&id) {
viewport.process_commands(&integration.egui_ctx, commands);
}
}
}
sleep_if_invisible_or_minimized(
shared
.borrow()
.viewports
.get(&viewport_id)
.and_then(|viewport| viewport.window.as_deref()),
);
return Ok(if integration.should_close() {
EventResult::CloseRequested
} else {
EventResult::Wait
});
}
// ------------------------------------------------------------
// Runs the update, which could call immediate viewports,
// so make sure we hold no locks here!
let full_output =
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui);
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
// ------------------------------------------------------------
@@ -699,6 +775,8 @@ impl WgpuWinitRunning<'_> {
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
let Some(viewport) = viewports.get_mut(&viewport_id) else {
@@ -735,7 +813,7 @@ impl WgpuWinitRunning<'_> {
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
pending_deltas,
screenshot_commands,
window,
);
@@ -796,16 +874,7 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
&& is_invisible_or_minimized(window)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
sleep_if_invisible_or_minimized(window.map(|window| window.as_ref()));
if integration.should_close() {
Ok(EventResult::CloseRequested)
@@ -960,6 +1029,25 @@ impl WgpuWinitRunning<'_> {
}
impl Viewport {
/// Apply the commands, or defer them until we have a window.
fn process_commands(
&mut self,
egui_ctx: &egui::Context,
mut commands: Vec<egui::ViewportCommand>,
) {
self.deferred_commands.append(&mut commands);
if let Some(window) = self.window.as_ref() {
egui_winit::process_viewport_commands(
egui_ctx,
&mut self.info,
core::mem::take(&mut self.deferred_commands),
window,
&mut self.actions_requested,
);
}
}
/// Create winit window, if needed.
fn initialize_window(
&mut self,
@@ -1125,8 +1213,11 @@ fn render_immediate_viewport(
} = &mut *shared_mut;
let Some(viewport) = viewports.get_mut(&ids.this) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
return;
@@ -1149,7 +1240,7 @@ fn render_immediate_viewport(
pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
vec![],
window,
);
@@ -1194,7 +1285,7 @@ fn handle_viewport_output(
class,
builder,
viewport_ui_cb,
mut commands,
commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
@@ -1204,30 +1295,23 @@ fn handle_viewport_output(
let viewport =
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
if let Some(window) = viewport.window.as_ref() {
let old_inner_size = window.inner_size();
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
viewport.deferred_commands.append(&mut commands);
viewport.process_commands(egui_ctx, commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
std::mem::take(&mut viewport.deferred_commands),
window,
&mut viewport.actions_requested,
);
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size
&& let (Some(width), Some(height)) = (
NonZeroU32::new(new_inner_size.width),
NonZeroU32::new(new_inner_size.height),
)
{
painter.on_window_resized(viewport_id, width, height);
}
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux")
&& let Some(window) = viewport.window.as_ref()
&& let Some(old_inner_size) = old_inner_size
{
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size
&& let (Some(width), Some(height)) = (
NonZeroU32::new(new_inner_size.width),
NonZeroU32::new(new_inner_size.height),
)
{
painter.on_window_resized(viewport_id, width, height);
}
}
}
@@ -1268,6 +1352,7 @@ fn initialize_or_update_viewport<'a>(
viewport_ui_cb,
window: None,
egui_winit: None,
pending_delta: Default::default(),
})
}

View File

@@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
}
/// On Mac, a minimized window uses up all CPU:
/// <https://github.com/emilk/egui/issues/325>
///
/// On Windows, an invisible window also uses up all CPU:
/// <https://github.com/emilk/egui/issues/7776>
pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) {
if window.is_some_and(is_invisible_or_minimized) {
profiling::scope!("minimized_sleep");
std::thread::sleep(core::time::Duration::from_millis(10));
}
}
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();

View File

@@ -280,52 +280,71 @@ impl AppRunner {
.and_then(|v| v.visible())
.unwrap_or(true);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
if is_visible {
if is_visible {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame);
}
});
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = full_output;
});
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = full_output;
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
for (_viewport_id, viewport_output) in viewport_output {
for command in viewport_output.commands {
match command {
ViewportCommand::Screenshot(user_data) => {
self.screenshot_commands_with_frame_delay
.push((user_data, 1));
}
_ => {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
}
}
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
}
self.handle_viewport_commands(
viewport_output
.into_values()
.flat_map(|viewport_output| viewport_output.commands),
);
self.handle_platform_output(platform_output);
if is_visible || !textures_delta.is_empty() {
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
} else {
// The tab is hidden, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when the tab is shown again.
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
self.app.logic(ctx, &mut self.frame);
});
// No pass consumed the input, so save it for the next one:
self.input.raw.append(raw_input);
self.handle_viewport_commands(viewport_commands.into_values().flatten());
self.handle_platform_output(platform_output);
}
}
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {
for command in commands {
match command {
ViewportCommand::Screenshot(user_data) => {
self.screenshot_commands_with_frame_delay
.push((user_data, 1));
}
_ => {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
}
}
}
}
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self) {
let textures_delta = std::mem::take(&mut self.textures_delta);
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
let clipped_primitives = core::mem::take(&mut self.clipped_primitives);
if let Some(clipped_primitives) = clipped_primitives {
let mut screenshot_commands = vec![];
@@ -347,7 +366,7 @@ impl AppRunner {
self.app.clear_color(&self.egui_ctx.global_style().visuals),
&clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
&mut self.textures_delta,
screenshot_commands,
) {
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
@@ -395,7 +414,10 @@ impl AppRunner {
if self.has_focus() {
// The eframe app has focus.
if ime.is_some() {
if let Some(ime) = ime {
if ime.should_interrupt_composition {
self.text_agent.interrupt_ime_composition();
}
// We are editing text: give the focus to the text agent.
self.text_agent.focus();
} else {
@@ -407,7 +429,7 @@ impl AppRunner {
if let Err(err) = self
.text_agent
.move_to(ime, self.canvas(), self.egui_ctx.zoom_factor())
.update(ime, self.canvas(), self.egui_ctx.zoom_factor())
{
log::error!(
"failed to update text agent position: {}",

View File

@@ -0,0 +1,45 @@
use core::{future::Future, pin::Pin};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub(crate) struct WebFile {
file: web_sys::File,
// We store a `PathBuf` here so that we can hand out `Path`s
// without allocating each time.
path: PathBuf,
}
impl From<web_sys::File> for WebFile {
fn from(file: web_sys::File) -> Self {
let path = file.name().into();
Self { file, path }
}
}
impl egui::DroppedFile for WebFile {
fn path(&self) -> &Path {
&self.path
}
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>> {
let file = self.file.clone();
Box::pin(async move {
if file.size() > f64::from(u32::MAX) {
return Err(format!(
"File is too large: browser file reads are limited to {} bytes",
u32::MAX
));
}
let array_buffer = file
.array_buffer()
.await
.map_err(|err| crate::web::string_from_js_value(&err))?;
Ok(js_sys::Uint8Array::new(&array_buffer).to_vec())
})
}
fn web_file(&self) -> Option<&web_sys::File> {
Some(&self.file)
}
}

View File

@@ -190,11 +190,6 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
return;
}
if event.is_composing() || event.key_code() == 229 {
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
return;
}
let modifiers = modifiers_from_kb_event(&event);
runner.input.set_modifiers(modifiers);
@@ -978,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
event.prevent_default();
})?;
runner_ref.add_event_listener(target, "drop", {
let runner_ref = runner_ref.clone();
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
move |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
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 mime = file.type_();
let last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
log::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();
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
if let Some(mut runner_lock) = runner_ref.try_lock() {
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
mime,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
}
}
Err(err) => {
log::error!(
"Failed to read file: {}",
string_from_js_value(&err)
);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
runner.input.raw.dropped_files.push(std::sync::Arc::new(
super::dropped_file::WebFile::from(file),
));
}
}
event.stop_propagation();
event.prevent_default();
}
event.stop_propagation();
event.prevent_default();
}
})?;

View File

@@ -32,7 +32,7 @@ pub fn primary_touch_pos(
event: &web_sys::TouchEvent,
) -> Option<(egui::Pos2, web_sys::Touch)> {
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
let all_touches: Vec<_> = std::iter::chain(
let all_touches: Vec<_> = core::iter::chain(
(0..event.touches().length()).filter_map(|i| event.touches().get(i)),
(0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
)

View File

@@ -5,6 +5,7 @@
mod app_runner;
mod backend;
mod dropped_file;
mod events;
mod input;
mod panic_handler;
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
return;
}
let promise = window.navigator().clipboard().write_text(s);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!("Copy/cut action failed: {}", string_from_js_value(&err));
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}
@@ -248,16 +248,15 @@ fn set_clipboard_image(image: &egui::ColorImage) {
};
let items = js_sys::Array::of1(&item);
let promise = window.navigator().clipboard().write(&items);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!(
"Copy/cut image action failed: {}",
string_from_js_value(&err)
);
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}

View File

@@ -1,16 +1,16 @@
//! The text agent is a hidden `<input>` element used to capture
//! IME and mobile keyboard input events.
use std::cell::Cell;
use core::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use web_sys::Document;
use super::{AppRunner, WebRunner};
pub struct TextAgent {
input: web_sys::HtmlInputElement,
prev_ime_output: Cell<Option<egui::output::IMEOutput>>,
input_state: Rc<RefCell<InputState>>,
}
impl TextAgent {
@@ -19,7 +19,8 @@ impl TextAgent {
runner_ref: &WebRunner,
canvas: &web_sys::HtmlCanvasElement,
) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
// create an `<input>` element
let input = document
@@ -27,11 +28,11 @@ impl TextAgent {
.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_attribute("autocapitalize", "off")?;
let input_state = Rc::new(RefCell::new(InputState::new(input.clone())));
// Hide the element, and park it over the canvas
// Hide the element, and park it over the top-left corner of the canvas
// so that focusing it can never scroll some other part
// of the page into view.
let canvas_rect = super::canvas_content_rect(canvas);
let style = input.style();
style.set_property("background-color", "transparent")?;
style.set_property("border", "none")?;
@@ -40,21 +41,22 @@ impl TextAgent {
style.set_property("height", "1px")?;
style.set_property("caret-color", "transparent")?;
style.set_property("position", "absolute")?;
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
style.set_property("top", &format!("{}px", canvas.offset_top()))?;
style.set_property("left", &format!("{}px", canvas.offset_left()))?;
// Prevent auto-zoom on mobile browsers (requires at least 16px).
style.set_property("font-size", "16px")?;
let root = canvas.get_root_node();
if root.has_type::<Document>() {
// root object is a document, append to its body
root.dyn_into::<Document>()?
.body()
.unwrap()
.append_child(&input)?;
} else {
// append input into root directly
root.append_child(&input)?;
// Insert the input as a sibling of the canvas, so that its
// `position: absolute` resolves against the same containing block
// as the canvas' `offset_top`/`offset_left`.
// This anchors the input to the canvas regardless of how the page
// is scrolled or how the canvas is embedded, and also works when
// the canvas is inside a shadow DOM.
if let Some(parent) = canvas.parent_node() {
parent.insert_before(&input, canvas.next_sibling().as_ref())?;
} else if let Some(body) = document.body() {
log::warn!("Canvas has no parent element - appending text agent to document body");
body.append_child(&input)?;
}
// Focus the app on startup, without scrolling the page.
@@ -66,152 +68,67 @@ impl TextAgent {
// attach event listeners
let on_input = {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
// Workaround for an Android Gboard issue: after typing a word,
// the user has to delete invisible characters (whose count
// matches the length of the current suggestion) before actual
// characters are deleted, unless the focus has been reset.
//
// this issue appears to have been fixed in Gboard sometime
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
super::focus_without_scroll(&input).ok();
}
if event.is_composing() {
// if `is_composing` is true, then user is using IME, for
// example: emoji, pinyin, kanji, hangul, etc. In that case,
// the browser emits both `input` and `compositionupdate`
// events.
// We handle the composition update here instead of in the
// `compositionupdate` event because the selection range
// has not yet been updated when `compositionupdate` fires.
let Some(text) = event.data() else { return };
let selection_start = input
.selection_start()
.unwrap_or(None)
.map(|pos| pos as usize);
let selection_end = input
.selection_end()
.unwrap_or(None)
.map(|pos| pos as usize);
let active_range_chars = if let Some(selection_start) = selection_start
&& let Some(selection_end) = selection_end
{
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
let text_before_selection =
String::from_utf16_lossy(&text_utf16[..selection_start]);
let text_in_selection =
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
let count_before_selection = text_before_selection.chars().count();
let count_in_selection = text_in_selection.chars().count();
Some(count_before_selection..count_before_selection + count_in_selection)
} else {
None
};
let event = egui::Event::Ime(egui::ImeEvent::Preedit {
text,
active_range_chars,
});
runner.input.raw.events.push(event);
} else {
if text.is_empty() {
return;
}
input.set_value("");
let event = egui::Event::Text(text);
runner.input.raw.events.push(event);
}
runner.needs_repaint.repaint_asap();
}
};
let on_composition_start = {
runner_ref.add_event_listener(
&input,
"compositionstart",
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
// Repaint moves the text agent into place,
// see `move_to` in `AppRunner::handle_platform_output`.
// see `AppRunner::handle_platform_output`, which calls
// `TextAgent::update`.
runner.needs_repaint.repaint_asap();
},
)?;
runner_ref.add_event_listener(&input, "input", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
input_state.borrow_mut().handle_input_event(&event, runner);
}
};
let on_composition_end = {
let input = input.clone();
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
let Some(text) = event.data() else { return };
input.set_value("");
let event = egui::Event::Ime(egui::ImeEvent::Commit(text));
runner.input.raw.events.push(event);
runner.needs_repaint.repaint_asap();
})?;
runner_ref.add_event_listener(&input, "compositionend", {
let input_state = Rc::clone(&input_state);
move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| {
input_state
.borrow_mut()
.handle_composition_end_event(runner);
}
};
})?;
runner_ref.add_event_listener(&input, "input", on_input)?;
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
runner_ref.add_event_listener(&input, "keydown", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
let is_consumed = InputState::handle_keydown_event(&input_state, &event);
if !is_consumed {
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
super::events::on_keydown(event, runner);
}
}
})?;
runner_ref.add_event_listener(&input, "keyup", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
let is_consumed = InputState::handle_keyup_event(&input_state, &event);
if !is_consumed {
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
super::events::on_keyup(event, runner);
}
}
})?;
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?;
runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?;
Ok(Self {
input,
prev_ime_output: Default::default(),
})
Ok(Self { input, input_state })
}
pub fn move_to(
pub fn update(
&self,
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
zoom_factor: f32,
) -> Result<(), JsValue> {
// Don't move the text agent unless the position actually changed:
if self.prev_ime_output.get() == ime {
return Ok(());
}
self.prev_ime_output.set(ime);
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
// no-op for now: currently, the text agent is sizeless, so any
// click shifts focus to the canvas, which naturally interrupts the
// composition.
}
let mut canvas_rect = super::canvas_content_rect(canvas);
// Fix for safari with virtual keyboard flapping position
if is_mobile_safari() {
canvas_rect.min.y = canvas.offset_top() as f32;
}
let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2());
let style = self.input.style();
let native_ppp = super::native_pixels_per_point();
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
let logical_canvas_width = canvas.width() as f32 / native_ppp;
let visible_x = cursor_rect.center().x * zoom_factor;
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
let logical_canvas_height = canvas.height() as f32 / native_ppp;
let visible_y = cursor_rect.center().y * zoom_factor;
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
// This is where the IME input will point to:
style.set_property("left", &format!("{clamped_x}px"))?;
style.set_property("top", &format!("{clamped_y}px"))?;
Ok(())
self.input_state
.borrow_mut()
.update(ime, canvas, zoom_factor)
}
pub fn set_focus(&self, on: bool) {
@@ -248,6 +165,11 @@ impl TextAgent {
if let Err(err) = self.input.blur() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
}
self.input_state.borrow_mut().clear();
}
pub(crate) fn interrupt_ime_composition(&self) {
self.input_state.borrow_mut().clear();
}
}
@@ -257,15 +179,273 @@ impl Drop for TextAgent {
}
}
/// Returns `true` if the app is likely running on a mobile device on navigator Safari.
fn is_mobile_safari() -> bool {
(|| {
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_ios = user_agent.contains("iPhone")
|| user_agent.contains("iPad")
|| user_agent.contains("iPod");
let is_safari = user_agent.contains("Safari");
Some(is_ios && is_safari)
})()
.unwrap_or(false)
struct InputState {
input: web_sys::HtmlInputElement,
last_text: String,
ime_output: Option<egui::output::IMEOutput>,
keydown_special_case: KeydownSpecialCase,
}
#[derive(Clone, Copy)]
enum KeydownSpecialCase {
None,
/// On Android Gboard 14.7.09, when suggestions remain visible while typing
/// letters without IME composition (e.g., Latin or Cyrillic), pressing
/// Backspace produces key code 229 instead of the expected Backspace key
/// code.
/// Without the workaround, users have to press Backspace twice before text
/// starts being deleted.
///
/// This workaround is also required for Android Gboard corrections and
/// completions (e.g., `tex|` -> `Texas`) to work correctly. In these
/// cases, a `deleteContentBackward` input event fires first (e.g., to
/// delete `tex`), followed by an `insertText` input event (e.g., to insert
/// `Texas`).
///
/// Since it is difficult to distinguish between a Backspace press and a
/// correction or completion (e.g., when the state is `t|`, it is unclear
/// whether the user wants to delete `t` or replace it with `Texas`), we
/// send a `DeleteSurrounding` IME event in all cases instead of
/// synthetically generating Backspace press and release events.
AndroidKeycode229,
/// iOS (18.6)'s built-in Korean keyboard uses `deleteContentBackward` to
/// compose Hangul characters. In these cases, the key code is 0.
IosKeycode0,
}
impl InputState {
fn new(input: web_sys::HtmlInputElement) -> Self {
Self {
input,
last_text: String::new(),
ime_output: None,
keydown_special_case: KeydownSpecialCase::None,
}
}
fn update(
&mut self,
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
zoom_factor: f32,
) -> Result<(), JsValue> {
// Don't move the text agent unless the position actually changed:
if self.ime_output == ime {
return Ok(());
}
self.ime_output = ime;
let Some(ime) = ime else { return Ok(()) };
// NOTE: we don't set the input's `type` to `password` based on
// `ime.purpose`, because that would confuse some password managers.
// For example, Chrome's password manager will always think the last
// letter typed in the password field is the password.
let style = self.input.style();
let native_ppp = super::native_pixels_per_point();
// The input is a sibling of the canvas (see `attach`), so we position
// it relative to the same containing block using the canvas offset.
// Unlike `get_bounding_client_rect`, the offset is unaffected by page
// scrolling, and doesn't flap when the virtual keyboard is shown on
// mobile Safari.
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
let logical_canvas_width = canvas.width() as f32 / native_ppp;
let visible_x = ime.cursor_rect.center().x * zoom_factor;
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
let logical_canvas_height = canvas.height() as f32 / native_ppp;
let visible_y = ime.cursor_rect.center().y * zoom_factor;
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
// This is where the IME input will point to:
style.set_property(
"left",
&format!("{}px", canvas.offset_left() as f32 + clamped_x),
)?;
style.set_property(
"top",
&format!("{}px", canvas.offset_top() as f32 + clamped_y),
)?;
Ok(())
}
fn clear(&mut self) {
self.input.set_value("");
self.last_text.clear();
}
fn handle_input_event(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
if self
.ime_output
.as_ref()
.is_some_and(|ime| ime.purpose == egui::IMEPurpose::Password)
{
self.handle_input_event_password(event, runner);
return;
}
let input_type = event.input_type();
if !event.is_composing()
&& input_type != "insertText"
// iOS uses this for corrections and completions (e.g., `tex|` ->
// `Texas`).
&& input_type != "insertReplacementText"
&& (matches!(self.keydown_special_case, KeydownSpecialCase::None)
|| input_type != "deleteContentBackward")
{
self.clear();
return;
}
let text = self.input.value();
let prefix_len = longest_common_prefix_length(&text, &self.last_text);
let last_text_len = self.last_text.chars().count();
if prefix_len < last_text_len {
let out_event = egui::Event::Ime(egui::ImeEvent::DeleteSurrounding {
before_chars: last_text_len - prefix_len,
after_chars: 0,
});
runner.input.raw.events.push(out_event);
}
let preedit_text: String = text.chars().skip(prefix_len).collect();
let out_event = if event.is_composing() {
// We handle the composition update here instead of in a
// `compositionupdate` event because the selection range
// has not yet been updated when `compositionupdate` fires.
let active_range_chars = self.active_range_chars(&text, prefix_len);
egui::Event::Ime(egui::ImeEvent::Preedit {
text: preedit_text,
active_range_chars,
})
} else {
egui::Event::Text(preedit_text)
};
runner.input.raw.events.push(out_event);
if event.is_composing() {
self.last_text = text.chars().take(prefix_len).collect();
} else {
self.last_text = text;
}
runner.needs_repaint.repaint_asap();
}
fn handle_input_event_password(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
let input_type = event.input_type();
if input_type != "insertText" {
return;
}
let text = self.input.value();
runner.input.raw.events.push(egui::Event::Text(text));
self.clear();
}
/// Compute the active range (cursor or conversion segment) within the
/// preedit text, based on the selection in the input element.
///
/// `text` is the full `input.value()`, and `prefix_len_chars` is the
/// number of chars at the start of `text` that are committed (not part
/// of the preedit). `selectionStart`/`selectionEnd` are UTF-16 offsets
/// within the full `input.value()`, so they are adjusted to be relative
/// to the preedit text.
fn active_range_chars(
&self,
text: &str,
prefix_len_chars: usize,
) -> Option<core::ops::Range<usize>> {
let selection_start = self.input.selection_start().unwrap_or(None)? as usize;
let selection_end = self.input.selection_end().unwrap_or(None)? as usize;
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
if selection_start > text_utf16.len() || selection_end > text_utf16.len() {
// This can occur on Android Chrome. see discussion in:
// <https://github.com/emilk/egui/pull/8045>.
return None;
}
let text_before_selection = String::from_utf16_lossy(&text_utf16[..selection_start]);
let text_in_selection =
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
let count_before_selection = text_before_selection.chars().count();
let count_in_selection = text_in_selection.chars().count();
// Adjust for the committed prefix to get the range within the preedit text.
let start = count_before_selection.saturating_sub(prefix_len_chars);
let end = start + count_in_selection;
Some(start..end)
}
fn handle_composition_end_event(&mut self, runner: &mut AppRunner) {
let text = self.input.value();
let commit_text = {
let prefix_len = self.last_text.chars().count();
text.chars().skip(prefix_len).collect::<String>()
};
let out_event = egui::Event::Ime(egui::ImeEvent::Commit(commit_text));
runner.input.raw.events.push(out_event);
self.last_text = text;
runner.needs_repaint.repaint_asap();
}
/// ## Returns
/// Whether the event is consumed. If `true`, the caller should not do
/// further processing for this event.
fn handle_keydown_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
// Platform-sniffing methods are unreliable, so they are not used as
// guards here.
let special_case = match event.key_code() {
229 => KeydownSpecialCase::AndroidKeycode229,
0 => KeydownSpecialCase::IosKeycode0,
_ => KeydownSpecialCase::None,
};
input_state.borrow_mut().keydown_special_case = special_case;
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
if event.is_composing() || !matches!(special_case, KeydownSpecialCase::None) {
true
} else {
if event.key().chars().count() > 1
|| event.ctrl_key()
|| event.alt_key()
|| event.meta_key()
{
input_state.borrow_mut().clear();
}
false
}
}
/// ## Returns
/// Whether the event is consumed. If `true`, the caller should not do
/// further processing for this event.
fn handle_keyup_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
input_state.borrow_mut().keydown_special_case = KeydownSpecialCase::None;
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
event.is_composing() || event.key_code() == 229
}
}
fn longest_common_prefix_length(a: &str, b: &str) -> usize {
core::iter::zip(a.chars(), b.chars())
.take_while(|(a, b)| a == b)
.count()
}

View File

@@ -24,7 +24,7 @@ pub(crate) trait WebPainter {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue>;

View File

@@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
for (id, image_delta) in &textures_delta.set {
self.painter.set_texture(*id, image_delta);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
self.painter.set_texture(id, &image_delta);
}
}
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
@@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow {
self.screenshots.push((image, capture));
}
for &id in &textures_delta.free {
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
self.painter.free_texture(id);
}

View File

@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture_data: Vec<UserData>,
) -> Result<(), JsValue> {
let capture = !capture_data.is_empty();
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
renderer.update_texture(
&render_state.device,
&render_state.queue,
id,
&image_delta,
);
}
}
renderer.update_buffers(
@@ -365,7 +368,7 @@ impl WebPainter for WebPainterWgpu {
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
.submit(std::iter::chain(user_cmd_bufs, [encoder.finish()]));
.submit(core::iter::chain(user_cmd_bufs, [encoder.finish()]));
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
if let Some(capture_buffer) = capture_buffer
@@ -388,8 +391,9 @@ impl WebPainter for WebPainterWgpu {
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
renderer.free_texture(&id);
}
}

View File

@@ -1,4 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use core::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
@@ -107,7 +108,7 @@ impl WebRunner {
fn unsubscribe_from_all_events(&self) {
let events_to_unsubscribe: Vec<_> =
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
core::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
if !events_to_unsubscribe.is_empty() {
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
@@ -139,7 +140,7 @@ impl WebRunner {
/// Returns `None` if there has been a panic, or if we have been destroyed.
/// In that case, just return to JS.
pub(crate) fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
pub(crate) fn try_lock(&self) -> Option<core::cell::RefMut<'_, AppRunner>> {
if self.panic_handler.has_panicked() {
// Unsubscribe from all events so that we don't get any more callbacks
// that will try to access the poisoned runner.
@@ -147,7 +148,7 @@ impl WebRunner {
None
} else {
let lock = self.app_runner.try_borrow_mut().ok()?;
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
core::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
.ok()
}
}
@@ -158,9 +159,9 @@ impl WebRunner {
/// and return `None` if this runner has panicked.
pub fn app_mut<ConcreteApp: 'static + App>(
&self,
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
) -> Option<core::cell::RefMut<'_, ConcreteApp>> {
self.try_lock()
.map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
.map(|lock| core::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
}
/// Convenience function to reduce boilerplate and ensure that all event handlers