Skip to main content

velox_core/
ergonomics.rs

1//! Vue-like ergonomic APIs for Velox.
2//!
3//! These helpers provide a more concise syntax for common reactive patterns,
4//! making Velox feel like Vue's Composition API while using Rust under the hood.
5//!
6//! # Examples
7//!
8//! ```rust,ignore
9//! // Vue-like ref() instead of Rc::new(Signal::new(...))
10//! let count = ref_value(0);           // Ref<i32>
11//! count.set(42);
12//! assert_eq!(count.get(), 42);
13//!
14//! // reactive!() macro for creating component state
15//! let state = reactive! {
16//!     count: i32 = 0,
17//!     name: String = String::from("Hello"),
18//! };
19//! state.count.set(10);
20//! state.name.set(String::from("World"));
21//! ```
22
23use std::rc::Rc;
24
25use crate::signal::Signal;
26
27/// A Vue-like reactive reference.
28///
29/// This is a thin wrapper around `Rc<Signal<T>>` that provides
30/// a more ergonomic API for creating reactive state.
31///
32/// # Examples
33///
34/// ```rust,ignore
35/// let count = ref_value(0);
36/// count.set(42);
37/// assert_eq!(count.get(), 42);
38/// ```
39#[derive(Clone)]
40pub struct Ref<T>(Rc<Signal<T>>);
41
42impl<T: Clone + 'static> Ref<T> {
43    /// Create a new reactive reference with the given initial value.
44    #[inline]
45    pub fn new(value: T) -> Self {
46        Self(Rc::new(Signal::new(value)))
47    }
48
49    /// Get the current value.
50    #[inline]
51    pub fn get(&self) -> T {
52        self.0.get()
53    }
54
55    /// Set a new value and notify subscribers.
56    #[inline]
57    pub fn set(&self, value: T) {
58        self.0.set(value);
59    }
60
61    /// Update the value using a closure.
62    #[inline]
63    pub fn update<F>(&self, f: F)
64    where
65        F: FnOnce(T) -> T,
66    {
67        self.0.update(f);
68    }
69
70    /// Set value only if it changed.
71    #[inline]
72    pub fn set_if_changed(&self, value: T)
73    where
74        T: PartialEq,
75    {
76        self.0.set_if_changed(value);
77    }
78
79    /// Get the inner `Rc<Signal<T>>` for use with effects and other APIs.
80    #[inline]
81    pub fn as_signal(&self) -> &Rc<Signal<T>> {
82        &self.0
83    }
84
85    /// Convert into the inner `Rc<Signal<T>>`.
86    #[inline]
87    pub fn into_signal(self) -> Rc<Signal<T>> {
88        self.0
89    }
90}
91
92impl<T: Clone + 'static> std::ops::Deref for Ref<T> {
93    type Target = Rc<Signal<T>>;
94
95    fn deref(&self) -> &Self::Target {
96        &self.0
97    }
98}
99
100impl<T: std::fmt::Debug + Clone + 'static> std::fmt::Debug for Ref<T> {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_tuple("Ref").field(&self.get()).finish()
103    }
104}
105
106/// Create a reactive reference (Vue-like `ref()`).
107///
108/// # Examples
109///
110/// ```rust,ignore
111/// let count = ref_value(0);
112/// let name = ref_value(String::from("Hello"));
113/// ```
114#[inline]
115pub fn ref_value<T: Clone + 'static>(value: T) -> Ref<T> {
116    Ref::new(value)
117}
118
119/// Create a reactive reference from a `Signal<T>`.
120///
121/// This is useful when you already have a `Signal` and want the Ref API.
122#[inline]
123pub fn from_signal<T: Clone + 'static>(signal: Rc<Signal<T>>) -> Ref<T> {
124    Ref(signal)
125}
126
127/// Create a reactive reference from a `Signal<T>`.
128///
129/// This is useful when you already have a `Signal` and want the Ref API.
130/// Alias for `from_signal()`.
131#[inline]
132pub fn cell_ref<T: Clone + 'static>(signal: Rc<Signal<T>>) -> Ref<T> {
133    from_signal(signal)
134}
135
136/// Convert a `Ref<T>` back to `Rc<Signal<T>>` for use with effects.
137#[inline]
138pub fn to_signal<T: Clone + 'static>(r: &Ref<T>) -> &Rc<Signal<T>> {
139    r.as_signal()
140}
141
142/// Create a shallow reactive reference using `std::cell::Cell`.
143///
144/// This is useful for simple types like `i32`, `bool`, `usize`
145/// that implement `Copy`. It's lighter weight than `Signal<T>`.
146pub struct ShallowRef<T: Copy>(std::rc::Rc<std::cell::Cell<T>>);
147
148impl<T: Copy> ShallowRef<T> {
149    #[inline]
150    pub fn new(value: T) -> Self {
151        Self(std::rc::Rc::new(std::cell::Cell::new(value)))
152    }
153
154    #[inline]
155    pub fn get(&self) -> T {
156        self.0.get()
157    }
158
159    #[inline]
160    pub fn set(&self, value: T) {
161        self.0.set(value);
162    }
163
164    #[inline]
165    pub fn update<F>(&self, f: F)
166    where
167        F: FnOnce(T) -> T,
168    {
169        self.0.set(f(self.0.get()));
170    }
171}
172
173impl<T: Copy> Clone for ShallowRef<T> {
174    fn clone(&self) -> Self {
175        Self(self.0.clone())
176    }
177}
178
179impl<T: Copy + std::fmt::Debug> std::fmt::Debug for ShallowRef<T> {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.debug_tuple("ShallowRef").field(&self.get()).finish()
182    }
183}
184
185/// Create a shallow reactive reference for `Copy` types.
186///
187/// This is lighter weight than `ref_value()` for simple types.
188#[inline]
189pub fn shallow_ref<T: Copy>(value: T) -> ShallowRef<T> {
190    ShallowRef::new(value)
191}
192
193/// Create a reactive struct with named fields.
194///
195/// Each field becomes a `Ref<T>`. This is useful for grouping
196/// related reactive state together.
197///
198/// # Examples
199///
200/// ```rust,ignore
201/// let state = reactive_struct! {
202///     count: Ref<i32> = ref_value(0),
203///     name: Ref<String> = ref_value(String::from("Hello")),
204/// };
205///
206/// state.count.set(42);
207/// state.name.set(String::from("World"));
208/// ```
209#[macro_export]
210macro_rules! reactive_struct {
211    ($( $field:ident: $ty:ty = $init:expr ),* $(,)?) => {
212        {
213            struct __State {
214                $( $field: $ty, )*
215            }
216            __State {
217                $( $field: $init, )*
218            }
219        }
220    };
221}
222
223/// Define reactive state inline using a struct expression.
224///
225/// This is a shorthand for creating an anonymous struct with Ref fields.
226///
227/// # Examples
228///
229/// ```rust,ignore
230/// let state = reactive! {
231///     count: i32 = 0,
232///     name: String = String::from("Hello"),
233/// };
234///
235/// // Usage:
236/// state.count.set(42);
237/// state.name.set(String::from("World"));
238/// ```
239#[macro_export]
240macro_rules! reactive {
241    ($( $field:ident: $ty:ty = $init:expr ),* $(,)?) => {
242        {
243            struct __ReactiveState {
244                $( $field: $crate::ergonomics::Ref<$ty>, )*
245            }
246            __ReactiveState {
247                $( $field: $crate::ergonomics::ref_value($init), )*
248            }
249        }
250    };
251}
252
253/// Define emits for a component (Vue-like `defineEmits`).
254///
255/// This macro documents which events a component can emit.
256/// At runtime it creates the emit helper function.
257///
258/// # Examples
259///
260/// ```rust,ignore
261/// define_emits!(change, reset);
262///
263/// // In your methods:
264/// pub fn increment(&self) {
265///     self.count.set(self.count.get() + 1);
266///     emit!("change", &self.count.get().to_string());
267/// }
268/// ```
269#[macro_export]
270macro_rules! define_emits {
271    ($( $event:ident ),* $(,)?) => {
272        /// Supported events for this component.
273        pub const EMIT_EVENTS: &[&str] = &[$(stringify!($event),)*];
274    };
275}
276
277/// Emit an event to the parent component.
278///
279/// This is a no-op placeholder for standalone usage.
280/// When a component is rendered via `render_with_callbacks`, the SFC compiler
281/// generates a local `emit()` function that replaces this one via module scoping.
282/// Users should call `emit!()` macro or the code-generated version, not this function directly.
283///
284/// # Examples
285///
286/// ```rust,ignore
287/// emit!("change", &value.to_string());
288/// emit!("click");
289/// ```
290pub fn emit(_event: &str, _payload: &str) {
291    // In standalone mode, this is a no-op.
292    // When rendered with callbacks, the codegen replaces this.
293}
294
295/// Emit an event without payload.
296#[inline]
297pub fn emit_void(event: &str) {
298    emit(event, "");
299}
300
301/// Vue-like `watch` shorthand.
302///
303/// Watches a signal and calls the callback when it changes.
304///
305/// # Examples
306///
307/// ```rust,ignore
308/// let count = ref_value(0);
309/// watch_ref(&count, |new, old| {
310///     println!("count changed: {} -> {}", old, new);
311/// });
312/// ```
313pub fn watch_ref<T: Clone + PartialEq + 'static>(
314    source: &Ref<T>,
315    callback: impl FnMut(T, T) + 'static,
316) {
317    let sig = source.as_signal().clone();
318    crate::watch::watch(
319        move || sig.get(),
320        callback,
321        crate::watch::WatchOptions::default(),
322    );
323}
324
325/// Vue-like `watchEffect` shorthand.
326///
327/// Runs the effect immediately and re-runs when dependencies change.
328///
329/// # Examples
330///
331/// ```rust,ignore
332/// let count = ref_value(0);
333/// watch_effect(|| {
334///     println!("count is: {}", count.get());
335/// });
336/// ```
337pub fn watch_effect<F: FnMut() + 'static>(f: F) -> crate::signal::EffectHandle {
338    crate::signal::effect(f)
339}
340
341/// Vue-like `computed` shorthand.
342///
343/// Creates a computed signal that derives its value from other signals.
344///
345/// # Examples
346///
347/// ```rust,ignore
348/// let count = ref_value(0);
349/// let doubled = computed_ref(|| count.get() * 2);
350/// assert_eq!(doubled.get(), 0);
351/// count.set(5);
352/// assert_eq!(doubled.get(), 10);
353/// ```
354pub fn computed_ref<T: Clone + PartialEq + 'static, F: Fn() -> T + 'static>(compute: F) -> Ref<T> {
355    Ref(crate::signal::computed(compute))
356}
357
358/// Create a readonly computed ref from a closure.
359///
360/// This is the same as `computed_ref()` but makes the intent clearer
361/// that the result is not meant to be written to directly.
362#[inline]
363pub fn readonly<T: Clone + PartialEq + 'static, F: Fn() -> T + 'static>(compute: F) -> Ref<T> {
364    computed_ref(compute)
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_ref_value() {
373        let count = ref_value(0);
374        assert_eq!(count.get(), 0);
375
376        count.set(42);
377        assert_eq!(count.get(), 42);
378
379        count.update(|v| v + 1);
380        assert_eq!(count.get(), 43);
381    }
382
383    #[test]
384    fn test_ref_clone() {
385        let a = ref_value(String::from("hello"));
386        let b = a.clone();
387        b.set(String::from("world"));
388        assert_eq!(a.get(), "world");
389    }
390
391    #[test]
392    fn test_ref_set_if_changed() {
393        let count = ref_value(0);
394        count.set(1);
395        count.set_if_changed(1); // no change
396        count.set_if_changed(2); // changed
397        assert_eq!(count.get(), 2);
398    }
399
400    #[test]
401    fn test_shallow_ref() {
402        let count = shallow_ref(0);
403        assert_eq!(count.get(), 0);
404
405        count.set(42);
406        assert_eq!(count.get(), 42);
407
408        count.update(|v| v * 2);
409        assert_eq!(count.get(), 84);
410    }
411
412    #[test]
413    fn test_shallow_ref_clone() {
414        let a = shallow_ref(10);
415        let b = a.clone();
416        b.set(20);
417        assert_eq!(a.get(), 20);
418    }
419
420    #[test]
421    fn test_reactive_macro() {
422        let state = reactive! {
423            count: i32 = 0,
424            name: String = String::from("hello"),
425        };
426
427        assert_eq!(state.count.get(), 0);
428        assert_eq!(state.name.get(), "hello");
429
430        state.count.set(42);
431        state.name.set(String::from("world"));
432
433        assert_eq!(state.count.get(), 42);
434        assert_eq!(state.name.get(), "world");
435    }
436
437    #[test]
438    #[allow(clippy::const_is_empty)]
439    fn test_define_emits_macro() {
440        // Just verify it compiles
441        define_emits!(change, reset);
442        assert!(!EMIT_EVENTS.is_empty());
443    }
444
445    #[test]
446    fn test_computed_ref() {
447        let count = ref_value(5);
448        let doubled = computed_ref({
449            let count = count.clone();
450            move || count.get() * 2
451        });
452
453        assert_eq!(doubled.get(), 10);
454
455        count.set(10);
456        assert_eq!(doubled.get(), 20);
457    }
458
459    #[test]
460    fn test_readonly() {
461        let count = ref_value(5);
462        let doubled = readonly({
463            let count = count.clone();
464            move || count.get() * 2
465        });
466
467        assert_eq!(doubled.get(), 10);
468    }
469
470    #[test]
471    fn test_from_signal() {
472        let signal = Rc::new(Signal::new(42));
473        let r = from_signal(signal);
474        assert_eq!(r.get(), 42);
475        r.set(100);
476        assert_eq!(r.get(), 100);
477    }
478
479    #[test]
480    fn test_into_signal() {
481        let r = ref_value(42);
482        let signal = r.into_signal();
483        assert_eq!(signal.get(), 42);
484    }
485
486    #[test]
487    fn test_deref() {
488        let r = ref_value(42);
489        // Deref allows calling Signal methods directly
490        assert_eq!(r.get(), 42);
491    }
492
493    #[test]
494    fn test_emit_void() {
495        // Should not panic
496        emit_void("test");
497    }
498
499    #[test]
500    fn test_emit_with_payload() {
501        // Should not panic
502        emit("test", "payload");
503    }
504}