Graceful exit from web (#1650)
Return a handle that can be used to stop a running egui instance.
This commit is contained in:
parent
3eccd341ad
commit
64496cacb9
10 changed files with 432 additions and 50 deletions
|
@ -33,6 +33,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Position canvas in center-top: */
|
/* Position canvas in center-top: */
|
||||||
|
@ -131,7 +133,11 @@
|
||||||
console.debug("wasm loaded. starting app…");
|
console.debug("wasm loaded. starting app…");
|
||||||
|
|
||||||
// This call installs a bunch of callbacks and then returns:
|
// This call installs a bunch of callbacks and then returns:
|
||||||
wasm_bindgen.start("the_canvas_id");
|
const handle = wasm_bindgen.start("the_canvas_id");
|
||||||
|
|
||||||
|
// call `handle.stop_web()` to stop
|
||||||
|
// uncomment to quick result
|
||||||
|
// setTimeout(() => {handle.stop_web(); handle.free())}, 2000)
|
||||||
|
|
||||||
console.debug("app started.");
|
console.debug("app started.");
|
||||||
document.getElementById("center_text").remove();
|
document.getElementById("center_text").remove();
|
||||||
|
|
200
docs/multiple_apps.html
Normal file
200
docs/multiple_apps.html
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
|
<!-- Disable zooming: -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>egui – An immediate mode GUI written in Rust</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
/* Remove touch delay: */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* Light mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #909090;
|
||||||
|
display:flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas_wrap{
|
||||||
|
/* height: 200px; */
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
/* Dark mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow canvas to fill entire web page: */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position canvas in center-top: */
|
||||||
|
canvas {
|
||||||
|
/* margin-right: auto;
|
||||||
|
margin-left: auto; */
|
||||||
|
/* display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%); */
|
||||||
|
width:90%;
|
||||||
|
height:90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------- */
|
||||||
|
/* Loading animation from https://loading.io/css/ */
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- The WASM code will resize the canvas dynamically -->
|
||||||
|
|
||||||
|
<div>controls</div>
|
||||||
|
|
||||||
|
<button class="stop_one">
|
||||||
|
stop
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="canvas_wrap one">
|
||||||
|
<canvas id="the_canvas_id_one"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas_wrap two">
|
||||||
|
<canvas id="the_canvas_id_two"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="centered" id="center_text">
|
||||||
|
<p style="font-size:16px">
|
||||||
|
Loading…
|
||||||
|
</p>
|
||||||
|
<div class="lds-dual-ring"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
|
||||||
|
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
|
||||||
|
// but this doesn't work with `file://` urls. This example is frequently
|
||||||
|
// viewed by simply opening `index.html` in a browser (with a `file://`
|
||||||
|
// url), so it would fail if we were to call this function!
|
||||||
|
//
|
||||||
|
// Work around this for now by deleting the function to ensure that the
|
||||||
|
// `no_modules.js` script doesn't have access to it. You won't need this
|
||||||
|
// hack when deploying over HTTP.
|
||||||
|
delete WebAssembly.instantiateStreaming;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
|
||||||
|
<script src="egui_demo_app.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// We'll defer our execution until the wasm is ready to go.
|
||||||
|
// Here we tell bindgen the path to the wasm file so it can start
|
||||||
|
// initialization and return to us a promise when it's done.
|
||||||
|
console.debug("loading wasm…");
|
||||||
|
wasm_bindgen("./egui_demo_app_bg.wasm")
|
||||||
|
.then(on_wasm_loaded)
|
||||||
|
.catch(on_wasm_error);
|
||||||
|
|
||||||
|
function on_wasm_loaded() {
|
||||||
|
console.debug("wasm loaded. starting app…");
|
||||||
|
|
||||||
|
// This call installs a bunch of callbacks and then returns:
|
||||||
|
|
||||||
|
wasm_bindgen.init_wasm_hooks()
|
||||||
|
|
||||||
|
const handle_one = wasm_bindgen.start_separate("the_canvas_id_one");
|
||||||
|
const handle_two = wasm_bindgen.start_separate("the_canvas_id_two");
|
||||||
|
|
||||||
|
const button = document.getElementsByClassName("stop_one")[0]
|
||||||
|
|
||||||
|
button.addEventListener("click", ()=>{
|
||||||
|
handle_one.stop_web()
|
||||||
|
handle_one.free()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// call `handle.stop_web()` to stop
|
||||||
|
// uncomment to quick result
|
||||||
|
// setTimeout(() => {handle.stop_web()}, 2000)
|
||||||
|
|
||||||
|
console.debug("app started.");
|
||||||
|
document.getElementById("center_text").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_wasm_error(error) {
|
||||||
|
console.error("Failed to start: " + error);
|
||||||
|
document.getElementById("center_text").innerHTML = `
|
||||||
|
<p>
|
||||||
|
An error occurred during loading:
|
||||||
|
</p>
|
||||||
|
<p style="font-family:Courier New">
|
||||||
|
${error}
|
||||||
|
</p>
|
||||||
|
<p style="font-size:14px">
|
||||||
|
Make sure you use a modern browser with WebGL and WASM enabled.
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
|
@ -28,6 +28,8 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||||
|
|
||||||
#### Web:
|
#### Web:
|
||||||
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
|
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
|
||||||
|
* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 0.18.0 - 2022-04-30
|
## 0.18.0 - 2022-04-30
|
||||||
|
|
|
@ -77,11 +77,19 @@ pub use epi::*;
|
||||||
// When compiling for web
|
// When compiling for web
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod web;
|
use egui::mutex::Mutex;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use wasm_bindgen;
|
pub use wasm_bindgen;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use web::AppRunner;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use web_sys;
|
pub use web_sys;
|
||||||
|
|
||||||
|
@ -93,12 +101,13 @@ pub use web_sys;
|
||||||
/// use wasm_bindgen::prelude::*;
|
/// use wasm_bindgen::prelude::*;
|
||||||
///
|
///
|
||||||
/// /// This is the entry-point for all the web-assembly.
|
/// /// This is the entry-point for all the web-assembly.
|
||||||
/// /// This is called once from the HTML.
|
/// /// This is called from the HTML.
|
||||||
/// /// It loads the app, installs some callbacks, then returns.
|
/// /// It loads the app, installs some callbacks, then returns.
|
||||||
|
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
|
||||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||||
/// #[cfg(target_arch = "wasm32")]
|
/// #[cfg(target_arch = "wasm32")]
|
||||||
/// #[wasm_bindgen]
|
/// #[wasm_bindgen]
|
||||||
/// pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
/// pub fn start(canvas_id: &str) -> Result<Arc<Mutex<AppRunner>>, eframe::wasm_bindgen::JsValue> {
|
||||||
/// let web_options = eframe::WebOptions::default();
|
/// let web_options = eframe::WebOptions::default();
|
||||||
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
|
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
|
||||||
/// }
|
/// }
|
||||||
|
@ -108,9 +117,10 @@ pub fn start_web(
|
||||||
canvas_id: &str,
|
canvas_id: &str,
|
||||||
web_options: WebOptions,
|
web_options: WebOptions,
|
||||||
app_creator: AppCreator,
|
app_creator: AppCreator,
|
||||||
) -> Result<(), wasm_bindgen::JsValue> {
|
) -> Result<Arc<Mutex<AppRunner>>, wasm_bindgen::JsValue> {
|
||||||
web::start(canvas_id, web_options, app_creator)?;
|
let handle = web::start(canvas_id, web_options, app_creator)?;
|
||||||
Ok(())
|
|
||||||
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -2,9 +2,10 @@ use super::{glow_wrapping::WrappedGlowPainter, *};
|
||||||
|
|
||||||
use crate::epi;
|
use crate::epi;
|
||||||
|
|
||||||
use egui::mutex::{Mutex, MutexGuard};
|
use egui::{
|
||||||
use egui::TexturesDelta;
|
mutex::{Mutex, MutexGuard},
|
||||||
|
TexturesDelta,
|
||||||
|
};
|
||||||
pub use egui::{pos2, Color32};
|
pub use egui::{pos2, Color32};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -67,6 +68,24 @@ impl NeedRepaint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct IsDestroyed(std::sync::atomic::AtomicBool);
|
||||||
|
|
||||||
|
impl Default for IsDestroyed {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(false.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsDestroyed {
|
||||||
|
pub fn fetch(&self) -> bool {
|
||||||
|
self.0.load(SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_true(&self) {
|
||||||
|
self.0.store(true, SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn web_location() -> epi::Location {
|
fn web_location() -> epi::Location {
|
||||||
|
@ -147,11 +166,19 @@ pub struct AppRunner {
|
||||||
pub(crate) input: WebInput,
|
pub(crate) input: WebInput,
|
||||||
app: Box<dyn epi::App>,
|
app: Box<dyn epi::App>,
|
||||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||||
|
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
|
||||||
last_save_time: f64,
|
last_save_time: f64,
|
||||||
screen_reader: super::screen_reader::ScreenReader,
|
screen_reader: super::screen_reader::ScreenReader,
|
||||||
pub(crate) text_cursor_pos: Option<egui::Pos2>,
|
pub(crate) text_cursor_pos: Option<egui::Pos2>,
|
||||||
pub(crate) mutable_text_under_cursor: bool,
|
pub(crate) mutable_text_under_cursor: bool,
|
||||||
textures_delta: TexturesDelta,
|
textures_delta: TexturesDelta,
|
||||||
|
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AppRunner {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
tracing::debug!("AppRunner has fully dropped");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppRunner {
|
impl AppRunner {
|
||||||
|
@ -220,11 +247,13 @@ impl AppRunner {
|
||||||
input: Default::default(),
|
input: Default::default(),
|
||||||
app,
|
app,
|
||||||
needs_repaint,
|
needs_repaint,
|
||||||
|
is_destroyed: Default::default(),
|
||||||
last_save_time: now_sec(),
|
last_save_time: now_sec(),
|
||||||
screen_reader: Default::default(),
|
screen_reader: Default::default(),
|
||||||
text_cursor_pos: None,
|
text_cursor_pos: None,
|
||||||
mutable_text_under_cursor: false,
|
mutable_text_under_cursor: false,
|
||||||
textures_delta: Default::default(),
|
textures_delta: Default::default(),
|
||||||
|
events_to_unsubscribe: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
|
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
|
||||||
|
@ -266,6 +295,24 @@ impl AppRunner {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn destroy(&mut self) -> Result<(), JsValue> {
|
||||||
|
let is_destroyed_already = self.is_destroyed.fetch();
|
||||||
|
|
||||||
|
if is_destroyed_already {
|
||||||
|
tracing::warn!("App was destroyed already");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
tracing::debug!("Destroying");
|
||||||
|
for x in self.events_to_unsubscribe.drain(..) {
|
||||||
|
x.unsubscribe()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.painter.destroy();
|
||||||
|
self.is_destroyed.set_true();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns how long to wait until the next repaint.
|
/// Returns how long to wait until the next repaint.
|
||||||
///
|
///
|
||||||
/// Call [`Self::paint`] later to paint
|
/// Call [`Self::paint`] later to paint
|
||||||
|
@ -358,19 +405,60 @@ impl AppRunner {
|
||||||
|
|
||||||
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
|
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
|
||||||
|
|
||||||
|
pub struct TargetEvent {
|
||||||
|
target: EventTarget,
|
||||||
|
event_name: String,
|
||||||
|
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IntervalHandle {
|
||||||
|
pub handle: i32,
|
||||||
|
pub closure: Closure<dyn FnMut()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum EventToUnsubscribe {
|
||||||
|
TargetEvent(TargetEvent),
|
||||||
|
#[allow(dead_code)]
|
||||||
|
IntervalHandle(IntervalHandle),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventToUnsubscribe {
|
||||||
|
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
EventToUnsubscribe::TargetEvent(handle) => {
|
||||||
|
handle.target.remove_event_listener_with_callback(
|
||||||
|
handle.event_name.as_str(),
|
||||||
|
handle.closure.as_ref().unchecked_ref(),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
EventToUnsubscribe::IntervalHandle(handle) => {
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
window.clear_interval_with_handle(handle.handle);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pub struct AppRunnerContainer {
|
pub struct AppRunnerContainer {
|
||||||
pub runner: AppRunnerRef,
|
pub runner: AppRunnerRef,
|
||||||
|
|
||||||
/// Set to `true` if there is a panic.
|
/// Set to `true` if there is a panic.
|
||||||
/// Used to ignore callbacks after a panic.
|
/// Used to ignore callbacks after a panic.
|
||||||
pub panicked: Arc<AtomicBool>,
|
pub panicked: Arc<AtomicBool>,
|
||||||
|
pub events: Vec<EventToUnsubscribe>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppRunnerContainer {
|
impl AppRunnerContainer {
|
||||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||||
/// are dealt with in the same way
|
/// are dealt with in the same way
|
||||||
|
///
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
||||||
&self,
|
&mut self,
|
||||||
target: &EventTarget,
|
target: &EventTarget,
|
||||||
event_name: &'static str,
|
event_name: &'static str,
|
||||||
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
||||||
|
@ -391,14 +479,19 @@ impl AppRunnerContainer {
|
||||||
|
|
||||||
closure(event, runner_ref.lock());
|
closure(event, runner_ref.lock());
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(_)>
|
}) as Box<dyn FnMut(web_sys::Event)>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the event listener to the target
|
// Add the event listener to the target
|
||||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||||
|
|
||||||
// Bypass closure drop so that event handler can call the closure
|
let handle = TargetEvent {
|
||||||
closure.forget();
|
target: target.clone(),
|
||||||
|
event_name: event_name.to_string(),
|
||||||
|
closure,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.events.push(EventToUnsubscribe::TargetEvent(handle));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -421,23 +514,26 @@ pub fn start(
|
||||||
/// Install event listeners to register different input events
|
/// Install event listeners to register different input events
|
||||||
/// and starts running the given [`AppRunner`].
|
/// and starts running the given [`AppRunner`].
|
||||||
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
||||||
let runner_container = AppRunnerContainer {
|
let mut runner_container = AppRunnerContainer {
|
||||||
runner: Arc::new(Mutex::new(app_runner)),
|
runner: Arc::new(Mutex::new(app_runner)),
|
||||||
panicked: Arc::new(AtomicBool::new(false)),
|
panicked: Arc::new(AtomicBool::new(false)),
|
||||||
|
events: Vec::with_capacity(20),
|
||||||
};
|
};
|
||||||
|
|
||||||
super::events::install_canvas_events(&runner_container)?;
|
super::events::install_canvas_events(&mut runner_container)?;
|
||||||
super::events::install_document_events(&runner_container)?;
|
super::events::install_document_events(&mut runner_container)?;
|
||||||
text_agent::install_text_agent(&runner_container)?;
|
text_agent::install_text_agent(&mut runner_container)?;
|
||||||
|
|
||||||
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
||||||
|
|
||||||
// Disable all event handlers on panic
|
// Disable all event handlers on panic
|
||||||
let previous_hook = std::panic::take_hook();
|
let previous_hook = std::panic::take_hook();
|
||||||
let panicked = runner_container.panicked;
|
|
||||||
|
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
|
||||||
|
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
tracing::info!("egui disabled all event handlers due to panic");
|
tracing::info!("egui disabled all event handlers due to panic");
|
||||||
panicked.store(true, SeqCst);
|
runner_container.panicked.store(true, SeqCst);
|
||||||
|
|
||||||
// Propagate panic info to the previously registered panic hook
|
// Propagate panic info to the previously registered panic hook
|
||||||
previous_hook(panic_info);
|
previous_hook(panic_info);
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
struct IsDestroyed(pub bool);
|
||||||
|
|
||||||
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> {
|
||||||
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), 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();
|
||||||
if runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
|
let is_destroyed = runner_lock.is_destroyed.fetch();
|
||||||
|
|
||||||
|
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
|
||||||
runner_lock.needs_repaint.clear();
|
runner_lock.needs_repaint.clear();
|
||||||
runner_lock.clear_color_buffer();
|
runner_lock.clear_color_buffer();
|
||||||
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
|
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
|
||||||
|
@ -18,7 +22,7 @@ pub fn paint_and_schedule(
|
||||||
runner_lock.auto_save();
|
runner_lock.auto_save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(IsDestroyed(is_destroyed))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_animation_frame(
|
fn request_animation_frame(
|
||||||
|
@ -35,14 +39,16 @@ pub fn paint_and_schedule(
|
||||||
|
|
||||||
// Only paint and schedule if there has been no panic
|
// Only paint and schedule if there has been no panic
|
||||||
if !panicked.load(Ordering::SeqCst) {
|
if !panicked.load(Ordering::SeqCst) {
|
||||||
paint_if_needed(runner_ref)?;
|
let is_destroyed = paint_if_needed(runner_ref)?;
|
||||||
request_animation_frame(runner_ref.clone(), panicked)?;
|
if !is_destroyed.0 {
|
||||||
|
request_animation_frame(runner_ref.clone(), panicked)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
|
|
||||||
|
@ -188,25 +194,27 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
// By default, right-clicks open a context menu.
|
// By default, right-clicks open a context menu.
|
||||||
// We don't want to do that (right clicks is handled by egui):
|
// We don't want to do that (right clicks is handled by egui):
|
||||||
let event_name = "contextmenu";
|
let event_name = "contextmenu";
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
|
||||||
event.prevent_default();
|
let closure =
|
||||||
}) as Box<dyn FnMut(_)>);
|
move |event: web_sys::MouseEvent,
|
||||||
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
mut _runner_lock: egui::mutex::MutexGuard<AppRunner>| {
|
||||||
closure.forget();
|
event.prevent_default();
|
||||||
|
};
|
||||||
|
|
||||||
|
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
runner_container.add_event_listener(
|
runner_container.add_event_listener(
|
||||||
&canvas,
|
&canvas,
|
||||||
"mousedown",
|
"mousedown",
|
||||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<AppRunner>| {
|
||||||
if let Some(button) = button_from_mouse_event(&event) {
|
if let Some(button) = button_from_mouse_event(&event) {
|
||||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||||
let modifiers = runner_lock.input.raw.modifiers;
|
let modifiers = runner_lock.input.raw.modifiers;
|
||||||
|
|
|
@ -87,6 +87,10 @@ impl WrappedGlowPainter {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn destroy(&mut self) {
|
||||||
|
self.painter.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns glow context and shader prefix.
|
/// Returns glow context and shader prefix.
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub mod storage;
|
||||||
mod text_agent;
|
mod text_agent;
|
||||||
|
|
||||||
pub use backend::*;
|
pub use backend::*;
|
||||||
|
use egui::Vec2;
|
||||||
pub use events::*;
|
pub use events::*;
|
||||||
pub use storage::*;
|
pub use storage::*;
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ pub fn now_sec() -> f64 {
|
||||||
/ 1000.0
|
/ 1000.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
|
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
|
||||||
let window = web_sys::window()?;
|
let window = web_sys::window()?;
|
||||||
Some(egui::vec2(
|
Some(egui::vec2(
|
||||||
|
@ -96,13 +98,21 @@ pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||||
|
|
||||||
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
||||||
let canvas = canvas_element(canvas_id)?;
|
let canvas = canvas_element(canvas_id)?;
|
||||||
|
let parent = canvas.parent_element()?;
|
||||||
|
|
||||||
|
let width = parent.scroll_width();
|
||||||
|
let height = parent.scroll_height();
|
||||||
|
|
||||||
|
let canvas_real_size = Vec2 {
|
||||||
|
x: width as f32,
|
||||||
|
y: height as f32,
|
||||||
|
};
|
||||||
|
|
||||||
let screen_size_points = screen_size_in_native_points()?;
|
|
||||||
let pixels_per_point = native_pixels_per_point();
|
let pixels_per_point = native_pixels_per_point();
|
||||||
|
|
||||||
let max_size_pixels = pixels_per_point * max_size_points;
|
let max_size_pixels = pixels_per_point * max_size_points;
|
||||||
|
|
||||||
let canvas_size_pixels = pixels_per_point * screen_size_points;
|
let canvas_size_pixels = pixels_per_point * canvas_real_size;
|
||||||
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
|
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
|
||||||
let canvas_size_points = canvas_size_pixels / pixels_per_point;
|
let canvas_size_points = canvas_size_pixels / pixels_per_point;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text event handler,
|
/// Text event handler,
|
||||||
pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
|
|
|
@ -5,6 +5,15 @@ mod backend_panel;
|
||||||
pub(crate) mod frame_history;
|
pub(crate) mod frame_history;
|
||||||
mod wrap_app;
|
mod wrap_app;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use eframe::web::AppRunner;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use egui::mutex::Mutex;
|
||||||
|
|
||||||
pub use wrap_app::WrapApp;
|
pub use wrap_app::WrapApp;
|
||||||
|
|
||||||
/// Time of day as seconds since midnight. Used for clock in demo app.
|
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||||
|
@ -19,23 +28,60 @@ pub(crate) fn seconds_since_midnight() -> f64 {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use eframe::wasm_bindgen::{self, prelude::*};
|
use eframe::wasm_bindgen::{self, prelude::*};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct WebHandle {
|
||||||
|
handle: Arc<Mutex<AppRunner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl WebHandle {
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||||
|
let mut app = self.handle.lock();
|
||||||
|
let res = app.destroy();
|
||||||
|
|
||||||
|
// let numw = Arc::weak_count(&app);
|
||||||
|
// let nums = Arc::strong_count(&app);
|
||||||
|
// tracing::debug!("runner ref {:?}, {:?}", numw, nums);
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init_wasm_hooks() {
|
||||||
|
// Make sure panics are logged using `console.error`.
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
// Redirect tracing to console.log and friends:
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
||||||
|
let web_options = eframe::WebOptions::default();
|
||||||
|
let handle = eframe::start_web(
|
||||||
|
canvas_id,
|
||||||
|
web_options,
|
||||||
|
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
||||||
|
)
|
||||||
|
.map(|handle| WebHandle { handle });
|
||||||
|
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
/// This is the entry-point for all the web-assembly.
|
/// This is the entry-point for all the web-assembly.
|
||||||
/// This is called once from the HTML.
|
/// This is called once from the HTML.
|
||||||
/// It loads the app, installs some callbacks, then returns.
|
/// It loads the app, installs some callbacks, then returns.
|
||||||
/// You can add more callbacks like this if you want to call in to your code.
|
/// You can add more callbacks like this if you want to call in to your code.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
|
pub fn start(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
||||||
// Make sure panics are logged using `console.error`.
|
init_wasm_hooks();
|
||||||
console_error_panic_hook::set_once();
|
start_separate(canvas_id)
|
||||||
|
|
||||||
// Redirect tracing to console.log and friends:
|
|
||||||
tracing_wasm::set_as_global_default();
|
|
||||||
|
|
||||||
let web_options = eframe::WebOptions::default();
|
|
||||||
eframe::start_web(
|
|
||||||
canvas_id,
|
|
||||||
web_options,
|
|
||||||
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue