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}