diff --git a/eframe/src/web/backend.rs b/eframe/src/web/backend.rs index 7deec4cd..465c16e1 100644 --- a/eframe/src/web/backend.rs +++ b/eframe/src/web/backend.rs @@ -2,7 +2,9 @@ use super::{glow_wrapping::WrappedGlowPainter, *}; use crate::epi; +use egui::mutex::{Mutex, MutexGuard}; use egui::TexturesDelta; + pub use egui::{pos2, Color32}; // ---------------------------------------------------------------------------- @@ -34,21 +36,34 @@ impl WebInput { use std::sync::atomic::Ordering::SeqCst; -pub struct NeedRepaint(std::sync::atomic::AtomicBool); +/// Stores when to do the next repaint. +pub struct NeedRepaint(Mutex); impl Default for NeedRepaint { fn default() -> Self { - Self(true.into()) + Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint } } impl NeedRepaint { - pub fn fetch_and_clear(&self) -> bool { - self.0.swap(false, SeqCst) + /// Returns the time (in [`now_sec`] scale) when + /// we should next repaint. + pub fn when_to_repaint(&self) -> f64 { + *self.0.lock() } - pub fn set_true(&self) { - self.0.store(true, SeqCst); + /// Unschedule repainting. + pub fn clear(&self) { + *self.0.lock() = f64::INFINITY; + } + + pub fn repaint_after(&self, num_seconds: f64) { + let mut repaint_time = self.0.lock(); + *repaint_time = repaint_time.min(now_sec() + num_seconds); + } + + pub fn repaint_asap(&self) { + *self.0.lock() = f64::NEG_INFINITY; } } @@ -194,7 +209,7 @@ impl AppRunner { { let needs_repaint = needs_repaint.clone(); egui_ctx.set_request_repaint_callback(move || { - needs_repaint.0.store(true, SeqCst); + needs_repaint.repaint_asap(); }); } @@ -420,7 +435,6 @@ fn start_runner(app_runner: AppRunner) -> Result { super::events::install_canvas_events(&runner_container)?; super::events::install_document_events(&runner_container)?; text_agent::install_text_agent(&runner_container)?; - super::events::repaint_every_ms(&runner_container, 1000)?; // just in case. TODO(emilk): make it a parameter super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?; diff --git a/eframe/src/web/events.rs b/eframe/src/web/events.rs index be679588..153fc7b4 100644 --- a/eframe/src/web/events.rs +++ b/eframe/src/web/events.rs @@ -7,14 +7,14 @@ pub fn paint_and_schedule( ) -> Result<(), JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.lock(); - if runner_lock.needs_repaint.fetch_and_clear() { + if runner_lock.needs_repaint.when_to_repaint() <= now_sec() { + runner_lock.needs_repaint.clear(); runner_lock.clear_color_buffer(); let (repaint_after, clipped_primitives) = runner_lock.logic()?; runner_lock.paint(&clipped_primitives)?; - if repaint_after.is_zero() { - runner_lock.needs_repaint.set_true(); - } - // TODO: schedule a repaint after `repaint_after` when it is not zero + runner_lock + .needs_repaint + .repaint_after(repaint_after.as_secs_f64()); runner_lock.auto_save(); } @@ -75,7 +75,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< { runner_lock.input.raw.events.push(egui::Event::Text(key)); } - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input(); @@ -123,7 +123,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< modifiers, }); } - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); }, )?; @@ -137,7 +137,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< let text = text.replace("\r\n", "\n"); if !text.is_empty() { runner_lock.input.raw.events.push(egui::Event::Paste(text)); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } event.stop_propagation(); event.prevent_default(); @@ -152,7 +152,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< "cut", |_: web_sys::ClipboardEvent, mut runner_lock| { runner_lock.input.raw.events.push(egui::Event::Cut); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); }, )?; @@ -162,7 +162,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< "copy", |_: web_sys::ClipboardEvent, mut runner_lock| { runner_lock.input.raw.events.push(egui::Event::Copy); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); }, )?; @@ -171,7 +171,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< &window, event_name, |_: web_sys::Event, runner_lock| { - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); }, )?; } @@ -190,38 +190,6 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result< Ok(()) } -/// Repaint at least every `ms` milliseconds. -pub fn repaint_every_ms( - runner_container: &AppRunnerContainer, - milliseconds: i32, -) -> Result<(), JsValue> { - assert!(milliseconds >= 0); - - use wasm_bindgen::JsCast; - - let window = web_sys::window().unwrap(); - - let closure = Closure::wrap(Box::new({ - let runner = runner_container.runner.clone(); - let panicked = runner_container.panicked.clone(); - - move || { - // Do not lock the runner if the code has panicked - if !panicked.load(Ordering::SeqCst) { - runner.lock().needs_repaint.set_true(); - } - } - }) as Box); - - window.set_interval_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - milliseconds, - )?; - - closure.forget(); - Ok(()) -} - pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> { use wasm_bindgen::JsCast; let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); @@ -254,7 +222,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() pressed: true, modifiers, }); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } event.stop_propagation(); // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. @@ -271,7 +239,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() .raw .events .push(egui::Event::PointerMoved(pos)); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -294,7 +262,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() pressed: false, modifiers, }); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); text_agent::update_text_agent(runner_lock); } @@ -308,7 +276,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() "mouseleave", |event: web_sys::MouseEvent, mut runner_lock| { runner_lock.input.raw.events.push(egui::Event::PointerGone); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -336,7 +304,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() }); push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -358,7 +326,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() .push(egui::Event::PointerMoved(pos)); push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -385,7 +353,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() runner_lock.input.raw.events.push(egui::Event::PointerGone); push_touches(&mut *runner_lock, egui::TouchPhase::End, &event); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } @@ -444,7 +412,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() .push(egui::Event::Scroll(delta)); } - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -464,7 +432,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() }); } } - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } @@ -476,7 +444,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() "dragleave", |event: web_sys::DragEvent, mut runner_lock| { runner_lock.input.raw.hovered_files.clear(); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, @@ -488,7 +456,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() move |event: web_sys::DragEvent, mut runner_lock| { if let Some(data_transfer) = event.data_transfer() { runner_lock.input.raw.hovered_files.clear(); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); // Unlock the runner so it can be locked after a future await point drop(runner_lock); @@ -524,7 +492,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<() ..Default::default() }, ); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } Err(err) => { tracing::error!("Failed to read file: {:?}", err); diff --git a/eframe/src/web/mod.rs b/eframe/src/web/mod.rs index 13b666e3..e234250b 100644 --- a/eframe/src/web/mod.rs +++ b/eframe/src/web/mod.rs @@ -20,7 +20,6 @@ use std::sync::{ Arc, }; -use egui::mutex::{Mutex, MutexGuard}; use wasm_bindgen::prelude::*; use web_sys::EventTarget; @@ -30,7 +29,9 @@ use crate::Theme; // ---------------------------------------------------------------------------- -/// Current time in seconds (since undefined point in time) +/// Current time in seconds (since undefined point in time). +/// +/// Monotonically increasing. pub fn now_sec() -> f64 { web_sys::window() .expect("should have a Window") diff --git a/eframe/src/web/text_agent.rs b/eframe/src/web/text_agent.rs index b7b035c2..b939971b 100644 --- a/eframe/src/web/text_agent.rs +++ b/eframe/src/web/text_agent.rs @@ -55,7 +55,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J if !text.is_empty() && !is_composing.get() { input_clone.set_value(""); runner_lock.input.raw.events.push(egui::Event::Text(text)); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } } })?; @@ -75,7 +75,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J .raw .events .push(egui::Event::CompositionStart); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } })?; @@ -85,7 +85,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| { if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { runner_lock.input.raw.events.push(event); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } }, )?; @@ -99,7 +99,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J if let Some(event) = event.data().map(egui::Event::CompositionEnd) { runner_lock.input.raw.events.push(event); - runner_lock.needs_repaint.set_true(); + runner_lock.needs_repaint.repaint_asap(); } } })?; diff --git a/egui_demo_app/src/backend_panel.rs b/egui_demo_app/src/backend_panel.rs index 854f24ad..17a60c7e 100644 --- a/egui_demo_app/src/backend_panel.rs +++ b/egui_demo_app/src/backend_panel.rs @@ -49,12 +49,11 @@ pub struct BackendPanel { pub open: bool, #[cfg_attr(feature = "serde", serde(skip))] - // go back to [`Reactive`] mode each time we start + // go back to [`RunMode::Reactive`] mode each time we start run_mode: RunMode, #[cfg_attr(feature = "serde", serde(skip))] - // reset to 1 second as default repaint_after idle timeout. - repaint_after_timeout: std::time::Duration, + repaint_after_seocnds: f32, /// current slider value for current gui scale #[cfg_attr(feature = "serde", serde(skip))] @@ -71,7 +70,7 @@ impl Default for BackendPanel { Self { open: false, run_mode: Default::default(), - repaint_after_timeout: std::time::Duration::from_secs(1), + repaint_after_seocnds: 1.0, pixels_per_point: None, frame_history: Default::default(), egui_windows: Default::default(), @@ -85,13 +84,16 @@ impl BackendPanel { .on_new_frame(ctx.input().time, frame.info().cpu_usage); match self.run_mode { - RunMode::Reactive => { - ctx.request_repaint_after(self.repaint_after_timeout); - } RunMode::Continuous => { // Tell the backend to repaint as soon as possible ctx.request_repaint(); } + RunMode::Reactive => { + // let the computer rest for a bit + ctx.request_repaint_after(std::time::Duration::from_secs_f32( + self.repaint_after_seocnds, + )); + } } } @@ -243,16 +245,18 @@ impl BackendPanel { )); } else { ui.label("Only running UI code when there are animations or input."); - ui.label("but if there's no input for the repaint_after duration, we force an update"); - ui.label("repaint_after (in seconds)"); - let mut seconds = self.repaint_after_timeout.as_secs_f32(); - if egui::DragValue::new(&mut seconds) - .clamp_range(0.1..=10.0) - .ui(ui) - .changed() - { - self.repaint_after_timeout = std::time::Duration::from_secs_f32(seconds); - } + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("(but at least every "); + egui::DragValue::new(&mut self.repaint_after_seocnds) + .clamp_range(0.1..=10.0) + .speed(0.1) + .suffix(" s") + .ui(ui) + .on_hover_text("Repaint this often, even if there is no input."); + ui.label(")"); + }); } } }