Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Context::request_repaint_after #1694

Merged
merged 18 commits into from Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 28 additions & 6 deletions eframe/src/native/run.rs
Expand Up @@ -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);
Expand All @@ -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
};
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to combine these with winit::event::Event::UserEvent(RequestRepaintEvent | winit::event::Event::NewEvents…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that we might see some sort of issue with repaint_after feature once people start using it.

so, i thought it would be better for debugging or adding special case handling for ResumeTimeReached event if both are treated as separate events.

..
}) => {
window.request_redraw();
}
_ => {}
coderedart marked this conversation as resolved.
Show resolved Hide resolved
}
});
}
Expand Down Expand Up @@ -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);
Expand All @@ -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
};
Expand Down
6 changes: 3 additions & 3 deletions eframe/src/web/backend.rs
Expand Up @@ -248,7 +248,7 @@ impl AppRunner {
/// Returns `true` if egui requests a repaint.
emilk marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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());
Expand All @@ -260,7 +260,7 @@ impl AppRunner {
});
let egui::FullOutput {
platform_output,
needs_repaint,
repaint_after,
textures_delta,
shapes,
} = full_output;
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions eframe/src/web/events.rs
Expand Up @@ -9,9 +9,9 @@ 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();
coderedart marked this conversation as resolved.
Show resolved Hide resolved
}
runner_lock.auto_save();
Expand Down
1 change: 0 additions & 1 deletion egui-winit/src/lib.rs
Expand Up @@ -475,7 +475,6 @@ impl State {
mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos,
} = platform_output;

coderedart marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down
55 changes: 44 additions & 11 deletions egui/src/context.rs
Expand Up @@ -28,7 +28,6 @@ impl Default for WrappedTextureManager {
}

// ----------------------------------------------------------------------------

coderedart marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Default)]
struct ContextImpl {
/// `None` until the start of the first frame.
Expand All @@ -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>>,
Expand Down Expand Up @@ -574,6 +575,39 @@ impl Context {
}
}

/// request repaint after the specified duration elapses in the case of no new input
emilk marked this conversation as resolved.
Show resolved Hide resolved
/// events being received.
///
/// function can be multiple times, but only the *smallest* duration will be considered.
coderedart marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
Expand Down Expand Up @@ -805,19 +839,18 @@ impl Context {

let platform_output: PlatformOutput = std::mem::take(&mut self.output());

let needs_repaint = if self.read().repaint_requests > 0 {
self.write().repaint_requests -= 1;
true
} else {
false
};
self.write().requested_repaint_last_frame = needs_repaint;

let repaint_after = self.read().repaint_after;
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.
emilk marked this conversation as resolved.
Show resolved Hide resolved
self.write().repaint_after = std::time::Duration::MAX;
let shapes = self.drain_paint_lists();

FullOutput {
platform_output,
needs_repaint,
repaint_after,
textures_delta,
shapes,
}
Expand Down
15 changes: 10 additions & 5 deletions egui/src/data/output.rs
Expand Up @@ -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).
///
Expand All @@ -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
}
Expand All @@ -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.
Expand Down
41 changes: 37 additions & 4 deletions 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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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>,
Expand All @@ -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();
}
}
}

Expand Down Expand Up @@ -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 milli_seconds = self.repaint_after_timeout.as_secs_f32();
if egui::DragValue::new(&mut milli_seconds)
.clamp_range(0.1..=10.0)
.ui(ui)
.changed()
{
self.repaint_after_timeout = std::time::Duration::from_secs_f32(milli_seconds);
coderedart marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions egui_glium/src/lib.rs
Expand Up @@ -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);
Expand All @@ -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`].
Expand Down