Add Context::request_repaint_after (#1694)

This commit is contained in:
Red Artist 2022-06-22 16:49:13 +05:30 committed by GitHub
parent 1a89cb35e1
commit 935913b1ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 180 additions and 43 deletions

View file

@ -16,6 +16,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<dyn Any>` 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)).

View file

@ -120,7 +120,7 @@ pub fn run_glow(
let egui::FullOutput {
platform_output,
needs_repaint,
repaint_after,
textures_delta,
shapes,
} = integration.update(app.as_mut(), window);
@ -148,9 +148,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
};
@ -167,7 +176,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
@ -209,7 +217,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();
}
_ => {}
}
});
}
@ -298,7 +311,7 @@ pub fn run_wgpu(
let egui::FullOutput {
platform_output,
needs_repaint,
repaint_after,
textures_delta,
shapes,
} = integration.update(app.as_mut(), window);
@ -319,9 +332,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
};
@ -395,6 +417,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();
}
_ => (),
}
});

View file

@ -251,10 +251,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<egui::ClippedPrimitive>), JsValue> {
pub fn logic(&mut self) -> Result<(std::time::Duration, Vec<egui::ClippedPrimitive>), JsValue> {
let frame_start = now_sec();
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
@ -266,7 +266,7 @@ impl AppRunner {
});
let egui::FullOutput {
platform_output,
needs_repaint,
repaint_after,
textures_delta,
shapes,
} = full_output;
@ -288,7 +288,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) {

View file

@ -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();
}

View file

@ -479,7 +479,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);

View file

@ -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<Box<dyn Fn() + Send + Sync>>,
@ -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,
}

View file

@ -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.

View file

@ -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<f32>,
@ -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);
}
}
}
}

View file

@ -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();
}
_ => (),
}

View file

@ -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();
}
_ => (),
}
});

View file

@ -60,13 +60,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);
@ -80,7 +84,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`].

View file

@ -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();
}
_ => (),
}

View file

@ -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`].