rsx_core/
lib.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use std::{any::Any, cell::RefCell, rc::Rc};
4
5/// Easy imports for RSX applications
6pub mod prelude {
7    pub use crate::{
8        fragment, h, on_click, on_event, text, use_signal, use_state, use_state_updater, use_state_with_updater, Signal, UseState, VNode,
9    };
10    pub use rsx_macro::{component, rsx, rsx_main};
11    pub use wasm_bindgen::prelude::*;
12}
13
14#[derive(Clone, PartialEq, Debug)]
15pub enum VNode {
16    Element(Element),
17    Text(String),
18    Fragment(Vec<VNode>),
19}
20
21#[derive(Clone, PartialEq, Debug)]
22pub struct Element {
23    pub tag: String,
24    pub props: IndexMap<String, PropValue>,
25    pub children: Vec<VNode>,
26}
27
28#[derive(Clone)]
29pub enum PropValue {
30    Str(String),
31    Bool(bool),
32    Num(f64),
33    Callback(String), // logical id; rsx-web binds at mount
34    Dataset(IndexMap<String, String>),
35    EventHandler(Rc<RefCell<dyn FnMut()>>), // closure-based event handler
36}
37
38// Manual Debug implementation since closures don't implement Debug
39impl std::fmt::Debug for PropValue {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            PropValue::Str(s) => f.debug_tuple("Str").field(s).finish(),
43            PropValue::Bool(b) => f.debug_tuple("Bool").field(b).finish(),
44            PropValue::Num(n) => f.debug_tuple("Num").field(n).finish(),
45            PropValue::Callback(s) => f.debug_tuple("Callback").field(s).finish(),
46            PropValue::Dataset(ds) => f.debug_tuple("Dataset").field(ds).finish(),
47            PropValue::EventHandler(_) => {
48                f.debug_tuple("EventHandler").field(&"<closure>").finish()
49            }
50        }
51    }
52}
53
54// Manual PartialEq implementation since closures don't implement PartialEq
55impl PartialEq for PropValue {
56    fn eq(&self, other: &Self) -> bool {
57        match (self, other) {
58            (PropValue::Str(a), PropValue::Str(b)) => a == b,
59            (PropValue::Bool(a), PropValue::Bool(b)) => a == b,
60            (PropValue::Num(a), PropValue::Num(b)) => a == b,
61            (PropValue::Callback(a), PropValue::Callback(b)) => a == b,
62            (PropValue::Dataset(a), PropValue::Dataset(b)) => a == b,
63            (PropValue::EventHandler(_), PropValue::EventHandler(_)) => false, // Never equal
64            _ => false,
65        }
66    }
67}
68
69// Manual Serialize implementation since closures can't be serialized
70impl Serialize for PropValue {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where
73        S: serde::Serializer,
74    {
75        match self {
76            PropValue::Str(s) => s.serialize(serializer),
77            PropValue::Bool(b) => b.serialize(serializer),
78            PropValue::Num(n) => n.serialize(serializer),
79            PropValue::Callback(s) => s.serialize(serializer),
80            PropValue::Dataset(ds) => ds.serialize(serializer),
81            PropValue::EventHandler(_) => serializer.serialize_str("event_handler"),
82        }
83    }
84}
85
86// Manual Deserialize implementation
87impl<'de> Deserialize<'de> for PropValue {
88    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89    where
90        D: serde::Deserializer<'de>,
91    {
92        // For now, just deserialize as strings - we can't reconstruct closures
93        let s = String::deserialize(deserializer)?;
94        Ok(PropValue::Str(s))
95    }
96}
97
98pub fn h<T: Into<String>>(
99    tag: T,
100    props: IndexMap<String, PropValue>,
101    children: Vec<VNode>,
102) -> VNode {
103    VNode::Element(Element {
104        tag: tag.into(),
105        props,
106        children,
107    })
108}
109
110pub fn text<T: Into<String>>(s: T) -> VNode {
111    VNode::Text(s.into())
112}
113
114pub fn fragment(children: Vec<VNode>) -> VNode {
115    VNode::Fragment(children)
116}
117
118/// Helper function to create event handlers more easily
119pub fn on_click<F>(handler: F) -> PropValue
120where
121    F: FnMut() + 'static,
122{
123    PropValue::EventHandler(Rc::new(RefCell::new(handler)))
124}
125
126/// Helper function to create any event handler
127pub fn on_event<F>(handler: F) -> PropValue
128where
129    F: FnMut() + 'static,
130{
131    PropValue::EventHandler(Rc::new(RefCell::new(handler)))
132}
133
134/// ---- Signals (very small reactive core) ----
135pub struct Signal<T>(Rc<RefCell<T>>);
136
137impl<T> Clone for Signal<T> {
138    fn clone(&self) -> Self {
139        Signal(self.0.clone())
140    }
141}
142
143impl<T> Signal<T> {
144    pub fn new(v: T) -> Self {
145        Signal(Rc::new(RefCell::new(v)))
146    }
147
148    pub fn get(&self) -> T
149    where
150        T: Clone,
151    {
152        self.0.borrow().clone()
153    }
154
155    pub fn set(&self, v: T) {
156        *self.0.borrow_mut() = v;
157        SCHEDULER.with(|s| s.borrow_mut().mark_dirty());
158    }
159
160    pub fn update(&self, f: impl FnOnce(&mut T)) {
161        {
162            let mut guard = self.0.borrow_mut();
163            f(&mut *guard);
164        }
165        SCHEDULER.with(|s| s.borrow_mut().mark_dirty());
166    }
167
168    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
169        f(&self.0.borrow())
170    }
171}
172
173#[derive(Clone)]
174pub struct UseState<T> {
175    signal: Signal<T>,
176}
177
178impl<T> UseState<T> {
179    pub fn signal(&self) -> Signal<T> {
180        self.signal.clone()
181    }
182
183    pub fn set(&self, v: T) {
184        self.signal.set(v);
185    }
186
187    pub fn update(&self, f: impl FnOnce(&mut T)) {
188        self.signal.update(f);
189    }
190
191    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
192        self.signal.with(f)
193    }
194}
195
196impl<T: Clone> UseState<T> {
197    pub fn get(&self) -> T {
198        self.signal.get()
199    }
200}
201
202// Effects / scheduler (frame-coalesced)
203thread_local! {
204    static SCHEDULER: RefCell<Scheduler> = RefCell::new(Scheduler::default());
205}
206
207#[derive(Default)]
208pub struct Scheduler {
209    dirty: bool,
210    hooks: Vec<Box<dyn FnMut()>>,
211}
212
213impl Scheduler {
214    pub fn register(cb: Box<dyn FnMut()>) {
215        SCHEDULER.with(|s| s.borrow_mut().hooks.push(cb));
216    }
217
218    pub fn mark_dirty(&mut self) {
219        self.dirty = true;
220    }
221
222    pub fn flush_if_dirty() {
223        SCHEDULER.with(|s| {
224            let mut s = s.borrow_mut();
225            if s.dirty {
226                s.dirty = false;
227                for cb in &mut s.hooks {
228                    cb();
229                }
230            }
231        });
232    }
233}
234
235pub fn effect(mut f: impl FnMut() + 'static) {
236    Scheduler::register(Box::new(move || f()));
237}
238
239/// ---- Component System & Hooks ----
240
241// Component hook context (thread-local for simplicity)
242thread_local! {
243    static COMPONENT_CONTEXT: RefCell<ComponentContext> = RefCell::new(ComponentContext::default());
244}
245
246#[derive(Default)]
247struct ComponentContext {
248    current_component: Option<ComponentId>,
249    components: IndexMap<ComponentId, ComponentState>,
250    next_component_id: usize,
251    hook_index: usize,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
255pub struct ComponentId(usize);
256
257struct ComponentState {
258    hooks: Vec<Hook>,
259    render_fn: std::rc::Rc<dyn Fn() -> VNode>,
260}
261
262enum Hook {
263    Signal(SignalHook),
264}
265
266struct SignalHook {
267    signal: Box<dyn Any>,
268}
269
270impl ComponentContext {
271    fn new_component_id(&mut self) -> ComponentId {
272        let id = ComponentId(self.next_component_id);
273        self.next_component_id += 1;
274        id
275    }
276
277    fn enter_component(&mut self, id: ComponentId) {
278        self.current_component = Some(id);
279        self.hook_index = 0;
280    }
281
282    fn exit_component(&mut self) {
283        self.current_component = None;
284    }
285}
286
287/// Hook to manage state for a component, exactly like React's useState.
288/// Returns (value, setter) - React-style API with cloneable setter.
289pub fn use_state<T: Clone + 'static>(initial_value: T) -> (T, impl Fn(T) + Clone) {
290    let signal = COMPONENT_CONTEXT.with(|ctx| {
291        let mut ctx = ctx.borrow_mut();
292        let component_id = ctx
293            .current_component
294            .expect("use_state called outside of a component render");
295        let hook_index = ctx.hook_index;
296
297        let component_state = ctx
298            .components
299            .get_mut(&component_id)
300            .expect("component state missing during use_state call");
301
302        let signal = if hook_index >= component_state.hooks.len() {
303            let signal = Signal::new(initial_value.clone());
304            component_state.hooks.push(Hook::Signal(SignalHook {
305                signal: Box::new(signal.clone()),
306            }));
307            signal
308        } else {
309            match &component_state.hooks[hook_index] {
310                Hook::Signal(hook) => hook
311                    .signal
312                    .as_ref()
313                    .downcast_ref::<Signal<T>>()
314                    .unwrap_or_else(|| panic!(
315                        "use_state type mismatch at hook index {}; did you change the hook call order?",
316                        hook_index
317                    ))
318                    .clone(),
319            }
320        };
321
322        ctx.hook_index += 1;
323        signal
324    });
325
326    let value = signal.get();
327
328    // Return cloneable setter
329    let setter = {
330        let signal = signal.clone();
331        move |new_value: T| {
332            signal.set(new_value);
333        }
334    };
335
336    (value, setter)
337}
338
339/// Alternative useState hook that returns (value, update_fn) where update_fn
340/// takes a closure that receives the current value and returns the new value.
341pub fn use_state_updater<T: Clone + 'static>(initial_value: T) -> (T, Rc<dyn Fn(Box<dyn Fn(T) -> T>)>) {
342    let signal = COMPONENT_CONTEXT.with(|ctx| {
343        let mut ctx = ctx.borrow_mut();
344        let component_id = ctx
345            .current_component
346            .expect("use_state_updater called outside of a component render");
347        let hook_index = ctx.hook_index;
348
349        let component_state = ctx
350            .components
351            .get_mut(&component_id)
352            .expect("component state missing during use_state_updater call");
353
354        let signal = if hook_index >= component_state.hooks.len() {
355            let signal = Signal::new(initial_value.clone());
356            component_state.hooks.push(Hook::Signal(SignalHook {
357                signal: Box::new(signal.clone()),
358            }));
359            signal
360        } else {
361            match &component_state.hooks[hook_index] {
362                Hook::Signal(hook) => hook
363                    .signal
364                    .as_ref()
365                    .downcast_ref::<Signal<T>>()
366                    .unwrap_or_else(|| panic!(
367                        "use_state_updater type mismatch at hook index {}; did you change the hook call order?",
368                        hook_index
369                    ))
370                    .clone(),
371            }
372        };
373
374        ctx.hook_index += 1;
375        signal
376    });
377
378    let value = signal.get();
379
380    // Functional updater that can be cloned
381    let updater = {
382        let signal = signal.clone();
383        Rc::new(move |update_fn: Box<dyn Fn(T) -> T>| {
384            let current = signal.get();
385            let new_value = update_fn(current);
386            signal.set(new_value);
387        })
388    };
389
390    (value, updater)
391}
392
393/// Alternative useState hook that returns (value, updater, setter) where:
394/// - updater: takes a function that modifies the current value
395/// - setter: takes a new value directly
396pub fn use_state_with_updater<T: Clone + 'static>(initial_value: T) -> (T, impl Fn(Box<dyn FnOnce(&mut T)>), impl Fn(T)) {
397    let signal = COMPONENT_CONTEXT.with(|ctx| {
398        let mut ctx = ctx.borrow_mut();
399        let component_id = ctx
400            .current_component
401            .expect("use_state_with_updater called outside of a component render");
402        let hook_index = ctx.hook_index;
403
404        let component_state = ctx
405            .components
406            .get_mut(&component_id)
407            .expect("component state missing during use_state_with_updater call");
408
409        let signal = if hook_index >= component_state.hooks.len() {
410            let signal = Signal::new(initial_value.clone());
411            component_state.hooks.push(Hook::Signal(SignalHook {
412                signal: Box::new(signal.clone()),
413            }));
414            signal
415        } else {
416            match &component_state.hooks[hook_index] {
417                Hook::Signal(hook) => hook
418                    .signal
419                    .as_ref()
420                    .downcast_ref::<Signal<T>>()
421                    .unwrap_or_else(|| panic!(
422                        "use_state_with_updater type mismatch at hook index {}; did you change the hook call order?",
423                        hook_index
424                    ))
425                    .clone(),
426            }
427        };
428
429        ctx.hook_index += 1;
430        signal
431    });
432
433    let value = signal.get();
434    let updater = {
435        let signal = signal.clone();
436        move |f: Box<dyn FnOnce(&mut T)>| {
437            signal.update(|val| f(val));
438        }
439    };
440    let setter = {
441        let signal = signal.clone();
442        move |new_value: T| {
443            signal.set(new_value);
444        }
445    };
446
447    (value, updater, setter)
448}
449
450/// Backwards-compatible hook returning a raw signal handle.
451pub fn use_signal<T: Clone + 'static>(initial_value: T) -> Signal<T> {
452    let signal = COMPONENT_CONTEXT.with(|ctx| {
453        let mut ctx = ctx.borrow_mut();
454        let component_id = ctx
455            .current_component
456            .expect("use_signal called outside of a component render");
457        let hook_index = ctx.hook_index;
458
459        let component_state = ctx
460            .components
461            .get_mut(&component_id)
462            .expect("component state missing during use_signal call");
463
464        let signal = if hook_index >= component_state.hooks.len() {
465            let signal = Signal::new(initial_value);
466            component_state.hooks.push(Hook::Signal(SignalHook {
467                signal: Box::new(signal.clone()),
468            }));
469            signal
470        } else {
471            match &component_state.hooks[hook_index] {
472                Hook::Signal(hook) => hook
473                    .signal
474                    .as_ref()
475                    .downcast_ref::<Signal<T>>()
476                    .unwrap_or_else(|| panic!(
477                        "use_signal type mismatch at hook index {}; did you change the hook call order?",
478                        hook_index
479                    ))
480                    .clone(),
481            }
482        };
483
484        ctx.hook_index += 1;
485        signal
486    });
487
488    signal
489}
490
491/// Simple component function type (accepts closures)
492pub type ComponentFn = std::rc::Rc<dyn Fn() -> VNode>;
493
494/// Register a component and get its ID
495pub fn register_component<F>(render_fn: F) -> ComponentId
496where
497    F: Fn() -> VNode + 'static,
498{
499    COMPONENT_CONTEXT.with(|ctx| {
500        let mut ctx = ctx.borrow_mut();
501        let id = ctx.new_component_id();
502        ctx.components.insert(
503            id,
504            ComponentState {
505                hooks: Vec::new(),
506                render_fn: std::rc::Rc::new(render_fn),
507            },
508        );
509        id
510    })
511}
512
513/// Render a component by ID
514pub fn render_component(id: ComponentId) -> VNode {
515    // First, set up the component context
516    COMPONENT_CONTEXT.with(|ctx| {
517        let mut ctx = ctx.borrow_mut();
518        ctx.enter_component(id);
519    });
520
521    // Get the render function (need to clone to avoid borrow conflicts)
522    let render_fn = COMPONENT_CONTEXT.with(|ctx| {
523        let ctx = ctx.borrow();
524        ctx.components
525            .get(&id)
526            .expect("Component not found")
527            .render_fn
528            .clone()
529    });
530
531    // Call the render function without holding any borrows
532    let result = render_fn();
533
534    // Clean up the component context
535    COMPONENT_CONTEXT.with(|ctx| {
536        let mut ctx = ctx.borrow_mut();
537        ctx.exit_component();
538    });
539
540    result
541}
542
543/// Renderer abstraction used by rsx-web
544pub trait Renderer {
545    /// Mount (first paint) and return an opaque handle.
546    type Handle;
547    fn mount(&mut self, root_selector: &str, vnode: &VNode) -> Self::Handle;
548    fn patch(&mut self, handle: &mut Self::Handle, prev: &VNode, next: &VNode);
549}
550
551/// A simple HTML string renderer for SSR/testing.
552pub fn render_to_string(node: &VNode) -> String {
553    use std::fmt::Write;
554
555    fn esc(s: &str) -> String {
556        s.replace('&', "&amp;")
557            .replace('<', "&lt;")
558            .replace('>', "&gt;")
559            .replace('"', "&quot;")
560            .replace('\'', "&#39;")
561    }
562
563    fn walk(n: &VNode, w: &mut String) {
564        match n {
565            VNode::Text(t) => {
566                let _ = write!(w, "{}", esc(t));
567            }
568            VNode::Fragment(cs) => {
569                for c in cs {
570                    walk(c, w)
571                }
572            }
573            VNode::Element(el) => {
574                let _ = write!(w, "<{}", el.tag);
575                for (k, v) in &el.props {
576                    if matches!(v, PropValue::Callback(_) | PropValue::EventHandler(_)) {
577                        continue;
578                    }
579                    let val = match v {
580                        PropValue::Str(s) => esc(s),
581                        PropValue::Bool(b) => b.to_string(),
582                        PropValue::Num(n) => format!("{}", n),
583                        PropValue::Dataset(ds) => {
584                            // emit as data-k="v"
585                            for (dk, dv) in ds {
586                                let _ = write!(w, " data-{}=\"{}\"", dk, esc(dv));
587                            }
588                            continue;
589                        }
590                        PropValue::Callback(_) | PropValue::EventHandler(_) => unreachable!(),
591                    };
592                    let _ = write!(w, " {}=\"{}\"", k, val);
593                }
594                if el.children.is_empty() {
595                    let _ = write!(w, "/>");
596                    return;
597                }
598                let _ = write!(w, ">");
599                for c in &el.children {
600                    walk(c, w);
601                }
602                let _ = write!(w, "</{}>", el.tag);
603            }
604        }
605    }
606
607    let mut s = String::new();
608    walk(node, &mut s);
609    s
610}
611
612// Conversions to allow `{expr}` inside RSX to coerce.
613
614impl From<&str> for VNode {
615    fn from(s: &str) -> Self {
616        text(s)
617    }
618}
619
620impl From<String> for VNode {
621    fn from(s: String) -> Self {
622        text(s)
623    }
624}
625
626impl From<usize> for VNode {
627    fn from(n: usize) -> Self {
628        text(n.to_string())
629    }
630}
631
632impl From<i64> for VNode {
633    fn from(n: i64) -> Self {
634        text(n.to_string())
635    }
636}
637
638impl From<f64> for VNode {
639    fn from(n: f64) -> Self {
640        text(n.to_string())
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647
648    #[test]
649    fn test_vnode_creation() {
650        let node = text("Hello World");
651        assert_eq!(node, VNode::Text("Hello World".to_string()));
652    }
653
654    #[test]
655    fn test_element_creation() {
656        let props = IndexMap::new();
657        let children = vec![text("Hello")];
658        let node = h("div", props, children);
659
660        match node {
661            VNode::Element(el) => {
662                assert_eq!(el.tag, "div");
663                assert_eq!(el.children.len(), 1);
664            }
665            _ => panic!("Expected element"),
666        }
667    }
668
669    #[test]
670    fn test_render_to_string() {
671        let props = IndexMap::new();
672        let children = vec![text("Hello World")];
673        let node = h("div", props, children);
674
675        let html = render_to_string(&node);
676        assert_eq!(html, "<div>Hello World</div>");
677    }
678
679    #[test]
680    fn test_signal() {
681        let signal = Signal::new(42);
682        assert_eq!(signal.get(), 42);
683
684        signal.set(100);
685        assert_eq!(signal.get(), 100);
686    }
687
688    #[test]
689    fn test_signal_with() {
690        let signal = Signal::new("hello".to_string());
691        let result = signal.with(|s| s.len());
692        assert_eq!(result, 5);
693    }
694}