diff --git a/TODO.md b/TODO.md index 0927f15d..fb4cb7f3 100644 --- a/TODO.md +++ b/TODO.md @@ -62,8 +62,8 @@ TODO-list for the Egui project. If you looking for something to do, look here. * [x] Read url fragment and redirect to a subpage (e.g. different examples apps)] * [x] Copy/paste support * [x] Async HTTP requests +* [x] Repaint on completed fetch request (etc) * [ ] Local storage of app state -* [ ] Repaint on completed fetch request (etc) * [ ] Fix WebGL colors/blending (try EXT_sRGB) * [ ] Embeddability * [ ] Support canvas that does NOT cover entire screen. diff --git a/docs/example_web.js b/docs/example_web.js index c78fb699..9776bf10 100644 --- a/docs/example_web.js +++ b/docs/example_web.js @@ -213,35 +213,35 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_26(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hcf118a068d67b888(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h131d7a2b30673af8(arg0, arg1); } function __wbg_adapter_29(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h232550930f57d037(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1fcff379f8a91500(arg0, arg1, addHeapObject(arg2)); } -function __wbg_adapter_32(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha5199f647b73bd92(arg0, arg1); +function __wbg_adapter_32(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1fcff379f8a91500(arg0, arg1, addHeapObject(arg2)); } -function __wbg_adapter_35(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h232550930f57d037(arg0, arg1, addHeapObject(arg2)); +function __wbg_adapter_35(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h41cd408ae7dd46f7(arg0, arg1); } function __wbg_adapter_38(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h232550930f57d037(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1fcff379f8a91500(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_41(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h232550930f57d037(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1fcff379f8a91500(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_44(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h232550930f57d037(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1fcff379f8a91500(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_47(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd5c13e870f8bfcb3(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h42bc6cd1f6265a35(arg0, arg1, addHeapObject(arg2)); } /** @@ -381,10 +381,6 @@ async function init(input) { var ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); }; - imports.wbg.__wbg_setInterval_06df6f1ebeafd66f = handleError(function(arg0, arg1, arg2) { - var ret = getObject(arg0).setInterval(getObject(arg1), arg2); - return ret; - }); imports.wbg.__wbg_body_8c888fe47d81765f = function(arg0) { var ret = getObject(arg0).body; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -870,36 +866,36 @@ async function init(input) { var ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper738 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_26); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper739 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_29); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper741 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_32); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper743 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_35); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper745 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_38); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_closure_wrapper748 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_41); + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_26); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper750 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_44); + imports.wbg.__wbindgen_closure_wrapper749 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_29); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1475 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 242, __wbg_adapter_47); + imports.wbg.__wbindgen_closure_wrapper751 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_32); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper753 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_35); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper755 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_38); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper758 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_41); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper760 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 214, __wbg_adapter_44); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper1496 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 252, __wbg_adapter_47); return addHeapObject(ret); }; diff --git a/docs/example_web_bg.wasm b/docs/example_web_bg.wasm index 362d0930..a3801a77 100644 Binary files a/docs/example_web_bg.wasm and b/docs/example_web_bg.wasm differ diff --git a/egui/src/app.rs b/egui/src/app.rs index 109a19f3..b34a2582 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -36,6 +36,8 @@ pub struct IntegrationContext<'a> { pub tex_allocator: Option<&'a mut dyn TextureAllocator>, /// Where the app can issue commands back to the integration. pub output: AppOutput, + /// If you need to request a repaint from another thread, clone this and give to that other thread + pub repaint_signal: std::sync::Arc, } #[derive(Clone, Debug)] @@ -92,6 +94,12 @@ pub trait TextureAllocator { fn free(&mut self, id: crate::TextureId); } +pub trait RepaintSignal: Send { + /// This signals the Egui integration that a repaint is required. + /// This is meant to be called when a background process finishes in an async context and/or background thread. + fn request_repaint(&self); +} + /// A place where you can store custom data in a way that persists when you restart the app. /// /// On the web this is backed by [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index 39fa1057..46aa80d7 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -29,10 +29,20 @@ impl egui::app::TextureAllocator for Painter { } } +struct RequestRepaintEvent; + +struct GliumRepaintSignal(glutin::event_loop::EventLoopProxy); + +impl egui::app::RepaintSignal for GliumRepaintSignal { + fn request_repaint(&self) { + self.0.send_event(RequestRepaintEvent).ok(); + } +} + fn create_display( title: &str, window_settings: Option, - event_loop: &glutin::event_loop::EventLoop<()>, + event_loop: &glutin::event_loop::EventLoop, ) -> glium::Display { let mut window_builder = glutin::window::WindowBuilder::new() .with_decorations(true) @@ -61,9 +71,11 @@ pub fn run( ) -> ! { let window_settings: Option = egui::app::get_value(storage.as_ref(), WINDOW_KEY); - let event_loop = glutin::event_loop::EventLoop::new(); + let event_loop = glutin::event_loop::EventLoop::with_user_event(); let display = create_display(title, window_settings, &event_loop); + let repaint_signal = std::sync::Arc::new(GliumRepaintSignal(event_loop.create_proxy())); + let mut ctx = egui::Context::new(); *ctx.memory() = egui::app::get_value(storage.as_ref(), EGUI_MEMORY_KEY).unwrap_or_default(); app.setup(&ctx); @@ -92,6 +104,7 @@ pub fn run( }, tex_allocator: Some(&mut painter), output: Default::default(), + repaint_signal: repaint_signal.clone(), }; app.ui(&ctx, &mut integration_context); let app_output = integration_context.output; @@ -158,6 +171,11 @@ pub fn run( app.on_exit(storage.as_mut()); storage.flush(); } + + glutin::event::Event::UserEvent(RequestRepaintEvent) => { + display.gl_window().window().request_redraw(); + } + _ => (), } }); diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 12250c4b..645da421 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -134,12 +134,40 @@ impl WebInput { // ---------------------------------------------------------------------------- +use std::sync::atomic::Ordering::SeqCst; + +pub struct NeedRepaint(std::sync::atomic::AtomicBool); + +impl Default for NeedRepaint { + fn default() -> Self { + Self(true.into()) + } +} + +impl NeedRepaint { + pub fn fetch_and_clear(&self) -> bool { + self.0.swap(false, SeqCst) + } + + pub fn set_true(&self) { + self.0.store(true, SeqCst); + } +} + +impl egui::app::RepaintSignal for NeedRepaint { + fn request_repaint(&self) { + self.0.store(true, SeqCst); + } +} + +// ---------------------------------------------------------------------------- + pub struct AppRunner { pixels_per_point: f32, pub web_backend: WebBackend, pub input: WebInput, pub app: Box, - pub needs_repaint: bool, // TODO: move + pub needs_repaint: std::sync::Arc, } impl AppRunner { @@ -150,7 +178,7 @@ impl AppRunner { web_backend, input: Default::default(), app, - needs_repaint: true, // TODO: move + needs_repaint: Default::default(), }) } @@ -175,6 +203,7 @@ impl AppRunner { }, tex_allocator: Some(&mut self.web_backend.painter), output: Default::default(), + repaint_signal: self.needs_repaint.clone(), }; let egui_ctx = &self.web_backend.ctx; diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 6a634052..3fbf7328 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -15,18 +15,22 @@ use wasm_bindgen::prelude::*; // ---------------------------------------------------------------------------- // Helpers to hide some of the verbosity of web_sys +/// Log some text to the developer console (`console.log(...)` in JS) pub fn console_log(s: impl Into) { web_sys::console::log_1(&s.into()); } +/// Log a warning to the developer console (`console.warn(...)` in JS) pub fn console_warn(s: impl Into) { web_sys::console::warn_1(&s.into()); } +/// Log an error to the developer console (`console.error(...)` in JS) pub fn console_error(s: impl Into) { web_sys::console::error_1(&s.into()); } +/// Current time in seconds (since undefined point in time) pub fn now_sec() -> f64 { web_sys::window() .expect("should have a Window") @@ -298,11 +302,12 @@ pub struct AppRunnerRef(Arc>); fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); - if runner_lock.needs_repaint { - runner_lock.needs_repaint = false; + if runner_lock.needs_repaint.fetch_and_clear() { let (output, paint_jobs) = runner_lock.logic()?; runner_lock.paint(paint_jobs)?; - runner_lock.needs_repaint |= output.needs_repaint; + if output.needs_repaint { + runner_lock.needs_repaint.set_true(); + } } Ok(()) } @@ -350,7 +355,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { if !modifiers.ctrl && !modifiers.command && !should_ignore_key(&key) { runner_lock.input.raw.events.push(egui::Event::Text(key)); } - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); // So, shall we call prevent_default? // YES: @@ -392,7 +397,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { modifiers, }); } - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); }) as Box); document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?; closure.forget(); @@ -406,7 +411,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { if let Ok(text) = data.get_data("text") { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.raw.events.push(egui::Event::Text(text)); - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); } } }) as Box); @@ -420,7 +425,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |_: web_sys::ClipboardEvent| { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.raw.events.push(egui::Event::Cut); - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); }) as Box); document.add_event_listener_with_callback("cut", closure.as_ref().unchecked_ref())?; closure.forget(); @@ -432,7 +437,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |_: web_sys::ClipboardEvent| { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.raw.events.push(egui::Event::Copy); - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); }) as Box); document.add_event_listener_with_callback("copy", closure.as_ref().unchecked_ref())?; closure.forget(); @@ -441,7 +446,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { for event_name in &["load", "pagehide", "pageshow", "resize"] { let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move || { - runner_ref.0.lock().needs_repaint = true; + runner_ref.0.lock().needs_repaint.set_true(); }) as Box); window.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; closure.forget(); @@ -457,7 +462,7 @@ fn repaint_every_ms(runner_ref: &AppRunnerRef, milliseconds: i32) -> Result<(), let window = web_sys::window().unwrap(); let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move || { - runner_ref.0.lock().needs_repaint = true; + runner_ref.0.lock().needs_repaint.set_true(); }) as Box); window.set_interval_with_callback_and_timeout_and_arguments_0( closure.as_ref().unchecked_ref(), @@ -497,7 +502,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); runner_lock.input.raw.mouse_down = true; runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); } @@ -514,7 +519,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { if !runner_lock.input.is_touch { runner_lock.input.mouse_pos = Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); } @@ -532,7 +537,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { runner_lock.input.mouse_pos = Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); runner_lock.input.raw.mouse_down = false; - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); } @@ -548,7 +553,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); if !runner_lock.input.is_touch { runner_lock.input.mouse_pos = None; - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); } @@ -565,7 +570,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { runner_lock.input.is_touch = true; runner_lock.input.mouse_pos = Some(pos_from_touch_event(&event)); runner_lock.input.raw.mouse_down = true; - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); }) as Box); @@ -580,7 +585,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.is_touch = true; runner_lock.input.mouse_pos = Some(pos_from_touch_event(&event)); - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); }) as Box); @@ -597,7 +602,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { runner_lock.input.raw.mouse_down = false; // First release mouse to click... runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead) runner_lock.input.mouse_pos = None; // ...remove hover effect - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); }) as Box); @@ -612,7 +617,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.scroll_delta.x -= event.delta_x() as f32; runner_lock.input.scroll_delta.y -= event.delta_y() as f32; - runner_lock.needs_repaint = true; + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); }) as Box); diff --git a/example_web/src/example_app.rs b/example_web/src/example_app.rs index 3f093c3a..f5cf5b17 100644 --- a/example_web/src/example_app.rs +++ b/example_web/src/example_app.rs @@ -46,6 +46,15 @@ impl egui::app::App for ExampleApp { ctx: &std::sync::Arc, integration_context: &mut egui::app::IntegrationContext, ) { + if let Some(receiver) = &mut self.in_progress { + // Are we there yet? + if let Ok(result) = receiver.try_recv() { + self.in_progress = None; + self.result = Some(result.map(Resource::from_response)); + } else { + } + } + egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Egui Example App"); ui.add(egui::github_link_file!( @@ -54,12 +63,13 @@ impl egui::app::App for ExampleApp { )); if ui_url(ui, &mut self.url) { + let repaint_signal = integration_context.repaint_signal.clone(); let (sender, receiver) = std::sync::mpsc::channel(); self.in_progress = Some(receiver); let url = self.url.clone(); egui_web::spawn_future(async move { sender.send(egui_web::fetch::get(&url).await).ok(); - // TODO: trigger egui repaint somehow + repaint_signal.request_repaint(); }); } @@ -79,14 +89,6 @@ impl egui::app::App for ExampleApp { } } }); - - if let Some(receiver) = &mut self.in_progress { - // Are we there yet? - if let Ok(result) = receiver.try_recv() { - self.in_progress = None; - self.result = Some(result.map(Resource::from_response)); - } - } } }