Skip to content

Commit ea71b7f

Browse files
authored
eframe web: detect and report panics during startup (#2992)
* Detect panics during initialization and show them to the user * PanicHandler now also logs the panics * Add example of how to call into your app from JS * Refactor: break out AppRunner and AppRunnerRef to own files * Hide AppRunner * Simplify user code * AppRunnerRef -> WebRunner * Better docs * Don't paint until first animation frame * Update multiple_apps.html * Update web demo * Cleanup and fixes * left-align panic message in html
1 parent ff8e482 commit ea71b7f

File tree

15 files changed

+790
-715
lines changed

15 files changed

+790
-715
lines changed

Cargo.lock

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/eframe/src/lib.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,74 @@
4242
//!
4343
//! ## Usage, web:
4444
//! ``` no_run
45-
//! #[cfg(target_arch = "wasm32")]
45+
//! # #[cfg(target_arch = "wasm32")]
4646
//! use wasm_bindgen::prelude::*;
4747
//!
48-
//! /// Call this once from the HTML.
49-
//! #[cfg(target_arch = "wasm32")]
48+
//! /// Your handle to the web app from JavaScript.
49+
//! # #[cfg(target_arch = "wasm32")]
50+
//! #[derive(Clone)]
5051
//! #[wasm_bindgen]
51-
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
52-
//! let web_options = eframe::WebOptions::default();
53-
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
52+
//! pub struct WebHandle {
53+
//! runner: WebRunner,
54+
//! }
55+
//!
56+
//! # #[cfg(target_arch = "wasm32")]
57+
//! #[wasm_bindgen]
58+
//! impl WebHandle {
59+
//! /// Installs a panic hook, then returns.
60+
//! #[allow(clippy::new_without_default)]
61+
//! #[wasm_bindgen(constructor)]
62+
//! pub fn new() -> Self {
63+
//! // Redirect [`log`] message to `console.log` and friends:
64+
//! eframe::web::WebLogger::init(log::LevelFilter::Debug).ok();
65+
//!
66+
//! Self {
67+
//! runner: WebRunner::new(),
68+
//! }
69+
//! }
70+
//!
71+
//! /// Call this once from JavaScript to start your app.
72+
//! #[wasm_bindgen]
73+
//! pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
74+
//! self.runner
75+
//! .start(
76+
//! canvas_id,
77+
//! eframe::WebOptions::default(),
78+
//! Box::new(|cc| Box::new(MyEguiApp::new(cc))),
79+
//! )
80+
//! .await
81+
//! }
82+
//!
83+
//! // The following are optional:
84+
//!
85+
//! #[wasm_bindgen]
86+
//! pub fn destroy(&self) {
87+
//! self.runner.destroy();
88+
//! }
89+
//!
90+
//! /// Example on how to call into your app from JavaScript.
91+
//! #[wasm_bindgen]
92+
//! pub fn example(&self) {
93+
//! if let Some(app) = self.runner.app_mut::<MyEguiApp>() {
94+
//! app.example();
95+
//! }
96+
//! }
97+
//!
98+
//! /// The JavaScript can check whether or not your app has crashed:
99+
//! #[wasm_bindgen]
100+
//! pub fn has_panicked(&self) -> bool {
101+
//! self.runner.has_panicked()
102+
//! }
103+
//!
104+
//! #[wasm_bindgen]
105+
//! pub fn panic_message(&self) -> Option<String> {
106+
//! self.runner.panic_summary().map(|s| s.message())
107+
//! }
108+
//!
109+
//! #[wasm_bindgen]
110+
//! pub fn panic_callstack(&self) -> Option<String> {
111+
//! self.runner.panic_summary().map(|s| s.callstack())
112+
//! }
54113
//! }
55114
//! ```
56115
//!
@@ -91,7 +150,7 @@ pub use web_sys;
91150
pub mod web;
92151

93152
#[cfg(target_arch = "wasm32")]
94-
pub use web::start_web;
153+
pub use web::WebRunner;
95154

96155
// ----------------------------------------------------------------------------
97156
// When compiling natively

crates/eframe/src/web/app_runner.rs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
use egui::TexturesDelta;
2+
use wasm_bindgen::JsValue;
3+
4+
use crate::{epi, App};
5+
6+
use super::{now_sec, web_painter::WebPainter, NeedRepaint};
7+
8+
pub struct AppRunner {
9+
pub(crate) frame: epi::Frame,
10+
egui_ctx: egui::Context,
11+
painter: super::ActiveWebPainter,
12+
pub(crate) input: super::WebInput,
13+
app: Box<dyn epi::App>,
14+
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
15+
last_save_time: f64,
16+
screen_reader: super::screen_reader::ScreenReader,
17+
pub(crate) text_cursor_pos: Option<egui::Pos2>,
18+
pub(crate) mutable_text_under_cursor: bool,
19+
textures_delta: TexturesDelta,
20+
}
21+
22+
impl Drop for AppRunner {
23+
fn drop(&mut self) {
24+
log::debug!("AppRunner has fully dropped");
25+
}
26+
}
27+
28+
impl AppRunner {
29+
/// # Errors
30+
/// Failure to initialize WebGL renderer.
31+
pub async fn new(
32+
canvas_id: &str,
33+
web_options: crate::WebOptions,
34+
app_creator: epi::AppCreator,
35+
) -> Result<Self, String> {
36+
let painter = super::ActiveWebPainter::new(canvas_id, &web_options).await?;
37+
38+
let system_theme = if web_options.follow_system_theme {
39+
super::system_theme()
40+
} else {
41+
None
42+
};
43+
44+
let info = epi::IntegrationInfo {
45+
web_info: epi::WebInfo {
46+
user_agent: super::user_agent().unwrap_or_default(),
47+
location: super::web_location(),
48+
},
49+
system_theme,
50+
cpu_usage: None,
51+
native_pixels_per_point: Some(super::native_pixels_per_point()),
52+
};
53+
let storage = LocalStorage::default();
54+
55+
let egui_ctx = egui::Context::default();
56+
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
57+
&super::user_agent().unwrap_or_default(),
58+
));
59+
super::load_memory(&egui_ctx);
60+
61+
let theme = system_theme.unwrap_or(web_options.default_theme);
62+
egui_ctx.set_visuals(theme.egui_visuals());
63+
64+
let app = app_creator(&epi::CreationContext {
65+
egui_ctx: egui_ctx.clone(),
66+
integration_info: info.clone(),
67+
storage: Some(&storage),
68+
69+
#[cfg(feature = "glow")]
70+
gl: Some(painter.gl().clone()),
71+
72+
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
73+
wgpu_render_state: painter.render_state(),
74+
#[cfg(all(feature = "wgpu", feature = "glow"))]
75+
wgpu_render_state: None,
76+
});
77+
78+
let frame = epi::Frame {
79+
info,
80+
output: Default::default(),
81+
storage: Some(Box::new(storage)),
82+
83+
#[cfg(feature = "glow")]
84+
gl: Some(painter.gl().clone()),
85+
86+
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
87+
wgpu_render_state: painter.render_state(),
88+
#[cfg(all(feature = "wgpu", feature = "glow"))]
89+
wgpu_render_state: None,
90+
};
91+
92+
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
93+
{
94+
let needs_repaint = needs_repaint.clone();
95+
egui_ctx.set_request_repaint_callback(move |info| {
96+
needs_repaint.repaint_after(info.after.as_secs_f64());
97+
});
98+
}
99+
100+
let mut runner = Self {
101+
frame,
102+
egui_ctx,
103+
painter,
104+
input: Default::default(),
105+
app,
106+
needs_repaint,
107+
last_save_time: now_sec(),
108+
screen_reader: Default::default(),
109+
text_cursor_pos: None,
110+
mutable_text_under_cursor: false,
111+
textures_delta: Default::default(),
112+
};
113+
114+
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
115+
116+
Ok(runner)
117+
}
118+
119+
pub fn egui_ctx(&self) -> &egui::Context {
120+
&self.egui_ctx
121+
}
122+
123+
/// Get mutable access to the concrete [`App`] we enclose.
124+
///
125+
/// This will panic if your app does not implement [`App::as_any_mut`].
126+
pub fn app_mut<ConcreteApp: 'static + App>(&mut self) -> &mut ConcreteApp {
127+
self.app
128+
.as_any_mut()
129+
.expect("Your app must implement `as_any_mut`, but it doesn't")
130+
.downcast_mut::<ConcreteApp>()
131+
.expect("app_mut got the wrong type of App")
132+
}
133+
134+
pub fn auto_save_if_needed(&mut self) {
135+
let time_since_last_save = now_sec() - self.last_save_time;
136+
if time_since_last_save > self.app.auto_save_interval().as_secs_f64() {
137+
self.save();
138+
}
139+
}
140+
141+
pub fn save(&mut self) {
142+
if self.app.persist_egui_memory() {
143+
super::save_memory(&self.egui_ctx);
144+
}
145+
if let Some(storage) = self.frame.storage_mut() {
146+
self.app.save(storage);
147+
}
148+
self.last_save_time = now_sec();
149+
}
150+
151+
pub fn canvas_id(&self) -> &str {
152+
self.painter.canvas_id()
153+
}
154+
155+
pub fn warm_up(&mut self) {
156+
if self.app.warm_up_enabled() {
157+
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
158+
self.egui_ctx
159+
.memory_mut(|m| m.set_everything_is_visible(true));
160+
self.logic();
161+
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
162+
self.egui_ctx.clear_animations();
163+
}
164+
}
165+
166+
pub fn destroy(mut self) {
167+
log::debug!("Destroying AppRunner");
168+
self.painter.destroy();
169+
}
170+
171+
/// Returns how long to wait until the next repaint.
172+
///
173+
/// Call [`Self::paint`] later to paint
174+
pub fn logic(&mut self) -> (std::time::Duration, Vec<egui::ClippedPrimitive>) {
175+
let frame_start = now_sec();
176+
177+
super::resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
178+
let canvas_size = super::canvas_size_in_points(self.canvas_id());
179+
let raw_input = self.input.new_frame(canvas_size);
180+
181+
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
182+
self.app.update(egui_ctx, &mut self.frame);
183+
});
184+
let egui::FullOutput {
185+
platform_output,
186+
repaint_after,
187+
textures_delta,
188+
shapes,
189+
} = full_output;
190+
191+
self.handle_platform_output(platform_output);
192+
self.textures_delta.append(textures_delta);
193+
let clipped_primitives = self.egui_ctx.tessellate(shapes);
194+
195+
{
196+
let app_output = self.frame.take_app_output();
197+
let epi::backend::AppOutput {} = app_output;
198+
}
199+
200+
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
201+
202+
(repaint_after, clipped_primitives)
203+
}
204+
205+
/// Paint the results of the last call to [`Self::logic`].
206+
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
207+
let textures_delta = std::mem::take(&mut self.textures_delta);
208+
209+
self.painter.paint_and_update_textures(
210+
self.app.clear_color(&self.egui_ctx.style().visuals),
211+
clipped_primitives,
212+
self.egui_ctx.pixels_per_point(),
213+
&textures_delta,
214+
)?;
215+
216+
Ok(())
217+
}
218+
219+
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
220+
if self.egui_ctx.options(|o| o.screen_reader) {
221+
self.screen_reader
222+
.speak(&platform_output.events_description());
223+
}
224+
225+
let egui::PlatformOutput {
226+
cursor_icon,
227+
open_url,
228+
copied_text,
229+
events: _, // already handled
230+
mutable_text_under_cursor,
231+
text_cursor_pos,
232+
#[cfg(feature = "accesskit")]
233+
accesskit_update: _, // not currently implemented
234+
} = platform_output;
235+
236+
super::set_cursor_icon(cursor_icon);
237+
if let Some(open) = open_url {
238+
super::open_url(&open.url, open.new_tab);
239+
}
240+
241+
#[cfg(web_sys_unstable_apis)]
242+
if !copied_text.is_empty() {
243+
super::set_clipboard_text(&copied_text);
244+
}
245+
246+
#[cfg(not(web_sys_unstable_apis))]
247+
let _ = copied_text;
248+
249+
self.mutable_text_under_cursor = mutable_text_under_cursor;
250+
251+
if self.text_cursor_pos != text_cursor_pos {
252+
super::text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
253+
self.text_cursor_pos = text_cursor_pos;
254+
}
255+
}
256+
}
257+
258+
// ----------------------------------------------------------------------------
259+
260+
#[derive(Default)]
261+
struct LocalStorage {}
262+
263+
impl epi::Storage for LocalStorage {
264+
fn get_string(&self, key: &str) -> Option<String> {
265+
super::local_storage_get(key)
266+
}
267+
268+
fn set_string(&mut self, key: &str, value: String) {
269+
super::local_storage_set(key, &value);
270+
}
271+
272+
fn flush(&mut self) {}
273+
}

0 commit comments

Comments
 (0)