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}