Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pages, Links, and Layouts

Beverly keeps route architecture explicit while letting each page and layout remain an ordinary composable UI tree.

  • A Page names a route and its content.
  • A Layout describes the reusable shell around a page.
  • A Link points to a route pattern.

Register Pages

Return pages from named functions and register them together on the root Ui:

#![allow(unused)]
fn main() {
use beverly::prelude::*;

fn application_ui() -> Ui {
    ui().pages([
        home_page(),
        users_page(),
        user_page(),
    ])
}

fn home_page() -> Page {
    page()
        .route("/")
        .layout(app_layout())
        .children([text("Home")])
}

fn users_page() -> Page {
    page()
        .route("/users")
        .layout(app_layout())
        .children([text("User list")])
}

fn user_page() -> Page {
    page()
        .route("/users/:id")
        .layout(app_layout())
        .children([text("User profile")])
}
}

The first registered route matching / is shown at startup. If no root route exists, Beverly starts with the first registered page. Route patterns match path segments exactly; a segment beginning with : captures one value.

Compose a Layout

Layouts use the same composable elements as pages. page_outlet() marks the location where the active page’s content is rendered:

#![allow(unused)]
fn main() {
fn app_layout() -> Layout {
    layout().children([
        text("Application header"),
        row()
            .gap(Val::Px(16.0))
            .children([
                text("Sidebar"),
                page_outlet(),
            ]),
        text("Application footer"),
    ])
}
}

A page without .layout(...) renders directly in the route surface. Pages using the same layout share its shell; changing the layout does not change their routes or content.

The visible string passed to link() is its label. Accessibility description and destination are separate:

#![allow(unused)]
fn main() {
fn user_link(user_id: u64) -> UiElement {
    link("View user")
        .aria("Opens this user's profile")
        .to("/users/:id")
        .params([("id", user_id)])
}
}

When activated, a link with a target navigates only if the resolved path matches a registered page. A link without .to(...) still emits LinkClicked and can be handled by application code. Disabled links do not activate.

Query strings can be included in the target and are retained when path parameters are substituted:

#![allow(unused)]
fn main() {
link("View activity")
    .aria("Opens this user's activity")
    .to("/users/:id?tab=activity&search=red+fox")
    .params([("id", user_id)])
}

The current path, path parameters, and parsed query are available globally through Res<RouteState>. Beverly also attaches a RouteContext component to every entity in the active page subtree, including layout elements, page content, and descendants created later. Component systems can read it with Query<&RouteContext>; custom context-aware builders can read the same snapshot through UiBuildContext::route_context() while constructing their entity.

#![allow(unused)]
fn main() {
fn inspect_route(route: Res<RouteState>) {
    info!("path: {}, id: {:?}, tab: {:?}", route.path, route.params.get("id"), route.query.get("tab"));
}

fn inspect_routed_widget(contexts: Query<&RouteContext>) {
    for context in &contexts {
        info!("id: {:?}, tab: {:?}", context.param("id"), context.query_value("tab"));
    }
}
}

RouteContext exposes path, params, and query maps, with param(name) and query_value(name) convenience accessors. Query keys and values are form-decoded (+ becomes a space); repeated query keys retain the last value. The same snapshot can be read during fluent construction:

#![allow(unused)]
fn main() {
fn user_header(context: &mut UiBuildContext<'_>) -> Entity {
    let user_id = context
        .route_context()
        .and_then(|route| route.param("id").map(str::to_owned))
        .unwrap_or_default();
    context.world.spawn((Text::new(format!("User {user_id}")), ChildOf(context.parent))).id()
}
}

Routing is internal to the Bevy application and does not synchronize a browser URL. Applications that need custom state transitions can continue to use NavButton and handle its interaction in their own systems.