Routing & Navigation
The blinc_router crate provides cross-platform routing with path matching, navigation history, guards, page transitions, and deep linking.
Setup
#![allow(unused)]
fn main() {
use blinc_router::{RouterBuilder, Route, PageTransition};
let router = RouterBuilder::new()
.route(Route::new("/").name("home").view(home_page))
.route(Route::new("/users").name("users").view(users_page)
.child(Route::new("/:id").name("user").view(user_detail)))
.route(Route::new("/settings")
.view(settings_page)
.transition(PageTransition::modal()))
.not_found(not_found_page)
.build();
}
Navigation
#![allow(unused)]
fn main() {
// Push (adds to history)
router.push("/users/42");
// Named route with params
router.push_named("user", &[("id", "42")]);
// Replace (no history entry)
router.replace("/login");
// Back / Forward
router.back();
router.forward();
// Check state
router.can_go_back();
router.current_path();
router.params().get("id");
router.query().get("page");
}
Route Outlet
Place router.outlet() where the page content should render:
#![allow(unused)]
fn main() {
fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
div().flex_col()
.child(nav_bar(&router))
.child(router.outlet()) // Current route renders here
}
}
use_router() Hook
Inside route views, use_router() returns the active router:
#![allow(unused)]
fn main() {
fn user_detail(ctx: RouteContext) -> Div {
let router = use_router(); // Same as ctx.router
let id = ctx.params.get("id").unwrap_or("?");
div()
.child(text(&format!("User #{}", id)))
.child(
div().on_click(move |_| router.back())
.child(text("Back"))
)
}
}
Nested routers work automatically — use_router() returns whichever router’s outlet() is currently building.
Page Transitions
Per-route transitions using Blinc’s animation system:
#![allow(unused)]
fn main() {
Route::new("/settings")
.view(settings_page)
.transition(PageTransition::slide()) // iOS push style
.transition(PageTransition::fade()) // Crossfade
.transition(PageTransition::modal()) // Slide up/down
.transition(PageTransition::scale()) // Scale in/out
.transition(PageTransition::none()) // Instant
// Custom with spring physics
.transition(PageTransition::slide().with_spring(SpringConfig::bouncy()))
}
Navigation Guards
Protect routes with guards that allow, redirect, or reject:
#![allow(unused)]
fn main() {
use blinc_router::{NavigationGuard, GuardResult};
use std::sync::Arc;
let auth_guard: NavigationGuard = Arc::new(|_from, _to| {
if is_authenticated() {
GuardResult::Allow
} else {
GuardResult::Redirect("/login".into())
}
});
RouterBuilder::new()
.route(Route::new("/dashboard").view(dashboard).guard(auth_guard))
.build();
}
Deep Linking
Deep linking is automatic — just build a router and it works on all platforms.
RouterBuilder::build() auto-registers the deep link handler and back button.
#![allow(unused)]
fn main() {
// That's it — no platform-specific setup needed in Rust
let router = RouterBuilder::new()
.route(Route::new("/users/:id").view(user_page))
.build();
// Deep links to myapp://host/users/42 automatically navigate
}
Platform Configuration
Android — add intent filters in AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent-filter>
iOS — add URL types in Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
Desktop — register a custom URL scheme with the OS:
macOS (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
Windows (registry, set up by installer):
HKEY_CLASSES_ROOT\myapp\shell\open\command = "C:\path\to\myapp.exe" "--deep-link=%1"
Linux (.desktop file):
MimeType=x-scheme-handler/myapp
Exec=myapp --deep-link=%u
CLI fallback:
myapp --deep-link=myapp://host/users/42
How It Works
RouterBuilder::build()registers a global deep link handler- Platform runners auto-dispatch incoming URIs to the handler
- The router parses the URI and calls
push(path) - No user code needed beyond building the router
System Back Button
Also automatic — RouterBuilder::build() registers a back button handler.
- Android: system back button navigates back if the router has history
- Desktop:
Key::Backdispatches through the back handler stack - If at the root route, the event propagates (app exits normally)
Route Matching
Express-style path patterns:
| Pattern | Example | Matches |
|---|---|---|
| Static | /about | Exact match |
| Parameter | /users/:id | /users/42 → {id: "42"} |
| Wildcard | /files/*path | /files/a/b/c → {path: "a/b/c"} |
| Nested | parent + child | /users + /:id → /users/42 |
| Query | any path | /search?q=hello → {q: "hello"} |
Named Routes
Look up routes by name for type-safe navigation:
#![allow(unused)]
fn main() {
// Check if a named route exists
router.path_for("user"); // Some("/users/:id")
// Navigate with params
router.push_named("user", &[("id", "42")]); // → /users/42
// Check if a path matches
router.has_route("/users/42"); // true
}
Tab Navigator
Use the tabs() component from blinc_cn with the router’s current path as the active tab:
#![allow(unused)]
fn main() {
use blinc_cn::tabs;
fn app_shell(router: &Router) -> Div {
// Track active tab via router path
let active_tab = ctx.use_state_keyed("tab", || router.current_path());
div().flex_col().w_full().h_full()
// Content area — router outlet
.child(router.outlet().flex_grow())
// Bottom tab bar
.child(
tabs(&active_tab)
.tab("Home", "/", {
let r = router.clone();
move || r.push("/")
})
.tab("Search", "/search", {
let r = router.clone();
move || r.push("/search")
})
.tab("Profile", "/profile", {
let r = router.clone();
move || r.push("/profile")
})
)
}
}
Each tab click calls router.push() which updates the outlet. The tab state stays in sync with the route.
Stack Navigator (Page Stack)
The router maintains a page stack — pages persist in the tree when new pages are pushed on top. Suspended pages have input disabled and are hidden, but their state (scroll position, form values, etc.) is preserved.
#![allow(unused)]
fn main() {
// Renders the page stack — active page visible, suspended pages preserved
router.outlet()
}
When you router.push("/details"):
- The current page becomes Suspended (opacity 0, pointer_events_none)
- The new page is pushed as Active on top
When you router.back():
- The top page is removed from the stack
- The page below becomes Active again (with preserved state)
Page state
#![allow(unused)]
fn main() {
use blinc_router::PageState;
let pages = router.page_stack();
for page in &pages {
match page.state {
PageState::Active => println!("Visible: {}", page.route.path),
PageState::Suspended => println!("Hidden: {}", page.route.path),
}
}
}
Entry/exit animations
Use motion() containers inside route views for animated transitions:
#![allow(unused)]
fn main() {
fn user_detail(ctx: RouteContext) -> Div {
motion()
.slide_in(SlideDirection::Right, 300)
.child(
div().w_full().h_full()
.child(text(&format!("User #{}", ctx.params.get("id").unwrap_or("?"))))
.child(
div().on_click({
let r = ctx.router.clone();
move |_| r.back()
})
.child(text("Back"))
)
)
}
}
Nested Route Stacks
Layout routes can contain their own scoped router for sub-navigation.
use_router() automatically returns the innermost router:
#![allow(unused)]
fn main() {
fn dashboard_layout(ctx: RouteContext) -> Div {
// Create a sub-router for dashboard tabs
let sub_router = RouterBuilder::new()
.route(Route::new("/").view(dashboard_overview))
.route(Route::new("/analytics").view(analytics))
.route(Route::new("/settings").view(settings))
.initial(&ctx.path) // Start at current sub-path
.build();
div().flex_row().w_full().h_full()
.child(dashboard_sidebar(&sub_router))
.child(sub_router.outlet()) // Nested outlet — use_router() returns sub_router here
}
}
Bottom Sheet Navigation
Use sheet() from blinc_cn for modal-like navigation that slides up from the bottom:
#![allow(unused)]
fn main() {
use blinc_cn::sheet;
fn show_details(router: &Router, item_id: &str) {
// Navigate to detail route
router.push(&format!("/items/{}", item_id));
// Or show as a bottom sheet overlay
sheet()
.title("Item Details")
.content(move || {
let router = use_router();
router.outlet() // Render the matched route inside the sheet
})
.show();
}
}
For gesture-dismissable sheets on mobile, the sheet component handles the swipe-down gesture automatically. On dismiss, call router.back():
#![allow(unused)]
fn main() {
sheet()
.on_close({
let r = router.clone();
move || r.back()
})
.content(|| detail_view())
.show();
}
Navigation Patterns Summary
| Pattern | Widget | Router Integration |
|---|---|---|
| Page navigation | router.outlet() | Direct — renders current route |
| Tab bar | blinc_cn::tabs() | Tab clicks call router.push() |
| Stack with animations | stack() + motion() | Wrap route views in motion containers |
| Bottom sheet | blinc_cn::sheet() | Content renders router.outlet(), dismiss calls router.back() |
| Drawer / sidebar | blinc_cn::drawer() | Navigation links call router.push() |
| Back button | Auto-registered | RouterBuilder::build() wires it |
| Deep links | Auto-registered | Platform dispatches to router automatically |