Skip to main content

ansiq_core/
hooks.rs

1use std::{
2    any::{Any, TypeId},
3    cell::RefCell,
4    rc::Rc,
5};
6
7use crate::reactivity::dispose_computed_scope;
8use crate::{Computed, EffectHandle, HistoryBlock, HistoryEntry, Signal, computed, signal};
9
10pub enum RuntimeRequest<Message> {
11    EmitMessage(Message),
12    CommitHistory(HistoryEntry),
13    SetFocusScope(Option<String>),
14    Quit,
15}
16
17#[derive(Default)]
18pub struct HookStore {
19    // Component-local signals, computeds, and effects are all stored as hook
20    // slots so they keep stable identities across rerenders.
21    slots: Vec<Box<dyn HookSlot>>,
22    cursor: usize,
23}
24
25impl HookStore {
26    pub fn begin_render(&mut self) {
27        self.cursor = 0;
28    }
29
30    pub fn signal<T, F>(&mut self, init: F) -> Signal<T>
31    where
32        T: Clone + 'static,
33        F: FnOnce() -> T,
34    {
35        let slot = self.cursor;
36        self.cursor += 1;
37
38        if slot == self.slots.len() {
39            self.slots.push(Box::new(StateSlot {
40                signal: signal(init()),
41            }));
42        }
43
44        assert!(
45            matches!(
46                self.slots[slot].kind(),
47                HookSlotKind::State(type_id) if type_id == TypeId::of::<T>()
48            ),
49            "hook type mismatch"
50        );
51
52        *self.slots[slot]
53            .clone_state_signal()
54            .expect("state hooks should expose a signal handle")
55            .downcast::<Signal<T>>()
56            .expect("hook type mismatch")
57    }
58
59    pub fn computed<T, F>(&mut self, compute: F) -> Computed<T>
60    where
61        T: Clone + 'static,
62        F: Fn() -> T + 'static,
63    {
64        let slot = self.cursor;
65        self.cursor += 1;
66
67        if slot == self.slots.len() {
68            self.slots.push(Box::new(ComputedSlot {
69                computed: computed(compute),
70            }));
71        }
72
73        assert!(
74            matches!(
75                self.slots[slot].kind(),
76                HookSlotKind::Computed(type_id) if type_id == TypeId::of::<T>()
77            ),
78            "hook type mismatch"
79        );
80
81        *self.slots[slot]
82            .clone_boxed_value()
83            .downcast::<Computed<T>>()
84            .expect("hook type mismatch")
85    }
86
87    pub fn effect<F>(&mut self, effect: F)
88    where
89        F: FnMut() + 'static,
90    {
91        let slot = self.cursor;
92        self.cursor += 1;
93
94        if slot == self.slots.len() {
95            self.slots.push(Box::new(EffectSlot::new(effect)));
96            return;
97        }
98
99        assert!(
100            matches!(self.slots[slot].kind(), HookSlotKind::Effect),
101            "hook type mismatch"
102        );
103    }
104
105    pub fn finish_render(&mut self) {
106        // Hooks beyond the last slot used this render have effectively been
107        // unmounted, so tear them down before dropping the slots.
108        while self.slots.len() > self.cursor {
109            let mut slot = self.slots.pop().expect("slot should exist");
110            slot.teardown();
111        }
112    }
113}
114
115trait HookSlot {
116    fn kind(&self) -> HookSlotKind;
117    fn clone_boxed_value(&self) -> Box<dyn Any>;
118    fn clone_state_signal(&self) -> Option<Box<dyn Any>> {
119        None
120    }
121    fn teardown(&mut self) {}
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum HookSlotKind {
126    State(TypeId),
127    Computed(TypeId),
128    Effect,
129}
130
131struct StateSlot<T> {
132    signal: Signal<T>,
133}
134
135impl<T: Clone + 'static> HookSlot for StateSlot<T> {
136    fn kind(&self) -> HookSlotKind {
137        HookSlotKind::State(TypeId::of::<T>())
138    }
139
140    fn clone_boxed_value(&self) -> Box<dyn Any> {
141        Box::new(self.signal.get())
142    }
143
144    fn clone_state_signal(&self) -> Option<Box<dyn Any>> {
145        Some(Box::new(self.signal.clone()))
146    }
147}
148
149struct ComputedSlot<T> {
150    computed: Computed<T>,
151}
152
153impl<T: Clone + 'static> HookSlot for ComputedSlot<T> {
154    fn kind(&self) -> HookSlotKind {
155        HookSlotKind::Computed(TypeId::of::<T>())
156    }
157
158    fn clone_boxed_value(&self) -> Box<dyn Any> {
159        Box::new(self.computed.clone())
160    }
161
162    fn teardown(&mut self) {
163        dispose_computed_scope(self.computed.scope_id());
164    }
165}
166
167struct EffectSlot {
168    handle: EffectHandle,
169}
170
171impl EffectSlot {
172    fn new<F>(callback_fn: F) -> Self
173    where
174        F: FnMut() + 'static,
175    {
176        let callback = Rc::new(RefCell::new(Box::new(callback_fn) as Box<dyn FnMut()>));
177        Self {
178            handle: crate::effect(move || (callback.borrow_mut())()),
179        }
180    }
181}
182
183impl HookSlot for EffectSlot {
184    fn kind(&self) -> HookSlotKind {
185        HookSlotKind::Effect
186    }
187
188    fn clone_boxed_value(&self) -> Box<dyn Any> {
189        panic!("effect slots do not expose values")
190    }
191
192    fn teardown(&mut self) {
193        self.handle.stop();
194    }
195}
196
197impl Drop for HookStore {
198    fn drop(&mut self) {
199        for slot in &mut self.slots {
200            slot.teardown();
201        }
202    }
203}
204
205pub struct ViewCtx<'a, Message> {
206    hooks: &'a mut HookStore,
207    marker: std::marker::PhantomData<Message>,
208}
209
210pub type Cx<'a, Message> = ViewCtx<'a, Message>;
211
212impl<'a, Message: Send + 'static> ViewCtx<'a, Message> {
213    pub fn new(hooks: &'a mut HookStore) -> Self {
214        Self {
215            hooks,
216            marker: std::marker::PhantomData,
217        }
218    }
219
220    pub fn signal<T, F>(&mut self, init: F) -> Signal<T>
221    where
222        T: Clone + 'static,
223        F: FnOnce() -> T,
224    {
225        self.hooks.signal(init)
226    }
227
228    pub fn effect<F>(&mut self, effect: F)
229    where
230        F: FnMut() + 'static,
231    {
232        // Install the effect once per hook slot. This avoids duplicate
233        // subscriptions while the component rerenders around the same stable
234        // reactive sources.
235        self.hooks.effect(effect);
236    }
237
238    pub fn computed<T, F>(&mut self, compute: F) -> Computed<T>
239    where
240        T: Clone + 'static,
241        F: Fn() -> T + 'static,
242    {
243        self.hooks.computed(compute)
244    }
245}
246
247impl<Message> RuntimeRequest<Message> {
248    pub fn commit_text(content: String) -> Self {
249        Self::CommitHistory(HistoryEntry::Text(content))
250    }
251
252    pub fn commit_block(block: HistoryBlock) -> Self {
253        Self::CommitHistory(HistoryEntry::Block(block))
254    }
255}