Skip to main content

telex/
scope.rs

1use std::any::{Any, TypeId};
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::rc::Rc;
5
6use crate::async_state::{Async, AsyncHandle};
7use crate::command::{CommandRegistry, KeyBinding};
8use crate::context::ContextStorage;
9use crate::state::State;
10use crate::stream_state::{StreamHandle, TextStreamHandle};
11
12/// Type alias for effect cleanup functions.
13type CleanupFn = Box<dyn FnOnce()>;
14
15/// Type alias for effect functions that return an optional cleanup.
16type EffectFn = Box<dyn FnOnce() -> Option<CleanupFn>>;
17
18/// State for a single effect hook.
19struct EffectState {
20    /// Cleanup function from the last effect run, if any.
21    cleanup: Option<CleanupFn>,
22    /// Dependencies from last run (boxed for type erasure).
23    last_deps: Option<Box<dyn Any>>,
24    /// Whether effect has ever run.
25    initialized: bool,
26}
27
28/// A pending effect to run after render (index-based).
29struct PendingEffect {
30    /// Index in the effects vec.
31    index: usize,
32    /// The effect function that returns an optional cleanup.
33    effect_fn: EffectFn,
34    /// New dependencies to store after running.
35    new_deps: Option<Box<dyn Any>>,
36}
37
38/// A pending keyed effect to run after render.
39struct PendingKeyedEffect {
40    /// TypeId key for the effect.
41    key: TypeId,
42    /// The effect function that returns an optional cleanup.
43    effect_fn: EffectFn,
44    /// New dependencies to store after running.
45    new_deps: Option<Box<dyn Any>>,
46}
47
48/// Maximum effect executions allowed within a window before we assume infinite loop.
49/// This is generous enough for legitimate use cases but catches runaway effects.
50const MAX_EFFECT_RUNS_PER_WINDOW: usize = 100;
51
52/// Number of frames in the sliding window for effect cycle detection.
53const EFFECT_WINDOW_FRAMES: usize = 10;
54
55/// Storage for component state across re-renders.
56#[derive(Default)]
57pub struct StateStorage {
58    /// Index-based state storage (legacy, for backwards compatibility)
59    states: RefCell<Vec<Rc<dyn Any>>>,
60    index: RefCell<usize>,
61    /// TypeId-keyed state storage (order-independent)
62    keyed_states: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
63    /// Index-based effect storage (legacy)
64    effects: RefCell<Vec<EffectState>>,
65    effect_index: RefCell<usize>,
66    /// TypeId-keyed effect storage (order-independent)
67    keyed_effects: RefCell<HashMap<TypeId, EffectState>>,
68    /// Index-based effects scheduled to run after render
69    pending_effects: RefCell<Vec<PendingEffect>>,
70    /// Keyed effects scheduled to run after render
71    pending_keyed_effects: RefCell<Vec<PendingKeyedEffect>>,
72    /// Rolling count of effect executions for cycle detection
73    effect_run_count: RefCell<usize>,
74    /// Frames since last counter decay
75    frames_since_decay: RefCell<usize>,
76}
77
78impl StateStorage {
79    pub fn new() -> Self {
80        Self {
81            states: RefCell::new(Vec::new()),
82            index: RefCell::new(0),
83            keyed_states: RefCell::new(HashMap::new()),
84            effects: RefCell::new(Vec::new()),
85            effect_index: RefCell::new(0),
86            keyed_effects: RefCell::new(HashMap::new()),
87            pending_effects: RefCell::new(Vec::new()),
88            pending_keyed_effects: RefCell::new(Vec::new()),
89            effect_run_count: RefCell::new(0),
90            frames_since_decay: RefCell::new(0),
91        }
92    }
93
94    /// Reset the hook indices for a new render pass.
95    /// Note: keyed_states don't need resetting - they're accessed by TypeId, not index.
96    pub fn reset_index(&self) {
97        *self.index.borrow_mut() = 0;
98        *self.effect_index.borrow_mut() = 0;
99    }
100
101    /// Get or create state by TypeId key (order-independent).
102    ///
103    /// This is the new API that doesn't require hook ordering rules.
104    /// The type K acts as the key - same K always returns the same state.
105    pub fn use_state_keyed<K: 'static, T: 'static>(&self, init: impl FnOnce() -> T) -> State<T> {
106        let key = TypeId::of::<K>();
107        let mut keyed_states = self.keyed_states.borrow_mut();
108
109        if let Some(any) = keyed_states.get(&key) {
110            // State exists, retrieve it
111            any.downcast_ref::<State<T>>()
112                .expect("State type mismatch for keyed state")
113                .clone()
114        } else {
115            // First access, create new state
116            let state = State::new(init());
117            keyed_states.insert(key, Rc::new(state.clone()));
118            state
119        }
120    }
121
122    /// Get or create state at the current index (legacy API).
123    ///
124    /// IMPORTANT: Hooks using this API must be called in the same order every render.
125    /// Consider using `use_state_keyed` instead for order-independent state.
126    pub fn use_state<T: 'static>(&self, init: impl FnOnce() -> T) -> State<T> {
127        let mut index = self.index.borrow_mut();
128        let mut states = self.states.borrow_mut();
129
130        let state = if *index < states.len() {
131            // State already exists, retrieve it
132            let any = &states[*index];
133            any.downcast_ref::<State<T>>()
134                .expect("State type mismatch - hooks called in different order?")
135                .clone()
136        } else {
137            // First render, create new state
138            let state = State::new(init());
139            states.push(Rc::new(state));
140            states
141                .last()
142                .unwrap()
143                .downcast_ref::<State<T>>()
144                .unwrap()
145                .clone()
146        };
147
148        *index += 1;
149        state
150    }
151
152    /// Get or create async state at the current index.
153    pub fn use_async<T, F>(&self, f: F) -> Async<T>
154    where
155        T: Clone + Send + 'static,
156        F: FnOnce() -> Result<T, String> + Send + 'static,
157    {
158        let mut index = self.index.borrow_mut();
159        let mut states = self.states.borrow_mut();
160
161        let handle = if *index < states.len() {
162            // Async handle already exists, retrieve it
163            let any = &states[*index];
164            any.downcast_ref::<AsyncHandle<T>>()
165                .expect("Async type mismatch - hooks called in different order?")
166                .clone()
167        } else {
168            // First render, create new async handle
169            let handle = AsyncHandle::new();
170            states.push(Rc::new(handle.clone()));
171            handle
172        };
173
174        *index += 1;
175
176        // Start the async operation if not already started
177        handle.start(f);
178
179        // Poll and return current state
180        handle.poll()
181    }
182
183    /// Get or create stream state at the current index.
184    pub fn use_stream<T, F, I>(&self, stream_fn: F) -> StreamHandle<T>
185    where
186        T: Clone + Default + Send + 'static,
187        F: FnOnce() -> I + Send + 'static,
188        I: Iterator<Item = T> + Send + 'static,
189    {
190        let mut index = self.index.borrow_mut();
191        let mut states = self.states.borrow_mut();
192
193        let handle = if *index < states.len() {
194            // Handle already exists, retrieve it
195            let any = &states[*index];
196            any.downcast_ref::<StreamHandle<T>>()
197                .expect("Stream type mismatch - hooks called in different order?")
198                .clone()
199        } else {
200            // First render, create new handle
201            let handle = StreamHandle::new();
202            states.push(Rc::new(handle.clone()));
203            handle
204        };
205
206        *index += 1;
207
208        // Start the stream if not already started
209        handle.start(stream_fn);
210
211        // Poll for updates
212        handle.poll(|acc, item| *acc = item);
213
214        handle
215    }
216
217    /// Get or create text stream state at the current index.
218    /// Automatically concatenates string tokens.
219    pub fn use_text_stream<F, I>(&self, stream_fn: F) -> TextStreamHandle
220    where
221        F: FnOnce() -> I + Send + 'static,
222        I: Iterator<Item = String> + Send + 'static,
223    {
224        self.use_text_stream_with_restart(false, stream_fn)
225    }
226
227    /// Get or create text stream state, with option to restart.
228    ///
229    /// If `restart` is true and a previous stream exists, it will be reset
230    /// before starting the new stream. Use this when you need to start
231    /// a fresh stream (e.g., for a new chat message).
232    pub fn use_text_stream_with_restart<F, I>(
233        &self,
234        restart: bool,
235        stream_fn: F,
236    ) -> TextStreamHandle
237    where
238        F: FnOnce() -> I + Send + 'static,
239        I: Iterator<Item = String> + Send + 'static,
240    {
241        let mut index = self.index.borrow_mut();
242        let mut states = self.states.borrow_mut();
243
244        let handle = if *index < states.len() {
245            let any = &states[*index];
246            any.downcast_ref::<TextStreamHandle>()
247                .expect("TextStream type mismatch - hooks called in different order?")
248                .clone()
249        } else {
250            let handle = TextStreamHandle::new();
251            states.push(Rc::new(handle.clone()));
252            handle
253        };
254
255        *index += 1;
256
257        // Reset if requested (for starting a new stream)
258        if restart {
259            handle.reset();
260        }
261
262        // Start the stream if not already started
263        handle.start(stream_fn);
264
265        // Poll and accumulate text
266        handle.poll_text();
267
268        handle
269    }
270
271    // ========== Effects ==========
272
273    /// Schedule an effect to run after every render.
274    pub fn use_effect<F, C>(&self, effect_fn: F)
275    where
276        F: FnOnce() -> C + 'static,
277        C: FnOnce() + 'static,
278    {
279        let effect_idx = *self.effect_index.borrow();
280        *self.effect_index.borrow_mut() += 1;
281
282        // Always schedule - runs every render
283        self.pending_effects.borrow_mut().push(PendingEffect {
284            index: effect_idx,
285            effect_fn: Box::new(move || {
286                let cleanup = effect_fn();
287                Some(Box::new(cleanup) as Box<dyn FnOnce()>)
288            }),
289            new_deps: None,
290        });
291    }
292
293    /// Schedule an effect to run only once (on first render).
294    pub fn use_effect_once<F, C>(&self, effect_fn: F)
295    where
296        F: FnOnce() -> C + 'static,
297        C: FnOnce() + 'static,
298    {
299        let effect_idx = *self.effect_index.borrow();
300        *self.effect_index.borrow_mut() += 1;
301
302        let effects = self.effects.borrow();
303        let should_run = effect_idx >= effects.len() || !effects[effect_idx].initialized;
304        drop(effects);
305
306        if should_run {
307            self.pending_effects.borrow_mut().push(PendingEffect {
308                index: effect_idx,
309                effect_fn: Box::new(move || {
310                    let cleanup = effect_fn();
311                    Some(Box::new(cleanup) as Box<dyn FnOnce()>)
312                }),
313                new_deps: None,
314            });
315        }
316    }
317
318    /// Schedule an effect to run when dependencies change.
319    pub fn use_effect_with<D, F, C>(&self, deps: D, effect_fn: F)
320    where
321        D: PartialEq + Clone + 'static,
322        F: FnOnce(&D) -> C + 'static,
323        C: FnOnce() + 'static,
324    {
325        let effect_idx = *self.effect_index.borrow();
326        *self.effect_index.borrow_mut() += 1;
327
328        let effects = self.effects.borrow();
329        let should_run = if effect_idx >= effects.len() {
330            // First render, always run
331            true
332        } else {
333            // Compare deps
334            match &effects[effect_idx].last_deps {
335                Some(last_deps) => {
336                    match last_deps.downcast_ref::<D>() {
337                        Some(last) => *last != deps,
338                        None => true, // Type mismatch, re-run
339                    }
340                }
341                None => true,
342            }
343        };
344        drop(effects);
345
346        if should_run {
347            let deps_for_effect = deps.clone();
348            let deps_to_store = deps;
349            self.pending_effects.borrow_mut().push(PendingEffect {
350                index: effect_idx,
351                effect_fn: Box::new(move || {
352                    let cleanup = effect_fn(&deps_for_effect);
353                    Some(Box::new(cleanup) as Box<dyn FnOnce()>)
354                }),
355                new_deps: Some(Box::new(deps_to_store)),
356            });
357        }
358    }
359
360    /// Run all pending effects (called after render).
361    /// Returns true if any effects actually ran (state may have changed).
362    ///
363    /// # Panics
364    /// Panics if effects run more than MAX_EFFECT_RUNS_PER_WINDOW times within
365    /// EFFECT_WINDOW_FRAMES frames, indicating a likely infinite loop.
366    pub fn flush_effects(&self) -> bool {
367        let pending: Vec<_> = self.pending_effects.borrow_mut().drain(..).collect();
368        let ran_any = !pending.is_empty();
369
370        for pending_effect in pending {
371            // Cycle detection: check if we've exceeded the threshold
372            let run_count = {
373                let mut count = self.effect_run_count.borrow_mut();
374                *count += 1;
375                *count
376            };
377
378            if run_count > MAX_EFFECT_RUNS_PER_WINDOW {
379                panic!(
380                    "\n\
381                    ┌─ Telex Effect Cycle Detected ─────────────────────────────────┐\n\
382                    │                                                               │\n\
383                    │  An effect has run {} times in {} frames.             │\n\
384                    │  This usually means an effect is updating state that          │\n\
385                    │  triggers itself to run again (infinite loop).                │\n\
386                    │                                                               │\n\
387                    │  Common causes:                                               │\n\
388                    │    • use_effect updating state without dependencies           │\n\
389                    │    • use_effect_with updating its own dependency              │\n\
390                    │                                                               │\n\
391                    │  Fix: Make sure effects don't write to their own deps.        │\n\
392                    │  Effects should flow outward (to external systems) or         │\n\
393                    │  sideways (to different state), not back to their triggers.   │\n\
394                    │                                                               │\n\
395                    └───────────────────────────────────────────────────────────────┘",
396                    run_count,
397                    EFFECT_WINDOW_FRAMES
398                );
399            }
400
401            let mut effects = self.effects.borrow_mut();
402
403            // Ensure effects vec is large enough
404            while effects.len() <= pending_effect.index {
405                effects.push(EffectState {
406                    cleanup: None,
407                    last_deps: None,
408                    initialized: false,
409                });
410            }
411
412            // Run previous cleanup
413            if let Some(cleanup) = effects[pending_effect.index].cleanup.take() {
414                cleanup();
415            }
416
417            // Drop the borrow before running the effect (effect might access state)
418            drop(effects);
419
420            // Run effect, get cleanup
421            let cleanup = (pending_effect.effect_fn)();
422
423            // Store cleanup and mark initialized
424            let mut effects = self.effects.borrow_mut();
425            effects[pending_effect.index].cleanup = cleanup;
426            effects[pending_effect.index].initialized = true;
427            if let Some(new_deps) = pending_effect.new_deps {
428                effects[pending_effect.index].last_deps = Some(new_deps);
429            }
430        }
431
432        // Process keyed effects
433        let pending_keyed: Vec<_> = self.pending_keyed_effects.borrow_mut().drain(..).collect();
434        let ran_any = ran_any || !pending_keyed.is_empty();
435
436        for pending_effect in pending_keyed {
437            // Cycle detection: check if we've exceeded the threshold
438            let run_count = {
439                let mut count = self.effect_run_count.borrow_mut();
440                *count += 1;
441                *count
442            };
443
444            if run_count > MAX_EFFECT_RUNS_PER_WINDOW {
445                panic!(
446                    "\n\
447                    ┌─ Telex Effect Cycle Detected ─────────────────────────────────┐\n\
448                    │                                                               │\n\
449                    │  An effect has run {} times in {} frames.             │\n\
450                    │  This usually means an effect is updating state that          │\n\
451                    │  triggers itself to run again (infinite loop).                │\n\
452                    │                                                               │\n\
453                    │  Common causes:                                               │\n\
454                    │    • effect! updating state without dependencies              │\n\
455                    │    • effect! updating its own dependency                      │\n\
456                    │                                                               │\n\
457                    │  Fix: Make sure effects don't write to their own deps.        │\n\
458                    │  Effects should flow outward (to external systems) or         │\n\
459                    │  sideways (to different state), not back to their triggers.   │\n\
460                    │                                                               │\n\
461                    └───────────────────────────────────────────────────────────────┘",
462                    run_count,
463                    EFFECT_WINDOW_FRAMES
464                );
465            }
466
467            // Run previous cleanup if this effect existed
468            {
469                let mut keyed_effects = self.keyed_effects.borrow_mut();
470                if let Some(effect_state) = keyed_effects.get_mut(&pending_effect.key) {
471                    if let Some(cleanup) = effect_state.cleanup.take() {
472                        drop(keyed_effects); // Release borrow before running cleanup
473                        cleanup();
474                    }
475                }
476            }
477
478            // Run effect, get cleanup
479            let cleanup = (pending_effect.effect_fn)();
480
481            // Store cleanup and mark initialized
482            let mut keyed_effects = self.keyed_effects.borrow_mut();
483            let effect_state = keyed_effects
484                .entry(pending_effect.key)
485                .or_insert_with(|| EffectState {
486                    cleanup: None,
487                    last_deps: None,
488                    initialized: false,
489                });
490            effect_state.cleanup = cleanup;
491            effect_state.initialized = true;
492            if let Some(new_deps) = pending_effect.new_deps {
493                effect_state.last_deps = Some(new_deps);
494            }
495        }
496
497        ran_any
498    }
499
500    /// Called once per frame to decay the effect run counter.
501    /// This implements a sliding window for cycle detection.
502    pub fn decay_effect_counter(&self) {
503        let mut frames = self.frames_since_decay.borrow_mut();
504        *frames += 1;
505
506        if *frames >= EFFECT_WINDOW_FRAMES {
507            // Reset the window
508            *frames = 0;
509            *self.effect_run_count.borrow_mut() = 0;
510        }
511    }
512
513    // ========== Keyed Effects (order-independent) ==========
514
515    /// Schedule a keyed effect to run only once (on first render).
516    /// Order-independent - safe to use in conditionals.
517    pub fn use_effect_once_keyed<K: 'static, F, C>(&self, effect_fn: F)
518    where
519        F: FnOnce() -> C + 'static,
520        C: FnOnce() + 'static,
521    {
522        let key = TypeId::of::<K>();
523        let keyed_effects = self.keyed_effects.borrow();
524        let should_run = !keyed_effects.contains_key(&key)
525            || !keyed_effects.get(&key).map(|e| e.initialized).unwrap_or(false);
526        drop(keyed_effects);
527
528        if should_run {
529            self.pending_keyed_effects
530                .borrow_mut()
531                .push(PendingKeyedEffect {
532                    key,
533                    effect_fn: Box::new(move || {
534                        let cleanup = effect_fn();
535                        Some(Box::new(cleanup) as Box<dyn FnOnce()>)
536                    }),
537                    new_deps: None,
538                });
539        }
540    }
541
542    /// Schedule a keyed effect to run when dependencies change.
543    /// Order-independent - safe to use in conditionals.
544    pub fn use_effect_keyed<K: 'static, D, F, C>(&self, deps: D, effect_fn: F)
545    where
546        D: PartialEq + Clone + 'static,
547        F: FnOnce(&D) -> C + 'static,
548        C: FnOnce() + 'static,
549    {
550        let key = TypeId::of::<K>();
551        let keyed_effects = self.keyed_effects.borrow();
552        let should_run = match keyed_effects.get(&key) {
553            None => true, // First render, always run
554            Some(effect_state) => {
555                match &effect_state.last_deps {
556                    Some(last_deps) => {
557                        match last_deps.downcast_ref::<D>() {
558                            Some(last) => *last != deps,
559                            None => true, // Type mismatch, re-run
560                        }
561                    }
562                    None => true,
563                }
564            }
565        };
566        drop(keyed_effects);
567
568        if should_run {
569            let deps_for_effect = deps.clone();
570            let deps_to_store = deps;
571            self.pending_keyed_effects
572                .borrow_mut()
573                .push(PendingKeyedEffect {
574                    key,
575                    effect_fn: Box::new(move || {
576                        let cleanup = effect_fn(&deps_for_effect);
577                        Some(Box::new(cleanup) as Box<dyn FnOnce()>)
578                    }),
579                    new_deps: Some(Box::new(deps_to_store)),
580                });
581        }
582    }
583
584    /// Run all cleanup functions (called on app exit).
585    pub fn cleanup_all_effects(&self) {
586        // Clean up index-based effects
587        let mut effects = self.effects.borrow_mut();
588        for effect in effects.iter_mut() {
589            if let Some(cleanup) = effect.cleanup.take() {
590                cleanup();
591            }
592        }
593        drop(effects);
594
595        // Clean up keyed effects
596        let mut keyed_effects = self.keyed_effects.borrow_mut();
597        for effect in keyed_effects.values_mut() {
598            if let Some(cleanup) = effect.cleanup.take() {
599                cleanup();
600            }
601        }
602    }
603}
604
605/// Context passed to components during rendering.
606///
607/// Provides access to hooks like `use_state`.
608#[derive(Clone)]
609pub struct Scope {
610    storage: Rc<StateStorage>,
611    commands: Option<Rc<CommandRegistry>>,
612    context: Rc<ContextStorage>,
613}
614
615impl Scope {
616    /// Create a new scope with fresh state storage.
617    pub fn new() -> Self {
618        Self {
619            storage: Rc::new(StateStorage::new()),
620            commands: None,
621            context: Rc::new(ContextStorage::new()),
622        }
623    }
624
625    /// Create a scope with existing storage (for re-renders).
626    pub fn with_storage(storage: Rc<StateStorage>) -> Self {
627        storage.reset_index();
628        Self {
629            storage,
630            commands: None,
631            context: Rc::new(ContextStorage::new()),
632        }
633    }
634
635    /// Create a scope with existing storage and command registry.
636    pub fn with_storage_and_commands(
637        storage: Rc<StateStorage>,
638        commands: Rc<CommandRegistry>,
639    ) -> Self {
640        storage.reset_index();
641        Self {
642            storage,
643            commands: Some(commands),
644            context: Rc::new(ContextStorage::new()),
645        }
646    }
647
648    /// Create a scope with all dependencies.
649    pub fn with_all(
650        storage: Rc<StateStorage>,
651        commands: Rc<CommandRegistry>,
652        context: Rc<ContextStorage>,
653    ) -> Self {
654        storage.reset_index();
655        Self {
656            storage,
657            commands: Some(commands),
658            context,
659        }
660    }
661
662    /// Get the underlying storage for persistence.
663    pub fn storage(&self) -> Rc<StateStorage> {
664        Rc::clone(&self.storage)
665    }
666
667    /// Create local state that persists across re-renders.
668    ///
669    /// # Example
670    /// ```rust,ignore
671    /// fn Counter(cx: Scope) -> View {
672    ///     let count = cx.use_state(|| 0);
673    ///     // ...
674    /// }
675    /// ```
676    ///
677    /// **Note:** This API requires hooks to be called in the same order every render.
678    /// For order-independent state, use `state!` macro instead.
679    pub fn use_state<T: 'static>(&self, init: impl FnOnce() -> T) -> State<T> {
680        self.storage.use_state(init)
681    }
682
683    /// Create keyed state that persists across re-renders (order-independent).
684    ///
685    /// Unlike `use_state`, this can be called conditionally or in any order.
686    /// The type K acts as the key - same K always returns the same state.
687    ///
688    /// # Example
689    /// ```rust,ignore
690    /// // Define a key type for shared state
691    /// struct CountKey;
692    ///
693    /// fn Counter(cx: Scope) -> View {
694    ///     // Safe to use in conditionals!
695    ///     let count = cx.use_state_keyed::<CountKey, _>(|| 0);
696    ///     // ...
697    /// }
698    /// ```
699    ///
700    /// For independent state, prefer the `state!` macro which auto-generates the key:
701    /// ```rust,ignore
702    /// let count = state!(cx, || 0);
703    /// ```
704    pub fn use_state_keyed<K: 'static, T: 'static>(&self, init: impl FnOnce() -> T) -> State<T> {
705        self.storage.use_state_keyed::<K, T>(init)
706    }
707
708    /// Load async data that persists across re-renders.
709    ///
710    /// The function is called once on first render. The result is cached
711    /// and returned on subsequent renders.
712    ///
713    /// # Example
714    /// ```rust,ignore
715    /// fn DataList(cx: Scope) -> View {
716    ///     let data = cx.use_async(|| {
717    ///         // This runs in a background thread
718    ///         Ok(fetch_data())
719    ///     });
720    ///
721    ///     match data {
722    ///         Async::Loading => view! { <Text>"Loading..."</Text> },
723    ///         Async::Ready(items) => view! { <List items={items} /> },
724    ///         Async::Error(e) => view! { <Text>{format!("Error: {}", e)}</Text> },
725    ///     }
726    /// }
727    /// ```
728    pub fn use_async<T, F>(&self, f: F) -> Async<T>
729    where
730        T: Clone + Send + 'static,
731        F: FnOnce() -> Result<T, String> + Send + 'static,
732    {
733        self.storage.use_async(f)
734    }
735
736    /// Stream data incrementally with automatic accumulation.
737    ///
738    /// Perfect for LLM token streaming or any iterator-based async data.
739    ///
740    /// # Example
741    /// ```rust,ignore
742    /// fn ChatMessage(cx: Scope) -> View {
743    ///     let stream = cx.use_stream(|| {
744    ///         // Returns an iterator that yields items over time
745    ///         vec!["Hello", " ", "world", "!"].into_iter()
746    ///     });
747    ///
748    ///     if stream.is_loading() {
749    ///         view! { <Text>{stream.get()}</Text><Text>"▌"</Text> }
750    ///     } else {
751    ///         view! { <Text>{stream.get()}</Text> }
752    ///     }
753    /// }
754    /// ```
755    pub fn use_stream<T, F, I>(&self, stream_fn: F) -> StreamHandle<T>
756    where
757        T: Clone + Default + Send + 'static,
758        F: FnOnce() -> I + Send + 'static,
759        I: Iterator<Item = T> + Send + 'static,
760    {
761        self.storage.use_stream(stream_fn)
762    }
763
764    /// Stream text with automatic concatenation.
765    ///
766    /// Convenience wrapper for `use_stream` that automatically concatenates
767    /// string tokens. Ideal for LLM streaming responses.
768    ///
769    /// # Example
770    /// ```rust,ignore
771    /// fn StreamingChat(cx: Scope) -> View {
772    ///     let response = cx.use_text_stream(|| {
773    ///         // Simulate LLM token stream
774    ///         llm_client.stream_completion("Hello!")
775    ///     });
776    ///
777    ///     let cursor = if response.is_loading() { "▌" } else { "" };
778    ///     view! { <Text>{format!("{}{}", response.get(), cursor)}</Text> }
779    /// }
780    /// ```
781    pub fn use_text_stream<F, I>(&self, stream_fn: F) -> TextStreamHandle
782    where
783        F: FnOnce() -> I + Send + 'static,
784        I: Iterator<Item = String> + Send + 'static,
785    {
786        self.storage.use_text_stream(stream_fn)
787    }
788
789    /// Stream text with automatic concatenation and restart support.
790    ///
791    /// Like `use_text_stream`, but allows forcing a restart when `restart` is true.
792    /// Use this when you need to start a fresh stream for each new request.
793    ///
794    /// # Example
795    /// ```rust,ignore
796    /// fn Chat(cx: Scope) -> View {
797    ///     let request_id = cx.use_state(|| 0u32);
798    ///     let last_id = cx.use_state(|| 0u32);
799    ///
800    ///     let needs_restart = request_id.get() != last_id.get();
801    ///     let stream = cx.use_text_stream_with_restart(needs_restart, || {
802    ///         stream_response()
803    ///     });
804    ///
805    ///     if needs_restart {
806    ///         last_id.set(request_id.get());
807    ///     }
808    ///     // ...
809    /// }
810    /// ```
811    pub fn use_text_stream_with_restart<F, I>(
812        &self,
813        restart: bool,
814        stream_fn: F,
815    ) -> TextStreamHandle
816    where
817        F: FnOnce() -> I + Send + 'static,
818        I: Iterator<Item = String> + Send + 'static,
819    {
820        self.storage
821            .use_text_stream_with_restart(restart, stream_fn)
822    }
823
824    /// Register a keyboard command/shortcut.
825    ///
826    /// The callback will be invoked when the key combination is pressed.
827    /// Commands registered later in the render tree take precedence.
828    ///
829    /// # Example
830    /// ```rust,ignore
831    /// fn App(cx: Scope) -> View {
832    ///     let count = cx.use_state(|| 0);
833    ///     let c = count.clone();
834    ///
835    ///     // Ctrl+R to reset counter
836    ///     cx.use_command(KeyBinding::ctrl('r'), move || {
837    ///         c.set(0);
838    ///     });
839    ///
840    ///     view! { <Text>{format!("Count: {}", count.get())}</Text> }
841    /// }
842    /// ```
843    pub fn use_command<F>(&self, binding: KeyBinding, callback: F)
844    where
845        F: Fn() + 'static,
846    {
847        if let Some(ref commands) = self.commands {
848            commands.register(binding, Rc::new(callback));
849        }
850    }
851
852    /// Provide a value in the context for child components to access.
853    ///
854    /// Values are stored by type, so each type can only have one value.
855    /// Providing a value of a type that already exists will replace it.
856    ///
857    /// # Example
858    /// ```rust,ignore
859    /// #[derive(Clone)]
860    /// struct UserState {
861    ///     name: String,
862    ///     logged_in: bool,
863    /// }
864    ///
865    /// fn App(cx: Scope) -> View {
866    ///     // Provide user state for all children
867    ///     cx.provide_context(UserState {
868    ///         name: "Alice".to_string(),
869    ///         logged_in: true,
870    ///     });
871    ///
872    ///     view! { <Header /> }
873    /// }
874    /// ```
875    pub fn provide_context<T: Clone + 'static>(&self, value: T) {
876        self.context.provide(value);
877    }
878
879    /// Get a value from the context.
880    ///
881    /// Returns None if no value of this type has been provided by a parent.
882    ///
883    /// # Example
884    /// ```rust,ignore
885    /// fn Header(cx: Scope) -> View {
886    ///     let user = cx.use_context::<UserState>();
887    ///
888    ///     match user {
889    ///         Some(u) => view! { <Text>{format!("Hello, {}", u.name)}</Text> },
890    ///         None => view! { <Text>"Not logged in"</Text> },
891    ///     }
892    /// }
893    /// ```
894    pub fn use_context<T: Clone + 'static>(&self) -> Option<T> {
895        self.context.get::<T>()
896    }
897
898    /// Get the context storage (for passing to child scopes).
899    pub fn context(&self) -> Rc<ContextStorage> {
900        Rc::clone(&self.context)
901    }
902
903    // ========== Effects ==========
904    //
905    // Experimental API - newly implemented, may have edge cases or API changes.
906
907    /// Run a side effect after every render.
908    ///
909    /// The effect function is called after each render completes.
910    /// Return a cleanup function that will be called before the next effect runs.
911    ///
912    /// # Example
913    /// ```rust,ignore
914    /// fn Logger(cx: Scope) -> View {
915    ///     let count = cx.use_state(|| 0);
916    ///
917    ///     cx.use_effect(|| {
918    ///         println!("Rendered with count: {}", count.get());
919    ///         || {} // cleanup (runs before next effect)
920    ///     });
921    ///
922    ///     // ...
923    /// }
924    /// ```
925    ///
926    /// **Warning:** Be careful not to create infinite loops by updating state
927    /// in an effect that runs every render.
928    pub fn use_effect<F, C>(&self, effect_fn: F)
929    where
930        F: FnOnce() -> C + 'static,
931        C: FnOnce() + 'static,
932    {
933        self.storage.use_effect(effect_fn)
934    }
935
936    /// Run a side effect only once (on first render).
937    ///
938    /// The effect function is called only on the first render.
939    /// The cleanup function is called on app exit.
940    ///
941    /// # Example
942    /// ```rust,ignore
943    /// fn App(cx: Scope) -> View {
944    ///     cx.use_effect_once(|| {
945    ///         println!("App initialized");
946    ///         || {
947    ///             println!("App cleanup");
948    ///         }
949    ///     });
950    ///
951    ///     // ...
952    /// }
953    /// ```
954    pub fn use_effect_once<F, C>(&self, effect_fn: F)
955    where
956        F: FnOnce() -> C + 'static,
957        C: FnOnce() + 'static,
958    {
959        self.storage.use_effect_once(effect_fn)
960    }
961
962    /// Run a side effect when dependencies change.
963    ///
964    /// The effect function is called on first render and whenever the
965    /// dependencies change (compared via `PartialEq`).
966    ///
967    /// # Example
968    /// ```rust,ignore
969    /// fn Counter(cx: Scope) -> View {
970    ///     let count = cx.use_state(|| 0);
971    ///
972    ///     cx.use_effect_with(count.get(), |count| {
973    ///         println!("Count changed to: {}", count);
974    ///         || {} // cleanup
975    ///     });
976    ///
977    ///     // ...
978    /// }
979    /// ```
980    ///
981    /// Multiple dependencies can be passed as a tuple:
982    /// ```rust,ignore
983    /// cx.use_effect_with((a.get(), b.get()), |(a, b)| {
984    ///     println!("a={}, b={}", a, b);
985    ///     || {}
986    /// });
987    /// ```
988    pub fn use_effect_with<D, F, C>(&self, deps: D, effect_fn: F)
989    where
990        D: PartialEq + Clone + 'static,
991        F: FnOnce(&D) -> C + 'static,
992        C: FnOnce() + 'static,
993    {
994        self.storage.use_effect_with(deps, effect_fn)
995    }
996
997    // ========== Keyed Effects (order-independent) ==========
998    //
999    // These are the recommended effect APIs. Unlike index-based effects,
1000    // keyed effects can be used conditionally or in any order.
1001    // Use the effect!() and effect_once!() macros for convenient access.
1002
1003    /// Run a keyed side effect only once (on first render).
1004    /// Order-independent - safe to use in conditionals.
1005    ///
1006    /// Prefer the `effect_once!` macro which auto-generates the key:
1007    /// ```rust,ignore
1008    /// effect_once!(cx, || {
1009    ///     println!("initialized");
1010    ///     || { println!("cleanup"); }
1011    /// });
1012    /// ```
1013    pub fn use_effect_once_keyed<K: 'static, F, C>(&self, effect_fn: F)
1014    where
1015        F: FnOnce() -> C + 'static,
1016        C: FnOnce() + 'static,
1017    {
1018        self.storage.use_effect_once_keyed::<K, F, C>(effect_fn)
1019    }
1020
1021    /// Run a keyed side effect when dependencies change.
1022    /// Order-independent - safe to use in conditionals.
1023    ///
1024    /// Prefer the `effect!` macro which auto-generates the key:
1025    /// ```rust,ignore
1026    /// effect!(cx, count.get(), |&c| {
1027    ///     println!("count changed to {}", c);
1028    ///     || {}  // cleanup
1029    /// });
1030    /// ```
1031    pub fn use_effect_keyed<K: 'static, D, F, C>(&self, deps: D, effect_fn: F)
1032    where
1033        D: PartialEq + Clone + 'static,
1034        F: FnOnce(&D) -> C + 'static,
1035        C: FnOnce() + 'static,
1036    {
1037        self.storage.use_effect_keyed::<K, D, F, C>(deps, effect_fn)
1038    }
1039
1040    /// Create or get a terminal handle.
1041    ///
1042    /// This uses keyed state internally so it's safe to use in conditionals.
1043    /// Each call site gets its own terminal handle based on the call location.
1044    ///
1045    /// # Example
1046    /// ```rust,ignore
1047    /// let terminal = cx.use_terminal();
1048    /// if !terminal.is_started() {
1049    ///     terminal.spawn("bash", &[], 80, 24);
1050    /// }
1051    /// terminal.poll();
1052    /// View::terminal().handle(terminal).build()
1053    /// ```
1054    #[track_caller]
1055    pub fn use_terminal(&self) -> crate::terminal_state::TerminalHandle {
1056        // For simplicity in MVP, just use indexed state
1057        // This requires maintaining call order like other use_* hooks
1058        let mut index = self.storage.index.borrow_mut();
1059        let mut states = self.storage.states.borrow_mut();
1060
1061        if *index < states.len() {
1062            let any = &states[*index];
1063            *index += 1;
1064            any.downcast_ref::<crate::terminal_state::TerminalHandle>()
1065                .expect("TerminalHandle type mismatch - hooks called in different order?")
1066                .clone()
1067        } else {
1068            let handle = crate::terminal_state::TerminalHandle::new(24, 80);
1069            states.push(Rc::new(handle.clone()));
1070            *index += 1;
1071            handle
1072        }
1073    }
1074}
1075
1076impl Default for Scope {
1077    fn default() -> Self {
1078        Self::new()
1079    }
1080}