diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1fc513679..374d1f7d581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w ### Changed * `PaintCallback` shapes now require the whole callback to be put in an `Arc` with the value being a backend-specific callback type. ([#1684](https://github.com/emilk/egui/pull/1684)) +* Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode.([#1694](https://github.com/emilk/egui/pull/1694)). ### Fixed 🐛 * Fixed `ImageButton`'s changing background padding on hover ([#1595](https://github.com/emilk/egui/pull/1595)). diff --git a/eframe/src/native/run.rs b/eframe/src/native/run.rs index 606d85d1bf9..fbfceb0afe5 100644 --- a/eframe/src/native/run.rs +++ b/eframe/src/native/run.rs @@ -107,7 +107,7 @@ pub fn run_glow( let egui::FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = integration.update(app.as_mut(), window); @@ -133,9 +133,18 @@ pub fn run_glow( *control_flow = if integration.should_quit() { winit::event_loop::ControlFlow::Exit - } else if needs_repaint { + } else if repaint_after.is_zero() { window.request_redraw(); winit::event_loop::ControlFlow::Poll + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) } else { winit::event_loop::ControlFlow::Wait }; @@ -152,7 +161,6 @@ pub fn run_glow( std::thread::sleep(std::time::Duration::from_millis(10)); } }; - match event { // Platform-dependent event handlers to workaround a winit bug // See: https://github.com/rust-windowing/winit/issues/987 @@ -194,7 +202,12 @@ pub fn run_glow( painter.destroy(); } winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(), - _ => (), + winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { + .. + }) => { + window.request_redraw(); + } + _ => {} } }); } @@ -279,7 +292,7 @@ pub fn run_wgpu( let egui::FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = integration.update(app.as_mut(), window); @@ -300,9 +313,18 @@ pub fn run_wgpu( *control_flow = if integration.should_quit() { winit::event_loop::ControlFlow::Exit - } else if needs_repaint { + } else if repaint_after.is_zero() { window.request_redraw(); winit::event_loop::ControlFlow::Poll + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) } else { winit::event_loop::ControlFlow::Wait }; @@ -376,6 +398,11 @@ pub fn run_wgpu( painter.destroy(); } winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(), + winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { + .. + }) => { + window.request_redraw(); + } _ => (), } }); diff --git a/eframe/src/web/backend.rs b/eframe/src/web/backend.rs index a636f40550a..71a6aa1a68f 100644 --- a/eframe/src/web/backend.rs +++ b/eframe/src/web/backend.rs @@ -245,10 +245,10 @@ impl AppRunner { Ok(()) } - /// Returns `true` if egui requests a repaint. + /// Returns how long to wait until the next repaint. /// /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> Result<(bool, Vec), JsValue> { + pub fn logic(&mut self) -> Result<(std::time::Duration, Vec), JsValue> { let frame_start = now_sec(); resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); @@ -260,7 +260,7 @@ impl AppRunner { }); let egui::FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = full_output; @@ -282,7 +282,7 @@ impl AppRunner { } self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - Ok((needs_repaint, clipped_primitives)) + Ok((repaint_after, clipped_primitives)) } pub fn clear_color_buffer(&self) { diff --git a/eframe/src/web/events.rs b/eframe/src/web/events.rs index 26e7fc34db8..be679588cc7 100644 --- a/eframe/src/web/events.rs +++ b/eframe/src/web/events.rs @@ -9,11 +9,12 @@ pub fn paint_and_schedule( let mut runner_lock = runner_ref.lock(); if runner_lock.needs_repaint.fetch_and_clear() { runner_lock.clear_color_buffer(); - let (needs_repaint, clipped_primitives) = runner_lock.logic()?; + let (repaint_after, clipped_primitives) = runner_lock.logic()?; runner_lock.paint(&clipped_primitives)?; - if needs_repaint { + 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.auto_save(); } diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 6ce5120437d..b869aed96c7 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -475,7 +475,6 @@ impl State { mutable_text_under_cursor: _, // only used in eframe web text_cursor_pos, } = platform_output; - self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI self.set_cursor_icon(window, cursor_icon); diff --git a/egui/src/context.rs b/egui/src/context.rs index 17c86327f4b..dc58bb61eea 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -28,7 +28,6 @@ impl Default for WrappedTextureManager { } // ---------------------------------------------------------------------------- - #[derive(Default)] struct ContextImpl { /// `None` until the start of the first frame. @@ -47,7 +46,9 @@ struct ContextImpl { output: PlatformOutput, 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_callbacks: Option>, @@ -574,6 +575,39 @@ impl Context { } } + /// Request repaint after the specified duration elapses in the case of no new input + /// events being received. + /// + /// 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 + /// after `1 second` + /// + /// This is primarily useful for applications who would like to save battery by avoiding wasted + /// redraws when the app is not in focus. But sometimes the GUI of the app might become stale + /// and outdated if it is not updated for too long. + /// + /// Lets say, something like a stop watch widget that displays the time in seconds. You would waste + /// resources repainting multiple times within the same second (when you have no input), + /// just calculate the difference of duration between current time and next second change, + /// and call this function, to make sure that you are displaying the latest updated time, but + /// not wasting resources on needless repaints within the same second. + /// + /// NOTE: only works if called before `Context::end_frame()`. to force egui to update, + /// use `Context::request_repaint()` instead. + /// + /// ### Quirk: + /// Duration begins at the next frame. lets say for example that its a very inefficient app + /// and takes 500 milliseconds per frame at 2 fps. The widget / user might want a repaint in + /// next 500 milliseconds. Now, app takes 1000 ms per frame (1 fps) because the backend event + /// timeout takes 500 milli seconds AFTER the vsync swap buffer. + /// So, its not that we are requesting repaint within X duration. We are rather timing out + /// 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()? + let mut ctx = self.write(); + ctx.repaint_after = ctx.repaint_after.min(duration); + } + /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. /// /// This lets you wake up a sleeping UI thread. @@ -805,19 +839,26 @@ impl Context { let platform_output: PlatformOutput = std::mem::take(&mut self.output()); - let needs_repaint = if self.read().repaint_requests > 0 { + // 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.read().repaint_requests > 0 { self.write().repaint_requests -= 1; - true + std::time::Duration::ZERO } else { - false + self.read().repaint_after }; - self.write().requested_repaint_last_frame = needs_repaint; + self.write().requested_repaint_last_frame = repaint_after.is_zero(); + // 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. + self.write().repaint_after = std::time::Duration::MAX; let shapes = self.drain_paint_lists(); FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 741112848ef..385cd6731d7 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -10,10 +10,15 @@ pub struct FullOutput { /// Non-rendering related output. pub platform_output: PlatformOutput, - /// If `true`, egui is requesting immediate repaint (i.e. on the next frame). + /// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame). /// /// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`. - pub needs_repaint: bool, + /// + /// If `Duration` is greater than zero, egui wants to be repainted at or before the specified + /// duration elapses. when in reactive mode, egui spends forever waiting for input and only then, + /// will it repaint itself. this can be used to make sure that backend will only wait for a + /// specified amount of time, and repaint egui without any new input. + pub repaint_after: std::time::Duration, /// Texture changes since last frame (including the font texture). /// @@ -32,13 +37,13 @@ impl FullOutput { pub fn append(&mut self, newer: Self) { let Self { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = newer; self.platform_output.append(platform_output); - self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint + self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint self.textures_delta.append(textures_delta); self.shapes = shapes; // Only paint the latest } @@ -49,7 +54,7 @@ impl FullOutput { /// You can access (and modify) this with [`crate::Context::output`]. /// /// The backend should use this. -#[derive(Clone, Default, PartialEq)] +#[derive(Default, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PlatformOutput { /// Set the cursor to this icon. diff --git a/egui_demo_app/src/backend_panel.rs b/egui_demo_app/src/backend_panel.rs index 28810075197..854f24ad01b 100644 --- a/egui_demo_app/src/backend_panel.rs +++ b/egui_demo_app/src/backend_panel.rs @@ -1,3 +1,5 @@ +use egui::Widget; + /// How often we repaint the demo app by default #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RunMode { @@ -41,7 +43,6 @@ impl Default for RunMode { // ---------------------------------------------------------------------------- -#[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct BackendPanel { @@ -51,6 +52,10 @@ pub struct BackendPanel { // go back to [`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, + /// current slider value for current gui scale #[cfg_attr(feature = "serde", serde(skip))] pixels_per_point: Option, @@ -61,14 +66,32 @@ pub struct BackendPanel { egui_windows: EguiWindows, } +impl Default for BackendPanel { + fn default() -> Self { + Self { + open: false, + run_mode: Default::default(), + repaint_after_timeout: std::time::Duration::from_secs(1), + 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 .on_new_frame(ctx.input().time, frame.info().cpu_usage); - if self.run_mode == RunMode::Continuous { - // Tell the backend to repaint as soon as possible - ctx.request_repaint(); + 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(); + } } } @@ -220,6 +243,16 @@ 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); + } } } } diff --git a/egui_glium/examples/native_texture.rs b/egui_glium/examples/native_texture.rs index de7ec818cdf..ae09ae9d14d 100644 --- a/egui_glium/examples/native_texture.rs +++ b/egui_glium/examples/native_texture.rs @@ -24,7 +24,7 @@ fn main() { let mut redraw = || { let mut quit = false; - let needs_repaint = egui_glium.run(&display, |egui_ctx| { + let repaint_after = egui_glium.run(&display, |egui_ctx| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { if ui .add(egui::Button::image_and_text( @@ -44,9 +44,13 @@ fn main() { *control_flow = if quit { glutin::event_loop::ControlFlow::Exit - } else if needs_repaint { + } else if repaint_after.is_zero() { display.gl_window().window().request_redraw(); glutin::event_loop::ControlFlow::Poll + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + glutin::event_loop::ControlFlow::WaitUntil(repaint_after_instant) } else { glutin::event_loop::ControlFlow::Wait }; @@ -85,6 +89,11 @@ fn main() { display.gl_window().window().request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead } + glutin::event::Event::NewEvents(glutin::event::StartCause::ResumeTimeReached { + .. + }) => { + display.gl_window().window().request_redraw(); + } _ => (), } diff --git a/egui_glium/examples/pure_glium.rs b/egui_glium/examples/pure_glium.rs index d1d81a91828..778f4aabb0f 100644 --- a/egui_glium/examples/pure_glium.rs +++ b/egui_glium/examples/pure_glium.rs @@ -14,7 +14,7 @@ fn main() { let mut redraw = || { let mut quit = false; - let needs_repaint = egui_glium.run(&display, |egui_ctx| { + let repaint_after = egui_glium.run(&display, |egui_ctx| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { @@ -25,9 +25,13 @@ fn main() { *control_flow = if quit { glutin::event_loop::ControlFlow::Exit - } else if needs_repaint { + } else if repaint_after.is_zero() { display.gl_window().window().request_redraw(); glutin::event_loop::ControlFlow::Poll + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + glutin::event_loop::ControlFlow::WaitUntil(repaint_after_instant) } else { glutin::event_loop::ControlFlow::Wait }; @@ -66,7 +70,11 @@ fn main() { display.gl_window().window().request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead } - + glutin::event::Event::NewEvents(glutin::event::StartCause::ResumeTimeReached { + .. + }) => { + display.gl_window().window().request_redraw(); + } _ => (), } }); diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 84a2459fb0b..fa7437f9d94 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -56,13 +56,17 @@ impl EguiGlium { /// Returns `true` if egui requests a repaint. /// /// Call [`Self::paint`] later to paint. - pub fn run(&mut self, display: &glium::Display, run_ui: impl FnMut(&egui::Context)) -> bool { + pub fn run( + &mut self, + display: &glium::Display, + run_ui: impl FnMut(&egui::Context), + ) -> std::time::Duration { let raw_input = self .egui_winit .take_egui_input(display.gl_window().window()); let egui::FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = self.egui_ctx.run(raw_input, run_ui); @@ -76,7 +80,7 @@ impl EguiGlium { self.shapes = shapes; self.textures_delta.append(textures_delta); - needs_repaint + repaint_after } /// Paint the results of the last call to [`Self::run`]. diff --git a/egui_glow/examples/pure_glow.rs b/egui_glow/examples/pure_glow.rs index 3af2f8bc5f1..1e10cb1dbe4 100644 --- a/egui_glow/examples/pure_glow.rs +++ b/egui_glow/examples/pure_glow.rs @@ -16,7 +16,7 @@ fn main() { let mut redraw = || { let mut quit = false; - let needs_repaint = egui_glow.run(gl_window.window(), |egui_ctx| { + let repaint_after = egui_glow.run(gl_window.window(), |egui_ctx| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { @@ -28,9 +28,13 @@ fn main() { *control_flow = if quit { glutin::event_loop::ControlFlow::Exit - } else if needs_repaint { + } else if repaint_after.is_zero() { gl_window.window().request_redraw(); glutin::event_loop::ControlFlow::Poll + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + glutin::event_loop::ControlFlow::WaitUntil(repaint_after_instant) } else { glutin::event_loop::ControlFlow::Wait }; @@ -82,6 +86,11 @@ fn main() { glutin::event::Event::LoopDestroyed => { egui_glow.destroy(); } + glutin::event::Event::NewEvents(glutin::event::StartCause::ResumeTimeReached { + .. + }) => { + gl_window.window().request_redraw(); + } _ => (), } diff --git a/egui_glow/src/winit.rs b/egui_glow/src/winit.rs index 52a0301f3d6..786cb18a43c 100644 --- a/egui_glow/src/winit.rs +++ b/egui_glow/src/winit.rs @@ -41,18 +41,18 @@ impl EguiGlow { self.egui_winit.on_event(&self.egui_ctx, event) } - /// Returns `true` if egui requests a repaint. + /// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events. /// /// Call [`Self::paint`] later to paint. pub fn run( &mut self, window: &winit::window::Window, run_ui: impl FnMut(&egui::Context), - ) -> bool { + ) -> std::time::Duration { let raw_input = self.egui_winit.take_egui_input(window); let egui::FullOutput { platform_output, - needs_repaint, + repaint_after, textures_delta, shapes, } = self.egui_ctx.run(raw_input, run_ui); @@ -62,7 +62,7 @@ impl EguiGlow { self.shapes = shapes; self.textures_delta.append(textures_delta); - needs_repaint + repaint_after } /// Paint the results of the last call to [`Self::run`].