webtau/
lib.rs

1/// Re-export the `#[command]` proc macro so users write `#[webtau::command]`.
2pub use webtau_macros::command;
3
4/// Generates thread-local state management boilerplate for WASM targets.
5///
6/// When building a Tauri game for the web, you need to replace
7/// `State<Mutex<T>>` (which requires threads) with thread-local storage
8/// (WASM is single-threaded). This macro generates the required
9/// `thread_local!` + `RefCell` pattern and accessor functions.
10///
11/// # Usage
12///
13/// ```rust
14/// use webtau::wasm_state;
15///
16/// struct GameWorld {
17///     score: i32,
18/// }
19///
20/// wasm_state!(GameWorld);
21/// ```
22///
23/// # Generated API
24///
25/// - `set_state(val: T)` — Initialize or replace the state.
26/// - `with_state(|state| ...)` — Read-only access to the state.
27/// - `with_state_mut(|state| ...)` — Mutable access to the state.
28///
29/// All three functions panic if called before `set_state()`.
30#[macro_export]
31macro_rules! wasm_state {
32    ($T:ty) => {
33        ::std::thread_local! {
34            static __WEBTAU_STATE: ::std::cell::RefCell<Option<$T>> =
35                ::std::cell::RefCell::new(None);
36        }
37
38        /// Initialize or replace the global game state.
39        fn set_state(val: $T) {
40            __WEBTAU_STATE.with(|cell| {
41                *cell.borrow_mut() = Some(val);
42            });
43        }
44
45        /// Read-only access to the game state.
46        ///
47        /// # Panics
48        /// Panics if `set_state()` has not been called.
49        fn with_state<__F, __R>(f: __F) -> __R
50        where
51            __F: FnOnce(&$T) -> __R,
52        {
53            __WEBTAU_STATE.with(|cell| {
54                let borrow = cell.borrow();
55                let state = borrow
56                    .as_ref()
57                    .expect("webtau: state not initialized — call set_state() first");
58                f(state)
59            })
60        }
61
62        /// Mutable access to the game state.
63        ///
64        /// # Panics
65        /// Panics if `set_state()` has not been called.
66        fn with_state_mut<__F, __R>(f: __F) -> __R
67        where
68            __F: FnOnce(&mut $T) -> __R,
69        {
70            __WEBTAU_STATE.with(|cell| {
71                let mut borrow = cell.borrow_mut();
72                let state = borrow
73                    .as_mut()
74                    .expect("webtau: state not initialized — call set_state() first");
75                f(state)
76            })
77        }
78
79        /// Read-only access to the game state. Returns `None` if not initialized.
80        fn try_with_state<__F, __R>(f: __F) -> Option<__R>
81        where
82            __F: FnOnce(&$T) -> __R,
83        {
84            __WEBTAU_STATE.with(|cell| {
85                let borrow = cell.borrow();
86                borrow.as_ref().map(f)
87            })
88        }
89
90        /// Mutable access to the game state. Returns `None` if not initialized.
91        fn try_with_state_mut<__F, __R>(f: __F) -> Option<__R>
92        where
93            __F: FnOnce(&mut $T) -> __R,
94        {
95            __WEBTAU_STATE.with(|cell| {
96                let mut borrow = cell.borrow_mut();
97                borrow.as_mut().map(f)
98            })
99        }
100    };
101}
102
103#[cfg(test)]
104mod tests {
105    #[derive(Debug, PartialEq)]
106    struct Counter {
107        value: i32,
108    }
109
110    wasm_state!(Counter);
111
112    #[test]
113    fn set_and_read_state() {
114        set_state(Counter { value: 42 });
115        let result = with_state(|c| c.value);
116        assert_eq!(result, 42);
117    }
118
119    #[test]
120    fn mutate_state() {
121        set_state(Counter { value: 0 });
122        with_state_mut(|c| c.value += 10);
123        let result = with_state(|c| c.value);
124        assert_eq!(result, 10);
125    }
126
127    #[test]
128    #[should_panic(expected = "state not initialized")]
129    fn panics_without_init() {
130        // Each test thread gets its own thread-local, so this is fresh.
131        // We need a separate type to avoid interference from other tests.
132        struct Uninitialized;
133        wasm_state!(Uninitialized);
134        with_state(|_| {});
135    }
136
137    #[test]
138    fn try_with_state_returns_none_before_init() {
139        struct Sentinel;
140        wasm_state!(Sentinel);
141        let result = try_with_state(|_| 99u32);
142        assert_eq!(result, None);
143    }
144
145    #[test]
146    fn try_with_state_returns_some_after_init() {
147        struct Value {
148            n: i32,
149        }
150        wasm_state!(Value);
151        set_state(Value { n: 7 });
152        let result = try_with_state(|v| v.n);
153        assert_eq!(result, Some(7));
154    }
155
156    #[test]
157    fn try_with_state_mut_returns_none_before_init() {
158        struct Tracker;
159        wasm_state!(Tracker);
160        let result = try_with_state_mut(|_| 42u32);
161        assert_eq!(result, None);
162    }
163}