From 834e2e9f5090824b853ba585f5f8b3f3a47f2359 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 20 Apr 2023 10:56:52 +0200 Subject: [PATCH] Fix: `request_repaint_after` works even when called from background thread (#2939) * Refactor repaint logic * request_repaint_after also fires the request_repaint callback * Bug fixes * Add test to egui_demo_app * build_demo_web: build debug unless --release is specified * Fix the web backend too * Run special clippy for wasm, forbidding some types/methods * Remove wasm_bindgen_check.sh * Fix typos * Revert "Remove wasm_bindgen_check.sh" This reverts commit 92dde253446a6930f34f2fcf67f76bc11669ec3b. * Only run cranky/clippy once --- .github/workflows/rust.yml | 5 +- Cargo.lock | 2 + clippy.toml | 11 +- crates/eframe/src/native/run.rs | 182 +++++++++++++-------- crates/eframe/src/web/backend.rs | 4 +- crates/eframe/src/web/events.rs | 7 +- crates/egui/src/context.rs | 184 +++++++++++++++------- crates/egui/src/lib.rs | 2 +- crates/egui_demo_app/Cargo.toml | 2 + crates/egui_demo_app/src/backend_panel.rs | 79 ++++++---- crates/egui_demo_app/src/frame_history.rs | 6 - scripts/build_demo_web.sh | 32 ++-- scripts/clippy_wasm.sh | 13 ++ scripts/clippy_wasm/clippy.toml | 29 ++++ 14 files changed, 370 insertions(+), 188 deletions(-) create mode 100755 scripts/clippy_wasm.sh create mode 100644 scripts/clippy_wasm/clippy.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3ca1d3e8..2f21663a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -157,10 +157,7 @@ jobs: - run: ./scripts/wasm_bindgen_check.sh --skip-setup - name: Cranky wasm32 - uses: actions-rs/cargo@v1 - with: - command: cranky - args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings + run: ./scripts/clippy_wasm.sh # --------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index b0fd1471..f3dc1a9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,7 +1272,9 @@ dependencies = [ "log", "poll-promise", "serde", + "wasm-bindgen", "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/clippy.toml b/clippy.toml index f09ee277..31bbadb8 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,10 @@ -doc-valid-idents = ["AccessKit", ".."] +# There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm. + +msrv = "1.65" + +# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown +doc-valid-idents = [ + # You must also update the same list in the root `clippy.toml`! + "AccessKit", + "..", +] diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index f64caf6d..1d3e461d 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,7 +1,7 @@ //! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`]. //! When making changes to one you often also want to apply it to the other. -use std::time::{Duration, Instant}; +use std::time::Instant; use winit::event_loop::{ ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, @@ -19,7 +19,12 @@ use super::epi_integration::{self, EpiIntegration}; #[derive(Debug)] pub enum UserEvent { - RequestRepaint, + RequestRepaint { + when: Instant, + /// What the frame number was when the repaint was _requested_. + frame_nr: u64, + }, + #[cfg(feature = "accesskit")] AccessKitActionRequest(accesskit_winit::ActionRequestEvent), } @@ -58,6 +63,9 @@ enum EventResult { } trait WinitApp { + /// The current frame number, as reported by egui. + fn frame_nr(&self) -> u64; + fn is_focused(&self) -> bool; fn integration(&self) -> Option<&EpiIntegration>; @@ -66,7 +74,7 @@ trait WinitApp { fn save_and_destroy(&mut self); - fn paint(&mut self) -> EventResult; + fn run_ui_and_paint(&mut self) -> EventResult; fn on_event( &mut self, @@ -136,18 +144,30 @@ fn run_and_return( // See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/1619 winit::event::Event::RedrawEventsCleared if cfg!(windows) => { - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint() + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint() } winit::event::Event::RedrawRequested(_) if !cfg!(windows) => { - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint() + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint() } - winit::event::Event::UserEvent(UserEvent::RequestRepaint) - | winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { + winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { + if winit_app.frame_nr() == *frame_nr { + log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}"); + EventResult::RepaintAt(*when) + } else { + log::trace!("Got outdated UserEvent::RequestRepaint"); + EventResult::Wait // old request - we've already repainted + } + } + + winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { .. - }) => EventResult::RepaintNext, + }) => { + log::trace!("Woke up to check next_repaint_time"); + EventResult::Wait + } winit::event::Event::WindowEvent { window_id, .. } if winit_app.window().is_none() @@ -174,8 +194,8 @@ fn run_and_return( log::trace!("Repaint caused by winit::Event: {:?}", event); if cfg!(windows) { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint(); + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint(); } else { // Fix for https://github.com/emilk/egui/issues/2425 next_repaint_time = Instant::now(); @@ -196,18 +216,20 @@ fn run_and_return( } } - *control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { - None => { - if let Some(window) = winit_app.window() { - window.request_redraw(); - } - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - ControlFlow::Poll + *control_flow = if next_repaint_time <= Instant::now() { + if let Some(window) = winit_app.window() { + log::trace!("request_redraw"); + window.request_redraw(); } - Some(time_until_next_repaint) => { - ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint) + next_repaint_time = extremely_far_future(); + ControlFlow::Poll + } else { + let time_until_next = next_repaint_time.saturating_duration_since(Instant::now()); + if time_until_next < std::time::Duration::from_secs(10_000) { + log::trace!("WaitUntil {time_until_next:?}"); } - } + ControlFlow::WaitUntil(next_repaint_time) + }; }); log::debug!("eframe window closed"); @@ -239,18 +261,25 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + // See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/1619 winit::event::Event::RedrawEventsCleared if cfg!(windows) => { - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint() + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint() } winit::event::Event::RedrawRequested(_) if !cfg!(windows) => { - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint() + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint() } - winit::event::Event::UserEvent(UserEvent::RequestRepaint) - | winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { + winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { + if winit_app.frame_nr() == frame_nr { + EventResult::RepaintAt(when) + } else { + EventResult::Wait // old request - we've already repainted + } + } + + winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { .. - }) => EventResult::RepaintNext, + }) => EventResult::Wait, // We just woke up to check next_repaint_time event => match winit_app.on_event(event_loop, &event) { Ok(event_result) => event_result, @@ -265,8 +294,8 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + EventResult::RepaintNow => { if cfg!(windows) { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); - winit_app.paint(); + next_repaint_time = extremely_far_future(); + winit_app.run_ui_and_paint(); } else { // Fix for https://github.com/emilk/egui/issues/2425 next_repaint_time = Instant::now(); @@ -286,17 +315,15 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + } } - *control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { - None => { - if let Some(window) = winit_app.window() { - window.request_redraw(); - } - ControlFlow::Poll + *control_flow = if next_repaint_time <= Instant::now() { + if let Some(window) = winit_app.window() { + window.request_redraw(); } - Some(time_until_next_repaint) => { - ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint) - } - } + next_repaint_time = extremely_far_future(); + ControlFlow::Poll + } else { + ControlFlow::WaitUntil(next_repaint_time) + }; }) } @@ -601,8 +628,6 @@ mod glow_integration { // suspends and resumes. app_creator: Option, is_focused: bool, - - frame_nr: u64, } impl GlowWinitApp { @@ -619,7 +644,6 @@ mod glow_integration { running: None, app_creator: Some(app_creator), is_focused: true, - frame_nr: 0, } } @@ -698,12 +722,17 @@ mod glow_integration { { let event_loop_proxy = self.repaint_proxy.clone(); - integration.egui_ctx.set_request_repaint_callback(move || { - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint) - .ok(); - }); + integration + .egui_ctx + .set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.after; + let frame_nr = info.current_frame_nr; + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { when, frame_nr }) + .ok(); + }); } let app_creator = std::mem::take(&mut self.app_creator) @@ -734,6 +763,12 @@ mod glow_integration { } impl WinitApp for GlowWinitApp { + fn frame_nr(&self) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr()) + } + fn is_focused(&self) -> bool { self.is_focused } @@ -756,7 +791,7 @@ mod glow_integration { } } - fn paint(&mut self) -> EventResult { + fn run_ui_and_paint(&mut self) -> EventResult { if let Some(running) = &mut self.running { #[cfg(feature = "puffin")] puffin::GlobalProfiler::lock().new_frame(); @@ -820,7 +855,7 @@ mod glow_integration { #[cfg(feature = "__screenshot")] // give it time to settle: - if self.frame_nr == 2 { + if integration.egui_ctx.frame_nr() == 2 { if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { assert!( path.ends_with(".png"), @@ -871,8 +906,6 @@ mod glow_integration { std::thread::sleep(std::time::Duration::from_millis(10)); } - self.frame_nr += 1; - control_flow } else { EventResult::Wait @@ -1150,13 +1183,18 @@ mod wgpu_integration { { let event_loop_proxy = self.repaint_proxy.clone(); - integration.egui_ctx.set_request_repaint_callback(move || { - event_loop_proxy - .lock() - .unwrap() - .send_event(UserEvent::RequestRepaint) - .ok(); - }); + integration + .egui_ctx + .set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.after; + let frame_nr = info.current_frame_nr; + event_loop_proxy + .lock() + .unwrap() + .send_event(UserEvent::RequestRepaint { when, frame_nr }) + .ok(); + }); } let app_creator = std::mem::take(&mut self.app_creator) @@ -1186,6 +1224,12 @@ mod wgpu_integration { } impl WinitApp for WgpuWinitApp { + fn frame_nr(&self) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr()) + } + fn is_focused(&self) -> bool { self.is_focused } @@ -1214,7 +1258,7 @@ mod wgpu_integration { } } - fn paint(&mut self) -> EventResult { + fn run_ui_and_paint(&mut self) -> EventResult { if let (Some(running), Some(window)) = (&mut self.running, &self.window) { #[cfg(feature = "puffin")] puffin::GlobalProfiler::lock().new_frame(); @@ -1433,12 +1477,11 @@ mod wgpu_integration { } } -// ---------------------------------------------------------------------------- - #[cfg(feature = "wgpu")] pub use wgpu_integration::run_wgpu; -#[cfg(any(target_os = "windows", target_os = "macos"))] +// ---------------------------------------------------------------------------- + fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option { if options.follow_system_theme { window @@ -1449,9 +1492,8 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti } } -// Winit only reads the system theme on macOS and Windows. -// See: https://github.com/rust-windowing/winit/issues/1549 -#[cfg(not(any(target_os = "windows", target_os = "macos")))] -fn system_theme(_window: &winit::window::Window, _options: &NativeOptions) -> Option { - None +// ---------------------------------------------------------------------------- + +fn extremely_far_future() -> std::time::Instant { + std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000) } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 2f1fc3f7..e07df652 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -259,8 +259,8 @@ impl AppRunner { let needs_repaint: std::sync::Arc = Default::default(); { let needs_repaint = needs_repaint.clone(); - egui_ctx.set_request_repaint_callback(move || { - needs_repaint.repaint_asap(); + egui_ctx.set_request_repaint_callback(move |info| { + needs_repaint.repaint_after(info.after.as_secs_f64()); }); } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 44881cb5..b8a7f0cd 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -4,12 +4,15 @@ use egui::Key; use super::*; -struct IsDestroyed(pub bool); - +/// Calls `request_animation_frame` to schedule repaint. +/// +/// It will only paint if needed, but will always call `request_animation_frame` immediately. pub fn paint_and_schedule( runner_ref: &AppRunnerRef, panicked: Arc, ) -> Result<(), JsValue> { + struct IsDestroyed(pub bool); + fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result { let mut runner_lock = runner_ref.lock(); let is_destroyed = runner_lock.is_destroyed.fetch(); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c3aa9922..bcb61d2c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -8,6 +8,21 @@ use crate::{ }; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; +/// Information given to the backend about when it is time to repaint the ui. +/// +/// This is given in the callback set by [`Context::set_request_repaint_callback`]. +#[derive(Clone, Copy, Debug)] +pub struct RequestRepaintInfo { + /// Repaint after this duration. If zero, repaint as soon as possible. + pub after: std::time::Duration, + + /// The current frame number. + /// + /// This can be compared to [`Context::frame_nr`] to see if we've already + /// triggered the painting of the next frame. + pub current_frame_nr: u64, +} + // ---------------------------------------------------------------------------- struct WrappedTextureManager(Arc>); @@ -29,6 +44,94 @@ impl Default for WrappedTextureManager { } // ---------------------------------------------------------------------------- + +/// Logic related to repainting the ui. +struct Repaint { + /// The current frame number. + /// + /// Incremented at the end of each frame. + frame_nr: u64, + + /// The duration backend will poll for new events, before forcing another egui update + /// even if there's no new events. + /// + /// Also used to suppress multiple calls to the repaint callback during the same frame. + repaint_after: std::time::Duration, + + /// While positive, keep requesting repaints. Decrement at the end of each frame. + repaint_requests: u32, + request_repaint_callback: Option>, + + requested_repaint_last_frame: bool, +} + +impl Default for Repaint { + fn default() -> Self { + Self { + frame_nr: 0, + repaint_after: std::time::Duration::from_millis(100), + // Start with painting an extra frame to compensate for some widgets + // that take two frames before they "settle": + repaint_requests: 1, + request_repaint_callback: None, + requested_repaint_last_frame: false, + } + } +} + +impl Repaint { + fn request_repaint(&mut self) { + self.request_repaint_after(std::time::Duration::ZERO); + } + + fn request_repaint_after(&mut self, after: std::time::Duration) { + if after == std::time::Duration::ZERO { + // Do a few extra frames to let things settle. + // This is a bit of a hack, and we don't support it for `repaint_after` callbacks yet. + self.repaint_requests = 2; + } + + // We only re-call the callback if we get a lower duration, + // otherwise it's already been covered by the previous callback. + if after < self.repaint_after { + self.repaint_after = after; + + if let Some(callback) = &self.request_repaint_callback { + let info = RequestRepaintInfo { + after, + current_frame_nr: self.frame_nr, + }; + (callback)(info); + } + } + } + + fn start_frame(&mut self) { + // We are repainting; no need to reschedule a repaint unless the user asks for it again. + self.repaint_after = std::time::Duration::MAX; + } + + // returns how long to wait until repaint + fn end_frame(&mut self) -> std::time::Duration { + // if repaint_requests is greater than zero. just set the duration to zero for immediate + // repaint. if there's no repaint requests, then we can use the actual repaint_after instead. + let repaint_after = if self.repaint_requests > 0 { + self.repaint_requests -= 1; + std::time::Duration::ZERO + } else { + self.repaint_after + }; + self.repaint_after = std::time::Duration::MAX; + + self.requested_repaint_last_frame = repaint_after.is_zero(); + self.frame_nr += 1; + + repaint_after + } +} + +// ---------------------------------------------------------------------------- + #[derive(Default)] struct ContextImpl { /// `None` until the start of the first frame. @@ -50,18 +153,7 @@ struct ContextImpl { paint_stats: PaintStats, - /// the duration backend will poll for new events, before forcing another egui update - /// even if there's no new events. - repaint_after: std::time::Duration, - - /// While positive, keep requesting repaints. Decrement at the end of each frame. - repaint_requests: u32, - request_repaint_callback: Option>, - - /// used to suppress multiple calls to [`Self::request_repaint_callback`] during the same frame. - has_requested_repaint_this_frame: bool, - - requested_repaint_last_frame: bool, + repaint: Repaint, /// Written to during the frame. layer_rects_this_frame: ahash::HashMap>, @@ -77,7 +169,7 @@ struct ContextImpl { impl ContextImpl { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { - self.has_requested_repaint_this_frame = false; // allow new calls during the frame + self.repaint.start_frame(); if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() { new_raw_input.pixels_per_point = Some(new_pixels_per_point); @@ -95,7 +187,7 @@ impl ContextImpl { self.memory.begin_frame(&self.input, &new_raw_input); self.input = std::mem::take(&mut self.input) - .begin_frame(new_raw_input, self.requested_repaint_last_frame); + .begin_frame(new_raw_input, self.repaint.requested_repaint_last_frame); self.frame_state.begin_frame(&self.input); @@ -239,12 +331,7 @@ impl std::cmp::PartialEq for Context { impl Default for Context { fn default() -> Self { - Self(Arc::new(RwLock::new(ContextImpl { - // Start with painting an extra frame to compensate for some widgets - // that take two frames before they "settle": - repaint_requests: 1, - ..ContextImpl::default() - }))) + Self(Arc::new(RwLock::new(ContextImpl::default()))) } } @@ -850,6 +937,15 @@ impl Context { } } + /// The current frame number. + /// + /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// + /// Between calls to [`Self::run`], this is the frame number of the coming frame. + pub fn frame_nr(&self) -> u64 { + self.read(|ctx| ctx.repaint.frame_nr) + } + /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. /// /// If this is called at least once in a frame, then there will be another frame right after this. @@ -860,19 +956,13 @@ impl Context { /// (this will work on `eframe`). pub fn request_repaint(&self) { // request two frames of repaint, just to cover some corner cases (frame delays): - self.write(|ctx| { - ctx.repaint_requests = 2; - if let Some(callback) = &ctx.request_repaint_callback { - if !ctx.has_requested_repaint_this_frame { - (callback)(); - ctx.has_requested_repaint_this_frame = true; - } - } - }); + self.write(|ctx| ctx.repaint.request_repaint()); } - /// Request repaint after the specified duration elapses in the case of no new input - /// events being received. + /// Request repaint after at most the specified duration elapses. + /// + /// The backend can chose to repaint sooner, for instance if some other code called + /// this method with a lower duration, or if new events arrived. /// /// The function can be multiple times, but only the *smallest* duration will be considered. /// So, if the function is called two times with `1 second` and `2 seconds`, egui will repaint @@ -900,7 +990,7 @@ impl Context { /// during app idle time where we are not receiving any new input events. pub fn request_repaint_after(&self, duration: std::time::Duration) { // Maybe we can check if duration is ZERO, and call self.request_repaint()? - self.write(|ctx| ctx.repaint_after = ctx.repaint_after.min(duration)); + self.write(|ctx| ctx.repaint.request_repaint_after(duration)); } /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. @@ -908,9 +998,12 @@ impl Context { /// This lets you wake up a sleeping UI thread. /// /// Note that only one callback can be set. Any new call overrides the previous callback. - pub fn set_request_repaint_callback(&self, callback: impl Fn() + Send + Sync + 'static) { + pub fn set_request_repaint_callback( + &self, + callback: impl Fn(RequestRepaintInfo) + Send + Sync + 'static, + ) { let callback = Box::new(callback); - self.write(|ctx| ctx.request_repaint_callback = Some(callback)); + self.write(|ctx| ctx.repaint.request_repaint_callback = Some(callback)); } /// Tell `egui` which fonts to use. @@ -1164,28 +1257,7 @@ impl Context { } } - // if repaint_requests is greater than zero. just set the duration to zero for immediate - // repaint. if there's no repaint requests, then we can use the actual repaint_after instead. - let repaint_after = self.write(|ctx| { - if ctx.repaint_requests > 0 { - ctx.repaint_requests -= 1; - std::time::Duration::ZERO - } else { - ctx.repaint_after - } - }); - - self.write(|ctx| { - ctx.requested_repaint_last_frame = repaint_after.is_zero(); - - ctx.has_requested_repaint_this_frame = false; // allow new calls between frames - - // make sure we reset the repaint_after duration. - // otherwise, if repaint_after is low, then any widget setting repaint_after next frame, - // will fail to overwrite the previous lower value. and thus, repaints will never - // go back to higher values. - ctx.repaint_after = std::time::Duration::MAX; - }); + let repaint_after = self.write(|ctx| ctx.repaint.end_frame()); let shapes = self.drain_paint_lists(); FullOutput { diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index f0364dac..f7eef71a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -354,7 +354,7 @@ pub mod text { pub use { containers::*, - context::Context, + context::{Context, RequestRepaintInfo}, data::{ input::*, output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo}, diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 49676fe2..0c011e7b 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -63,4 +63,6 @@ env_logger = "0.10" # web: [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" +wasm-bindgen = "=0.2.84" wasm-bindgen-futures = "0.4" +web-sys = "0.3" diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index d71538cf..fabbe86a 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -1,5 +1,3 @@ -use egui::Widget; - /// How often we repaint the demo app by default #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RunMode { @@ -43,6 +41,7 @@ impl Default for RunMode { // ---------------------------------------------------------------------------- +#[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct BackendPanel { @@ -52,9 +51,6 @@ pub struct BackendPanel { // go back to [`RunMode::Reactive`] mode each time we start run_mode: RunMode, - #[cfg_attr(feature = "serde", serde(skip))] - repaint_after_seconds: f32, - /// current slider value for current gui scale #[cfg_attr(feature = "serde", serde(skip))] pixels_per_point: Option, @@ -65,19 +61,6 @@ pub struct BackendPanel { egui_windows: EguiWindows, } -impl Default for BackendPanel { - fn default() -> Self { - Self { - open: false, - run_mode: Default::default(), - repaint_after_seconds: 1.0, - pixels_per_point: None, - frame_history: Default::default(), - egui_windows: Default::default(), - } - } -} - impl BackendPanel { pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { self.frame_history @@ -90,9 +73,6 @@ impl BackendPanel { } RunMode::Reactive => { // let the computer rest for a bit - ctx.request_repaint_after(std::time::Duration::from_secs_f32( - self.repaint_after_seconds, - )); } } } @@ -271,17 +251,27 @@ impl BackendPanel { } else { ui.label("Only running UI code when there are animations or input."); - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("(but at least every "); - egui::DragValue::new(&mut self.repaint_after_seconds) - .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(")"); - }); + // Add a test for `request_repaint_after`, but only in debug + // builds to keep the noise down in the official demo. + if cfg!(debug_assertions) { + ui.collapsing("More…", |ui| { + ui.horizontal(|ui| { + ui.label("Frame number:"); + ui.monospace(ui.ctx().frame_nr().to_string()); + }); + if ui + .button("Wait 2s, then request repaint after another 3s") + .clicked() + { + log::info!("Waiting 2s before requesting repaint..."); + let ctx = ui.ctx().clone(); + call_after_delay(std::time::Duration::from_secs(2), move || { + log::info!("Request a repaint in 3s..."); + ctx.request_repaint_after(std::time::Duration::from_secs(3)); + }); + } + }); + } } } } @@ -394,3 +384,28 @@ impl EguiWindows { }); } } + +// ---------------------------------------------------------------------------- + +#[cfg(not(target_arch = "wasm32"))] +fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) { + std::thread::spawn(move || { + std::thread::sleep(delay); + f(); + }); +} + +#[cfg(target_arch = "wasm32")] +fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) { + use wasm_bindgen::prelude::*; + let window = web_sys::window().unwrap(); + let closure = Closure::once(f); + let delay_ms = delay.as_millis() as _; + window + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + delay_ms, + ) + .unwrap(); + closure.forget(); // We must forget it, or else the callback is canceled on drop +} diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index c2672a05..946f8ff9 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -33,12 +33,6 @@ impl FrameHistory { } pub fn ui(&mut self, ui: &mut egui::Ui) { - ui.label(format!( - "Total frames painted: {}", - self.frame_times.total_count() - )) - .on_hover_text("Includes this frame."); - ui.label(format!( "Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time() diff --git a/scripts/build_demo_web.sh b/scripts/build_demo_web.sh index 35ec1f71..c55f2c96 100755 --- a/scripts/build_demo_web.sh +++ b/scripts/build_demo_web.sh @@ -3,29 +3,41 @@ set -eu script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cd "$script_path/.." +./scripts/setup_web.sh + +# This is required to enable the web_sys clipboard API which eframe web uses +# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html +# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html +export RUSTFLAGS=--cfg=web_sys_unstable_apis + CRATE_NAME="egui_demo_app" + # NOTE: persistence use up about 400kB (10%) of the WASM! FEATURES="glow,http,persistence,web_screen_reader" OPEN=false OPTIMIZE=false +BUILD=debug +BUILD_FLAGS="" while test $# -gt 0; do case "$1" in -h|--help) - echo "build_demo_web.sh [--optimize] [--open]" + echo "build_demo_web.sh [--release] [--open]" echo "" - echo " --optimize: Enable optimization step" - echo " Runs wasm-opt." - echo " NOTE: --optimize also removes debug symbols which are otherwise useful for in-browser profiling." + echo " --release: Build with --release, and enable extra optimization step" + echo " Runs wasm-opt." + echo " NOTE: --release also removes debug symbols which are otherwise useful for in-browser profiling." echo "" echo " --open: Open the result in a browser" exit 0 ;; - -O|--optimize) + --release) shift OPTIMIZE=true + BUILD="release" + BUILD_FLAGS="--release" ;; --open) @@ -39,22 +51,14 @@ while test $# -gt 0; do esac done -./scripts/setup_web.sh - -# This is required to enable the web_sys clipboard API which eframe web uses -# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html -# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html -export RUSTFLAGS=--cfg=web_sys_unstable_apis - # Clear output from old stuff: rm -f "docs/${CRATE_NAME}_bg.wasm" echo "Building rust…" -BUILD=release (cd crates/$CRATE_NAME && cargo build \ - --release \ + ${BUILD_FLAGS} \ --lib \ --target wasm32-unknown-unknown \ --no-default-features \ diff --git a/scripts/clippy_wasm.sh b/scripts/clippy_wasm.sh new file mode 100755 index 00000000..b4a1e3e6 --- /dev/null +++ b/scripts/clippy_wasm.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# This scripts run clippy on the wasm32-unknown-unknown target, +# using a special clippy.toml config file which forbids a few more things. + +set -eu +script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +cd "$script_path/.." +set -x + +# Use scripts/clippy_wasm/clippy.toml +export CLIPPY_CONF_DIR="scripts/clippy_wasm" + +cargo cranky --all-features --target wasm32-unknown-unknown --target-dir target_wasm -p egui_demo_app --lib -- --deny warnings diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml new file mode 100644 index 00000000..e2ec8be9 --- /dev/null +++ b/scripts/clippy_wasm/clippy.toml @@ -0,0 +1,29 @@ +# This is used by `scripts/clippy_wasm.sh` so we can forbid some methods that are not available in wasm. +# +# We cannot forbid all these methods in the main `clippy.toml` because of +# https://github.com/rust-lang/rust-clippy/issues/10406 + +msrv = "1.65" + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods +disallowed-methods = [ + "std::time::Instant::now", # use `instant` crate instead for wasm/web compatibility + "std::time::Duration::elapsed", # use `instant` crate instead for wasm/web compatibility + "std::time::SystemTime::now", # use `instant` or `time` crates instead for wasm/web compatibility + + # Cannot spawn threads on wasm: + "std::thread::spawn", +] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types +disallowed-types = [ + # Cannot spawn threads on wasm: + "std::thread::Builder", +] + +# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown +doc-valid-idents = [ + # You must also update the same list in the root `clippy.toml`! + "AccessKit", + "..", +]