Examples
The repo ships four runnable web examples under examples/. Each is a single-file lib.rs under 250 lines, with the same index.html + serve.sh scaffolding from the Setup chapter. They’re the canonical reference for every pattern in this book — when in doubt, copy from one of these.
| Example | What it demonstrates | Lines |
|---|---|---|
web_hello | Minimum: canvas + bundled font + one Div | ~130 |
web_scroll | Wheel input → scroll widget → physics tick | ~200 |
web_drag | Mouse drag → Stateful + State::set → visual update | ~245 |
web_assets | Fetch fonts at runtime via WebAssetLoader | ~135 |
web_hello — the smallest possible app
The “Hello, WebGPU!” canonical example. Centered text on a dark background, no input handlers, no animations, no state. If this draws on a real canvas in a real browser, the entire wgpu / wasm-bindgen / requestAnimationFrame / WebApp::run pipeline is alive end-to-end.
#![allow(unused)]
fn main() {
use blinc_app::web::WebApp;
use blinc_layout::div::{div, Div};
use blinc_layout::text::text;
use blinc_core::Color;
use wasm_bindgen::prelude::*;
const ARIAL: &[u8] = include_bytes!("../fonts/Arial.ttf");
#[wasm_bindgen(start)]
pub fn _start() {
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(async {
WebApp::run_with_setup(
"blinc-canvas",
|app| { app.load_font_data(ARIAL.to_vec()); },
build_ui,
).await.unwrap();
});
}
fn build_ui(_ctx: &mut blinc_app::windowed::WindowedContext) -> Div {
div()
.w_full().h_full()
.bg(Color::rgba(0.07, 0.07, 0.10, 1.0))
.items_center().justify_center()
.child(
text("Hello, WebGPU!")
.size(32.0)
.color(Color::WHITE),
)
}
}
Read the full example: examples/web_hello/src/lib.rs.
web_scroll — wheel input + scroll physics
A vertical list of 24 cards inside a scroll() container. Demonstrates:
- Wheel input routes through
EventRouter::on_scroll_nestedand is dispatched viaRenderTree::dispatch_scroll_chain(which walks the chain of scroll containers from leaf to root for nested-scroll consumption) - The scroll widget’s per-frame physics tick (
tree.tick_scroll_physics(now_ms)) runs every rAF frame, advancing any active deceleration - Click events also fire on cards via
on_clickhandlers - The no-bounce default for wasm32 —
scroll()returns aScrollConfig::no_bounce()config because DOM wheel events have no “gesture ended” phase to drive bounce-back from. See the Overview for the rationale.
#![allow(unused)]
fn main() {
const CARD_COUNT: usize = 24;
let mut content = div().w_full().flex_col().p_px(20.0).gap_px(12.0);
for idx in 0..CARD_COUNT {
let label = format!("Card {}", idx + 1);
let card_index = idx + 1;
content = content.child(
div()
.w_full().h_fit()
.bg(Color::rgba(0.16, 0.16, 0.21, 1.0))
.rounded(12.0).p_px(16.0)
.child(text(&label).size(20.0).color(Color::WHITE))
.on_click(move |_| {
web_sys::console::log_1(
&format!("clicked card #{card_index}").into(),
);
}),
);
}
div()
.w_full().h_full()
.bg(Color::rgba(0.07, 0.07, 0.10, 1.0))
.child(
scroll()
.w_full()
.h(ctx.height - 96.0)
.child(content)
.on_scroll(|e| {
tracing::info!(
"scroll delta=({:.1}, {:.1})",
e.scroll_delta_x, e.scroll_delta_y,
);
}),
)
}
Full example: examples/web_scroll/src/lib.rs.
web_drag — gesture interaction with Stateful + State
A single draggable card that lifts (opacity dip + raised z-index) and follows the cursor while held, then snaps back on release. Structurally identical to the sortable_list_section in the desktop sortable_demo.rs — same DragFSM, same Stateful::on_state recipe, same handler chain, same code that runs on Android and iOS:
#![allow(unused)]
fn main() {
use blinc_layout::stateful::{stateful_with_key, StateTransitions};
use blinc_core::reactive::State;
use blinc_core::context_state::BlincContextState;
use blinc_core::events::event_types;
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum DragFSM {
#[default]
Idle,
Dragging,
}
impl StateTransitions for DragFSM {
fn on_event(&self, event: u32) -> Option<Self> {
match (self, event) {
(DragFSM::Idle, event_types::DRAG) => Some(DragFSM::Dragging),
(DragFSM::Dragging, event_types::DRAG_END) => Some(DragFSM::Idle),
(DragFSM::Dragging, event_types::POINTER_UP) => Some(DragFSM::Idle),
_ => None,
}
}
}
fn draggable_card() -> Stateful<DragFSM> {
let blinc = BlincContextState::get();
let offset: State<(f32, f32)> =
blinc.use_state_keyed("card_offset", || (0.0, 0.0));
let offset_for_drag = offset.clone();
let offset_for_end = offset.clone();
stateful_with_key::<DragFSM>("draggable-card")
.deps([offset.signal_id()])
.on_state(move |ctx| {
let (ox, oy) = offset.get();
let dragging = matches!(ctx.state(), DragFSM::Dragging);
let mut card = div()
.w(220.0).h(120.0)
.bg(Color::rgba(0.32, 0.55, 0.92, 1.0))
.rounded(16.0)
.items_center().justify_center()
.child(text("Drag me").size(20.0).color(Color::WHITE));
if dragging {
card = card
.transform(Transform::translate(ox, oy))
.opacity(0.85)
.z_index(100);
}
card
})
.on_drag(move |e| {
offset_for_drag.set((e.drag_delta_x, e.drag_delta_y));
})
.on_drag_end(move |_e| {
offset_for_end.set((0.0, 0.0));
})
}
}
The framework’s DragFSM Stateful state transitions automatically as DRAG / DRAG_END events fire — ctx.state() reads the current FSM state without you having to maintain a parallel bool. The visual offset lives in its own State<(f32, f32)> cell because it changes far more frequently than the FSM (every drag tick vs only at gesture boundaries).
Full example: examples/web_drag/src/lib.rs.
web_assets — fetched font instead of bundled
Demonstrates the WebApp::run_with_async_setup + WebAssetLoader::fetch_bytes pattern. The font isn’t bundled inside the wasm artifact; it’s fetched at runtime as a separate static asset that the browser caches independently. This example’s wasm is 612 KB smaller than web_hello purely because Arial is no longer baked into the bundle:
#![allow(unused)]
fn main() {
use blinc_app::web::WebApp;
use blinc_app::BlincError;
use blinc_platform_web::WebAssetLoader;
const FONT_URL: &str = "fonts/Arial.ttf";
#[wasm_bindgen(start)]
pub fn _start() {
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(async {
WebApp::run_with_async_setup(
"blinc-canvas",
// The `Box::pin(async move { ... })` ceremony is the
// stable-Rust workaround for the lack of `async FnOnce`.
// Once async closures stabilize this drops back to
// `|app| async move { ... }`.
|app| Box::pin(async move {
let bytes = WebAssetLoader::fetch_bytes(FONT_URL)
.await
.map_err(|e| BlincError::Platform(e.to_string()))?;
app.load_font_data(bytes);
Ok(())
}),
build_ui,
).await.unwrap();
});
}
}
See the Fonts & Assets chapter for the full picture, including the recommended bundled-fallback-then-fetched-real-font pattern for production apps.
Full example: examples/web_assets/src/lib.rs.
Running an example locally
git clone https://github.com/project-blinc/Blinc
cd Blinc/examples/web_hello
wasm-pack build --target web --release
./serve.sh
# open http://localhost:8000/
./serve.sh picks the first available static-file server on your system (python3 → python → ruby → npx http-server) and runs it from the example directory. If pkg/ doesn’t exist yet (i.e. you forgot to wasm-pack build first), the script exits with a hint.