4 releases
| 0.1.3 | Jan 27, 2026 |
|---|---|
| 0.1.2 | Jan 19, 2026 |
| 0.1.1 | Jan 19, 2026 |
| 0.1.0 | Jan 19, 2026 |
#167 in GUI
300KB
5.5K
SLoC
GPUI Navigator
A declarative navigation library for GPUI with smooth transitions, nested routing, and beautiful default error pages.
Features
- 🎨 Smooth Transitions - Fade, slide animations with dual enter/exit support
- 🔀 Nested Routing - Parent/child route hierarchies with
RouterOutlet - 🎯 Simple API - Intuitive route definition with closures
- 🖼️ Beautiful Defaults - Pre-styled 404 and error pages out of the box
- 🔗 RouterLink Widget - Navigation links with active state styling
- 🛡️ Route Guards - Authentication and authorization (optional)
- 🔌 Middleware - Before/after navigation hooks (optional)
- 📝 Named Routes - Navigate by name instead of path
Why GPUI Navigator?
Unlike other GPUI routers:
- Zero Boilerplate - Define routes with simple closures, no complex builders
- Smooth Animations - Dual-animation system for professional transitions
- Production Ready - Beautiful error pages included, not placeholder text
- Developer Experience - Clean API inspired by modern web frameworks
Installation
[dependencies]
gpui-navigator = "0.1"
gpui = "0.2"
Quick Start
use gpui::prelude::*;
use gpui::*;
use gpui_navigator::*;
fn main() {
Application::new().run(|cx: &mut App| {
// Initialize router
init_router(cx, |router| {
// Define routes - simple and ergonomic!
router.add_route(
Route::view("/", || home_page().into_any_element())
.transition(Transition::fade(300))
);
router.add_route(
Route::view("/about", || about_page().into_any_element())
.transition(Transition::slide_left(400))
);
});
// Open window with RouterOutlet
cx.open_window(WindowOptions::default(), |_, cx| {
cx.new(|cx| AppView::new(cx))
}).unwrap();
});
}
fn home_page() -> impl IntoElement {
div().child("Home Page")
}
fn about_page() -> impl IntoElement {
div().child("About Page")
}
struct AppView {
outlet: Entity<RouterOutlet>,
}
impl AppView {
fn new(cx: &mut Context<'_, Self>) -> Self {
Self {
outlet: cx.new(|_| RouterOutlet::new()),
}
}
}
impl Render for AppView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.child(self.outlet.clone())
}
}
Route Builders
GPUI Navigator provides three ergonomic methods for defining routes:
Route::view() - Stateless Pages
For simple pages that don't need state or parameters:
Route::view("/about", || {
div().child("About Page").into_any_element()
})
Route::component() - Stateful Pages
For pages with internal state that persists across navigation. The component is automatically cached using window.use_keyed_state():
use gpui::*;
use gpui_navigator::*;
struct CounterPage {
count: i32,
}
impl CounterPage {
fn new() -> Self {
Self { count: 0 }
}
}
impl Render for CounterPage {
fn render(&mut self, _: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
div()
.child(format!("Count: {}", self.count))
.child(
div()
.on_mouse_down(MouseButton::Left, cx.listener(|page, _, _, cx| {
page.count += 1;
cx.notify();
}))
.child("Increment")
)
}
}
// Route definition - component state persists across navigation!
Route::component("/counter", CounterPage::new)
Benefits:
- ✅ State persists when navigating away and back
- ✅ Automatic Entity caching
- ✅ Clean, concise API
Route::component_with_params() - Stateful Pages with Route Params
For pages that need route parameters and maintain state:
struct UserPage {
user_id: String,
}
impl UserPage {
fn new(user_id: String) -> Self {
Self { user_id }
}
}
impl Render for UserPage {
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
div().child(format!("User: {}", self.user_id))
}
}
// Each unique user_id gets its own cached component instance
Route::component_with_params("/user/:id", |params| {
let id = params.get("id").unwrap().to_string();
UserPage::new(id)
})
Route::new() - Full Control
For advanced use cases when you need full control over the builder function:
Route::new("/advanced", |window, cx, params| {
// Full access to Window, App context, and route params
custom_page(window, cx, params).into_any_element()
})
Navigation
Programmatic Navigation
use gpui_navigator::Navigator;
// Push new route
Navigator::push(cx, "/about");
// Replace current route
Navigator::replace(cx, "/login");
// Go back
Navigator::pop(cx);
// Go forward
Navigator::forward(cx);
// Get current path
let path = Navigator::current_path(cx);
// Check if can go back
if Navigator::can_pop(cx) {
Navigator::pop(cx);
}
RouterLink Widget
Create clickable navigation links with automatic active state:
use gpui_navigator::*;
fn navbar(cx: &mut Context<'_, AppView>) -> impl IntoElement {
div()
.flex()
.gap_4()
// Basic link
.child(
RouterLink::new("/".to_string())
.child(div().child("Home"))
.build(cx)
)
// Link with active styling
.child(
RouterLink::new("/about".to_string())
.child(div().px_4().py_2().child("About"))
.active_class(|div| {
div.bg(rgb(0x2196f3))
.text_color(white())
})
.build(cx)
)
}
RouterLink features:
- ✅ Instant navigation with immediate UI updates
- ✅ Automatic active state detection
- ✅ Customizable active styling
- ✅ Works with nested routes
Route Transitions
Add smooth animations between pages:
use gpui_navigator::*;
// Fade transition
Route::new("/fade", |_, _| page().into_any_element())
.transition(Transition::fade(300))
// Slide transitions
Route::new("/slide-left", |_, _| page().into_any_element())
.transition(Transition::slide_left(400))
Route::new("/slide-right", |_, _| page().into_any_element())
.transition(Transition::slide_right(400))
Route::new("/slide-up", |_, _| page().into_any_element())
.transition(Transition::slide_up(400))
Route::new("/slide-down", |_, _| page().into_any_element())
.transition(Transition::slide_down(400))
// No transition
Route::new("/instant", |_, _| page().into_any_element())
.transition(Transition::None)
Dual Animation System: GPUI Navigator uses the new route's transition for both exit and enter animations, creating smooth, professional transitions.
Route Parameters
Extract dynamic values from URLs:
use gpui_navigator::*;
// Define route with parameter
router.add_route(
Route::new("/users/:id", |_, params| {
user_page(params).into_any_element()
})
);
fn user_page(params: &RouteParams) -> impl IntoElement {
let user_id = params.get("id").unwrap_or(&"unknown".to_string());
div().child(format!("User: {}", user_id))
}
// Navigate with parameter
Navigator::push(cx, "/users/123");
Nested Routes
Create layouts with child routes:
use gpui_navigator::*;
router.add_route(
Route::new("/dashboard", |_, _| dashboard_layout().into_any_element())
.children(vec![
Route::new("overview", |_, _| overview_page().into_any_element()).into(),
Route::new("settings", |_, _| settings_page().into_any_element()).into(),
])
);
fn dashboard_layout() -> impl IntoElement {
div()
.flex()
.flex_col()
.child("Dashboard Header")
.child(RouterOutlet::new()) // Child routes render here
}
Access nested routes:
/dashboard- Shows dashboard layout/dashboard/overview- Shows overview inside layout/dashboard/settings- Shows settings inside layout
Error Handling
Default Error Pages
GPUI Navigator includes beautiful, pre-styled error pages:
- 404 Page - Shown when no route matches (styled with red badge)
- Loading Page - Optional loading state
- Error Page - Generic error display
These work automatically - no configuration needed!
Custom Error Pages
Override defaults if desired:
use gpui_navigator::*;
let default_pages = DefaultPages::new()
.with_not_found(|| {
div()
.child("Custom 404")
.child("Page not found")
.into_any_element()
});
Named Routes
Navigate by name instead of hardcoded paths:
// Define named route
router.add_route(
Route::new("/users/:id", |_, params| user_page(params).into_any_element())
.name("user-profile")
);
// Navigate by name
let mut params = RouteParams::new();
params.set("id".to_string(), "123".to_string());
Navigator::push_named(cx, "user-profile", ¶ms);
Optional Features
Enable advanced features in Cargo.toml:
[dependencies]
gpui-navigator = { version = "0.1", features = ["guard", "middleware", "cache"] }
Route Guards
Protect routes with authentication:
#[cfg(feature = "guard")]
use gpui_navigator::*;
fn is_logged_in(cx: &App) -> bool {
// Check auth state
true
}
Route::new("/profile", |_, _| profile_page().into_any_element())
.guard(AuthGuard::new(is_logged_in, "/login"))
Middleware
Add hooks before/after navigation:
#[cfg(feature = "middleware")]
use gpui_navigator::*;
struct LoggingMiddleware;
impl RouteMiddleware for LoggingMiddleware {
// Implement before_navigation and after_navigation
}
Route::new("/", |_, _| home().into_any_element())
.middleware(LoggingMiddleware)
Examples
Run the included examples:
# Transition animations demo
cargo run --example transition_demo
# RouterLink and error handling demo
cargo run --example error_demo
API Summary
| Function/Type | Description |
|---|---|
init_router(cx, |router| {...}) |
Initialize the router with routes |
Route::new(path, handler) |
Create a new route |
.transition(Transition::fade(ms)) |
Add transition animation |
.name("route-name") |
Name the route for reference |
.children(vec![...]) |
Add child routes |
Navigator::push(cx, path) |
Navigate to path |
Navigator::pop(cx) |
Go back |
RouterOutlet::new() |
Render current/child routes |
RouterLink::new(path) |
Create navigation link |
RouteParams::get("key") |
Get route parameter |
Architecture
GPUI Navigator is built with a clean, modular architecture:
- Core: Route matching, state management, navigation
- Widgets: RouterOutlet (route renderer), RouterLink (nav links)
- Optional: Guards, middleware, caching (feature-gated)
- Defaults: Beautiful error pages included
Minimum Supported Rust Version
Rust 1.75 or later.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Contributing
Contributions welcome! Please:
- Fork the repository
- Create your feature branch
- Add tests for new features
- Ensure all tests pass:
cargo test - Submit a Pull Request
Acknowledgments
- Built for GPUI by Zed Industries
- Inspired by modern web routing libraries
Dependencies
~0–47MB
~693K SLoC