Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/winit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
/android/app/build
/android/app/src/main/jniLibs
/android/local.properties
/dist*
/pushups-sw.js
3 changes: 3 additions & 0 deletions examples/winit/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions examples/winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 13 additions & 0 deletions examples/winit/Trunk.toml
Original file line number Diff line number Diff line change
@@ -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\""]
11 changes: 11 additions & 0 deletions examples/winit/build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
20 changes: 20 additions & 0 deletions examples/winit/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>pushups winit example</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link data-trunk rel="rust" data-bin="pushups-winit-example">
<link data-trunk rel="copy-file" href="sw.js">
<style>
html, body { margin: 0; background: #0e1824; }
body { font-family: system-ui, sans-serif; color: #f5f7fa; padding: 20px 16px; }
button { font-size: 16px; font-weight: 600; color: #ffffff; padding: 10px 16px; border: none; border-radius: 10px; background: linear-gradient(135deg, #fa1e13, #f67017); }
</style>
</head>
<body>
<h2>pushups winit example</h2>
<!-- Asks for the notification permission on click, which the prompt needs from a gesture. -->
<button onclick="Notification.requestPermission().then((answer) => { this.textContent = 'Notifications: ' + answer; })">Allow notifications</button>
</body>
</html>
79 changes: 73 additions & 6 deletions examples/winit/src/app.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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.
Expand All @@ -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<dyn std::error::Error>> {
let proxy = event_loop.create_proxy();
Expand All @@ -56,6 +83,29 @@ pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box<dyn std::error::Error
})?)
}

/// Registers, asks for permission, and runs the event loop, which on the web does not block and keeps running in the page's event loop after `main` returns.
#[cfg(target_arch = "wasm32")]
pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box<dyn std::error::Error>> {
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<Box<dyn Window>>,
events: Receiver<Event>,
Expand Down Expand Up @@ -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 {
Expand All @@ -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}");
Expand All @@ -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) {
Expand Down
27 changes: 26 additions & 1 deletion examples/winit/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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}")),
}
}
5 changes: 5 additions & 0 deletions examples/winit/sw.js
Original file line number Diff line number Diff line change
@@ -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();
Loading