use super::instance::HookSlot;
use super::runtime::{InputHandlerId, RuntimeHandle, TimelineId};
use super::scope::Scope;
use super::signal::Signal;
use crate::input::Key;
use crate::timeline::{Animatable, Timeline, TimelineDebugInfo};
use std::marker::PhantomData;
pub fn use_state<T, F>(cx: Scope, init: F) -> Signal<T>
where
T: Clone + 'static,
F: FnOnce() -> T,
{
let rt = cx.rt.clone();
let component_id = cx.component_id;
let cursor = rt
.with_instance_mut(component_id, |instance| instance.advance_cursor())
.expect("Component instance not found");
let existing = rt.with_instance(component_id, |instance| instance.get_hook(cursor).cloned());
match existing {
Some(Some(HookSlot::State(id))) => {
Signal {
id,
rt,
_marker: PhantomData,
}
}
Some(Some(other)) => {
panic!(
"Hook order changed: expected State hook at position {}, found {:?}. \
Hooks must be called unconditionally and in the same order every render.",
cursor, other
);
}
Some(None) | None => {
let value = init();
let signal_id = rt.create_signal(value);
rt.with_instance_mut(component_id, |instance| {
instance.push_hook(HookSlot::State(signal_id));
});
Signal {
id: signal_id,
rt,
_marker: PhantomData,
}
}
}
}
pub fn use_input<F>(cx: Scope, handler: F)
where
F: Fn(&Key) + 'static,
{
let rt = cx.rt.clone();
let component_id = cx.component_id;
let cursor = rt
.with_instance_mut(component_id, |instance| instance.advance_cursor())
.expect("Component instance not found");
let existing = rt.with_instance(component_id, |instance| instance.get_hook(cursor).cloned());
match existing {
Some(Some(HookSlot::Input(id))) => {
if !rt.has_input_handler(id) {
panic!("Input handler was unexpectedly removed");
}
}
Some(Some(other)) => {
panic!(
"Hook order changed: expected Input hook at position {}, found {:?}. \
Hooks must be called unconditionally and in the same order every render.",
cursor, other
);
}
Some(None) | None => {
let handler_id: InputHandlerId = rt.register_input_handler(handler);
rt.with_instance_mut(component_id, |instance| {
instance.push_hook(HookSlot::Input(handler_id));
});
}
}
}
pub fn use_timeline(cx: Scope, timeline: Timeline) -> TimelineHandle {
let rt = cx.rt.clone();
let component_id = cx.component_id;
let cursor = rt
.with_instance_mut(component_id, |instance| instance.advance_cursor())
.expect("Component instance not found");
let existing = rt.with_instance(component_id, |instance| instance.get_hook(cursor).cloned());
match existing {
Some(Some(HookSlot::Timeline(id))) => {
if !rt.has_timeline(id) {
panic!("Timeline was unexpectedly removed");
}
TimelineHandle { id, rt }
}
Some(Some(other)) => {
panic!(
"Hook order changed: expected Timeline hook at position {}, found {:?}. \
Hooks must be called unconditionally and in the same order every render.",
cursor, other
);
}
Some(None) | None => {
let playing = timeline.start();
let timeline_id = rt.create_timeline(playing);
rt.with_instance_mut(component_id, |instance| {
instance.push_hook(HookSlot::Timeline(timeline_id));
});
TimelineHandle {
id: timeline_id,
rt,
}
}
}
}
#[derive(Clone)]
pub struct TimelineHandle {
id: TimelineId,
rt: RuntimeHandle,
}
impl TimelineHandle {
pub fn id(&self) -> TimelineId {
self.id
}
pub fn get<T: Animatable + Clone>(&self, property: &str) -> Option<T> {
self.rt.with_timeline(self.id, |tl| tl.get::<T>(property))?
}
pub fn get_or<T: Animatable + Clone>(&self, property: &str, default: T) -> T {
self.get(property).unwrap_or(default)
}
pub fn elapsed(&self) -> f64 {
self.rt
.with_timeline(self.id, |tl| tl.elapsed())
.unwrap_or(0.0)
}
pub fn current_act(&self) -> String {
self.rt
.with_timeline(self.id, |tl| tl.current_act())
.unwrap_or_default()
}
pub fn act_progress(&self) -> f64 {
self.rt
.with_timeline(self.id, |tl| tl.act_progress())
.unwrap_or(0.0)
}
pub fn progress(&self) -> f64 {
self.rt
.with_timeline(self.id, |tl| tl.progress())
.unwrap_or(0.0)
}
pub fn duration(&self) -> f64 {
self.rt
.with_timeline(self.id, |tl| tl.duration())
.unwrap_or(0.0)
}
pub fn is_paused(&self) -> bool {
self.rt
.with_timeline(self.id, |tl| tl.is_paused())
.unwrap_or(false)
}
pub fn is_playing(&self) -> bool {
!self.is_paused()
}
pub fn pause(&self) {
self.rt.with_timeline_mut(self.id, |tl| tl.pause());
}
pub fn play(&self) {
self.rt.with_timeline_mut(self.id, |tl| tl.play());
}
pub fn toggle_pause(&self) {
self.rt.with_timeline_mut(self.id, |tl| tl.toggle_pause());
}
pub fn seek(&self, time: f64) {
self.rt.with_timeline_mut(self.id, |tl| tl.seek(time));
}
pub fn restart(&self) {
self.rt.with_timeline_mut(self.id, |tl| tl.restart());
}
pub fn set_speed(&self, speed: f64) {
self.rt.with_timeline_mut(self.id, |tl| tl.set_speed(speed));
}
pub fn speed(&self) -> f64 {
self.rt
.with_timeline(self.id, |tl| tl.speed())
.unwrap_or(1.0)
}
pub fn loop_count(&self) -> u32 {
self.rt
.with_timeline(self.id, |tl| tl.loop_count())
.unwrap_or(0)
}
pub fn update(&self) -> bool {
self.rt
.with_timeline_mut(self.id, |tl| tl.update())
.unwrap_or(false)
}
pub fn get_stagger<T: Animatable + Clone>(&self, property: &str, index: usize) -> Option<T> {
self.rt
.with_timeline(self.id, |tl| tl.get_stagger::<T>(property, index))?
}
pub fn get_stagger_or<T: Animatable + Clone>(
&self,
property: &str,
index: usize,
default: T,
) -> T {
self.get_stagger(property, index).unwrap_or(default)
}
pub fn get_stagger_all<T: Animatable + Clone>(&self, property: &str, default: T) -> Vec<T> {
self.rt
.with_timeline(self.id, |tl| tl.get_stagger_all::<T>(property, default))
.unwrap_or_default()
}
pub fn stagger_count(&self, property: &str) -> usize {
self.rt
.with_timeline(self.id, |tl| tl.stagger_count(property))
.unwrap_or(0)
}
pub fn debug_info(&self) -> Option<TimelineDebugInfo> {
self.rt.with_timeline(self.id, |tl| tl.debug_info())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::runtime::RuntimeHandle;
use std::cell::Cell;
use std::rc::Rc;
fn setup_scope() -> (RuntimeHandle, Scope) {
let rt = RuntimeHandle::new();
let component_id = rt.create_instance();
rt.set_current_instance(Some(component_id));
let scope = Scope::new(rt.clone(), component_id);
(rt, scope)
}
#[test]
fn test_use_state_initial() {
let (_rt, cx) = setup_scope();
let signal = use_state(cx, || 42);
assert_eq!(signal.get(), 42);
}
#[test]
fn test_use_state_reuses_on_rerender() {
let (rt, cx) = setup_scope();
let signal1 = use_state(cx.clone(), || 0);
signal1.set(100);
rt.reset_hook_cursor(cx.component_id);
let signal2 = use_state(cx, || 999); assert_eq!(signal2.get(), 100); assert_eq!(signal1.id(), signal2.id()); }
#[test]
fn test_use_state_multiple() {
let (rt, cx) = setup_scope();
let a = use_state(cx.clone(), || 1);
let b = use_state(cx.clone(), || 2);
let c = use_state(cx.clone(), || 3);
assert_eq!(a.get(), 1);
assert_eq!(b.get(), 2);
assert_eq!(c.get(), 3);
b.set(20);
rt.reset_hook_cursor(cx.component_id);
let a2 = use_state(cx.clone(), || 0);
let b2 = use_state(cx.clone(), || 0);
let c2 = use_state(cx, || 0);
assert_eq!(a2.get(), 1);
assert_eq!(b2.get(), 20);
assert_eq!(c2.get(), 3);
}
#[test]
#[should_panic(expected = "Hook order changed")]
fn test_use_state_wrong_order_panics() {
let (rt, cx) = setup_scope();
let _ = use_state(cx.clone(), || 0);
rt.reset_hook_cursor(cx.component_id);
use_input(cx, |_| {});
}
#[test]
fn test_use_input_registers_once() {
let (rt, cx) = setup_scope();
let call_count = Rc::new(Cell::new(0));
let count = call_count.clone();
use_input(cx.clone(), move |_| {
count.set(count.get() + 1);
});
let key = Key::new(crossterm::event::KeyCode::Char('a'));
rt.dispatch_input(&key);
assert_eq!(call_count.get(), 1);
rt.reset_hook_cursor(cx.component_id);
let count2 = call_count.clone();
use_input(cx, move |_| {
count2.set(count2.get() + 100);
});
rt.dispatch_input(&key);
assert_eq!(call_count.get(), 2); }
#[test]
fn test_use_input_receives_key() {
let (_rt, cx) = setup_scope();
let received = Rc::new(Cell::new(false));
let r = received.clone();
use_input(cx, move |key| {
if key.is_char('x') {
r.set(true);
}
});
let key = Key::new(crossterm::event::KeyCode::Char('x'));
_rt.dispatch_input(&key);
assert!(received.get());
}
#[test]
fn test_use_input_with_signal() {
let (rt, cx) = setup_scope();
let count = use_state(cx.clone(), || 0);
let count_for_handler = count.clone();
use_input(cx, move |key| {
if key.is_char(' ') {
count_for_handler.set(count_for_handler.get() + 1);
}
});
assert_eq!(count.get(), 0);
let space = Key::new(crossterm::event::KeyCode::Char(' '));
rt.dispatch_input(&space);
assert_eq!(count.get(), 1);
rt.dispatch_input(&space);
assert_eq!(count.get(), 2);
let other = Key::new(crossterm::event::KeyCode::Char('a'));
rt.dispatch_input(&other);
assert_eq!(count.get(), 2); }
#[test]
#[should_panic(expected = "Hook order changed")]
fn test_use_input_wrong_order_panics() {
let (rt, cx) = setup_scope();
use_input(cx.clone(), |_| {});
rt.reset_hook_cursor(cx.component_id);
let _ = use_state(cx, || 0i32);
}
}