pub mod history;
pub mod route;
pub mod transition;
use std::sync::{Arc, Mutex};
use history::RouterHistory;
use route::RouteTrie;
pub type RouteView = fn(RouteContext) -> blinc_layout::div::Div;
pub type NavigationGuard = Arc<dyn Fn(&HistoryEntry, &MatchedRoute) -> GuardResult + Send + Sync>;
pub enum GuardResult {
Allow,
Redirect(String),
Reject(String),
}
struct RouterInner {
trie: RouteTrie,
views: Vec<RouteView>,
guards: Vec<NavigationGuard>,
history: RouterHistory,
current_match: Option<MatchedRoute>,
named_routes: rustc_hash::FxHashMap<String, String>,
route_scopes: rustc_hash::FxHashMap<String, blinc_animation::suspension::ScopeId>,
active_route_path: Option<String>,
}
#[derive(Clone)]
pub struct Router {
inner: Arc<Mutex<RouterInner>>,
}
impl Router {
pub fn push(&self, path: impl Into<String>) {
let path = path.into();
let mut state = self.inner.lock().unwrap();
let matched = state.trie.match_path(&path);
if let Some(ref m) = matched {
for guard in &state.guards {
match guard(&state.history.current, m) {
GuardResult::Allow => {}
GuardResult::Redirect(redirect_path) => {
drop(state);
self.push(redirect_path);
return;
}
GuardResult::Reject(reason) => {
tracing::warn!("Navigation to '{}' rejected: {}", path, reason);
return;
}
}
}
}
let entry = HistoryEntry {
path: path.clone(),
params: matched
.as_ref()
.map(|m| m.params.clone())
.unwrap_or_default(),
query: matched
.as_ref()
.map(|m| m.query.clone())
.unwrap_or_default(),
title: None,
};
state.history.push(entry);
state.current_match = matched;
}
pub fn replace(&self, path: impl Into<String>) {
let path = path.into();
let mut state = self.inner.lock().unwrap();
let matched = state.trie.match_path(&path);
let entry = HistoryEntry {
path: path.clone(),
params: matched
.as_ref()
.map(|m| m.params.clone())
.unwrap_or_default(),
query: matched
.as_ref()
.map(|m| m.query.clone())
.unwrap_or_default(),
title: None,
};
state.history.replace(entry);
state.current_match = matched;
}
pub fn back(&self) {
let mut state = self.inner.lock().unwrap();
if let Some(entry) = state.history.back() {
let path = entry.path.clone();
state.current_match = state.trie.match_path(&path);
}
}
pub fn forward(&self) {
let mut state = self.inner.lock().unwrap();
if let Some(entry) = state.history.forward() {
let path = entry.path.clone();
state.current_match = state.trie.match_path(&path);
}
}
pub fn can_go_back(&self) -> bool {
self.inner.lock().unwrap().history.can_go_back()
}
pub fn can_go_forward(&self) -> bool {
self.inner.lock().unwrap().history.can_go_forward()
}
pub fn current_path(&self) -> String {
self.inner.lock().unwrap().history.current.path.clone()
}
pub fn current_route(&self) -> Option<MatchedRoute> {
self.inner.lock().unwrap().current_match.clone()
}
pub fn params(&self) -> RouteParams {
self.current_route().map(|r| r.params).unwrap_or_default()
}
pub fn query(&self) -> QueryParams {
self.current_route().map(|r| r.query).unwrap_or_default()
}
pub fn has_route(&self, path: &str) -> bool {
let state = self.inner.lock().unwrap();
state.trie.match_path(path).is_some()
}
pub fn path_for(&self, name: &str) -> Option<String> {
self.inner.lock().unwrap().named_routes.get(name).cloned()
}
pub fn push_named(&self, name: &str, params: &[(&str, &str)]) {
if let Some(template) = self.path_for(name) {
let mut path = template;
for (key, value) in params {
path = path.replace(&format!(":{}", key), value);
}
self.push(path);
} else {
tracing::warn!("Named route '{}' not found", name);
}
}
pub fn handle_deep_link(&self, uri: &str) {
use blinc_platform::deep_link::{DeepLink, DeepLinkSource};
if let Some(dl) = DeepLink::parse(uri, DeepLinkSource::System) {
let path = dl.route_path();
tracing::info!("Deep link: {} → {}", uri, path);
self.push(path);
} else {
tracing::warn!("Failed to parse deep link URI: {}", uri);
}
}
pub fn register_back_handler(&self) -> blinc_layout::back_handler::BackHandlerHandle {
let router = self.clone();
blinc_layout::back_handler::push_back_handler(move || {
if router.can_go_back() {
router.back();
true } else {
false }
})
}
pub fn outlet(&self) -> blinc_layout::div::Div {
let view_and_ctx = {
let state = self.inner.lock().unwrap();
state.current_match.as_ref().and_then(|matched| {
state.views.get(matched.view_index).map(|view| {
let ctx = RouteContext {
params: matched.params.clone(),
query: matched.query.clone(),
path: matched.path.clone(),
router: self.clone(),
};
(*view, ctx)
})
})
};
if let Some((view, ctx)) = view_and_ctx {
let route_path = ctx.path.clone();
{
let mut state = self.inner.lock().unwrap();
let handle = blinc_animation::get_scheduler();
if let Some(ref old_path) = state.active_route_path {
if *old_path != route_path {
if let Some(&scope) = state.route_scopes.get(old_path) {
blinc_animation::suspension::suspend_scope(scope, &handle);
}
}
}
let scope = *state
.route_scopes
.entry(route_path.clone())
.or_insert_with(blinc_animation::suspension::create_scope);
blinc_animation::suspension::resume_scope(scope, &handle);
blinc_animation::suspension::enter_scope(scope);
state.active_route_path = Some(route_path);
}
push_router_context(self);
let result = view(ctx);
pop_router_context();
blinc_animation::suspension::exit_scope();
result
} else {
blinc_layout::div::div()
}
}
}
pub struct RouterBuilder {
routes: Vec<Route>,
not_found: Option<RouteView>,
guards: Vec<NavigationGuard>,
initial_path: String,
}
impl RouterBuilder {
pub fn new() -> Self {
Self {
routes: Vec::new(),
not_found: None,
guards: Vec::new(),
initial_path: "/".to_string(),
}
}
pub fn route(mut self, route: Route) -> Self {
self.routes.push(route);
self
}
pub fn not_found(mut self, view: RouteView) -> Self {
self.not_found = Some(view);
self
}
pub fn guard(mut self, guard: NavigationGuard) -> Self {
self.guards.push(guard);
self
}
pub fn initial(mut self, path: impl Into<String>) -> Self {
self.initial_path = path.into();
self
}
pub fn build(self) -> Router {
let mut trie = RouteTrie::new();
let mut views: Vec<RouteView> = Vec::new();
let mut named_routes = rustc_hash::FxHashMap::default();
fn register_routes(
trie: &mut RouteTrie,
views: &mut Vec<RouteView>,
named: &mut rustc_hash::FxHashMap<String, String>,
routes: &[Route],
prefix: &str,
) {
for route in routes {
let full_path = if prefix == "/" {
route.path.clone()
} else {
format!("{}{}", prefix, route.path)
};
if let Some(view) = route.view {
let idx = views.len();
views.push(view);
trie.add(
&full_path,
idx,
route.name.as_deref(),
route.transition.clone(),
);
if let Some(ref name) = route.name {
named.insert(name.clone(), full_path.clone());
}
}
if !route.children.is_empty() {
register_routes(trie, views, named, &route.children, &full_path);
}
}
}
register_routes(&mut trie, &mut views, &mut named_routes, &self.routes, "/");
if let Some(nf_view) = self.not_found {
let idx = views.len();
views.push(nf_view);
trie.set_not_found(idx);
}
let initial_match = trie.match_path(&self.initial_path);
let router = Router {
inner: Arc::new(Mutex::new(RouterInner {
trie,
views,
guards: self.guards,
history: RouterHistory::new(&self.initial_path),
current_match: initial_match,
named_routes,
route_scopes: rustc_hash::FxHashMap::default(),
active_route_path: None,
})),
};
{
let r = router.clone();
*DEEP_LINK_HANDLER.lock().unwrap() = Some(Box::new(move |uri| {
r.handle_deep_link(uri);
}));
}
router.register_back_handler();
if let Some(uri) = cli_deep_link() {
router.handle_deep_link(&uri);
}
router
}
}
impl Default for RouterBuilder {
fn default() -> Self {
Self::new()
}
}
pub use history::HistoryEntry;
pub use route::{MatchedRoute, QueryParams, Route, RouteContext, RouteParams};
pub use transition::PageTransition;
type DeepLinkFn = Box<dyn Fn(&str) + Send + Sync>;
static DEEP_LINK_HANDLER: std::sync::Mutex<Option<DeepLinkFn>> = std::sync::Mutex::new(None);
pub fn dispatch_deep_link(uri: &str) {
if let Ok(guard) = DEEP_LINK_HANDLER.lock() {
if let Some(ref handler) = *guard {
handler(uri);
}
}
}
pub fn cli_deep_link() -> Option<String> {
std::env::args().find_map(|arg| arg.strip_prefix("--deep-link=").map(|s| s.to_string()))
}
use std::cell::RefCell;
thread_local! {
static ROUTER_STACK: RefCell<Vec<Router>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn push_router_context(router: &Router) {
ROUTER_STACK.with(|stack| {
stack.borrow_mut().push(router.clone());
});
}
pub(crate) fn pop_router_context() {
ROUTER_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
pub fn use_router() -> Router {
ROUTER_STACK.with(|stack| {
stack
.borrow()
.last()
.cloned()
.expect("use_router() called outside of a route outlet. Ensure the component is rendered inside a Router.outlet().")
})
}
pub fn use_params() -> RouteParams {
use_router().params()
}
pub fn use_query() -> QueryParams {
use_router().query()
}