Skip to main content

rustolio_web/hooks/
calculated.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
11use crate::hooks::{SignalBase, SignalUpdater as _, effect::EffectStore};
12
13use super::{Scope, Signal, SignalGetter, signal_updater_callback};
14
15#[derive(Debug, PartialEq, Eq)]
16pub struct Calculated<T>(Signal<T>, usize);
17
18impl<T> Calculated<T>
19where
20    T: 'static,
21{
22    pub fn new(f: impl Fn() -> T + 'static) -> Self {
23        // Effect for removing the listener after drop
24        let id = EffectStore::new();
25        Scope::register_effect(id);
26
27        // Signal for storing and updating the value
28        let signal = unsafe {
29            // SAFETY: The actual value is set immediately in the `signal_updater_callback` below.
30            Signal::empty_always_mutable(true)
31        };
32
33        let s = signal;
34        signal_updater_callback(
35            move || !EffectStore::is_dropped(id),
36            move || {
37                s.set_unchecked(f());
38            },
39        );
40
41        Calculated(signal, id)
42    }
43}
44
45impl<T> Calculated<T>
46where
47    T: Default + 'static,
48{
49    /// If an error is returned the signals value will be set to the default value.
50    pub fn new_result(f: impl Fn() -> rustolio_utils::Result<T> + 'static) -> Self {
51        Self::new(crate::error::convert_fn_0("Calculated", f))
52    }
53}
54
55impl<T> SignalBase<T> for Calculated<T> {
56    fn base(&self) -> Signal<T> {
57        self.0
58    }
59
60    unsafe fn globalize(&self) {
61        unsafe {
62            // SAFETY: Caller is responsible for safe call.
63            self.0.globalize();
64            Scope::deregister_effect(self.1);
65        }
66    }
67}
68impl<T> SignalGetter<T> for Calculated<T> where T: Clone + 'static {}
69
70// Impl `Clone` and `Copy` manually to avoid requiring `T: Clone + Copy`
71impl<T> Clone for Calculated<T> {
72    fn clone(&self) -> Self {
73        *self
74    }
75}
76impl<T> Copy for Calculated<T> {}
77
78#[cfg(test)]
79mod tests {
80    use crate::prelude::*;
81
82    #[test]
83    fn test_calculated() {
84        let m = Calculated::new(|| 5);
85        assert_eq!(m.value(), 5);
86
87        let s = Signal::new(10);
88        let m = Calculated::new(move || s.value() * 2);
89        assert_eq!(s.value(), 10);
90        assert_eq!(s.peek(), 10);
91        assert_eq!(m.value(), 20);
92        assert_eq!(m.peek(), 20);
93        let old = s.set(20);
94        assert_eq!(old, 10);
95        assert_eq!(s.value(), 20);
96        assert_eq!(s.peek(), 20);
97        assert_eq!(m.value(), 40);
98        assert_eq!(m.peek(), 40);
99        // m.set(30); // Not implemented
100
101        // Test that calculated impls `Copy` with `T: !Copy`
102        let m = Calculated::new(|| "Hello".to_string());
103        let _m1 = m;
104        let _m2 = m;
105    }
106}