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