diff --git a/.gitignore b/.gitignore index f7cd28a5a7442878e937728999cb6803d04f6210..ed80ebed950ce1fbe33ad36b6eccbe9162d0d98e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Os generated files .DS_Store -# Build related -/target +# trunk output folder +dist + +# Rust compile target directories: +target +target_ra +target_wasm diff --git a/Cargo.lock b/Cargo.lock index 315eaa6e6c6b9903edf25c6cb9a2c0a3630a206c..c6681b5370ae724638fba860cfce97546faba939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2617,6 +2617,8 @@ dependencies = [ "log", "serde", "serde_json", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6a26a05776fdee570e1ffe58d58df61c6d1dc6cb..94436f6c188a6ce95d4a52b4fd6de86fef051754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,7 @@ log = "0.4" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +web-sys = "0.3.70" # to access the DOM (to hide the loading text) diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9f783f8513dcd23129e26bde3613763fef87e6b0 Binary files /dev/null and b/assets/apple-touch-icon.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1db51358f1426f8bf8bb12cc9ebdb298baa9cbaf Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/icon-1024.png b/assets/icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..454f84d9068717f8e88f52d9417e831e66434880 Binary files /dev/null and b/assets/icon-1024.png differ diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000000000000000000000000000000000000..544fa819b58e04220267e5927814ba339e2ec0e3 Binary files /dev/null and b/assets/icon-256.png differ diff --git a/assets/manifest.json b/assets/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..6ad27f17168185cb47f95b2c7a1a761f5b9061b6 --- /dev/null +++ b/assets/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Skyward Enhanced Ground Station", + "short_name": "segs", + "icons": [ + { + "src": "./logo_256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./logo_1024.png", + "sizes": "1024x1024", + "type": "image/png" + } + ], + "lang": "en-US", + "id": "/index.html", + "start_url": "./index.html", + "display": "standalone", + "background_color": "white", + "theme_color": "white" +} diff --git a/assets/sw.js b/assets/sw.js new file mode 100644 index 0000000000000000000000000000000000000000..2113f90204cfefe66ff65daf7d9b5824173f4f7a --- /dev/null +++ b/assets/sw.js @@ -0,0 +1,25 @@ +var cacheName = "egui-template-pwa"; +var filesToCache = [ + "./", + "./index.html", + "./eframe_template.js", + "./eframe_template_bg.wasm", +]; + +/* Start the service worker and cache all of the app's content */ +self.addEventListener("install", function (e) { + e.waitUntil( + caches.open(cacheName).then(function (cache) { + return cache.addAll(filesToCache); + }), + ); +}); + +/* Serve cached content when offline */ +self.addEventListener("fetch", function (e) { + e.respondWith( + caches.match(e.request).then(function (response) { + return response || fetch(e.request); + }), + ); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..725b781090ec352803f84e59f979bf30ccc79722 --- /dev/null +++ b/index.html @@ -0,0 +1,175 @@ +<!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>Skyward Enhanced Ground Station</title> + + <!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization --> + <link data-trunk rel="rust" data-wasm-opt="2" /> + <!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option --> + <base data-trunk-public-url /> + + <link data-trunk rel="icon" href="assets/favicon.ico" /> + + <link data-trunk rel="copy-file" href="assets/sw.js" /> + <link + data-trunk + rel="copy-file" + href="assets/manifest.json" + data-target-path="assets" + /> + <link + data-trunk + rel="copy-file" + href="assets/icon-1024.png" + data-target-path="assets" + /> + <link + data-trunk + rel="copy-file" + href="assets/icon-256.png" + data-target-path="assets" + /> + <link + data-trunk + rel="copy-file" + href="assets/apple-touch-icon.png" + data-target-path="assets" + /> + + <link rel="manifest" href="assets/manifest.json" /> + <link rel="apple-touch-icon" href="assets/apple-touch-icon.png" /> + <meta + name="theme-color" + media="(prefers-color-scheme: light)" + content="white" + /> + <meta + name="theme-color" + media="(prefers-color-scheme: dark)" + content="#404040" + /> + + <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; + } + + @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%; + } + + /* Make canvas fill entire document: */ + canvas { + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .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 --> + <!-- the id is hardcoded in main.rs . so, make sure both match. --> + <canvas id="segs_canvas"></canvas> + + <!-- the loading spinner will be removed in main.rs --> + <div class="centered" id="loading_text"> + <p style="font-size: 16px">Loading…</p> + <div class="lds-dual-ring"></div> + </div> + + <!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). --> + <!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files --> + <script> + // We disable caching during development so that we always view the latest version. + if ( + "serviceWorker" in navigator && + window.location.hash !== "#dev" + ) { + window.addEventListener("load", function () { + navigator.serviceWorker.register("sw.js"); + }); + } + </script> + </body> +</html> + +<!-- Powered by egui: https://github.com/emilk/egui/ --> diff --git a/src/main.rs b/src/main.rs index 64e7c45fc68288774b9c7da110acf79273a06718..c03d0546d2ac340c6814c6e1778542a5edcb3f34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use ui::ComposableView; mod ui; +#[cfg(not(target_arch = "wasm32"))] fn main() -> Result<(), eframe::Error> { // set up logging (USE RUST_LOG=debug to see logs) env_logger::init(); @@ -25,3 +26,49 @@ fn main() -> Result<(), eframe::Error> { Box::new(|_| Ok(Box::<ComposableView>::default())), ) } + +#[cfg(target_arch = "wasm32")] +fn main() { + use eframe::wasm_bindgen::JsCast as _; + + // Redirect `log` message to `console.log` and friends: + eframe::WebLogger::init(log::LevelFilter::Debug).ok(); + + let web_options = eframe::WebOptions::default(); + + wasm_bindgen_futures::spawn_local(async { + let document = web_sys::window() + .expect("No window") + .document() + .expect("No document"); + + let canvas = document + .get_element_by_id("segs_canvas") + .expect("Failed to find the_canvas_id") + .dyn_into::<web_sys::HtmlCanvasElement>() + .expect("segs_canvas was not a HtmlCanvasElement"); + + let start_result = eframe::WebRunner::new() + .start( + canvas, + web_options, + Box::new(|_| Ok(Box::<ComposableView>::default())), + ) + .await; + + // Remove the loading text and spinner: + if let Some(loading_text) = document.get_element_by_id("loading_text") { + match start_result { + Ok(_) => { + loading_text.remove(); + } + Err(e) => { + loading_text.set_inner_html( + "<p> The app has crashed. See the developer console for details. </p>", + ); + panic!("Failed to start eframe: {e:?}"); + } + } + } + }); +}