diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index e7456649..5a0ead01 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,5 +1,4 @@ use egui::TexturesDelta; -use wasm_bindgen::JsValue; use crate::{epi, App}; @@ -17,7 +16,10 @@ pub struct AppRunner { screen_reader: super::screen_reader::ScreenReader, pub(crate) text_cursor_pos: Option, pub(crate) mutable_text_under_cursor: bool, + + // Output for the last run: textures_delta: TexturesDelta, + clipped_primitives: Option>, } impl Drop for AppRunner { @@ -115,6 +117,7 @@ impl AppRunner { text_cursor_pos: None, mutable_text_under_cursor: false, textures_delta: Default::default(), + clipped_primitives: None, }; runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); @@ -170,8 +173,26 @@ impl AppRunner { self.painter.destroy(); } - /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> Vec { + /// Runs the user code and paints the UI. + /// + /// If there is already an outstanding frame of output, + /// that is painted instead. + pub fn run_and_paint(&mut self) { + if self.clipped_primitives.is_none() { + // Run user code, and paint the results: + self.logic(); + self.paint(); + } else { + // We have already run the logic, e.g. in an on-click event, + // so let's only present the results: + self.paint(); + } + } + + /// Runs the logic, but doesn't paint the result. + /// + /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. + pub fn logic(&mut self) { let frame_start = now_sec(); super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points); @@ -203,25 +224,26 @@ impl AppRunner { self.handle_platform_output(platform_output); self.textures_delta.append(textures_delta); - let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point); + self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - - clipped_primitives } /// Paint the results of the last call to [`Self::logic`]. - pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { + 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); - self.painter.paint_and_update_textures( - self.app.clear_color(&self.egui_ctx.style().visuals), - clipped_primitives, - self.egui_ctx.pixels_per_point(), - &textures_delta, - )?; - - Ok(()) + if let Some(clipped_primitives) = clipped_primitives { + if let Err(err) = self.painter.paint_and_update_textures( + self.app.clear_color(&self.egui_ctx.style().visuals), + &clipped_primitives, + self.egui_ctx.pixels_per_point(), + &textures_delta, + ) { + log::error!("Failed to paint: {}", super::string_from_js_value(&err)); + } + } } fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 29a06339..2dc3af4e 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -73,6 +73,10 @@ impl NeedRepaint { *repaint_time = repaint_time.min(super::now_sec() + num_seconds); } + pub fn needs_repaint(&self) -> bool { + self.when_to_repaint() <= super::now_sec() + } + pub fn repaint_asap(&self) { *self.0.lock() = f64::NEG_INFINITY; } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index a00ed573..bd8bc88a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -8,22 +8,19 @@ use super::*; 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() { - paint_if_needed(&mut runner_lock)?; + paint_if_needed(&mut runner_lock); drop(runner_lock); request_animation_frame(runner_ref.clone())?; } - Ok(()) } -fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { - if runner.needs_repaint.when_to_repaint() <= now_sec() { +fn paint_if_needed(runner: &mut AppRunner) { + if runner.needs_repaint.needs_repaint() { runner.needs_repaint.clear(); - let clipped_primitives = runner.logic(); - runner.paint(&clipped_primitives)?; - runner.auto_save_if_needed(); + runner.run_and_paint(); } - Ok(()) + runner.auto_save_if_needed(); } pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { @@ -177,10 +174,14 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa "cut", |event: web_sys::ClipboardEvent, runner| { runner.input.raw.events.push(egui::Event::Cut); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + event.stop_propagation(); event.prevent_default(); }, @@ -192,10 +193,14 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa "copy", |event: web_sys::ClipboardEvent, runner| { runner.input.raw.events.push(egui::Event::Copy); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + event.stop_propagation(); event.prevent_default(); }, @@ -281,9 +286,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu pressed: true, modifiers, }); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); } event.stop_propagation(); @@ -313,9 +321,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu pressed: false, modifiers, }); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); text_agent::update_text_agent(runner); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 252dddd3..85e50ea2 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -49,6 +49,10 @@ use crate::Theme; // ---------------------------------------------------------------------------- +pub(crate) fn string_from_js_value(value: &JsValue) -> String { + value.as_string().unwrap_or_else(|| format!("{value:#?}")) +} + /// Current time in seconds (since undefined point in time). /// /// Monotonically increasing. @@ -196,7 +200,7 @@ fn set_clipboard_text(s: &str) { let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { if let Err(err) = future.await { - log::error!("Copy/cut action failed: {err:?}"); + log::error!("Copy/cut action failed: {}", string_from_js_value(&err)); } }; wasm_bindgen_futures::spawn_local(future); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 31ed1464..67d05b24 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -95,7 +95,10 @@ impl WebRunner { log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len()); for x in events_to_unsubscribe { if let Err(err) = x.unsubscribe() { - log::warn!("Failed to unsubscribe from event: {err:?}"); + log::warn!( + "Failed to unsubscribe from event: {}", + super::string_from_js_value(&err) + ); } } }