tessera_ui/
router.rs

1//! Controller-driven shard routing primitives.
2//!
3//! ## Usage
4//!
5//! Mount `shard_home` at the app shell root, then render `router_outlet` where
6//! the current shard page should appear.
7
8use std::sync::Arc;
9
10use tessera_macros::tessera;
11
12pub use tessera_shard::{
13    ShardState, ShardStateLifeCycle,
14    router::{RouterController, RouterDestination},
15};
16
17use crate::{
18    RenderSlot, State,
19    context::{context_from_previous_snapshot_for_instance, provide_context, use_context},
20    runtime::{RuntimePhase, current_component_instance_key_from_scope, current_phase, remember},
21};
22
23#[derive(Clone)]
24struct RouterContext {
25    controller: State<RouterController>,
26}
27
28/// Shared destination handle used by `RouterController` builders and
29/// `shard_home`.
30pub struct RouterDestinationHandle {
31    inner: Arc<dyn RouterDestination>,
32}
33
34impl RouterDestinationHandle {
35    fn clone_destination(&self) -> Arc<dyn RouterDestination> {
36        Arc::clone(&self.inner)
37    }
38}
39
40impl<T> From<T> for RouterDestinationHandle
41where
42    T: RouterDestination + 'static,
43{
44    fn from(value: T) -> Self {
45        Self {
46            inner: Arc::new(value),
47        }
48    }
49}
50
51impl Clone for RouterDestinationHandle {
52    fn clone(&self) -> Self {
53        Self {
54            inner: Arc::clone(&self.inner),
55        }
56    }
57}
58
59impl PartialEq for RouterDestinationHandle {
60    fn eq(&self, other: &Self) -> bool {
61        Arc::ptr_eq(&self.inner, &other.inner)
62    }
63}
64
65impl Eq for RouterDestinationHandle {}
66
67fn resolve_router_controller_state() -> State<RouterController> {
68    match current_phase() {
69        Some(RuntimePhase::Build) => {
70            let context = use_context::<RouterContext>()
71                .expect("Router is missing in build scope. Mount UI inside shard_home.");
72            context.get().controller
73        }
74        Some(RuntimePhase::Input) => {
75            let instance_key = current_component_instance_key_from_scope()
76                .expect("Router command requires an active component scope during input handling");
77            let context = context_from_previous_snapshot_for_instance::<RouterContext>(
78                instance_key,
79            )
80            .expect("Router is missing in input scope. Ensure callbacks run inside shard_home.");
81            context.get().controller
82        }
83        _ => {
84            panic!("Router access must happen during build or input phase");
85        }
86    }
87}
88
89pub(crate) fn current_router_controller() -> State<RouterController> {
90    resolve_router_controller_state()
91}
92
93pub(crate) fn with_current_router_shard_state<T, F, R>(
94    shard_id: &str,
95    life_cycle: ShardStateLifeCycle,
96    f: F,
97) -> R
98where
99    T: Default + Send + Sync + 'static,
100    F: FnOnce(ShardState<T>) -> R,
101{
102    let controller = current_router_controller();
103    controller.with(|router| router.init_or_get_with_lifecycle(shard_id, life_cycle, f))
104}
105
106/// Render the current destination from the nearest shard home.
107pub fn router_outlet() {
108    let executed = current_router_controller().with(RouterController::exec_current);
109    assert!(executed, "Router stack should not be empty");
110}
111
112/// # shard_home
113///
114/// Provide a router controller and render shard UI rooted at the active
115/// destination.
116///
117/// ## Usage
118///
119/// Mount the root route for an app shell and optionally render custom chrome
120/// around `router_outlet()`.
121///
122/// ## Parameters
123///
124/// - `root` — initial destination used when `controller` is omitted
125/// - `controller` — optional external router controller state
126/// - `child` — optional shell content; defaults to `router_outlet()`
127///
128/// ## Examples
129///
130/// ```rust
131/// use tessera_ui::router::shard_home;
132///
133/// # #[derive(Clone)]
134/// # struct DemoDestination;
135/// # impl tessera_ui::router::RouterDestination for DemoDestination {
136/// #     fn exec_component(&self) {}
137/// #     fn shard_id(&self) -> &'static str { "demo" }
138/// # }
139/// # #[tessera_ui::tessera]
140/// # fn demo() {
141/// shard_home().root(DemoDestination);
142/// # }
143/// # demo();
144/// ```
145#[tessera(crate)]
146pub fn shard_home(
147    #[prop(into)] root: Option<RouterDestinationHandle>,
148    controller: Option<State<RouterController>>,
149    child: Option<RenderSlot>,
150) {
151    let internal_controller = remember({
152        let root = root.clone();
153        move || match root.clone() {
154            Some(root) => RouterController::with_root_shared(root.clone_destination()),
155            None => RouterController::new(),
156        }
157    });
158    let controller = controller.unwrap_or(internal_controller);
159
160    if root.is_none()
161        && controller == internal_controller
162        && controller.with(RouterController::is_empty)
163    {
164        panic!("shard_home requires `root` when `controller` is not provided");
165    }
166
167    provide_context(
168        || RouterContext { controller },
169        move || {
170            if let Some(child) = child {
171                child.render();
172            } else {
173                router_outlet();
174            }
175        },
176    );
177}