use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
type ComponentId = usize;
thread_local! {
static NEXT_COMPONENT_ID: Cell<usize> = const { Cell::new(1) };
#[allow(clippy::type_complexity, clippy::missing_const_for_thread_local)]
static MOUNTED_HOOKS: RefCell<HashMap<ComponentId, Vec<Box<dyn FnOnce()>>>> = RefCell::new(HashMap::new());
#[allow(clippy::type_complexity, clippy::missing_const_for_thread_local)]
static DESTROY_HOOKS: RefCell<HashMap<ComponentId, Vec<Box<dyn FnOnce()>>>> = RefCell::new(HashMap::new());
#[allow(clippy::missing_const_for_thread_local)]
static CURRENT_COMPONENT: RefCell<Option<ComponentId>> = RefCell::new(None);
}
pub fn generate_component_id() -> ComponentId {
NEXT_COMPONENT_ID.with(|id| {
let current = id.get();
id.set(current.saturating_add(1));
current
})
}
pub fn set_current_component(id: ComponentId) {
CURRENT_COMPONENT.with(|c| {
if let Ok(mut guard) = c.try_borrow_mut() {
*guard = Some(id);
}
});
}
pub fn clear_current_component() {
CURRENT_COMPONENT.with(|c| {
if let Ok(mut guard) = c.try_borrow_mut() {
*guard = None;
}
});
}
pub fn current_component_id() -> Option<ComponentId> {
CURRENT_COMPONENT.with(|c| c.try_borrow().ok().and_then(|guard| *guard))
}
pub fn on_mounted(f: impl FnOnce() + 'static) {
if let Some(id) = current_component_id() {
MOUNTED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.entry(id).or_default().push(Box::new(f));
} else {
log::warn!(
"on_mounted: hook registry is borrowed; the hook was dropped \
instead of panicking. This indicates a hook running while \
the registry is held, which run paths never do."
);
}
});
} else {
log::warn!(
"on_mounted called without a current component context. \
Did you forget to call set_current_component()? The hook will not be registered."
);
}
}
pub fn run_mounted_hooks(id: ComponentId) {
let hooks = MOUNTED_HOOKS.with(|h| h.try_borrow_mut().ok().and_then(|mut map| map.remove(&id)));
if let Some(hooks) = hooks {
for hook in hooks {
hook();
}
}
}
pub fn before_destroy(f: impl FnOnce() + 'static) {
if let Some(id) = current_component_id() {
DESTROY_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.entry(id).or_default().push(Box::new(f));
} else {
log::warn!(
"before_destroy: hook registry is borrowed; the hook was \
dropped instead of panicking."
);
}
});
} else {
log::warn!(
"before_destroy called without a current component context. \
Did you forget to call set_current_component()? The hook will not be registered."
);
}
}
pub fn run_destroy_hooks(id: ComponentId) {
let hooks = DESTROY_HOOKS.with(|h| h.try_borrow_mut().ok().and_then(|mut map| map.remove(&id)));
if let Some(hooks) = hooks {
for hook in hooks {
hook();
}
}
}
pub fn on_unmounted(f: impl FnOnce() + 'static) {
before_destroy(f);
}
pub fn run_unmounted_hooks(id: ComponentId) {
run_destroy_hooks(id);
}
type UpdatedHook = dyn FnMut();
thread_local! {
#[allow(clippy::type_complexity, clippy::missing_const_for_thread_local)]
static UPDATED_HOOKS: RefCell<HashMap<ComponentId, Vec<Box<UpdatedHook>>>> = RefCell::new(HashMap::new());
}
pub fn on_updated(f: impl FnMut() + 'static) {
if let Some(id) = current_component_id() {
UPDATED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.entry(id).or_default().push(Box::new(f));
} else {
log::warn!(
"on_updated: hook registry is borrowed; the hook was \
dropped instead of panicking."
);
}
});
} else {
log::warn!(
"on_updated called without a current component context. \
Did you forget to call set_current_component()? The hook will not be registered."
);
}
}
pub fn run_updated_hooks(id: ComponentId) {
let Some(mut running) =
UPDATED_HOOKS.with(|h| h.try_borrow_mut().ok().and_then(|mut map| map.remove(&id)))
else {
return;
};
let len = running.len();
for i in 0..len {
if let Some(hook) = running.get_mut(i) {
hook();
}
}
UPDATED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
match map.get_mut(&id) {
Some(fresh) => {
let mut done = std::mem::take(&mut running);
done.append(fresh);
*fresh = done;
}
None => {
map.insert(id, running);
}
}
} else {
log::warn!(
"run_updated_hooks: registry is borrowed while merging hooks \
back; the component's updated hooks were dropped instead of \
panicking."
);
}
});
}
pub fn clear_updated_hooks(id: ComponentId) {
UPDATED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.remove(&id);
}
});
}
pub fn has_updated_hooks(id: ComponentId) -> bool {
UPDATED_HOOKS.with(|h| match h.try_borrow() {
Ok(map) => map.get(&id).is_some_and(|hooks| !hooks.is_empty()),
Err(_) => false,
})
}
type ResizeHook = dyn FnMut(u32, u32);
type ResizeHookRef = Rc<RefCell<ResizeHook>>;
thread_local! {
#[allow(clippy::type_complexity, clippy::missing_const_for_thread_local)]
static RESIZE_HOOKS: RefCell<HashMap<ComponentId, Vec<ResizeHookRef>>> = RefCell::new(HashMap::new());
}
pub fn on_resize(f: impl FnMut(u32, u32) + 'static) {
if let Some(id) = current_component_id() {
RESIZE_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.entry(id).or_default().push(Rc::new(RefCell::new(f)));
} else {
log::warn!(
"on_resize: hook registry is borrowed; the hook was \
dropped instead of panicking."
);
}
});
} else {
log::warn!(
"on_resize called without a current component context. \
Did you forget to call set_current_component()? The hook will not be registered."
);
}
}
pub fn run_resize_hooks(width: u32, height: u32) {
let hooks: Vec<ResizeHookRef> = RESIZE_HOOKS.with(|h| {
h.try_borrow()
.ok()
.map(|map| {
map.values()
.flat_map(|component_hooks| component_hooks.iter().cloned())
.collect()
})
.unwrap_or_default()
});
for hook in hooks {
if let Ok(mut callback) = hook.try_borrow_mut() {
callback(width, height);
}
}
}
pub fn run_all_resize_hooks(width: u32, height: u32) {
run_resize_hooks(width, height);
}
pub fn clear_resize_hooks(id: ComponentId) {
RESIZE_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.remove(&id);
}
});
}
pub fn has_resize_hooks(id: ComponentId) -> bool {
RESIZE_HOOKS.with(|h| match h.try_borrow() {
Ok(map) => map.get(&id).is_some_and(|hooks| !hooks.is_empty()),
Err(_) => false,
})
}
pub fn run_all_mounted_hooks() {
let ids: Vec<ComponentId> = MOUNTED_HOOKS.with(|h| {
h.try_borrow()
.ok()
.map(|map| map.keys().copied().collect())
.unwrap_or_default()
});
for id in ids {
run_mounted_hooks(id);
}
}
pub fn run_all_updated_hooks() {
let ids: Vec<ComponentId> = UPDATED_HOOKS.with(|h| {
h.try_borrow()
.ok()
.map(|map| map.keys().copied().collect())
.unwrap_or_default()
});
for id in ids {
run_updated_hooks(id);
}
}
pub fn run_all_destroy_hooks() {
let ids: Vec<ComponentId> = DESTROY_HOOKS.with(|h| {
h.try_borrow()
.ok()
.map(|map| map.keys().copied().collect())
.unwrap_or_default()
});
for id in ids {
run_destroy_hooks(id);
}
UPDATED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.clear();
}
});
RESIZE_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.clear();
}
});
MOUNTED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.clear();
}
});
}
pub fn cleanup_component(id: ComponentId) {
MOUNTED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.remove(&id);
}
});
DESTROY_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.remove(&id);
}
});
UPDATED_HOOKS.with(|h| {
if let Ok(mut map) = h.try_borrow_mut() {
map.remove(&id);
}
});
clear_resize_hooks(id);
}
pub struct LifecycleHandle {
id: ComponentId,
mounted: Cell<bool>,
}
impl LifecycleHandle {
#[must_use]
pub fn new() -> Self {
let id = generate_component_id();
set_current_component(id);
Self {
id,
mounted: Cell::new(false),
}
}
#[must_use]
pub fn with_id(id: ComponentId) -> Self {
Self {
id,
mounted: Cell::new(false),
}
}
pub fn id(&self) -> ComponentId {
self.id
}
pub fn mount(&self) {
if !self.mounted.get() {
self.mounted.set(true);
run_mounted_hooks(self.id);
}
}
pub fn is_mounted(&self) -> bool {
self.mounted.get()
}
}
impl Default for LifecycleHandle {
fn default() -> Self {
Self::new()
}
}
impl Drop for LifecycleHandle {
fn drop(&mut self) {
run_destroy_hooks(self.id);
cleanup_component(self.id);
}
}
#[macro_export]
macro_rules! on_mounted {
({ $($body:tt)* }) => {
$crate::lifecycle::on_mounted(move || { $($body)* })
};
($f:expr) => {
$crate::lifecycle::on_mounted($f)
};
}
#[macro_export]
macro_rules! on_updated {
({ $($body:tt)* }) => {
$crate::lifecycle::on_updated(move || { $($body)* })
};
($f:expr) => {
$crate::lifecycle::on_updated($f)
};
}
#[macro_export]
macro_rules! on_resize {
({ $($body:tt)* }) => {
$crate::lifecycle::on_resize(move |width, height| { $($body)* })
};
($f:expr) => {
$crate::lifecycle::on_resize($f)
};
}
#[macro_export]
macro_rules! on_unmounted {
({ $($body:tt)* }) => {
$crate::lifecycle::on_unmounted(move || { $($body)* })
};
($f:expr) => {
$crate::lifecycle::on_unmounted($f)
};
}