Hot reloading
CSS files
Enable css-hot-reload to update named stylesheets while a debug desktop
app is running. No Rust recompilation or dev server is needed. File watching
is off by default and does nothing in release, web or mobile builds.
Expose an application feature so development builds can opt in:
[dependencies]
blinc = "0.6"
[features]
dev-css = ["blinc/css-hot-reload"]
Register an embedded stylesheet for every build, then watch its source file
when developing. Given src/main.rs and assets/app.css:
use blinc::prelude::*; use blinc::app::windowed::WindowedApp; fn main() -> Result<()> { WindowedApp::run(WindowConfig::default(), |ctx| { if ctx.rebuild_count == 0 { ctx.set_css("app", include_str!("../assets/app.css")) .expect("valid application CSS"); #[cfg(all(debug_assertions, feature = "dev-css"))] ctx.watch_css("app", concat!(env!("CARGO_MANIFEST_DIR"), "/assets/app.css")) .expect("watch application CSS"); } div().class("card").child(text("Project settings")) }) }
cargo run --features dev-css
Each save replaces the named source through set_css; rules removed from the
file disappear, and theme and :root variables remain available. Signals and
component state survive. Invalid CSS or unreadable files produce a warning and
keep the last valid stylesheet; a later valid save recovers automatically. An
empty file removes that source's rules.
The watcher debounces writes and follows file replacement on save. It wakes an idle window without requiring input. Repeated registration of the same name and path reuses its watcher. Different names cascade in registration order; registering an existing name with a different path replaces that watcher only after the new file loads successfully.
Watchers belong to their window's context. ctx.unwatch_css("app") stops one
without removing its styles; closing the window stops all of them. Release
builds contain no CSS watcher, worker thread or file reads, even if the feature
remains enabled. Keep the embedded set_css call so those builds retain styles.
Browser hosts can send CSS strings directly to set_css without a file watcher.
Rust hot-patching (experimental)
Status: Experimental, debug-only. Currently driven by Dioxus's
dxCLI through its--hot-patchmode. The patch points and the websocket client that talks to dx's dev-server both live insideblinc_app— Blinc apps don't depend on the Dioxus reactive runtime.
Blinc can hot-patch the body of your UI builder closure while the
app is running, so iterating on a layout or styling tweak doesn't
need a full rebuild + relaunch. State held by Stateful widgets
and Signal hooks is preserved across
patches because Blinc keys it by InstanceKey rather than by
closure identity — your reactive graph survives the swap.
The integration uses the subsecond
crate. subsecond is itself gated on debug_assertions, so even if
you ship a release binary with the hot-reload feature enabled, the
patch points compile down to direct calls and there is zero runtime
overhead.
When you enable the hot-reload feature, Blinc also pulls in
tungstenite (websocket
client, no TLS — dx serves over ws://localhost) and
dioxus-cli-config
(tiny env-var convention crate, zero deps on native). Both stay
out of default builds; they only show up when the feature is on.
Setup
-
Enable the feature on
blinc:[dependencies] blinc = { version = "0.6", features = ["windowed", "hot-reload"] } -
Install the Dioxus CLI (one-time):
cargo install dioxus-cli -
Run your binary crate under
dx:dx serve --hot-patch \ --package my_app \ --features blinc/hot-reload \ --platform macosdxbuilds your binary, launches it, and watches the source tree. The Blinc runtime opens a websocket back to dx as soon as the window is ready (announcing the running process's ASLR offset, build id, and pid as query parameters), so dx can compute jump-table offsets when it ships a patch. When dx detects a change in the binary crate it compiles a patch, ships it over the websocket, andsubsecond::apply_patchapplies it in place. The next frame Blinc renders picks up the new closure body.
The websocket protocol matches
dxCLI 0.7+. Blinc keeps a small mirror of the relevantDevserverMsgsubset locally; if a future dx release changes the schema for the fields we read (jump_table,for_pid), thehot-reloadfeature will need a compatibility bump. Other fields (template patches, asset reloads, etc.) deserialise opaquely so dx can add new fields without breaking us.
What gets hot-reloaded
- Code in the binary crate — i.e. the crate where
main.rslives.subsecondonly patches the "tip" crate; this is a hard limitation of the dynamic-linking approach it uses. - CSS strings inlined in your UI builder. The patched closure
sees the new string literal and re-registers the stylesheet on
the rebuild that follows the patch. Blinc's hot-reload runtime
clears the accumulated stylesheet, drops
css_sources, and resetsrebuild_countto zero before re-invoking the closure, so the common
guard fires again with the new content and the result is a fresh sheet — no stale rules from the pre-patch run.#![allow(unused)] fn main() { if ctx.rebuild_count == 0 { ctx.add_css(MY_CSS); } } - Runtime-loaded image and SVG assets, when you opt the
app's asset directory into the file watcher:
The watcher tails the directory recursively. When a file under it changes, Blinc drops the matching entry from its image cache (matched by path-suffix against the URI you passed tofn main() -> blinc::app::Result<()> { #[cfg(feature = "hot-reload")] blinc::app::hot_reload::watch_dir("assets"); WindowedApp::run(WindowConfig::default(), |ctx| build_ui(ctx)) }image("...")), wipes the SVG document cache and atlas, and forces the next frame to re-read the bytes off disk. Works for PNG, JPEG, WebP, runtime-loaded fonts, and SVG sources loaded from a path. Does not fire forinclude_bytes!-embedded assets — those live in rodata that subsecond can't touch (see What doesn't below). - State held by
Statefulwidgets and hooks. Blinc'sInstanceKeyis derived from#[track_caller]+ a per-frame call counter, so the same logical widget gets the same key before and after a patch. Signal hooks and FSM state survive.
What doesn't
- Code in workspace dependencies (
blinc_layout,blinc_app,blinc_gpu, etc.). Editing the framework itself still requires a full rebuild. - CSS in
conststrings.subsecondpatches function bodies, not the binary's read-only data segment. Aconst STYLESHEET: &str = "..."referenced from your UI builder will keep pointing at the original string after a patch, so edits to the const won't be picked up. Inline the CSS directly in thectx.add_css(r#"..."#)call — that string literal lives in the patched function body, so subsecond rewrites it alongside the rest of the closure. Same reasoning applies tostaticstrings. include_bytes!/include_str!assets. Files baked into the binary at compile time live in the same rodata segment that CSS const strings do — subsecond can't update them. Aregister_font(include_bytes!("Inter.ttf").to_vec())keeps serving the original font bytes after a patch. To get hot-reloadable font / image assets, load them via runtime path instead (image("assets/logo.png"),register_font(std::fs::read("Inter.ttf")?)) and put the containing dir underwatch_dir.- Static initialisers in the patched crate.
subsecondtracks statics across patches but does not re-run their destructors, and thread-locals get reset. If your binary crate initialises a heavyOnceLockat startup, that initialiser won't re-run after a patch. - Struct field-layout changes.
subsecondcannot safely patch a closure if a struct it captures has had fields added, removed, or reordered — the in-memory layout has shifted out from under the patched code. In practice this means: change behaviour freely, but if you change a field on a struct that participates in your reactive graph (e.g. a custom#[derive(BlincComponent)]struct), restart. - Release builds.
subsecond::callis a no-op whendebug_assertionsis off. Release builds compile thehot-reloadfeature in but receive no patches at runtime.
Multi-window apps
The integration patches both the primary-window UI builder and any
secondary windows opened via WindowedApp::create_window(...).
Each window's builder closure goes through its own subsecond::call
patch point, so a code change is picked up on the next frame of
every open window.
Troubleshooting
- "Patch had no effect." Double-check that the change is in
your binary crate, not in a workspace dependency.
dx serve --hot-patchprints which crate it patched; if the line doesn't mention your binary, the edit was outside the patchable scope. - dx logs
Ignoring hotpatch since there is no ASLR reference. The app didn't open the websocket back to dx, so dx couldn't compute jump-table offsets. Check that you compiled the app with thehot-reloadfeature on (--features blinc/hot-reload) and that you're running the resulting binary as a child ofdx serve --hot-patch(which sets theDIOXUS_DEVSERVER_*env vars the client reads). On startup the client logshot-reload: connectedat info level when the websocket opens. - CSS edit doesn't show up. For external CSS files, use
watch_css. For Rust hot-patching, if your stylesheet lives in aconst STYLESHEET: &str = ...(or other binary-resident static), the patched closure references the pre-patch address and never sees the new string — see What doesn't above. Inline the CSS in thectx.add_css(r#"..."#)call instead. Once the closure body owns the literal, edits round-trip through the patch without restart. - State got cleared after a patch. This means
subseconddetected a structural change (a captured struct's fields moved) and forced a full re-instance. The next frame rebuilds from scratch. This is expected — restart and rebuild if you need the old state back. - Crash after a patch. Most likely a struct-layout edit that
squeaked past
subsecond's safety checks. Restart the app to recover, then file an issue against Blinc with the diff that triggered it. Even if the root cause is insubsecond, the Blinc team wants to know which patterns are unsafe in practice so we can document them here.
What is shipped
The in-process patch points sit behind a feature flag, alongside the
websocket client that talks to dx serve --hot-patch. Editing a UI
body in your binary crate patches in place without a restart. Blinc
apps link no part of the Dioxus reactive runtime — only subsecond,
tungstenite, and the env-var convention crate.