From 4747c8fcee756c84151fd670123a6b68fa0de1b1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 20 Apr 2023 09:09:10 +0200 Subject: [PATCH] request_repaint_after also fires the request_repaint callback --- crates/eframe/src/native/run.rs | 149 ++++++++++++++++++++------------ crates/egui/src/context.rs | 79 +++++++++++++---- crates/egui/src/lib.rs | 2 +- 3 files changed, 156 insertions(+), 74 deletions(-) diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index f64caf6d9..6571bf445 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -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, @@ -137,17 +145,26 @@ fn run_and_return( // 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() + 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() + 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, + }) => EventResult::Wait, // We just woke up to check next_repaint_time winit::event::Event::WindowEvent { window_id, .. } if winit_app.window().is_none() @@ -175,7 +192,7 @@ fn run_and_return( 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(); + winit_app.run_ui_and_paint(); } else { // Fix for https://github.com/emilk/egui/issues/2425 next_repaint_time = Instant::now(); @@ -196,18 +213,16 @@ 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 = Instant::now() + Duration::from_secs(1_000_000_000); + ControlFlow::Poll + } else { + ControlFlow::WaitUntil(next_repaint_time) + }; }); log::debug!("eframe window closed"); @@ -240,17 +255,24 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + // 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() + 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() + 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, @@ -266,7 +288,7 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + 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(); + winit_app.run_ui_and_paint(); } else { // Fix for https://github.com/emilk/egui/issues/2425 next_repaint_time = Instant::now(); @@ -286,17 +308,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 = Instant::now() + Duration::from_secs(1_000_000_000); + ControlFlow::Poll + } else { + ControlFlow::WaitUntil(next_repaint_time) + }; }) } @@ -601,8 +621,6 @@ mod glow_integration { // suspends and resumes. app_creator: Option, is_focused: bool, - - frame_nr: u64, } impl GlowWinitApp { @@ -619,7 +637,6 @@ mod glow_integration { running: None, app_creator: Some(app_creator), is_focused: true, - frame_nr: 0, } } @@ -698,12 +715,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 +756,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 +784,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 +848,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 +899,6 @@ mod glow_integration { std::thread::sleep(std::time::Duration::from_millis(10)); } - self.frame_nr += 1; - control_flow } else { EventResult::Wait @@ -1150,13 +1176,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 +1217,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 +1251,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(); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 21cc42d51..3778e4278 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 curent 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>); @@ -32,16 +47,20 @@ impl Default for WrappedTextureManager { /// Logic related to repainting the ui. struct Repaint { - /// the duration backend will poll for new events, before forcing another egui update + /// 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>, - - /// used to suppress multiple calls to [`Self::request_repaint_callback`] during the same frame. - has_requested_repaint_this_frame: bool, + request_repaint_callback: Option>, requested_repaint_last_frame: bool, } @@ -49,12 +68,12 @@ struct Repaint { 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, - has_requested_repaint_this_frame: false, requested_repaint_last_frame: false, } } @@ -62,18 +81,33 @@ impl Default for Repaint { impl Repaint { fn request_repaint(&mut self) { - self.repaint_requests = 2; - if let Some(callback) = &self.request_repaint_callback { - if !self.has_requested_repaint_this_frame { - (callback)(); - self.has_requested_repaint_this_frame = true; + 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 request_repaint_after(&mut self, duration: std::time::Duration) { - self.repaint_after = self.repaint_after.min(duration); - } + #[allow(clippy::unused_self)] + fn start_frame(&mut self) {} // returns how long to wait until repaint fn end_frame(&mut self) -> std::time::Duration { @@ -88,7 +122,6 @@ impl Repaint { self.repaint_after = std::time::Duration::MAX; self.requested_repaint_last_frame = repaint_after.is_zero(); - self.has_requested_repaint_this_frame = false; // allow new calls between frames repaint_after } @@ -133,7 +166,7 @@ struct ContextImpl { impl ContextImpl { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { - self.repaint.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); @@ -901,6 +934,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. @@ -951,7 +993,10 @@ 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.repaint.request_repaint_callback = Some(callback)); } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index f0364dacd..f7eef71a9 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},