euv_core/reactive/hook/impl.rs
1use super::*;
2
3/// Implementation of hook context lifecycle and hook index management.
4impl HookContext {
5 /// Resets the hook index for a new render cycle.
6 ///
7 /// Sets the internal hook index back to zero so that subsequent
8 /// `use_signal` calls start indexing from the beginning of the hook list.
9 pub fn reset_index(&mut self) {
10 if let Ok(mut inner) = self.get_inner().try_borrow_mut() {
11 inner.set_hook_index(0);
12 }
13 }
14
15 /// Notifies the hook context that a match arm is being entered.
16 ///
17 /// If the arm index has changed, all existing hooks and cleanups
18 /// are cleared and re-initialized for the new arm. If the arm
19 /// is unchanged, only the hook index is reset.
20 ///
21 /// # Arguments
22 ///
23 /// - `usize` - The index of the new match arm.
24 pub fn switch_arm(&mut self, changed: usize) {
25 let cleanups: Vec<Box<dyn FnOnce()>>;
26 {
27 let Ok(mut inner) = self.get_inner().try_borrow_mut() else {
28 return;
29 };
30 if inner.get_arm_changed() == changed {
31 drop(inner);
32 self.reset_index();
33 return;
34 }
35 cleanups = take(inner.get_mut_cleanups());
36 inner.get_mut_hooks().clear();
37 inner.set_arm_changed(changed);
38 }
39 for cleanup in cleanups {
40 cleanup();
41 }
42 self.reset_index();
43 }
44
45 /// Creates or reuses a `NodeRef<T>` at the current hook index.
46 ///
47 /// On the first call at a given hook index, a fresh empty `NodeRef`
48 /// is stored. On subsequent re-renders the same instance is returned,
49 /// so a ref cloned into a closure stays attached to the live DOM
50 /// element across renders.
51 ///
52 /// The element type `T` is a phantom marker only — we downcast the
53 /// stored `Box<dyn Any>` back to `NodeRef<T>` using the same pattern
54 /// as `Signal::signal` above. Note that two calls at the same hook
55 /// index with different `T` would still match (both are `NodeRef<...>`)
56 /// because the `downcast_ref` ignores the phantom parameter.
57 ///
58 /// # Returns
59 ///
60 /// - `NodeRef<T>` - A `NodeRef<T>` value.
61 pub fn noderef<T>() -> NodeRef<T>
62 where
63 T: ?Sized + 'static,
64 {
65 let hook_context: HookContext = Self::current();
66 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
67 // Borrow failed (renderer re-entered); fall back to a fresh
68 // empty ref so the caller still gets a usable handle.
69 return NodeRef::new();
70 };
71 let index: usize = inner.get_hook_index();
72 inner.set_hook_index(index + 1);
73 if index < inner.get_hooks().len() {
74 // Re-render path: try to reuse the existing NodeRef stored at
75 // this hook index. If a different hook type was at this slot
76 // (e.g. user swapped `use_signal` for `use_node_ref`), replace
77 // it with a fresh ref rather than panicking.
78 if let Some(existing) = inner.get_hooks()[index].downcast_ref::<NodeRef<T>>() {
79 return existing.clone();
80 }
81 let new_ref: NodeRef<T> = NodeRef::new();
82 inner.get_mut_hooks()[index] = Box::new(new_ref.clone());
83 return new_ref;
84 }
85 let new_ref: NodeRef<T> = NodeRef::new();
86 inner.get_mut_hooks().push(Box::new(new_ref.clone()));
87 new_ref
88 }
89}
90
91/// Clones the hook context, sharing the same inner state.
92///
93/// All clones share the same underlying `Rc<RefCell<HookContextInner>>`,
94/// so modifications through one clone are visible through all others.
95///
96/// # Returns
97///
98/// - `Self` - A new `HookContext` sharing the same inner state.
99impl Clone for HookContext {
100 /// Clones the [`HookContext`] by reusing shared, cheap-to-clone state where possible.
101 fn clone(&self) -> Self {
102 Self::new(self.get_inner().clone())
103 }
104}
105
106/// Provides a default empty hook context.
107///
108/// Creates a fresh `Rc<RefCell<HookContextInner>>` with default values
109/// (empty hook list, zero hook index, empty cleanup list).
110///
111/// # Returns
112///
113/// - `Self` - A new `HookContext` with default inner state.
114impl Default for HookContext {
115 /// Constructs a default [`HookContext`] value.
116 fn default() -> Self {
117 Self::new(Rc::new(RefCell::new(HookContextInner::default())))
118 }
119}
120
121/// Implementation of interval handle lifecycle management.
122impl IntervalHandle {
123 /// Cancels the associated browser interval timer.
124 ///
125 /// Calls `window.clearInterval` with the stored interval ID.
126 /// After calling this method the interval callback will no longer fire.
127 ///
128 /// # Panics
129 ///
130 /// Panics if `window()` is unavailable on the current platform.
131 pub fn clear(&self) {
132 if let Some(cleanup_window) = web_sys::window() {
133 cleanup_window.clear_interval_with_handle(self.get_interval_id());
134 }
135 }
136}
137
138/// Associated functions for hook context management.
139///
140/// These are crate-internal static methods for managing the active hook
141/// context, creating signals, registering cleanups, and scheduling intervals.
142impl HookContext {
143 /// Returns a shared reference to the current hook context global state.
144 ///
145 /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
146 fn try_get_current() -> &'static Option<HookContextRc> {
147 unsafe { &*(*std::ptr::addr_of!(CURRENT_HOOK_CONTEXT)).get_0().get() }
148 }
149
150 /// Returns a mutable reference to the current hook context global state.
151 ///
152 /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
153 fn try_get_mut_current() -> &'static mut Option<HookContextRc> {
154 unsafe {
155 &mut *(*std::ptr::addr_of_mut!(CURRENT_HOOK_CONTEXT))
156 .get_0()
157 .get()
158 }
159 }
160
161 /// Returns the currently active `HookContext`.
162 ///
163 /// If no hook context has been set, creates and stores a default one
164 /// in the global `CURRENT_HOOK_CONTEXT` cell so subsequent calls
165 /// return the same instance.
166 ///
167 /// # Returns
168 ///
169 /// - `HookContext` - The currently active hook context.
170 pub fn current() -> HookContext {
171 match Self::try_get_current() {
172 Some(hook_context_rc) => HookContext::new(hook_context_rc.clone()),
173 None => {
174 let rc: HookContextRc = Rc::new(RefCell::new(HookContextInner::default()));
175 *Self::try_get_mut_current() = Some(rc.clone());
176 HookContext::new(rc)
177 }
178 }
179 }
180
181 /// Runs a closure with the given `HookContext` set as the active context.
182 ///
183 /// Saves the previous context, sets the new one, executes the closure,
184 /// and restores the previous context afterward.
185 ///
186 /// # Arguments
187 ///
188 /// - `HookContext` - The hook context to set as active during closure execution.
189 /// - `F: FnOnce() -> R` - The closure to execute with the given context.
190 ///
191 /// # Returns
192 ///
193 /// - `R` - The result of the closure execution.
194 pub fn with<F, R>(context: HookContext, callback: F) -> R
195 where
196 F: FnOnce() -> R,
197 {
198 let previous: Option<HookContextRc> = Self::try_get_mut_current().take();
199 *Self::try_get_mut_current() = Some(context.get_inner().clone());
200 let result: R = callback();
201 *Self::try_get_mut_current() = previous;
202 result
203 }
204
205 /// Creates a new reactive signal with the given initial value.
206 ///
207 /// Uses the current `HookContext` to maintain signal identity across
208 /// re-renders. On the first call at a given hook index, the signal
209 /// is created with `init()` and stored. On subsequent re-renders,
210 /// the existing signal at that index is returned unchanged.
211 ///
212 /// # Arguments
213 ///
214 /// - `FnOnce() -> T` - A closure that computes the initial value of the signal.
215 ///
216 /// # Returns
217 ///
218 /// - `Signal<T>` - A reactive signal containing the initialized or existing value.
219 pub fn signal<T, F>(init: F) -> Signal<T>
220 where
221 T: Clone + PartialEq + 'static,
222 F: FnOnce() -> T,
223 {
224 let hook_context: HookContext = Self::current();
225 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
226 return Signal::create(init());
227 };
228 let index: usize = inner.get_hook_index();
229 inner.set_hook_index(index + 1);
230 if index < inner.get_hooks().len()
231 && let Some(existing) = inner.get_hooks()[index].downcast_ref::<Signal<T>>()
232 {
233 return *existing;
234 }
235 let signal: Signal<T> = Signal::create(init());
236 inner
237 .get_mut_cleanups()
238 .push(Box::new(move || signal.deactivate()));
239 if index < inner.get_hooks().len() {
240 inner.get_mut_hooks()[index] = Box::new(signal);
241 } else {
242 inner.get_mut_hooks().push(Box::new(signal));
243 }
244 signal
245 }
246
247 /// Registers a cleanup callback that will be executed when the current
248 /// hook context is cleared (e.g., when a `match` arm switches).
249 ///
250 /// This is useful for cleaning up side effects like intervals, timeouts,
251 /// or subscriptions that are not automatically managed by signals.
252 ///
253 /// The cleanup callback is only registered once on the first render.
254 /// On subsequent re-renders at the same hook index, this is a no-op.
255 ///
256 /// # Arguments
257 ///
258 /// - `FnOnce() + 'static` - The cleanup callback to execute on context teardown.
259 pub fn cleanup<F>(cleanup: F)
260 where
261 F: FnOnce() + 'static,
262 {
263 let hook_context: HookContext = Self::current();
264 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
265 return;
266 };
267 let index: usize = inner.get_hook_index();
268 inner.set_hook_index(index + 1);
269 if index < inner.get_hooks().len() {
270 return;
271 }
272 inner.get_mut_cleanups().push(Box::new(cleanup));
273 inner.get_mut_hooks().push(Box::new(()));
274 }
275
276 /// Registers a `window.addEventListener` callback using event delegation,
277 /// automatically removed when the hook context is cleared.
278 ///
279 /// Uses the global window event proxy registry so that only one
280 /// `window.addEventListener` call is made per event name regardless of
281 /// how many components listen to the same event. On cleanup, only the
282 /// handler entry is removed from the proxy registry; the shared window
283 /// listener remains active for other consumers.
284 ///
285 /// The event listener is only registered once on the first render.
286 /// On subsequent re-renders at the same hook index, this is a no-op.
287 ///
288 /// # Arguments
289 ///
290 /// - `E: AsRef<str>` - The event name to listen for (e.g., "hashchange", "popstate", "resize").
291 /// - `FnMut() + 'static` - The callback to invoke when the event fires.
292 pub fn window_event<E, F>(event_name: E, callback: F)
293 where
294 E: AsRef<str>,
295 F: FnMut() + 'static,
296 {
297 let event_name: &str = event_name.as_ref();
298 let hook_context: HookContext = Self::current();
299 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
300 return;
301 };
302 let index: usize = inner.get_hook_index();
303 inner.set_hook_index(index + 1);
304 if index < inner.get_hooks().len() {
305 return;
306 }
307 let event_name_owned: String = event_name.to_owned();
308 let handler_id: usize = Registry::register_window_event(event_name, callback);
309 inner.get_mut_cleanups().push(Box::new(move || {
310 Registry::unregister_window_event(&event_name_owned, handler_id);
311 }));
312 inner.get_mut_hooks().push(Box::new(()));
313 }
314
315 /// Creates a recurring interval that invokes the given closure at the
316 /// specified period, returning an `IntervalHandle` that is automatically
317 /// cleared when the hook context is cleared (i.e., when the component
318 /// unmounts or a `match` arm switches).
319 ///
320 /// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
321 /// + `Closure::forget()` manually, this hook ensures the interval is
322 /// properly cleaned up, preventing memory leaks and stale callbacks.
323 ///
324 /// The interval is only created once on the first render.
325 /// On subsequent re-renders at the same hook index, the existing handle
326 /// is returned unchanged.
327 ///
328 /// # Arguments
329 ///
330 /// - `i32` - The interval period in milliseconds.
331 /// - `FnMut() + 'static` - The closure to invoke on each interval tick.
332 ///
333 /// # Returns
334 ///
335 /// - `IntervalHandle` - A handle that can be used to cancel the interval early.
336 ///
337 /// # Panics
338 ///
339 /// Panics if `window()` is unavailable on the current platform.
340 pub fn interval<F>(millis: i32, callback: F) -> IntervalHandle
341 where
342 F: FnMut() + 'static,
343 {
344 let hook_context: HookContext = Self::current();
345 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
346 return IntervalHandle::new(0);
347 };
348 let index: usize = inner.get_hook_index();
349 inner.set_hook_index(index + 1);
350 if index < inner.get_hooks().len()
351 && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
352 {
353 return *existing;
354 }
355 let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
356 let Some(window) = window() else {
357 closure.forget();
358 return IntervalHandle::new(0);
359 };
360 let Ok(interval_id) = window.set_interval_with_callback_and_timeout_and_arguments_0(
361 closure.as_ref().unchecked_ref(),
362 millis,
363 ) else {
364 closure.forget();
365 return IntervalHandle::new(0);
366 };
367 closure.forget();
368 let handle: IntervalHandle = IntervalHandle::new(interval_id);
369 inner.get_mut_cleanups().push(Box::new(move || {
370 let Some(cleanup_window) = web_sys::window() else {
371 return;
372 };
373 cleanup_window.clear_interval_with_handle(interval_id);
374 }));
375 if index < inner.get_hooks().len() {
376 inner.get_mut_hooks()[index] = Box::new(handle);
377 } else {
378 inner.get_mut_hooks().push(Box::new(handle));
379 }
380 handle
381 }
382}
383
384/// Inherent implementation of [`HookContext`].
385impl HookContext {
386 /// Registers a hook value with the current hook context and returns
387 /// the existing instance if one was stored at this index from a
388 /// previous render cycle.
389 ///
390 /// This is the public extension point for custom hook types.
391 /// `ui` and downstream crates implement `use_form`, `use_i18n`, etc.
392 /// on top of this primitive instead of poking at the hook array
393 /// directly. `factory` runs once per hook slot on the first render;
394 /// subsequent renders in the same arm return the previously stored
395 /// instance.
396 ///
397 /// # Arguments
398 ///
399 /// - `F: FnOnce() -> T` - Constructor that produces a fresh value
400 /// of type `T` when the slot has never been written.
401 ///
402 /// # Returns
403 ///
404 /// - `T: Clone + 'static` - Either the previously-stored
405 /// value (cheap clone / copy) or a fresh one from
406 /// `factory`.
407 pub fn use_hook<T, F>(factory: F) -> T
408 where
409 F: FnOnce() -> T,
410 T: Clone + 'static,
411 {
412 let hook_context: HookContext = Self::current();
413 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
414 return factory();
415 };
416 let index: usize = inner.get_hook_index();
417 inner.set_hook_index(index + 1);
418 if index < inner.get_hooks().len()
419 && let Some(existing) = inner.get_hooks()[index].downcast_ref::<T>()
420 {
421 return existing.clone();
422 }
423 let state: T = factory();
424 if index < inner.get_hooks().len() {
425 inner.get_mut_hooks()[index] = Box::new(state.clone());
426 } else {
427 inner.get_mut_hooks().push(Box::new(state.clone()));
428 }
429 state
430 }
431}