Skip to main content

rustolio_web/hooks/signal/
mod.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11mod store;
12
13use super::{Dependency, Scope};
14
15pub(super) use store::SignalStore;
16
17pub trait SignalBase<T> {
18    /// Returns the underlying signal of the concrete implementation.
19    ///
20    /// This is used the implement all other common signal methods, but can be useful to "flatten" other Hooks so they can be used as a "standard" signal
21    fn base(&self) -> Signal<T>;
22
23    /// Should only called using the [`global!`] macro.
24    ///
25    /// # Unsafe
26    /// Breaks if this is not called directly after signal creation.
27    ///
28    /// [`global!`]: crate::prelude::global!
29    #[doc(hidden)]
30    unsafe fn globalize(&self) {
31        unsafe {
32            // SAFETY: Caller is responsible for safe call.
33            Scope::deregister_signal(self.base().slot)
34        };
35    }
36}
37
38pub trait SignalGetter<T>: SignalBase<T>
39where
40    T: Clone + 'static,
41{
42    // /// Calls all dependencies of the signal without changing its value.
43    // fn refresh(&self) {
44    //     let s = self.signal();
45    //     SignalStore::refresh(s.slot);
46    // }
47
48    /// Returns the current value of the signal without tracking updates.
49    ///
50    /// If updates should be tracked, use [`Self::value()`].
51    #[track_caller]
52    fn peek(&self) -> T {
53        let s = self.base();
54        SignalStore::read::<T>(s.slot)
55    }
56
57    /// Return the value of the signal and appends it to the current scope's dependencies.
58    /// If the values changes, the scope gets reexecuted to update to the new value.
59    ///
60    /// If update should not be tracked, use [`Self::peek()`].
61    #[track_caller]
62    fn value(&self) -> T {
63        let s = self.base();
64
65        #[cfg(debug_assertions)]
66        if s.is_immutable() {
67            panic!(
68                r#"
69A signal value was accessed within the same scope in which it was created, so the signal itself will be recreated with every update. This makes the signal effectively immutable.
70
71Wrap the signal access in a dynamic element (e.g., `move || my_signal.value()`) to fix this.
72                "#
73            );
74        }
75
76        SignalStore::update_read(s.slot)
77    }
78}
79
80pub trait SignalSetter<T>: SignalBase<T>
81where
82    T: PartialEq + 'static,
83{
84    /// Sets the value of the signal and notifies all dependencies.
85    ///
86    /// If the value is the same as the current value, nothing happens.
87    ///
88    /// Returns the old value of the signal.
89    #[track_caller]
90    fn set(&self, value: impl Into<T>) -> T {
91        let s = self.base();
92        SignalStore::set(s.slot, value.into())
93    }
94}
95
96pub trait SignalUpdater<T>: SignalBase<T>
97where
98    T: 'static,
99{
100    /// Updates the signal value without cloning it using the provided updater function and notifies all dependencies.
101    /// This will happen, no matter if the value acually changed or not.
102    ///
103    /// Returns whatever the updater function returns.
104    ///
105    /// # Panic
106    /// This will panic if other signals are read or created in the updater closure.
107    /// Do this beforehand if its needed.
108    #[track_caller]
109    fn update<U>(&self, updater: impl FnOnce(&mut T) -> U) -> U {
110        let s = self.base();
111        SignalStore::update(s.slot, updater)
112    }
113
114    /// Sets the value of the signal and notifies all its dependencies.
115    ///
116    /// This does not check if the value is the same as the current value.
117    #[track_caller]
118    fn set_unchecked(&self, value: impl Into<T>) {
119        let s = self.base();
120        SignalStore::set_unchecked(s.slot, value.into());
121    }
122}
123
124pub trait SignalToggle: SignalUpdater<bool> {
125    #[track_caller]
126    fn toggle(&self) -> bool {
127        self.update(|s| {
128            let old = *s;
129            *s = !old;
130            old
131        })
132    }
133}
134impl<S> SignalToggle for S where S: SignalUpdater<bool> {}
135
136#[derive(Debug, PartialEq, Eq)]
137pub struct Signal<T> {
138    pub(super) slot: usize,
139    _value: std::marker::PhantomData<T>,
140
141    #[cfg(debug_assertions)]
142    always_mutable: bool,
143}
144
145impl<T> SignalBase<T> for Signal<T> {
146    fn base(&self) -> Signal<T> {
147        *self
148    }
149}
150impl<T> SignalGetter<T> for Signal<T> where T: Clone + 'static {}
151impl<T> SignalSetter<T> for Signal<T> where T: PartialEq + 'static {}
152impl<T> SignalUpdater<T> for Signal<T> where T: 'static {}
153
154// Impl `Clone` and `Copy` manually to avoid requiring `T: Clone + Copy`
155impl<T> Clone for Signal<T> {
156    fn clone(&self) -> Self {
157        *self
158    }
159}
160impl<T> Copy for Signal<T> {}
161
162impl<T> Default for Signal<T>
163where
164    T: Default + 'static,
165{
166    fn default() -> Self {
167        Self::new(T::default())
168    }
169}
170
171impl<T> Signal<T>
172where
173    T: 'static,
174{
175    /// Creates a new signal with the given initial value.
176    ///
177    /// A signal is a reactive value that can be read and updated. When a signal is updated, all function components that read the signal will be re-run to reflect the new value.
178    ///
179    /// The signal is removed when the component which created it goes out of scope.
180    ///
181    /// # Example
182    /// ```
183    /// use rustolio_web::prelude::*;
184    ///
185    /// let s = Signal::new("Hello World".to_string());
186    /// div! {
187    ///     class: "container",
188    ///     move || s.value(),
189    /// };
190    /// ```
191    ///
192    /// # Caveats
193    /// All caveats will throw an runtime error in debug mode.
194    ///
195    /// ## Immutable Signals
196    /// When creating a signal, it is important to ensure that the signal is read in a function component (e.g., `move || s.value()`). If this is not done, the signal itself will be recreated on every update, making it effectively immutable.
197    ///
198    /// Dont do this:
199    /// ``` ignore
200    /// use rustolio_web::prelude::*;
201    ///
202    /// let s = Signal::new("Hello World".to_string());
203    /// div! {
204    ///    s.value()
205    /// };
206    /// ```
207    /// Do this instead:
208    /// ```
209    /// use rustolio_web::prelude::*;
210    ///
211    /// let s = Signal::new("Hello World".to_string());
212    /// div! {
213    ///    move || s.value()
214    /// };
215    /// // OR
216    /// move || div! {
217    ///    s.value()
218    /// };
219    /// ```
220    ///
221    /// ## Breaking Signals
222    /// When reading a signal value, it is important to ensure that the next component is not a function component. This would break the signal's updater, as it would replace the component without notifying the signal's updater.
223    ///
224    /// Dont do this:
225    /// ``` ignore
226    /// use rustolio_web::prelude::*;
227    ///
228    /// let s = Signal::new("Hello World".to_string());
229    /// let s1 = Signal::new("Hello World 2".to_string());
230    /// div! {
231    ///    move || {
232    ///       let value = s.value();
233    ///       move || div! {
234    ///          format!("S: {} - S1: {}", value, s1.value())
235    ///       }
236    ///    }
237    /// };
238    /// ```
239    /// Do this instead:
240    /// ```
241    /// use rustolio_web::prelude::*;
242    ///
243    /// let s = Signal::new("Hello World".to_string());
244    /// let s1 = Signal::new("Hello World 2".to_string());
245    /// div! {
246    ///    move || {
247    ///       let value = s.value();
248    ///       div! {
249    ///          move || div! {
250    ///             format!("S: {} - S1: {}", value, s1.value())
251    ///          }
252    ///       }
253    ///    }
254    /// };
255    /// ```
256    ///
257    pub fn new(initial: T) -> Self {
258        let slot = SignalStore::insert(initial);
259        Scope::register_signal(slot);
260        Signal {
261            slot,
262            _value: std::marker::PhantomData,
263            #[cfg(debug_assertions)]
264            always_mutable: false,
265        }
266    }
267
268    /// Creates the Signal (same as [`Self::new`]) but the check for immutablility will alway succeed.
269    ///
270    /// This can be useful for Signal-Wrapper which are allowed the be read in the same scope.
271    pub fn always_mutable(initial: T) -> Self {
272        let slot = SignalStore::insert(initial);
273        Scope::register_signal(slot);
274        Signal {
275            slot,
276            _value: std::marker::PhantomData,
277            #[cfg(debug_assertions)]
278            always_mutable: true,
279        }
280    }
281
282    /// Creates a new signal without setting an initial value.
283    ///
284    /// This is unsafe because reading, updating or setting the signal before it has been initialized will cause a panic.
285    ///
286    /// The signal must be set by using the [`Self::set_unchecked`] method before using any other method.
287    pub(super) unsafe fn empty_always_mutable(scoped: bool) -> Self {
288        let slot = SignalStore::insert(());
289        if scoped {
290            Scope::register_signal(slot);
291        }
292        Signal {
293            slot,
294            _value: std::marker::PhantomData,
295            #[cfg(debug_assertions)]
296            always_mutable: true,
297        }
298    }
299
300    pub(super) fn from_slot(slot: usize) -> Self {
301        Signal {
302            slot,
303            _value: std::marker::PhantomData,
304            #[cfg(debug_assertions)]
305            always_mutable: false,
306        }
307    }
308
309    #[cfg(debug_assertions)]
310    fn is_immutable(&self) -> bool {
311        !self.always_mutable && Scope::signal_created_in_current_scope(*self)
312    }
313
314    // #[cfg(debug_assertions)]
315    // #[track_caller]
316    // fn check_breaking_read(&self) -> Option<HashSet<SignalReadInfo>> {
317    //     BreakingSignalChecker::signal_read(self.slot)
318    // }
319}
320
321#[derive(Debug, PartialEq, Eq)]
322pub struct GlobalSignal<T>(Signal<T>);
323
324impl<T> GlobalSignal<T>
325where
326    T: 'static,
327{
328    /// Will not be removed when the component which created it goes out of scope.
329    ///
330    /// This is useful for signals that should be globally accessible.
331    pub fn new(initial: T) -> Self {
332        let slot = SignalStore::insert(initial);
333        Self(Signal {
334            slot,
335            _value: std::marker::PhantomData,
336            #[cfg(debug_assertions)]
337            always_mutable: true,
338        })
339    }
340}
341
342impl<T> Default for GlobalSignal<T>
343where
344    T: Default + 'static,
345{
346    fn default() -> Self {
347        Self::new(T::default())
348    }
349}
350
351impl<T> SignalBase<T> for GlobalSignal<T> {
352    fn base(&self) -> Signal<T> {
353        self.0
354    }
355    unsafe fn globalize(&self) {
356        // Nothing to do - the underlying signal was never registered
357    }
358}
359impl<T> SignalGetter<T> for GlobalSignal<T> where T: Clone + 'static {}
360impl<T> SignalSetter<T> for GlobalSignal<T> where T: PartialEq + 'static {}
361impl<T> SignalUpdater<T> for GlobalSignal<T> where T: 'static {}
362
363impl<T> From<GlobalSignal<T>> for Signal<T>
364where
365    T: Clone + 'static,
366{
367    fn from(val: GlobalSignal<T>) -> Self {
368        val.base()
369    }
370}
371
372// Impl `Clone` and `Copy` manually to avoid requiring `T: Clone + Copy`
373impl<T> Clone for GlobalSignal<T> {
374    fn clone(&self) -> Self {
375        *self
376    }
377}
378impl<T> Copy for GlobalSignal<T> {}
379
380#[cfg(test)]
381mod tests {
382    use crate::prelude::*;
383
384    #[test]
385    fn test_signal() {
386        let s = Signal::new(0);
387        assert_eq!(s.value(), 0);
388        assert_eq!(s.peek(), 0);
389        let old = s.set(42);
390        assert_eq!(old, 0);
391        assert_eq!(s.value(), 42);
392        assert_eq!(s.peek(), 42);
393        let res = s.update(|v| {
394            *v += 1;
395            "Some random return value"
396        });
397        assert_eq!(res, "Some random return value");
398        assert_eq!(s.value(), 43);
399        assert_eq!(s.peek(), 43);
400        s.set_unchecked(100);
401        assert_eq!(s.value(), 100);
402        assert_eq!(s.peek(), 100);
403
404        // Test that signal impls `Copy` with `T: !Copy`
405        let s = Signal::new("Hello".to_string());
406        let _s1 = s;
407        let _s2 = s;
408
409        // Test that signal impls `Copy` with `T: !Copy`
410        let s = GlobalSignal::new("Hello".to_string());
411        let _s1 = s;
412        let _s2 = s;
413    }
414}