1use smol_str::SmolStr;
3use std::any::Any;
4use std::cell::{ Cell, RefCell };
5use std::collections::{ HashMap, HashSet };
6use std::marker::PhantomData;
7use std::rc::Rc;
8use crate::RedrawRequester;
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11pub struct ComponentId(SmolStr);
12
13impl ComponentId {
14 pub fn root() -> Self {
15 Self(SmolStr::new("root"))
16 }
17
18 pub fn as_str(&self) -> &str {
19 self.0.as_str()
20 }
21}
22
23#[derive(Clone, Debug)]
24pub struct ComponentKey(SmolStr);
25
26impl From<&str> for ComponentKey {
27 fn from(v: &str) -> Self {
28 Self(SmolStr::new(v))
29 }
30}
31
32impl From<String> for ComponentKey {
33 fn from(v: String) -> Self {
34 Self(SmolStr::new(v))
35 }
36}
37
38impl From<SmolStr> for ComponentKey {
39 fn from(v: SmolStr) -> Self {
40 Self(v)
41 }
42}
43
44macro_rules! impl_component_key_from_int {
45 ($($t:ty),*) => {
46 $(
47 impl From<$t> for ComponentKey {
48 fn from(v: $t) -> Self {
49 Self(SmolStr::new(v.to_string()))
50 }
51 }
52 )*
53 };
54}
55impl_component_key_from_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
56
57struct ComponentState {
58 slots: Vec<Rc<RefCell<Box<dyn Any>>>>,
59 cursor: usize,
60}
61
62impl ComponentState {
63 fn new() -> Self {
64 Self { slots: Vec::new(), cursor: 0 }
65 }
66}
67
68thread_local! {
69 static HOOK_STORE: RefCell<HashMap<ComponentId, ComponentState>> = RefCell::new(HashMap::new());
70
71 static COMPONENT_STACK: RefCell<Vec<ComponentId>> = const { RefCell::new(Vec::new()) };
72
73 static LIVE_COMPONENTS: RefCell<HashSet<ComponentId>> = RefCell::new(HashSet::new());
74
75 static DIRTY: Cell<bool> = const { Cell::new(false) };
76
77 static REDRAW_HANDLE: RefCell<Option<Rc<dyn RedrawRequester>>> = const { RefCell::new(None) };
78
79 static RENDER_GENERATION: Cell<u64> = const { Cell::new(0) };
80
81 static PENDING_EFFECTS: RefCell<Vec<PendingEffect>> = const { RefCell::new(Vec::new()) };
82}
83
84pub fn begin_render() {
85 RENDER_GENERATION.with(|g| g.set(g.get() + 1));
88 LIVE_COMPONENTS.with(|s| s.borrow_mut().clear());
89 COMPONENT_STACK.with(|s| {
90 let mut s = s.borrow_mut();
91 debug_assert!(
92 s.is_empty(),
93 "xengui hooks: component stack is not empty - begin_render/end_render may have been called unevenly"
94 );
95 s.clear();
96 });
97}
98
99pub fn end_render() {
100 LIVE_COMPONENTS.with(|live| {
101 let live = live.borrow();
102 HOOK_STORE.with(|store| {
103 store.borrow_mut().retain(|id, state| {
104 let keep = live.contains(id);
105 if !keep {
106 run_unmount_cleanups(state);
107 }
108 keep
109 });
110 });
111 });
112}
113
114fn run_unmount_cleanups(state: &ComponentState) {
117 for slot in &state.slots {
118 let cleanup = slot
119 .borrow_mut()
120 .downcast_mut::<EffectRecord>()
121 .and_then(|record| record.cleanup.take());
122
123 if let Some(cleanup) = cleanup {
124 cleanup();
125 }
126 }
127}
128
129pub fn take_dirty() -> bool {
130 DIRTY.with(|d| d.replace(false))
131}
132
133pub fn set_redraw_handle(handle: Rc<dyn RedrawRequester>) {
134 REDRAW_HANDLE.with(|h| {
135 *h.borrow_mut() = Some(handle);
136 });
137}
138
139fn request_redraw() {
140 REDRAW_HANDLE.with(|h| {
141 if let Some(handle) = h.borrow().as_ref() {
142 handle.request_redraw();
143 }
144 });
145}
146
147fn current_component_id() -> ComponentId {
148 COMPONENT_STACK.with(|s| {
149 s.borrow()
150 .last()
151 .cloned()
152 .unwrap_or_else(|| {
153 panic!(
154 "use_state: called outside a component() scope. \
155 use_state can only be used within App::render's root function or \
156 inside a component(key, ...) scope."
157 )
158 })
159 })
160}
161
162fn push_component(key: ComponentKey) -> ComponentId {
163 let id = COMPONENT_STACK.with(|s| {
164 match s.borrow().last() {
165 Some(parent) =>
166 ComponentId(SmolStr::new(format!("{}\u{1f}{}", parent.as_str(), key.0))),
167 None => ComponentId(key.0),
168 }
169 });
170
171 HOOK_STORE.with(|store| {
172 let mut store = store.borrow_mut();
173 let state = store.entry(id.clone()).or_insert_with(ComponentState::new);
174 state.cursor = 0;
175 });
176
177 let first_time_this_frame = LIVE_COMPONENTS.with(|s| s.borrow_mut().insert(id.clone()));
178 if !first_time_this_frame {
179 log::warn!(
180 "xengui: duplicate component key '{}' - used twice in the same frame. \
181 In dynamic lists, give each item a unique key (like React's 'key' prop).",
182 id.as_str()
183 );
184 }
185
186 COMPONENT_STACK.with(|s| s.borrow_mut().push(id.clone()));
187 id
188}
189
190fn pop_component() {
191 COMPONENT_STACK.with(|s| {
192 s.borrow_mut().pop();
193 });
194}
195
196pub fn component<R>(key: impl Into<ComponentKey>, render: impl FnOnce() -> R) -> R {
210 push_component(key.into());
211 let result = render();
212 pop_component();
213 result
214}
215
216pub fn use_state<T: Clone + 'static>(initial: T) -> (T, SetState<T>) {
243 let id = current_component_id();
244
245 let (slot, idx) = HOOK_STORE.with(|store| {
246 let mut store = store.borrow_mut();
247 let state = store
248 .get_mut(&id)
249 .expect("use_state: internal error - provided binding used without begin/push");
250
251 let idx = state.cursor;
252 state.cursor += 1;
253
254 if idx == state.slots.len() {
255 state.slots.push(Rc::new(RefCell::new(Box::new(initial) as Box<dyn Any>)));
256 }
257
258 (state.slots[idx].clone(), idx)
259 });
260
261 let value = {
262 let borrowed = slot.borrow();
263 borrowed
264 .downcast_ref::<T>()
265 .unwrap_or_else(|| {
266 panic!(
267 "use_state: hook order broken in component '{}' (slot #{idx}) - do not call use_state conditionally (inside an if/loop). In dynamic lists, wrap each item in a component (e.g., component(key, ...)) to give it its own isolated hook order.",
268 id.as_str()
269 )
270 })
271 .clone()
272 };
273
274 (
275 value,
276 SetState {
277 slot,
278 _marker: PhantomData,
279 },
280 )
281}
282
283pub struct SetState<T> {
284 slot: Rc<RefCell<Box<dyn Any>>>,
285 _marker: PhantomData<T>,
286}
287
288impl<T> Clone for SetState<T> {
289 fn clone(&self) -> Self {
290 Self {
291 slot: self.slot.clone(),
292 _marker: PhantomData,
293 }
294 }
295}
296
297impl<T: 'static> SetState<T> {
298 pub fn set(&self, value: T) {
299 *self.slot.borrow_mut() = Box::new(value);
300 DIRTY.with(|d| d.set(true));
301 request_redraw();
302 }
303
304 pub fn update(&self, f: impl FnOnce(&mut T)) {
305 {
306 let mut borrowed = self.slot.borrow_mut();
307 let current = borrowed
308 .downcast_mut::<T>()
309 .expect("use_state: SetState<T> used with the wrong type");
310
311 f(current);
312 }
313 DIRTY.with(|d| d.set(true));
314 request_redraw();
315 }
316}
317
318pub fn mark_dirty_and_redraw() {
319 DIRTY.with(|d| d.set(true));
320 request_redraw();
321}
322
323fn current_generation() -> u64 {
324 RENDER_GENERATION.with(Cell::get)
325}
326
327trait StoredDeps: Any {
331 fn eq_dyn(&self, other: &dyn StoredDeps) -> bool;
332 fn as_any(&self) -> &dyn Any;
333}
334
335impl<T: PartialEq + 'static> StoredDeps for T {
336 fn eq_dyn(&self, other: &dyn StoredDeps) -> bool {
337 other
338 .as_any()
339 .downcast_ref::<T>()
340 .is_some_and(|o| self == o)
341 }
342
343 fn as_any(&self) -> &dyn Any {
344 self
345 }
346}
347
348pub struct DepsSnapshot(Box<dyn StoredDeps>);
351
352fn deps_changed(old: &DepsSnapshot, new: &DepsSnapshot) -> bool {
353 !old.0.eq_dyn(new.0.as_ref())
354}
355
356pub trait EffectDeps {
360 fn snapshot(self) -> DepsSnapshot;
361}
362
363impl EffectDeps for () {
364 fn snapshot(self) -> DepsSnapshot {
365 DepsSnapshot(Box::new(()))
366 }
367}
368
369impl<T: PartialEq + 'static, const N: usize> EffectDeps for [T; N] {
370 fn snapshot(self) -> DepsSnapshot {
371 DepsSnapshot(Box::new(self))
372 }
373}
374
375impl<T: PartialEq + Clone + 'static, const N: usize> EffectDeps for &[T; N] {
376 fn snapshot(self) -> DepsSnapshot {
377 DepsSnapshot(Box::new(self.clone()))
378 }
379}
380
381impl<T: PartialEq + Clone + 'static> EffectDeps for &[T] {
382 fn snapshot(self) -> DepsSnapshot {
383 DepsSnapshot(Box::new(self.to_vec()))
384 }
385}
386
387pub trait EffectCleanup {
391 fn into_cleanup(self) -> Option<Box<dyn FnOnce()>>;
392}
393
394impl EffectCleanup for () {
395 fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
396 None
397 }
398}
399
400impl<F: FnOnce() + 'static> EffectCleanup for F {
401 fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
402 Some(Box::new(self))
403 }
404}
405
406struct EffectRecord {
407 deps: Option<DepsSnapshot>,
408 cleanup: Option<Box<dyn FnOnce()>>,
409 mounted: bool,
410 pending: bool,
411}
412
413type BoxedEffectFn = Box<dyn FnOnce() -> Option<Box<dyn FnOnce()>>>;
414
415struct PendingEffect {
416 slot: Rc<RefCell<Box<dyn Any>>>,
417 new_deps: DepsSnapshot,
418 run: BoxedEffectFn,
419 generation: u64,
420}
421
422pub fn use_effect<F, R, D>(effect: F, deps: D)
435 where F: FnOnce() -> R + 'static, R: EffectCleanup + 'static, D: EffectDeps
436{
437 let id = current_component_id();
438 let new_deps = deps.snapshot();
439
440 let (slot, idx) = HOOK_STORE.with(|store| {
441 let mut store = store.borrow_mut();
442 let state = store
443 .get_mut(&id)
444 .expect("use_effect: internal error - provided binding used without begin/push");
445
446 let idx = state.cursor;
447 state.cursor += 1;
448
449 if idx == state.slots.len() {
450 state.slots.push(
451 Rc::new(
452 RefCell::new(
453 Box::new(EffectRecord {
454 deps: None,
455 cleanup: None,
456 mounted: false,
457 pending: false,
458 }) as Box<dyn Any>
459 )
460 )
461 );
462 }
463
464 (state.slots[idx].clone(), idx)
465 });
466
467 let should_run = {
468 let borrowed = slot.borrow();
469 let record = borrowed
470 .downcast_ref::<EffectRecord>()
471 .unwrap_or_else(|| {
472 panic!(
473 "use_effect: hook order broken in component '{}' (slot #{idx}) - do not call use_effect conditionally.",
474 id.as_str()
475 )
476 });
477
478 match &record.deps {
479 None => true,
480 Some(old_deps) => deps_changed(old_deps, &new_deps),
481 }
482 };
483
484 if !should_run {
485 return;
486 }
487
488 slot
489 .borrow_mut()
490 .downcast_mut::<EffectRecord>()
491 .expect("use_effect: internal error").pending = true;
492
493 let run: BoxedEffectFn = Box::new(move || effect().into_cleanup());
494
495 PENDING_EFFECTS.with(|q| {
496 q.borrow_mut().push(PendingEffect {
497 slot,
498 new_deps,
499 run,
500 generation: current_generation(),
501 });
502 });
503}
504
505pub fn run_pending_effects() {
510 let generation = current_generation();
511 let pending = PENDING_EFFECTS.with(|q| std::mem::take(&mut *q.borrow_mut()));
512
513 for entry in pending {
514 if entry.generation != generation {
518 continue;
519 }
520
521 let old_cleanup = {
522 let mut boxed = entry.slot.borrow_mut();
523 let record = boxed.downcast_mut::<EffectRecord>().expect("use_effect: internal error");
524 record.pending = false;
525 record.cleanup.take()
526 };
527
528 if let Some(cleanup) = old_cleanup {
529 cleanup();
530 }
531
532 let new_cleanup = (entry.run)();
533
534 let mut boxed = entry.slot.borrow_mut();
535 let record = boxed.downcast_mut::<EffectRecord>().expect("use_effect: internal error");
536 record.deps = Some(entry.new_deps);
537 record.cleanup = new_cleanup;
538 record.mounted = true;
539 }
540}
541
542#[cfg(test)]
543mod effect_tests {
544 use super::*;
545
546 #[test]
547 fn runs_once_on_mount_and_skips_unchanged_deps() {
548 let log = Rc::new(RefCell::new(Vec::<String>::new()));
549
550 let build = || {
551 component("effect_mount_root", || {
552 let log = log.clone();
553 use_effect(move || {
554 log.borrow_mut().push("mount".to_string());
555 }, ());
556 });
557 };
558
559 begin_render();
560 build();
561 end_render();
562 run_pending_effects();
563
564 begin_render();
565 build();
566 end_render();
567 run_pending_effects();
568
569 assert_eq!(*log.borrow(), vec!["mount".to_string()]);
570 }
571
572 #[test]
573 fn reruns_when_deps_change() {
574 let log = Rc::new(RefCell::new(Vec::<String>::new()));
575
576 let build = |value: i32| {
577 component("effect_deps_root", || {
578 let log = log.clone();
579 use_effect(
580 move || {
581 log.borrow_mut().push(format!("run:{value}"));
582 },
583 [value]
584 );
585 });
586 };
587
588 begin_render();
589 build(1);
590 end_render();
591 run_pending_effects();
592
593 begin_render();
594 build(1);
595 end_render();
596 run_pending_effects();
597
598 begin_render();
599 build(2);
600 end_render();
601 run_pending_effects();
602
603 assert_eq!(*log.borrow(), vec!["run:1".to_string(), "run:2".to_string()]);
604 }
605
606 #[test]
607 fn cleanup_runs_before_rerun_and_on_unmount() {
608 let log = Rc::new(RefCell::new(Vec::<String>::new()));
609
610 let build_child = |value: i32| {
611 component("effect_cleanup_child", || {
612 let log = log.clone();
613 use_effect(
614 move || {
615 log.borrow_mut().push(format!("run:{value}"));
616 move || {
617 log.borrow_mut().push(format!("cleanup:{value}"));
618 }
619 },
620 [value]
621 );
622 });
623 };
624
625 begin_render();
626 component("effect_cleanup_root", || build_child(1));
627 end_render();
628 run_pending_effects();
629
630 begin_render();
631 component("effect_cleanup_root", || build_child(2));
632 end_render();
633 run_pending_effects();
634
635 assert_eq!(
636 *log.borrow(),
637 vec!["run:1".to_string(), "cleanup:1".to_string(), "run:2".to_string()]
638 );
639
640 begin_render();
643 component("effect_cleanup_root", || {});
644 end_render();
645 run_pending_effects();
646
647 assert_eq!(
648 *log.borrow(),
649 vec![
650 "run:1".to_string(),
651 "cleanup:1".to_string(),
652 "run:2".to_string(),
653 "cleanup:2".to_string()
654 ]
655 );
656 }
657}