use smol_str::SmolStr;
use std::any::Any;
use std::cell::{ Cell, RefCell };
use std::collections::{ HashMap, HashSet };
use std::marker::PhantomData;
use std::rc::Rc;
use crate::RedrawRequester;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ComponentId(SmolStr);
impl ComponentId {
pub fn root() -> Self {
Self(SmolStr::new("root"))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Clone, Debug)]
pub struct ComponentKey(SmolStr);
impl From<&str> for ComponentKey {
fn from(v: &str) -> Self {
Self(SmolStr::new(v))
}
}
impl From<String> for ComponentKey {
fn from(v: String) -> Self {
Self(SmolStr::new(v))
}
}
impl From<SmolStr> for ComponentKey {
fn from(v: SmolStr) -> Self {
Self(v)
}
}
macro_rules! impl_component_key_from_int {
($($t:ty),*) => {
$(
impl From<$t> for ComponentKey {
fn from(v: $t) -> Self {
Self(SmolStr::new(v.to_string()))
}
}
)*
};
}
impl_component_key_from_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
struct ComponentState {
slots: Vec<Rc<RefCell<Box<dyn Any>>>>,
cursor: usize,
}
impl ComponentState {
fn new() -> Self {
Self { slots: Vec::new(), cursor: 0 }
}
}
thread_local! {
static HOOK_STORE: RefCell<HashMap<ComponentId, ComponentState>> = RefCell::new(HashMap::new());
static COMPONENT_STACK: RefCell<Vec<ComponentId>> = const { RefCell::new(Vec::new()) };
static LIVE_COMPONENTS: RefCell<HashSet<ComponentId>> = RefCell::new(HashSet::new());
static DIRTY: Cell<bool> = const { Cell::new(false) };
static REDRAW_HANDLE: RefCell<Option<Rc<dyn RedrawRequester>>> = const { RefCell::new(None) };
static RENDER_GENERATION: Cell<u64> = const { Cell::new(0) };
static PENDING_EFFECTS: RefCell<Vec<PendingEffect>> = const { RefCell::new(Vec::new()) };
}
pub fn begin_render() {
RENDER_GENERATION.with(|g| g.set(g.get() + 1));
LIVE_COMPONENTS.with(|s| s.borrow_mut().clear());
COMPONENT_STACK.with(|s| {
let mut s = s.borrow_mut();
debug_assert!(
s.is_empty(),
"xengui hooks: component stack is not empty - begin_render/end_render may have been called unevenly"
);
s.clear();
});
}
pub fn end_render() {
LIVE_COMPONENTS.with(|live| {
let live = live.borrow();
HOOK_STORE.with(|store| {
store.borrow_mut().retain(|id, state| {
let keep = live.contains(id);
if !keep {
run_unmount_cleanups(state);
}
keep
});
});
});
}
fn run_unmount_cleanups(state: &ComponentState) {
for slot in &state.slots {
let cleanup = slot
.borrow_mut()
.downcast_mut::<EffectRecord>()
.and_then(|record| record.cleanup.take());
if let Some(cleanup) = cleanup {
cleanup();
}
}
}
pub fn take_dirty() -> bool {
DIRTY.with(|d| d.replace(false))
}
pub fn set_redraw_handle(handle: Rc<dyn RedrawRequester>) {
REDRAW_HANDLE.with(|h| {
*h.borrow_mut() = Some(handle);
});
}
fn request_redraw() {
REDRAW_HANDLE.with(|h| {
if let Some(handle) = h.borrow().as_ref() {
handle.request_redraw();
}
});
}
fn current_component_id() -> ComponentId {
COMPONENT_STACK.with(|s| {
s.borrow()
.last()
.cloned()
.unwrap_or_else(|| {
panic!(
"use_state: called outside a component() scope. \
use_state can only be used within App::render's root function or \
inside a component(key, ...) scope."
)
})
})
}
fn push_component(key: ComponentKey) -> ComponentId {
let id = COMPONENT_STACK.with(|s| {
match s.borrow().last() {
Some(parent) =>
ComponentId(SmolStr::new(format!("{}\u{1f}{}", parent.as_str(), key.0))),
None => ComponentId(key.0),
}
});
HOOK_STORE.with(|store| {
let mut store = store.borrow_mut();
let state = store.entry(id.clone()).or_insert_with(ComponentState::new);
state.cursor = 0;
});
let first_time_this_frame = LIVE_COMPONENTS.with(|s| s.borrow_mut().insert(id.clone()));
if !first_time_this_frame {
log::warn!(
"xengui: duplicate component key '{}' - used twice in the same frame. \
In dynamic lists, give each item a unique key (like React's 'key' prop).",
id.as_str()
);
}
COMPONENT_STACK.with(|s| s.borrow_mut().push(id.clone()));
id
}
fn pop_component() {
COMPONENT_STACK.with(|s| {
s.borrow_mut().pop();
});
}
pub fn component<R>(key: impl Into<ComponentKey>, render: impl FnOnce() -> R) -> R {
push_component(key.into());
let result = render();
pop_component();
result
}
pub fn use_state<T: Clone + 'static>(initial: T) -> (T, SetState<T>) {
let id = current_component_id();
let (slot, idx) = HOOK_STORE.with(|store| {
let mut store = store.borrow_mut();
let state = store
.get_mut(&id)
.expect("use_state: internal error - provided binding used without begin/push");
let idx = state.cursor;
state.cursor += 1;
if idx == state.slots.len() {
state.slots.push(Rc::new(RefCell::new(Box::new(initial) as Box<dyn Any>)));
}
(state.slots[idx].clone(), idx)
});
let value = {
let borrowed = slot.borrow();
borrowed
.downcast_ref::<T>()
.unwrap_or_else(|| {
panic!(
"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.",
id.as_str()
)
})
.clone()
};
(
value,
SetState {
slot,
_marker: PhantomData,
},
)
}
pub struct SetState<T> {
slot: Rc<RefCell<Box<dyn Any>>>,
_marker: PhantomData<T>,
}
impl<T> Clone for SetState<T> {
fn clone(&self) -> Self {
Self {
slot: self.slot.clone(),
_marker: PhantomData,
}
}
}
impl<T: 'static> SetState<T> {
pub fn set(&self, value: T) {
*self.slot.borrow_mut() = Box::new(value);
DIRTY.with(|d| d.set(true));
request_redraw();
}
pub fn update(&self, f: impl FnOnce(&mut T)) {
{
let mut borrowed = self.slot.borrow_mut();
let current = borrowed
.downcast_mut::<T>()
.expect("use_state: SetState<T> used with the wrong type");
f(current);
}
DIRTY.with(|d| d.set(true));
request_redraw();
}
}
pub fn mark_dirty_and_redraw() {
DIRTY.with(|d| d.set(true));
request_redraw();
}
fn current_generation() -> u64 {
RENDER_GENERATION.with(Cell::get)
}
trait StoredDeps: Any {
fn eq_dyn(&self, other: &dyn StoredDeps) -> bool;
fn as_any(&self) -> &dyn Any;
}
impl<T: PartialEq + 'static> StoredDeps for T {
fn eq_dyn(&self, other: &dyn StoredDeps) -> bool {
other
.as_any()
.downcast_ref::<T>()
.is_some_and(|o| self == o)
}
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct DepsSnapshot(Box<dyn StoredDeps>);
fn deps_changed(old: &DepsSnapshot, new: &DepsSnapshot) -> bool {
!old.0.eq_dyn(new.0.as_ref())
}
pub trait EffectDeps {
fn snapshot(self) -> DepsSnapshot;
}
impl EffectDeps for () {
fn snapshot(self) -> DepsSnapshot {
DepsSnapshot(Box::new(()))
}
}
impl<T: PartialEq + 'static, const N: usize> EffectDeps for [T; N] {
fn snapshot(self) -> DepsSnapshot {
DepsSnapshot(Box::new(self))
}
}
impl<T: PartialEq + Clone + 'static, const N: usize> EffectDeps for &[T; N] {
fn snapshot(self) -> DepsSnapshot {
DepsSnapshot(Box::new(self.clone()))
}
}
impl<T: PartialEq + Clone + 'static> EffectDeps for &[T] {
fn snapshot(self) -> DepsSnapshot {
DepsSnapshot(Box::new(self.to_vec()))
}
}
pub trait EffectCleanup {
fn into_cleanup(self) -> Option<Box<dyn FnOnce()>>;
}
impl EffectCleanup for () {
fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
None
}
}
impl<F: FnOnce() + 'static> EffectCleanup for F {
fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
Some(Box::new(self))
}
}
struct EffectRecord {
deps: Option<DepsSnapshot>,
cleanup: Option<Box<dyn FnOnce()>>,
mounted: bool,
pending: bool,
}
type BoxedEffectFn = Box<dyn FnOnce() -> Option<Box<dyn FnOnce()>>>;
struct PendingEffect {
slot: Rc<RefCell<Box<dyn Any>>>,
new_deps: DepsSnapshot,
run: BoxedEffectFn,
generation: u64,
}
pub fn use_effect<F, R, D>(effect: F, deps: D)
where F: FnOnce() -> R + 'static, R: EffectCleanup + 'static, D: EffectDeps
{
let id = current_component_id();
let new_deps = deps.snapshot();
let (slot, idx) = HOOK_STORE.with(|store| {
let mut store = store.borrow_mut();
let state = store
.get_mut(&id)
.expect("use_effect: internal error - provided binding used without begin/push");
let idx = state.cursor;
state.cursor += 1;
if idx == state.slots.len() {
state.slots.push(
Rc::new(
RefCell::new(
Box::new(EffectRecord {
deps: None,
cleanup: None,
mounted: false,
pending: false,
}) as Box<dyn Any>
)
)
);
}
(state.slots[idx].clone(), idx)
});
let should_run = {
let borrowed = slot.borrow();
let record = borrowed
.downcast_ref::<EffectRecord>()
.unwrap_or_else(|| {
panic!(
"use_effect: hook order broken in component '{}' (slot #{idx}) - do not call use_effect conditionally.",
id.as_str()
)
});
match &record.deps {
None => true,
Some(old_deps) => deps_changed(old_deps, &new_deps),
}
};
if !should_run {
return;
}
slot
.borrow_mut()
.downcast_mut::<EffectRecord>()
.expect("use_effect: internal error").pending = true;
let run: BoxedEffectFn = Box::new(move || effect().into_cleanup());
PENDING_EFFECTS.with(|q| {
q.borrow_mut().push(PendingEffect {
slot,
new_deps,
run,
generation: current_generation(),
});
});
}
pub fn run_pending_effects() {
let generation = current_generation();
let pending = PENDING_EFFECTS.with(|q| std::mem::take(&mut *q.borrow_mut()));
for entry in pending {
if entry.generation != generation {
continue;
}
let old_cleanup = {
let mut boxed = entry.slot.borrow_mut();
let record = boxed.downcast_mut::<EffectRecord>().expect("use_effect: internal error");
record.pending = false;
record.cleanup.take()
};
if let Some(cleanup) = old_cleanup {
cleanup();
}
let new_cleanup = (entry.run)();
let mut boxed = entry.slot.borrow_mut();
let record = boxed.downcast_mut::<EffectRecord>().expect("use_effect: internal error");
record.deps = Some(entry.new_deps);
record.cleanup = new_cleanup;
record.mounted = true;
}
}
#[cfg(test)]
mod effect_tests {
use super::*;
#[test]
fn runs_once_on_mount_and_skips_unchanged_deps() {
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let build = || {
component("effect_mount_root", || {
let log = log.clone();
use_effect(move || {
log.borrow_mut().push("mount".to_string());
}, ());
});
};
begin_render();
build();
end_render();
run_pending_effects();
begin_render();
build();
end_render();
run_pending_effects();
assert_eq!(*log.borrow(), vec!["mount".to_string()]);
}
#[test]
fn reruns_when_deps_change() {
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let build = |value: i32| {
component("effect_deps_root", || {
let log = log.clone();
use_effect(
move || {
log.borrow_mut().push(format!("run:{value}"));
},
[value]
);
});
};
begin_render();
build(1);
end_render();
run_pending_effects();
begin_render();
build(1);
end_render();
run_pending_effects();
begin_render();
build(2);
end_render();
run_pending_effects();
assert_eq!(*log.borrow(), vec!["run:1".to_string(), "run:2".to_string()]);
}
#[test]
fn cleanup_runs_before_rerun_and_on_unmount() {
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let build_child = |value: i32| {
component("effect_cleanup_child", || {
let log = log.clone();
use_effect(
move || {
log.borrow_mut().push(format!("run:{value}"));
move || {
log.borrow_mut().push(format!("cleanup:{value}"));
}
},
[value]
);
});
};
begin_render();
component("effect_cleanup_root", || build_child(1));
end_render();
run_pending_effects();
begin_render();
component("effect_cleanup_root", || build_child(2));
end_render();
run_pending_effects();
assert_eq!(
*log.borrow(),
vec!["run:1".to_string(), "cleanup:1".to_string(), "run:2".to_string()]
);
begin_render();
component("effect_cleanup_root", || {});
end_render();
run_pending_effects();
assert_eq!(
*log.borrow(),
vec![
"run:1".to_string(),
"cleanup:1".to_string(),
"run:2".to_string(),
"cleanup:2".to_string()
]
);
}
}