diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4191252..0db7898 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -552,6 +552,38 @@ jobs:
- name: The static-worker build in Chrome, with real pushes
run: node ci/web/example.mjs check examples/yew/dist-static "$RUNNER_TEMP/vapid.json" '/pushups-sw\.js\?pushups=static'
+ example-winit-web:
+ name: Example winit app on Trunk in Chrome
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v7
+ - run: |
+ rustup toolchain install stable --profile minimal
+ rustup target add --toolchain stable wasm32-unknown-unknown
+ - uses: taiki-e/install-action@v2
+ with:
+ tool: trunk@0.21.14
+ - uses: actions/setup-node@v7
+ with:
+ node-version: "24"
+ cache: npm
+ cache-dependency-path: ci/web/package-lock.json
+ - run: npm ci
+ working-directory: ci/web
+ - name: Build the winit app in both worker modes
+ working-directory: examples/winit
+ run: |
+ key=$(node ../../ci/web/example.mjs keys "$RUNNER_TEMP/vapid.json")
+ PUSHUPS_VAPID_PUBLIC_KEY="$key" trunk build --release --dist dist-rust
+ PUSHUPS_WEB_WORKER=static PUSHUPS_VAPID_PUBLIC_KEY="$key" trunk build --release --dist dist-static
+ # `build.rs` wrote `pushups::SERVICE_WORKER`, which the Trunk hook copied.
+ cmp dist-static/pushups-sw.js ../../js/pushups-sw.js
+ - name: The Rust-handler build in Chrome, with real pushes
+ run: node ci/web/example.mjs check examples/winit/dist-rust "$RUNNER_TEMP/vapid.json" '/sw\.js\?pushups=rust' --rust
+ - name: The static-worker build in Chrome, with real pushes
+ run: node ci/web/example.mjs check examples/winit/dist-static "$RUNNER_TEMP/vapid.json" '/pushups-sw\.js\?pushups=static'
+
example-leptos:
name: Example Leptos SSR app in Chrome
runs-on: ubuntu-latest
diff --git a/examples/winit/.gitignore b/examples/winit/.gitignore
index 949be0c..ffc490a 100644
--- a/examples/winit/.gitignore
+++ b/examples/winit/.gitignore
@@ -4,3 +4,5 @@
/android/app/build
/android/app/src/main/jniLibs
/android/local.properties
+/dist*
+/pushups-sw.js
diff --git a/examples/winit/Cargo.lock b/examples/winit/Cargo.lock
index 4684c69..662bbe5 100644
--- a/examples/winit/Cargo.lock
+++ b/examples/winit/Cargo.lock
@@ -1459,6 +1459,9 @@ dependencies = [
"futures-executor",
"pushups",
"serde_json",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "web-time",
"winit",
]
diff --git a/examples/winit/Cargo.toml b/examples/winit/Cargo.toml
index 6346ee3..bed315d 100644
--- a/examples/winit/Cargo.toml
+++ b/examples/winit/Cargo.toml
@@ -20,9 +20,18 @@ winit = "=0.31.0-beta.3"
[target.'cfg(target_os = "android")'.dependencies]
winit = { version = "=0.31.0-beta.3", features = ["android-native-activity"] }
+# The web build, which `Trunk` starts from `index.html` and which `build.rs` writes its worker beside.
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen-futures = "0.4"
+web-sys = { version = "0.3", features = ["console"] }
+web-time = "1"
[workspace]
+# The build script writes the static worker from `pushups::SERVICE_WORKER` for Trunk to copy.
+[build-dependencies]
+pushups = { path = "../.." }
+
# winit's Android loop ignores `Destroy` and allows one loop per process, so a relaunched Activity
# freezes its main thread (winit#4303). The fix's PR head, until a winit release carries it.
[patch.crates-io]
diff --git a/examples/winit/Trunk.toml b/examples/winit/Trunk.toml
new file mode 100644
index 0000000..7e6ff56
--- /dev/null
+++ b/examples/winit/Trunk.toml
@@ -0,0 +1,13 @@
+[build]
+# `sw.js` imports the glue by name, so its file name carries no hash.
+filehash = false
+
+[watch]
+# The build script writes it, so watching it would rebuild in a loop.
+ignore = ["pushups-sw.js"]
+
+# Runs after the build script wrote the worker, into the directory Trunk turns into `dist`.
+[[hooks]]
+stage = "post_build"
+command = "sh"
+command_arguments = ["-c", "cp pushups-sw.js \"$TRUNK_STAGING_DIR/pushups-sw.js\""]
diff --git a/examples/winit/build.rs b/examples/winit/build.rs
new file mode 100644
index 0000000..43dc8db
--- /dev/null
+++ b/examples/winit/build.rs
@@ -0,0 +1,11 @@
+//! Writes the static service worker beside the manifest, where the Trunk hook in `Trunk.toml`
+//! copies it into the site.
+
+fn main() {
+ let dir = std::env::var_os("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR");
+ let path = std::path::Path::new(&dir).join("pushups-sw.js");
+ // Rewriting an unchanged file would wake `trunk serve`'s watcher for nothing.
+ if std::fs::read_to_string(&path).ok().as_deref() != Some(pushups::SERVICE_WORKER) {
+ std::fs::write(&path, pushups::SERVICE_WORKER).expect("the worker is written");
+ }
+}
diff --git a/examples/winit/index.html b/examples/winit/index.html
new file mode 100644
index 0000000..85a322c
--- /dev/null
+++ b/examples/winit/index.html
@@ -0,0 +1,20 @@
+
+
+
+ pushups winit example
+
+
+
+
+
+
+
+
pushups winit example
+
+
+
+
diff --git a/examples/winit/src/app.rs b/examples/winit/src/app.rs
index fa11208..464c4c3 100644
--- a/examples/winit/src/app.rs
+++ b/examples/winit/src/app.rs
@@ -1,10 +1,19 @@
-//! The app both entry points run, `main` on desktop and iOS and `android_main` on Android.
+//! The app both entry points run, `main` on desktop, iOS and the web and `android_main` on Android.
use std::sync::mpsc::{Receiver, channel};
+#[cfg(not(target_arch = "wasm32"))]
+use std::time::{SystemTime, UNIX_EPOCH};
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-use pushups::{Config, Context, Event, LinuxConfig, Message, UnifiedPushConfig, WnsConfig};
+use pushups::{Config, Context, Event, Message};
+
+// The Linux and Windows sub-configs, unused on the web, which takes its key for `WebPushConfig` instead.
+#[cfg(not(target_arch = "wasm32"))]
+use pushups::{LinuxConfig, UnifiedPushConfig, WnsConfig};
+// `std`'s clock panics on `wasm32-unknown-unknown`, where `web-time` reads `Date.now()` instead.
+#[cfg(target_arch = "wasm32")]
+use web_time::{SystemTime, UNIX_EPOCH};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
@@ -18,6 +27,7 @@ pub(crate) fn install() {
}
}
+#[cfg(not(target_arch = "wasm32"))]
/// The app's configuration from build-time keys, `PUSHUPS_VAPID_PUBLIC_KEY` (base64url) for UnifiedPush on Linux and Android and `PUSHUPS_WNS_REMOTE_ID` for Windows.
fn config() -> Config {
// The bundle identifier, which also names the app on the Linux session bus.
@@ -34,6 +44,23 @@ fn config() -> Config {
config
}
+/// The web configuration from `PUSHUPS_VAPID_PUBLIC_KEY` (base64url). `PUSHUPS_WEB_WORKER=static` registers the static worker, which `build.rs` writes from `pushups::SERVICE_WORKER`, in place of the Rust handler, which runs from the `sw.js` entry.
+#[cfg(target_arch = "wasm32")]
+fn config() -> Config {
+ let key = option_env!("PUSHUPS_VAPID_PUBLIC_KEY")
+ .and_then(|key| URL_SAFE_NO_PAD.decode(key).ok())
+ .and_then(|key| <[u8; 65]>::try_from(key).ok());
+ let Some(key) = key else {
+ return Config::new();
+ };
+ let web = pushups::WebPushConfig::new(key);
+ Config::new().web(match option_env!("PUSHUPS_WEB_WORKER") {
+ Some("static") => web,
+ _ => web.rust_handler().service_worker_path("/sw.js"),
+ })
+}
+
+#[cfg(not(target_arch = "wasm32"))]
/// Registers, asks for permission, and runs the event loop until the app exits.
pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box> {
let proxy = event_loop.create_proxy();
@@ -56,6 +83,29 @@ pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box Result<(), Box> {
+ let proxy = event_loop.create_proxy();
+ let (sender, events) = channel();
+ pushups::set_handler(move |event| {
+ // A closed channel means the app is exiting.
+ let _ = sender.send(event);
+ proxy.wake_up();
+ });
+ if let Err(error) = pushups::register() {
+ log(&format!("register failed: {error}"));
+ }
+ wasm_bindgen_futures::spawn_local(async move {
+ let answer = pushups::request_permission().await;
+ log(&format!("permission {answer:?}"));
+ });
+ Ok(event_loop.run_app(App {
+ window: None,
+ events,
+ })?)
+}
+
struct App {
window: Option>,
events: Receiver,
@@ -128,6 +178,17 @@ fn on_push(_context: Context, message: Message) {
));
}
+/// Runs in the service worker for every push, and decides what the user sees.
+#[cfg(target_arch = "wasm32")]
+pub(crate) async fn notification_for(message: Message) -> pushups::Notification {
+ let at = now_ms();
+ let line = timing(&message, at);
+ log(&format!("worker push at_ms={at} {line}"));
+ pushups::Notification::new("pushups from Rust")
+ .body(format!("pushups winit example, {line}"))
+ .navigate("/")
+}
+
/// The push's `seq` and its latency from the sender's `sent_at_ms` to `at`, for a JSON payload
/// such as an FCM data map.
fn timing(message: &Message, at: u128) -> String {
@@ -146,14 +207,14 @@ fn timing(message: &Message, at: u128) -> String {
}
fn now_ms() -> u128 {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_millis())
}
/// Prints the line, and appends it to `pushups-example.log` in the temporary directory, which outlives a process the system launched with nothing reading its output.
-#[cfg(not(target_os = "android"))]
-fn log(line: &str) {
+#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
+pub(crate) fn log(line: &str) {
use std::io::Write;
let line = format!("pushups-winit-example {line}");
@@ -169,6 +230,12 @@ fn log(line: &str) {
}
}
+/// Logs the line to the console, which `ci/web/example.mjs` reads with the page's lines.
+#[cfg(target_arch = "wasm32")]
+pub(crate) fn log(line: &str) {
+ web_sys::console::log_1(&format!("pushups-winit-example {line}").into());
+}
+
/// Writes the line to logcat under the tag `pushups-example`, which `ci/android/example.sh` reads.
#[cfg(target_os = "android")]
fn log(line: &str) {
diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs
index 7ee3159..4175dfd 100644
--- a/examples/winit/src/main.rs
+++ b/examples/winit/src/main.rs
@@ -1,8 +1,33 @@
-//! Receives pushes through `pushups` under winit 0.31, which installs no app delegate on Apple, and logs every step with its wall-clock time to standard output and to `pushups-example.log` in the temporary directory.
+//! Receives pushes through `pushups` under winit 0.31, which installs no app delegate on Apple,
+//! and logs every step with its wall-clock time to standard output and to `pushups-example.log`
+//! in the temporary directory, or to the console on the web.
mod app;
+#[cfg(not(target_arch = "wasm32"))]
fn main() -> Result<(), Box> {
app::install();
app::run(winit::event_loop::EventLoop::new()?)
}
+
+/// The web entry, which Trunk builds from this target. In the service worker it serves the Rust
+/// handler, and in the page it installs pushups and runs the winit event loop.
+#[cfg(target_arch = "wasm32")]
+fn main() {
+ if pushups::in_service_worker() {
+ if let Err(error) = pushups::serve_service_worker(app::notification_for) {
+ app::log(&format!("serving the service worker failed: {error}"));
+ }
+ return;
+ }
+ app::install();
+ match winit::event_loop::EventLoop::new() {
+ // The event loop does not block on the web, and keeps running after `main` returns.
+ Ok(event_loop) => {
+ if let Err(error) = app::run(event_loop) {
+ app::log(&format!("event loop failed: {error}"));
+ }
+ }
+ Err(error) => app::log(&format!("event loop failed: {error}")),
+ }
+}
diff --git a/examples/winit/sw.js b/examples/winit/sw.js
new file mode 100644
index 0000000..84a4cd2
--- /dev/null
+++ b/examples/winit/sw.js
@@ -0,0 +1,5 @@
+// The service worker under `rust_handler`. Trunk starts the wasm only in the page, so this module
+// starts it in the worker, where the app's `main` serves its Rust handler.
+import init from "./pushups-winit-example.js";
+
+init();