1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use std::{any::Any, cell::RefCell, rc::Rc};
4
5pub 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), Dataset(IndexMap<String, String>),
35 EventHandler(Rc<RefCell<dyn FnMut()>>), }
37
38impl 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
54impl 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, _ => false,
65 }
66 }
67}
68
69impl 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
86impl<'de> Deserialize<'de> for PropValue {
88 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89 where
90 D: serde::Deserializer<'de>,
91 {
92 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
118pub fn on_click<F>(handler: F) -> PropValue
120where
121 F: FnMut() + 'static,
122{
123 PropValue::EventHandler(Rc::new(RefCell::new(handler)))
124}
125
126pub fn on_event<F>(handler: F) -> PropValue
128where
129 F: FnMut() + 'static,
130{
131 PropValue::EventHandler(Rc::new(RefCell::new(handler)))
132}
133
134pub 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
202thread_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
239thread_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
287pub 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 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
339pub 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 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
393pub 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
450pub 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
491pub type ComponentFn = std::rc::Rc<dyn Fn() -> VNode>;
493
494pub 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
513pub fn render_component(id: ComponentId) -> VNode {
515 COMPONENT_CONTEXT.with(|ctx| {
517 let mut ctx = ctx.borrow_mut();
518 ctx.enter_component(id);
519 });
520
521 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 let result = render_fn();
533
534 COMPONENT_CONTEXT.with(|ctx| {
536 let mut ctx = ctx.borrow_mut();
537 ctx.exit_component();
538 });
539
540 result
541}
542
543pub trait Renderer {
545 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
551pub fn render_to_string(node: &VNode) -> String {
553 use std::fmt::Write;
554
555 fn esc(s: &str) -> String {
556 s.replace('&', "&")
557 .replace('<', "<")
558 .replace('>', ">")
559 .replace('"', """)
560 .replace('\'', "'")
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 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
612impl 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}