use super::context::current_context;
use crate::cmd::Cmd;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub trait Deps {
type Output;
fn deps_hash(&self) -> u64;
fn output(&self) -> Self::Output;
}
impl<T> Deps for T
where
T: Hash + Clone,
{
type Output = T;
fn deps_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
fn output(&self) -> Self::Output {
self.clone()
}
}
#[derive(Clone)]
struct CmdHookState {
deps_hash: u64,
is_first_render: bool,
}
pub fn use_cmd<D, F>(deps: D, f: F)
where
D: Deps + 'static,
F: FnOnce(D::Output) -> Cmd + 'static,
{
let Some(ctx) = current_context() else {
let _ = f(deps.output());
return;
};
let Ok(mut ctx_ref) = ctx.write() else {
let _ = f(deps.output());
return;
};
let new_hash = deps.deps_hash();
let hook = ctx_ref.use_hook(|| CmdHookState {
deps_hash: 0,
is_first_render: true,
});
let mut state = hook.get::<CmdHookState>().unwrap_or(CmdHookState {
deps_hash: 0,
is_first_render: true,
});
let old_hash = state.deps_hash;
let is_first = state.is_first_render;
if is_first || old_hash != new_hash {
state.deps_hash = new_hash;
state.is_first_render = false;
hook.set(state);
let cmd = f(deps.output());
ctx_ref.queue_cmd(cmd);
}
}
pub fn use_cmd_once<F>(f: F)
where
F: FnOnce(()) -> Cmd + 'static,
{
use_cmd((), f);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::context::{HookContext, with_hooks};
use std::sync::{Arc, RwLock};
#[test]
fn test_deps_unit() {
let hash1 = ().deps_hash();
let hash2 = ().deps_hash();
assert_eq!(hash1, hash2);
}
#[test]
fn test_deps_single_value() {
let deps = 42i32;
let hash1 = deps.deps_hash();
let hash2 = 42i32.deps_hash();
assert_eq!(hash1, hash2);
let hash3 = 43i32.deps_hash();
assert_ne!(hash1, hash3);
}
#[test]
fn test_deps_tuple() {
let deps = (1, 2);
let hash1 = deps.deps_hash();
let hash2 = (1, 2).deps_hash();
assert_eq!(hash1, hash2);
let hash3 = (1, 3).deps_hash();
assert_ne!(hash1, hash3);
}
#[test]
fn test_deps_tuple_three_items() {
let hash1 = (1, 2, 3).deps_hash();
let hash2 = (1, 2, 3).deps_hash();
let hash3 = (1, 2, 4).deps_hash();
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_deps_tuple_four_items() {
let hash1 = (1, 2, 3, 4).deps_hash();
let hash2 = (1, 2, 3, 4).deps_hash();
let hash3 = (1, 2, 3, 5).deps_hash();
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_deps_vec() {
let deps = vec![1, 2, 3];
let hash1 = deps.deps_hash();
let hash2 = vec![1, 2, 3].deps_hash();
assert_eq!(hash1, hash2);
let hash3 = vec![1, 2, 4].deps_hash();
assert_ne!(hash1, hash3);
}
#[test]
fn test_use_cmd_executes_on_first_render() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let cmd_executed = Arc::new(RwLock::new(false));
{
let flag = Arc::clone(&cmd_executed);
with_hooks(ctx.clone(), move || {
use_cmd((), move |_| {
*flag.write().unwrap() = true;
Cmd::none()
});
});
}
assert!(*cmd_executed.read().unwrap());
assert_eq!(ctx.write().unwrap().take_cmds().len(), 1);
}
#[test]
fn test_use_cmd_executes_on_deps_change() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let execution_count = Arc::new(RwLock::new(0));
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd(1, move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1);
ctx.write().unwrap().take_cmds();
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd(1, move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1);
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd(2, move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 2); }
#[test]
fn test_use_cmd_receives_correct_value() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let received_value = Arc::new(RwLock::new(0));
{
let value = Arc::clone(&received_value);
with_hooks(ctx.clone(), move || {
use_cmd(42, move |val| {
*value.write().unwrap() = val;
Cmd::none()
});
});
}
assert_eq!(*received_value.read().unwrap(), 42);
}
#[test]
fn test_use_cmd_queues_command() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
with_hooks(ctx.clone(), || {
use_cmd((), |_| Cmd::perform(|| async {}));
});
let cmds = ctx.write().unwrap().take_cmds();
assert_eq!(cmds.len(), 1);
assert!(matches!(cmds[0], Cmd::Perform { .. }));
}
#[test]
fn test_use_cmd_once() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let execution_count = Arc::new(RwLock::new(0));
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd_once(move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1);
ctx.write().unwrap().take_cmds();
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd_once(move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1); }
#[test]
fn test_use_cmd_with_tuple_deps() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let execution_count = Arc::new(RwLock::new(0));
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd((1, 2), move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1);
ctx.write().unwrap().take_cmds();
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd((1, 2), move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 1);
{
let count = Arc::clone(&execution_count);
with_hooks(ctx.clone(), move || {
use_cmd((1, 3), move |_| {
*count.write().unwrap() += 1;
Cmd::none()
});
});
}
assert_eq!(*execution_count.read().unwrap(), 2);
}
#[test]
fn test_use_cmd_multiple_in_same_render() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
with_hooks(ctx.clone(), || {
use_cmd(1, |_| Cmd::perform(|| async {}));
use_cmd(2, |_| Cmd::sleep(std::time::Duration::from_secs(1)));
use_cmd(3, |_| Cmd::none());
});
let cmds = ctx.write().unwrap().take_cmds();
assert_eq!(cmds.len(), 3);
}
#[test]
fn test_use_cmd_outside_context_does_not_panic() {
let called = Arc::new(RwLock::new(0usize));
let called_ref = called.clone();
use_cmd((), move |_| {
*called_ref.write().unwrap() += 1;
Cmd::none()
});
assert_eq!(*called.read().unwrap(), 1);
}
}