tessera_ui_shard/router.rs
1//! Stack-based routing utilities for shard components.
2//!
3//! Each `#[shard]` function generates a `*Destination` type that implements
4//! [`RouterDestination`]. These destinations are managed in a LIFO stack.
5//!
6//! # Responsibilities
7//!
8//! * Maintain an ordered stack (`route_stack`) of active destinations
9//! * Expose [`Router::push`] / [`Router::pop`] helpers that also manage shard state lifetimes
10//! * Remove per‑shard state from the registry when a destination whose lifecycle is
11//! `ShardStateLifeCycle::Shard` is popped
12//! * Keep routing logic minimal; rendering happens when the top destination's
13//! `exec_component()` is invoked every frame by `router_root`
14//!
15//! # Related
16//!
17//! * `#[shard]` macro – generates the `*Destination` structs + optional state injection
18//! * `tessera_ui::router::router_root` – executes the current top destination each frame
19use std::sync::OnceLock;
20
21use parking_lot::RwLock;
22
23use crate::{ShardRegistry, ShardStateLifeCycle};
24
25static ROUTER: OnceLock<RwLock<Router>> = OnceLock::new();
26
27pub struct Router {
28 /// Whether the router has been initialized with a default destination
29 initialized: bool,
30 /// Current route stack
31 route_stack: Vec<Box<dyn RouterDestination>>,
32}
33
34impl Router {
35 fn new() -> Self {
36 Self {
37 initialized: false,
38 route_stack: Vec::new(),
39 }
40 }
41
42 /// Initialize the router with a default destination if not already done.
43 pub fn try_init(defualt_dest: impl RouterDestination + 'static) -> bool {
44 Self::with_mut(|router| {
45 if router.initialized {
46 return false;
47 }
48 router.push(defualt_dest);
49 router.initialized = true;
50 true
51 })
52 }
53
54 /// Execute a closure with exclusive mutable access to the router.
55 pub fn with_mut<F, R>(f: F) -> R
56 where
57 F: FnOnce(&mut Self) -> R,
58 {
59 let router = ROUTER.get_or_init(|| RwLock::new(Self::new()));
60 let mut router = router.write();
61 f(&mut router)
62 }
63
64 /// Execute a closure with shared read access to the router.
65 pub fn with<F, R>(f: F) -> R
66 where
67 F: FnOnce(&Self) -> R,
68 {
69 let router = ROUTER.get_or_init(|| RwLock::new(Self::new()));
70 let router = router.read();
71 f(&router)
72 }
73
74 /// Push a new route destination onto the stack (internal helper).
75 pub fn push<T: RouterDestination + 'static>(&mut self, destination: T) {
76 self.route_stack.push(Box::new(destination));
77 }
78
79 /// Pop the top route destination from the stack.
80 ///
81 /// Returns `None` if the stack is empty.
82 pub fn pop(&mut self) -> Option<Box<dyn RouterDestination>> {
83 let dest = self.route_stack.pop()?;
84 // Decide cleanup by destination lifecycle, defaulting to Shard.
85 let life_cycle = dest.life_cycle();
86 if life_cycle == ShardStateLifeCycle::Shard {
87 // Remove per-shard state when destination is discarded
88 ShardRegistry::get().shards.remove(dest.shard_id());
89 }
90 Some(dest)
91 }
92
93 /// Whether the router is empty.
94 pub fn is_empty(&self) -> bool {
95 self.route_stack.is_empty()
96 }
97
98 /// Get the current top route destination, used for route component display.
99 pub fn last(&self) -> Option<&dyn RouterDestination> {
100 self.route_stack.last().map(|v| &**v)
101 }
102
103 /// Get the length of the route stack.
104 pub fn len(&self) -> usize {
105 self.route_stack.len()
106 }
107
108 /// Clear all routes from the stack.
109 pub fn clear(&mut self) {
110 self.route_stack.clear();
111 }
112
113 /// Clear all routes from the stack and reset initialization state.
114 ///
115 /// This allows the root destination to be initialized again.
116 pub fn reset(&mut self) {
117 self.route_stack.clear();
118 self.initialized = false;
119 }
120
121 /// Clear all routes from the stack and push a new root destination.
122 pub fn reset_with(&mut self, root_dest: impl RouterDestination + 'static) {
123 self.route_stack.clear();
124 self.push(root_dest);
125 }
126}
127
128/// A navigation destination produced automatically by the `#[shard]` macro.
129///
130/// You should not manually implement this trait. Each annotated shard function
131/// creates a `*Destination` struct that implements `RouterDestination`.
132pub trait RouterDestination: Send + Sync {
133 /// Execute the component associated with this destination.
134 fn exec_component(&self);
135 /// Stable shard identifier used for state registry lookups / cleanup.
136 fn shard_id(&self) -> &'static str;
137 /// Lifecycle policy for the shard state tied to this destination.
138 ///
139 /// Default is `Shard`, which means the associated shard state will be
140 /// removed from the registry when this destination is popped.
141 /// Override in generated implementations to persist for the whole app.
142 fn life_cycle(&self) -> ShardStateLifeCycle {
143 ShardStateLifeCycle::Shard
144 }
145}