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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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)).
Expand Down
39 changes: 33 additions & 6 deletions eframe/src/native/run.rs
Original file line number Diff line number Diff line change
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 Expand Up @@ -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();
}
_ => (),
}
});
Expand Down
8 changes: 4 additions & 4 deletions eframe/src/web/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
5 changes: 3 additions & 2 deletions eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
coderedart marked this conversation as resolved.
Show resolved Hide resolved
}
// TODO: schedule a repaint after `repaint_after` when it is not zero
runner_lock.auto_save();
}

Expand Down
1 change: 0 additions & 1 deletion egui-winit/src/lib.rs
Original file line number Diff line number Diff line change
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: 48 additions & 7 deletions egui/src/context.rs
Original file line number Diff line number Diff line change
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
/// 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.
Expand Down Expand Up @@ -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.
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 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);
}
}
}
}
Expand Down
Loading