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
- name: Cranky wasm32
uses: actions-rs/cargo@v1
with:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
run: ./scripts/clippy_wasm.sh
# ---------------------------------------------------------------------------

2
Cargo.lock generated
View File

@ -1272,7 +1272,9 @@ dependencies = [
"log",
"poll-promise",
"serde",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[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`].
//! 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::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
@ -19,7 +19,12 @@ use super::epi_integration::{self, EpiIntegration};
#[derive(Debug)]
pub enum UserEvent {
RequestRepaint,
RequestRepaint {
when: Instant,
/// What the frame number was when the repaint was _requested_.
frame_nr: u64,
},
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
}
@ -58,6 +63,9 @@ enum EventResult {
}
trait WinitApp {
/// The current frame number, as reported by egui.
fn frame_nr(&self) -> u64;
fn is_focused(&self) -> bool;
fn integration(&self) -> Option<&EpiIntegration>;
@ -66,7 +74,7 @@ trait WinitApp {
fn save_and_destroy(&mut self);
fn paint(&mut self) -> EventResult;
fn run_ui_and_paint(&mut self) -> EventResult;
fn on_event(
&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/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::UserEvent(UserEvent::RequestRepaint)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => {
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, .. }
if winit_app.window().is_none()
@ -174,8 +194,8 @@ fn run_and_return(
log::trace!("Repaint caused by winit::Event: {:?}", event);
if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint();
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint();
} else {
// Fix for https://github.com/emilk/egui/issues/2425
next_repaint_time = Instant::now();
@ -196,18 +216,20 @@ fn run_and_return(
}
}
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
None => {
if let Some(window) = winit_app.window() {
window.request_redraw();
}
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
ControlFlow::Poll
*control_flow = if next_repaint_time <= Instant::now() {
if let Some(window) = winit_app.window() {
log::trace!("request_redraw");
window.request_redraw();
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
next_repaint_time = extremely_far_future();
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");
@ -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/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::UserEvent(UserEvent::RequestRepaint)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => {
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) {
Ok(event_result) => event_result,
@ -265,8 +294,8 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
EventResult::RepaintNow => {
if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint();
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint();
} else {
// Fix for https://github.com/emilk/egui/issues/2425
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()) {
None => {
if let Some(window) = winit_app.window() {
window.request_redraw();
}
ControlFlow::Poll
*control_flow = if next_repaint_time <= Instant::now() {
if let Some(window) = winit_app.window() {
window.request_redraw();
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
}
}
next_repaint_time = extremely_far_future();
ControlFlow::Poll
} else {
ControlFlow::WaitUntil(next_repaint_time)
};
})
}
@ -601,8 +628,6 @@ mod glow_integration {
// suspends and resumes.
app_creator: Option<epi::AppCreator>,
is_focused: bool,
frame_nr: u64,
}
impl GlowWinitApp {
@ -619,7 +644,6 @@ mod glow_integration {
running: None,
app_creator: Some(app_creator),
is_focused: true,
frame_nr: 0,
}
}
@ -698,12 +722,17 @@ mod glow_integration {
{
let event_loop_proxy = self.repaint_proxy.clone();
integration.egui_ctx.set_request_repaint_callback(move || {
event_loop_proxy
.lock()
.send_event(UserEvent::RequestRepaint)
.ok();
});
integration
.egui_ctx
.set_request_repaint_callback(move |info| {
log::trace!("request_repaint_callback: {info:?}");
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)
@ -734,6 +763,12 @@ mod glow_integration {
}
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 {
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 {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
@ -820,7 +855,7 @@ mod glow_integration {
#[cfg(feature = "__screenshot")]
// 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") {
assert!(
path.ends_with(".png"),
@ -871,8 +906,6 @@ mod glow_integration {
std::thread::sleep(std::time::Duration::from_millis(10));
}
self.frame_nr += 1;
control_flow
} else {
EventResult::Wait
@ -1150,13 +1183,18 @@ mod wgpu_integration {
{
let event_loop_proxy = self.repaint_proxy.clone();
integration.egui_ctx.set_request_repaint_callback(move || {
event_loop_proxy
.lock()
.unwrap()
.send_event(UserEvent::RequestRepaint)
.ok();
});
integration
.egui_ctx
.set_request_repaint_callback(move |info| {
log::trace!("request_repaint_callback: {info:?}");
let when = Instant::now() + info.after;
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)
@ -1186,6 +1224,12 @@ mod wgpu_integration {
}
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 {
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) {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
@ -1433,12 +1477,11 @@ mod wgpu_integration {
}
}
// ----------------------------------------------------------------------------
#[cfg(feature = "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> {
if options.follow_system_theme {
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 system_theme(_window: &winit::window::Window, _options: &NativeOptions) -> Option<crate::Theme> {
None
// ----------------------------------------------------------------------------
fn extremely_far_future() -> std::time::Instant {
std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000)
}

View File

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

View File

@ -4,12 +4,15 @@ use egui::Key;
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(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
struct IsDestroyed(pub bool);
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
let mut runner_lock = runner_ref.lock();
let is_destroyed = runner_lock.is_destroyed.fetch();

View File

@ -8,6 +8,21 @@ use crate::{
};
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>>);
@ -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)]
struct ContextImpl {
/// `None` until the start of the first frame.
@ -50,18 +153,7 @@ struct ContextImpl {
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_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,
repaint: Repaint,
/// Written to during the frame.
layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
@ -77,7 +169,7 @@ struct ContextImpl {
impl ContextImpl {
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() {
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.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);
@ -239,12 +331,7 @@ impl std::cmp::PartialEq for Context {
impl Default for Context {
fn default() -> Self {
Self(Arc::new(RwLock::new(ContextImpl {
// Start with painting an extra frame to compensate for some widgets
// that take two frames before they "settle":
repaint_requests: 1,
..ContextImpl::default()
})))
Self(Arc::new(RwLock::new(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.
///
/// 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`).
pub fn request_repaint(&self) {
// request two frames of repaint, just to cover some corner cases (frame delays):
self.write(|ctx| {
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;
}
}
});
self.write(|ctx| ctx.repaint.request_repaint());
}
/// Request repaint after the specified duration elapses in the case of no new input
/// events being received.
/// Request repaint after at most the specified duration elapses.
///
/// 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.
/// 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.
pub fn request_repaint_after(&self, duration: std::time::Duration) {
// 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`].
@ -908,9 +998,12 @@ impl Context {
/// This lets you wake up a sleeping UI thread.
///
/// 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);
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.
@ -1164,28 +1257,7 @@ impl Context {
}
}
// 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 = 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 repaint_after = self.write(|ctx| ctx.repaint.end_frame());
let shapes = self.drain_paint_lists();
FullOutput {

View File

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

View File

@ -63,4 +63,6 @@ env_logger = "0.10"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
wasm-bindgen = "=0.2.84"
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
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
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", serde(default))]
pub struct BackendPanel {
@ -52,9 +51,6 @@ pub struct BackendPanel {
// go back to [`RunMode::Reactive`] mode each time we start
run_mode: RunMode,
#[cfg_attr(feature = "serde", serde(skip))]
repaint_after_seconds: f32,
/// current slider value for current gui scale
#[cfg_attr(feature = "serde", serde(skip))]
pixels_per_point: Option<f32>,
@ -65,19 +61,6 @@ pub struct BackendPanel {
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 {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history
@ -90,9 +73,6 @@ impl BackendPanel {
}
RunMode::Reactive => {
// 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 {
ui.label("Only running UI code when there are animations or input.");
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("(but at least every ");
egui::DragValue::new(&mut self.repaint_after_seconds)
.clamp_range(0.1..=10.0)
.speed(0.1)
.suffix(" s")
.ui(ui)
.on_hover_text("Repaint this often, even if there is no input.");
ui.label(")");
});
// Add a test for `request_repaint_after`, but only in debug
// builds to keep the noise down in the official demo.
if cfg!(debug_assertions) {
ui.collapsing("More…", |ui| {
ui.horizontal(|ui| {
ui.label("Frame number:");
ui.monospace(ui.ctx().frame_nr().to_string());
});
if ui
.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) {
ui.label(format!(
"Total frames painted: {}",
self.frame_times.total_count()
))
.on_hover_text("Includes this frame.");
ui.label(format!(
"Mean CPU usage: {:.2} ms / frame",
1e3 * self.mean_frame_time()

View File

@ -3,29 +3,41 @@ set -eu
script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
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"
# NOTE: persistence use up about 400kB (10%) of the WASM!
FEATURES="glow,http,persistence,web_screen_reader"
OPEN=false
OPTIMIZE=false
BUILD=debug
BUILD_FLAGS=""
while test $# -gt 0; do
case "$1" in
-h|--help)
echo "build_demo_web.sh [--optimize] [--open]"
echo "build_demo_web.sh [--release] [--open]"
echo ""
echo " --optimize: Enable optimization step"
echo " Runs wasm-opt."
echo " NOTE: --optimize also removes debug symbols which are otherwise useful for in-browser profiling."
echo " --release: Build with --release, and enable extra optimization step"
echo " Runs wasm-opt."
echo " NOTE: --release also removes debug symbols which are otherwise useful for in-browser profiling."
echo ""
echo " --open: Open the result in a browser"
exit 0
;;
-O|--optimize)
--release)
shift
OPTIMIZE=true
BUILD="release"
BUILD_FLAGS="--release"
;;
--open)
@ -39,22 +51,14 @@ while test $# -gt 0; do
esac
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:
rm -f "docs/${CRATE_NAME}_bg.wasm"
echo "Building rust…"
BUILD=release
(cd crates/$CRATE_NAME &&
cargo build \
--release \
${BUILD_FLAGS} \
--lib \
--target wasm32-unknown-unknown \
--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",
"..",
]