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}