Fix: `request_repaint_after` works even when called from background thread (#2939)

* Refactor repaint logic

* request_repaint_after also fires the request_repaint callback

* Bug fixes

* Add test to egui_demo_app

* build_demo_web: build debug unless --release is specified

* Fix the web backend too

* Run special clippy for wasm, forbidding some types/methods

* Remove wasm_bindgen_check.sh

* Fix typos

* Revert "Remove wasm_bindgen_check.sh"

This reverts commit 92dde253446a6930f34f2fcf67f76bc11669ec3b.

* Only run cranky/clippy once
This commit is contained in:
Emil Ernerfeldt 2023-04-20 10:56:52 +02:00 committed by GitHub
parent d46cf067ea
commit 834e2e9f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 370 additions and 188 deletions

View File

@ -157,10 +157,7 @@ jobs:
- run: ./scripts/wasm_bindgen_check.sh --skip-setup - run: ./scripts/wasm_bindgen_check.sh --skip-setup
- name: Cranky wasm32 - name: Cranky wasm32
uses: actions-rs/cargo@v1 run: ./scripts/clippy_wasm.sh
with:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

2
Cargo.lock generated
View File

@ -1272,7 +1272,9 @@ dependencies = [
"log", "log",
"poll-promise", "poll-promise",
"serde", "serde",
"wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys",
] ]
[[package]] [[package]]

View File

@ -1 +1,10 @@
doc-valid-idents = ["AccessKit", ".."] # There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm.
msrv = "1.65"
# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`!
"AccessKit",
"..",
]

View File

@ -1,7 +1,7 @@
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`]. //! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
//! When making changes to one you often also want to apply it to the other. //! When making changes to one you often also want to apply it to the other.
use std::time::{Duration, Instant}; use std::time::Instant;
use winit::event_loop::{ use winit::event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
@ -19,7 +19,12 @@ use super::epi_integration::{self, EpiIntegration};
#[derive(Debug)] #[derive(Debug)]
pub enum UserEvent { pub enum UserEvent {
RequestRepaint, RequestRepaint {
when: Instant,
/// What the frame number was when the repaint was _requested_.
frame_nr: u64,
},
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit_winit::ActionRequestEvent), AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
} }
@ -58,6 +63,9 @@ enum EventResult {
} }
trait WinitApp { trait WinitApp {
/// The current frame number, as reported by egui.
fn frame_nr(&self) -> u64;
fn is_focused(&self) -> bool; fn is_focused(&self) -> bool;
fn integration(&self) -> Option<&EpiIntegration>; fn integration(&self) -> Option<&EpiIntegration>;
@ -66,7 +74,7 @@ trait WinitApp {
fn save_and_destroy(&mut self); fn save_and_destroy(&mut self);
fn paint(&mut self) -> EventResult; fn run_ui_and_paint(&mut self) -> EventResult;
fn on_event( fn on_event(
&mut self, &mut self,
@ -136,18 +144,30 @@ fn run_and_return(
// See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619 // See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => { winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint() winit_app.run_ui_and_paint()
} }
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => { winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint() winit_app.run_ui_and_paint()
} }
winit::event::Event::UserEvent(UserEvent::RequestRepaint) winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => {
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { 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, }) => {
log::trace!("Woke up to check next_repaint_time");
EventResult::Wait
}
winit::event::Event::WindowEvent { window_id, .. } winit::event::Event::WindowEvent { window_id, .. }
if winit_app.window().is_none() if winit_app.window().is_none()
@ -174,8 +194,8 @@ fn run_and_return(
log::trace!("Repaint caused by winit::Event: {:?}", event); log::trace!("Repaint caused by winit::Event: {:?}", event);
if cfg!(windows) { if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint(); winit_app.run_ui_and_paint();
} else { } else {
// Fix for https://github.com/emilk/egui/issues/2425 // Fix for https://github.com/emilk/egui/issues/2425
next_repaint_time = Instant::now(); next_repaint_time = Instant::now();
@ -196,18 +216,20 @@ fn run_and_return(
} }
} }
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { *control_flow = if next_repaint_time <= Instant::now() {
None => { if let Some(window) = winit_app.window() {
if let Some(window) = winit_app.window() { log::trace!("request_redraw");
window.request_redraw(); window.request_redraw();
}
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
ControlFlow::Poll
} }
Some(time_until_next_repaint) => { next_repaint_time = extremely_far_future();
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint) ControlFlow::Poll
} else {
let time_until_next = next_repaint_time.saturating_duration_since(Instant::now());
if time_until_next < std::time::Duration::from_secs(10_000) {
log::trace!("WaitUntil {time_until_next:?}");
} }
} ControlFlow::WaitUntil(next_repaint_time)
};
}); });
log::debug!("eframe window closed"); log::debug!("eframe window closed");
@ -239,18 +261,25 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
// See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619 // See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => { winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint() winit_app.run_ui_and_paint()
} }
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => { winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint() winit_app.run_ui_and_paint()
} }
winit::event::Event::UserEvent(UserEvent::RequestRepaint) winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => {
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { 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) { event => match winit_app.on_event(event_loop, &event) {
Ok(event_result) => event_result, Ok(event_result) => event_result,
@ -265,8 +294,8 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
EventResult::RepaintNow => { EventResult::RepaintNow => {
if cfg!(windows) { if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000); next_repaint_time = extremely_far_future();
winit_app.paint(); winit_app.run_ui_and_paint();
} else { } else {
// Fix for https://github.com/emilk/egui/issues/2425 // Fix for https://github.com/emilk/egui/issues/2425
next_repaint_time = Instant::now(); next_repaint_time = Instant::now();
@ -286,17 +315,15 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
} }
} }
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { *control_flow = if next_repaint_time <= Instant::now() {
None => { if let Some(window) = winit_app.window() {
if let Some(window) = winit_app.window() { window.request_redraw();
window.request_redraw();
}
ControlFlow::Poll
} }
Some(time_until_next_repaint) => { next_repaint_time = extremely_far_future();
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint) ControlFlow::Poll
} } else {
} ControlFlow::WaitUntil(next_repaint_time)
};
}) })
} }
@ -601,8 +628,6 @@ mod glow_integration {
// suspends and resumes. // suspends and resumes.
app_creator: Option<epi::AppCreator>, app_creator: Option<epi::AppCreator>,
is_focused: bool, is_focused: bool,
frame_nr: u64,
} }
impl GlowWinitApp { impl GlowWinitApp {
@ -619,7 +644,6 @@ mod glow_integration {
running: None, running: None,
app_creator: Some(app_creator), app_creator: Some(app_creator),
is_focused: true, is_focused: true,
frame_nr: 0,
} }
} }
@ -698,12 +722,17 @@ mod glow_integration {
{ {
let event_loop_proxy = self.repaint_proxy.clone(); let event_loop_proxy = self.repaint_proxy.clone();
integration.egui_ctx.set_request_repaint_callback(move || { integration
event_loop_proxy .egui_ctx
.lock() .set_request_repaint_callback(move |info| {
.send_event(UserEvent::RequestRepaint) log::trace!("request_repaint_callback: {info:?}");
.ok(); 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) let app_creator = std::mem::take(&mut self.app_creator)
@ -734,6 +763,12 @@ mod glow_integration {
} }
impl WinitApp for GlowWinitApp { 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 { fn is_focused(&self) -> bool {
self.is_focused self.is_focused
} }
@ -756,7 +791,7 @@ mod glow_integration {
} }
} }
fn paint(&mut self) -> EventResult { fn run_ui_and_paint(&mut self) -> EventResult {
if let Some(running) = &mut self.running { if let Some(running) = &mut self.running {
#[cfg(feature = "puffin")] #[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame(); puffin::GlobalProfiler::lock().new_frame();
@ -820,7 +855,7 @@ mod glow_integration {
#[cfg(feature = "__screenshot")] #[cfg(feature = "__screenshot")]
// give it time to settle: // 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") { if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
assert!( assert!(
path.ends_with(".png"), path.ends_with(".png"),
@ -871,8 +906,6 @@ mod glow_integration {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
self.frame_nr += 1;
control_flow control_flow
} else { } else {
EventResult::Wait EventResult::Wait
@ -1150,13 +1183,18 @@ mod wgpu_integration {
{ {
let event_loop_proxy = self.repaint_proxy.clone(); let event_loop_proxy = self.repaint_proxy.clone();
integration.egui_ctx.set_request_repaint_callback(move || { integration
event_loop_proxy .egui_ctx
.lock() .set_request_repaint_callback(move |info| {
.unwrap() log::trace!("request_repaint_callback: {info:?}");
.send_event(UserEvent::RequestRepaint) let when = Instant::now() + info.after;
.ok(); 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) let app_creator = std::mem::take(&mut self.app_creator)
@ -1186,6 +1224,12 @@ mod wgpu_integration {
} }
impl WinitApp for WgpuWinitApp { 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 { fn is_focused(&self) -> bool {
self.is_focused self.is_focused
} }
@ -1214,7 +1258,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) { if let (Some(running), Some(window)) = (&mut self.running, &self.window) {
#[cfg(feature = "puffin")] #[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame(); puffin::GlobalProfiler::lock().new_frame();
@ -1433,12 +1477,11 @@ mod wgpu_integration {
} }
} }
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
pub use wgpu_integration::run_wgpu; pub use wgpu_integration::run_wgpu;
#[cfg(any(target_os = "windows", target_os = "macos"))] // ----------------------------------------------------------------------------
fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option<crate::Theme> { fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option<crate::Theme> {
if options.follow_system_theme { if options.follow_system_theme {
window window
@ -1449,9 +1492,8 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti
} }
} }
// Winit only reads the system theme on macOS and Windows. // ----------------------------------------------------------------------------
// See: https://github.com/rust-windowing/winit/issues/1549
#[cfg(not(any(target_os = "windows", target_os = "macos")))] fn extremely_far_future() -> std::time::Instant {
fn system_theme(_window: &winit::window::Window, _options: &NativeOptions) -> Option<crate::Theme> { std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000)
None
} }

View File

@ -259,8 +259,8 @@ impl AppRunner {
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default(); let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
{ {
let needs_repaint = needs_repaint.clone(); let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move || { egui_ctx.set_request_repaint_callback(move |info| {
needs_repaint.repaint_asap(); needs_repaint.repaint_after(info.after.as_secs_f64());
}); });
} }

View File

@ -4,12 +4,15 @@ use egui::Key;
use super::*; use super::*;
struct IsDestroyed(pub bool); /// Calls `request_animation_frame` to schedule repaint.
///
/// It will only paint if needed, but will always call `request_animation_frame` immediately.
pub fn paint_and_schedule( pub fn paint_and_schedule(
runner_ref: &AppRunnerRef, runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>, panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
struct IsDestroyed(pub bool);
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
let mut runner_lock = runner_ref.lock(); let mut runner_lock = runner_ref.lock();
let is_destroyed = runner_lock.is_destroyed.fetch(); let is_destroyed = runner_lock.is_destroyed.fetch();

View File

@ -8,6 +8,21 @@ use crate::{
}; };
use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; 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 current 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<RwLock<epaint::TextureManager>>); struct WrappedTextureManager(Arc<RwLock<epaint::TextureManager>>);
@ -29,6 +44,94 @@ impl Default for WrappedTextureManager {
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Logic related to repainting the ui.
struct Repaint {
/// 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<Box<dyn Fn(RequestRepaintInfo) + Send + Sync>>,
requested_repaint_last_frame: bool,
}
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,
requested_repaint_last_frame: false,
}
}
}
impl Repaint {
fn request_repaint(&mut self) {
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 start_frame(&mut self) {
// We are repainting; no need to reschedule a repaint unless the user asks for it again.
self.repaint_after = std::time::Duration::MAX;
}
// returns how long to wait until repaint
fn end_frame(&mut self) -> std::time::Duration {
// 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.repaint_requests > 0 {
self.repaint_requests -= 1;
std::time::Duration::ZERO
} else {
self.repaint_after
};
self.repaint_after = std::time::Duration::MAX;
self.requested_repaint_last_frame = repaint_after.is_zero();
self.frame_nr += 1;
repaint_after
}
}
// ----------------------------------------------------------------------------
#[derive(Default)] #[derive(Default)]
struct ContextImpl { struct ContextImpl {
/// `None` until the start of the first frame. /// `None` until the start of the first frame.
@ -50,18 +153,7 @@ struct ContextImpl {
paint_stats: PaintStats, paint_stats: PaintStats,
/// the duration backend will poll for new events, before forcing another egui update repaint: Repaint,
/// 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_callback: Option<Box<dyn Fn() + Send + Sync>>,
/// used to suppress multiple calls to [`Self::request_repaint_callback`] during the same frame.
has_requested_repaint_this_frame: bool,
requested_repaint_last_frame: bool,
/// Written to during the frame. /// Written to during the frame.
layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>, layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
@ -77,7 +169,7 @@ struct ContextImpl {
impl ContextImpl { impl ContextImpl {
fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) {
self.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() { 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); new_raw_input.pixels_per_point = Some(new_pixels_per_point);
@ -95,7 +187,7 @@ impl ContextImpl {
self.memory.begin_frame(&self.input, &new_raw_input); self.memory.begin_frame(&self.input, &new_raw_input);
self.input = std::mem::take(&mut self.input) self.input = std::mem::take(&mut self.input)
.begin_frame(new_raw_input, self.requested_repaint_last_frame); .begin_frame(new_raw_input, self.repaint.requested_repaint_last_frame);
self.frame_state.begin_frame(&self.input); self.frame_state.begin_frame(&self.input);
@ -239,12 +331,7 @@ impl std::cmp::PartialEq for Context {
impl Default for Context { impl Default for Context {
fn default() -> Self { fn default() -> Self {
Self(Arc::new(RwLock::new(ContextImpl { Self(Arc::new(RwLock::new(ContextImpl::default())))
// Start with painting an extra frame to compensate for some widgets
// that take two frames before they "settle":
repaint_requests: 1,
..ContextImpl::default()
})))
} }
} }
@ -850,6 +937,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. /// 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. /// If this is called at least once in a frame, then there will be another frame right after this.
@ -860,19 +956,13 @@ impl Context {
/// (this will work on `eframe`). /// (this will work on `eframe`).
pub fn request_repaint(&self) { pub fn request_repaint(&self) {
// request two frames of repaint, just to cover some corner cases (frame delays): // request two frames of repaint, just to cover some corner cases (frame delays):
self.write(|ctx| { self.write(|ctx| ctx.repaint.request_repaint());
ctx.repaint_requests = 2;
if let Some(callback) = &ctx.request_repaint_callback {
if !ctx.has_requested_repaint_this_frame {
(callback)();
ctx.has_requested_repaint_this_frame = true;
}
}
});
} }
/// Request repaint after the specified duration elapses in the case of no new input /// Request repaint after at most the specified duration elapses.
/// events being received. ///
/// The backend can chose to repaint sooner, for instance if some other code called
/// this method with a lower duration, or if new events arrived.
/// ///
/// The function can be multiple times, but only the *smallest* duration will be considered. /// 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 /// So, if the function is called two times with `1 second` and `2 seconds`, egui will repaint
@ -900,7 +990,7 @@ impl Context {
/// during app idle time where we are not receiving any new input events. /// during app idle time where we are not receiving any new input events.
pub fn request_repaint_after(&self, duration: std::time::Duration) { pub fn request_repaint_after(&self, duration: std::time::Duration) {
// Maybe we can check if duration is ZERO, and call self.request_repaint()? // Maybe we can check if duration is ZERO, and call self.request_repaint()?
self.write(|ctx| ctx.repaint_after = ctx.repaint_after.min(duration)); self.write(|ctx| ctx.repaint.request_repaint_after(duration));
} }
/// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`].
@ -908,9 +998,12 @@ impl Context {
/// This lets you wake up a sleeping UI thread. /// This lets you wake up a sleeping UI thread.
/// ///
/// Note that only one callback can be set. Any new call overrides the previous callback. /// 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); let callback = Box::new(callback);
self.write(|ctx| ctx.request_repaint_callback = Some(callback)); self.write(|ctx| ctx.repaint.request_repaint_callback = Some(callback));
} }
/// Tell `egui` which fonts to use. /// Tell `egui` which fonts to use.
@ -1164,28 +1257,7 @@ impl Context {
} }
} }
// if repaint_requests is greater than zero. just set the duration to zero for immediate let repaint_after = self.write(|ctx| ctx.repaint.end_frame());
// repaint. if there's no repaint requests, then we can use the actual repaint_after instead.
let repaint_after = self.write(|ctx| {
if ctx.repaint_requests > 0 {
ctx.repaint_requests -= 1;
std::time::Duration::ZERO
} else {
ctx.repaint_after
}
});
self.write(|ctx| {
ctx.requested_repaint_last_frame = repaint_after.is_zero();
ctx.has_requested_repaint_this_frame = false; // allow new calls between frames
// 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.
ctx.repaint_after = std::time::Duration::MAX;
});
let shapes = self.drain_paint_lists(); let shapes = self.drain_paint_lists();
FullOutput { FullOutput {

View File

@ -354,7 +354,7 @@ pub mod text {
pub use { pub use {
containers::*, containers::*,
context::Context, context::{Context, RequestRepaintInfo},
data::{ data::{
input::*, input::*,
output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo}, output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo},

View File

@ -63,4 +63,6 @@ env_logger = "0.10"
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6" console_error_panic_hook = "0.1.6"
wasm-bindgen = "=0.2.84"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-sys = "0.3"

View File

@ -1,5 +1,3 @@
use egui::Widget;
/// How often we repaint the demo app by default /// How often we repaint the demo app by default
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RunMode { enum RunMode {
@ -43,6 +41,7 @@ impl Default for RunMode {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct BackendPanel { pub struct BackendPanel {
@ -52,9 +51,6 @@ pub struct BackendPanel {
// go back to [`RunMode::Reactive`] mode each time we start // go back to [`RunMode::Reactive`] mode each time we start
run_mode: RunMode, run_mode: RunMode,
#[cfg_attr(feature = "serde", serde(skip))]
repaint_after_seconds: f32,
/// current slider value for current gui scale /// current slider value for current gui scale
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pixels_per_point: Option<f32>, pixels_per_point: Option<f32>,
@ -65,19 +61,6 @@ pub struct BackendPanel {
egui_windows: EguiWindows, egui_windows: EguiWindows,
} }
impl Default for BackendPanel {
fn default() -> Self {
Self {
open: false,
run_mode: Default::default(),
repaint_after_seconds: 1.0,
pixels_per_point: None,
frame_history: Default::default(),
egui_windows: Default::default(),
}
}
}
impl BackendPanel { impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history self.frame_history
@ -90,9 +73,6 @@ impl BackendPanel {
} }
RunMode::Reactive => { RunMode::Reactive => {
// let the computer rest for a bit // let the computer rest for a bit
ctx.request_repaint_after(std::time::Duration::from_secs_f32(
self.repaint_after_seconds,
));
} }
} }
} }
@ -271,17 +251,27 @@ impl BackendPanel {
} else { } else {
ui.label("Only running UI code when there are animations or input."); ui.label("Only running UI code when there are animations or input.");
ui.horizontal(|ui| { // Add a test for `request_repaint_after`, but only in debug
ui.spacing_mut().item_spacing.x = 0.0; // builds to keep the noise down in the official demo.
ui.label("(but at least every "); if cfg!(debug_assertions) {
egui::DragValue::new(&mut self.repaint_after_seconds) ui.collapsing("More…", |ui| {
.clamp_range(0.1..=10.0) ui.horizontal(|ui| {
.speed(0.1) ui.label("Frame number:");
.suffix(" s") ui.monospace(ui.ctx().frame_nr().to_string());
.ui(ui) });
.on_hover_text("Repaint this often, even if there is no input."); if ui
ui.label(")"); .button("Wait 2s, then request repaint after another 3s")
}); .clicked()
{
log::info!("Waiting 2s before requesting repaint...");
let ctx = ui.ctx().clone();
call_after_delay(std::time::Duration::from_secs(2), move || {
log::info!("Request a repaint in 3s...");
ctx.request_repaint_after(std::time::Duration::from_secs(3));
});
}
});
}
} }
} }
} }
@ -394,3 +384,28 @@ impl EguiWindows {
}); });
} }
} }
// ----------------------------------------------------------------------------
#[cfg(not(target_arch = "wasm32"))]
fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) {
std::thread::spawn(move || {
std::thread::sleep(delay);
f();
});
}
#[cfg(target_arch = "wasm32")]
fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) {
use wasm_bindgen::prelude::*;
let window = web_sys::window().unwrap();
let closure = Closure::once(f);
let delay_ms = delay.as_millis() as _;
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
delay_ms,
)
.unwrap();
closure.forget(); // We must forget it, or else the callback is canceled on drop
}

View File

@ -33,12 +33,6 @@ impl FrameHistory {
} }
pub fn ui(&mut self, ui: &mut egui::Ui) { pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(format!(
"Total frames painted: {}",
self.frame_times.total_count()
))
.on_hover_text("Includes this frame.");
ui.label(format!( ui.label(format!(
"Mean CPU usage: {:.2} ms / frame", "Mean CPU usage: {:.2} ms / frame",
1e3 * self.mean_frame_time() 1e3 * self.mean_frame_time()

View File

@ -3,29 +3,41 @@ set -eu
script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$script_path/.." cd "$script_path/.."
./scripts/setup_web.sh
# This is required to enable the web_sys clipboard API which eframe web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
export RUSTFLAGS=--cfg=web_sys_unstable_apis
CRATE_NAME="egui_demo_app" CRATE_NAME="egui_demo_app"
# NOTE: persistence use up about 400kB (10%) of the WASM! # NOTE: persistence use up about 400kB (10%) of the WASM!
FEATURES="glow,http,persistence,web_screen_reader" FEATURES="glow,http,persistence,web_screen_reader"
OPEN=false OPEN=false
OPTIMIZE=false OPTIMIZE=false
BUILD=debug
BUILD_FLAGS=""
while test $# -gt 0; do while test $# -gt 0; do
case "$1" in case "$1" in
-h|--help) -h|--help)
echo "build_demo_web.sh [--optimize] [--open]" echo "build_demo_web.sh [--release] [--open]"
echo "" echo ""
echo " --optimize: Enable optimization step" echo " --release: Build with --release, and enable extra optimization step"
echo " Runs wasm-opt." echo " Runs wasm-opt."
echo " NOTE: --optimize also removes debug symbols which are otherwise useful for in-browser profiling." echo " NOTE: --release also removes debug symbols which are otherwise useful for in-browser profiling."
echo "" echo ""
echo " --open: Open the result in a browser" echo " --open: Open the result in a browser"
exit 0 exit 0
;; ;;
-O|--optimize) --release)
shift shift
OPTIMIZE=true OPTIMIZE=true
BUILD="release"
BUILD_FLAGS="--release"
;; ;;
--open) --open)
@ -39,22 +51,14 @@ while test $# -gt 0; do
esac esac
done done
./scripts/setup_web.sh
# This is required to enable the web_sys clipboard API which eframe web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
export RUSTFLAGS=--cfg=web_sys_unstable_apis
# Clear output from old stuff: # Clear output from old stuff:
rm -f "docs/${CRATE_NAME}_bg.wasm" rm -f "docs/${CRATE_NAME}_bg.wasm"
echo "Building rust…" echo "Building rust…"
BUILD=release
(cd crates/$CRATE_NAME && (cd crates/$CRATE_NAME &&
cargo build \ cargo build \
--release \ ${BUILD_FLAGS} \
--lib \ --lib \
--target wasm32-unknown-unknown \ --target wasm32-unknown-unknown \
--no-default-features \ --no-default-features \

13
scripts/clippy_wasm.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# This scripts run clippy on the wasm32-unknown-unknown target,
# using a special clippy.toml config file which forbids a few more things.
set -eu
script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$script_path/.."
set -x
# Use scripts/clippy_wasm/clippy.toml
export CLIPPY_CONF_DIR="scripts/clippy_wasm"
cargo cranky --all-features --target wasm32-unknown-unknown --target-dir target_wasm -p egui_demo_app --lib -- --deny warnings

View File

@ -0,0 +1,29 @@
# This is used by `scripts/clippy_wasm.sh` so we can forbid some methods that are not available in wasm.
#
# We cannot forbid all these methods in the main `clippy.toml` because of
# https://github.com/rust-lang/rust-clippy/issues/10406
msrv = "1.65"
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
disallowed-methods = [
"std::time::Instant::now", # use `instant` crate instead for wasm/web compatibility
"std::time::Duration::elapsed", # use `instant` crate instead for wasm/web compatibility
"std::time::SystemTime::now", # use `instant` or `time` crates instead for wasm/web compatibility
# Cannot spawn threads on wasm:
"std::thread::spawn",
]
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
disallowed-types = [
# Cannot spawn threads on wasm:
"std::thread::Builder",
]
# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`!
"AccessKit",
"..",
]