GitHub Hot Board

GitHub Hot Board · Arsip repositori

justjavac/gpui-router

A router for GPUI App. (Deskripsi berasal dari repositori itu sendiri.)

Data per October 24justjavac

Peringkat terbaik

#199

Permata yang Terlupakan

100

Total bintang

9

Total fork

1

Hari di papan (30 hari)

Pasang lencana di README kamu · justjavac/gpui-router[![Pasang lencana di README kamu](https://git.vaiox.xyz/api/badge/justjavac/gpui-router.svg)](https://git.vaiox.xyz/id/repo/justjavac/gpui-router)

Dokumentasi proyek

gpui-router

ci Crate Crates.io Total Downloads Documentation License

A router for GPUI App, inspired by React-Router.

Features

  • Nested Routes
  • Index Routes
  • Dynamic Segments
  • Wildcard Routes
  • Navigation Links

Coming from React Router?

The API follows React Router, so most of the knowledge transfers directly:

React Router gpui-router
<Routes> / createBrowserRouter Routes::new().children(vec![...])
<Route path element index> Route::new().path("users/:id").element(|_, _| ...), .index()
<Outlet /> Outlet::new()
path="users/:id" the same, and {id} also works
path="*" the same, and it matches / like React Router
to="about", to="../sibling" the same, resolved against the route that renders the link (relative="path" is available as Relative::Path)
<Link to> Link::new().to(...)
<Link to replace /> Link::new().to(...).replace(true)
<NavLink to> NavLink::new().to(...).active(...).end(...)
useParams(), useLocation() use_params(cx), use_location(cx)
useMatches(), useMatch(pattern) use_matches(cx), use_match(cx, "users/:id")
useNavigate() use_navigate(window, cx), a Navigator with push / replace / back / forward
useNavigationType() use_navigation_type(cx), a NavigationType
navigate(-1), navigate(1) nav.back(), nav.forward()
useSearchParams() use_search_params(cx) to read, use_set_search_params(window, cx) to write
navigate(to, { state }), location.state nav.state([("returnTo", "/x")]).push(to) and use_location(cx).state
<Navigate to replace /> Redirect::to("/login").replace(true)
loader, action, errorElement not planned yet; the plan covers a design pass first

docs/react-router-alignment.md keeps the full register of what is aligned, what is adapted to Rust and GPUI, and what is still open (<Link replace>, useNavigationType, useBlocker, and the data APIs).

Two deliberate differences: Route::layout(...) plus #[derive(IntoLayout)] has no React Router equivalent (it is an optional, explicit alternative to an element with an Outlet), and GPUI has no context lookup, so useOutletContext becomes passing an entity instead.

GPUI versions

gpui-router builds against either GPUI release line, selected by a feature:

Feature GPUI crate Use it for
gpui (default) gpui 0.2.x Applications that depend on GPUI directly
gpui-pre gpui-pre 0.3.x Applications built with gpui-kit
# Plain GPUI application
gpui-router = "0.7"

# gpui-kit application, which re-exports GPUI as `gpui_kit::*`
gpui-router = { version = "0.7", default-features = false, features = ["gpui-pre"] }

Both backends expose the same router API; only the GPUI types behind it change. gpui-kit pins an exact gpui-pre release, so both crates have to resolve to the same 0.3.x version, otherwise Cargo compiles two copies of GPUI and route elements no longer type-check.

Enable exactly one backend: the choice applies to a whole build graph, so the members of one workspace cannot mix backends, and --all-features fails with a compile_error! instead of silently mixing GPUI types.

Feature passthroughs follow the enabled backend through weak dep?/feature references. font-kit, macos-blade and runtime_shaders only exist on the 0.2.x line: on gpui-pre they are no-ops, because font-kit is part of that crate's default features and runtime_shaders belongs to the platform crate that gpui-kit selects for you.

On macOS, GPUI 0.2.x compiles its Metal shaders at build time with xcrun metal. Machines with only the CommandLineTools installed (no full Xcode) need

gpui-router = { version = "0.7", features = ["runtime_shaders"] }

which stitches the shaders for runtime compilation instead.

gpui-kit applications only need gpui-kit and gpui-router: derive macros expand through gpui_router::__private::gpui, so a crate named gpui is not required. The bundled examples bootstrap GPUI directly and therefore build on the default backend only.

The GPUI test harness used by this crate (gpui::test, TestAppContext) needs the backend's test-support feature, which the 0.2.x dev-dependency enables by default. Without it the backend-specific tests are skipped:

cargo test                                                          # default backend, all tests
cargo test --no-default-features --features gpui-pre,test-support   # gpui-kit backend, all tests

Usage

use gpui::prelude::*;
use gpui::{App, Application, Context, Window, WindowOptions, div};
use gpui_router::prelude::*;

struct HelloWorld {}

impl Render for HelloWorld {
  fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
    div()
      .child(
        div()
          .child(NavLink::new().to("/").child(div().child("Home")))
          .child(NavLink::new().to("/about").child(div().child("About")))
          .child(NavLink::new().to("/dashboard").child(div().child("Dashboard")))
          .child(NavLink::new().to("/nothing-here").child(div().child("Not Match"))),
      )
      .child(
        Routes::new()
          .basename("/")
          .child(Route::new().index().element(|_, _| home()))
          .child(Route::new().path("about").element(|_, _| about()))
          .child(Route::new().path("dashboard").element(|_, _| dashboard()))
          .child(Route::new().path("{*not_match}").element(|_, _| not_match())),
      )
  }
}

fn home() -> impl IntoElement {
  div().child("Home")
}

fn about() -> impl IntoElement {
  div().child("About")
}

fn dashboard() -> impl IntoElement {
  div().child("Dashboard")
}

fn not_match() -> impl IntoElement {
  div()
    .child(div().child("Nothing to see here!"))
    .child(NavLink::new().to("/").child(div().child("Go to the home page")))
}

fn main() {
  Application::new().run(|cx: &mut App| {
    init(cx);
    cx.activate(true);
    cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_cx| HelloWorld {}))
      .unwrap();
  });
}

Note: The element() method now accepts a closure that returns an IntoElement. This allows for lazy evaluation of route elements - they are only rendered when the route matches, improving performance when you have many routes.

Note: Router state is per window. Each window that renders a Routes tree gets its own state, created lazily the first time the tree renders, so two windows can show different routes without fighting over a single application-wide state. Call gpui_router::init once during startup to register the state map; rendering without that call panics with the line to add, instead of silently rendering the wrong thing.

Reading the location

Hooks read the router state from any Render implementation or event handler:

Hook Returns
use_location(cx) the current Location: normalized pathname, search, hash, key and state
use_pattern(cx) the route pattern that matched, for example /users/:id
use_params(cx) the dynamic parameters of the current match
use_matches(cx) the matched routes from the root to the leaf, for breadcrumbs
use_match(cx, pattern) whether the pathname matches the current location; query strings are not part of the match
use_search_params(cx) the query string, parsed into key/value pairs
use_set_search_params(window, cx) a setter that navigates with new query parameters
use_navigate(window, cx) a Navigator with push, replace, back and forward
use_navigation_type(cx) how the current location was reached: NavigationType::{Pop, Push, Replace}
use gpui_router::{use_params, use_pattern};

fn user_page(cx: &App) -> impl IntoElement {
  let id = use_params(cx).get("id").cloned().unwrap_or_default();
  let pattern = use_pattern(cx).cloned();

  div().child(format!("{id} matched {pattern:?}"))
}

The hooks read a snapshot of the router state: it is refreshed while the Routes tree renders, so values seen in an event handler that navigates are from the previous frame until the next one renders. use_navigate and use_set_search_params take the window they navigate in so they can refresh it themselves, which is why a click handler no longer needs to call window.refresh() by hand.

Async navigation

An event handler has the &mut Window the interaction happened in, but an async task does not. Navigate from a task by updating the window first, for example with the handle an open_window call returned:

window_handle.update(cx, |_, window, cx| use_navigate(window, cx).push("/about"));

or, when only the location matters and a repaint of every window is acceptable, fall back to App::refresh_windows() after updating the router state directly. The router deliberately does not refresh windows implicitly for async navigation, because it cannot know which window a cross-window navigation belongs to.

Nested routes

Paths use React Router's syntax: users/:id for a dynamic segment and * for a splat. The {id} and {*splat} spellings of the matcher keep working, so existing routes do not have to change. A splat also matches its parent path, the way React Router's * does: path("*") covers / and files/* covers /files with an empty params["*"], while a real route such as an index route still wins that path. Optional segments (docs/:page?) are not supported yet: they panic with the route path instead of compiling to a parameter that would silently never match /docs; use an index route plus a route with the segment.

Give a route children and render an Outlet where the matched child should appear:

use gpui_router::{NavLink, Outlet, Route, Routes};

Routes::new().child(
  Route::new()
    .path("/")
    .element(|_, _| layout())
    .children(vec![
      Route::new().index().element(|_, _| home()),
      Route::new().path("dashboard").element(|_, _| dashboard()),
      Route::new().path("{*not_match}").element(|_, _| not_match()),
    ]),
)

fn layout() -> impl IntoElement {
  div()
    .child(NavLink::new().to("/").child(div().child("Home")))
    .child(NavLink::new().to("/dashboard").child(div().child("Dashboard")))
    .child(Outlet::new())
}

An outlet picks up the matched child while the route's element is built, so it has to be created inside that element closure. Nesting deeper works the same way: give a child route its own .element(...) plus children. Only the first outlet of an element receives the child, and an element that never creates one renders no child content.

This is the React Router shape, and the recommended one. When the shared chrome needs its own type and state, an optional alternative is to implement Layout and pass it with Route::layout(...); #[derive(IntoLayout)] wires the outlet of a struct that has an outlet field. See examples/nested_router.rs for a complete two-level example of that style.

Examples

See the examples folder for more usage examples.

License

This project is licensed under the MIT License. See LICENSE for details.

Peringkat saat ini

  • 199

    Permata yang Terlupakan

    100 bintang · 9 fork · Total Star (menaik)

    Terlupakan →

Grafik 30 hari

Jumlah bintang kumulatif

Pergerakan peringkat

Tetangga peringkat di papan yang sama · Terlupakan

← Kembali ke papan